從零開始打造 Flutter 架構:第二部分


摘要

在這篇文章中,我們深入探索從零開始打造 Flutter 架構的重要技巧與方法,幫助開發者提升應用效率和可維護性。 歸納要點:

  • 依賴注入的運用:利用 Provider Package 管理狀態與依賴,提升 Flutter 應用的可測試性與維護性。
  • 分層設計的實務應用:結合 Bloc 或 MobX 等模式,有效將 UI 與業務邏輯分離,提高代碼可讀性。
  • 微服務架構趨勢:探討如何融合雲端功能及 Serverless 架構,讓 Flutter 應用更具彈性和擴展性。
本文提供了有效運用依賴注入、優化分層設計以及掌握微服務架構趨勢的關鍵策略,為 Flutter 開發者指引未來方向。


Flutter 架構:依賴注入與分層設計

嗨!我是 Denis Lomov,今天我準備與大家分享我們 Flutter 系列的第二部分也是最後一部分。您可以在這裡找到第一部分。在這篇文章中,我的同事 Eugene Efanov,一位移動開發者,將引導我們如何實現依賴注入、如何將架構結構化為各個層次,以及如何測試我們所建立的邏輯。

依賴注入在架構中至關重要,因為它使我們能夠在測試期間替換依賴項。要實現這一點,我們需要為連線類別到 MvvmInstance 建立一個介面並生成物件建立的程式碼。由於我們在實現中使用預設建構函式,因此生成這段程式碼非常簡單。

為了儲存例項,我們需要一個包含可用例項字典的單例(singleton)。以下是一個簡化版本的此類類別可能看起來像這樣:
我們在研究許多文章後,彙整重點如下
網路文章觀點與我們總結
  • 在工作中,保持良好的溝通能提升團隊效率。
  • 情緒管理對於職場表現至關重要,避免負面情緒影響同事。
  • 持續學習和自我提升是職業發展的關鍵。
  • 建立良好的人際關係有助於創造更和諧的工作環境。
  • 設定明確的目標可以提高工作的方向性與動力。
  • 適度休息與放鬆能增強專注力及生產力.

在當今快節奏的生活中,我們都希望能在工作上表現得更好,但往往會遇到各種挑戰。透過良好的溝通、情緒管理以及持續學習,我們能夠克服這些困難。此外,建立人際關係與明確目標也讓我們在職場上走得更加順利。不論是小小的休息還是大大的志向,每一步都是成就的一部分。

觀點延伸比較:
主題具體策略最新趨勢權威觀點
良好溝通定期舉行團隊會議,使用即時通訊工具保持聯繫遠端工作的普及使得虛擬溝通技能更重要哈佛商業評論指出,開放式的交流文化能提升團隊信任感
情緒管理學習壓力管理技巧,如正念冥想和情緒日記心理健康在工作中的重視日益增加,企業提供心理輔導資源成為趨勢專家建議,情緒智力是領導者必備的核心能力
持續學習與自我提升參加線上課程或工作坊,不斷更新專業知識與技能數位轉型促進了終身學習的重要性,各大平台如Coursera、Udemy等蓬勃發展職場教練強調,不斷學習能提高適應變化的能力
建立良好人際關係主動參與社交活動與同事互動,分享個人經歷以增進了解企業越來越重視員工之間的合作和支持系統,以提升整體士氣及生產力研究顯示,人際關係強會直接影響到工作滿意度與留任率
設定明確目標 利用SMART原則設置具體可達成的目標並定期檢視進度 `OKR`(目標關鍵成果)方法已被許多科技公司廣泛采納以提高透明度和問責制 `福布斯`報告指出,有效目標設定可顯著提高員工績效
適度休息與放鬆 規劃短暫休息時間,例如每小時5分鐘的站立或伸展運動 `番茄鐘`技術逐漸受到青睞,以提高專注力和效率 `心理科學雜誌`研究表明,適當休息有助於創造力及問題解決能力

mixin SavableStatefulMvvmInstance on StatefulMvvmInstance {   StreamSubscription? _storeSaveSubscription;    Map get savedStateObject => {};    @protected   void restoreCachedStateSync() {     if (!stateFullInstanceSettings.isRestores) {       return;     }      final stateFromCacheJsonString = UMvvmApp.cacheGetDelegate(       stateFullInstanceSettings.stateId,     );      if (stateFromCacheJsonString == null || stateFromCacheJsonString.isEmpty) {       return;     }      final restoredMap = json.decode(stateFromCacheJsonString);     onRestore(restoredMap);   }    void onRestore(Map savedStateObject) {}    @override   void initializeStore() {     _subscribeToStoreUpdates();   }    void initializeStatefullInstance() {     initializeStore();     restoreCachedStateSync();   }    void _subscribeToStoreUpdates() {     if (!stateFullInstanceSettings.isRestores) {       return;     }      _storeSaveSubscription = _store.stream.listen((_) async {       final stateId = state.runtimeType.toString();       await UMvvmApp.cachePutDelegate(stateId, json.encode(savedStateObject));     });   }    @override   void disposeStore() {     _storeSaveSubscription?.cancel();   }    StateFullInstanceSettings get stateFullInstanceSettings => StateFullInstanceSettings(         stateId: state.runtimeType.toString(),       ); }

在這裡,我們儲存一個建構器的字典,這些建構器將會被生成,還有一個已建立物件的容器,用於檢索已存在的例項。

我們還將把物件字典劃分為不同的範疇,以便區分我們的例項。作為預設範疇,我們可以指定全域範疇,在這裡單例會在最終應用程式初始化時儲存和初始化。我們也可以新增一個獨特的範疇,總是建立新的例項。可以新增一個弱範疇——全域物件會儲存在這裡,但與全域範疇不同的是,當所有依賴例項被銷毀時,它們也會被銷毀。其他使用者自定義的範疇將類似於弱範疇;當所有依賴例項被銷毀後,該範疇中的所有物件也將被銷毀。

class InstanceCollection {   final container = ScopedContainer();   final builders = HashMap();    static final InstanceCollection _singletonInstanceCollection = InstanceCollection._internal();    static InstanceCollection get instance {     return _singletonInstanceCollection;   }    InstanceCollection._internal();    void addBuilder(Function builder) {     final id = Instance.toString();      builders[id] = builder;   }    Instance get({     DefaultInputType? params,     int? index,     String scope = BaseScopes.global,   }) {     return getWithParams(       params: params,       index: index,       scope: scope,     );   }    Instance getWithParams({     InputState? params,     int? index,     String scope = BaseScopes.global,   }) {     final runtimeType = Instance.toString();      return getInstanceFromCache(       runtimeType,       params: params,       index: index,       scopeId: scope,     );   }    void addWithParams({     required String type,     InputState? params,     int? index,     String? scope,   }) {     final id = type;     final scopeId = scope ?? BaseScopes.global;      if (container.contains(scopeId, id, index) && index == null) {       return;     }      final builder = builders[id];      final newInstance = builder!() as MvvmInstance;      container.addObjectInScope(       object: newInstance,       type: type,       scopeId: scopeId,     );      if (!newInstance.isInitialized) {       newInstance.initialize(params);     }   }    Instance constructAndInitializeInstance(     String id, {     dynamic params,     bool withNoConnections = false,   }) {     final builder = builders[id];      final instance = builder!() as Instance;      instance.initialize(params);      return instance;   }    Instance getInstanceFromCache(     String id, {     dynamic params,     int? index,     String scopeId = BaseScopes.global,     bool withoutConnections = false,   }) {     final scope = scopeId;      final instance = container.getObjectInScope(       type: id,       scopeId: scope,       index: index ?? 0,     ) as Instance;      if (!instance.isInitialized) {       instance.initialize(params);     }      return instance;   } }

如果所需的例項尚未在我們的字典中建立,我們將構造它,呼叫初始化方法,並返回已初始化的例項。如果它已經存在,我們則簡單地返回現有物件。您可以使用 source_gen 來生成一個構建器的字典。建立註解以標記我們的例項。然後,在生成器中檢索這些例項並生成相應的構建器。我們可以從兩個註解開始:一個用於常規物件,另一個用於單例物件。單例物件將自動放置在全域性範圍內。

class TestInstance1 extends MvvmInstance {}  class TestInstance2 extends MvvmInstance {}  void testInstanceCollection() {   // singleton instance   final singletonInstance = InstanceCollection.instance.get(scope: BaseScopes.global);    // weak instance   final weakInstance = InstanceCollection.instance.get(scope: BaseScopes.weak);   final weakInstance2 = InstanceCollection.instance.get(scope: BaseScopes.weak); // same instance    // unique instance   final uniqueInstance = InstanceCollection.instance.get(scope: BaseScopes.unique);   final uniqueInstance2 = InstanceCollection.instance.get(scope: BaseScopes.unique); // new instance }

在獲得物件字典後,我們可以將它們連線到我們的 MvvmInstance。為此,我們將把依賴項新增到例項配置中。該配置將包括一個“聯結器”列表,每個聯結器都包含引數,例如輸入資料和應從中檢索連線實體的範圍。

class Instance {   final Type inputType;   final bool singleton;   final bool isAsync;    const Instance({     this.inputType = Map,     this.singleton = false,     this.isAsync = false,   }); }  const basicInstance = Instance(); const singleton = Instance(singleton: true);  class MainAppGenerator extends GeneratorForAnnotation {   @override   FutureOr generateForAnnotatedElement(     sg.Element element,     ConstantReader annotation,     BuildStep buildStep,   ) async {     const className = 'AppGen';     final classBuffer = StringBuffer();      final instanceJsons = Glob('lib/**.mvvm.json');      final jsonData = [];      await for (final id in buildStep.findAssets(instanceJsons)) {       final json = jsonDecode(await buildStep.readAsString(id));       jsonData.addAll([...json]);     }      // ...      classBuffer       ..writeln('@override')       ..writeln('void registerInstances() {');      classBuffer.writeln('instances');      for (final element in instances) {       classBuffer.writeln('..addBuilder<${element.name}>(() => ${element.name}())');     }      classBuffer.writeln(';');      // ...      return classBuffer.toString();   } }

現在,隨著我們的 MvvmInstance 的依賴項清單,我們可以在初始化過程中從例項字典中檢索它們。

class Connector {   final Type type;   final dynamic input;   final String scope;   final bool isAsync;    const Connector({     required this.type,     this.input,     this.scope = BaseScopes.weak,     this.isAsync = false,   }); }  class DependentMvvmInstanceConfiguration extends MvvmInstanceConfiguration {   const DependentMvvmInstanceConfiguration({     super.isAsync,     this.dependencies = const [],   });    final List dependencies;


現在,透過一個處理事件、狀態和依賴的類別,我們可以將我們的架構結構化為多層次。


每個 MvvmInstance 都與事件相連。在領域層中,我專注於 interactor,這是一個持有狀態的實體。這個實體對於隔離邏輯元件非常有用,例如管理一系列的帖子。在這裡,我們可以從伺服器檢索帖子並訂閱特定帖子的按讚事件,以更新物件狀態中的集合。

abstract class BaseInteractor extends MvvmInstance     with StatefulMvvmInstance, DependentMvvmInstance {   @mustCallSuper   @override   void initialize(Input? input) {     super.initialize(input);      initializeDependencies();     initializeStatefullInstance();   }    @mustCallSuper   @override   void dispose() {     super.dispose();      disposeStore();     disposeDependencies();   }    @mustCallSuper   @override   Future initializeAsync() async {     await super.initializeAsync();   } }

我還引入了一個包裝器——這個元素不儲存狀態,而是作為第三方庫的介面。例如,我們可以用它來檢查當前的網路狀態。這種抽象使我們能夠在測試過程中輕鬆替換庫方法。


abstract class BaseStaticWrapper extends MvvmInstance     with DependentMvvmInstance {   /// Inititalizes wrapper   @mustCallSuper   @override   void initialize(Input? input) {     super.initialize(input);      initializeDependencies();   }    @override   void dispose() {     super.dispose();      disposeDependencies();   }    @mustCallSuper   @override   Future initializeAsync() async {     await super.initializeAsync();   } }


在展示層面上,我介紹了檢視模型。它基本上是一個互動器,其輸入資料是它所連線的檢視。在這裡,我們可以將互動器與必要的資料連結起來,並設定顯示繫結。

abstract class BaseViewModel extends MvvmInstance     with StatefulMvvmInstance, DependentMvvmInstance {   void onLaunch() {}    void onFirstFrame() {}    @mustCallSuper   @override   void initialize(Widget input) {     super.initialize(input);      initializeDependencies();     initializeStatefullInstance();   }    @mustCallSuper   @override   void dispose() {     super.dispose();      disposeStore();     disposeDependencies();   }    @mustCallSuper   @override   Future initializeAsync() async {     await super.initializeAsync();   } }

在最終版本中,我們載入文章的結構如下:

part 'main.mvvm.dart'; part 'main.mapper.dart';  class PostLikedEvent {   final int id;    const PostLikedEvent({     required this.id,   }); }  @MappableClass() class Post with PostMappable {   const Post({     required this.title,     required this.body,     required this.id,     this.isLiked = false,   });    final String? title;   final String? body;   final int? id;   final bool isLiked;    static const fromMap = PostMapper.fromMap; }  @MappableClass() class PostsState with PostsStateMappable {   const PostsState({     this.posts,     this.active,   });    final StatefulData>? posts;   final bool? active; }  @mainApi class Apis with ApisGen {}  @mainApp class App extends UMvvmApp with AppGen {   final apis = Apis();    @override   Future initialize() async {     await super.initialize();   } }  final app = App();  // ...  @basicInstance class PostsInteractor extends BaseInteractor?> {   Future loadPosts(int offset, int limit, {bool refresh = false}) async {     updateState(state.copyWith(posts: const LoadingData()));      late Response> response;      if (refresh) {       response = await executeAndCancelOnDispose(         app.apis.posts.getPosts(0, limit),       );     } else {       response = await executeAndCancelOnDispose(         app.apis.posts.getPosts(offset, limit),       );     }      if (response.isSuccessful) {       updateState(         state.copyWith(posts: SuccessData(result: response.result ?? [])),       );     } else {       updateState(state.copyWith(posts: ErrorData(error: response.error)));     }   }    @override   List subscribe() => [         on(           (event) {             // update state           },         ),       ];    @override   PostsState get initialState => const PostsState(); }  class PostsListViewState {}  class PostsListViewModel extends BaseViewModel {   @override   DependentMvvmInstanceConfiguration get configuration => DependentMvvmInstanceConfiguration(         dependencies: [           app.connectors.postsInteractorConnector(),         ],       );    late final postsInteractor = getLocalInstance();    @override   void onLaunch() {     postsInteractor.loadPosts(0, 30, refresh: true);   }    void like(int id) {     app.eventBus.send(PostLikedEvent(id: id));   }    Stream>?> get postsStream => postsInteractor.updates((state) => state.posts);    @override   PostsListViewState get initialState => PostsListViewState(); }  class PostsListView extends BaseWidget {   const PostsListView({     super.key,     super.viewModel,   });    @override   State createState() {     return _PostsListViewWidgetState();   } }  class _PostsListViewWidgetState extends BaseView {   @override   Widget buildView(BuildContext context) {     // ...   } } 

為了測試我們所建立的邏輯,我們可以用預先建立的元素替換 DI 容器中的元素,並將測試資料傳遞給狀態。該庫還包括檢查事件派發和檢測我們所建立實體中的迴圈依賴的方法。雖然我不會在這裡提供具體的實現細節,但你可以在程式碼庫中檢視它們。以下是如何測試每個實體的示例。

class MockPostsApi extends PostsApi {   @override   HttpRequest> getPosts(int offset, int limit) => super.getPosts(offset, limit)     ..simulateResult = Response(code: 200, result: [       Post(         title: '',         body: '',         id: 1,       )     ]); }  void main() {   test('PostsInteractorTest', () async {     await initApp(testMode: true);      app.apis.posts = MockPostsApi();      final postsInteractor = PostsInteractor();      postsInteractor.initialize(null);      await postsInteractor.loadPosts(0, 30);      expect((postsInteractor.state.posts! as SuccessData).result[0].id, 1);   }); }  // ...  class PostInteractorMock extends PostInteractor {   @override   Future loadPost(int id, {bool refresh = false}) async {     updateState(state.copyWith(       post: SuccessData(result: Post(id: 1)),     ));   } }  void main() {   test('PostViewModelTest', () async {     await initApp(testMode: true);      app.registerInstances();     await app.createSingletons();      final postInteractor = PostInteractorMock();     app.instances.addBuilder(() => postInteractor);      final postViewModel = PostViewModel();     const mockWidget = PostView(id: 1);      postViewModel       ..initialize(mockWidget)       ..onLaunch();      expect((postViewModel.currentPost as SuccessData).result.id, 1);   }); }  void main() {   IntegrationTestWidgetsFlutterBinding.ensureInitialized();    group('PostsListViewTest', () {     testWidgets('PostsListViewTest InitialLoadTest', (tester) async {       await initApp(testMode: true);        app.registerInstances();       await app.createSingletons();        app.apis.posts = MockPostsApi();        await tester.pumpAndSettle();        await tester.pumpWidget(const MaterialApp(         home: Material(child: PostsListView()),       ));        await Future.delayed(const Duration(seconds: 3), () {});        await tester.pumpAndSettle();        final titleFinder = find.text('TestTitle');        expect(titleFinder, findsOneWidget);     });   }); } 

微服務架構中的邏輯元件和最佳實踐

我們已經建立了一套邏輯元件來實現架構的每一層,並具備測試所有功能的能力。特別地,在依賴注入和 HTTP 操作方面,我們可以使用其他解決方案,僅依賴於架構的結構。在我參與的實際專案中,我積極使用這些架構的所有元件。測試這些元件非常方便,因為商業邏輯完全由單元測試覆蓋。

商業邏輯由特定數量的互動者組成,即使在大型專案中也能迅速進行依賴注入,確保初始化過程流暢而不會有任何延遲。得益於事件機制,各個元件之間的耦合度降低。主要全域性應用程式元件允許在程式碼中的任何地方使用這些元件,使我們能夠自由實現功能,而不影響可測試性。

**深入說明事件機制在降低耦合方面的優勢:** 事件機制是微服務架構中減少耦合的重要手段,它賦予系統更高的可擴充套件性和可維護性。例如,透過非阻塞方式進行非同步通訊,使得傳送者無需了解接收者具體實現,只需傳送事件即可;而接收者則只需訂閱感興趣的事件,無需理解傳送者內部運作。此種設計大大提高了系統擴充套件新功能時的不幹擾性,同時簡化了單元測試過程。

**探討在大型專案中進行依賴注入的最佳實踐:** 在處理複雜專案時,有效地管理依賴關係至關重要。良好的依賴注入策略能夠使開發人員快速替換或模擬不同元件,而不必重新編寫大量程式碼。在大規模專案中,如果將互動者分離為獨立模組,不僅提高了可重用性,也讓團隊協作變得更加順利。因此,在設計階段就考慮到依賴注入,可以顯著提升整體開發效率和維護便利性。

現在,我們看到越來越多以事件驅動架構(EDA)為基礎的平台,如 Apache Kafka 和 Confluent Kafka 的廣泛應用,它們有效地管理和處理大量事件,為當前微服務架構提供強大的基礎設施支援。在此趨勢下,更加靈活且可擴充套件的方法正在改變企業如何設計其軟體系統。

我將在接下來的文章中說明如何在 SwiftUI 和 Compose 中實現類似的機制。訂閱我們的電子報,獲取更多有用的見解!

訂閱我們的電子報!


🛸 獲取您的數位創意估算 👉 hello@redcollar.co 💻 網站 | LinkedIn | Twitter | Instagram | Behance | 作品集

參考來源


JH

專家

相關討論

❖ 相關專欄