跨平台移动应用开发实战:Flutter电商APP + React Native社交应用 + UI组件库与状态管理全方案
本文从零到一拆解两个完整的跨平台项目——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,第三篇将覆盖上架全流程与运营实战。
更多推荐


所有评论(0)