Flutter MVVM 完整实战:网络请求、状态管理、分页加载一网打尽

我来提供一个完整的、可直接运行的 Flutter MVVM 实战项目,包含网络请求、错误处理、加载状态、列表展示等真实场景。

完整项目代码

  1. 项目依赖 (pubspec.yaml)
yaml 复制代码
name: flutter_mvvm_demo
description: Flutter MVVM实战项目
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  riverpod: ^2.4.0
  flutter_riverpod: ^2.4.0
  dio: ^5.3.0
  json_annotation: ^4.8.1
  intl: ^0.18.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.6
  json_serializable: ^6.7.1

flutter:
  uses-material-design: true
  1. 项目结构

    lib/
    ├── main.dart
    ├── models/
    │ ├── post_model.dart
    │ └── user_model.dart
    ├── viewmodels/
    │ ├── post_viewmodel.dart
    │ └── auth_viewmodel.dart
    ├── views/
    │ ├── post_screen.dart
    │ ├── login_screen.dart
    │ └── widgets/
    │ ├── loading_widget.dart
    │ └── error_widget.dart
    ├── services/
    │ ├── api_service.dart
    │ └── http_client.dart
    └── constants/
    └── app_constants.dart

  2. 数据模型 (Models)

lib/models/post_model.dart

dart 复制代码
import 'package:json_annotation/json_annotation.dart';

part 'post_model.g.dart';

@JsonSerializable()
class PostModel {
  final int id;
  final int userId;
  final String title;
  final String body;
  
  @JsonKey(name: 'created_at')
  final DateTime? createdAt;

  PostModel({
    required this.id,
    required this.userId,
    required this.title,
    required this.body,
    this.createdAt,
  });

  factory PostModel.fromJson(Map<String, dynamic> json) => _$PostModelFromJson(json);
  Map<String, dynamic> toJson() => _$PostModelToJson(this);

  @override
  String toString() => 'PostModel(id: $id, title: $title)';
}

lib/models/user_model.dart

dart 复制代码
import 'package:json_annotation/json_annotation.dart';

part 'user_model.g.dart';

@JsonSerializable()
class UserModel {
  final int id;
  final String name;
  final String email;
  final String? avatar;
  
  @JsonKey(name: 'phone')
  final String? phoneNumber;

  UserModel({
    required this.id,
    required this.name,
    required this.email,
    this.avatar,
    this.phoneNumber,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
  Map<String, dynamic> toJson() => _$UserModelToJson(this);
}
  1. 网络服务 (Services)

lib/services/http_client.dart - 封装Dio

dart 复制代码
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';

class HttpClient {
  static final HttpClient _instance = HttpClient._internal();
  factory HttpClient() => _instance;
  HttpClient._internal();

  late Dio _dio;

  void init() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://jsonplaceholder.typicode.com',
      connectTimeout: const Duration(seconds: 30),
      receiveTimeout: const Duration(seconds: 30),
      headers: {
        'Content-Type': 'application/json',
      },
    ));

    // 添加拦截器
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        if (kDebugMode) {
          print('🚀 Request: ${options.method} ${options.path}');
          print('📦 Params: ${options.queryParameters}');
        }
        return handler.next(options);
      },
      onResponse: (response, handler) {
        if (kDebugMode) {
          print('✅ Response: ${response.statusCode} ${response.requestOptions.path}');
        }
        return handler.next(response);
      },
      onError: (error, handler) {
        if (kDebugMode) {
          print('❌ Error: ${error.message}');
        }
        return handler.next(error);
      },
    ));
  }

  Future<dynamic> get(
    String path, {
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    try {
      final response = await _dio.get(
        path,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  Future<dynamic> post(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    try {
      final response = await _dio.post(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  Exception _handleError(DioException error) {
    if (error.type == DioExceptionType.connectionTimeout) {
      return Exception('连接超时,请检查网络');
    } else if (error.type == DioExceptionType.receiveTimeout) {
      return Exception('接收超时,请稍后重试');
    } else if (error.type == DioExceptionType.cancel) {
      return Exception('请求已取消');
    } else if (error.response != null) {
      final statusCode = error.response!.statusCode;
      if (statusCode == 401) {
        return Exception('未授权,请重新登录');
      } else if (statusCode == 404) {
        return Exception('请求的资源不存在');
      } else if (statusCode == 500) {
        return Exception('服务器错误,请稍后重试');
      } else {
        return Exception('网络错误: $statusCode');
      }
    } else {
      return Exception('网络连接失败,请检查网络设置');
    }
  }
}

lib/services/api_service.dart

dart 复制代码
import 'package:flutter_mvvm_demo/models/post_model.dart';
import 'package:flutter_mvvm_demo/models/user_model.dart';
import 'package:flutter_mvvm_demo/services/http_client.dart';

class ApiService {
  final HttpClient _httpClient = HttpClient();

  // 获取帖子列表
  Future<List<PostModel>> fetchPosts({int page = 1, int limit = 10}) async {
    try {
      final data = await _httpClient.get(
        '/posts',
        queryParameters: {
          '_page': page,
          '_limit': limit,
        },
      );
      
      return List<PostModel>.from(
        (data as List).map((json) => PostModel.fromJson(json))
      );
    } catch (e) {
      rethrow;
    }
  }

  // 获取单个帖子详情
  Future<PostModel> fetchPostDetail(int postId) async {
    try {
      final data = await _httpClient.get('/posts/$postId');
      return PostModel.fromJson(data);
    } catch (e) {
      rethrow;
    }
  }

  // 获取用户信息
  Future<UserModel> fetchUser(int userId) async {
    try {
      final data = await _httpClient.get('/users/$userId');
      return UserModel.fromJson(data);
    } catch (e) {
      rethrow;
    }
  }

  // 创建帖子
  Future<PostModel> createPost({
    required int userId,
    required String title,
    required String body,
  }) async {
    try {
      final data = await _httpClient.post(
        '/posts',
        data: {
          'userId': userId,
          'title': title,
          'body': body,
        },
      );
      return PostModel.fromJson(data);
    } catch (e) {
      rethrow;
    }
  }
}
  1. ViewModels (核心业务逻辑)

lib/viewmodels/post_viewmodel.dart

dart 复制代码
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_mvvm_demo/models/post_model.dart';
import 'package:flutter_mvvm_demo/services/api_service.dart';

// 定义状态
class PostState {
  final List<PostModel> posts;
  final bool isLoading;
  final bool isLoadingMore;
  final String? error;
  final int currentPage;
  final bool hasMore;

  const PostState({
    this.posts = const [],
    this.isLoading = false,
    this.isLoadingMore = false,
    this.error,
    this.currentPage = 1,
    this.hasMore = true,
  });

  PostState copyWith({
    List<PostModel>? posts,
    bool? isLoading,
    bool? isLoadingMore,
    String? error,
    int? currentPage,
    bool? hasMore,
  }) {
    return PostState(
      posts: posts ?? this.posts,
      isLoading: isLoading ?? this.isLoading,
      isLoadingMore: isLoadingMore ?? this.isLoadingMore,
      error: error ?? this.error,
      currentPage: currentPage ?? this.currentPage,
      hasMore: hasMore ?? this.hasMore,
    );
  }
}

// ViewModel
class PostViewModel extends StateNotifier<PostState> {
  final ApiService _apiService;
  
  PostViewModel(this._apiService) : super(const PostState());

  // 获取帖子列表
  Future<void> fetchPosts({bool refresh = false}) async {
    try {
      // 如果是刷新,重置状态
      if (refresh) {
        state = state.copyWith(
          isLoading: true,
          posts: [],
          currentPage: 1,
          error: null,
        );
      } else if (state.posts.isEmpty) {
        state = state.copyWith(isLoading: true);
      }

      final newPosts = await _apiService.fetchPosts(
        page: state.currentPage,
        limit: 10,
      );

      final hasMore = newPosts.length == 10;
      final updatedPosts = refresh ? newPosts : [...state.posts, ...newPosts];

      state = state.copyWith(
        posts: updatedPosts,
        isLoading: false,
        hasMore: hasMore,
        error: null,
      );
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: e.toString(),
      );
    }
  }

  // 加载更多
  Future<void> loadMore() async {
    if (state.isLoadingMore || !state.hasMore) return;

    state = state.copyWith(isLoadingMore: true);

    try {
      final nextPage = state.currentPage + 1;
      final newPosts = await _apiService.fetchPosts(
        page: nextPage,
        limit: 10,
      );

      final hasMore = newPosts.length == 10;
      
      state = state.copyWith(
        posts: [...state.posts, ...newPosts],
        currentPage: nextPage,
        hasMore: hasMore,
        isLoadingMore: false,
      );
    } catch (e) {
      state = state.copyWith(
        isLoadingMore: false,
        error: e.toString(),
      );
    }
  }

  // 刷新
  Future<void> refresh() async {
    await fetchPosts(refresh: true);
  }

  // 创建新帖子
  Future<bool> createPost({
    required int userId,
    required String title,
    required String body,
  }) async {
    state = state.copyWith(isLoading: true);

    try {
      final newPost = await _apiService.createPost(
        userId: userId,
        title: title,
        body: body,
      );
      
      // 将新帖子添加到列表开头
      state = state.copyWith(
        posts: [newPost, ...state.posts],
        isLoading: false,
      );
      
      return true;
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: e.toString(),
      );
      return false;
    }
  }

  // 清除错误
  void clearError() {
    state = state.copyWith(error: null);
  }
}

// Provider 定义
final postViewModelProvider = StateNotifierProvider<PostViewModel, PostState>((ref) {
  return PostViewModel(ApiService());
});

lib/viewmodels/auth_viewmodel.dart - 展示登录场景

dart 复制代码
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_mvvm_demo/models/user_model.dart';
import 'package:flutter_mvvm_demo/services/api_service.dart';

class AuthState {
  final UserModel? user;
  final bool isLoading;
  final String? error;
  final bool isAuthenticated;

  const AuthState({
    this.user,
    this.isLoading = false,
    this.error,
    this.isAuthenticated = false,
  });

  AuthState copyWith({
    UserModel? user,
    bool? isLoading,
    String? error,
    bool? isAuthenticated,
  }) {
    return AuthState(
      user: user ?? this.user,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
      isAuthenticated: isAuthenticated ?? this.isAuthenticated,
    );
  }
}

class AuthViewModel extends StateNotifier<AuthState> {
  final ApiService _apiService;
  
  AuthViewModel(this._apiService) : super(const AuthState());

  Future<bool> login(String email, String password) async {
    state = state.copyWith(isLoading: true, error: null);

    try {
      // 模拟登录验证(实际项目应该有登录接口)
      await Future.delayed(const Duration(seconds: 1));
      
      // 获取用户信息(这里用id=1作为示例)
      final user = await _apiService.fetchUser(1);
      
      // 验证邮箱(简单示例)
      if (email != user.email) {
        throw Exception('邮箱或密码错误');
      }
      
      state = state.copyWith(
        user: user,
        isLoading: false,
        isAuthenticated: true,
      );
      
      return true;
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: e.toString(),
      );
      return false;
    }
  }

  void logout() {
    state = const AuthState();
  }

  void clearError() {
    state = state.copyWith(error: null);
  }
}

final authViewModelProvider = StateNotifierProvider<AuthViewModel, AuthState>((ref) {
  return AuthViewModel(ApiService());
});
  1. UI组件 (Views)

lib/views/widgets/loading_widget.dart

dart 复制代码
import 'package:flutter/material.dart';

class LoadingWidget extends StatelessWidget {
  final String? message;
  
  const LoadingWidget({Key? key, this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const CircularProgressIndicator(),
          if (message != null) ...[
            const SizedBox(height: 16),
            Text(
              message!,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ],
        ],
      ),
    );
  }
}

lib/views/widgets/error_widget.dart

dart 复制代码
import 'package:flutter/material.dart';

class ErrorDisplayWidget extends StatelessWidget {
  final String error;
  final VoidCallback onRetry;
  
  const ErrorDisplayWidget({
    Key? key,
    required this.error,
    required this.onRetry,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.error_outline,
            size: 64,
            color: Theme.of(context).colorScheme.error,
          ),
          const SizedBox(height: 16),
          Text(
            '加载失败',
            style: Theme.of(context).textTheme.titleLarge,
          ),
          const SizedBox(height: 8),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 32),
            child: Text(
              error,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
          const SizedBox(height: 24),
          ElevatedButton.icon(
            onPressed: onRetry,
            icon: const Icon(Icons.refresh),
            label: const Text('重试'),
          ),
        ],
      ),
    );
  }
}

lib/views/post_screen.dart - 主界面

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_mvvm_demo/viewmodels/post_viewmodel.dart';
import 'package:flutter_mvvm_demo/views/widgets/loading_widget.dart';
import 'package:flutter_mvvm_demo/views/widgets/error_widget.dart';

class PostScreen extends ConsumerStatefulWidget {
  const PostScreen({Key? key}) : super(key: key);

  @override
  ConsumerState<PostScreen> createState() => _PostScreenState();
}

class _PostScreenState extends ConsumerState<PostScreen> {
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _loadInitialData();
    _setupScrollListener();
  }

  void _loadInitialData() {
    Future.microtask(() {
      ref.read(postViewModelProvider.notifier).fetchPosts();
    });
  }

  void _setupScrollListener() {
    _scrollController.addListener(() {
      if (_scrollController.position.pixels >= 
          _scrollController.position.maxScrollExtent - 200) {
        ref.read(postViewModelProvider.notifier).loadMore();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(postViewModelProvider);
    final viewModel = ref.read(postViewModelProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        title: const Text('帖子列表'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => viewModel.refresh(),
          ),
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: () => _showCreatePostDialog(context, viewModel),
          ),
        ],
      ),
      body: _buildBody(state, viewModel),
    );
  }

  Widget _buildBody(PostState state, PostViewModel viewModel) {
    if (state.isLoading && state.posts.isEmpty) {
      return const LoadingWidget(message: '加载中...');
    }

    if (state.error != null && state.posts.isEmpty) {
      return ErrorDisplayWidget(
        error: state.error!,
        onRetry: () => viewModel.refresh(),
      );
    }

    return RefreshIndicator(
      onRefresh: () => viewModel.refresh(),
      child: ListView.builder(
        controller: _scrollController,
        itemCount: state.posts.length + (state.hasMore ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == state.posts.length) {
            return _buildLoadingMore(state);
          }
          return _buildPostItem(state.posts[index]);
        },
      ),
    );
  }

  Widget _buildPostItem(PostModel post) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: ListTile(
        leading: CircleAvatar(
          child: Text('${post.id}'),
        ),
        title: Text(
          post.title,
          style: const TextStyle(fontWeight: FontWeight.bold),
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
        subtitle: Text(
          post.body,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
        isThreeLine: false,
        trailing: const Icon(Icons.chevron_right),
        onTap: () {
          _showPostDetail(context, post);
        },
      ),
    );
  }

  Widget _buildLoadingMore(PostState state) {
    if (state.isLoadingMore) {
      return const Padding(
        padding: EdgeInsets.all(16.0),
        child: Center(child: CircularProgressIndicator()),
      );
    }
    
    if (!state.hasMore && state.posts.isNotEmpty) {
      return Padding(
        padding: const EdgeInsets.all(16.0),
        child: Center(
          child: Text(
            '没有更多数据了',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
        ),
      );
    }
    
    return const SizedBox.shrink();
  }

  void _showPostDetail(BuildContext context, PostModel post) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(post.title),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('ID: ${post.id}'),
            const SizedBox(height: 8),
            Text('用户ID: ${post.userId}'),
            const SizedBox(height: 16),
            Text(post.body),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('关闭'),
          ),
        ],
      ),
    );
  }

  void _showCreatePostDialog(BuildContext context, PostViewModel viewModel) {
    final titleController = TextEditingController();
    final bodyController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('创建新帖子'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: titleController,
              decoration: const InputDecoration(
                labelText: '标题',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: bodyController,
              decoration: const InputDecoration(
                labelText: '内容',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () async {
              if (titleController.text.isNotEmpty && 
                  bodyController.text.isNotEmpty) {
                final success = await viewModel.createPost(
                  userId: 1,
                  title: titleController.text,
                  body: bodyController.text,
                );
                
                if (success && context.mounted) {
                  Navigator.pop(context);
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('创建成功')),
                  );
                }
              }
            },
            child: const Text('发布'),
          ),
        ],
      ),
    );
  }
}

lib/views/login_screen.dart - 登录界面

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_mvvm_demo/viewmodels/auth_viewmodel.dart';
import 'package:flutter_mvvm_demo/views/post_screen.dart';

class LoginScreen extends ConsumerStatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  ConsumerState<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends ConsumerState<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final authState = ref.watch(authViewModelProvider);
    final authViewModel = ref.read(authViewModelProvider.notifier);

    ref.listen(authViewModelProvider, (previous, next) {
      if (next.isAuthenticated && mounted) {
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (context) => const PostScreen()),
        );
      }
    });

    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24.0),
          child: Form(
            key: _formKey,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const FlutterLogo(size: 100),
                const SizedBox(height: 48),
                TextFormField(
                  controller: _emailController,
                  decoration: const InputDecoration(
                    labelText: '邮箱',
                    prefixIcon: Icon(Icons.email),
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.emailAddress,
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return '请输入邮箱';
                    }
                    if (!value.contains('@')) {
                      return '请输入有效的邮箱地址';
                    }
                    return null;
                  },
                ),
                const SizedBox(height: 16),
                TextFormField(
                  controller: _passwordController,
                  decoration: const InputDecoration(
                    labelText: '密码',
                    prefixIcon: Icon(Icons.lock),
                    border: OutlineInputBorder(),
                  ),
                  obscureText: true,
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return '请输入密码';
                    }
                    if (value.length < 6) {
                      return '密码至少6位';
                    }
                    return null;
                  },
                ),
                const SizedBox(height: 24),
                if (authState.error != null)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 16),
                    child: Text(
                      authState.error!,
                      style: TextStyle(
                        color: Theme.of(context).colorScheme.error,
                      ),
                    ),
                  ),
                SizedBox(
                  width: double.infinity,
                  height: 48,
                  child: ElevatedButton(
                    onPressed: authState.isLoading
                        ? null
                        : () async {
                            if (_formKey.currentState!.validate()) {
                              await authViewModel.login(
                                _emailController.text,
                                _passwordController.text,
                              );
                            }
                          },
                    child: authState.isLoading
                        ? const CircularProgressIndicator()
                        : const Text('登录'),
                  ),
                ),
                const SizedBox(height: 16),
                TextButton(
                  onPressed: () {
                    // 测试账号提示
                    showDialog(
                      context: context,
                      builder: (context) => AlertDialog(
                        title: const Text('测试账号'),
                        content: const Text('邮箱: Sincere@april.biz\n密码: 123456'),
                        actions: [
                          TextButton(
                            onPressed: () => Navigator.pop(context),
                            child: const Text('知道了'),
                          ),
                        ],
                      ),
                    );
                  },
                  child: const Text('测试账号?'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
  1. 主入口 (lib/main.dart)
dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_mvvm_demo/services/http_client.dart';
import 'package:flutter_mvvm_demo/views/login_screen.dart';

void main() {
  // 初始化网络客户端
  HttpClient().init();
  
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MVVM Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const LoginScreen(),
    );
  }
}
  1. 生成JSON序列化代码

运行以下命令生成 .g.dart 文件:

bash 复制代码
flutter pub run build_runner build --delete-conflicting-outputs

关键特性说明

  1. 状态管理

· 使用 StateNotifier + Riverpod 实现不可变状态

· 每个状态变化都创建新的State对象,确保可预测性

  1. 网络层

· 封装 HttpClient 统一处理Dio配置和错误

· ApiService 提供业务接口

  1. ViewModel设计

· 不持有 BuildContext

· 暴露方法供UI调用

· 管理加载、成功、失败状态

  1. UI响应

· 使用 ConsumerWidget 监听状态

· 根据状态显示不同UI(加载/错误/内容)

· 支持下拉刷新和上拉加载更多

  1. 错误处理

· 统一的错误捕获和展示

· 提供重试机制

这个完整示例展示了:

· ✅ 网络请求与数据解析

· ✅ 分页加载

· ✅ 表单验证

· ✅ 路由跳转

· ✅ 状态管理最佳实践

你可以直接复制代码运行,体验完整的MVVM架构。如果需要特定场景的实现,我可以进一步细化。

相关推荐
Lanren的编程日记14 小时前
Flutter鸿蒙应用开发:基础UI组件库设计与实现实战
flutter·ui·harmonyos
西西学代码14 小时前
Flutter---波形动画
flutter
于慨18 小时前
flutter基础组件用法
开发语言·javascript·flutter
恋猫de小郭20 小时前
Android CLI ,谷歌为 Android 开发者专研的 AI Agent,提速三倍
android·前端·flutter
火柴就是我21 小时前
flutter pushAndRemoveUntil 的一次小疑惑
flutter
于慨21 小时前
flutter doctor问题解决
flutter
唔6621 小时前
flutter 图片加载类 图片的安全使用
安全·flutter
Nathan202406161 天前
Flutter - InheritedWidget
flutter·dart
恋猫de小郭1 天前
JetBrains Amper 0.10 ,期待它未来替代 Gradle
android·前端·flutter
Lanren的编程日记1 天前
Flutter鸿蒙应用开发:实时聊天功能集成实战
flutter·华为·harmonyos