使用GetIt和Injectable在Flutter中进行依赖注入

2,067 阅读7分钟

为什么使用依赖性注入?

今天,构建现代应用程序不仅仅是简单地知道要写什么或有哪些工具,更重要的是要了解你想构建什么。你必须考虑到维护问题,比如你的代码的可读性如何,修复一个错误或增加一个新功能需要多少努力,或者从项目库更新的破坏性变化中更新项目。考虑到这些问题,它并不像简单的编写和完成那样容易。还有更多的事情要做。

例如,在编写Flutter应用程序时,你经常需要一个类依赖于另一个类的功能或方法。解决这个问题的方法是简单地在该类中创建一个新的实例,你就可以了。

当你需要对一个依赖于其他多个类的特定类或函数运行测试时,问题就出现了。依赖性注入试图解决这个问题。

依赖性注入是使一个类独立于它自己的依赖关系的一种简单方法。它允许你以一种更可维护的方式分离应用程序的不同部分,因为每个类都可以调用它所需要的任何依赖关系。这创建了一个松散耦合的应用程序,有助于运行和编写测试,并使错误修复和功能改进更容易和更快。

在这篇文章中,我将建立一个示例应用程序,并解释如何使用GetItInjectable在你自己的Flutter项目中实现依赖注入。

为什么使用GetIt和Injectable?

GetIt是一个服务定位器,它允许你创建接口和它们的实现,并在你的应用程序中的任何地方全局地访问这些实现。Injectable生成代码,否则我们会通过使用注解来编写。这使我们能够更多地关注逻辑,而不是如何访问它。

构建一个Flutter示例应用程序

为了让我们更好地理解如何在Flutter应用程序中使用依赖注入,我们将使用Firebase和Bloc制作一个简单的笔记应用程序。我们将学习如何进行网络调用,以及如何将重复的功能分离成可以在任何地方访问的服务。

在这个项目中,我不会向你展示如何安装Firebase或连接它,因为这已经超出了本主题的范围。要学习如何用Flutter安装Firebase,你可以访问这里的文档。

开始吧

我们将使用Android Studio创建一个新的项目(如果你愿意,也可以使用命令行)。

对于Android Studio,你可以使用它提供的GUI来创建一个新的项目,或者使用以下命令。

flutter create name_of_your_app

命令完成后,用你喜欢的IDE(无论是Visual Studio Code还是Android Studio)打开它。

在你的pubspec.yaml 文件中添加以下依赖项。

dependencies:
  flutter:
  sdk: flutter
  // Our service locator
  get_it: ^7.2.0
  // For state management
  bloc: ^8.0.1
  // Allows value based equality for our classes e.g Eat() == Eat() is true
  equatable: ^2.0.3
  // Generates code for us by providing annotations we can use
  injectable: ^1.5.0
  // Allows converting json to dart class and back 
  json_annotation: ^4.4.0
  // Allows easier routing
  auto_route: ^3.2.0
  // Required to work with firebase. 
  firebase_core: ^1.11.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  # add the generator to your dev_dependencies
  injectable_generator:
  # add build runner if not already added
  build_runner:

get_it 将作为我们的服务定位器使用。Injectable将与injectable_generator 一起使用,为我们生成代码。我们将通过在我们想要的类上使用注解来给它具体的指示,它将处理其余的事情。

最后,build_runner 允许我们使用命令行来生成文件。从你的命令行运行flutter pub get ,以获得所有的依赖性。

这个过程需要一个全局文件来提供你要使用的资源。在你的lib 文件夹中创建一个文件,将其命名为injection.dart ,并添加以下代码。

import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

final getIt = GetIt.instance;

@InjectableInit()
void configureDependencies() => $initGetIt(getIt);

这将处理get_it 的新文件的生成。要生成该文件,运行以下命令。

flutter pub run build_runner build --delete-conflicting-outputs

这段代码会生成一个名为injection.config.dart 的新文件,它将包括所有用例的所有依赖关系。

然后我们可以在主函数中加入configureDependencies() 。这样就可以先运行服务,以备在应用运行前有任何生成的令牌或异步函数需要解决。

void main() {
  configureDependencies();
  runApp(MyApp());
} 

我们的应用程序现在已经设置好了,我们可以继续进行更有趣的功能。

构建样本应用程序的主要功能

在我们开始允许用户登录和注册之前,我们将需要创建某些关键功能。我们将从Firebase、我们的接口、实现和我们的状态管理开始,然后完成我们的用户界面。这个流程要好得多,因为它将解释如何处理在一个真实世界的项目中建立一个类似的应用程序。

首先,要在Flutter应用程序中使用Firebase,你需要首先像这样调用一个异步函数。

await Firebase.initializeApp()

这个函数在本地进行必要的调用,并使用添加到Android和iOS文件夹中的配置文件将应用程序连接到云服务。这个函数需要在小部件重建之前在主函数中调用。我们可以用GetIt来做,因为有一个configureDependencies() ,我们可以用它来进行异步调用。

接下来,我们将创建一个新的服务文件夹,并在其中创建一个应用模块,在这里我们可以注册所有的服务(在这种情况下,我们的Firebase服务)。我们将添加服务并创建一个静态函数,等待初始化的完成。完成后,我们将返回该类的实例。

class FirebaseService {
  static Future<FirebaseService> init() async {
    await Firebase.initializeApp();
    return FirebaseService();
  }
}

然后,在我们的应用程序模块中,我们将使用preResolve 注解来添加它,这意味着我们将需要应用程序在继续其他工作之前初始化该函数。

@module
abstract class AppModule {
  @preResolve
  Future<FirebaseService> get fireService => FirebaseService.init();
}

模块注解是用来标记类为模块的。运行生成器命令后,我们在injectable.config.dart 内得到以下生成的代码。

Future<_i1.GetIt> $initGetIt(_i1.GetIt get,
    {String? environment, _i2.EnvironmentFilter? environmentFilter}) async {
  final gh = _i2.GetItHelper(get, environment, environmentFilter);
  final appModule = _$AppModule();
  await gh.factoryAsync<_i3.FirebaseService>(() => appModule.fireService,
      preResolve: true);
  return get;
}

因为它返回的是一个未来,我们需要在async/awaitconfigureDependencies ,否则代码将永远无法通过这个阶段。

injection.dart 类中,我们将做如下修改。

final locator = GetIt.instance;

@InjectableInit()
Future<void> configureDependencies() async => await $initGetIt(locator);

现在让我们添加一个返回类型为Future ,然后async/awaitinitGetIt 函数。在这之后,我们将在main.dart 文件中再做一次更新,并在函数上调用await,如下所示。

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await configureDependencies();
  runApp(Dependo());
}

当我们运行应用程序时,一切都在正常运行。

Blank Flutter app that just reads "dependency injection"

认证

为了允许使用电子邮件和密码进行认证,我们将需要添加Firebase认证。在pubspec.yaml 中添加以下包。

firebase_auth: ^3.3.5

现在运行flutter pub get ,并重新启动以确保一切运行良好。一旦它看起来不错,我们将需要为认证添加一个接口。使用接口是很重要的,因为它将允许你在不影响你的主要实现的情况下进行模拟测试,而主要实现可以访问你的API。

为了在Flutter中创建一个接口,我们使用关键字abstract。但首先,在lib 文件夹下添加一个data 文件夹,然后再添加一个名为repository 。然后,添加一个i_auth_facade.dart 文件。
你的结构应该如下图所示。

File structure in Flutter app

在最后一个文件中添加以下功能。

abstract class IAuthFacade {
  Future<void> signIn({required String email, required String password,});
  Future<void> register({required String username, required String email, required String password});
  Future<User?> getUser();
  Future<void> logOut();
}

impl 文件夹下创建一个新文件,称为auth_facade.dart 。这将起到为上述函数添加实现的作用。

我们将实现IAuthFacade 类,并将其提供给我们的服务定位器GetIt,方法是用Injectable作为接口注释该类。这意味着我们可以在任何地方使用该接口,而Injectable将使用这里创建的实现(当我们进入签到和注册部分时,我将进一步解释)。

@Injectable(as: IAuthFacade)
class AuthFacade implements IAuthFacade {
  @override
  Future<User?> getUser() {
    // TODO: implement getUser
    throw UnimplementedError();
  }

  @override
  Future<void> register({required String username, required String email, required String password}) {
    // TODO: implement register
    throw UnimplementedError();
  }

  @override
  Future<void> signIn({required String email, required String password}) {
    // TODO: implement signIn
    throw UnimplementedError();
  }
}

在我们为该类添加功能之前,我们需要创建我们的User 类,像这样。

@JsonSerializable()
class User extends Equatable {
  String id;
  final String email;
  final String username;

  User({required this.id, required this.email, required this.username});

  @override
  List<Object?> get props => [this.id, this.email, this.username];

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

函数fromDocument 将允许我们把存储在Firebase的Cloud Firestore中的用户文档转换成我们的User 类。

为了使用Cloud Firestore,在你的pubspec.yaml 文件中加入以下内容。

cloud_firestore: ^3.1.6

从终端运行flutter pub get ,并使其可以使用我们的app.module

@module
abstract class AppModule {
  // ....
  @injectable
  FirebaseFirestore get store => FirebaseFirestore.instance;

  @injectable
  FirebaseAuth get auth => FirebaseAuth.instance;
}

现在我们可以向我们的Facade 提供服务,如下所示。

@Injectable(as: IAuthFacade)
class AuthFacade implements IAuthFacade {
  final FirebaseAuth _firebaseAuth;
  final FirebaseFirestore _firebaseFirestore;

  AuthFacade(this._firebaseAuth, this._firebaseFirestore);

  // ...Implementation..
  }

GetIt将查看我们的AuthFacade 需要的类型并提供它们。这很好,因为我们将不必从Facade 类中实例化服务。

生成的代码将如下所示。

Future<_i1.GetIt> $initGetIt(_i1.GetIt get,
    {String? environment, _i2.EnvironmentFilter? environmentFilter}) async {
  final gh = _i2.GetItHelper(get, environment, environmentFilter);
  final appModule = _$AppModule();
  // The services are provided here
  gh.factory<_i3.FirebaseAuth>(() => appModule.auth);
  gh.factory<_i4.FirebaseFirestore>(() => appModule.store);

  await gh.factoryAsync<_i5.FirebaseService>(() => appModule.fireService,
      preResolve: true);

  // GetIt supplies the instances here
  gh.factory<_i6.IAuthFacade>(() =>
      _i7.AuthFacade(get<_i3.FirebaseAuth>(), get<_i4.FirebaseFirestore>()));
  return get;
}

重新运行该应用程序,以确保一切工作正常。

我们现在可以为IAuthFacade 提供实现。

@Injectable(as: IAuthFacade)
class AuthFacade implements IAuthFacade {
  final FirebaseAuth _firebaseAuth;
  final FirebaseFirestore _firebaseFirestore;

  AuthFacade(this._firebaseAuth, this._firebaseFirestore);

  @override
  Future<u.User?> getUser() async {
    try {
      final uid = _firebaseAuth.currentUser!.uid;
      final currentUser = await _firebaseFirestore.doc("users/$uid").snapshots().first;
      return currentUser.toUser();
    } on FirebaseAuthException catch(e) {
      print("We failed ${e.message}");
    }
  }

  @override
  Future<void> register({required String username, required String email, required String password}) {
      return _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password)
          .then((value) async {
            return _firebaseFirestore.doc("users/${value.user!.uid}")
        .set({"email": email, "username": username});
      });
  }

  @override
  Future<void> signIn({required String email, required String password}) {
    return _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
  }

  @override
  Future<void> logOut() => _firebaseAuth.signOut();
}

// Simple extension to convert firestore document snapshots to our class
extension DocumentSnapX on DocumentSnapshot<Map<String, dynamic>> {
  u.User toUser() {
    return u.User.fromJson(this.data()!)
        ..id = this.id;
  }
}

我们需要IAuthFacade 中的Firestore,以允许我们从云端Firestore中访问签入的用户。我们不能从Firebase认证中访问当前的用户ID,所以要添加像username 这样的属性,你需要使用signIn ,然后在Cloud Firestore中使用签到的用户ID创建一个新文档。

有了这样的设置,就可以更容易地与资源库一起工作。比如说。

@injectable
class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
  final IAuthFacade authFacade;

  LoginFormBloc(this.authFacade) : super(LoginFormInitial()) {
      // Update login state according to events
      on<LoginButtonPressed>((event, emit) async {
      final currentState = state as LoginFormState;
        final data = authFacade.signIn(currentState.email, currentState.password);
      })
    }
  }

结论

当涉及到依赖注入时,GetIt和Injectable是一个完美的搭配。当涉及到可读和易于维护的代码时,你需要了解正确的工具。要了解我们建立的应用程序,你可以通过这个链接在GitHub中找到存储库。