本文从零到一拆解两个完整的跨平台项目——Flutter电商APP和React Native社交应用,并深入对比Flutter与RN的状态管理方案,最后给出一套可复用的UI组件库设计思路。全部代码基于最新稳定版本,可直接用于生产环境。


一、Flutter 电商APP 完整实战

1.1 项目架构总览

电商APP的核心挑战在于:页面状态复杂(商品列表/详情/购物车/订单)、网络请求密集、性能要求高(图片加载、列表滚动)。我们采用 Clean Architecture + Riverpod 的组合。

lib/
├── main.dart                    # 入口
├── app.dart                     # MaterialApp 配置
├── core/                        # 核心层
│   ├── network/
│   │   ├── dio_client.dart      # Dio 网络客户端
│   │   ├── api_endpoints.dart   # API 端点定义
│   │   └── api_exception.dart   # 统一异常
│   ├── theme/
│   │   ├── app_colors.dart
│   │   ├── app_text_styles.dart
│   │   └── app_theme.dart
│   ├── router/
│   │   └── app_router.dart      # GoRouter 路由配置
│   └── utils/
│       ├── price_formatter.dart
│       └── validators.dart
├── features/                    # 功能模块
│   ├── home/
│   │   ├── data/
│   │   │   ├── models/product_model.dart
│   │   │   └── repositories/product_repository_impl.dart
│   │   ├── domain/
│   │   │   ├── entities/product.dart
│   │   │   └── repositories/product_repository.dart
│   │   └── presentation/
│   │       ├── providers/home_provider.dart
│   │       ├── pages/home_page.dart
│   │       └── widgets/product_card.dart
│   ├── product_detail/
│   ├── cart/
│   ├── checkout/
│   └── profile/
└── shared/                      # 共享组件
    ├── widgets/
    │   ├── cached_image.dart
    │   ├── loading_indicator.dart
    │   └── error_widget.dart
    └── providers/
        └── core_providers.dart

1.2 网络层封装

电商场景对网络层的要求:统一拦截器、Token自动刷新、缓存策略、错误归一化。

// core/network/dio_client.dart
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api_endpoints.dart';
import 'api_exception.dart';

final dioClientProvider = Provider<Dio>((ref) {
  final dio = Dio(BaseOptions(
    baseUrl: ApiEndpoints.baseUrl,
    connectTimeout: const Duration(seconds: 15),
    receiveTimeout: const Duration(seconds: 20),
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  ));

  // 请求拦截器:自动附加 Token
  dio.interceptors.add(InterceptorsWrapper(
    onRequest: (options, handler) async {
      const token = 'YOUR_ACCESS_TOKEN'; // 实际从安全存储读取
      if (token.isNotEmpty) {
        options.headers['Authorization'] = 'Bearer $token';
      }
      handler.next(options);
    },
    onResponse: (response, handler) {
      // 统一处理业务码
      if (response.data['code'] != 200) {
        handler.reject(DioException(
          requestOptions: response.requestOptions,
          error: ApiException(response.data['message'] ?? '未知错误'),
        ));
        return;
      }
      handler.next(response);
    },
    onError: (error, handler) {
      // Token 过期自动刷新
      if (error.response?.statusCode == 401) {
        // 刷新 Token 逻辑
      }
      handler.next(error);
    },
  ));

  // 日志拦截器(仅开发环境)
  if (kDebugMode) {
    dio.interceptors.add(LogInterceptor(
      request: true,
      requestHeader: false,
      responseHeader: false,
      responseBody: true,
      error: true,
    ));
  }

  return dio;
});

1.3 商品列表页:无限滚动 + 缓存优化

电商APP的首页商品列表是最核心的页面,需要实现无限滚动加载、下拉刷新、骨架屏和图片缓存。

// features/home/presentation/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../providers/home_provider.dart';
import '../widgets/product_card.dart';
import '../../../../shared/widgets/loading_indicator.dart';

class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});

  
  ConsumerState<HomePage> createState() => _HomePageState();
}

class _HomePageState extends ConsumerState<HomePage> {
  final RefreshController _refreshController = RefreshController();
  final ScrollController _scrollController = ScrollController();

  
  void initState() {
    super.initState();
    // 首次加载
    Future.microtask(() => ref.read(homeProvider.notifier).loadProducts());
  }

  
  void dispose() {
    _refreshController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final state = ref.watch(homeProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('商城'),
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => _navigateToSearch(),
          ),
          IconButton(
            icon: const Badge(
              label: Text('3'),
              child: Icon(Icons.shopping_cart_outlined),
            ),
            onPressed: () => _navigateToCart(),
          ),
        ],
      ),
      body: SmartRefresher(
        controller: _refreshController,
        enablePullDown: true,
        enablePullUp: true,
        onRefresh: () async {
          await ref.read(homeProvider.notifier).refreshProducts();
          _refreshController.refreshCompleted();
        },
        onLoading: () async {
          final hasMore = await ref.read(homeProvider.notifier).loadMore();
          if (!hasMore) {
            _refreshController.loadNoData();
          } else {
            _refreshController.loadComplete();
          }
        },
        child: state.when(
          loading: () => _buildSkeletonList(),
          error: (error, stack) => _buildErrorView(error),
          data: (products) => CustomScrollView(
            slivers: [
              // 顶部 Banner
              SliverToBoxAdapter(
                child: _buildBanner(),
              ),
              // 分类导航
              SliverToBoxAdapter(
                child: _buildCategoryNav(),
              ),
              // 商品瀑布流
              SliverPadding(
                padding: const EdgeInsets.all(12),
                sliver: SliverGrid(
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    mainAxisSpacing: 12,
                    crossAxisSpacing: 12,
                    childAspectRatio: 0.68,
                  ),
                  delegate: SliverChildBuilderDelegate(
                    (context, index) => ProductCard(
                      product: products[index],
                      onTap: () => _navigateToDetail(products[index]),
                    ),
                    childCount: products.length,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildSkeletonList() {
    return GridView.builder(
      padding: const EdgeInsets.all(12),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        childAspectRatio: 0.68,
      ),
      itemCount: 6,
      itemBuilder: (context, index) => const ProductCardSkeleton(),
    );
  }
}

1.4 商品详情页:复杂交互与动画

商品详情页包含图片轮播、规格选择、加入购物车飞入动画。

// features/product_detail/presentation/pages/product_detail_page.dart
class ProductDetailPage extends ConsumerStatefulWidget {
  final String productId;
  const ProductDetailPage({super.key, required this.productId});

  
  ConsumerState<ProductDetailPage> createState() => _ProductDetailPageState();
}

class _ProductDetailPageState extends ConsumerState<ProductDetailPage>
    with TickerProviderStateMixin {
  late PageController _imageController;
  late AnimationController _flyController;
  int _currentImageIndex = 0;
  String? _selectedSku;

  
  void initState() {
    super.initState();
    _imageController = PageController();
    _flyController = AnimationController(
      duration: const Duration(milliseconds: 600),
      vsync: this,
    );
    Future.microtask(
      () => ref.read(productDetailProvider(widget.productId).notifier).load(),
    );
  }

  
  Widget build(BuildContext context) {
    final state = ref.watch(productDetailProvider(widget.productId));

    return Scaffold(
      body: state.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, _) => Center(child: Text('加载失败: $e')),
        data: (product) => Stack(
          children: [
            CustomScrollView(
              slivers: [
                // 图片轮播
                SliverAppBar(
                  expandedHeight: 400,
                  pinned: false,
                  flexibleSpace: FlexibleSpaceBar(
                    background: PageView.builder(
                      controller: _imageController,
                      itemCount: product.images.length,
                      onPageChanged: (i) =>
                          setState(() => _currentImageIndex = i),
                      itemBuilder: (context, index) => CachedNetworkImage(
                        imageUrl: product.images[index],
                        fit: BoxFit.cover,
                      ),
                    ),
                  ),
                ),
                // 商品信息
                SliverToBoxAdapter(
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        // 价格
                        Row(
                          crossAxisAlignment: CrossAxisAlignment.end,
                          children: [
                            Text(
                              ${product.price.toStringAsFixed(2)}',
                              style: const TextStyle(
                                fontSize: 28,
                                fontWeight: FontWeight.bold,
                                color: Colors.red,
                              ),
                            ),
                            const SizedBox(width: 8),
                            Text(
                              ${product.originalPrice.toStringAsFixed(2)}',
                              style: TextStyle(
                                fontSize: 14,
                                color: Colors.grey[400],
                                decoration: TextDecoration.lineThrough,
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 8),
                        Text(
                          product.title,
                          style: const TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 16),
                        // SKU 选择
                        _buildSkuSelector(product.skus),
                      ],
                    ),
                  ),
                ),
                // 商品详情图
                SliverToBoxAdapter(
                  child: Column(
                    children: product.detailImages.map((url) =>
                      CachedNetworkImage(imageUrl: url, fit: BoxFit.width),
                    ).toList(),
                  ),
                ),
              ],
            ),
            // 底部操作栏
            Positioned(
              bottom: 0,
              left: 0,
              right: 0,
              child: _buildBottomBar(product),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBottomBar(Product product) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        children: [
          _buildActionButton(Icons.favorite_border, '收藏'),
          _buildActionButton(Icons.shopping_cart_outlined, '购物车'),
          const SizedBox(width: 8),
          Expanded(
            child: ElevatedButton(
              onPressed: () => _addToCart(product),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.orange,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 14),
                shape: const RoundedRectangleBorder(
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(20),
                    bottomLeft: Radius.circular(20),
                  ),
                ),
              ),
              child: const Text('加入购物车'),
            ),
          ),
          Expanded(
            child: ElevatedButton(
              onPressed: () => _buyNow(product),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.red,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 14),
                shape: const RoundedRectangleBorder(
                  borderRadius: BorderRadius.only(
                    topRight: Radius.circular(20),
                    bottomRight: Radius.circular(20),
                  ),
                ),
              ),
              child: const Text('立即购买'),
            ),
          ),
        ],
      ),
    );
  }
}

1.5 Riverpod 状态管理:购物车逻辑

购物车是电商APP最复杂的状态模块——需要跨页面同步、持久化存储、数量增减、选中状态管理。

// features/cart/presentation/providers/cart_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/cart_item.dart';

// 购物车状态
class CartState {
  final List<CartItem> items;
  final bool isLoading;
  final String? error;

  const CartState({
    this.items = const [],
    this.isLoading = false,
    this.error,
  });

  // 计算属性:选中商品
  List<CartItem> get selectedItems =>
      items.where((item) => item.isSelected).toList();

  // 计算属性:总价
  double get totalPrice =>
      selectedItems.fold(0, (sum, item) => sum + item.price * item.quantity);

  // 计算属性:总数量
  int get totalCount =>
      items.fold(0, (sum, item) => sum + item.quantity);

  // 计算属性:是否全选
  bool get isAllSelected => items.isNotEmpty && items.every((i) => i.isSelected);

  CartState copyWith({
    List<CartItem>? items,
    bool? isLoading,
    String? error,
  }) {
    return CartState(
      items: items ?? this.items,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

// 购物车 Notifier
class CartNotifier extends StateNotifier<CartState> {
  final Ref _ref;

  CartNotifier(this._ref) : super(const CartState());

  // 添加商品到购物车
  Future<void> addToCart(CartItem item) async {
    state = state.copyWith(isLoading: true);

    try {
      final existingIndex = state.items.indexWhere(
        (i) => i.productId == item.productId && i.skuId == item.skuId,
      );

      List<CartItem> newItems;
      if (existingIndex >= 0) {
        // 已存在,数量增加
        newItems = List.from(state.items);
        final existing = newItems[existingIndex];
        newItems[existingIndex] = existing.copyWith(
          quantity: existing.quantity + item.quantity,
        );
      } else {
        // 新商品
        newItems = [...state.items, item];
      }

      state = CartState(items: newItems);
      await _saveToStorage(newItems); // 持久化
    } catch (e) {
      state = state.copyWith(isLoading: false, error: e.toString());
    }
  }

  // 更新数量
  void updateQuantity(String productId, String skuId, int quantity) {
    if (quantity <= 0) {
      removeFromCart(productId, skuId);
      return;
    }

    final newItems = state.items.map((item) {
      if (item.productId == productId && item.skuId == skuId) {
        return item.copyWith(quantity: quantity);
      }
      return item;
    }).toList();

    state = CartState(items: newItems);
    _saveToStorage(newItems);
  }

  // 切换选中状态
  void toggleSelection(String productId, String skuId) {
    final newItems = state.items.map((item) {
      if (item.productId == productId && item.skuId == skuId) {
        return item.copyWith(isSelected: !item.isSelected);
      }
      return item;
    }).toList();

    state = CartState(items: newItems);
  }

  // 全选/取消全选
  void toggleSelectAll() {
    final targetState = !state.isAllSelected;
    final newItems = state.items
        .map((item) => item.copyWith(isSelected: targetState))
        .toList();

    state = CartState(items: newItems);
  }

  // 移除商品
  void removeFromCart(String productId, String skuId) {
    final newItems = state.items
        .where((item) => !(item.productId == productId && item.skuId == skuId))
        .toList();

    state = CartState(items: newItems);
    _saveToStorage(newItems);
  }

  // 清空购物车
  void clearCart() {
    state = const CartState();
    _clearStorage();
  }

  Future<void> _saveToStorage(List<CartItem> items) async {
    // 使用 SharedPreferences / Hive 持久化
  }

  Future<void> _clearStorage() async {
    // 清除本地存储
  }
}

// Provider 定义
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
  return CartNotifier(ref);
});

// 派生 Provider:购物车总价
final cartTotalPriceProvider = Provider<double>((ref) {
  return ref.watch(cartProvider).totalPrice;
});

// 派生 Provider:选中商品数量
final cartSelectedCountProvider = Provider<int>((ref) {
  return ref.watch(cartProvider).selectedItems.length;
});

二、React Native 社交应用实战

2.1 项目架构与技术栈

社交应用的核心特征:实时消息推送、富文本内容流、用户关系图谱、大量图片视频处理。技术栈选型如下:

模块 技术选型 选型理由
导航 React Navigation 7 社交应用需要嵌套Tab+Stack导航
状态管理 Zustand + TanStack Query Zustand 管理UI状态,Query 管理服务端状态
实时通信 Socket.IO Client IM消息实时推送
本地存储 MMKV 替代 AsyncStorage,性能提升100倍
图片处理 Expo Image 自动缓存、占位图、渐进加载
列表 FlashList 替代FlatList,性能提升5倍
src/
├── api/                     # API 层
│   ├── client.ts            # Axios 实例
│   ├── auth.ts              # 认证 API
│   ├── feed.ts              # 内容流 API
│   ├── message.ts           # 消息 API
│   └── user.ts              # 用户 API
├── store/                   # Zustand 状态
│   ├── authStore.ts         # 登录状态
│   ├── themeStore.ts        # 主题切换
│   └── chatStore.ts         # 聊天状态
├── hooks/                   # 自定义 Hooks
│   ├── useFeed.ts           # 内容流 Hook
│   ├── useChat.ts           # 聊天 Hook
│   └── usePushNotification.ts
├── components/              # UI 组件
│   ├── Feed/
│   │   ├── FeedCard.tsx
│   │   ├── FeedList.tsx
│   │   └── FeedSkeleton.tsx
│   ├── Chat/
│   │   ├── MessageBubble.tsx
│   │   └── ChatInput.tsx
│   └── common/
│       ├── Avatar.tsx
│       └── ImageViewer.tsx
├── screens/                 # 页面
│   ├── auth/
│   ├── home/
│   ├── chat/
│   └── profile/
├── navigation/              # 导航配置
│   └── AppNavigator.tsx
├── theme/                   # 主题
│   ├── colors.ts
│   └── spacing.ts
└── utils/                   # 工具函数
    ├── format.ts
    └── permissions.ts

2.2 内容流:TanStack Query + FlashList

社交应用的信息流是最核心的页面,需要处理分页加载、乐观更新、缓存策略。

// src/hooks/useFeed.ts
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchFeed, likePost, unlikePost } from '../api/feed';
import { Alert } from 'react-native';

export function useFeed() {
  const queryClient = useQueryClient();

  // 无限滚动查询
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isRefetching,
    refetch,
  } = useInfiniteQuery({
    queryKey: ['feed'],
    queryFn: ({ pageParam = 1 }) => fetchFeed(pageParam, 10),
    getNextPageParam: (lastPage) =>
      lastPage.hasMore ? lastPage.nextPage : undefined,
    initialPageParam: 1,
    // 缓存 5 分钟
    staleTime: 5 * 60 * 1000,
    // 保持上次数据,避免加载闪烁
    placeholderData: (prev) => prev,
  });

  // 点赞 Mutation(乐观更新)
  const likeMutation = useMutation({
    mutationFn: async (postId: string) => {
      // 先在本地找到帖子状态决定是 like 还是 unlike
      const cached = queryClient.getQueryData<any>(['feed']);
      let isLiked = false;

      if (cached?.pages) {
        for (const page of cached.pages) {
          const post = page.posts.find((p: any) => p.id === postId);
          if (post) { isLiked = post.isLiked; break; }
        }
      }

      return isLiked ? unlikePost(postId) : likePost(postId);
    },
    onMutate: async (postId: string) => {
      // 取消正在进行的查询,防止覆盖乐观更新
      await queryClient.cancelQueries({ queryKey: ['feed'] });

      // 快照当前数据
      const previousData = queryClient.getQueryData(['feed']);

      // 乐观更新:立即在 UI 上反映点赞
      queryClient.setQueryData(['feed'], (old: any) => {
        if (!old) return old;
        return {
          ...old,
          pages: old.pages.map((page: any) => ({
            ...page,
            posts: page.posts.map((post: any) => {
              if (post.id === postId) {
                return {
                  ...post,
                  isLiked: !post.isLiked,
                  likeCount: post.isLiked
                    ? post.likeCount - 1
                    : post.likeCount + 1,
                };
              }
              return post;
            }),
          })),
        };
      });

      return { previousData };
    },
    onError: (err, postId, context) => {
      // 回滚到之前的状态
      if (context?.previousData) {
        queryClient.setQueryData(['feed'], context.previousData);
      }
      Alert.alert('操作失败', '请稍后重试');
    },
    onSettled: () => {
      // 重新验证数据
      queryClient.invalidateQueries({ queryKey: ['feed'] });
    },
  });

  return {
    feed: data?.pages.flatMap((page) => page.posts) ?? [],
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isRefetching,
    refetch,
    toggleLike: likeMutation.mutate,
  };
}

2.3 实时聊天:Socket.IO + 消息状态管理

社交应用的即时通讯模块需要处理连接状态、消息收发、离线消息、已读回执。

// src/store/chatStore.ts
import { create } from 'zustand';
import { io, Socket } from 'socket.io-client';

interface Message {
  id: string;
  conversationId: string;
  senderId: string;
  content: string;
  type: 'text' | 'image' | 'voice';
  status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
  createdAt: number;
}

interface Conversation {
  id: string;
  participantId: string;
  participantName: string;
  participantAvatar: string;
  lastMessage: Message | null;
  unreadCount: number;
}

interface ChatState {
  socket: Socket | null;
  isConnected: boolean;
  conversations: Conversation[];
  messages: Record<string, Message[]>; // conversationId -> messages
  typingUsers: Record<string, boolean>; // conversationId -> isTyping

  connect: (token: string) => void;
  disconnect: () => void;
  sendMessage: (conversationId: string, content: string) => void;
  markAsRead: (conversationId: string) => void;
  setTyping: (conversationId: string, isTyping: boolean) => void;
}

export const useChatStore = create<ChatState>((set, get) => ({
  socket: null,
  isConnected: false,
  conversations: [],
  messages: {},
  typingUsers: {},

  connect: (token: string) => {
    const socket = io('https://api.yoursocialapp.com', {
      auth: { token },
      transports: ['websocket'],
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionAttempts: 5,
    });

    socket.on('connect', () => {
      set({ isConnected: true });
    });

    socket.on('disconnect', () => {
      set({ isConnected: false });
    });

    // 收到新消息
    socket.on('message:new', (message: Message) => {
      const { messages } = get();
      const convMessages = messages[message.conversationId] || [];

      set({
        messages: {
          ...messages,
          [message.conversationId]: [...convMessages, message],
        },
      });

      // 更新会话列表的最后一条消息
      const { conversations } = get();
      set({
        conversations: conversations.map((conv) =>
          conv.id === message.conversationId
            ? { ...conv, lastMessage: message, unreadCount: conv.unreadCount + 1 }
            : conv
        ),
      });
    });

    // 消息状态更新(已送达/已读)
    socket.on('message:status', ({ messageId, status }) => {
      const { messages } = get();
      const updated = { ...messages };

      for (const convId of Object.keys(updated)) {
        updated[convId] = updated[convId].map((msg) =>
          msg.id === messageId ? { ...msg, status } : msg
        );
      }
      set({ messages: updated });
    });

    // 对方正在输入
    socket.on('typing', ({ conversationId, userId, isTyping }) => {
      const { typingUsers } = get();
      set({
        typingUsers: { ...typingUsers, [conversationId]: isTyping },
      });
    });

    set({ socket });
  },

  disconnect: () => {
    const { socket } = get();
    if (socket) {
      socket.disconnect();
      set({ socket: null, isConnected: false });
    }
  },

  sendMessage: (conversationId: string, content: string) => {
    const { socket } = get();
    if (!socket || !socket.connected) return;

    const tempId = `temp_${Date.now()}`;
    const message: Message = {
      id: tempId,
      conversationId,
      senderId: 'me',
      content,
      type: 'text',
      status: 'sending',
      createdAt: Date.now(),
    };

    // 乐观更新:先在 UI 上显示
    const { messages } = get();
    const convMessages = messages[conversationId] || [];
    set({
      messages: {
        ...messages,
        [conversationId]: [...convMessages, message],
      },
    });

    // 发送到服务器
    socket.emit('message:send', { conversationId, content, tempId }, (response: any) => {
      // 服务器确认后,更新消息 ID 和状态
      const { messages } = get();
      const updated = { ...messages };

      if (updated[conversationId]) {
        updated[conversationId] = updated[conversationId].map((msg) =>
          msg.id === tempId
            ? { ...msg, id: response.messageId, status: 'sent' }
            : msg
        );
      }
      set({ messages: updated });
    });
  },

  markAsRead: (conversationId: string) => {
    const { socket, conversations } = get();
    if (socket) {
      socket.emit('message:read', { conversationId });
    }
    set({
      conversations: conversations.map((conv) =>
        conv.id === conversationId ? { ...conv, unreadCount: 0 } : conv
      ),
    });
  },

  setTyping: (conversationId: string, isTyping: boolean) => {
    const { socket } = get();
    if (socket) {
      socket.emit('typing', { conversationId, isTyping });
    }
  },
}));

2.4 聊天页面 UI

// src/screens/chat/ChatScreen.tsx
import React, { useCallback, useRef, useState } from 'react';
import {
  View,
  TextInput,
  TouchableOpacity,
  KeyboardAvoidingView,
  Platform,
  FlatList,
} from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useChatStore } from '../../store/chatStore';
import { MessageBubble } from '../../components/Chat/MessageBubble';
import { ChatInput } from '../../components/Chat/ChatInput';

interface Props {
  route: { params: { conversationId: string; title: string } };
}

export const ChatScreen: React.FC<Props> = ({ route }) => {
  const { conversationId } = route.params;
  const [inputText, setInputText] = useState('');
  const listRef = useRef<FlashList<any>>(null);

  const messages = useChatStore((s) => s.messages[conversationId] || []);
  const isConnected = useChatStore((s) => s.isConnected);
  const sendMessage = useChatStore((s) => s.sendMessage);
  const markAsRead = useChatStore((s) => s.markAsRead);
  const setTyping = useChatStore((s) => s.setTyping);

  // 进入聊天时标记已读
  React.useEffect(() => {
    markAsRead(conversationId);
  }, [conversationId]);

  const handleSend = useCallback(() => {
    const text = inputText.trim();
    if (!text) return;
    sendMessage(conversationId, text);
    setInputText('');
  }, [inputText, conversationId, sendMessage]);

  const handleTyping = useCallback(
    (text: string) => {
      setInputText(text);
      setTyping(conversationId, text.length > 0);
    },
    [conversationId, setTyping]
  );

  return (
    <KeyboardAvoidingView
      style={{ flex: 1, backgroundColor: '#f5f5f5' }}
      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
    >
      {/* 消息列表 */}
      <FlashList
        ref={listRef}
        data={messages}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <MessageBubble
            message={item}
            isOwn={item.senderId === 'me'}
          />
        )}
        inverted
        estimatedItemSize={80}
        contentContainerStyle={{ paddingVertical: 16 }}
        onEndReachedThreshold={0.5}
      />

      {/* 连接状态指示器 */}
      {!isConnected && (
        <View style={{
          paddingVertical: 6,
          backgroundColor: '#ff9800',
          alignItems: 'center',
        }}>
          <Text style={{ color: '#fff', fontSize: 12 }}>
            正在重新连接...
          </Text>
        </View>
      )}

      {/* 输入框 */}
      <ChatInput
        value={inputText}
        onChangeText={handleTyping}
        onSend={handleSend}
        onAttach={() => {}}
      />
    </KeyboardAvoidingView>
  );
};

三、状态管理方案深度对比

3.1 Flutter 状态管理选型

方案 适用场景 学习曲线 性能 推荐度
Riverpod 中大型项目 中等 ★★★★★
BLoC 企业级项目 陡峭 ★★★★
GetX 快速原型 ★★★
Provider 小型项目 ★★★

Riverpod 2.0 相比 Provider 的核心优势是编译时安全、无 BuildContext 依赖、自动释放。在电商项目中,购物车状态需要跨多个页面共享(首页角标、详情页加购、购物车页结算),Riverpod 的全局 Provider 完美解决。

// Riverpod 推荐的代码生成模式
// pubspec.yaml: dependencies: riverpod_annotation, dev: riverpod_generator, build_runner

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'cart_provider.g.dart';


class Cart extends _$Cart {
  
  List<CartItem> build() => [];

  void add(CartItem item) {
    state = [...state, item];
  }

  void remove(String id) {
    state = state.where((item) => item.id != id).toList();
  }
}

// 使用时:ref.watch(cartProvider) 即可
// 无需手动编写 Provider,代码生成器自动处理

3.2 React Native 状态管理选型

方案 适用场景 学习曲线 Bundle 大小 推荐度
Zustand + TanStack Query 大多数应用 ★★★★★
Redux Toolkit 大型企业项目 ★★★★
Jotai 原子化状态 ★★★★
MobX 响应式风格 ★★★

社交应用的推荐组合是 Zustand + TanStack Query,原因:

  • Zustand 管理客户端状态(UI 状态、WebSocket 连接、本地缓存),API 极简,一个 create 函数搞定
  • TanStack Query 管理服务端状态(API 数据缓存、自动重试、乐观更新),省去了手写 loading/error 处理
  • 两者职责清晰分离,不会像 Redux 那样将服务端状态和客户端状态混在一个 store 中
// Zustand + Immer 模式
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface UIState {
  theme: 'light' | 'dark';
  activeTab: string;
  setTheme: (theme: 'light' | 'dark') => void;
  setActiveTab: (tab: string) => void;
}

export const useUIStore = create<UIState>()(
  immer((set) => ({
    theme: 'light',
    activeTab: 'home',
    setTheme: (theme) =>
      set((state) => {
        state.theme = theme;
      }),
    setActiveTab: (tab) =>
      set((state) => {
        state.activeTab = tab;
      }),
  }))
);

四、可复用 UI 组件库设计

4.1 跨平台设计令牌系统

一套好的组件库从设计令牌(Design Token)开始,保证 Flutter 和 RN 两端视觉一致。

// Flutter: theme/design_tokens.dart
class DesignTokens {
  // 间距 (4pt grid system)
  static const double spacing4 = 4;
  static const double spacing8 = 8;
  static const double spacing12 = 12;
  static const double spacing16 = 16;
  static const double spacing24 = 24;
  static const double spacing32 = 32;
  static const double spacing48 = 48;

  // 圆角
  static const double radiusSm = 4;
  static const double radiusMd = 8;
  static const double radiusLg = 16;
  static const double radiusXl = 24;
  static const double radiusFull = 9999;

  // 字体大小
  static const double textXs = 11;
  static const double textSm = 13;
  static const double textBase = 15;
  static const double textLg = 17;
  static const double textXl = 20;
  static const double text2Xl = 24;
  static const double text3Xl = 30;

  // 颜色
  static const Color primary = Color(0xFF2563EB);
  static const Color primaryLight = Color(0xFFDBEAFE);
  static const Color success = Color(0xFF059669);
  static const Color warning = Color(0xFFF59E0B);
  static const Color error = Color(0xFFEF4444);

  // 阴影
  static List<BoxShadow> shadowSm = [
    BoxShadow(
      color: Colors.black.withOpacity(0.04),
      blurRadius: 4,
      offset: const Offset(0, 1),
    ),
  ];
  static List<BoxShadow> shadowMd = [
    BoxShadow(
      color: Colors.black.withOpacity(0.08),
      blurRadius: 8,
      offset: const Offset(0, 2),
    ),
  ];
}
// React Native: theme/tokens.ts
export const tokens = {
  spacing: {
    4: 4, 8: 8, 12: 12, 16: 16, 24: 24, 32: 32, 48: 48,
  },
  radius: {
    sm: 4, md: 8, lg: 16, xl: 24, full: 9999,
  },
  fontSize: {
    xs: 11, sm: 13, base: 15, lg: 17, xl: 20, '2xl': 24, '3xl': 30,
  },
  colors: {
    primary: '#2563EB',
    primaryLight: '#DBEAFE',
    success: '#059669',
    warning: '#F59E0B',
    error: '#EF4444',
    text: '#1A1D27',
    textMuted: '#6B7080',
    bg: '#FFFFFF',
    bg2: '#F6F7F9',
    border: '#E2E4EA',
  },
  shadows: {
    sm: {
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 1 },
      shadowOpacity: 0.04,
      shadowRadius: 4,
      elevation: 1,
    },
    md: {
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.08,
      shadowRadius: 8,
      elevation: 3,
    },
  },
} as const;

4.2 核心组件实现

以 Button 组件为例,展示 Flutter 和 RN 各自的组件封装模式:

// Flutter: shared/widgets/app_button.dart
enum AppButtonVariant { primary, secondary, outline, ghost, danger }
enum AppButtonSize { sm, md, lg }

class AppButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;
  final AppButtonVariant variant;
  final AppButtonSize size;
  final IconData? icon;
  final bool isLoading;
  final bool isFullWidth;

  const AppButton({
    super.key,
    required this.label,
    this.onPressed,
    this.variant = AppButtonVariant.primary,
    this.size = AppButtonSize.md,
    this.icon,
    this.isLoading = false,
    this.isFullWidth = false,
  });

  
  Widget build(BuildContext context) {
    final styles = _getStyles();

    return SizedBox(
      width: isFullWidth ? double.infinity : null,
      height: _getHeight(),
      child: ElevatedButton(
        onPressed: isLoading ? null : onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: styles.backgroundColor,
          foregroundColor: styles.foregroundColor,
          elevation: 0,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(DesignTokens.radiusMd),
            side: styles.borderSide,
          ),
        ),
        child: isLoading
            ? SizedBox(
                width: 20, height: 20,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                  color: styles.foregroundColor,
                ),
              )
            : Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (icon != null) ...[
                    Icon(icon, size: _getIconSize()),
                    const SizedBox(width: 8),
                  ],
                  Text(label, style: TextStyle(fontSize: _getFontSize())),
                ],
              ),
      ),
    );
  }

  _ButtonStyles _getStyles() {
    switch (variant) {
      case AppButtonVariant.primary:
        return _ButtonStyles(
          backgroundColor: DesignTokens.primary,
          foregroundColor: Colors.white,
          borderSide: BorderSide.none,
        );
      case AppButtonVariant.outline:
        return _ButtonStyles(
          backgroundColor: Colors.transparent,
          foregroundColor: DesignTokens.primary,
          borderSide: const BorderSide(color: DesignTokens.primary),
        );
      case AppButtonVariant.ghost:
        return _ButtonStyles(
          backgroundColor: Colors.transparent,
          foregroundColor: DesignTokens.primary,
          borderSide: BorderSide.none,
        );
      case AppButtonVariant.danger:
        return _ButtonStyles(
          backgroundColor: DesignTokens.error,
          foregroundColor: Colors.white,
          borderSide: BorderSide.none,
        );
      default:
        return _ButtonStyles(
          backgroundColor: DesignTokens.primaryLight,
          foregroundColor: DesignTokens.primary,
          borderSide: BorderSide.none,
        );
    }
  }

  double _getHeight() {
    switch (size) {
      case AppButtonSize.sm: return 36;
      case AppButtonSize.lg: return 52;
      default: return 44;
    }
  }

  double _getFontSize() {
    switch (size) {
      case AppButtonSize.sm: return DesignTokens.textSm;
      case AppButtonSize.lg: return DesignTokens.textLg;
      default: return DesignTokens.textBase;
    }
  }

  double _getIconSize() {
    return size == AppButtonSize.sm ? 16 : 20;
  }
}

class _ButtonStyles {
  final Color backgroundColor;
  final Color foregroundColor;
  final BorderSide borderSide;
  _ButtonStyles({
    required this.backgroundColor,
    required this.foregroundColor,
    required this.borderSide,
  });
}
// React Native: components/common/Button.tsx
import React from 'react';
import {
  TouchableOpacity,
  Text,
  ActivityIndicator,
  View,
  StyleSheet,
} from 'react-native';
import { tokens } from '../../theme/tokens';

type Variant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
type Size = 'sm' | 'md' | 'lg';

interface Props {
  label: string;
  onPress?: () => void;
  variant?: Variant;
  size?: Size;
  icon?: React.ReactNode;
  loading?: boolean;
  disabled?: boolean;
  fullWidth?: boolean;
}

export const Button: React.FC<Props> = ({
  label,
  onPress,
  variant = 'primary',
  size = 'md',
  icon,
  loading = false,
  disabled = false,
  fullWidth = false,
}) => {
  const variantStyles = getVariantStyles(variant);
  const sizeStyles = getSizeStyles(size);

  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={disabled || loading}
      style={[
        styles.base,
        sizeStyles.container,
        variantStyles.container,
        fullWidth && { width: '100%' },
        (disabled || loading) && { opacity: 0.5 },
      ]}
      activeOpacity={0.7}
    >
      {loading ? (
        <ActivityIndicator
          size="small"
          color={variantStyles.textColor}
        />
      ) : (
        <View style={styles.content}>
          {icon && <View style={styles.iconWrapper}>{icon}</View>}
          <Text
            style={[
              sizeStyles.text,
              { color: variantStyles.textColor },
            ]}
          >
            {label}
          </Text>
        </View>
      )}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  base: {
    borderRadius: tokens.radius.md,
    alignItems: 'center',
    justifyContent: 'center',
  },
  content: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  iconWrapper: {
    marginRight: tokens.spacing[2],
  },
});

function getVariantStyles(variant: Variant) {
  const map = {
    primary: { container: { backgroundColor: tokens.colors.primary }, textColor: '#fff' },
    secondary: { container: { backgroundColor: tokens.colors.primaryLight }, textColor: tokens.colors.primary },
    outline: { container: { backgroundColor: 'transparent', borderWidth: 1, borderColor: tokens.colors.primary }, textColor: tokens.colors.primary },
    ghost: { container: { backgroundColor: 'transparent' }, textColor: tokens.colors.primary },
    danger: { container: { backgroundColor: tokens.colors.error }, textColor: '#fff' },
  };
  return map[variant];
}

function getSizeStyles(size: Size) {
  const map = {
    sm: { container: { height: 36, paddingHorizontal: 12 }, text: { fontSize: tokens.fontSize.sm, fontWeight: '600' } },
    md: { container: { height: 44, paddingHorizontal: 16 }, text: { fontSize: tokens.fontSize.base, fontWeight: '600' } },
    lg: { container: { height: 52, paddingHorizontal: 24 }, text: { fontSize: tokens.fontSize.lg, fontWeight: '700' } },
  };
  return map[size];
}

五、性能优化清单

Flutter 性能优化

优化项 方法 效果
列表性能 使用 ListView.builder / SliverGrid 替代 Column 避免一次性渲染所有子组件
图片缓存 cached_network_image + 占位图 避免重复下载,减少白屏
状态隔离 Riverpod select 只监听需要的变化 避免不必要的 rebuild
const 构造器 所有静态 Widget 加 const 编译期常量,跳过 rebuild
路由动画 使用 Hero 动画共享元素过渡 60fps 页面切换体验
避免抖动 RepaintBoundary 隔离复杂动画 独立图层,减少重绘范围
// Riverpod select 示例:只监听购物车数量变化
final cartCount = ref.watch(
  cartProvider.select((state) => state.items.length),
);
// 只有数量变化时才 rebuild,价格变化不触发

React Native 性能优化

优化项 方法 效果
列表性能 FlashList 替代 FlatList 回收复用,内存降低 60%
图片优化 Expo Image 自动缓存 + 渐进加载 减少 50% 图片白屏时间
重渲染 React.memo + useMemo + useCallback 避免不必要渲染
动画 Reanimated 3 在 UI 线程执行 60fps 复杂手势动画
Bundle Hermes 引擎 + ProGuard 包体积减少 30%,启动快 40%
内存 及时清理 setInterval / 监听器 避免内存泄漏
// React.memo + useMemo 性能优化示例
const FeedCard = React.memo(({ post, onLike }: Props) => {
  // 只在 post 数据变化时重新渲染
  const formattedTime = useMemo(
    () => formatRelativeTime(post.createdAt),
    [post.createdAt]
  );

  const handleLike = useCallback(() => {
    onLike(post.id);
  }, [post.id, onLike]);

  return (
    <View>
      <Text>{post.content}</Text>
      <Text>{formattedTime}</Text>
      <Button label="赞" onPress={handleLike} />
    </View>
  );
}, (prevProps, nextProps) => {
  // 自定义比较函数:只在关键数据变化时重渲染
  return (
    prevProps.post.id === nextProps.post.id &&
    prevProps.post.likeCount === nextProps.post.likeCount &&
    prevProps.post.isLiked === nextProps.post.isLiked
  );
});

总结

维度 Flutter React Native
电商APP适配度 优(高性能列表、自定义渲染) 良(依赖原生模块优化)
社交应用适配度 良(Socket.IO需桥接) 优(生态丰富、实时通信成熟)
状态管理推荐 Riverpod 2.0(代码生成) Zustand + TanStack Query
组件库复用 Design Token + Widget 封装 Design Token + 组件封装
性能关键 const 构造器 + RepaintBoundary React.memo + FlashList

跨平台框架的选型没有银弹——Flutter 在高性能渲染和一致性上更强,React Native 在生态和原生模块集成上更灵活。理解它们的架构差异,根据项目实际需求做选型,比追逐"哪个更好"重要得多。

本系列共三篇文章,本文为第一篇。第二篇将深入 Android Jetpack 和 iOS Core Animation/Core ML,第三篇将覆盖上架全流程与运营实战。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐