使用带有提供程序包的“模块”的 Flutter 应用程序架构

Posted

技术标签:

【中文标题】使用带有提供程序包的“模块”的 Flutter 应用程序架构【英文标题】:Flutter app architecture using "modules" with provider package 【发布时间】:2020-08-09 08:41:07 【问题描述】:

几周以来我一直在用 Flutter 编写应用程序,几天前我开始想知道最好的架构是什么。

先说一点上下文:

这是一个使用 Firebase 作为后端的消息传递应用程序; 它在很大程度上依赖于美妙的Provider 包来处理整个应用程序的状态 计划是拥有多个可以相互交互的功能。 我是 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 类成员) 每个功能都有一个(愚蠢的)UI 部分,可以直接与“逻辑”部分交互(因此可能不那么愚蠢); 每个功能都可以将其 UI 部分替换为自定义 UI,但是,自定义 UI 必须自己实现与逻辑部分的交互; 如果我以后需要将它们存储在 Firebase 中,每个功能都可以具有代表功能资源的模型

我已经在我的应用程序的一个功能(或 2 个取决于您如何看待它)上尝试了这种方法,即录制/收听语音笔记的能力。 我觉得这很有趣,因为您可以在一个地方录制,但可以在多个地方收听录音:例如,在录音之后或录音发送给您时。

这是我想出的:

folder structure of the test 在这种情况下没有 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 小部件可以使用多个功能,那么我将需要多个 Fe​​atureNotifier(即使 Multiprovider 在这种情况下很有用);

最后,问题如下:

如果我以后可以继续以这种方式创建功能而不会遇到麻烦,您认为这种方法是否可扩展/推荐? 您还发现其他缺点吗?

【问题讨论】:

看看这两个系列:resocoder.com/2019/08/27/… 和resocoder.com/2020/03/09/… 另外我建议使用 bloc 库来解决更复杂的状态管理问题 【参考方案1】:

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),
            ),
          );
        );
  

演示

【讨论】:

以上是关于使用带有提供程序包的“模块”的 Flutter 应用程序架构的主要内容,如果未能解决你的问题,请参考以下文章

获取错误java在flutter中使用或覆盖带有contacts_service包的已弃用API

未找到模块'apple_sign_in'

使用 .aar 模块的 Flutter 插件在示例应用程序中构建和运行良好,但无法在其他应用程序中构建

向下滚动时隐藏的 Flutter TabBar 和 SliverAppBar

由于命令中的错误,带有 Flutter 的 Facebook Auth 未提供密钥哈希

带有小部件列表的 Flutter 动画切换器