Architecture
Last modified on Tue 29 Jun 2021

Architecture will make code easier to maintain, test and understand.

Here is the top-level folder structure you will find in the project under the /lib:

1

In previously defined layers there are several classes that you'll often use.

2

Repository and Manager

Repository is outer part of our application. It belongs to source_remote, source_local or device. It uses concrete implementations like dio, hive, add2calendar, other plugins and abstracts them from the rest of the application.

Repository should be behind an interface. This means that you should create interface YourRepository and concrete class YourRepositoryImpl implements YourRepository. The YourRepository interface belongs to the domain and the YourRepositoryImpl belongs to the outer layers. This way the domain can access repository, but it does not know about specific implementation of that interface (dependency inversion princinple).

Example of repository to fetch list of meetups:


//domain/repository/meetup_repository/meetup_repository.dart

abstract class MeetupRepository {
  Future<List<Meetup>> getListOfMeetups();
}


//source_remote/impl/meetup_repository/meetup_repository_impl.dart

class MeetupRepositoryImpl implements MeetupRepository {
  MeetupRepositoryImpl(this._dio);

  final Dio _dio;

  @override
  Future<List<Meetup>> getListOfMeetups() async {
    final Response<String> response = await _dio.post<String>('/api/meetups');
    return MeetupsResponse.fromJson(jsonDecode(response.data)).meetups;
  }
}


Manager works same as Repository, but we use it just for the better naming. Sometimes this layer can actively manage, e.g. by adding events to calendar, turning on bluetooth or managing permissions. Instead of calling them BluetoothRepository, we use something like BluetoothManager.

Interactor

Interactor is the inner part that belongs to domain. Domain contains business logic of the application, they can access other classes from domain including repositories. Interactor is also behind an interface for easier testing, so we create YourInteractor and YourInteractorImpl. Main job of interactor is combining diferrent repositories and handling business logic.

Example of interactor to add meetup event to calendar


//domain/interactor/add_event_to_interactor/add_event_to_interactor.dart

abstract class AddEventToCalendarInteractor {
  Future<void> addEventToCalendar(Meetup event);
}

//domain/interactor/add_event_to_interactor/add_event_to_interactor_impl.dart

class AddEventToCalendarInteractorImpl extends AddEventToCalendarInteractor {
  AddEventToCalendarInteractorImpl(this._calendarManager, this._meetupRepository);

  final CalendarManager _calendarManager;
  final MeetupRepository _meetupRepository;

  @override
  Future<void> addEventToCalendar(Meetup meetup) async {
    final dateTimeOfEvent = await _meetupRepository.getMeetupEventDate(meetup);

    final CalendarEvent event = CalendarEvent(meetup.name, dateTimeOfEvent);

    return await _calendarManager.addEventToCalendar(event);
  }
}

Providers and widgets

Providers and widgets are part of the presentation that we put in the ui. They are packaged together by layer which makes it easier to navigate because they work closely together. Provider contains presentation logic, usually controling the view state. Widget observes that state and can rebuild on state change.

This way view is passive and just reacts to changes. It's easy to maintain and test. View should mostly consist of stateless widgets that observe provider view state.

Example of Meetup provider:


//ui/meetup/provider/meetup_screen_provider.dart

class MeetupScreenProvider extends ChangeNotifier {
  MeetupScreenProvider(this._addEventToCalendarInteractor);

  final AddEventToCalendarInteractor _addEventToCalendarInteractor;
  final AddMeetupToFavoritesInteractor _addMeetupToFavoritesInteractor;

  void onAddToCalendar(Meetup meetup) {
    _addEventToCalendarInteractor.addEventToCalendar(meetup);
  }

  void onMeetupFavorite(Meetup meetup) {
    _addMeetupToFavoritesInteractor.addToFavorites(meetup);
  }

}

Example of UI that reacts to provider and rebuilds:


    Consumer<MeetupScreenProvider>(
      builder: (context, provider, _) {
        return _MeetupList(list: provider.state);
      },
    ),

The requests are done a bit diferrently. Request is state of it's own with values like loading, success and error. That's why we generally don't bundle requests all in one provider together with all other states into one mega state. Since you often need to listen exactly one request states to show loading indicator or error, it causes problems because provider will update all listeners that listens on mega state. That mega provider can also be large and hard to maintain.

That's why we split them and extend requestprovider which will handle states like _loading, success and error for us. You can read more about request provider in bits: https://github.com/infinum/flutter-bits/tree/master/request_provider

Models

Models are simple data structures. Usually they will be part of common (/common/models). These are the models that are used by multiple layers. For example, you can have User with @JsonSerializable that's used by source_remote, but that same model is also used by domain and UI.

You can also have models as a be part of specific layer (/source_remote/model or ui/my_feature/model) or inside specific feature (/domain/manager/permission_manager/device_permissions.dart) if that makes more sense.

To differentiate all of these models, we have special naming for some layers. We suffix the UI models with Ui (e.g. ArticleUi) and use this when domain model doesn't fit what needs for us to show on the screen. These models are only used in the ui.

Models from other outer layers are suffixed with Dto which stands for Domain Transfer Object (e.g. ArticleDto). We use them when structure we receive from outer layers (like from API) is not in format we would like to work with, so we create ArticleDto (model from API) and Article (model for our app).

As we said that inner layer should never know about specific of outer layers, same applies here. Domain should never know about Ui or Dto models. They need to be mapped in repository or provider.

Data holders

DataHolder is singleton class that holds data in memory. It doesn't have an interface and it only has data and methods to get or set data. Data holders are part of domain and they don't call repositories or other outer layers.

Mappers

These are classes with static methods that will map models between different layers.

For ArticleDto -> Article mapping we create ArticleMapper. For vice-versa Article -> ArticleDto we create ArticleDtoMapper.

Mapper could have multiple methods:

class ArticleMapper {
  Article map(ArticleDTO dto){...}
  Article mapFromXyz(XyzDTO dto){...}
  List<Article> mapToList(List<ArticleDTO> dto){...}
}

Mappers are the part of the ui, source_remote, device and other outer layers (/source_remote/mapper). If your case doesn't required ui or dto models then you don't need mappers.