[英]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:先说一点上下文:
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:理想情况下,每个功能都遵循以下准则:
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:这是我想出的:
models/
folder in this case because it's just a file that I'm handling elsewheremodels/
文件夹,因为它只是我在其他地方处理的文件 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:这是一个新的测试,所以我认为有一些我现在看不到的缺点,但我觉得有几个优点:
Text('hi')
but I can still "override" the UI for specific display of the feature;但我仍然可以“覆盖” UI 以特定显示该功能;
The drawbacks I see:我看到的缺点:
Finally, here are the questions:最后,以下是问题:
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.