簡體   English   中英

Flutter 應用程序架構使用“模塊”與提供者 package

[英]Flutter app architecture using "modules" with provider package

幾周以來,我一直在 Flutter 中編寫應用程序,幾天前我開始想知道最好的架構是什么。

先說一點上下文:

  • 這是一個使用 Firebase 作為后端的消息應用程序;
  • 它嚴重依賴出色的Provider package 來處理整個應用程序中的 state
  • 該計划是具有多個功能,可以相互交互。
  • 我對 Flutter (主要是 React/ReactNative 背景)相當陌生,它可以解釋我下面的奇怪方法。

我一直在體驗不同的架構方法,並設法找到一種最終似乎適合我的工作。

由於我將擁有多個功能,可在應用程序的不同位置重用,我想將代碼按功能(或模塊)拆分,然后可以在不同的屏幕上獨立使用。 文件夾架構如下:

FlutterApp
|
|--> ios/
|--> android/
|--> lib/
      |
      |--> main.dart
      |--> screens/
      |       |
      |       |--> logged/
      |       |      |
      |       |      |--> profile.dart
      |       |      |--> settings.dart
      |       |      |--> ...
      |       |
      |       |--> notLogged/
      |       |      |
      |       |      |--> home.dart
      |       |      |--> loading.dart
      |       |      |--> ...
      |       
      |--> features/
              |
              |--> featureA/
              |       |
              |       |--> ui/
              |       |     |--> simpleUI.dart
              |       |     |--> complexUI.dart
              |       |--> provider/
              |       |     |-->featureAProvider.dart
              |       |--> models/
              |             |--> featureAModel1.dart
              |             |--> featureAModel2.dart
              |             |--> ...
              |
              |
              |--> featureB/
              |       |
              |       |--> ui/
              |       |     |--> simpleUI.dart
              |       |     |--> complexUI.dart
              |       |--> provider/
              |       |     |--> featureBProvider.dart
              |       |--> models/
              |             |--> featureBModel1.dart
              |             |--> featureBModel2.dart
              |             |--> ...
              |
             ...

理想情況下,每個功能都遵循以下准則:

  • 每個特性都有一個邏輯部分(通常使用Provider Package);
  • 每個功能邏輯部分都可以從另一個功能請求變量(ChangeNotifier class 成員)
  • 每個功能都有一個(愚蠢的)UI 部分,可以直接與“邏輯”部分交互(因此可能不那么愚蠢);
  • 每個功能都可以將其 UI 部分替換為自定義 UI,但是,自定義 UI 必須自己實現與邏輯部分的交互;
  • 如果我需要稍后將它們存儲在 Firebase 中,每個特征都可以具有代表特征資源的模型

我已經用我的應用程序的一個功能(或 2 個取決於你如何看待它)嘗試了這種方法,即錄制/收聽語音筆記的能力。 我發現這很有趣,因為您可以在一個地方錄制,但可以在多個地方收聽錄音:例如,在錄音之后或錄音發送給您時。

這是我想出的:

  • 測試的文件夾結構在這種情況下沒有models/文件夾,因為它只是我在其他地方處理的文件
  • voiceRecorderNotifier 處理文件(添加/刪除)和錄音(開始/結束)
  • voicePlayerNotifier 需要實例化一個文件(命名構造函數),然后處理音頻文件播放(播放、暫停、停止)。

在代碼中,它有點冗長,但按預期工作,例如,在屏幕中,我可以請求 voiceRecorder 功能,如下所示:

class Screen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => VoiceRecorderNotifier(),
      child: Column(
        children: [
          AnUIWidget(),
          AnotherUIWidget(),
          ...,
          // The "dumb" feature UI widget from 'features/voiceRecorder/ui/simpleButton.dart' that can be overrided if you follow use the VoiceRecorderNotifier
          VoiceRecorderButtonSimple(), 
          ...,
        ]
      )
    );
  }
}

我也可以讓這兩個功能(voiceRecorder / voicePlayer)一起工作,如下所示:

class Screen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => VoiceRecorderNotifier(),
      child: Column(
        children: [
          AnUIWidget(),
          AnotherUIWidget(),
          ...,
          VoiceRecorderButtonSimple(),
          ...,
          // adding the dependent voicePlayer feature (using the VoiceRecorderNotifier data);
          Consumer<VoiceRecorderNotifier>(
            builder: (_, _voiceRecorderNotifier, __) {
              if (_voiceRecorderNotifier.audioFile != null) {
                // We do have audio file, so we put the VoicePlayer feature
                return ChangeNotifierProvider<VoicePlayerNotifier>(
                  create: (_) => VoicePlayerNotifier.withFile(_voiceRecorderNotifier.audioFile),
                  child: VoicePlayerButtonSimple(),
                );
              } else {
                // We don't have audio file, so no voicePlayer needed
                return AnotherUIWidget(); 
              }
            }
          ),
          ...,
          AnotherUIWidget(),
        ]
      )
    );
  }
}

這是一個新的測試,所以我認為有一些我現在看不到的缺點,但我覺得有幾個優點:

  • 更干凈的文件夾結構;
  • 一個地方處理與功能相關的“高級”邏輯,易於更新;
  • 在應用程序的任何地方添加、移動、刪除功能都很容易;
  • 提供了一個基本 UI 以使該功能按預期工作,例如一個簡單的
Text('hi')

但我仍然可以“覆蓋” UI 以特定顯示該功能;

  • 我可以專注於 UI 和功能的使用,而不是創建大量有狀態的組件來在不同的地方復制功能的相同邏輯;

我看到的缺點:

  • 功能邏輯是“隱藏的”,每次我想對功能執行特定操作以記住功能的行為時,我都需要通過通知程序 go ;
  • 在好地方實現通知器可能會變得一團糟,如果 UI 小部件可以使用多個功能,那么我將需要多個 FeatureNotifier(即使 Multiprovider 在這種情況下很有用);

最后,以下是問題:

  • 您是否認為這種方法是可擴展的/推薦的,因此如果我可以繼續以這種方式創建功能而以后不會遇到麻煩?
  • 你看到其他的缺點了嗎?

Provider 是一個很棒的工具,可以幫助您訪問整個應用程序中的所有數據。 我沒有看到有關它當前如何在您的應用上實現的任何問題。

在您正在尋找的點上,例如處理邏輯和更新 UI,您可能需要研究 BloC 模式。 有了這個,您可以通過 Stream 處理 UI 更新,並且可以在 StreamBuilder 上更新 UI。

此示例演示使用 BloC 模式更新 Flutter 應用程序上的 UI。 這是可以處理所有邏輯的部分。 Timer用於模擬 HTTP 響應的等待時間。

class Bloc {
  /// UI updates can be handled in Bloc
  final _repository = Repository();
  final _progressIndicator = StreamController<bool>.broadcast();
  final _updatedNumber = StreamController<String>.broadcast();

  /// StreamBuilder listens to [showProgress] to update UI to show/hide the LinearProgressBar
  Stream<bool> get showProgress => _progressIndicator.stream;

  /// StreamBuilder listens to [updatedNumber] to update UI 
  Stream<String> get updatedNumber => _updatedNumber.stream;

  updateShowProgress(bool showProgress) {
    _progressIndicator.sink.add(showProgress);
  }

  /// Updates the List<UserThreads> Stream
  fetchUpdatedNumber(String number) async {
    bloc.updateShowProgress(true); // Show ProgressBar

    /// Timer mocks an instance where we're waiting for
    /// a response from the HTTP request
    Timer(Duration(seconds: 2), () async {
      // delay for 4 seconds to display LinearProgressBar
      var updatedNumber = await _repository.fetchUpdatedNumber(number);
      _updatedNumber.sink.add(updatedNumber); // Update Stream
      bloc.updateShowProgress(false); // Hide ProgressBar
    });
  }

  dispose() {
    _updatedNumber.close();
  }

  disposeProgressIndicator() {
    _progressIndicator.close();
  }
}

/// this enables Bloc to be globally accessible
final bloc = Bloc();

/// Class where we can keep Repositories that can be accessed in Bloc class
class Repository {
  final provider = Provider();

  Future<String> fetchUpdatedNumber(String number) =>
      provider.updateNumber(number);
}

/// Class where all backend tasks can be handled
class Provider {
  Future<String> updateNumber(String number) async {
    /// HTTP requests can be done here
    return number;
  }
}

這是我們的主要應用程序。 注意我們不再需要調用setState()來刷新 UI。 UI 更新依賴於對它們設置的 StreamBuilder。 每次使用StreamController.broadcast.sink.add(Object)對 Stream 進行更新時,StreamBuilder 都會再次重建以更新 UI。 StreamBuilder 還用於顯示/隱藏 LinearProgressBar。

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BloC Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    bloc.fetchUpdatedNumber('${++_counter}');
    // setState(() {
    //   _counter++;
    // });
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<bool>(
        stream: bloc.showProgress,
        builder: (BuildContext context, AsyncSnapshot<bool> progressBarData) {
          /// To display/hide LinearProgressBar
          /// call bloc.updateShowProgress(bool)
          var showProgress = false;
          if (progressBarData.hasData && progressBarData.data != null)
            showProgress = progressBarData.data!;
          return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
              bottom: showProgress
                  ? PreferredSize(
                      preferredSize: Size(double.infinity, 4.0),
                      child: LinearProgressIndicator())
                  : null,
            ),
            body: StreamBuilder<String>(
                stream: bloc.updatedNumber,
                builder: (BuildContext context,
                    AsyncSnapshot<String> numberSnapshot) {
                  var number = '0';
                  if (numberSnapshot.hasData && numberSnapshot.data != null)
                    number = numberSnapshot.data!;
                  return Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text(
                          'You have pushed the button this many times:',
                        ),
                        Text(
                          '$number',
                          style: Theme.of(context).textTheme.headline4,
                        ),
                      ],
                    ),
                  );
                }),
            floatingActionButton: FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
          );
        });
  }
}

演示

演示

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM