如何在Flutter与Redux中实现登录流程的进度指示器?
Hey there! Let's work through implementing that login flow with Flutter Redux step by step—since you've already built counter, Firebase Auth, and Cloud Firestore examples with Redux, this should slot right into what you already know.
1. 先定义Redux状态与Action
First, we need to model our app's login-related state: whether we're loading, if the user is logged in, and any error messages from failed attempts.
State Class
class AppState { final bool isLoading; final bool isLoggedIn; final String? authError; AppState({ required this.isLoading, required this.isLoggedIn, this.authError, }); // Initial state when the app starts static AppState initial() => AppState(isLoading: false, isLoggedIn: false); // Copy method to safely update state (Redux requires immutable state) AppState copyWith({ bool? isLoading, bool? isLoggedIn, String? authError, }) { return AppState( isLoading: isLoading ?? this.isLoading, isLoggedIn: isLoggedIn ?? this.isLoggedIn, authError: authError ?? this.authError, ); } }
Action Classes
These actions trigger state changes in our reducer:
// Trigger login with user credentials class LoginAction { final String email; final String password; LoginAction(this.email, this.password); } // Mark login as successful class LoginSuccessAction {} // Mark login as failed with an error message class LoginFailureAction { final String error; LoginFailureAction(this.error); } // Trigger logout class LogoutAction {}
2. 编写Reducer处理状态更新
The reducer takes the current state and an action, then returns a new state based on what action was dispatched:
AppState appReducer(AppState state, dynamic action) { if (action is LoginAction) { // Start loading when login is initiated, clear any previous errors return state.copyWith(isLoading: true, authError: null); } else if (action is LoginSuccessAction) { // Stop loading and mark user as logged in return state.copyWith(isLoading: false, isLoggedIn: true); } else if (action is LoginFailureAction) { // Stop loading, reset login state, and save the error return state.copyWith(isLoading: false, isLoggedIn: false, authError: action.error); } else if (action is LogoutAction) { // Reset state to initial when user logs out return AppState.initial(); } // Return current state if action isn't recognized return state; }
3. 实现中间件处理异步API调用
Middleware is where we handle async operations like API calls. This example uses a mock API—replace it with your real login endpoint:
Middleware<AppState> createLoginMiddleware() { return (Store<AppState> store, dynamic action, NextDispatcher next) async { if (action is LoginAction) { try { // Simulate API delay (remove this in production) await Future.delayed(const Duration(seconds: 2)); // Replace this with your actual login API call // final response = await http.post( // Uri.parse('your-login-api-url'), // body: {'email': action.email, 'password': action.password} // ); // Dispatch success action if login works store.dispatch(LoginSuccessAction()); } catch (e) { // Dispatch failure action with error message if something goes wrong store.dispatch(LoginFailureAction(e.toString())); } } next(action); }; }
4. UI组件:根据状态动态切换按钮
Use StoreConnector to connect your UI to the Redux store, so it updates automatically when state changes:
class LoginPage extends StatelessWidget { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Redux Login Demo')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, ), TextField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), obscureText: true, ), const SizedBox(height: 20), // Connect to Redux store StoreConnector<AppState, _ViewModel>( converter: (store) => _ViewModel( isLoading: store.state.isLoading, isLoggedIn: store.state.isLoggedIn, authError: store.state.authError, onLogin: (email, password) => store.dispatch(LoginAction(email, password)), onLogout: () => store.dispatch(LogoutAction()), ), builder: (context, viewModel) { // Show error message if login failed if (viewModel.authError != null) { return Text( viewModel.authError!, style: const TextStyle(color: Colors.red, fontSize: 14), ); } // Render correct widget based on state if (viewModel.isLoading) { return const CircularProgressIndicator(); } else if (viewModel.isLoggedIn) { return ElevatedButton( onPressed: viewModel.onLogout, child: const Text('Logout'), ); } else { return ElevatedButton( onPressed: () { viewModel.onLogin( _emailController.text.trim(), _passwordController.text.trim(), ); }, child: const Text('Login'), ); } }, ), ], ), ), ); } } // ViewModel to pass only needed state and callbacks to the UI class _ViewModel { final bool isLoading; final bool isLoggedIn; final String? authError; final Function(String, String) onLogin; final VoidCallback onLogout; _ViewModel({ required this.isLoading, required this.isLoggedIn, required this.authError, required this.onLogin, required this.onLogout, }); }
5. 初始化Store并注入到应用
Finally, set up the Redux store in your main function and wrap your app with StoreProvider:
void main() { final store = Store<AppState>( appReducer, initialState: AppState.initial(), middleware: [createLoginMiddleware()], ); runApp(StoreProvider( store: store, child: const MyApp(), )); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Redux Login', home: LoginPage(), ); } }
Common Troubleshooting Tips
- If the button doesn't update: Double-check that your reducer returns a new state instance using
copyWith(never modify the original state directly). - If the API call doesn't trigger: Ensure your middleware is added to the store's
middlewarelist. - If login success doesn't switch to logout button: Verify that
LoginSuccessActionis being dispatched correctly and that the reducer setsisLoggedIntotrue.
内容的提问来源于stack exchange,提问作者user9225839

