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.');
}
}
几个关键特点:
-
灵活的注册方式:你可以根据需求选择注册为单例(全局一个)、懒加载单例(第一次用到时才创建)或者工厂(每次都新建)。
-
作用域支持 :这在测试时特别有用。你可以临时创建一个新的、隔离的作用域,在里面注册模拟服务,测试完了再切回来,完全不影响主流程。
dart// 测试时非常方便 final testLocator = GetIt.asNewInstance(); testLocator.registerSingleton<ApiService>(MockApiService()); -
异步初始化 :有些服务启动时需要异步操作(比如读本地配置),
get_it可以等它们都准备好再通知你。dartawait 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 愉快!