Flutter Modular使用教程

牛奶燕麥發表於2021-08-21

什麼是Flutter Modular?

隨著應用專案發展和變得越來越複雜,保持程式碼和專案結構可維護和可複用越來越難。Modular提供了一堆適配Flutter的解決方案來解決這些問題,比如依賴注入,路由系統和“一次性單例”系統(也就是說,當注入模組超出範圍時,模組化自動配置注入模組)。

Modular的依賴注入為任何狀態管理系統提供了開箱即用的支援,管理你應用的記憶體。

Modular也支援動態路由和相對路由,像在Web一樣。

Modular結構

Modular結構由分離和獨立的模組組成,這些模組將代表應用程式的特性。 每個模組都位於自己的目錄中,並控制自己的依賴關係、路由、頁面、小部件和業務邏輯。因此,您可以很容易地從專案中分離出一個模組,並在任何需要的地方使用它。

Modular支柱

這是Modular關注的幾個方面:

  • 自動記憶體管理
  • 依賴注入
  • 動態和相對路由
  • 程式碼模組化

在專案中使用Modular

安裝

開啟你專案的pubspec.yaml並且新增flutter_modular作為依賴:

dependencies:
  flutter_modular: any
複製程式碼

在一個新專案中使用

為了在新專案中使用Modular,你必須做一些初始化步驟:

  1. MaterialApp建立你的main widget並且呼叫MaterialApp().modular()方法。

    // app_widget.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_modular/flutter_modular.dart';
    
    class AppWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          initialRoute: "/",
        ).modular();
      }
    }
    複製程式碼
  2. 建立繼承自Module的你專案的main module檔案:

    // app_module.dart
    class AppModule extends Module {
    
      // Provide a list of dependencies to inject into your project
      @override
      final List<Bind> binds = [];
    
      // Provide all the routes for your module
      @override
      final List<ModularRoute> routes = [];
    
    }
    複製程式碼
  3. main.dart檔案中,將main module包裹在ModularApp中以使Modular初始化它:

    // main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_modular/flutter_modular.dart';
    
    import 'app/app_module.dart';
    
    void main() => runApp(ModularApp(module: AppModule(), child: AppWidget()));
    複製程式碼
  4. 完成!你的應用已經設定完成並且準備好和Modular一起工作!

建立child modules

你可以在你的專案中建立任意多module:

class HomeModule extends Module {
  @override
  final List<Bind> binds = [
    Bind.singleton((i) => HomeBloc()),
  ];

  @override
  final List<ModularRoute> routes = [
    ChildRoute('/', child: (_, args) => HomeWidget()),
    ChildRoute('/list', child: (_, args) => ListWidget()),
  ];

}
複製程式碼

你可以通過module引數將子模組傳遞給你main module中的一個Route

class AppModule extends Module {

  @override
  final List<ModularRoute> routes = [
    ModuleRoute('/home', module: HomeModule()),
  ];
}
複製程式碼

我們建議你講程式碼分散到不同模組中,例如一個AuthModule,並將與此模組相關的所有路由放入其中。通過這樣做,維護和與其他專案分享你的程式碼將變得更加容易。

**注意:**使用ModuleRoute物件建立複雜的路由。

新增路由

模組路由是通過覆蓋routes屬性來提供的。

// app_module.dart
class AppModule extends Module {

  // Provide a list of dependencies to inject into your project
  @override
  final List<Bind> binds = [];

  // Provide all the routes for your module
  @override
  final List<ModularRoute>  routes = [
      // Simple route using the ChildRoute
      ChildRoute('/', child: (_, __) => HomePage()),
      ChildRoute('/login', child: (_, __) => LoginPage()),
  ];
}
複製程式碼

**注意:**使用ChildRoute物件來建立簡單路由。

動態路由

你可以使用動態路由系統來提供引數給你的Route

// 使用 :引數名 語法來為你的路由提供引數。
// 路由引數可以通過' args '獲得,也可以在' params '屬性中訪問,
// 使用方括號符號 (['引數名']).

@override
final List<ModularRoute> routes = [
  ChildRoute(
    '/product/:id',
    child: (_, args) => Product(id: args.params['id']),
  ),
];
複製程式碼

當呼叫給定路由時,引數將是模式匹配的。例如:

// In this case, `args.params['id']` will have the value `1`.
Modular.to.pushNamed('/product/1');
複製程式碼

你也可以在多個介面中使用它。例如:

@override
final List<ModularRoute> routes = [
  // We are sending an ID to the DetailPage
  ChildRoute(
    '/product/:id/detail',
    child: (_, args) => DetailPage(id: args.params['id']),
  ),
  // We are sending an ID to the RatingPage
  ChildRoute(
    '/product/:id/rating',
    child: (_, args) => RatingPage(id: args.params['id']),
  ),
];
複製程式碼

與第一個例項相同,我們只需要呼叫這個路由。例如:

// In this case, modular will open the page DetailPage with the id of the product equals 1
Modular.to.navigate('/product/1/detail');
// We can use the pushNamed too

// The same here, but with RatingPage
Modular.to.navigate('/product/1/rating');

複製程式碼

然而,這種表示法只對簡單的文字有效。

傳送物件

如果你想傳遞一個複雜物件給你的路由,通過arguments引數傳遞給它::

Modular.to.navigate('/product', arguments: ProductModel());
複製程式碼

並且,它將通過args.data屬性提供而不是args.params

@override
final List<ModularRoute> routes = [
  ChildRoute(
    '/product',
    child: (_, args) => Product(model: args.data),
  ),
];
複製程式碼

你可以直接通過binds來找回這些引數:


@override
final List<Bind> binds = [
  Bind.singleton((i) => MyController(data: i.args.data)),
];

複製程式碼

路由泛型型別

你可以從導航返回一個值,就像.pop。為了實現這個,將你期望返回的引數作為型別引數傳遞給Route:

@override
final List<ModularRoute> routes = [
  // This router expects to receive a `String` when popped.
  ChildRoute<String>('/event', child: (_, __) => EventPage()),
]
複製程式碼

現在,使用.pop就像你使用Navigator.pop

// Push route
String name = await Modular.to.pushNamed<String>('/event');

// And pass the value when popping
Modular.to.pop('banana');
複製程式碼

路由守衛

路由守衛是一種類似中介軟體的物件,允許你從其它路由控制給定路由的訪問許可權。你通過讓一個類implements RouteGuard可以實現一個路由守衛.

例如,下面的類只允許來自/admin的路由的重定向:

class MyGuard implements RouteGuard {
  @override
  Future<bool> canActivate(String url, ModularRoute route) {
    if (url != '/admin'){
      // Return `true` to allow access
      return Future.value(true);
    } else {
      // Return `false` to disallow access
      return Future.value(false);
    }
  }
}
複製程式碼

要在路由中使用你的RouteGuard,通過guards引數傳遞:

@override
final List<ModularRoute> routes = [
  final ModuleRoute('/', module: HomeModule()),
  final ModuleRoute(
    '/admin',
    module: AdminModule(),
    guards: [MyGuard()],
  ),
];

複製程式碼

如果你設定到module route上,RouteGuard將全域性生效。

如果RouteGuard驗證失敗,新增guardedRoute屬性來新增路由選擇路由:

@override
final List<ModularRoute> routes = [
    ChildRoute(
      '/home',
      child: (context, args) => HomePage(),
      guards: [AuthGuard()],
      guardedRoute: '/login',
    ),
    ChildRoute(
      '/login',
      child: (context, args) => LoginPage(),
    ),
];

複製程式碼

什麼時候和如何使用navigate或pushNamed

你可以在你的應用中使用任何一個,但是需要理解每一個。

pushNamed

無論何時使用,這個方法都將想要的路由放在當前路由的上面,並且您可以使用AppBar上的後退按鈕返回到上一個頁面。 它就像一個模態,它更適合移動應用程式。

假設你需要深入你的路線,例如:

// Initial route
Modular.to.pushNamed('/home');
// User route
Modular.to.pushNamed('/home/user');
// User profile route
Modular.to.pushNamed('/home/user/profile');
複製程式碼

最後,您可以看到返回到前一頁的back按鈕,這加強了模態頁面在前一頁上面的想法。

navigate

它刪除堆疊中先前的所有路由,並將新路由放到堆疊中。因此,在本例中,您不會在AppBar中看到後退按鈕。這更適合於Web應用程式

假設您需要為移動應用程式建立一個登出功能。這樣,您需要從堆疊中清除所有路由。

// Initial route
Modular.to.pushNamed('/home');
// User route
Modular.to.pushNamed('/home/user');
// User profile route
Modular.to.pushNamed('/home/user/profile');

// Then you need to go again to the Login page, only use the navigation to clean all the stack.
Modular.to.navigate('/login');
複製程式碼

Relative Navigation

要在頁面之間導航,請使用Modular.to.navigate

Modular.to.navigate('/login');
複製程式碼

你可以使用相對導航來導航,就像在web程式一樣:

// Modules Home → Product
Modular.to.navigate('/home/product/list');
Modular.to.navigate('/home/product/detail/3');

// Relative Navigation inside /home/product/list
Modular.to.navigate('detail/3'); // it's the same as /home/product/detail/3
Modular.to.navigate('../config'); // it's the same as /home/config
複製程式碼

您仍然可以使用舊的Navigator API來堆疊頁面。

Navigator.pushNamed(context, '/login');
複製程式碼

或者,您可以使用Modular.to.pushhnamed,你不需要提供BuildContext:

Modular.to.pushNamed('/login');
複製程式碼

Flutter Web URL routes (Deeplink-like)

路由系統可以識別URL中的內容,並導航到應用程式的特定部分。動態路由也適用於此。例如,下面的URL將開啟帶有引數的Product檢視。args.params['id']設定為1。

https://flutter-website.com/#/product/1
複製程式碼

它也可以處理查詢引數或片段:

https://flutter-website.com/#/product?id=1
複製程式碼

路由過渡動畫

通過設定Route的轉換引數,提供一個TransitionType,您可以選擇在頁面轉換中使用的動畫型別。

ModuleRoute('/product',
  module: AdminModule(),
  transition: TransitionType.fadeIn,
), //use for change transition
複製程式碼

如果你在一個Module中指定了一個過渡動畫,那麼該Module中的所有路由都將繼承這個過渡動畫。

自定義過渡動畫路由

你也可以通過將路由器的transitioncustomTransition引數分別設定為TransitionType.custom和你的CustomTransition來使用自定義的過渡動畫:

import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

CustomTransition get myCustomTransition => CustomTransition(
    transitionDuration: Duration(milliseconds: 500),
    transitionBuilder: (context, animation, secondaryAnimation, child){
      return RotationTransition(turns: animation,
        child: SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(-1.0, 0.0),
            end: Offset.zero,
          ).animate(animation),
          child: ScaleTransition(
            scale: Tween<double>(
              begin: 0.0,
              end: 1.0,
            ).animate(CurvedAnimation(
              parent: animation,
              curve: Interval(
                0.00,
                0.50,
                curve: Curves.linear,
              ),
            ),
            ),
            child: child,
          ),
        ),
      )
      ;
    },
  );
複製程式碼

依賴注入

可以通過重寫Modulebinds的getter將任何類注入到Module中。典型的注入例子有BLoCs、ChangeNotifier例項或(MobX)。

一個Bind物件負責配置物件注入。我們有4個Bind工廠型別和一個AsyncBind

class AppModule extends Module {

  // Provide a list of dependencies to inject into your project
  @override
  List<Bind> get binds => [
    Bind((i) => AppBloc()),
    Bind.factory((i) => AppBloc()),
    Bind.instance(myObject),
    Bind.singleton((i) => AppBloc()),
    Bind.lazySingleton((i) => AppBloc()),
    AsyncBind((i) => SharedPreferences.getInstance())
  ];
...
}
複製程式碼

Factory

每當呼叫類時例項化它。

@override
List<Bind> get binds => [
    Bind.factory((i) => AppBloc()),
];
複製程式碼

Instance

使用已經例項化的物件。

@override
List<Bind> get binds => [
    Bind.instance((i) => AppBloc()),
];
複製程式碼

Singleton

建立一個類的全域性例項。

@override
List<Bind> get binds => [
	Bind.singleton((i) => AppBloc()),
];
複製程式碼

LazySingleton

只在第一次呼叫類時建立一個全域性例項。

@override
List<Bind> get binds => [
    Bind.lazySingleton((i) => AppBloc()),
];
複製程式碼

AsyncBind

若干類的一些方法返回一個Future。要注入那些特定方法返回的例項,你應該使用AsyncBind而不是普通的同步繫結。使用Modular.isModuleReady<Module>()等待所有AsyncBinds解析,以便放開Module供使用。

重要:如果有其他非同步繫結的相互依賴,那麼AsyncBind的順序很重要。例如,如果有兩個AsyncBind,其中A依賴於B, AsyncBind B必須在A之前宣告。注意這種型別的順序!

import 'package:flutter_modular/flutter_modular.dart' show Disposable;

// In Modular, `Disposable` classes are automatically disposed when out of the module scope.

class AppBloc extends Disposable {
  final controller = StreamController();

  @override
  void dispose() {
    controller.close();
  }
}
複製程式碼

isModuleReady

如果你想確保所有的AsyncBinds都在Module載入到記憶體之前被解析,isModuleReady是一個方法。使用它的一種方法是使用RouteGuard,將一個AsyncBind新增到你的AppModule中,並將一個RouteGuard新增到你的ModuleRoute中。

class AppModule extends Module {
  @override
  List<Bind> get binds => [
    AsyncBind((i)=> SharedPreferences.getInstance()),
  ];

  @override
  List<ModularRoute> get routes => [
    ModuleRoute(Modular.initialRoute, module: HomeModule(), guards: [HomeGuard()]),
  ];
}
複製程式碼

然後,像下面這樣建立一個RouteGuard。這樣,在進入HomeModule之前,模組化會評估你所有的非同步依賴項。

import 'package:flutter_modular/flutter_modular.dart';

class HomeGuard extends RouteGuard {
  @override
  Future<bool> canActivate(String path, ModularRoute router) async {
    await Modular.isModuleReady<AppModule>();
    return true;
  }
}
複製程式碼

在檢視中檢索注入的依賴項

讓我們假設下面的BLoC已經定義並注入到我們的模組中(就像前面的例子一樣):

import 'package:flutter_modular/flutter_modular.dart' show Disposable;

// In Modular, `Disposable` classes are automatically disposed when out of the module scope.

class AppBloc extends Disposable {
  final controller = StreamController();

  @override
  void dispose() {
    controller.close();
  }
}
複製程式碼

注意:Modular自動呼叫這些Binds型別的銷燬方法:Sink/Stream, ChangeNotifier和[Store/Triple]

有幾種方法可以檢索注入的AppBloc

class HomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    // You can use the object Inject to retrieve..

    final appBloc = Modular.get<AppBloc>();
    //or for no-ready AsyncBinds
    final share = Modular.getAsync<SharedPreferences>();
  }
}
複製程式碼

使用Modular小部件檢索例項

ModularState

在本例中,我們將使用下面的MyWidget作為頁面,因為這個頁面需要是StatefulWidget

讓我們來了解一下ModularState的用法。當我們定義類_MyWidgetState擴充套件ModularState<MyWidget, HomeStore>時,我們正在為這個小部件(在本例中是HomeStore)將Modular與我們的Store連結起來。當我們進入這個頁面時,HomeStore將被建立,store/controller變數將被提供給我們,以便在MyWidget中使用。

在此之後,我們可以使用儲存/控制器而沒有任何問題。在我們關閉頁面後,模組化將自動處理HomeStore

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends ModularState<MyWidget, HomeStore> {
  store.myVariableInsideStore = 'Hello!';
  controller.myVariableInsideStore = 'Hello!';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Modular"),
      ),
      body: Center(child: Text("${store.counter}"),),
    );
  }
}
複製程式碼

WidgetModule

WidgetModule具有與Module相同的結構。如果你想要一個帶有Modular頁面的TabBar,這是非常有用的。

class TabModule extends WidgetModule {

  @override
  List<Bind> binds => [
    Bind((i) => TabBloc(repository: i())),
    Bind((i) => TabRepository()),
  ];

  final Widget view = TabPage();

}
複製程式碼

Mock導航系統

我們認為,在使用Modular.toModular.link時,提供一種native方式來mock導航系統會很有趣。要做到這一點,您只需實現IModularNavigator並將您的實現傳遞給Modular.navigatorDelegate

使用 Mockito示例:

main() {
    var navigatorMock = MyNavigatorMock();

    // Modular.to and Modular.link will be called MyNavigatorMock implements!
    Modular.navigatorDelegate = navigatorMock;

    test('test navigator mock', () async {
        when(navigatorMock.pushNamed('/test')).thenAnswer((_) async => {});

        Modular.to.pushNamed('/test');
        verify(navigatorMock.pushNamed('/test')).called(1);
    });
}

class MyNavigatorMock extends Mock implements IModularNavigator {
  @override
  Future<T?> pushNamed<T extends Object?>(String? routeName, {Object? arguments, bool? forRoot = false}) =>
      (super.noSuchMethod(Invocation.method(#pushNamed, [routeName], {#arguments: arguments, #forRoot: forRoot}), returnValue: Future.value(null)) as Future<T?>);
}
複製程式碼

本例使用手動實現,但您也可以使用 程式碼生成器來建立模擬。

RouterOutlet

每個ModularRoute都可以有一個ModularRoute列表,這樣它就可以顯示在父ModularRoute中。反映這些內部路由的小部件叫做RouterOutlet。每個頁面只能有一個RouterOutlet,而且它只能瀏覽該頁面的子頁面。


class StartModule extends Module {
    @override
    List<Bind> get binds => [];

    @override
    List<ModularRoute> get routes => [
        ChildRoute(
            '/start',
            child: (context, args) => StartPage(),
            children: [
                ChildRoute('/home', child: (_, __) => HomePage()),
                ChildRoute('/product', child: (_, __) => ProductPage()),
                ChildRoute('/config', child: (_, __) => ConfigPage()),
            ],
        ),
    ];
}
複製程式碼
@override
Widget build(BuildContext context) {
    return Scaffold(
        body: RouterOutlet(),
        bottomNavigationBar: BottomNavigationBar(
            onTap: (id) {
                if (id == 0) {
                    Modular.to.navigate('/start/home');
                } else if (id == 1) {
                    Modular.to.navigate('/start/product');
                } else if (id == 2) {
                    Modular.to.navigate('/start/config');
                }
            },
            currentIndex: currentIndex,
            items: const [
                BottomNavigationBarItem(
                    icon: Icon(Icons.home),
                    label: 'Home',
                ),
                BottomNavigationBarItem(
                    icon: Icon(Icons.control_camera),
                    label: 'product',
                ),
                BottomNavigationBarItem(
                    icon: Icon(Icons.settings),
                    label: 'Config',
                ),
            ],
        ),
    );
}
複製程式碼

相關文章