Flutter Clean Architecture Sample App - Dasher

Flutter Clean Architecture Sample App – Dasher

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.

Architecture structure

Dasher app uses the architecture structure described in handbook.

Presentation

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.

Widgets (UI)

  • Notify presenter of events such as screen display and user touch events.
  • Observes presenter state and can rebuild on state change.

Presenter

  • Contains presentation logic, usually controlling the view state.

Domain

This layer is responsible for business logic.

Interactor

  • The main job of the interactor is combining different repositories and handling business logic.

Data Holder

  • Singleton class that holds data in memory, that doesn’t call repositories or other outer layers.

Outer layer

Repository

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

Source remote

  • Represents communication with remote sources (web, http clients, sockets).

Source local

  • Represents communication with local sources (database, shared_prefs).

Device

  • Represents communication with device hardware (e.g. sensors) or software (calendar, permissions).

Folder structure

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.

Riverpod and GetIt

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.

Example of architecture flow

In this example, we’ll show the architecture flow for fetching new Tweets on the Dashboard screen.

Presentation

Widget

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);

Presenter

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

Interactor

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();
  }
}

Repository

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(),
  ),

Screenshots

Login Feed
Profile New Tweet

Infinum architecture Mason brick

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.

How to use

Tools to install

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 project

Create new Flutter project:

flutter create {project_name}

move to project folder:

cd {project_name}

Mason brick setup

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

Variables

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

Outputs

? 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

GitHub

View Github