- Flutter Clean Architecture Sample App - Dasher
- Architecture structure
- Folder structure
- Riverpod and GetIt
- Example of architecture flow
- Screenshots
- Infinum architecture Mason brick
This project is a starting point for a Flutter application. Dasher App will introduce you to clean architecture structure and how inner / outer layers are connected.
Dasher app uses the architecture structure described in handbook.
There is no business logic on this layer, it is only used to show UI and handle events. Read more about Presentation layer in handbook.
- Notify presenter of events such as screen display and user touch events.
- Observes presenter state and can rebuild on state change.
- Contains presentation logic, usually controlling the view state.
This layer is responsible for business logic.
- The main job of the interactor is combining different repositories and handling business logic.
- Singleton class that holds data in memory, that doesn't call repositories or other outer layers.
- It uses concrete implementations like dio, hive, add2calendar, other plugins and abstracts them from the rest of the application.
- Repository should be behind and interface.
- Interface belongs to the domain and the implementation belongs to the outer layers.
- Represents communication with remote sources (web, http clients, sockets).
- Represents communication with local sources (database, shared_prefs).
- Represents communication with device hardware (e.g. sensors) or software (calendar, permissions).
Top-level folder structure you will find in the project under the /lib:
- app contains app run_app with various setups like the setup of flutter.onError crash handling and dependency initialization.
- common contains code that's common to all layers and accessible by all layers.
- device is an outer layer that represents communication with device hardware (e.g. sensors) or software (calendar, permissions).
- domain is the inner layer that usually contains interactors, data holders. This layer should only contain business logic and not know about specific of ui, web, etc. or other layers.
- source_local is an outer layer that represents communication with local sources (database, shared_prefs).
- source_remote is an outer layer that represents communication with remote sources (web, http clients, sockets).
- ui is the layer where we package by feature widgets and presenters. Presenters contains presentation logic and they access domain and are provided in the view tree by Provider/Riverpod package.
- main_production.dart and main_staging.dart two versions of main file, each version has it's own flavor in practice this usually means having two versions. Find more about flavors here.
This architecture structure is using Riverpod for Presentation layer and GetIt for Domain and outer layers (source remote, source local and device).
Read more about how to use riverpod in handbook.
In this example, we'll show the architecture flow for fetching new Tweets on the Dashboard screen.
One of the widgets on the Dashboard screen is DasherTweetsList
. Inside the Tweets list widget is
created reference to watch feedRequestPresenter
.
final _presenter = ref.watch(feedRequestPresenter);
For FeedRequestPresenter
we are using RequestProvider
, you can find more about it here.
Inside FeedRequestPresenter
we created instance of FetchFeedInteractor
interface.
final feedRequestPresenter = ChangeNotifierProvider.autoDispose<FeedRequestPresenter>(
(ref) => FeedRequestPresenter(GetIt.instance.get()),
);
class FeedRequestPresenter extends RequestProvider<List<Tweet>> {
FeedRequestPresenter(this._feedTimelineInteractor) {
fetchTweetsTimeline();
}
final FetchFeedInteractor _feedTimelineInteractor;
Future<void> fetchTweetsTimeline() {
return executeRequest(requestBuilder: _feedTimelineInteractor.fetchFeedTimeline);
}
}
From this part, we slowly transition toward Domain layer.
Domain is a business logic layer, where we have an implementation of FetchFeedInteractor
called
FetchFeedInteractorImpl
. Our task is to create an instance of Repository which is responsible
for handling outer logic for getting user timeline tweets. FeedRepository
is also behind an interface.
class FetchFeedInteractorImpl implements FetchFeedInteractor {
FetchFeedInteractorImpl(this._feedRepository);
final FeedRepository _feedRepository;
@override
Future<List<Tweet>> fetchFeedTimeline() {
return _feedRepository.fetchFeedTimeline();
}
}
FeedRepositoryImpl
is part of Source remote layer. This repository is using twitter_api_v2
package for fetching data from Twitter's API.
Future<List<Tweet>> fetchFeedTimeline() async {
final response = await twitterApi.tweetsService.lookupHomeTimeline(
userId: userDataHolder.user!.id,
tweetFields: [
TweetField.publicMetrics,
TweetField.createdAt,
],
userFields: [
UserField.createdAt,
UserField.profileImageUrl,
],
expansions: [
TweetExpansion.authorId,
],
);
return _getTweetsListWithAuthors(response);
}
after a successful response, data is passed back to FeedRequestPresenter
in his state,
and it triggers state listeners. Inside the build method of DasherTweetsList
we use state
listeners of FeedRequestPresenter
so we can easily show/hide widgets depending on the emitted event.
_presenter.state.maybeWhen(
success: (feed) => _TweetsList(
feed: feed,
),
initial: () => const CircularProgressIndicator(),
loading: (feed) {
if (feed == null) {
return const CircularProgressIndicator();
} else {
return _TweetsList(
feed: feed,
);
}
},
failure: (e) => Text('Error occurred $e'),
orElse: () => const CircularProgressIndicator(),
),
Login | Feed |
---|---|
Profile | New Tweet |
---|---|
Easiest way to set up our architecture in the project is with usage of Mason bricks. The infinum_architecture brick is published on https://brickhub.dev/bricks/infinum_architecture/ and it will generate all the required directories and files ready to start the project.
Make sure you have installed FVM - Flutter Version Management.
dart pub global activate fvm
Also install Mason CLI it's must have for using Mason bricks.
dart pub global activate mason_cli
Create new Flutter project:
flutter create {project_name}
move to project folder:
cd {project_name}
Initialize mason:
mason init
Add mason brick to your project:
mason add infinum_architecture
Start generating Infinum architecture folder structure:
mason make infinum_architecture --on-conflict overwrite
Variable | Description | Default | Type |
---|---|---|---|
project_name |
This name is used to name main function and files run{project_name}App() |
example | string |
flutter_version |
Defines which version of FVM you want to install | stable | string |
brick_look |
Optional Look | true | bool |
brick_request_provider |
Optional Request Provider | true | bool |
π¦ lib
β£ π app
β β£ π di
β β β π inject_dependencies.dart
β β£ π example_app.dart
β β π run_example_app.dart
β£ π common
β β£ π error_handling
β β β£ π base
β β β β£ π expected_exception.dart
β β β β π localized_exception.dart
β β β π error_formatter.dart
β β£ π flavor
β β β£ π app_build_mode.dart
β β β£ π flavor.dart
β β β£ π flavor_config.dart
β β β π flavor_values.dart
β β π logger
β β£ π custom_loggers.dart
β β π firebase_log_printer.dart
β£ π device
β β π di
β β π inject_dependencies.dart
β£ π domain
β β π di
β β π inject_dependencies.dart
β£ π source_local
β β π di
β β π inject_dependencies.dart
β£ π source_remote
β β π di
β β π inject_dependencies.dart
β£ π ui
β β£ π common
β β β£ π generic
β β β β π generic_error.dart
β β π home
β β π home_screen.dart
β£ π main_production.dart
β π main_staging.dart