Reputation: 31
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.
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. 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 elsewhereIn the code, it's a bit verbose but works as expected, for instance, in the screens, I can request the voiceRecorder feature like this:
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:
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;
The drawbacks I see:
Finally, here are the questions:
Upvotes: 3
Views: 3564
Reputation: 11
Be aware of common functionality across features such as session/user information that also might come from a provider - but in general common functionality across features to prevent duplicated code.
Upvotes: 0
Reputation: 10519
Provider is a great tool that can help you access data all throughout the app. 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. With this, you can handle UI updates via Stream and the UI can be updated on StreamBuilder.
This sample demonstrates updating the UI on the Flutter app using BloC pattern. Here's the part where all logic can be handled. Timer
is used to mock the waiting time for an HTTP response.
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. UI updates are dependent on the StreamBuilder set on them. With each update on Stream with StreamController.broadcast.sink.add(Object)
, StreamBuilder gets rebuilt again to update the UI. StreamBuilder was also used to show/hide the 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
Upvotes: 0