什麼是Flutter Modular?
隨著應用專案發展和變得越來越複雜,保持程式碼和專案結構可維護和可複用越來越難。Modular提供了一堆適配Flutter的解決方案來解決這些問題,比如依賴注入,路由系統和“一次性單例”系統(也就是說,當注入模組超出範圍時,模組化自動配置注入模組)。
Modular的依賴注入為任何狀態管理系統提供了開箱即用的支援,管理你應用的記憶體。
Modular也支援動態路由和相對路由,像在Web一樣。
Modular結構
Modular結構由分離和獨立的模組組成,這些模組將代表應用程式的特性。 每個模組都位於自己的目錄中,並控制自己的依賴關係、路由、頁面、小部件和業務邏輯。因此,您可以很容易地從專案中分離出一個模組,並在任何需要的地方使用它。
Modular支柱
這是Modular關注的幾個方面:
- 自動記憶體管理
- 依賴注入
- 動態和相對路由
- 程式碼模組化
在專案中使用Modular
安裝
開啟你專案的pubspec.yaml
並且新增flutter_modular
作為依賴:
dependencies:
flutter_modular: any
複製程式碼
在一個新專案中使用
為了在新專案中使用Modular,你必須做一些初始化步驟:
-
用
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(); } } 複製程式碼
-
建立繼承自
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 = []; } 複製程式碼
-
在
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())); 複製程式碼
-
完成!你的應用已經設定完成並且準備好和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
中的所有路由都將繼承這個過渡動畫。
自定義過渡動畫路由
你也可以通過將路由器的transition
和customTransition
引數分別設定為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,
),
),
)
;
},
);
複製程式碼
依賴注入
可以通過重寫Module
的binds
的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.to
和Modular.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',
),
],
),
);
}
複製程式碼