Flutter 完整的项目页面架构

我将为你展示一个企业级 Flutter 项目的完整页面架构设计,包含分层架构、状态管理、路由管理、组件化等最佳实践。

一、项目目录结构

lib/
├── main.dart                      # 应用入口
├── app/                           # 应用层
│   ├── config/                    # 应用配置
│   │   ├── app_config.dart        # 应用配置类
│   │   ├── environment.dart       # 环境配置
│   │   └── theme.dart             # 主题配置
│   ├── di/                        # 依赖注入
│   │   ├── injector.dart
│   │   └── service_locator.dart
│   └── bootstrap.dart             # 应用启动器
├── core/                          # 核心层
│   ├── constants/                 # 常量
│   ├── errors/                    # 错误处理
│   ├── extensions/                # 扩展方法
│   ├── utils/                     # 工具类
│   ├── widgets/                   # 核心组件
│   └── base/                      # 基类
│       ├── base_page.dart
│       └── base_viewmodel.dart
├── data/                          # 数据层
│   ├── models/                    # 数据模型
│   ├── datasources/               # 数据源
│   │   ├── local/
│   │   └── remote/
│   ├── repositories/              # 仓库实现
│   └── mapper/                    # 数据映射
├── domain/                        # 领域层
│   ├── entities/                  # 业务实体
│   ├── repositories/              # 仓库接口
│   └── usecases/                  # 用例
├── presentation/                  # 表现层
│   ├── pages/                     # 页面
│   │   ├── auth/                  # 认证模块
│   │   │   ├── login/
│   │   │   │   ├── login_page.dart
│   │   │   │   ├── login_viewmodel.dart
│   │   │   │   └── widgets/      # 页面专属组件
│   │   │   └── register/
│   │   ├── home/                  # 首页模块
│   │   ├── profile/               # 个人中心
│   │   └── ...
│   ├── widgets/                   # 共享组件
│   │   ├── common/
│   │   ├── forms/
│   │   └── dialogs/
│   └── state/                     # 状态管理
│       ├── providers/
│       ├── blocs/
│       └── cubits/
└── routes/                        # 路由管理
    ├── app_router.dart
    ├── route_names.dart
    └── route_transitions.dart

二、分层架构详解

  1. 应用层 (app/)
// lib/app/config/app_config.dart
class AppConfig {
  final String appName;
  final String baseUrl;
  final Environment environment;
  final bool enableLogging;

  AppConfig({
    required this.appName,
    required this.baseUrl,
    required this.environment,
    this.enableLogging = false,
  });

  static AppConfig dev() => AppConfig(
    appName: 'Flutter App Dev',
    baseUrl: 'https://api.dev.example.com',
    environment: Environment.development,
    enableLogging: true,
  );
}
  1. 核心层 (core/)
// lib/core/base/base_page.dart
abstract class BasePage<T extends BaseViewModel> extends StatefulWidget {
  const BasePage({Key? key}) : super(key: key);
  
  
  _BasePageState<T> createState() => _BasePageState<T>();
}

class _BasePageState<T extends BaseViewModel> extends State<BasePage<T>> {
  late T _viewModel;
  
  
  void initState() {
    super.initState();
    _viewModel = getIt<T>();
    _viewModel.initialize();
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: buildBody(context, _viewModel),
    );
  }
  
  Widget buildBody(BuildContext context, T viewModel);
}

三、页面架构实现

  1. 页面基类设计
// lib/core/base/base_view.dart
abstract class BaseView<T extends BaseViewModel> extends StatelessWidget {
  const BaseView({Key? key}) : super(key: key);

  
  T get viewModel;

  
  Widget buildBody(BuildContext context);

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<T>.value(
      value: viewModel,
      child: Consumer<T>(
        builder: (context, vm, child) {
          return Scaffold(
            appBar: buildAppBar(context, vm),
            body: buildBody(context),
            bottomNavigationBar: buildBottomBar(context, vm),
            floatingActionButton: buildFloatingActionButton(context, vm),
            drawer: buildDrawer(context, vm),
          );
        },
      ),
    );
  }

  // 可重写的组件方法
  PreferredSizeWidget? buildAppBar(BuildContext context, T vm) => null;
  Widget? buildBottomBar(BuildContext context, T vm) => null;
  Widget? buildFloatingActionButton(BuildContext context, T vm) => null;
  Widget? buildDrawer(BuildContext context, T vm) => null;
}
  1. ViewModel 基类
// lib/core/base/base_viewmodel.dart
abstract class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.idle;
  String? _errorMessage;
  
  ViewState get state => _state;
  String? get errorMessage => _errorMessage;
  bool get isLoading => _state == ViewState.loading;
  bool get hasError => _state == ViewState.error;
  
  void setState(ViewState newState) {
    _state = newState;
    notifyListeners();
  }
  
  void setError(String message) {
    _errorMessage = message;
    _state = ViewState.error;
    notifyListeners();
  }
  
  Future<void> initialize() async {}
  
  
  void dispose() {
    super.dispose();
  }
}

enum ViewState { idle, loading, success, error }

四、完整页面示例

  1. 登录页面实现
// lib/presentation/pages/auth/login/login_page.dart
class LoginPage extends BaseView<LoginViewModel> {
  const LoginPage({Key? key}) : super(key: key);

  
  LoginViewModel get viewModel => getIt<LoginViewModel>();

  
  Widget buildBody(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20.0),
      child: Form(
        key: viewModel.formKey,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _buildEmailField(),
            const SizedBox(height: 20),
            _buildPasswordField(),
            const SizedBox(height: 30),
            _buildLoginButton(context),
            if (viewModel.hasError) _buildErrorText(),
          ],
        ),
      ),
    );
  }

  Widget _buildEmailField() {
    return TextFormField(
      controller: viewModel.emailController,
      decoration: const InputDecoration(
        labelText: 'Email',
        prefixIcon: Icon(Icons.email),
        border: OutlineInputBorder(),
      ),
      validator: viewModel.validateEmail,
      keyboardType: TextInputType.emailAddress,
    );
  }

  Widget _buildPasswordField() {
    return TextFormField(
      controller: viewModel.passwordController,
      decoration: const InputDecoration(
        labelText: 'Password',
        prefixIcon: Icon(Icons.lock),
        border: OutlineInputBorder(),
      ),
      obscureText: true,
      validator: viewModel.validatePassword,
    );
  }

  Widget _buildLoginButton(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton(
        onPressed: viewModel.isLoading ? null : () => viewModel.login(context),
        child: viewModel.isLoading
            ? const CircularProgressIndicator()
            : const Text('Login'),
      ),
    );
  }

  Widget _buildErrorText() {
    return Padding(
      padding: const EdgeInsets.only(top: 16.0),
      child: Text(
        viewModel.errorMessage!,
        style: const TextStyle(color: Colors.red),
      ),
    );
  }

  
  AppBar? buildAppBar(BuildContext context, LoginViewModel vm) {
    return AppBar(
      title: const Text('Login'),
      centerTitle: true,
    );
  }
}
  1. Login ViewModel
// lib/presentation/pages/auth/login/login_viewmodel.dart
class LoginViewModel extends BaseViewModel {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  
  final LoginUseCase _loginUseCase;
  final SaveUserUseCase _saveUserUseCase;
  
  LoginViewModel(this._loginUseCase, this._saveUserUseCase);
  
  String? validateEmail(String? value) {
    if (value == null || value.isEmpty) {
      return 'Email is required';
    }
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
      return 'Enter a valid email';
    }
    return null;
  }
  
  String? validatePassword(String? value) {
    if (value == null || value.isEmpty) {
      return 'Password is required';
    }
    if (value.length < 6) {
      return 'Password must be at least 6 characters';
    }
    return null;
  }
  
  Future<void> login(BuildContext context) async {
    if (!formKey.currentState!.validate()) return;
    
    setState(ViewState.loading);
    
    try {
      final result = await _loginUseCase.execute(LoginParams(
        email: emailController.text,
        password: passwordController.text,
      ));
      
      await _saveUserUseCase.execute(result.user);
      
      setState(ViewState.success);
      
      // 导航到主页
      Navigator.of(context).pushNamedAndRemoveUntil(
        RouteNames.home,
        (route) => false,
      );
      
    } on ApiException catch (e) {
      setError(e.message);
    } catch (e) {
      setError('An unexpected error occurred');
    }
  }
  
  
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }
}

五、路由管理

  1. 路由配置
// lib/routes/app_router.dart
class AppRouter {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case RouteNames.splash:
        return MaterialPageRoute(builder: (_) => const SplashPage());
      case RouteNames.login:
        return MaterialPageRoute(builder: (_) => const LoginPage());
      case RouteNames.register:
        return MaterialPageRoute(builder: (_) => const RegisterPage());
      case RouteNames.home:
        return MaterialPageRoute(builder: (_) => const HomePage());
      case RouteNames.profile:
        return MaterialPageRoute(builder: (_) => const ProfilePage());
      case RouteNames.settings:
        return MaterialPageRoute(builder: (_) => const SettingsPage());
      default:
        return MaterialPageRoute(
          builder: (_) => Scaffold(
            body: Center(
              child: Text('No route defined for ${settings.name}'),
            ),
          ),
        );
    }
  }
  
  static Map<String, WidgetBuilder> routes = {
    RouteNames.splash: (context) => const SplashPage(),
    RouteNames.login: (context) => const LoginPage(),
    RouteNames.register: (context) => const RegisterPage(),
    RouteNames.home: (context) => const HomePage(),
    RouteNames.profile: (context) => const ProfilePage(),
  };
}
  1. 路由名称常量
// lib/routes/route_names.dart
class RouteNames {
  static const String splash = '/';
  static const String login = '/login';
  static const String register = '/register';
  static const String home = '/home';
  static const String profile = '/profile';
  static const String settings = '/settings';
  static const String productDetail = '/product/:id';
  
  static String productDetailPath(String id) => '/product/$id';
}

六、状态管理(Provider + Riverpod)

  1. 全局状态提供器
// lib/presentation/state/providers/app_provider.dart
final appProvider = ChangeNotifierProvider<AppViewModel>((ref) {
  return AppViewModel();
});

class AppViewModel extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.light;
  Locale _locale = const Locale('en');
  User? _currentUser;
  
  ThemeMode get themeMode => _themeMode;
  Locale get locale => _locale;
  User? get currentUser => _currentUser;
  bool get isLoggedIn => _currentUser != null;
  
  void toggleTheme() {
    _themeMode = _themeMode == ThemeMode.light 
        ? ThemeMode.dark 
        : ThemeMode.light;
    notifyListeners();
  }
  
  void setLocale(Locale locale) {
    _locale = locale;
    notifyListeners();
  }
  
  void setUser(User? user) {
    _currentUser = user;
    notifyListeners();
  }
}
  1. 使用 Riverpod 2.0
// lib/presentation/state/providers/auth_provider.dart
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  return AuthNotifier(ref.watch(authRepositoryProvider));
});

class AuthNotifier extends StateNotifier<AuthState> {
  final AuthRepository _repository;
  
  AuthNotifier(this._repository) : super(const AuthState.initial());
  
  Future<void> login(String email, String password) async {
    state = const AuthState.loading();
    try {
      final user = await _repository.login(email, password);
      state = AuthState.authenticated(user);
    } on ApiException catch (e) {
      state = AuthState.error(e.message);
    }
  }
  
  Future<void> logout() async {
    await _repository.logout();
    state = const AuthState.unauthenticated();
  }
}


class AuthState with _$AuthState {
  const factory AuthState.initial() = _Initial;
  const factory AuthState.loading() = _Loading;
  const factory AuthState.authenticated(User user) = _Authenticated;
  const factory AuthState.unauthenticated() = _Unauthenticated;
  const factory AuthState.error(String message) = _Error;
}

七、组件化架构

  1. 可复用组件库
// lib/presentation/widgets/common/app_button.dart
class AppButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;
  final bool isLoading;
  final bool isEnabled;
  final ButtonType type;
  
  const AppButton({
    Key? key,
    required this.text,
    required this.onPressed,
    this.isLoading = false,
    this.isEnabled = true,
    this.type = ButtonType.primary,
  }) : super(key: key);
  
  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isEnabled && !isLoading ? onPressed : null,
      style: _getButtonStyle(context),
      child: isLoading
          ? const SizedBox(
              height: 20,
              width: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : Text(
              text,
              style: Theme.of(context).textTheme.button,
            ),
    );
  }
  
  ButtonStyle _getButtonStyle(BuildContext context) {
    switch (type) {
      case ButtonType.primary:
        return ElevatedButton.styleFrom(
          backgroundColor: Theme.of(context).primaryColor,
        );
      case ButtonType.secondary:
        return ElevatedButton.styleFrom(
          backgroundColor: Colors.grey[300],
        );
      case ButtonType.danger:
        return ElevatedButton.styleFrom(
          backgroundColor: Colors.red,
        );
    }
  }
}

enum ButtonType { primary, secondary, danger }
  1. 表单组件
// lib/presentation/widgets/forms/app_text_field.dart
class AppTextField extends StatelessWidget {
  final TextEditingController controller;
  final String label;
  final String? hint;
  final IconData? prefixIcon;
  final IconData? suffixIcon;
  final bool obscureText;
  final TextInputType? keyboardType;
  final String? Function(String?)? validator;
  final VoidCallback? onSuffixPressed;
  
  const AppTextField({
    Key? key,
    required this.controller,
    required this.label,
    this.hint,
    this.prefixIcon,
    this.suffixIcon,
    this.obscureText = false,
    this.keyboardType,
    this.validator,
    this.onSuffixPressed,
  }) : super(key: key);
  
  
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      decoration: InputDecoration(
        labelText: label,
        hintText: hint,
        prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
        suffixIcon: suffixIcon != null
            ? IconButton(
                icon: Icon(suffixIcon),
                onPressed: onSuffixPressed,
              )
            : null,
        border: const OutlineInputBorder(),
        filled: true,
        fillColor: Colors.grey[100],
      ),
      obscureText: obscureText,
      keyboardType: keyboardType,
      validator: validator,
    );
  }
}

八、依赖注入配置

// lib/app/di/injector.dart
final getIt = GetIt.instance;

Future<void> init() async {
  // 第三方服务
  await _initExternalServices();
  
  // 数据层
  _initDataLayer();
  
  // 领域层
  _initDomainLayer();
  
  // 表现层
  _initPresentationLayer();
}

void _initPresentationLayer() {
  // ViewModels
  getIt.registerFactory(() => LoginViewModel(
    getIt<LoginUseCase>(),
    getIt<SaveUserUseCase>(),
  ));
  
  getIt.registerFactory(() => HomeViewModel(
    getIt<GetProductsUseCase>(),
  ));
}

void _initDomainLayer() {
  // UseCases
  getIt.registerLazySingleton(() => LoginUseCase(getIt()));
  getIt.registerLazySingleton(() => GetProductsUseCase(getIt()));
  getIt.registerLazySingleton(() => SaveUserUseCase(getIt()));
}

void _initDataLayer() {
  // 仓库
  getIt.registerLazySingleton<AuthRepository>(
    () => AuthRepositoryImpl(
      localDataSource: getIt(),
      remoteDataSource: getIt(),
    ),
  );
  
  // 数据源
  getIt.registerLazySingleton<AuthRemoteDataSource>(
    () => AuthRemoteDataSourceImpl(getIt()),
  );
  
  getIt.registerLazySingleton<AuthLocalDataSource>(
    () => AuthLocalDataSourceImpl(getIt()),
  );
}

九、应用入口配置

// lib/main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化依赖注入
  await init();
  
  // 运行应用
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => getIt<AppViewModel>()),
        Provider(create: (_) => getIt<AuthRepository>()),
      ],
      child: Consumer<AppViewModel>(
        builder: (context, appViewModel, child) {
          return MaterialApp(
            title: 'Flutter Enterprise App',
            theme: AppTheme.lightTheme,
            darkTheme: AppTheme.darkTheme,
            themeMode: appViewModel.themeMode,
            locale: appViewModel.locale,
            debugShowCheckedModeBanner: false,
            initialRoute: RouteNames.splash,
            onGenerateRoute: AppRouter.generateRoute,
            navigatorKey: NavigationService.navigatorKey,
            builder: (context, child) {
              return MediaQuery(
                data: MediaQuery.of(context).copyWith(
                  textScaleFactor: 1.0, // 防止系统字体缩放
                ),
                child: child!,
              );
            },
          );
        },
      ),
    );
  }
}

十、最佳实践总结

  1. 严格分层:保持清晰的关注点分离
  2. 单一职责:每个类/文件只做一件事
  3. 依赖倒置:依赖抽象,不依赖具体实现
  4. 响应式设计:使用状态管理确保UI与数据同步
  5. 错误处理:统一的错误处理机制
  6. 国际化支持:为多语言做好准备
  7. 主题管理:支持明暗主题切换
  8. 性能优化:懒加载、分页、缓存策略
  9. 测试友好:依赖注入便于单元测试
  10. 可扩展性:模块化设计,便于功能扩展

这个架构支持大型企业级应用的开发,具有良好的可维护性、可测试性和可扩展性。可以根据项目需求进行调整和扩展。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐