Flutter | 经过 ServiceLocator 实现无 context 导航

前言

最近在开发过程当中看到不少同窗问过这个问题。我想要在网络请求失败的时候弹出一个统一的处理页面告诉用户检查网络链接。因为这个行为能够发生在任何页面,咱们固然不但愿在每个页面之中都要从新实现一遍这个逻辑,那样耦合就过高了,这时候咱们的第一反应是在网络请求后某个部分统一处理这部分逻辑。设计模式

看上去没什么问题,可是若是你作过这个需求话,你就会发现:当咱们实现跳转提示页面的时候,须要使用到 Navigator 这个组件。回想一下咱们通常是如何进行跳转的。网络

Navigator.of(context).pushNamed('/errorPage');app

咱们发现,要实现跳转到 ErrorPage 这个操做,咱们缺乏了一个重要的元素 BuildContextNavigator.of(context) 操做实际上是在祖先节点中寻找最近的一个 NavigatorState。而这里的 BuildContext 就是寻找的起点。 因此不少同窗都卡在这里了,那咱们就来解决这个问题。less

在正式开始本文以前你须要已经理解下面几个概念:ide

理解导航原理

什么是Navigator,MaterialApp作了什么

咱们常常会在应用中打开许多页面,当咱们返回的时候,它会前后退到上一个打开的页面,而后一层一层后退,没错这就是一个堆栈。而在Flutter中,则是由Navigator来负责管理维护这些页面堆栈。函数

压一个新的页面到屏幕上
    Navigator.of(context).push
    把路由顶层的页面移除
    Navigator.of(context).pop
复制代码

一般咱们咱们在构建应用的时候并无手动去建立一个 Navigator,也能进行页面导航,这又是为何呢。post

没错,这个 Navigator 正是 MaterialApp 为咱们提供的。可是若是 home,routes,onGenerateRoute 和 onUnknownRoute 都为 null,而且 builder 不为 null,MaterialApp 则不会建立任何 Navigator。性能

既然咱们的 Navigator.of(context) 实际上就是在获取 MaterialApp 提供的 NavigatorState 实例。而 BuildContext 跟当前 Element 有关,要统一控制实际上至关复杂。咱们是否可使用另一种方式来获取 Navigator,这样就能够再也不受 BuildContext 的约束了。测试

获取 Navigator 实例

要获取某个 Widget 咱们在以前的文章中介绍了可使用 GlobalKey 来实现。那咱们应该如何获取到 Navigator 呢?ui

class _AppState extends State<App> {
  GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'navigate');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: _navigatorKey,
      home: HomeScreen(),
    );
  }
}
复制代码

因为 MaterialApp 封装了 Navigator,而且将 Navigator 的 key 属性做为 navigatorKey 暴露出来,咱们只须要绑定一个 GlobalKey 就好了。

可是如今问题又来了,咱们假如想要在外部使用这个 GlobalKey 好像仍是不太方便。咱们的 Navigator 可能在多处须要使用,假如直接依赖的话每一处都包含了用于建立、定位和管理依赖项的重复代码。假如咱们如今仅仅只是想进行网络调试的测试,因为依赖了 Navigator 相关的代码,想要进行测试很是困难。

这时候就须要 ServiceLocator 来帮助咱们进行解耦。

ServiceLocator

这是一种经典的设计模式,主要目的是将类与依赖解耦,让类在编译的时候并知道依赖相的具体实现。从而提高其隔离性和可测试性。

get_it

而今天咱们要介绍的是一个来自 Flutter Community 和 Thomas Burkhart 制做的库 get_it。它是一个轻量级 ServiceLocator 库,仅仅用到了 99 行代码(包括注释)。建议有时间都去阅读一下。

简单上手

get_it 很是简单,使用就分两步。

  • 注册服务
  • 依赖注入

注册服务

首先建立出一个 GetIt 容器对象。

GetIt getIt = new GetIt();
复制代码

而后把须要注册的服务在容器中注册。

getIt.registerSingleton<AppModel>(new AppModelImplementation());
getIt.registerLazySingleton<RESTAPI>(() =>new RestAPIImplementation());
复制代码

依赖注入

在须要使用到这个依赖的地方咱们仍是经过这个容器来获取依赖。

var myAppModel = getIt<AppModel>();

你也可使用 var myAppModel = getIt.get<AppModel>(); 这个方法,效果是同样的。

因为 dart 支持全局变量,咱们就把容器直接写在一个 Dart 文件中就行了。是否是很简单呢?

这样咱们的服务就是在容器中建立的,在实际依赖的时候,咱们能够只依赖于接口,而后经过容器注入(DI)实现了该接口的实际对象,达到了解耦的效果。

实现 NavigateService

如今咱们来看看该如何使用 get_it 实现一个 NavigateService。

添加依赖

建立全局 Locater

咱们在项目中新建一个 service_locator.dart 文件。而后在这个文件中建立一个全局 GetIt 实例。

import 'package:get_it/get_it.dart';

    final GetIt getIt = GetIt();
    void setupLocator(){}
复制代码

这里先写上 setupLocator 方法,以后会在这里进行服务注册。

建立 NavigateService

咱们把导航相关的功能封装成 Service,方便以后使用。

import 'package:flutter/material.dart';

class NavigateService {
  final GlobalKey<NavigatorState> key = GlobalKey(debugLabel: 'navigate_key');

  NavigatorState get navigator => key.currentState;

  get pushNamed => navigator.pushNamed;
  get push => navigator.push;
}
复制代码

经过 key.currentState 获取到 NavigatorState 实例。

我这里简单暴露了导航的 push 和 pushName 功能,你能够根据本身的功能来进行扩展。

注册服务

如今就须要在容器中注册这个服务,回到 service_locator.dart。

void setupLocator(){
  getIt.registerSingleton(NavigateService());
}
复制代码

经过调用 registerSingleton,咱们在容器中注册了一个单例模式使用的 NavigateService。以后咱们全部须要注册的 Service 都在这里注册一遍便可。

容器初始化

刚刚已经写好了注册函数,如今就须要在咱们的 Flutter 应用运行时初始化一次,main 函数是一个不错的选择。

void main() {
  setupLocator();
  runApp(App());
}
复制代码

这样在咱们程序运行的时候就可以把服务都初始化到容器中。

依赖注入

刚才咱们说了,要想得到 Navigator 须要在 MaterialApp 的 navigatorKey 绑定一个 GlobalKey。因此咱们如今经过容器注入服务,来绑定这个 GlobalKey。

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: getIt<NavigateService>().key,
      routes: {'/ErrorScreen': (_) => ErrorScreen()},
      home: HomeScreen(),
    );
  }
}
复制代码

上面经过 getIt() 注入了 NavigateService 的依赖。这个 getIt 就是咱们的全局实例。

而后添加了一个命名路由。这里我把 HomeScreen 和 ErrorScreen 的代码放在下面。

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(onPressed: () {
        getIt<NavigateService>().pushNamed('/ErrorScreen');
      }),
    );
  }
}

class ErrorScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      color: Colors.red,
      child: Text('Error'),
    );
  }
}
复制代码

在 HomeScreen 中点击一下 FloatingActionButton 就会经过注入的 NavigateService 跳转到 ErrorScreen。

在进行跳转时,咱们能够看到并无使用 context。

getIt<NavigateService>().pushNamed('/ErrorScreen');

这样你就能够在你想要的地方恰当的处理一些全局导航操做了。它的一个巨大的好处在于你不只能够在 Widget 中使用,并且能够在任何地方使用容器中的服务。

get_it 详解

不一样的注册方式

GetIt 提供了多种注册方式,这将会影响这些对象的生命周期。目前有三种:

  • 工厂模式:void registerFactory<T>(FactoryFunc<T> func) 每次都会返回新的实例。
  • 单例模式:void registerSingleton<T>(T instance) 每次返回同一实例。 这种模式须要手动初始化,就像咱们上面例子中那样。
  • 单例模式(懒加载): void registerLazySingleton<T>(FactoryFunc<T> func) 这种方式只有第一次注入依赖的时候,才会初始化服务,而且每次返回相同实例。

覆盖注册

若是你在容器中注册了两次同一服务的话,默认状况下会在调试模式中获得一个断言,就像下面这样。

void setupLocator(){
  getIt.registerSingleton(NavigateService());
  getIt.registerSingleton(NavigateService());
}
复制代码

Failed assertion: line 53 pos 12: 'allowReassignment || !_factories.containsKey(T)': Type NavigateService is already registered

get_it 会认为你多是写错了,因此提醒你这里注册了两次相同服务。若是你真的必须覆盖注册,那么你能够经过设置属性 allowReassignment == true 来关闭此断言。

重置容器

若是你想要重置全部容器,能够调用 reset() 方法。通常在作测试的时候会用到。

Q&A

ServiceLocator 与 Dependency Injection & Inversion of Control 的关系

咱们在上面看到,当咱们使用 ServiceLocator 以后,实现了控制反转(Ioc)。服务再也不由使用者建立,而是经过容器注入。这样咱们能够再也不依赖于具体的实现,而是依赖于一层薄薄的的接口。这样调用者再也不知道服务具体实现细节,能够很轻松的使用 mock 数据进行替换。ServiceLocator 其实就是一种特殊的控制反转。

Dependency Injection 实际上和 ServiceLocator 解决的是一样的问题。可是它又与DI的实现原理上有所不一样。因为 Flutter 为了减小打包后应用体积禁用了 dart 的反射包,因此你不知道神奇注入对象的来源,这样一来大多数依赖于反射的 DI 包也就无法用了。

获取服务的性能

咱们能够从 get_it 的源码中看到,这个 ServiceLocator 就是用一个 map 在储存数据。

final _factories = new Map<Type, _ServiceFactory<dynamic>>();
复制代码

因此获取服务的性能是 O(1)。

写在最后

本文参考了如下资料:

感兴趣的同窗能够去阅读一下大师的文章。

此次介绍的库很是轻量,你能够很快速的上手它。这里你可能会以为它与 InheritWidget 有些类似。虽然都在解决模型依赖问题,get_it 不只可以在 Widget tree 中进行使用,并且可以解决模型间的依赖问题。你们能够根据本身项目的状况来选择使用。

若是文章中还存在任何问题还请老师指正!欢迎在下方评论区以及个人邮箱1652219550a@gmail.com 一块儿讨论,我会及时回复!

题外话,个人我的博客也在同步连载中!欢迎各位光顾鸭 xinlei.dev/

相关文章
相关标签/搜索