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架构。如果需要特定场景的实现,我可以进一步细化。

相关推荐
孤影过客3 小时前
Flutter优雅构建:从零打造开发级工作流
arm开发·数据库·flutter
p1gd0g5 小时前
flutter web 如何确保用户收到更新
flutter
GoCoding5 小时前
Flutter ngspice 插件
flutter
恋猫de小郭5 小时前
Android Studio Panda 2 ,支持 AI 用 Vibe Coding 创建项目
android·前端·flutter
Gorit7 小时前
如何使用 Flutter 开发 HarmonyOS 应用
flutter·华为·harmonyos
孤影过客7 小时前
Flutter高性能任务管理APP开发实战代码解析
jvm·flutter·oracle
键盘鼓手苏苏19 小时前
Flutter 三方库 p2plib 的鸿蒙化适配指南 - 实现高性能的端到端(P2P)加密通讯、支持分布式节点发现与去中心化数据流传输实战
flutter·harmonyos·鸿蒙·openharmony
加农炮手Jinx19 小时前
Flutter for OpenHarmony:postgrest 直接访问 PostgreSQL 数据库的 RESTful 客户端(Supabase 核心驱动) 深度解析与鸿蒙适配指南
数据库·flutter·华为·postgresql·restful·harmonyos·鸿蒙
加农炮手Jinx19 小时前
Flutter 组件 heart 适配鸿蒙 HarmonyOS 实战:分布式心跳监控,构建全场景保活检测与链路哨兵架构
flutter·harmonyos·鸿蒙·openharmony