You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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.

实现Flutter Redux登录流程的完整方案

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 middleware list.
  • If login success doesn't switch to logout button: Verify that LoginSuccessAction is being dispatched correctly and that the reducer sets isLoggedIn to true.

内容的提问来源于stack exchange,提问作者user9225839

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:36:43