<div id="qayss"></div>
  • <xmp id="qayss">
  • <small id="qayss"><small id="qayss"></small></small>
    <div id="qayss"></div>
  • <li id="qayss"></li>
  • <div id="qayss"><div id="qayss"></div></div>
  • <xmp id="qayss"><li id="qayss"></li>
    <div id="qayss"><li id="qayss"></li></div>
  • <small id="qayss"></small>
  • <div id="qayss"></div>
  • <xmp id="qayss"><div id="qayss"></div><div id="qayss"><div id="qayss"></div></div><xmp id="qayss">
    <small id="qayss"><div id="qayss"></div></small><div id="qayss"></div>
  • Flutter 應用程序架構:演示層

    來源:codewithandrea.com 更新時間:2023-10-31 20:57

    如果您需要幫助為您的 Flutter 應用程序選擇最適合的項目結構,可以查看:

    如果您想探索其他流行架構(如 MVP、MVVM 或 Clean Architecture)并了解它們如何與此處提出的架構相比較,可以閱讀以下內容:

    要了解 Riverpod 架構中每個層的更多信息,請閱讀本系列中的其他文章:

    在編寫Flutter應用程序時,將業務邏輯與UI代碼分離非常重要。

    這使得我們的代碼更容易進行測試和理解,尤其在我們的應用變得更加復雜時尤為重要。

    為了實現這一點,我們可以使用設計模式來在應用程序中的不同組件之間引入關注點分離。

    作為參考,我們可以采用分層應用程序架構,就像在此圖表中所表示的那樣: flutter-app-architecture.png
    使用數據、領域、應用和演示層的Flutter應用架構。箭頭顯示了各層之間的依賴關系。

    這一次,我們將專注于演示層,并學習如何使用控制器來:

    • 執行業務邏輯
    • 管理小部件狀態
    • 與數據層的存儲庫進行交互

    這種類型的控制器與MVVM模式中使用的視圖模型相同。如果您以前使用過flutter_bloc,它的作用與一個cubit相同。

    我們將了解AsyncNotifier類,它是Flutter SDK中StateNotifierValueNotifier / ChangeNotifier類的替代品。

    為了使這更有用,我們將實現一個簡單的身份驗證流程作為示例。

    準備好了嗎?讓我們開始吧!

    一個簡單的身份驗證流程

    讓我們考慮一個非常簡單的應用程序,我們可以使用它進行匿名登錄并在兩個屏幕之間切換: sign-in-screen-flows.png 簡單的登錄流程 在本文中,我們將重點介紹如何實現以下內容:

    • 一個身份驗證倉庫,我們可以用來進行登錄和注銷操作
    • 一個顯示給用戶的登錄小部件界面
    • 兩者之間進行中介的相應控制器類

    以下是此特定示例的參考架構的簡化版本: Layered architecture for the sign in feature

    登錄功能的分層架構

    您可以在GitHub上找到此應用的完整源代碼。要了解有關其組織方式的更多信息,請閱讀這篇文章:Flutter項目結構:Feature-first還是Layer-first?

    AuthRepository類

    作為起點,我們可以定義一個簡單的抽象類,其中包含三個方法,我們將使用這些方法進行登錄、登出和檢查身份驗證狀態:

    
    abstract class AuthRepository {
      // emits a new value every time the authentication state changes
      Stream<AppUser?> authStateChanges();
      Future<AppUser> signInAnonymously();
      Future<void> signOut();
    }
    

    實際應用中,我們還需要一個具體的類來實現AuthRepository。這可以基于Firebase或任何其他后端實現。甚至可以暫時使用虛擬倉庫進行實現。更多細節,請參閱有關存儲庫模式的文章。

    為了完整起見,我們還可以定義一個簡單的AppUser模型類:

    
    /// Simple class representing the user UID and email.
    class AppUser {
      const AppUser({required this.uid});
      final String uid;
      // TODO: Add other fields as needed (email, displayName etc.)
    }
    

    如果我們使用Riverpod,我們還需要一個Provider,用于訪問我們的存儲庫:

    
    final authRepositoryProvider = Provider<AuthRepository>((ref) {
      // return a concrete implementation of AuthRepository
      return FakeAuthRepository();
    });
    

    接下來,讓我們專注于登錄界面。

    登錄界面小部件

    假設我們有一個簡單的SignInScreen小部件,定義如下:

    
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    class SignInScreen extends ConsumerWidget {
      const SignInScreen({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Sign In'),
          ),
          body: Center(
            child: ElevatedButton(
              child: Text('Sign in anonymously'),
              onPressed: () { /* TODO: Implement */ },
            ),
          ),
        );
      }
    }
    

    這只是一個簡單的Scaffold,其中包含一個位于中間的ElevatedButton。

    需要注意的是,由于這個類擴展了ConsumerWidget,在build()方法中,我們有一個額外的ref對象,可以用來根據需要訪問提供者。

    直接從我們的小部件中訪問AuthRepository

    作為下一步,我們可以使用onPressed回調來執行登錄操作,如下所示:

    
    ElevatedButton(
      child: Text('Sign in anonymously'),
      onPressed: () => ref.read(authRepositoryProvider).signInAnonymously(),
    )
    

    這段代碼的工作方式是通過調用 ref.read(authRepositoryProvider) 來獲取 AuthRepository,然后調用它的 signInAnonymously() 方法。

    這覆蓋了正常情況下的操作(即登錄成功)。但是我們還需要考慮加載和錯誤狀態,具體做法包括:

    • 在登錄過程中禁用登錄按鈕并顯示加載指示器
    • 如果調用因任何原因失敗,顯示 SnackBar 或警告

    采用"StatefulWidget + setState"方式

    解決這個問題的一種簡單方法是:

    • 將我們的小部件轉換為 StatefulWidget(或者更確切地說,ConsumerStatefulWidget,因為我們在使用Riverpod)
    • 添加一些本地變量來跟蹤狀態變化
    • 在調用 setState() 時設置這些變量以觸發小部件重建
    • 利用這些變量來更新用戶界面

    下面是最終的代碼可能會如何看起來的示例:

    
    class SignInScreen extends ConsumerStatefulWidget {
      const SignInScreen({Key? key}) : super(key: key);
      @override
      ConsumerState<SignInScreen> createState() => _SignInScreenState();
    }
    class _SignInScreenState extends ConsumerState<SignInScreen> {
      // keep track of the loading state
      bool isLoading = false;
      // call this from the `onPressed` callback
      Future<void> _signInAnonymously() async {
        try {
          // update the state
          setState(() => isLoading = true);
          // sign in using the repository
          await ref
              .read(authRepositoryProvider)
              .signInAnonymously();
        } catch (e) {
          // show a snackbar if something went wrong
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(e.toString())),
          );
        } finally {
          // check if we're still on this screen (widget is mounted)
          if (mounted) {
            // reset the loading state
            setState(() => isLoading = false);
          }
        }
      }
      ...
    }
    

    對于一個簡單的應用程序,這種方法可能還可以。

    但是,當我們有更復雜的小部件時,這種方法很快會變得難以維護,因為我們在同一個小部件類中混合了業務邏輯和UI代碼。

    如果我們想要在多個小部件之間一致地處理加載和錯誤狀態,復制粘貼并調整上面的代碼就會變得相當容易出錯(而且不太有趣)。

    相反,最好將所有這些關注點移到一個單獨的控制器類中,該類可以:

    • 在我們的SignInScreenAuthRepository之間進行協調
    • 管理小部件狀態
    • 為小部件提供觀察狀態更改并根據結果重建自身的方法 sign-in-layers.png 登錄功能的分層架構 現在讓我們看看如何將其實際實現。

    基于 AsyncNotifier 的控制器類

    第一步是創建一個AsyncNotifier子類,其結構如下:

    
    class SignInScreenController extends AsyncNotifier<void> {
      @override
      FutureOr<void> build() {
        // no-op
      }
    }
    

    或者更好的是,我們可以使用新的 @riverpod 語法,讓Riverpod Generator為我們處理繁重的工作:

    
    part 'sign_in_controller.g.dart';
    @riverpod
    class SignInScreenController extends _$SignInScreenController {
      @override
      FutureOr<void> build() {
        // no-op
      }
    }
    // A signInScreenControllerProvider will be generated by build_runner
    

    無論哪種方式,我們需要實現一個build方法,它會在控制器首次加載時返回初始值。

    如果需要的話,我們可以利用build方法進行一些異步初始化操作(比如從網絡加載數據)。但如果控制器在創建后立即"準備就緒"(就像在這個案例中一樣),我們可以將方法體留空并將返回類型設置為Future。

    實現登錄方法

    接下來,讓我們添加一個用于登錄的方法:

    
    @riverpod
    class SignInScreenController extends _$SignInScreenController {
      @override
      FutureOr<void> build() {
        // no-op
      }
      Future<void> signInAnonymously() async {
        final authRepository = ref.read(authRepositoryProvider);
        state = const AsyncLoading();
        state = await AsyncValue.guard(() => authRepository.signInAnonymously());
      }
    }
    

    一些說明:

    • 我們通過在相應的提供程序上調用 ref.readref 是基礎 AsyncNotifier 類的屬性)來獲取 authRepository。
    • signInAnonymously() 內部,我們將狀態設置為 AsyncLoading,以便小部件可以顯示加載中的用戶界面。
    • 然后,我們調用 AsyncValue.guard 并等待結果(結果將是 AsyncDataAsyncError)。

    AsyncValue.guardtry/catch 的一個方便替代方法。更多信息,請閱讀:在 StateNotifier 子類中使用 AsyncValue.guard 而不是 try/catch

    額外的提示,我們可以使用方法的撕裂以進一步簡化我們的代碼:

    
    // pass authRepository.signInAnonymously directly using tear-off
    state = await AsyncValue.guard(authRepository.signInAnonymously);
    

    這完成了我們的控制器類的實現,只需幾行代碼:

    
    @riverpod
    class SignInScreenController extends _$SignInScreenController {
      @override
      FutureOr<void> build() {
        // no-op
      }
      Future<void> signInAnonymously() async {
        final authRepository = ref.read(authRepositoryProvider);
        state = const AsyncLoading();
        state = await AsyncValue.guard(authRepository.signInAnonymously);
      }
    }
    // A signInScreenControllerProvider will be generated by build_runner
    

    關于類型之間關系的注意事項

    請注意,build 方法的返回類型與state 屬性的類型之間存在明顯的關系: async-notifier-void.png AsyncNotifier子類:如果build方法返回一個Future,那么狀態將是AsyncValue。實際上,將AsyncValue作為狀態允許我們表示三種可能的值:

    • 默認(未加載)作為AsyncData(與AsyncValue.data相同)
    • 加載中作為AsyncLoading(與AsyncValue.loading相同)
    • 錯誤作為AsyncError(與AsyncValue.error相同)

    如果您對AsyncValue及其子類不熟悉,請閱讀此文:如何使用StateNotifier和Flutter中的AsyncValue處理加載和錯誤狀態

    現在是時候回到我們的小部件類并將一切連接起來了!

    在小部件類中使用我們的控制器

    這是使用我們的新SignInScreenController類的SignInScreen的更新版本:

    
    class SignInScreen extends ConsumerWidget {
      const SignInScreen({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // watch and rebuild when the state changes
        final AsyncValue<void> state = ref.watch(signInScreenControllerProvider);
        return Scaffold(
          appBar: AppBar(
            title: const Text('Sign In'),
          ),
          body: Center(
            child: ElevatedButton(
              // conditionally show a CircularProgressIndicator if the state is "loading"
              child: state.isLoading
                  ? const CircularProgressIndicator()
                  : const Text('Sign in anonymously'),
              // disable the button if the state is loading
              onPressed: state.isLoading
                  ? null
                  // otherwise, get the notifier and sign in
                  : () => ref
                      .read(signInScreenControllerProvider.notifier)
                      .signInAnonymously(),
            ),
          ),
        );
      }
    }
    

    請注意,在 build() 方法中,我們會監聽我們的提供者,并在狀態更改時重新構建小部件。

    而在 onPressed 回調中,我們會讀取提供者的通知器并調用 signInAnonymously()。 此外,我們還可以使用 isLoading 屬性來有條件地禁用按鈕,以便在登錄過程中進行簽到。

    我們快要完成了,只剩一件事情要做。

    監聽狀態變化

    build 方法的最頂部,我們可以添加以下代碼:

    
    @override
    Widget build(BuildContext context, WidgetRef ref) {
      ref.listen<AsyncValue>(
        signInScreenControllerProvider,
        (_, state) {
          if (!state.isLoading && state.hasError) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.error.toString())),
            );
          }
        },
      );
      // rest of the build method
    }
    

    我們可以使用這段代碼,在狀態發生變化時調用監聽器回調函數。

    這對于在登錄時出現錯誤時顯示錯誤警報或SnackBar非常有用。

    額外獎勵: 一個 AsyncValue 擴展方法

    上述的監聽器代碼非常有用,我們可能希望在多個小部件中重復使用它。

    為此,我們可以定義以下的 AsyncValue 擴展方法:

    
    extension AsyncValueUI on AsyncValue {
      void showSnackbarOnError(BuildContext context) {
        if (!isLoading && hasError) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(error.toString())),
          );
        }
      }
    }
    

    然后,在我們的小部件中,我們只需導入我們的擴展并調用它:

    
    ref.listen<AsyncValue>(
      signInScreenControllerProvider,
      (_, state) => state.showSnackbarOnError(context),
    );
    

    結論

    通過基于 AsyncNotifier 實現自定義控制器類,我們成功將業務邏輯與UI代碼分離。

    因此,我們的小部件類現在完全無狀態,只關心以下內容:

    • 監聽狀態變化并在結果發生變化時進行重建(使用 ref.watch
    • 通過調用控制器中的方法響應用戶輸入(使用 ref.read
    • 監聽狀態變化,如果出現問題,則顯示錯誤(使用 ref.listen

    與此同時,我們的控制器的任務是:

    • 代表小部件與存儲庫進行通信
    • 根據需要發出狀態更改

    由于控制器不依賴于任何UI代碼,因此可以輕松進行單元測試,這使其成為存儲任何特定于小部件的業務邏輯的理想位置。


    總之,小部件和控制器屬于我們應用架構中的演示層: flutter-app-architecture.png
    應用架構使用數據、領域、應用和展示層。箭頭顯示了各層之間的依賴關系。此外,還有三個額外的層:數據層、領域層和應用層

    曰本丰满熟妇XXXX性,一女多男在疯狂伦交,多人乱P杂交公车调教,成人AV在线一区二区三区
    <div id="qayss"></div>
  • <xmp id="qayss">
  • <small id="qayss"><small id="qayss"></small></small>
    <div id="qayss"></div>
  • <li id="qayss"></li>
  • <div id="qayss"><div id="qayss"></div></div>
  • <xmp id="qayss"><li id="qayss"></li>
    <div id="qayss"><li id="qayss"></li></div>
  • <small id="qayss"></small>
  • <div id="qayss"></div>
  • <xmp id="qayss"><div id="qayss"></div><div id="qayss"><div id="qayss"></div></div><xmp id="qayss">
    <small id="qayss"><div id="qayss"></div></small><div id="qayss"></div>