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

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

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

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

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

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

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

你会在这篇文章里看到:

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

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

一、理解我们的工具: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 里,使用 MultiProvider 将 provider 和 get_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 中,我们使用 Consumer 或 Selector 来消费 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 参数进行优化

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

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

相关推荐
yuanlaile2 小时前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭13 小时前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白13 小时前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
传奇开心果编程1 天前
【React Native娓娓道来】第7课 表单与用户输入:TextInput、表单校验与键盘处理
移动开发·表单·编程语言理论·开发范式·编程框架·声明式ui·用户输入
传奇开心果编程1 天前
【React Native娓娓道来】第9课 动画与手势:Animated、Reanimated 与 Pressable 进阶
移动开发·动画·手势·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程1 天前
【声明式UI开发实用技术学与练】第2课 状态建模:把界面拆成可预测的数据
移动开发·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程1 天前
【React Native娓娓道来】第3课 组件组合与列表渲染:把界面拆成可复用的函数
移动开发·列表渲染·编程语言理论·开发范式·编程框架·声明式ui·组件组合
陆断枫1 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
传奇开心果编程1 天前
【声明式UI开发实用技术学与练】第6课 副作用管理:Effect 的正确用法
移动开发·编程语言理论·开发范式·编程框架·声明式ui
程序员老刘1 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端