简体   繁体   English

Flutter 应用程序架构使用“模块”与提供者 package

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

I've been coding an app in Flutter since few weeks now and started wondering what the best architecture could be a few days ago.几周以来,我一直在 Flutter 中编写应用程序,几天前我开始想知道最好的架构是什么。

A little bit of context first:先说一点上下文:

  • It's a messaging app using Firebase as a backend;这是一个使用 Firebase 作为后端的消息应用程序;
  • It's heavily relying on the wonderful Provider package to handle the state throughout the whole app它严重依赖出色的Provider package 来处理整个应用程序中的 state
  • The plan is to have multiple features, that can interact with each other.该计划是具有多个功能,可以相互交互。
  • I'm fairly new to Flutter (React/ReactNative background mostly), it can explain the strange approach I've below.我对 Flutter (主要是 React/ReactNative 背景)相当陌生,它可以解释我下面的奇怪方法。

I've been experiencing different architecture approaches and managed to get one working that finally seems to suit me.我一直在体验不同的架构方法,并设法找到一种最终似乎适合我的工作。

As I'll have multiple features, reusable at different places in the app, I want to split the code by features(or Modules) that can then be used independently in different screens.由于我将拥有多个功能,可在应用程序的不同位置重用,我想将代码按功能(或模块)拆分,然后可以在不同的屏幕上独立使用。 The folder architecture would be like this:文件夹架构如下:

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
              |             |--> ...
              |
             ...

Ideally each feature would follow these guidelines:理想情况下,每个功能都遵循以下准则:

  • Each feature has a logic part (often using Provider Package);每个特性都有一个逻辑部分(通常使用Provider Package);
  • Each feature logic part can request variables (ChangeNotifier class members) from another Feature每个功能逻辑部分都可以从另一个功能请求变量(ChangeNotifier class 成员)
  • Each feature has a (dumb) UI part that can directly interact with the "logic" part (thus maybe not so dumb);每个功能都有一个(愚蠢的)UI 部分,可以直接与“逻辑”部分交互(因此可能不那么愚蠢);
  • Each feature can have its UI part being replaced by a custom UI but then, the custom UI must implement the interaction(s) with the logic part on its own;每个功能都可以将其 UI 部分替换为自定义 UI,但是,自定义 UI 必须自己实现与逻辑部分的交互;
  • Each feature, can have models that are the representation of the feature ressources if I need to store them in Firebase later如果我需要稍后将它们存储在 Firebase 中,每个特征都可以具有代表特征资源的模型

I've tried this approach with one feature (or 2 depends how you see it) of my app, which is the ability to record / listen to voice notes.我已经用我的应用程序的一个功能(或 2 个取决于你如何看待它)尝试了这种方法,即录制/收听语音笔记的能力。 I found it interesting because you can record at one place but listen to the recording at many places: just after the recording for instance or when a recording is sent to you as well.我发现这很有趣,因为您可以在一个地方录制,但可以在多个地方收听录音:例如,在录音之后或录音发送给您时。

Here is what I came up with:这是我想出的:

  • folder structure of the test There is no models/ folder in this case because it's just a file that I'm handling elsewhere测试的文件夹结构在这种情况下没有models/文件夹,因为它只是我在其他地方处理的文件
  • The voiceRecorderNotifier handles a file (add/remove) and a recording (start/end) voiceRecorderNotifier 处理文件(添加/删除)和录音(开始/结束)
  • The voicePlayerNotifier requires a file to be instanciated (named constructor) and then handles the audio file playing (play, pause, stop). voicePlayerNotifier 需要实例化一个文件(命名构造函数),然后处理音频文件播放(播放、暂停、停止)。

In the code, it's a bit verbose but works as expected, for instance, in the screens, I can request the voiceRecorder feature like this:在代码中,它有点冗长,但按预期工作,例如,在屏幕中,我可以请求 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(), 
          ...,
        ]
      )
    );
  }
}

I can have the two features (voiceRecorder / voicePlayer) working together as well, like this:我也可以让这两个功能(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(),
        ]
      )
    );
  }
}

It's a fresh test so I assume that there are drawbacks that I can't see now but I feel like there is few good points:这是一个新的测试,所以我认为有一些我现在看不到的缺点,但我觉得有几个优点:

  • Cleaner folders structure;更干净的文件夹结构;
  • One place to handle the "high level" logic related to the feature, easy to update;一个地方处理与功能相关的“高级”逻辑,易于更新;
  • It's easy to add, move, remove the feature everywhere is the app;在应用程序的任何地方添加、移动、删除功能都很容易;
  • A basic UI is provided for the feature to work as expected, such as a simple提供了一个基本 UI 以使该功能按预期工作,例如一个简单的
Text('hi')

but I can still "override" the UI for specific display of the feature;但我仍然可以“覆盖” UI 以特定显示该功能;

  • I can focus on UI and usage of features rather than creating a lot of Stateful components to replicate the same logic of a feature a different places;我可以专注于 UI 和功能的使用,而不是创建大量有状态的组件来在不同的地方复制功能的相同逻辑;

The drawbacks I see:我看到的缺点:

  • Feature logic is "hidden", I'll need to go through the Notifier each time I want to do something specific with the feature to remember how the feature behaves;功能逻辑是“隐藏的”,每次我想对功能执行特定操作以记住功能的行为时,我都需要通过通知程序 go ;
  • Implementing the notifiers at the good place can become a mess, if a UI widgets can have multiple features usage then i'll require multiple FeatureNotifier (even if Multiprovider is useful in this case);在好地方实现通知器可能会变得一团糟,如果 UI 小部件可以使用多个功能,那么我将需要多个 FeatureNotifier(即使 Multiprovider 在这种情况下很有用);

Finally, here are the questions:最后,以下是问题:

  • Do you think this approach is scalable/recommended thus if I can continue creating features this way without having troubles later?您是否认为这种方法是可扩展的/推荐的,因此如果我可以继续以这种方式创建功能而以后不会遇到麻烦?
  • Do you see any other drawbacks?你看到其他的缺点了吗?

Provider is a great tool that can help you access data all throughout the app. Provider 是一个很棒的工具,可以帮助您访问整个应用程序中的所有数据。 I don't seen any issues on how it's currently implemented on your app.我没有看到有关它当前如何在您的应用上实现的任何问题。

On the points that you're looking for like handling logic and updating UI, you may want to look into BloC pattern.在您正在寻找的点上,例如处理逻辑和更新 UI,您可能需要研究 BloC 模式。 With this, you can handle UI updates via Stream and the UI can be updated on StreamBuilder.有了这个,您可以通过 Stream 处理 UI 更新,并且可以在 StreamBuilder 上更新 UI。

This sample demonstrates updating the UI on the Flutter app using BloC pattern.此示例演示使用 BloC 模式更新 Flutter 应用程序上的 UI。 Here's the part where all logic can be handled.这是可以处理所有逻辑的部分。 Timer is used to mock the waiting time for an HTTP response. 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;
  }
}

Here's our main app.这是我们的主要应用程序。 Notice how we don't need to call setState() to refresh the UI anymore.注意我们不再需要调用setState()来刷新 UI。 UI updates are dependent on the StreamBuilder set on them. UI 更新依赖于对它们设置的 StreamBuilder。 With each update on Stream with StreamController.broadcast.sink.add(Object) , StreamBuilder gets rebuilt again to update the UI.每次使用StreamController.broadcast.sink.add(Object)对 Stream 进行更新时,StreamBuilder 都会再次重建以更新 UI。 StreamBuilder was also used to show/hide the LinearProgressBar. 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),
            ),
          );
        });
  }
}

Demo演示

演示

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM