flutter - 使用带有提供程序包的“模块”的 Flutter 应用程序架构
问题描述
几周以来,我一直在用 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 个取决于你如何看待它)尝试了这种方法,即录制/收听语音笔记的能力。我觉得这很有趣,因为您可以在一个地方录制,但可以在多个地方收听录音:例如,在录音之后或录音发送给您时。
这是我想出的:
- 测试的文件夹结构在这种情况下
没有
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 和功能的使用,而不是创建大量有状态的组件来在不同的地方复制功能的相同逻辑;
我看到的缺点:
- 功能逻辑是“隐藏的”,每次我想要对功能执行特定操作时都需要通过通知程序来记住功能的行为方式;
- 在好地方实现通知器可能会变得一团糟,如果 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。随着 Stream with 的每次更新StreamController.broadcast.sink.add(Object)
,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),
),
);
});
}
}
演示
推荐阅读
- c - 如何优化 3d 数组查找
- android - 是否可以在flutter应用程序中为android应用程序创建一个mySQL数据库并将该连接用作flutter本身的数据库服务器?
- javascript - 验证单选按钮和复选框
- jquery - jQuery从ajax加载的内容替换字符串
- php - php和ajax传递日期strtotime转换不同
- rest - 使用 ADF REST API 的 RESTful Web 服务无法获取超过 500 条记录
- sql - BigQuery:如何简化此 SQL
- php - 使用 PHP 从 mySQL 导出数据
- python-3.x - PyTorch 已安装但未导入
- reactjs - 由顶级 BrowserRouter 调用的组件内部的 BrowserRouter 与 NavLinks 不同步