跳转到正文
YGJoon 技术博客

Flutter Redux 状态管理详解

发布于:
·1.3k 字 · 约 3 分钟·作者:
单向数据流那一套:Action 描述意图、Reducer 纯函数产出新状态,Store 集中管理,状态不可变靠 copyWith
Flutter Redux 状态管理详解文章封面
目录

简介

Redux 是一个可预测的状态容器,它基于单向数据流的原则,通过 Action、Reducer 和 Store 来管理应用状态。Flutter Redux 是 Redux 在 Flutter 中的实现。

基本概念

State

// 应用状态
class AppState {
  final int counter;
  final bool isLoading;
  
  AppState({
    this.counter = 0,
    this.isLoading = false,
  });
  
  AppState copyWith({
    int? counter,
    bool? isLoading,
  }) {
    return AppState(
      counter: counter ?? this.counter,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

Action

// 定义操作
class IncrementAction {}
class DecrementAction {}
class LoadDataAction {}
class LoadDataSuccessAction {}
class LoadDataFailureAction {
  final String error;
  LoadDataFailureAction(this.error);
}

Reducer

// 状态更新逻辑
AppState appReducer(AppState state, dynamic action) {
  if (action is IncrementAction) {
    return state.copyWith(counter: state.counter + 1);
  }
  
  if (action is DecrementAction) {
    return state.copyWith(counter: state.counter - 1);
  }
  
  if (action is LoadDataAction) {
    return state.copyWith(isLoading: true);
  }
  
  if (action is LoadDataSuccessAction) {
    return state.copyWith(isLoading: false);
  }
  
  return state;
}

基本用法

创建 Store

void main() {
  final store = Store<AppState>(
    appReducer,
    initialState: AppState(),
    middleware: [thunkMiddleware],
  );
  
  runApp(
    StoreProvider(
      store: store,
      child: MyApp(),
    ),
  );
}

使用 Store

// 读取状态
StoreConnector<AppState, int>(
  converter: (store) => store.state.counter,
  builder: (context, counter) {
    return Text('Counter: $counter');
  },
);

// 发送 Action
StoreProvider.of<AppState>(context).dispatch(IncrementAction());

完整示例

// 用户状态
class UserState {
  final User? user;
  final bool isLoading;
  final String? error;
  
  UserState({
    this.user,
    this.isLoading = false,
    this.error,
  });
  
  UserState copyWith({
    User? user,
    bool? isLoading,
    String? error,
  }) {
    return UserState(
      user: user ?? this.user,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }
}

// 用户操作
class LoadUserAction {}
class LoadUserSuccessAction {
  final User user;
  LoadUserSuccessAction(this.user);
}
class LoadUserFailureAction {
  final String error;
  LoadUserFailureAction(this.error);
}
class UpdateUserAction {
  final String name;
  final int age;
  UpdateUserAction({required this.name, required this.age});
}

// 用户 Reducer
UserState userReducer(UserState state, dynamic action) {
  if (action is LoadUserAction) {
    return state.copyWith(isLoading: true, error: null);
  }
  
  if (action is LoadUserSuccessAction) {
    return state.copyWith(
      user: action.user,
      isLoading: false,
    );
  }
  
  if (action is LoadUserFailureAction) {
    return state.copyWith(
      error: action.error,
      isLoading: false,
    );
  }
  
  if (action is UpdateUserAction) {
    final updatedUser = state.user?.copyWith(
      name: action.name,
      age: action.age,
    );
    return state.copyWith(user: updatedUser);
  }
  
  return state;
}

// 异步 Action Creator
ThunkAction<UserState> loadUser() {
  return (Store<UserState> store) async {
    store.dispatch(LoadUserAction());
    
    try {
      final user = await UserRepository().fetchUser();
      store.dispatch(LoadUserSuccessAction(user));
    } catch (e) {
      store.dispatch(LoadUserFailureAction(e.toString()));
    }
  };
}

// 页面组件
class UserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Redux Demo')),
      body: StoreConnector<UserState, UserViewModel>(
        converter: (store) => UserViewModel(
          user: store.state.user,
          isLoading: store.state.isLoading,
          error: store.state.error,
          onLoadUser: () => store.dispatch(loadUser()),
          onUpdateUser: (name, age) => store.dispatch(
            UpdateUserAction(name: name, age: age),
          ),
        ),
        builder: (context, vm) {
          if (vm.isLoading) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (vm.error != null) {
            return Center(child: Text('Error: ${vm.error}'));
          }
          
          if (vm.user == null) {
            return Center(
              child: ElevatedButton(
                onPressed: vm.onLoadUser,
                child: Text('Load User'),
              ),
            );
          }
          
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Name: ${vm.user!.name}',
                  style: TextStyle(fontSize: 24),
                ),
                Text(
                  'Age: ${vm.user!.age}',
                  style: TextStyle(fontSize: 24),
                ),
                ElevatedButton(
                  onPressed: () => vm.onUpdateUser('John Doe', 30),
                  child: Text('Update User'),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

高级用法

中间件

Middleware<AppState> loggingMiddleware = (
  Store<AppState> store,
  dynamic action,
  NextDispatcher next,
) {
  print('Action: $action');
  next(action);
  print('State: ${store.state}');
};

选择器

class Selectors {
  static User? getUser(UserState state) => state.user;
  static bool isLoading(UserState state) => state.isLoading;
  static String? getError(UserState state) => state.error;
}

组合 Reducer

AppState appReducer(AppState state, dynamic action) {
  return AppState(
    user: userReducer(state.user, action),
    settings: settingsReducer(state.settings, action),
  );
}

最佳实践

  1. Reducer 必须是纯函数:同样输入同样输出,不调网络、不读时间、不改传入的 state(返回新对象)。反例:在 Reducer 里 http.get 再 emit——devtools 的 time-travel 会回放 reducer,每回放一次就发一次请求
  2. 异步走 middleware(redux_thunk / redux_epic):Action 只描述”发生了什么”,副作用全在 middleware 里做,reducer 只管”状态怎么变”
  3. state 做范式化:别嵌三层 user.orders[0].items,用 Map<id, Order> 扁平存;更新一条订单不用深拷贝整棵树
  4. Action 类名说人话:AddToCart 比 UpdateState 好;payload 字段显式声明,别塞个 dynamic data 万能包——三个月后你自己都看不懂

注意事项

  1. 高频 ephemeral 状态别进全局 store:表单输入中这种每敲一个键就变的状态也塞 store,每次按键全量走一遍 middleware+reducer;用局部 setState,提交时再 dispatch
  2. 别在 Reducer 里深拷贝整棵树:state 越大每次 dispatch 越慢;大列表分页存,reducer 里只拼 id 列表
  3. StoreBuilder 别包整页:整页一个 StoreBuilder 监听整个 state,任何 action 都 rebuild;拆小 StoreConnector,或开 distinct: true
  4. 选型提醒:状态只是”列表+表单+用户信息”就别上 Redux——样板代码量在 Flutter 里比前端还劝退,架构是为复杂度服务的(实战案例的结论)

实战案例

Redux(flutter_redux)在我的项目里出场率为零。原因:它是为”超复杂全局状态”设计的(想想前端 Redux 的 store+reducer+middleware 全家桶),而我的 App(电商、货运、旅行)状态无非是”列表数据+表单+用户信息”,GetX 的响应式变量完全够用。Redux 的样板代码量在 Flutter 里比在前端还劝退——每个 action 要写类,每个 reducer 要写 switch。我看过一个用 flutter_redux 的开源项目,状态管理代码占了三分之一行数,维护者自己都说”早知道用 GetX”。架构是为复杂度服务的,没有那个复杂度就别请那尊佛。

总结

Redux 提供了一种可预测的状态管理方案,通过严格的单向数据流和不可变状态,可以构建出更易维护的应用程序。理解并掌握 Redux 的使用对于开发大型 Flutter 应用很有帮助。


分享这篇文章:


相关文章

上一篇
Flutter SingleChildScrollView 组件详解
下一篇
Flutter Riverpod 状态管理详解