React Native Expo 开发从入门到精通教程

🚀 React Native Expo 开发从入门到精通教程

欢迎来到React Native Expo开发完整教程!

本教程将带您从零基础开始,通过4个精心设计的实战案例,逐步掌握React Native Expo开发的精髓。无论您是前端开发者、移动端开发者,还是完全的编程新手,都能在这里找到适合的学习路径。

📚 教程目录

📋 第一部分:环境搭建与基础入门

1.1 开发环境搭建

⚠️ 系统要求:

步骤1:安装Node.js

访问 nodejs.org 下载并安装最新LTS版本。

# 验证安装
node --version
npm --version

步骤2:安装Expo CLI

npm install -g @expo/cli

步骤3:安装Expo Go应用

📱 Android设备

在Google Play商店搜索"Expo Go"并安装

🍎 iOS设备

在App Store搜索"Expo Go"并安装

1.2 创建第一个Expo项目

# 创建新项目,选择一个空白模板
npx create-expo-app my-first-app
cd my-first-app

# 启动开发服务器
npx expo start
💡 提示:

启动后,您会看到一个二维码。使用Expo Go应用扫描二维码,即可在真机上预览应用效果。

1.3 JavaScript基础回顾

ES6+ 核心语法

  • 箭头函数
  • 解构赋值
  • 模板字符串
  • 导入/导出模块
  • async/await

React 基础概念

  • 函数组件
  • JSX语法
  • Props和State
  • Hooks机制
  • 事件处理

⚛️ 第二部分:React Native Expo核心基础

2.1 基础组件

📦 View 容器组件

用于包装其他组件,支持布局和样式

import { View, Text } from 'react-native';

<View style={{ padding: 20, backgroundColor: 'lightblue' }}>
  <Text>Hello World!</Text>
</View>

📝 Text 文本组件

显示文本内容,支持嵌套样式

import { Text } from 'react-native';

<Text style={{ fontSize: 18, color: 'darkblue' }}>
  欢迎学习 <Text style={{ fontWeight: 'bold' }}>React Native</Text>
</Text>

2.2 状态管理

🎯 useState Hook:

在函数组件中添加状态管理的核心工具

import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';

export default function Counter() {
  const [count, setCount] = useState(0); // 定义状态变量 count 及更新函数 setCount

  return (
    <View style={{ padding: 20 }}>
      <Text>当前计数:{count}</Text>
      <Button 
        title="增加" 
        onPress={() => setCount(count + 1)} // 点击按钮更新 count
      />
    </View>
  );
}

2.3 样式系统

推荐使用 StyleSheet.create 创建样式对象,提高性能和可读性。

import { StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f0f0f0'
  },
  text: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333'
  },
  button: {
    backgroundColor: '#007bff',
    padding: 15,
    borderRadius: 5,
    marginTop: 10
  }
});

💼 第三部分:实战案例开发

🎯 学习建议:

以下4个案例难度递增,建议按顺序完成。每个案例都包含完整的项目开发流程,从需求分析到最终发布。

🎮 案例1:计数器应用(入门级)

📋 项目需求

🏗️ 项目结构

counter-app/
├── App.js              # 主应用组件,包含计数器逻辑和UI
└── package.json

📱 核心代码实现 (App.js)

import React, { useState } from 'react';
import {
  SafeAreaView, // 确保内容在安全区域内显示
  StyleSheet,
  Text,
  View,
  TouchableOpacity, // 用于创建自定义按钮
  Alert // 用于弹出确认框
} from 'react-native';

export default function App() {
  const [count, setCount] = useState(0); // 初始化计数器状态为0

  // 增加计数
  const increment = () => setCount(prev => prev + 1);
  // 减少计数,确保不小于0
  const decrement = () => setCount(prev => (prev > 0 ? prev - 1 : 0));
  
  // 重置计数器,带有确认弹窗
  const reset = () => {
    Alert.alert(
      '重置确认',
      '确定要重置计数器吗?',
      [
        { text: '取消', style: 'cancel' },
        { text: '确定', onPress: () => setCount(0) }
      ]
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>计数器应用</Text>
        
        <View style={styles.counterDisplay}>
          <Text style={styles.countText}>{count}</Text>
        </View>

        <View style={styles.buttonContainer}>
          <TouchableOpacity style={[styles.button, styles.decrementButton]} onPress={decrement}>
            <Text style={styles.buttonText}>-</Text>
          </TouchableOpacity>
          
          <TouchableOpacity style={[styles.button, styles.resetButton]} onPress={reset}>
            <Text style={styles.buttonText}>重置</Text>
          </TouchableOpacity>
          
          <TouchableOpacity style={[styles.button, styles.incrementButton]} onPress={increment}>
            <Text style={styles.buttonText}>+</Text>
          </TouchableOpacity>
        </View>

        <Text style={styles.subtitle}>使用 Expo 开发</Text>
      </View>
    </SafeAreaView>
  );
}

// 样式定义
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#2c3e50',
    marginBottom: 30,
    textAlign: 'center',
  },
  counterDisplay: {
    backgroundColor: 'white',
    borderRadius: 15,
    padding: 30,
    marginBottom: 30,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  countText: {
    fontSize: 48,
    fontWeight: 'bold',
    color: '#3498db',
    textAlign: 'center',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '80%',
    marginBottom: 20,
  },
  button: {
    padding: 20,
    borderRadius: 10,
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: 60,
  },
  incrementButton: {
    backgroundColor: '#27ae60', // 绿色
  },
  decrementButton: {
    backgroundColor: '#e74c3c', // 红色
  },
  resetButton: {
    backgroundColor: '#f39c12', // 黄色
    paddingHorizontal: 20,
  },
  buttonText: {
    color: 'white',
    fontSize: 24,
    fontWeight: 'bold',
  },
  subtitle: {
    fontSize: 16,
    color: '#7f8c8d',
    textAlign: 'center',
  },
});

🚀 扩展功能建议

  • ✅ 添加历史记录功能
  • ✅ 实现减法限制(不能小于0)
  • ✅ 添加动画效果
  • ✅ 支持长按连续操作

✅ 案例2:待办事项应用(进阶级)

📋 项目需求

🏗️ 项目结构

todo-app/
├── App.js                    # 主应用入口,可能包含导航
├── components/               # 可复用的UI组件
│   ├── TodoInput.js          # 输入待办事项组件
│   ├── TodoItem.js           # 单个待办事项组件
│   └── FilterButtons.js      # 筛选按钮组件
├── screens/                  # 屏幕/页面组件
│   └── TodoScreen.js         # 待办事项主屏幕
└── utils/                    # 工具函数
    └── storage.js            # 本地存储工具

📱 核心代码实现

本地存储工具 (utils/storage.js)
import AsyncStorage from '@react-native-async-storage/async-storage'; // 导入AsyncStorage

const STORAGE_KEY = '@todo_app_data';

export const storage = {
  // 保存数据
  async save(todos) {
    try {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
    } catch (error) {
      console.error('保存数据失败:', error);
    }
  },

  // 读取数据
  async load() {
    try {
      const data = await AsyncStorage.getItem(STORAGE_KEY);
      return data ? JSON.parse(data) : []; // 如果没有数据,返回空数组
    } catch (error) {
      console.error('读取数据失败:', error);
      return [];
    }
  },

  // 清空数据 (可选)
  async clear() {
    try {
      await AsyncStorage.removeItem(STORAGE_KEY);
    } catch (error) {
      console.error('清空数据失败:', error);
    }
  }
};
主屏幕组件 (screens/TodoScreen.js 节选)
import React, { useState, useEffect, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  FlatList, // 用于高效渲染列表
  KeyboardAvoidingView, // 避免键盘遮挡输入框
  Platform, // 检测平台
  Alert,
  TextInput, // 输入框
  TouchableOpacity // 自定义按钮
} from 'react-native';
// import TodoInput from '../components/TodoInput'; // 假设这些组件已定义
// import TodoItem from '../components/TodoItem';
// import FilterButtons from '../components/FilterButtons';
import { storage } from '../utils/storage'; // 导入本地存储工具

// 模拟的 TodoInput, TodoItem, FilterButtons
const TodoInput = ({ onAddTodo }) => {
  const [text, setText] = useState('');
  const handleAdd = () => {
    onAddTodo(text);
    setText('');
  };
  return (
    <View style={todoInputStyles.container}>
      <TextInput
        style={todoInputStyles.input}
        placeholder="添加新的待办事项..."
        value={text}
        onChangeText={setText}
        onSubmitEditing={handleAdd}
      />
      <TouchableOpacity style={todoInputStyles.button} onPress={handleAdd}>
        <Text style={todoInputStyles.buttonText}>添加</Text>
      </TouchableOpacity>
    </View>
  );
};
const todoInputStyles = StyleSheet.create({ /* ... */ }); // 简化,实际代码中应有样式

const TodoItem = ({ todo, onToggle, onDelete, onEdit }) => (
  <View style={todoItemStyles.container}>
    <TouchableOpacity onPress={onToggle} style={todoItemStyles.checkbox}>
      <Text>{todo.completed ? '✅' : '⬜'}</Text>
    </TouchableOpacity>
    <Text 
      style={[
        todoItemStyles.text, 
        todo.completed && todoItemStyles.completedText
      ]}
    >
      {todo.text}
    </Text>
    <TouchableOpacity onPress={onDelete} style={todoItemStyles.deleteButton}>
      <Text style={todoItemStyles.deleteButtonText}>删除</Text>
    </TouchableOpacity>
  </View>
);
const todoItemStyles = StyleSheet.create({ /* ... */ }); // 简化

const FilterButtons = ({ currentFilter, onFilterChange, stats }) => (
  <View style={filterButtonsStyles.container}>
    {['all', 'active', 'completed'].map(filter => (
      <TouchableOpacity
        key={filter}
        style={[
          filterButtonsStyles.button,
          currentFilter === filter && filterButtonsStyles.activeButton
        ]}
        onPress={() => onFilterChange(filter)}
      >
        <Text style={filterButtonsStyles.buttonText}>{filter.toUpperCase()}</Text>
      </TouchableOpacity>
    ))}
  </View>
);
const filterButtonsStyles = StyleSheet.create({ /* ... */ }); // 简化


const FILTER_OPTIONS = {
  ALL: 'all',
  ACTIVE: 'active',
  COMPLETED: 'completed'
};

export default function TodoScreen() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState(FILTER_OPTIONS.ALL);

  // 初次加载时从本地存储读取待办事项
  useEffect(() => {
    loadTodos();
  }, []);

  // 待办事项列表更新时,保存到本地存储
  useEffect(() => {
    if (todos.length >= 0) { // 确保即使空数组也保存
      storage.save(todos);
    }
  }, [todos]);

  // 从本地存储加载待办事项
  const loadTodos = async () => {
    const savedTodos = await storage.load();
    setTodos(savedTodos);
  };

  // 添加待办事项
  const addTodo = useCallback((text) => {
    if (text.trim()) {
      const newTodo = {
        id: Date.now().toString(), // 使用时间戳作为唯一ID
        text: text.trim(),
        completed: false,
        createdAt: new Date().toISOString()
      };
      setTodos(prev => [newTodo, ...prev]); // 新事项添加到列表顶部
    }
  }, []);

  // 切换待办事项的完成状态
  const toggleTodo = useCallback((id) => {
    setTodos(prev => 
      prev.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  // 删除待办事项,带有确认弹窗
  const deleteTodo = useCallback((id) => {
    Alert.alert(
      '确认删除',
      '确定要删除这个待办事项吗?',
      [
        { text: '取消', style: 'cancel' },
        { 
          text: '删除', 
          style: 'destructive',
          onPress: () => {
            setTodos(prev => prev.filter(todo => todo.id !== id));
          }
        }
      ]
    );
  }, []);

  // 编辑待办事项文本
  const editTodo = useCallback((id, newText) => {
    if (newText.trim()) {
      setTodos(prev => 
        prev.map(todo => 
          todo.id === id ? { ...todo, text: newText.trim() } : todo
        )
      );
    }
  }, []);

  // 根据当前筛选条件过滤待办事项
  const filteredTodos = todos.filter(todo => {
    switch (filter) {
      case FILTER_OPTIONS.ACTIVE:
        return !todo.completed;
      case FILTER_OPTIONS.COMPLETED:
        return todo.completed;
      default: // ALL
        return true;
    }
  });

  // 计算待办事项统计数据
  const getStats = () => {
    const total = todos.length;
    const completed = todos.filter(todo => todo.completed).length;
    const active = total - completed;
    return { total, completed, active };
  };

  const stats = getStats();

  return (
    <KeyboardAvoidingView 
      style={styles.container}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'} // iOS使用'padding',Android使用'height'
    >
      <View style={styles.content}>
        <Text style={styles.title}>我的待办事项</Text>
        
        {/* 统计信息 */}
        <View style={styles.statsContainer}>
          <Text style={styles.statsText}>
            总计: {stats.total} | 进行中: {stats.active} | 已完成: {stats.completed}
          </Text>
        </View>

        {/* 输入框 */}
        <TodoInput onAddTodo={addTodo} />

        {/* 筛选按钮 */}
        <FilterButtons 
          currentFilter={filter}
          onFilterChange={setFilter}
          stats={stats}
        />

        {/* 待办事项列表 */}
        <FlatList
          style={styles.list}
          data={filteredTodos}
          keyExtractor={(item) => item.id}
          renderItem={({ item }) => (
            <TodoItem
              todo={item}
              onToggle={() => toggleTodo(item.id)}
              onDelete={() => deleteTodo(item.id)}
              onEdit={(newText) => editTodo(item.id, newText)}
            />
          )}
          showsVerticalScrollIndicator={false}
          ListEmptyComponent={ // 列表为空时的提示
            <Text style={styles.emptyListText}>暂无待办事项,快来添加吧!</Text>
          }
        />
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  content: {
    flex: 1,
    padding: 20,
    paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 20, // 适配Android刘海屏
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#2c3e50',
    textAlign: 'center',
    marginBottom: 20,
  },
  statsContainer: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 10,
    marginBottom: 15,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 2,
  },
  statsText: {
    textAlign: 'center',
    color: '#7f8c8d',
    fontSize: 14,
  },
  list: {
    flex: 1,
  },
  emptyListText: {
    textAlign: 'center',
    marginTop: 50,
    fontSize: 16,
    color: '#7f8c8d',
  },
  // 简化的 TodoInput 样式
  ...StyleSheet.create({
    container: {
      flexDirection: 'row',
      marginBottom: 15,
      height: 50,
    },
    input: {
      flex: 1,
      borderColor: '#ccc',
      borderWidth: 1,
      borderRadius: 8,
      paddingHorizontal: 10,
      marginRight: 10,
      backgroundColor: 'white',
      fontSize: 16,
    },
    button: {
      backgroundColor: '#3498db',
      paddingHorizontal: 20,
      borderRadius: 8,
      justifyContent: 'center',
      alignItems: 'center',
    },
    buttonText: {
      color: 'white',
      fontSize: 16,
      fontWeight: 'bold',
    },
  }).todoInput,
  // 简化的 TodoItem 样式
  ...StyleSheet.create({
    container: {
      flexDirection: 'row',
      alignItems: 'center',
      backgroundColor: 'white',
      padding: 15,
      borderRadius: 10,
      marginBottom: 10,
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 1 },
      shadowOpacity: 0.05,
      shadowRadius: 2,
      elevation: 1,
    },
    checkbox: {
      marginRight: 10,
      padding: 5,
    },
    text: {
      flex: 1,
      fontSize: 17,
      color: '#333',
    },
    completedText: {
      textDecorationLine: 'line-through',
      color: '#aaa',
    },
    deleteButton: {
      backgroundColor: '#e74c3c',
      paddingHorizontal: 12,
      paddingVertical: 6,
      borderRadius: 5,
      marginLeft: 10,
    },
    deleteButtonText: {
      color: 'white',
      fontSize: 14,
    },
  }).todoItem,
  // 简化的 FilterButtons 样式
  ...StyleSheet.create({
    container: {
      flexDirection: 'row',
      justifyContent: 'space-around',
      marginBottom: 20,
      backgroundColor: 'white',
      borderRadius: 10,
      padding: 5,
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 1 },
      shadowOpacity: 0.05,
      shadowRadius: 2,
      elevation: 1,
    },
    button: {
      flex: 1,
      paddingVertical: 10,
      borderRadius: 8,
      alignItems: 'center',
    },
    activeButton: {
      backgroundColor: '#3498db',
    },
    buttonText: {
      color: '#555',
      fontWeight: 'bold',
    },
    activeButtonText: {
      color: 'white',
    },
  }).filterButtons,
});

🔧 关键技术点

🗄️ 本地存储

使用 AsyncStorage 实现数据持久化

🔍 搜索筛选

实现多种过滤条件的数据展示

✏️ 实时编辑

支持待办事项的即时编辑功能

🎨 状态管理

使用 useState 和 useCallback 优化性能

🌤️ 案例3:天气应用(中级)

📋 项目需求

🏗️ 项目结构

weather-app/
├── App.js
├── components/
│   ├── WeatherCard.js       # 当前天气卡片
│   ├── SearchBar.js         # 城市搜索栏
│   ├── ForecastList.js      # 7天预报列表
│   └── LoadingSpinner.js    # 加载指示器
├── services/
│   ├── weatherAPI.js        # OpenWeatherMap API服务
│   └── locationService.js   # Expo Location 服务
└── screens/
    └── WeatherScreen.js     # 主天气显示屏幕

📱 核心代码实现

天气API服务 (services/weatherAPI.js)
import { WeatherAPI_KEY } from '@env'; // 假设您使用@env或类似库管理环境变量

const BASE_URL = 'https://api.openweathermap.org/data/2.5';

export const weatherAPI = {
  // 获取当前位置天气
  async getCurrentWeather(lat, lon) {
    try {
      const response = await fetch(
        `${BASE_URL}/weather?lat=${lat}&lon=${lon}&appid=${WeatherAPI_KEY}&units=metric&lang=zh_cn`
      );
      const data = await response.json();
      if (data.cod !== 200) throw new Error(data.message || '获取当前天气失败');
      return this.formatWeatherData(data);
    } catch (error) {
      console.error('获取天气数据失败:', error);
      throw error;
    }
  },

  // 搜索城市天气
  async searchCityWeather(cityName) {
    try {
      const response = await fetch(
        `${BASE_URL}/weather?q=${encodeURIComponent(cityName)}&appid=${WeatherAPI_KEY}&units=metric&lang=zh_cn`
      );
      const data = await response.json();
      if (data.cod !== 200) throw new Error(data.message || '搜索城市天气失败');
      return this.formatWeatherData(data);
    } catch (error) {
      console.error('搜索城市天气失败:', error);
      throw error;
    }
  },

  // 获取7天天气预报 (需要One Call API,该API返回当前、小时、日预报)
  async getForecast(lat, lon) {
    try {
      const response = await fetch(
        `${BASE_URL}/onecall?lat=${lat}&lon=${lon}&exclude=minutely,hourly,alerts&appid=${WeatherAPI_KEY}&units=metric&lang=zh_cn`
      );
      const data = await response.json();
      if (data.cod) throw new Error(data.message || '获取预报失败'); // One Call API没有cod字段,但以防万一
      return this.formatForecastData(data);
    } catch (error) {
      console.error('获取预报数据失败:', error);
      throw error;
    }
  },

  // 格式化当前天气数据
  formatWeatherData(data) {
    return {
      city: data.name,
      country: data.sys.country,
      temperature: Math.round(data.main.temp),
      description: data.weather[0].description,
      icon: data.weather[0].icon,
      humidity: data.main.humidity,
      windSpeed: data.wind.speed,
      pressure: data.main.pressure,
      feelsLike: Math.round(data.main.feels_like),
      visibility: data.visibility / 1000, // 转换为公里
      lat: data.coord.lat, // 用于获取预报
      lon: data.coord.lon, // 用于获取预报
    };
  },

  // 格式化预报数据
  formatForecastData(data) {
    return data.daily.slice(0, 7).map(day => ({
      date: new Date(day.dt * 1000).toLocaleDateString('zh-CN', { 
        weekday: 'long', 
        month: 'short', 
        day: 'numeric' 
      }),
      high: Math.round(day.temp.max),
      low: Math.round(day.temp.min),
      description: day.weather[0].description,
      icon: day.weather[0].icon,
      humidity: day.humidity,
      windSpeed: day.wind_speed,
    }));
  }
};
位置服务 (services/locationService.js)
import * as Location from 'expo-location'; // 导入Expo位置服务

export const locationService = {
  // 请求位置权限
  async requestLocationPermission() {
    const { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== 'granted') {
      throw new Error('位置权限被拒绝,请在设置中允许应用获取位置信息。');
    }
    return status;
  },

  // 获取当前位置
  async getCurrentPosition() {
    try {
      const status = await this.requestLocationPermission();
      if (status !== 'granted') {
        throw new Error('需要位置权限才能获取天气');
      }

      // 获取当前位置,精度高
      const location = await Location.getCurrentPositionAsync({
        accuracy: Location.Accuracy.High,
      });

      return {
        latitude: location.coords.latitude,
        longitude: location.coords.longitude,
      };
    } catch (error) {
      console.error('获取位置失败:', error);
      throw error;
    }
  },

  // 反向地理编码(坐标转地址,可选功能)
  async reverseGeocode(latitude, longitude) {
    try {
      const addresses = await Location.reverseGeocodeAsync({
        latitude,
        longitude,
      });
      
      if (addresses.length > 0) {
        const address = addresses[0];
        return {
          city: address.city || address.region, // 优先城市,其次是区域
          region: address.region,
          country: address.country,
        };
      }
      return null;
    } catch (error) {
      console.error('反向地理编码失败:', error);
      return null;
    }
  }
};
主屏幕组件 (screens/WeatherScreen.js 节选)
import React, { useState, useEffect, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView, // 用于可滚动内容
  RefreshControl, // 下拉刷新
  Alert,
  StatusBar, // 控制状态栏样式
} from 'react-native';
import { weatherAPI } from '../services/weatherAPI';
import { locationService } from '../services/locationService';
// import WeatherCard from '../components/WeatherCard'; // 假设已定义
// import SearchBar from '../components/SearchBar';
// import ForecastList from '../components/ForecastList';
// import LoadingSpinner from '../components/LoadingSpinner';

// 模拟的 WeatherCard, SearchBar, ForecastList, LoadingSpinner
const WeatherCard = ({ weather }) => (
  <View style={weatherCardStyles.container}>
    <Text style={weatherCardStyles.city}>{weather.city}, {weather.country}</Text>
    <Text style={weatherCardStyles.temperature}>{weather.temperature}°C</Text>
    <Text style={weatherCardStyles.description}>{weather.description}</Text>
    {/* 更多天气详情 */}
  </View>
);
const weatherCardStyles = StyleSheet.create({ /* ... */ });

const SearchBar = ({ onSearch, onLocationPress, searching }) => {
  const [query, setQuery] = useState('');
  return (
    <View style={searchBarStyles.container}>
      <TextInput
        style={searchBarStyles.input}
        placeholder="搜索城市..."
        value={query}
        onChangeText={setQuery}
        onSubmitEditing={() => onSearch(query)}
      />
      <TouchableOpacity style={searchBarStyles.button} onPress={() => onSearch(query)}>
        <Text style={searchBarStyles.buttonText}>搜索</Text>
      </TouchableOpacity>
      <TouchableOpacity style={searchBarStyles.button} onPress={onLocationPress}>
        <Text style={searchBarStyles.buttonText}>📍</Text>
      </TouchableOpacity>
    </View>
  );
};
const searchBarStyles = StyleSheet.create({ /* ... */ });

const ForecastList = ({ forecast }) => (
  <View style={forecastListStyles.container}>
    <Text style={forecastListStyles.title}>未来7天预报</Text>
    {forecast.map((day, index) => (
      <View key={index} style={forecastListStyles.item}>
        <Text style={forecastListStyles.date}>{day.date}</Text>
        <Text>{day.high}°C / {day.low}°C</Text>
        <Text>{day.description}</Text>
      </View>
    ))}
  </View>
);
const forecastListStyles = StyleSheet.create({ /* ... */ });

const LoadingSpinner = () => (
  <View style={loadingSpinnerStyles.container}>
    <ActivityIndicator size="large" color="#0000ff" />
    <Text>加载中...</Text>
  </View>
);
const loadingSpinnerStyles = StyleSheet.create({ /* ... */ });


export default function WeatherScreen() {
  const [weather, setWeather] = useState(null);
  const [forecast, setForecast] = useState([]);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [searching, setSearching] = useState(false);

  // 初始化加载天气数据
  useEffect(() => {
    loadWeatherData(true); // 默认加载当前位置天气
  }, []);

  // 加载天气数据函数
  const loadWeatherData = async (useLocation = true, cityName = null, lat = null, lon = null) => {
    try {
      setLoading(true);
      
      let currentLat = lat;
      let currentLon = lon;

      if (useLocation) {
        // 使用当前位置
        const location = await locationService.getCurrentPosition();
        currentLat = location.latitude;
        currentLon = location.longitude;
      } else if (cityName) {
        // 搜索指定城市,需要先获取城市坐标
        // OpenWeatherMap的/weather接口会返回coord,可以提取出来用于获取forecast
        const searchedWeather = await weatherAPI.searchCityWeather(cityName);
        setWeather(searchedWeather); // 立即更新当前天气
        currentLat = searchedWeather.lat;
        currentLon = searchedWeather.lon;
      }

      if (currentLat && currentLon) {
        // 同时请求当前天气和预报
        const [weatherData, forecastData] = await Promise.all([
          useLocation ? weatherAPI.getCurrentWeather(currentLat, currentLon) : weatherAPI.searchCityWeather(cityName),
          weatherAPI.getForecast(currentLat, currentLon),
        ]);
        setWeather(weatherData);
        setForecast(forecastData);
      } else {
        throw new Error('无法获取地理位置信息');
      }

    } catch (error) {
      Alert.alert('错误', error.message || '获取天气数据失败');
      setWeather(null); // 清空天气数据
      setForecast([]);
    } finally {
      setLoading(false);
      setRefreshing(false);
      setSearching(false);
    }
  };

  // 下拉刷新回调
  const onRefresh = useCallback(() => {
    setRefreshing(true);
    // 刷新当前显示的天气,如果是搜索结果,则刷新搜索结果
    if (weather && weather.lat && weather.lon) {
      loadWeatherData(false, null, weather.lat, weather.lon);
    } else {
      loadWeatherData(true); // 否则加载当前位置
    }
  }, [weather]);

  // 处理城市搜索
  const handleSearch = useCallback(async (query) => {
    if (!query.trim()) return;
    
    setSearching(true);
    await loadWeatherData(false, query.trim());
  }, []);

  // 切换到当前位置
  const handleLocationPress = useCallback(() => {
    loadWeatherData(true);
  }, []);

  if (loading) {
    return <LoadingSpinner />;
  }

  return (
    <View style={styles.container}>
      <StatusBar barStyle="light-content" backgroundColor="#74b9ff" />
      
      <ScrollView
        style={styles.scrollView}
        contentContainerStyle={styles.scrollContent}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            colors={['#0984e3']}
            tintColor="#0984e3" // iOS加载指示器颜色
          />
        }
      >
        {/* 搜索栏 */}
        <SearchBar
          onSearch={handleSearch}
          onLocationPress={handleLocationPress}
          searching={searching}
        />

        {/* 当前天气卡片 */}
        {weather ? (
          <WeatherCard
            weather={weather}
          />
        ) : (
          <Text style={styles.noDataText}>未能获取天气数据。</Text>
        )}

        {/* 7天预报 */}
        {forecast.length > 0 && (
          <ForecastList forecast={forecast} />
        )}
      </ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#74b9ff', // 背景色为蓝色
  },
  scrollView: {
    flex: 1,
  },
  scrollContent: {
    paddingBottom: 20,
    alignItems: 'center', // 使内容居中
  },
  noDataText: {
    color: 'white',
    fontSize: 18,
    marginTop: 50,
  },
});

🔧 关键技术点

🌍 位置服务

使用 Expo Location 获取用户位置

☁️ 天气API

集成 OpenWeatherMap 天气服务

📱 响应式设计

适配不同屏幕尺寸的布局

🎭 动画效果

添加天气状态动画效果(此教程未详述,作为扩展点)

💬 案例4:社交媒体应用(高级)

📋 项目需求

🏗️ 项目结构

social-app/
├── App.js                   # 主入口,可能包含AuthContext提供者
├── config/                  # Firebase配置
│   └── firebase.js
├── navigation/              # 导航配置
│   └── AppNavigator.js      # 根导航器
├── services/                # 业务逻辑服务
│   ├── authService.js       # Firebase认证服务
│   ├── database.js          # Firestore数据库操作
│   ├── storageService.js    # Firebase Storage图片存储
│   └── notificationService.js # 推送通知服务
├── screens/                 # 页面组件
│   ├── AuthScreen.js        # 登录/注册界面
│   ├── HomeScreen.js        # 动态主页
│   ├── ProfileScreen.js     # 个人资料页
│   ├── ChatScreen.js        # 聊天界面
│   └── PostScreen.js        # 发布动态界面
├── components/              # 可复用UI组件
│   ├── PostCard.js          # 动态展示卡片
│   ├── ChatBubble.js        # 聊天气泡
│   └── UserAvatar.js        # 用户头像组件
└── context/                 # React Context API
    └── AuthContext.js       # 认证状态上下文

📱 核心代码实现

认证服务 (services/authService.js)
import { 
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
  signOut,
  updateProfile, // 更新用户资料
  onAuthStateChanged // 监听认证状态
} from 'firebase/auth';
import { auth } from '../config/firebase'; // 导入Firebase认证实例
import { storageService } from './storageService'; // 用于本地存储用户信息

export const authService = {
  // 监听认证状态变化
  onAuthStateChange(callback) {
    return onAuthStateChanged(auth, callback);
  },

  // 注册新用户
  async signUp(email, password, displayName) {
    try {
      const userCredential = await createUserWithEmailAndPassword(auth, email, password);
      const user = userCredential.user;

      // 更新用户资料,设置显示名称
      await updateProfile(user, {
        displayName: displayName
      });

      // 保存用户信息到本地
      await storageService.saveUserInfo({
        uid: user.uid,
        email: user.email,
        displayName: user.displayName,
        photoURL: user.photoURL,
        createdAt: new Date().toISOString()
      });

      return user;
    } catch (error) {
      console.error('注册失败:', error);
      throw this.handleAuthError(error);
    }
  },

  // 用户登录
  async signIn(email, password) {
    try {
      const userCredential = await signInWithEmailAndPassword(auth, email, password);
      const user = userCredential.user;

      // 更新最后登录时间,并保存用户信息
      await storageService.saveUserInfo({
        uid: user.uid, // 确保保存完整的用户对象或必要字段
        email: user.email,
        displayName: user.displayName,
        photoURL: user.photoURL,
        lastLoginAt: new Date().toISOString()
      });

      return user;
    } catch (error) {
      console.error('登录失败:', error);
      throw this.handleAuthError(error);
    }
  },

  // 用户登出
  async signOut() {
    try {
      await signOut(auth);
      await storageService.clearUserInfo(); // 清除本地存储的用户信息
    } catch (error) {
      console.error('登出失败:', error);
      throw error;
    }
  },

  // 获取当前用户
  getCurrentUser() {
    return auth.currentUser;
  },

  // 统一错误处理
  handleAuthError(error) {
    const errorMessages = {
      'auth/user-not-found': '用户不存在',
      'auth/wrong-password': '密码错误',
      'auth/email-already-in-use': '邮箱已被使用',
      'auth/weak-password': '密码强度不够',
      'auth/invalid-email': '邮箱格式无效',
      'auth/too-many-requests': '请求过于频繁,请稍后重试'
    };

    return new Error(errorMessages[error.code] || '认证失败');
  }
};
数据库服务 (services/database.js 节选)
import {
  collection,
  doc,
  addDoc,
  updateDoc,
  deleteDoc,
  onSnapshot, // 实时监听
  query,
  orderBy,
  where, // 查询条件
  limit, // 限制数量
  startAfter, // 分页加载
  getDocs, // 获取一次性快照
  serverTimestamp // 服务器时间戳
} from 'firebase/firestore';
import { db } from '../config/firebase'; // 导入Firestore实例

export const databaseService = {
  // 动态相关操作
  posts: {
    // 发布动态
    async createPost(userId, content, imageUrl = null) {
      try {
        const postData = {
          userId,
          content,
          imageUrl,
          likes: [], // 点赞用户ID列表
          commentsCount: 0, // 评论数量
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp()
        };

        const docRef = await addDoc(collection(db, 'posts'), postData);
        return docRef.id;
      } catch (error) {
        console.error('发布动态失败:', error);
        throw error;
      }
    },

    // 获取动态列表 (支持分页)
    async getPosts(limitCount = 10, lastPostSnapshot = null) {
      try {
        let postsQuery = query(
          collection(db, 'posts'),
          orderBy('createdAt', 'desc'),
          limit(limitCount)
        );

        if (lastPostSnapshot) { // 如果提供了上一个文档快照,则从该文档之后开始加载
          postsQuery = query(postsQuery, startAfter(lastPostSnapshot));
        }

        const snapshot = await getDocs(postsQuery);
        return {
          posts: snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })),
          lastVisible: snapshot.docs[snapshot.docs.length - 1] // 返回最后一个文档快照用于下一页
        };
      } catch (error) {
        console.error('获取动态失败:', error);
        throw error;
      }
    },

    // 监听动态实时更新 (用于首页实时展示)
    listenToPosts(callback) {
      const postsQuery = query(
        collection(db, 'posts'),
        orderBy('createdAt', 'desc'),
        limit(50) // 最多监听50条最新动态
      );

      return onSnapshot(postsQuery, (snapshot) => {
        const posts = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        }));
        callback(posts);
      }, (error) => {
        console.error('监听动态失败:', error);
      });
    },

    // 点赞/取消点赞
    async toggleLike(postId, userId) {
      try {
        const postRef = doc(db, 'posts', postId);
        // 首先获取当前点赞列表
        const postDoc = await getDocs(query(collection(db, 'posts'), where('__name__', '==', postId)));
        
        if (!postDoc.empty) {
          const postData = postDoc.docs[0].data();
          const likes = postData.likes || [];
          const isLiked = likes.includes(userId);
          
          const updatedLikes = isLiked
            ? likes.filter(id => id !== userId) // 取消点赞
            : [...likes, userId]; // 点赞

          await updateDoc(postRef, {
            likes: updatedLikes,
            updatedAt: serverTimestamp()
          });
        }
      } catch (error) {
        console.error('点赞失败:', error);
        throw error;
      }
    }
  },

  // 评论相关操作
  comments: {
    // 发表评论
    async addComment(postId, userId, content) {
      try {
        const commentData = {
          postId,
          userId,
          content,
          createdAt: serverTimestamp()
        };

        const docRef = await addDoc(collection(db, 'comments'), commentData);
        
        // 同时更新动态的评论计数
        const postRef = doc(db, 'posts', postId);
        await updateDoc(postRef, {
          commentsCount: (await getPostCommentsCount(postId)) + 1, // 获取当前评论数+1
          updatedAt: serverTimestamp()
        });

        return docRef.id;
      } catch (error) {
        console.error('发表评论失败:', error);
        throw error;
      }
    },

    // 获取某个动态的评论列表
    listenToPostComments(postId, callback) {
      const commentsQuery = query(
        collection(db, 'comments'),
        where('postId', '==', postId),
        orderBy('createdAt', 'asc')
      );

      return onSnapshot(commentsQuery, (snapshot) => {
        const comments = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        }));
        callback(comments);
      }, (error) => {
        console.error('监听评论失败:', error);
      });
    },

    // 获取评论数量
    async getPostCommentsCount(postId) {
      const commentsQuery = query(
        collection(db, 'comments'),
        where('postId', '==', postId)
      );
      const snapshot = await getDocs(commentsQuery);
      return snapshot.size;
    }
  },

  // 聊天消息相关
  messages: {
    // 发送消息
    async sendMessage(conversationId, senderId, content, type = 'text') {
      try {
        const messageData = {
          conversationId,
          senderId,
          content,
          type,
          createdAt: serverTimestamp(),
          read: false // 消息是否已读
        };

        const docRef = await addDoc(collection(db, 'messages'), messageData);
        return docRef.id;
      } catch (error) {
        console.error('发送消息失败:', error);
        throw error;
      }
    },

    // 监听会话消息
    listenToMessages(conversationId, callback) {
      const messagesQuery = query(
        collection(db, 'messages'),
        where('conversationId', '==', conversationId),
        orderBy('createdAt', 'asc')
      );

      return onSnapshot(messagesQuery, (snapshot) => {
        const messages = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        }));
        callback(messages);
      }, (error) => {
        console.error('监听消息失败:', error);
      });
    }
  }
};
认证上下文 (context/AuthContext.js)
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { authService } from '../services/authService';
import { storageService } from '../services/storageService'; // 用于本地存储用户session

const AuthContext = createContext(); // 创建认证上下文

// 认证状态的reducer函数
const authReducer = (state, action) => {
  switch (action.type) {
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_USER':
      return { ...state, user: action.payload, loading: false, error: null }; // 设置用户,清除错误
    case 'SIGN_OUT':
      return { ...state, user: null, loading: false, error: null }; // 登出,清除用户和错误
    case 'SET_ERROR':
      return { ...state, error: action.payload, loading: false }; // 设置错误
    default:
      return state;
  }
};

const initialState = {
  user: null,
  loading: true, // 初始加载状态
  error: null
};

export const AuthProvider = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, initialState);

  // 初始化认证状态:在应用启动时检查用户登录状态
  useEffect(() => {
    const initializeAuth = async () => {
      try {
        // 首先尝试从本地存储加载用户信息(可选,用于快速恢复状态)
        const savedUser = await storageService.getUserInfo();
        if (savedUser) {
          // 如果有本地用户,且Firebase当前也有用户,则设置
          const currentUser = authService.getCurrentUser();
          if (currentUser && currentUser.uid === savedUser.uid) { // 确保是同一个用户
            dispatch({ type: 'SET_USER', payload: currentUser });
          } else {
            // 本地用户失效或不匹配,清除本地数据
            await storageService.clearUserInfo();
            dispatch({ type: 'SET_USER', payload: null });
          }
        } else {
          dispatch({ type: 'SET_USER', payload: null });
        }
      } catch (error) {
        console.error('初始化认证状态失败:', error);
        dispatch({ type: 'SET_USER', payload: null });
        dispatch({ type: 'SET_ERROR', payload: error.message });
      }
      // 监听Firebase认证状态变化,这是最权威的
      const unsubscribe = authService.onAuthStateChange((user) => {
        if (user) {
          dispatch({ type: 'SET_USER', payload: user });
          // 每次登录成功也更新本地存储
          storageService.saveUserInfo({
            uid: user.uid,
            email: user.email,
            displayName: user.displayName,
            photoURL: user.photoURL,
            lastLoginAt: new Date().toISOString()
          });
        } else {
          dispatch({ type: 'SIGN_OUT' });
          storageService.clearUserInfo(); // 登出时清除本地存储
        }
      });

      return unsubscribe; // 清理订阅
    };

    initializeAuth();
  }, []);

  // 登录函数
  const signIn = async (email, password) => {
    try {
      dispatch({ type: 'SET_LOADING', payload: true });
      dispatch({ type: 'SET_ERROR', payload: null });
      
      const user = await authService.signIn(email, password);
      // SET_USER会在onAuthStateChange中触发
      return user;
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message });
      throw error;
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };

  // 注册函数
  const signUp = async (email, password, displayName) => {
    try {
      dispatch({ type: 'SET_LOADING', payload: true });
      dispatch({ type: 'SET_ERROR', payload: null });
      
      const user = await authService.signUp(email, password, displayName);
      // SET_USER会在onAuthStateChange中触发
      return user;
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message });
      throw error;
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };

  // 登出函数
  const signOut = async () => {
    try {
      await authService.signOut();
      // SIGN_OUT会在onAuthStateChange中触发
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message });
      throw error;
    }
  };

  // 提供给子组件的上下文值
  const value = {
    user: state.user,
    loading: state.loading,
    error: state.error,
    signIn,
    signUp,
    signOut
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

// 自定义Hook,用于在组件中方便地使用认证上下文
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

🔧 关键技术点

🔐 用户认证

使用Firebase Authentication实现完整的注册、登录、登出

🗄️ 实时数据库

使用Firestore实现实时数据同步(动态、评论、聊天)

📸 图片上传

集成Firebase Storage处理图片资源

🔔 推送通知

实现消息推送和系统通知(需要Expo Notifications)

🧭 第四部分:导航与路由

4.1 React Navigation 基础

安装导航依赖

# 安装核心导航组件
npx expo install @react-navigation/native @react-navigation/bottom-tabs @react-navigation/stack

# 安装导航所需的其他依赖
npx expo install react-native-screens react-native-safe-area-context

导航配置示例 (AppNavigator.js)

import React from 'react';
import { NavigationContainer } from '@react-navigation/native'; // 导航容器
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; // 底部Tab导航
import { createStackNavigator } from '@react-navigation/stack'; // 堆栈导航
import { Ionicons } from '@expo/vector-icons'; // 图标库

// 假设的屏幕组件
import HomeScreen from '../screens/HomeScreen';
import ProfileScreen from '../screens/ProfileScreen';
import SettingsScreen from '../screens/SettingsScreen';
import DetailScreen from '../screens/DetailScreen';

const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();

// HomeStack: 一个包含Home和Detail页面的堆栈导航
function HomeStack() {
  return (
    <Stack.Navigator initialRouteName="HomeContent">
      <Stack.Screen name="HomeContent" component={HomeScreen} options={{ title: '首页' }} />
      <Stack.Screen name="Detail" component={DetailScreen} options={{ title: '详情' }} />
    </Stack.Navigator>
  );
}

// AppNavigator: 应用程序的主导航,底部Tab导航
export default function AppNavigator() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            // 根据路由名称设置不同的图标
            if (route.name === 'HomeTab') {
              iconName = focused ? 'home' : 'home-outline';
            } else if (route.name === 'Profile') {
              iconName = focused ? 'person' : 'person-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'settings' : 'settings-outline';
            }

            // 返回Ionicons组件
            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: '#007AFF', // 选中Tab的颜色
          tabBarInactiveTintColor: 'gray', // 未选中Tab的颜色
          headerShown: false, // 默认隐藏顶部导航栏,由Stack Navigator控制
        })}
      >
        <Tab.Screen 
          name="HomeTab" 
          component={HomeStack} // Tab中嵌套Stack导航
          options={{ title: '首页' }} // TabBar中显示的名称
        />
        <Tab.Screen name="Profile" component={ProfileScreen} options={{ title: '我的' }} />
        <Tab.Screen name="Settings" component={SettingsScreen} options={{ title: '设置' }} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

4.2 导航参数传递与操作

import { useNavigation, useRoute } from '@react-navigation/native';

// 在源屏幕中传递参数
const navigation = useNavigation();
navigation.navigate('Detail', { 
  itemId: 123,
  itemTitle: '产品详情' 
});

// 在目标屏幕中接收参数
const route = useRoute();
const { itemId, itemTitle } = route.params;

// 返回上一页
navigation.goBack();

// 返回到导航栈的根页面
navigation.popToTop();

// 替换当前屏幕,使返回按钮不会回到当前屏幕
navigation.replace('NewScreen', { data: 'some data' });

⚡ 第五部分:高级功能与特性

5.1 设备功能访问

📷 相机与相册功能

使用 expo-cameraexpo-image-picker

import { Camera } from 'expo-camera';
import * as ImagePicker from 'expo-image-picker';
import React, { useRef, useState, useEffect } from 'react';
import { View, Button, Image, Alert } from 'react-native';

function CameraExample() {
  const [hasPermission, setHasPermission] = useState(null);
  const cameraRef = useRef(null);

  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      setHasPermission(status === 'granted');
    })();
  }, []);

  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync();
      console.log(photo.uri); // 照片的URI
      // 可以在此处将照片上传或显示
    }
  };

  const pickImage = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
    });

    if (!result.canceled) {
      console.log(result.assets[0].uri); // 选择的图片URI
    }
  };

  if (hasPermission === null) return <View />;
  if (hasPermission === false) return <Text>无权限访问相机</Text>;

  return (
    <View style={{ flex: 1 }}>
      <Camera style={{ flex: 1 }} type={Camera.Constants.Type.back} ref={cameraRef}>
        <View style={{ flex: 1, backgroundColor: 'transparent', flexDirection: 'row' }}>
          <Button title="拍照" onPress={takePicture} />
          <Button title="选择图片" onPress={pickImage} />
        </View>
      </Camera>
    </View>
  );
}

📍 地理位置

使用 expo-location 获取设备位置

import * as Location from 'expo-location';
import React, { useState, useEffect } from 'react';
import { View, Text, Button, Alert } from 'react-native';

function LocationExample() {
  const [location, setLocation] = useState(null);
  const [errorMsg, setErrorMsg] = useState(null);

  useEffect(() => {
    (async () => {
      let { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') {
        setErrorMsg('位置权限被拒绝');
        Alert.alert('权限不足', '需要位置权限才能获取您的位置');
        return;
      }

      let currentLocation = await Location.getCurrentPositionAsync({});
      setLocation(currentLocation);
    })();
  }, []);

  return (
    <View>
      <Text>{errorMsg || '等待获取位置...'}</Text>
      {location && (
        <Text>
          纬度: {location.coords.latitude}, 
          经度: {location.coords.longitude}
        </Text>
      )}
    </View>
  );
}

5.2 推送通知

使用 expo-notifications 实现本地和远程推送

import * as Notifications from 'expo-notifications';
import React, { useEffect } from 'react';
import { Button, View, Text, Alert } from 'react-native';

// 设置通知处理程序,控制应用在前台时通知的行为
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true, // 是否显示通知横幅
    shouldPlaySound: true, // 是否播放声音
    shouldSetBadge: false, // 是否更新应用图标上的徽章
  }),
});

function NotificationExample() {
  useEffect(() => {
    // 请求通知权限
    (async () => {
      const { status: existingStatus } = await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;
      if (existingStatus !== 'granted') {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
      if (finalStatus !== 'granted') {
        Alert.alert('权限不足', '无法发送推送通知,请在设置中启用通知权限。');
        return;
      }

      // 获取Expo Push Token,用于发送远程通知
      const token = (await Notifications.getExpoPushTokenAsync()).data;
      console.log('Expo Push Token:', token);
      // 您可以将此token发送到您的后端服务器,以便通过Expo的API发送通知
    })();
  }, []);

  // 发送本地通知
  const sendLocalNotification = async () => {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: '我的本地通知!',
        body: '这是从您的应用内部发送的本地通知。',
        data: { someData: 'goes here' }, // 可携带自定义数据
      },
      trigger: { seconds: 2 }, // 2秒后触发
    });
    Alert.alert('通知已发送', '您将在2秒后收到本地通知。');
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Button title="发送本地通知" onPress={sendLocalNotification} />
    </View>
  );
}

5.3 动画与手势

使用 react-native-reanimated 实现高性能声明式动画

import Animated, {
  useAnimatedStyle, // 用于创建动画样式
  useSharedValue, // 创建共享值
  withTiming, // 时间动画
  withSpring, // 弹性动画
  withDelay, // 延迟动画
} from 'react-native-reanimated';
import { View, Button, StyleSheet } from 'react-native';
import React from 'react';

function AnimatedBox() {
  const translateX = useSharedValue(0); // 声明一个共享值,初始为0
  const scale = useSharedValue(1); // 声明一个共享值,初始为1

  // 创建动画样式,会监听共享值的变化
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { scale: scale.value }
    ],
  }));

  const handleAnimate = () => {
    // 点击后,translateX值变为100,带有时间动画效果
    translateX.value = withTiming(100, { duration: 1000 });
    // scale值变为1.5,带有弹性动画效果
    scale.value = withSpring(1.5, { damping: 10, stiffness: 100 });
  };

  const handleReset = () => {
    translateX.value = withTiming(0, { duration: 500 });
    scale.value = withSpring(1, { damping: 10, stiffness: 100 });
  };

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, animatedStyle]} />
      <Button title="动画一下" onPress={handleAnimate} />
      <Button title="重置" onPress={handleReset} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: '#3498db',
    borderRadius: 10,
    marginBottom: 20,
  },
});

🔧 第六部分:原生模块与扩展

6.1 Expo Managed vs Bare Workflow

📦 Managed Workflow (托管工作流)

  • 特点: 无需接触原生代码,通过 Expo 抽象层开发。
  • 优势: 开发效率高,无需配置原生环境,易于上手,跨平台特性良好,通过 Expo Go 应用直接预览。
  • 限制: 只能使用 Expo SDK 提供的原生模块,无法直接集成自定义原生代码或某些第三方原生库。

🔧 Bare Workflow (裸工作流)

  • 特点: 完全暴露 iOS (Xcode) 和 Android (Android Studio) 原生项目,拥有完全控制权。
  • 优势: 可以集成任何原生库和原生模块,自定义原生代码,无 Expo SDK 的功能限制。
  • 劣势: 需要熟悉原生开发知识,环境配置复杂,构建和发布流程更繁琐,开发效率相对较低。
💡 什么时候选择哪个工作流?

Managed Workflow 适合大部分初学者和需要快速迭代、不涉及复杂原生功能的项目。
当您的项目需要深度定制原生功能,集成某些Expo SDK不支持的第三方原生库时,可以考虑 Bare Workflow。 您也可以从 Managed Workflow 通过 expo eject 或 EAS Build 的自定义插件功能逐步迁移到 Bare Workflow。

6.2 EAS (Expo Application Services) 构建与发布

EAS 是 Expo 提供的一套云服务,用于构建、提交和更新您的Expo和React Native应用。

# 安装 EAS CLI (如果尚未安装)
npm install -g eas-cli

# 登录 Expo 账号 (在浏览器中完成认证)
eas login

# 初始化项目配置 (创建 eas.json 文件)
eas build:configure

# 构建 Android 应用 (生成 .apk 或 .aab 文件)
eas build --platform android --profile production

# 构建 iOS 应用 (生成 .ipa 文件,需要配置 Apple 开发者账号)
eas build --platform ios --profile production

# 提交应用到 Google Play Store
eas submit --platform android --latest

# 提交应用到 Apple App Store Connect
eas submit --platform ios --latest

🚀 第七部分:部署与发布

7.1 构建不同平台版本

通过 app.json (或 app.config.js) 配置应用程序在不同平台上的行为和外观。

# app.json 配置示例
{
  "expo": {
    "name": "我的应用",           // 应用在设备上显示的名称
    "slug": "my-app-slug",      // Expo项目唯一标识符,通常用于URL
    ""version": "1.0.0",         // 应用版本号
    "orientation": "portrait",  // 应用方向:portrait (竖屏) 或 landscape (横屏)
    "icon": "./assets/icon.png", // 应用图标路径
    "splash": {                 // 启动画面配置
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {                // 更新配置
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [    // 资产文件打包模式
      "**/*"
    ],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.yourcompany.myapp", // iOS 应用唯一Bundle ID
      "buildNumber": "1.0.0"                      // iOS 构建版本号
    },
    "android": {
      "adaptiveIcon": {                           // Android 自适应图标
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      },
      "package": "com.yourcompany.myapp",         // Android 应用唯一包名
      "versionCode": 1                            // Android 版本代码,每次发布递增
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "plugins": [ // 如果使用Bare Workflow或需要原生功能
      "expo-camera"
    ]
  }
}

7.2 应用商店发布

📱 Google Play 发布检查清单

🍎 App Store 发布检查清单

🏆 第八部分:项目最佳实践

8.1 代码规范与架构模式

📁 项目结构最佳实践

src/
├── api/             # API 请求相关服务
├── components/      # 可复用UI组件 (原子组件)
├── screens/         # 页面组件 (组织大型UI)
├── navigation/      # 导航配置 (React Navigation)
├── services/        # 业务逻辑服务 (如认证、数据库)
├── utils/           # 通用工具函数
├── hooks/           # 自定义React Hooks
├── context/         # React Context API (全局状态管理)
├── assets/          # 静态资源 (图片、字体)
├── config/          # 配置文件 (如Firebase配置)
└── theme/           # 主题配置 (颜色、字体、间距)

🎨 样式管理与主题系统

建议使用 StyleSheet.create 结合主题系统。

import { StyleSheet } from 'react-native';

// theme.js
export const theme = {
  colors: {
    primary: '#3498db',
    secondary: '#2ecc71',
    text: '#2c3e50',
    background: '#f5f5f5',
    danger: '#e74c3c'
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32
  },
  fontSize: {
    sm: 14,
    md: 16,
    lg: 18,
    xl: 24
  }
};

// MyComponent.js
import { theme } from '../theme';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: theme.colors.background,
    padding: theme.spacing.md
  },
  title: {
    fontSize: theme.fontSize.xl,
    color: theme.colors.primary,
    marginBottom: theme.spacing.lg
  }
});

8.2 性能优化

⚡ 性能优化技巧:
import React, { useMemo, useCallback } from 'react';
import { FlatList, Text, View, TouchableOpacity, StyleSheet } from 'react-native';

// 假设的 UserItem 组件
const UserItem = React.memo(({ user, onPress }) => (
  <TouchableOpacity onPress={onPress} style={itemStyles.container}>
    <Text style={itemStyles.text}>{user.name} ({user.email})</Text>
  </TouchableOpacity>
));

const ITEM_HEIGHT = 60; // 每个列表项的固定高度

const UserList = ({ users, onUserPress }) => {
  // 使用 useMemo 缓存排序后的用户列表,只有当 users 数组变化时才重新计算
  const sortedUsers = useMemo(() => {
    console.log('Sorting users...');
    return users.sort((a, b) => a.name.localeCompare(b.name));
  }, [users]);

  // 使用 useCallback 缓存 renderItem 回调函数,避免每次渲染都创建新的函数实例
  const renderItem = useCallback(({ item }) => (
    <UserItem user={item} onPress={() => onUserPress(item)} />
  ), [onUserPress]); // 依赖 onUserPress 函数

  return (
    <FlatList
      data={sortedUsers}
      renderItem={renderItem}
      keyExtractor={(item) => item.id.toString()} // 确保key是字符串
      initialNumToRender={10} // 首次渲染的项数
      maxToRenderPerBatch={5} // 每次批量渲染的项数
      windowSize={21} // 渲染窗口大小
      removeClippedSubviews={true} // 移除屏幕外组件以节省内存 (需谨慎使用)
      getItemLayout={(data, index) => ({ // 优化滚动性能,如果列表项高度固定
        length: ITEM_HEIGHT,
        offset: ITEM_HEIGHT * index,
        index,
      })}
    />
  );
};

const itemStyles = StyleSheet.create({
  container: {
    padding: 15,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
    height: ITEM_HEIGHT, // 固定高度
    justifyContent: 'center',
  },
  text: {
    fontSize: 16,
    color: '#333',
  },
});

8.3 错误处理与调试

在生产环境中捕获错误,提供更好的用户体验。

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

// 错误回退组件
const ErrorFallback = ({ error }) => (
  <View style={fallbackStyles.container}>
    <Text style={fallbackStyles.title}>出现问题!</Text>
    <Text style={fallbackStyles.text}>{error ? error.message : '未知错误'}</Text>
    <Text style={fallbackStyles.subText}>请稍后再试或联系支持。</Text>
  </View>
);

// 错误边界组件 (适用于Class Component)
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  // 当子组件抛出错误时,更新状态
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // 捕获错误信息,可用于发送错误报告
  componentDidCatch(error, errorInfo) {
    console.error('应用错误:', error, errorInfo);
    // 可以在此处发送错误报告到Sentry、Bugsnag等服务
    // logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 渲染自定义的回退 UI
      return <ErrorFallback error={this.state.error} />;
    }

    return this.props.children; // 正常渲染子组件
  }
}

const fallbackStyles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ffebee', // 浅红色背景
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#c62828', // 深红色
    marginBottom: 10,
  },
  text: {
    fontSize: 16,
    color: '#d32f2f',
    textAlign: 'center',
    marginBottom: 5,
  },
  subText: {
    fontSize: 14,
    color: '#e57373',
    textAlign: 'center',
  },
});

export default ErrorBoundary;

// 在App.js中使用
// import ErrorBoundary from './ErrorBoundary';
// <ErrorBoundary>
//   <AppNavigator />
// </ErrorBoundary>

8.4 测试策略

单元测试和集成测试是保证应用质量的关键。

🧪 单元测试

测试独立的函数、组件逻辑。使用 Jest 和 React Native Testing Library。

import { render, fireEvent, screen } from '@testing-library/react-native';
import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';

// 假设我们测试的计数器组件
const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <View>
      <Text testID="count-display">{count}</Text>
      <Button title="Increment" onPress={() => setCount(count + 1)} />
      <Button title="Decrement" onPress={() => setCount(count - 1)} />
    </View>
  );
};

test('计数器增加功能正常工作', () => {
  render(<Counter />);
  const countDisplay = screen.getByTestId('count-display');
  const incrementButton = screen.getByText('Increment');

  // 初始状态为 0
  expect(countDisplay.props.children).toBe(0);

  // 点击增加按钮
  fireEvent.press(incrementButton);
  expect(countDisplay.props.children).toBe(1);

  fireEvent.press(incrementButton);
  expect(countDisplay.props.children).toBe(2);
});

test('计数器减少功能正常工作', () => {
  render(<Counter />);
  const countDisplay = screen.getByTestId('count-display');
  const incrementButton = screen.getByText('Increment');
  const decrementButton = screen.getByText('Decrement');

  // 先增加两次
  fireEvent.press(incrementButton);
  fireEvent.press(incrementButton);
  expect(countDisplay.props.children).toBe(2);

  // 点击减少按钮
  fireEvent.press(decrementButton);
  expect(countDisplay.props.children).toBe(1);

  fireEvent.press(decrementButton);
  expect(countDisplay.props.children).toBe(0);
});

🔧 集成测试

测试多个组件协同工作,模拟用户交互流程。

import { render, fireEvent, screen, waitFor } from '@testing-library/react-native';
import React, { useState } from 'react';
import { TextInput, Button, View, Text, FlatList } from 'react-native';

// 模拟的 TodoApp,简化自案例2
const TodoApp = () => {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState('');

  const addTodo = () => {
    if (text.trim()) {
      setTodos([{ id: Date.now().toString(), text: text, completed: false }, ...todos]);
      setText('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo));
  };

  return (
    <View>
      <TextInput
        testID="todo-input"
        placeholder="输入待办事项..."
        value={text}
        onChangeText={setText}
      />
      <Button title="添加" onPress={addTodo} />
      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => toggleTodo(item.id)}>
            <Text testID={`todo-item-${item.id}`} style={{ textDecorationLine: item.completed ? 'line-through' : 'none' }}>
              {item.text}
            </Text>
          </TouchableOpacity>
        )}
      />
    </View>
  );
};

test('添加和切换待办事项的功能正常工作', async () => {
  render(<TodoApp />);
  const input = screen.getByTestId('todo-input');
  const addButton = screen.getByText('添加');

  // 添加第一个待办事项
  fireEvent.changeText(input, '学习React Native');
  fireEvent.press(addButton);
  await waitFor(() => expect(screen.getByText('学习React Native')).toBeTruthy());
  const todo1 = screen.getByTestId(/todo-item-.*/); // 匹配任意id的todo项

  // 添加第二个待办事项
  fireEvent.changeText(input, '完成项目');
  fireEvent.press(addButton);
  await waitFor(() => expect(screen.getByText('完成项目')).toBeTruthy());
  const todo2 = screen.getByText('完成项目');

  // 验证第一个待办事项未完成
  expect(todo1.props.style.textDecorationLine).toBe('none');

  // 切换第一个待办事项为完成状态
  fireEvent.press(todo1);
  await waitFor(() => expect(todo1.props.style.textDecorationLine).toBe('line-through'));

  // 验证第二个待办事项仍未完成
  expect(todo2.props.style.textDecorationLine).toBe('none');
});

🎉 恭喜完成学习!

您已经完成了一份完整的React Native Expo开发教程学习。通过4个实战案例的练习,您应该已经掌握了:

  • ✅ React Native Expo 基础知识
  • ✅ 组件开发与状态管理
  • ✅ 导航与路由实现
  • ✅ 设备功能集成
  • ✅ 性能优化技巧
  • ✅ 应用部署发布
  • ✅ 项目最佳实践

📚 继续学习资源

🛠️ 开发工具

  • Expo Developer Tools
  • React Native Debugger
  • Flipper (高级调试工具)
  • Hermes 引擎 (提升JS性能)

🌟 推荐库

  • Redux/Zustand (状态管理)
  • React Query (数据获取/缓存)
  • React Hook Form (表单处理)
  • React Native Reanimated (动画)
  • NativeBase/React Native Paper (UI组件库)

🎯 下一步建议

  • 学习 TypeScript,提升代码质量和可维护性
  • 深入理解原生模块开发,扩展应用能力
  • 掌握 CI/CD (持续集成/持续部署) 流程,自动化发布
  • 参与开源项目,贡献代码,学习最佳实践
💡 学习建议:

技术学习是一个持续的过程。建议您在掌握基础后,多参与实际项目开发,关注社区动态,不断提升自己的技能水平。记住,最好的学习方法就是动手实践!

互动区域

登录后可以点赞此内容

参与互动

登录后可以点赞和评论此内容,与作者互动交流