likes
comments
collection
share

Flutter大型项目架构:依赖管理篇

作者站长头像
站长
· 阅读数 8

Flutter大型项目架构:依赖管理篇

为啥需要依赖管理

你可能会问,为啥搞那么麻烦使用依赖管理?不就是实例化的时候多写几句代码嘛,没有用它照样能把功能完成,不是吗?这么说也没错,如果只是在项目的几个位置这样写,问题也不大,可是放大到大型的项目中呢,该如何应对呢?下面这段代码是没有使用依赖管理之前的:

class RepositoryTool {
  ApiService _apiService;
  AppPreferences _appPreferences;
  AppDatabase _appDatabase;

  factory RepositoryTool() => _singleton;
  static final RepositoryTool _singleton = RepositoryTool._();
  static RepositoryTool get instance => RepositoryTool();

  RepositoryTool._() {
    _apiService = ApiService();
    _appPreferences = AppPreferences();
    _appDatabase = AppDatabase();
  }

  // 登录请求
  Future<void> login({
    required String username,
    required String password,
  }) async {
    _appApiService.login(username: username, password: password);
  }

  // 从数据库中获取用户信息
  Future<User> getLocalUser(int id) async {
    _appDatabase.getLocalUser(id);
  }
}

这里单例类 RepositoryTool 为整个应用提供数据的,其有三个成员变量:_apiService 负责接口请求服务;_appPreferences 负责从偏好设置中读取或者存储数据,主要是一些字段或者 Bool 值;_appDatabase 负责从数据库中读取或者存储数据,这三个也是应用中经常使用的,它们都和数据相关却各司其职。但是当我们需更改一下 _apiService 的构造函数的时候,如下面的代码:

class ApiService {
  ApiService(
    this._noneAuthApiClient, 
    this._authApiClient);

  final NoneAuthApiClient _noneAuthApiClient;
  final AuthApiClient _authApiClient;

  // 登录操作
  Future<void> login({String username, String password}) async {
    await _authApiClient.request(
        method: RestMethod.post,
        path: '/v1/auth/login',
        queryParameters: {"username": username, "password": password});
    }
}

// 继承 RestApiClient,用于发送不带 Token 的接口请求,如登录操作
class NoneAuthApiClient extends RestApiClient {
  NoneAuthApiClient(HeaderInterceptor _headerInterceptor)
super(baseUrl: "" interceptors: [
          _headerInterceptor,
        ]);
}

// 继承 RestApiClient,用于发送带 Token 的接口请求
class AuthApiClient extends RestApiClient {
  AuthApiClient(
    HeaderInterceptor _headerInterceptor,
    AccessTokenInterceptor _accessTokenInterceptor,
    RefreshTokenInterceptor _refreshTokenInterceptor,
  ) : super(baseUrl: "", interceptors: [
          _headerInterceptor,
          _accessTokenInterceptor,
          _refreshTokenInterceptor,
        ]);
}

// Api 请求的基类
class RestApiClient {
  RestApiClient({
    this.baseUrl = '',
    this.interceptors = const [],
  })

  // 用 Dio 发送请求
  Future<T> request<T, D>(){}
}

这个时候又得回到 RepositoryToolRepositoryTool._() 修改实例 _apiService 初始化代码,而在 ApiService 类中的成员变量 _noneAuthApiClient_authApiClient 也需要在实例化的时候传入不同的 Interceptor

同样 _appPreferences_appDatabase 的构造函数也是需要传入不同类实例参数,如果不把这些参数放在构造函数中传入的话,就需要在用到地方去实例化。如 RepositoryToolRepositoryTool._() 的代码,导致这些类 ApiServiceAppPreferencesAppDatabaseRepositoryTool 紧密耦合在一起,难以进行单元测试和扩展,而且,构造函数一改其它所有用到的地方都得改,代码维护成本太高了。

依赖管理是什么

用过 get_it 的同学可能会说,用依赖注入不就可以优化上面的问题吗,和依赖管理有什么关系呢?依赖注入(Dependency Injection,简称:DI)和依赖管理(Dependency Management)在软件开发中确实是两个相关但不同的概念。

依赖注入是一种软件设计模式,它通过将依赖关系从代码中分离出来,并由外部系统在运行时动态地注入到代码中,从而实现了依赖的管理,是一种实现依赖管理的技术手段。

依赖注入需要依赖管理来实现,在依赖注入的实践过程中,需要一个依赖管理的机制来管理各个组件之间的依赖关系。这包括管理依赖的声明周期、版本、配置等信息,并确保依赖的正确注入和使用。

如下图体现出来的各个组件的依赖关系:

Flutter大型项目架构:依赖管理篇

在主工程 App 中的 LoginPage 发送登录请求,上图中是在 Bloc 调用抽象类 Repository 登录函数,Repository 是在 domain 组件包的,实际上登录操作的实现是在 data 组件包中的类 RepositoryImpllogin 函数。App 只需要依赖 domain 组件包,不直接依赖 data,而这之间的实现代码则是由DI 帮我们完成的。

Flutter 中依赖管理方案

abstract class AppNavigator {
  const AppNavigator();

  int get currentBottomTab;

  Future<T?> push<T extends Object?>(AppRouteInfo appRouteInfo);

  Future<bool> pop<T extends Object?>({
    T? result,
    bool useRootNavigator = false,
  });
}

这里的 AppNavigator 还是 abstract 类,和 Repository 一样,也是在 domain 组件包中,从上面的各个组件的依赖关系图看出,AppNavigator 实现类 AppNavigatorImpl 放在了主工程 App 组件包中的。

AppNavigatorImpl 实现代码如下:

import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:domain/domain.dart';
import 'package:flutter/material.dart' as m;
import 'package:injectable/injectable.dart';

@LazySingleton(as: AppNavigator)
class AppNavigatorImpl extends AppNavigator{
  AppNavigatorImpl(
    this._appRouter,
  );

  final AppRouter _appRouter;

  @override
  int get currentBottomTab {
    return 0;
  }

  @override
  Future<T?> push<T extends Object?>(AppRouteInfo appRouteInfo) {
    return _appRouter.push<T>(_appRouteInfoMapper.map(appRouteInfo));
  }

  @override
  Future<bool> pop<T extends Object?>({T? result, bool useRootNavigator = false}) {
    return _appRouter.pop<T>(result);
  }
}

AppNavigatorImpl 中实现的路由导航是插件 auto_route,通过构造函数传过来,此时运行

flutter packages pub run build_runner build

di.config.dart 自动生成了如下代码:

import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;
import 'package:domain/domain.dart' as _i4;
import 'package:app/navigation/routes/app_router.dart' as _i5;
import 'package:app/app.dart' as _i6;
import 'package:app/navigation/app_navigator_impl.dart' as _i7;

extension GetItInjectableX on _i1.GetIt {
  // initializes the registration of main-scope dependencies inside of GetIt
  _i1.GetIt init({
    String? environment,
    _i2.EnvironmentFilter? environmentFilter,
  }) {
    final gh = _i2.GetItHelper(
      this,
      environment,
      environmentFilter,
    );

    gh.lazySingleton<_i5.AppRouter>(() => _i5.AppRouter());
    gh.lazySingleton<_i4.AppNavigator>(() => _i7.AppNavigatorImpl(
          gh<_i6.AppRouter>(),
        ));
    return this;
  }
}

DI 生成的代码也能看出它们之间的依赖关系,在用到导航跳转的时候从 get_it 容器中取出使用即可:

late final AppNavigator navigator = GetIt.instance.get<AppNavigator>();
navigator.push(...);

这样调用的时候是通过抽象类 AppNavigator ,不需要关心导航的具体是怎么实现的。或许哪天不用插件 auto_route 来做路由导航,只需要继承 AppNavigator 的并实现它的函数即可,而调用的地方不需要做任何更改。

上面的例子是以 Navigator 相关的作为切入点,其实在大型 Flutter 项目中用这种 并依赖于抽象而不是具体的实现 的理念来做依赖管理的地方有很多,这样做的好处是使得代码更加清晰、模块化,并且方便进行单元测试和维护。同时,通过依赖注入容器的注册和获取,可以实现依赖对象的延迟加载和单例管理,提高了应用程序的性能和效率。