Flutter艺术探索-Flutter依赖注入:get_it与provider组合使用

Flutter依赖注入实战:当get_it遇上provider

写在前面:我们为什么需要更好的依赖管理?

不知道你有没有经历过这种场景:一个Flutter项目刚开始还挺清晰,随着功能越来越多,各个模块之间的调用关系逐渐变成了"一团乱麻"。某个业务逻辑改起来牵一发而动全身,写单元测试时Mock依赖项都要花上半天------如果你遇到过这些问题,那么大概能理解我们为什么需要一套清晰的依赖管理方案。

Flutter社区为我们提供了不少工具,其中 get_itprovider 算是两个"明星选手"。前者是个轻量级的服务定位器,管理服务实例很拿手;后者则是状态管理的"老熟人",在UI和数据同步方面表现优秀。但单独用它们,总觉得差点意思。

get_it 能很好地管理各种服务对象,但它不关心UI更新;provider 擅长状态响应,可让它来管一堆非UI相关的服务层依赖,又显得有点"越俎代庖"。那......能不能让它们俩配合起来工作呢?

当然可以。这篇文章就是来聊聊,如何把 get_itprovider 组合起来,取长补短,搭建一个既清晰又易于维护的Flutter应用架构。

你会在这篇文章里看到:

  • get_itprovider 各自的核心工作方式
  • 一套完整的、可落地的组合使用方案
  • 实际项目中代码该怎么组织
  • 一些提升性能和便于调试的技巧

准备好了吗?我们开始吧。

一、理解我们的工具:get_it 与 provider 如何工作

1.1 get_it:你的应用服务"大管家"

可以把 get_it 想象成你应用的"服务中心"或"电话总机"。当某个部分需要一项服务(比如网络请求、本地存储)时,它不自己直接创建,而是向这个中心"要"一个已经准备好的实例。这样做的好处是,服务本身在哪里创建、生命周期如何管理,都被集中处理了,业务代码会干净很多。

它的核心其实是服务定位器模式的一个非常简洁的实现。我们来看一个高度简化的内部示意,理解它怎么运转的:

dart 复制代码
// 理解 get_it 的核心机制(简化模型)
class ServiceLocator {
  // 它内部有两个重要的"登记簿"
  final Map<Type, _ServiceFactory> _factories = {}; // 记录"如何创建"
  final Map<Type, Object> _instances = {};          // 记录"已经创建好的"

  // 最常用的注册方法:全局单例
  void registerSingleton<T extends Object>(T instance) {
    _instances[T] = instance; // 直接存好,下次直接用
  }

  // 工厂方法:每次获取都新建一个
  void registerFactory<T extends Object>(FactoryFunc<T> factoryFunc) {
    _factories[T] = _ServiceFactory.factory(factoryFunc);
  }

  // 懒加载单例:用到的时候才创建,之后复用
  void registerLazySingleton<T extends Object>(FactoryFunc<T> factoryFunc) {
    _factories[T] = _ServiceFactory.lazySingleton(factoryFunc);
  }

  // 获取服务的入口
  T get<T extends Object>() {
    // 先看有没有现成的单例
    if (_instances.containsKey(T)) return _instances[T] as T;
    
    // 没有的话,看看有没有对应的工厂方法,用它创建
    final factory = _factories[T];
    if (factory != null) return factory.getInstance();
    
    // 都没找到?那只能抛异常了
    throw Exception('Service $T not registered.');
  }
}

几个关键特点:

  1. 灵活的注册方式:你可以根据需求选择注册为单例(全局一个)、懒加载单例(第一次用到时才创建)或者工厂(每次都新建)。

  2. 作用域支持 :这在测试时特别有用。你可以临时创建一个新的、隔离的作用域,在里面注册模拟服务,测试完了再切回来,完全不影响主流程。

    dart 复制代码
    // 测试时非常方便
    final testLocator = GetIt.asNewInstance();
    testLocator.registerSingleton<ApiService>(MockApiService());
  3. 异步初始化 :有些服务启动时需要异步操作(比如读本地配置),get_it 可以等它们都准备好再通知你。

    dart 复制代码
    await getIt.allReady(); // 等待所有异步注册的服务就绪

1.2 provider:让UI和数据自动同步

provider 的根基是Flutter自带的 InheritedWidget。简单说,它提供了一个在Widget树中自上而下高效传递数据和状态的能力,并且能在数据变化时,自动通知依赖它的Widget更新。

它的生态挺丰富的,我们大致可以这么看:

复制代码
Provider 家族
├── 核心 (负责提供数据)
│   ├── Provider: 基础款,提供任意对象
│   ├── ChangeNotifierProvider: 配合 ChangeNotifier,数据变,UI自动变
│   └── FutureProvider/StreamProvider: 专门处理异步数据流
├── 消费端 (如何获取数据)
│   ├── Consumer: 在 Widget 内部获取,并选择性重建
│   ├── Selector: "精打细算",只在自己关心的数据变化时才重建
│   └── 传统的 Provider.of<T>(context): 直接获取
└── 工具
    ├── MultiProvider: 一次性提供多个 Provider,代码更整洁
    └── ProxyProvider: 处理 Provider 之间的依赖关系

其中,ChangeNotifierProvider 是我们最常用的状态管理搭档。它的工作原理是:

dart 复制代码
class AppState extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;
  
  void increment() {
    _counter++;
    notifyListeners(); // 关键一步:通知所有监听者"我变了"
  }
}

// 在 Provider 内部,它大概是这样监听变化的
class ChangeNotifierProvider<T extends ChangeNotifier> extends InheritedProvider<T> {
  
  void _updateListeners() {
    // 当 Provider 重建或数据源更换时,重新设置监听
    _oldNotifier?.removeListener(_markNeedRebuild);
    _currentNotifier = widget.create;
    _currentNotifier?.addListener(_markNeedRebuild);
  }
  
  void _markNeedRebuild() {
    // 当 ChangeNotifier 调用 notifyListeners 时,这里被触发
    // 从而通知依赖它的 Widget 进行重建
    markNeedsNotifyDependents();
  }
}

1.3 为什么组合起来是更好的选择?

单用任何一个工具,在一些复杂场景下都会遇到麻烦:

  • 只用 get_it :你的 ViewModel 或 Service 可以很方便地获取到,但当它们内部数据变化时,UI 无法自动更新。你得手动去调用 setState 或者找其他方式通知界面,容易遗漏,也破坏了响应式的流畅性。
  • 只用 provider :让 provider 去管理所有层级的对象(包括纯粹的服务层),会让 create 方法变得异常复杂,依赖链难以理清,而且也不符合"关注点分离"的原则。

组合方案就优雅多了:

dart 复制代码
// 服务层:统统交给 get_it 管理,干净利落
getIt.registerSingleton<ApiService>(ApiServiceImpl());
getIt.registerSingleton<AnalyticsService>(AnalyticsServiceImpl());

// UI状态层:交给 provider 管理,享受响应式更新的便利
ChangeNotifierProvider(
  create: (context) => HomeViewModel(
    // ViewModel 所需要的服务,直接从 get_it 这个"服务中心"获取
    api: getIt<ApiService>(),
    analytics: getIt<AnalyticsService>(),
  ),
  child: MyHomePage(),
);

这样,get_it 专心管理那些"笨重"的、与UI无关的服务实例;provider 则专注于连接 ViewModel 和 UI,实现状态驱动视图。两者职责清晰,边界明确。

二、从零搭建:一个完整的项目示例

理论说完了,我们来点实际的。假设我们要构建一个简单的博客应用,它能显示文章列表,并且有用户登录功能。

2.1 项目起步:配置依赖

首先,在 pubspec.yaml 里把需要的包引进来:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  get_it: ^7.6.0        # 服务定位器
  provider: ^6.1.0      # 状态管理
  dio: ^5.3.0           # 好用的网络请求库
  shared_preferences: ^2.2.0 # 本地持久化存储

dev_dependencies:
  flutter_test:
    sdk: flutter
  mocktail: ^0.3.0      # 写测试时用来模拟对象
  build_runner: ^2.4.0

2.2 构建坚实的服务层

服务层是应用的基石,我们用 get_it 来管理它们。

1. 网络请求服务 (ApiService)

我们先定义一个抽象类,这样以后切换实现或者做Mock测试都会很方便。

dart 复制代码
// lib/core/services/api_service.dart
abstract class ApiService {
  Future<User> getUser(int id);
  Future<List<Post>> getPosts();
  Future<void> updateUser(User user);
}

// 具体的实现,基于 Dio
class ApiServiceImpl implements ApiService {
  final Dio _dio;
  final String _baseUrl;
  
  ApiServiceImpl({required Dio dio, required String baseUrl})
      : _dio = dio,
        _baseUrl = baseUrl;
  
  @override
  Future<User> getUser(int id) async {
    try {
      final response = await _dio.get('$_baseUrl/users/$id');
      return User.fromJson(response.data);
    } on DioException catch (e) {
      // 针对不同的错误码,可以抛出更具体的异常
      if (e.response?.statusCode == 404) {
        throw UserNotFoundException('User $id not found');
      }
      throw ApiException('Failed to fetch user: ${e.message}');
    }
  }
  
  // ... 其他方法(getPosts, updateUser)类似
}

// 自定义异常,让错误处理更清晰
class ApiException implements Exception {
  final String message;
  ApiException(this.message);
  
  @override
  String toString() => 'ApiException: $message';
}

2. 认证服务 (AuthService)

这个服务负责登录、登出,并管理用户的认证状态。我们用 Stream 来广播状态变化,这样任何地方都可以监听。

dart 复制代码
// lib/core/services/auth_service.dart
class AuthService {
  final ApiService _api;
  final StorageService _storage;
  // 使用 BehaviorSubject 来管理认证状态流
  final _authState = BehaviorSubject<AuthState>.seeded(AuthState.unauthorized());
  
  AuthService(this._api, this._storage);
  
  // 对外暴露一个只读的 Stream
  Stream<AuthState> get authState => _authState.stream;
  // 当前状态
  AuthState get currentState => _authState.value;
  
  Future<void> login(String email, String password) async {
    try {
      _authState.add(AuthState.loading()); // 状态:加载中
      
      final user = await _api.login(email, password); // 调用API
      
      // 登录成功,保存 token 和用户信息
      await _storage.saveString('auth_token', user.token);
      await _storage.saveString('user_data', jsonEncode(user.toJson()));
      
      _authState.add(AuthState.authorized(user)); // 状态:已授权
    } on ApiException catch (e) {
      _authState.add(AuthState.error(e.message)); // 状态:错误
      rethrow;
    }
  }
  
  Future<void> logout() async {
    await _storage.remove('auth_token');
    await _storage.remove('user_data');
    _authState.add(AuthState.unauthorized()); // 状态:未授权
  }
  
  // App启动时调用,检查本地是否有保存的登录信息
  Future<void> initialize() async {
    final token = await _storage.getString('auth_token');
    final userJson = await _storage.getString('user_data');
    
    if (token != null && userJson != null) {
      final user = User.fromJson(jsonDecode(userJson));
      _authState.add(AuthState.authorized(user));
    }
  }
  
  void dispose() {
    _authState.close(); // 记得关闭 StreamController
  }
}

// 用一个枚举清晰定义所有可能的认证状态
enum AuthStatus { unauthorized, loading, authorized, error }

// 用一个类封装状态,可以携带额外数据(如用户信息、错误信息)
class AuthState {
  final AuthStatus status;
  final User? user;
  final String? error;
  
  const AuthState._(this.status, {this.user, this.error});
  
  // 一些方便的工厂构造方法
  factory AuthState.unauthorized() => const AuthState._(AuthStatus.unauthorized);
  factory AuthState.loading() => const AuthState._(AuthStatus.loading);
  factory AuthState.authorized(User user) => AuthState._(AuthStatus.authorized, user: user);
  factory AuthState.error(String error) => AuthState._(AuthStatus.error, error: error);
}

2.3 承上启下的 ViewModel 层

ViewModel 是连接服务层和UI层的桥梁。它从 get_it 获取服务,处理业务逻辑,并通过 ChangeNotifier 通知UI更新。

dart 复制代码
// lib/features/home/presentation/home_viewmodel.dart
class HomeViewModel extends ChangeNotifier {
  final ApiService _api;
  final AnalyticsService _analytics;
  
  HomeState _state = HomeState.initial();
  List<Post> _posts = [];
  bool _isLoading = false;
  
  HomeState get state => _state;
  List<Post> get posts => _posts;
  bool get isLoading => _isLoading;
  
  HomeViewModel(this._api, this._analytics);
  
  Future<void> loadPosts() async {
    if (_isLoading) return; // 防止重复加载
    
    _isLoading = true;
    notifyListeners(); // 通知UI:开始加载了
    
    try {
      _analytics.logEvent('home_load_posts'); // 记录分析事件
      
      _posts = await _api.getPosts();
      _state = HomeState.success(posts: _posts); // 更新状态为成功
      
      _analytics.logEvent('home_posts_loaded', 
          parameters: {'count': _posts.length});
    } on ApiException catch (e) {
      _state = HomeState.error(e.message); // 更新状态为失败
      _analytics.logError('load_posts_failed', error: e);
    } catch (e) {
      _state = HomeState.error('Unexpected error'); // 未知错误
      _analytics.logError('load_posts_unexpected', error: e);
    } finally {
      _isLoading = false;
      notifyListeners(); // 无论成功失败,加载结束,通知UI
    }
  }
}

2.4 核心步骤:依赖注入配置

现在,是时候把所有的服务"装配"起来了。我们在应用启动时完成这个工作。

dart 复制代码
// lib/core/dependency_injection.dart
import 'package:get_it/get_it.dart';

final GetIt getIt = GetIt.instance; // 全局唯一的 GetIt 实例

class DependencyInjector {
  static Future<void> setup() async {
    // 1. 初始化一些异步的第三方库,比如 SharedPreferences
    final sharedPreferences = await SharedPreferences.getInstance();
    getIt.registerSingleton<SharedPreferences>(sharedPreferences);
    
    // 2. 注册网络客户端 Dio(懒加载单例,用到时才创建)
    getIt.registerLazySingleton<Dio>(
      () => Dio(BaseOptions(
        baseUrl: 'https://jsonplaceholder.typicode.com',
        connectTimeout: const Duration(seconds: 10),
      )),
    );
    
    // 3. 注册我们自己的服务
    getIt.registerSingleton<StorageService>(
      SharedPreferencesStorage(getIt<SharedPreferences>()), // 依赖已注册的 SharedPreferences
    );
    
    getIt.registerSingleton<ApiService>(
      ApiServiceImpl(
        dio: getIt<Dio>(), // 依赖已注册的 Dio
        baseUrl: 'https://jsonplaceholder.typicode.com',
      ),
    );
    
    getIt.registerLazySingleton<AuthService>(
      () {
        final service = AuthService(
          getIt<ApiService>(),
          getIt<StorageService>(),
        );
        service.initialize(); // 创建后立即初始化,检查登录状态
        return service;
      },
      dispose: (service) => service.dispose(), // 记得告诉 get_it 如何销毁它
    );
    
    // 4. 等待所有需要异步初始化的服务都准备好
    await getIt.allReady();
  }
  
  // 专门为测试环境准备的配置
  static void setupForTesting() {
    getIt.reset(); // 清空所有注册
    // 注册模拟对象
    getIt.registerSingleton<ApiService>(MockApiService());
    getIt.registerSingleton<StorageService>(MockStorageService());
    // ... 其他模拟服务
  }
}

2.5 在UI层将它们组合起来

最后,我们在应用的根 Widget 里,使用 MultiProviderproviderget_it 连接起来。

dart 复制代码
// lib/app.dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // 使用 StreamProvider 监听认证状态流
        StreamProvider<AuthState>.value(
          initialData: AuthState.unauthorized(),
          value: getIt<AuthService>().authState, // 数据源来自 get_it
          catchError: (_, err) => AuthState.error(err.toString()),
        ),
        
        // 为首页提供 ViewModel
        ChangeNotifierProvider(
          create: (_) => HomeViewModel(
            getIt<ApiService>(),      // 依赖从 get_it 获取
            getIt<AnalyticsService>(),
          ),
        ),
        
        // 更复杂的例子:UserViewModel 依赖认证状态
        ChangeNotifierProxyProvider<AuthState, UserViewModel>(
          create: (_) => UserViewModel(getIt<ApiService>()),
          update: (_, authState, previousUserViewModel) {
            // 当认证状态变化时,更新 ViewModel 中的用户信息
            if (authState.status == AuthStatus.authorized) {
              previousUserViewModel?.updateUser(authState.user!);
            }
            return previousUserViewModel ?? UserViewModel(getIt<ApiService>());
          },
        ),
      ],
      child: MaterialApp(
        title: 'Flutter DI 示例',
        home: const AppWrapper(), // 一个根据认证状态决定首页的包装器
      ),
    );
  }
}

// 应用包装器:根据 AuthState 显示不同的页面
class AppWrapper extends StatelessWidget {
  const AppWrapper({super.key});
  
  @override
  Widget build(BuildContext context) {
    final authState = context.watch<AuthState>(); // 监听认证状态
    
    return switch (authState.status) {
      AuthStatus.unauthorized => const LoginScreen(),
      AuthStatus.loading => const Scaffold(body: Center(child: CircularProgressIndicator())),
      AuthStatus.authorized => const HomeScreen(),
      AuthStatus.error => ErrorScreen(error: authState.error!),
    };
  }
}

在具体的 HomeScreen 中,我们使用 ConsumerSelector 来消费 ViewModel,并实现刷新、登出等交互。

dart 复制代码
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('首页'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => context.read<HomeViewModel>().refresh(),
          ),
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () => getIt<AuthService>().logout(), // 直接调用 get_it 中的服务
          ),
        ],
      ),
      body: Consumer<HomeViewModel>(
        builder: (context, viewModel, child) {
          if (viewModel.isLoading) return const CircularProgressIndicator();
          if (viewModel.state.error != null) return Text('出错啦:${viewModel.state.error}');
          if (viewModel.posts.isEmpty) return const Text('暂无内容');
          
          return ListView.builder(
            itemCount: viewModel.posts.length,
            itemBuilder: (context, index) => PostItem(post: viewModel.posts[index]),
          );
        },
      ),
    );
  }
}

三、更进一步:优化与高级技巧

当应用变得复杂时,一些优化技巧能显著提升体验。

3.1 精细化UI重建:别动不动就刷新整个页面

Consumer 默认会在 ViewModel 的任何变化时重建其 builder。如果只是 posts 数量变了,但 isLoading 没变,我们不想重建整个列表。这时可以用 Selector

dart 复制代码
class PostCountBadge extends StatelessWidget {
  const PostCountBadge({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Selector<HomeViewModel, int>(
      selector: (ctx, vm) => vm.posts.length, // 只关注 posts 的数量
      builder: (ctx, count, child) {
        // 只有 count 变化时,这个 builder 才会被调用
        return Badge(label: Text('$count 篇'));
      },
    );
  }
}

3.2 善用 Provider 的 child 参数进行优化

Consumerbuilder 会重建,但传给它的 child 参数如果是不依赖状态的 Widget,则会被缓存,避免不必要的重建。

dart 复制代码
Consumer<HomeViewModel>(
  child: const PostCountBadge(), // 这个 child 会被缓存
  builder: (context, viewModel, child) {
    return Column(
      children: [
        child!, // 直接使用缓存的 child
        Expanded(child: PostListView(posts: viewModel.posts)),
      ],
    );
  },
)

3.3 为测试而设计:依赖注入的最大优势

因为我们通过抽象和 get_it 注册,所以为 HomeViewModel 写单元测试变得极其简单:

dart 复制代码
void main() {
  late HomeViewModel viewModel;
  late MockApiService mockApi;
  late MockAnalyticsService mockAnalytics;
  
  setUp(() {
    mockApi = MockApiService();
    mockAnalytics = MockAnalyticsService();
    // 准备模拟数据
    when(() => mockApi.getPosts()).thenAnswer((_) async => [Post(id: 1, title: '测试')]);
    
    viewModel = HomeViewModel(mockApi, mockAnalytics);
  });
  
  test('成功加载文章后,状态应更新为成功', () async {
    await viewModel.loadPosts();
    expect(viewModel.state.posts, hasLength(1));
    expect(viewModel.isLoading, false);
    // 验证是否记录了正确的分析事件
    verify(() => mockAnalytics.logEvent('home_posts_loaded')).called(1);
  });
}

总结一下 ,将 get_itprovider 组合使用,就像是为你 Flutter 应用请了两位专业的"管家":get_it 负责后台所有服务资源的调度和管理,让业务代码保持整洁;provider 则在前台负责状态与UI的同步,确保界面能及时、高效地响应变化。它们各司其职,又通过 ViewModel 紧密协作,共同构建出一个清晰、可测试、易于维护的现代应用架构。

希望这篇指南能帮助你更好地组织你的 Flutter 项目。在实践中,你可以根据项目的具体规模调整这套方案,比如引入更复杂的路由管理、状态持久化等。祝 coding 愉快!

相关推荐
向哆哆4 小时前
构建健康档案管理快速入口:Flutter × OpenHarmony 跨端开发实战
flutter·开源·鸿蒙·openharmony·开源鸿蒙
mocoding4 小时前
使用Flutter强大的图标库fl_chart优化鸿蒙版天气预报温度、降水量、湿度展示
flutter·华为·harmonyos
向哆哆4 小时前
构建智能健康档案管理与预约挂号系统:Flutter × OpenHarmony 跨端开发实践
flutter·开源·鸿蒙·openharmony·开源鸿蒙
Swift社区4 小时前
Flutter 路由系统,对比 RN / Web / iOS 有什么本质不同?
前端·flutter·ios
向哆哆5 小时前
Flutter × OpenHarmony:打造校园勤工俭学个人中心界面实战
flutter·开源·鸿蒙·openharmony
2601_949833395 小时前
flutter_for_openharmony口腔护理app实战+我的实现
开发语言·javascript·flutter
雨季6665 小时前
Flutter 三端应用实战:OpenHarmony 简易“动态色盘生成器”交互模式深度解析
开发语言·前端·flutter·ui·交互
linweidong5 小时前
屏幕尺寸的万花筒:如何在 iOS 碎片化生态中以不变应万变?
macos·ios·移动开发·objective-c·cocoa·ios面试·ios面经
雨季6665 小时前
Flutter 三端应用实战:OpenHarmony 简易“可展开任务详情卡片”交互模式深度解析
开发语言·前端·javascript·flutter·ui·交互