繁体   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