preface

I’ve seen a lot of students ask this question recently during the development process. I want a unified processing page to pop up telling the user to check the network connection when a network request fails. Since this behavior can occur on any page, we certainly don’t want to have to re-implement the logic on every page because the coupling would be too high, and our first response would be to process the logic in one part of the network after the request.

This doesn’t seem to be a problem, but if you’ve done this requirement, you’ll see that we need to use the Navigator component when we implement the jump to the prompt page. Think back to how we usually jump.

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

We found that we were missing an important BuildContext element to implement the jump to ErrorPage. The navigator.of (context) operation actually looks for the nearest NavigatorState in the ancestor node. And BuildContext here is the starting point. So a lot of students are stuck here, so let’s solve this problem.

You need to understand the following concepts before you begin this article:

  • Understanding the BuildContext: Flutter | BuildContext
  • Key: Flutter | a simple Key

Understanding navigation principles

What is Navigator, what does the MaterialApp do

We often open a bunch of pages in an app, and when we return, it goes back to the last open page, and then it goes back layer by layer, and yes it’s a stack. In Flutter, the page stack is managed and maintained by the Navigator.

Push a new page to the screen of navigator.of (context). Push removes the top page of the route from navigator.of (context).popCopy the code

Usually when we build an application we don’t manually create a Navigator that can also navigate the page, and that’s why.

Yes, this Navigator is exactly what MaterialApp provides for us. However, if home, Routes, onGenerateRoute and onUnknownRoute are all null and Builder is not null, the MaterialApp will not create any Navigator.

Now that our navigator. of(context) is actually retrieving the NavigatorState instance provided by the MaterialApp. BuildContext is related to the current Element, and unifying control is actually quite complex. Can we use a different way to get the Navigator so that we are not constrained by BuildContext?

Get the Navigator instance

To obtain a Widget we described in the previous article, you can use GlobalKey. So how do we get the Navigator?

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

  @override
  Widget build(BuildContext context) {
    returnMaterialApp( navigatorKey: _navigatorKey, home: HomeScreen(), ); }}Copy the code

Since the MaterialApp encapsulates the Navigator and exposes the Navigator’s key property as the navigatorKey, we only need to bind a GlobalKey.

However, the problem is that it is still not convenient for us to use the GlobalKey externally. Our Navigator may need to be used in multiple places, each containing repetitive code for creating, locating, and managing dependencies if directly dependent. If we just wanted to test for network debugging now, it would be very difficult to do so because we rely on the code associated with Navigator.

This is where the ServiceLocator is needed to help us decouple.

ServiceLocator

This is a classic design pattern whose main purpose is to decouple classes from dependencies so that classes are compiled knowing the implementation of the dependency phase. Thus improving its isolation and testability.

get_it

Today we are going to introduce a library called Get_it from Flutter Community and Thomas Burkhart. It is a lightweight ServiceLocator library with only 99 lines of code (including comments). I suggest you read it whenever you have time.

Simple to fit

Get_it is very simple to use in two steps.

  • The registration service
  • Dependency injection

The registration service

Start by creating a GetIt container object.

GetIt getIt = new GetIt();
Copy the code

The services to be registered are then registered in the container.

getIt.registerSingleton<AppModel>(new AppModelImplementation());
getIt.registerLazySingleton<RESTAPI>(() =>new RestAPIImplementation());
Copy the code

Dependency injection

We still get dependencies through the container where we need them.

var myAppModel = getIt<AppModel>();

You can also use var myAppModel = geti.get

(); This way, the effect is the same.

Since DART supports global variables, we’ll just write the container directly into a DART file. Is it easy?

In this way, our services are created in the container, and we can only rely on the interface when we actually rely on it, and then implement the actual object of the interface through container injection (DI) for decoupling.

Implement NavigateService

Now let’s see how to implement a NavigateService using get_it.

Add the dependent

  • For details, see pub.dev/packages/ge…
  • For details, see juejin.cn/post/684490…

Create the global Locater

We create a new service_locator. Dart file in the project. Then create a global GetIt instance in this file.

import 'package:get_it/get_it.dart';

    final GetIt getIt = GetIt();
    void setupLocator(){}
Copy the code

This is where the setupLocator method is first written, and then the service is registered.

Create NavigateService

We encapsulate navigation-related functions as Services for later use.

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;
}
Copy the code

Get the NavigatorState instance with key.currentState.

I’ve briefly exposed the push and pushName functions of navigation, which you can extend to suit your own capabilities.

The registration service

Now you need to register the service in the container, back to service_locator.dart.

void setupLocator(){
  getIt.registerSingleton(NavigateService());
}
Copy the code

By calling registerSingleton, we register a NavigateService used by the singleton pattern in the container. Then we can register all the services we need to register here.

Container initialization

Having just written the registration function, we now need to initialize it once when our Flutter application runs. The main function is a good choice.

void main() {
  setupLocator();
  runApp(App());
}
Copy the code

This way we can initialize all the services into the container when our program runs.

Dependency injection

As we said earlier, to obtain the Navigator you need to bind a GlobalKey to the MaterialApp’s navigatorKey. So we now bind the GlobalKey through container injection services.

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

NavigateService dependencies are injected above via getIt(). This getIt is our global instance.

Then a named route is added. I’ve put the HomeScreen and ErrorScreen codes below.

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')); }}Copy the code

Clicking on FloatingActionButton in HomeScreen takes you to the ErrorScreen via the injected NavigateService.

When we jump, we can see that the context is not used.

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

This way you can properly handle global navigation operations where you want them. One of its great benefits is that you can use the services in the container not only in widgets, but also anywhere.

Get_it,

Different ways to register

GetIt provides multiple ways to register these objects, which will affect their life cycle. There are currently three:

  • Factory mode:void registerFactory<T>(FactoryFunc<T> func)A new instance is returned each time.
  • Singleton mode:void registerSingleton<T>(T instance)Return the same instance each time. This mode requires manual initialization, as in our example above.
  • Singleton mode (lazy loading) :void registerLazySingleton<T>(FactoryFunc<T> func)In this way, the service is initialized only when the dependency is injected for the first time and the same instance is returned each time.

Cover the registered

If you register the same service twice in the container, by default you get an assertion in debug mode, as shown below.

void setupLocator(){
  getIt.registerSingleton(NavigateService());
  getIt.registerSingleton(NavigateService());
}
Copy the code

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

Get_it thinks you may have made a mistake, so it reminds you that you registered for the same service twice. If you really must override registration, you can turn off this assertion by setting the property allowReassignment == true.

Reset the container

If you want to reset all containers, call the reset() method. It’s usually used when you’re doing tests.

Q&A

The relationship between ServiceLocator and Dependency Injection & Inversion of Control

We saw above that inversion of control (Ioc) was implemented when we used ServiceLocator. Services are no longer created by consumers, but are injected through containers. Instead of relying on concrete implementations, we can rely on a thin layer of interfaces. This way the caller no longer knows the implementation details of the service and can easily replace it with mock data. A ServiceLocator is a special kind of inversion of control.

Dependency Injection solves the same problem as ServiceLocator. However, it differs from THE implementation principle of DI. Since Flutter disables dart’s reflection pack in order to reduce the post-pack application volume, you don’t know the source of the magic injected object, which makes most DI packs that rely on reflection unusable.

Get the performance of the service

The get_it ServiceLocator uses a map to store data.

final _factories = new Map<Type, _ServiceFactory<dynamic> > ();Copy the code

So the performance of getting the service is O(1).

Write in the last

This article refers to the following information:

  • Navigate without context in Flutter with a Navigation Service
  • One to find them all: How to use Service Locators with Flutter

Those of you who are interested can go and read the master’s article.

The library is very lightweight, and you can get used to it very quickly. Here you might find it somewhat similar to the InheritWidget. While both address model dependencies, get_it can be used not only in Widget Tree, but also between model dependencies. You can choose to use it according to your own project.

If there are any problems in the article, please correct them! Feel free to discuss it in the comments section below and at [email protected], and I will reply as soon as possible!

As an aside, my personal blog is also being serialized synchronously! Welcome to duck xinlei.dev/