Skip to content

Architecture

Nikita Sirovskiy edited this page May 6, 2022 · 4 revisions

⚠️ This page is being written on the go along with the ticket #213

Core Libraries

Component. Library
State Management controllable
Dependency Injection get_it + injectable
Navigation auto_route
Localization TBD

Architecture

The app is divided in three modules:

  • Data — contains all the models used in the app;
  • Domain — contains services interfaces, their implementations and use cases;
  • Presentation — the UI, the platform specific implementations of services, all the controllers, localization etc.

Data

Domain

Use Cases

Services

Presentation

Features

Every screen is a so-called «feature». Home — is a parent feature while App Picker and Settings are sub-features of Home.

The file structure for a feature is the following:

feature_name
  controller
    /* feature controller files */
  widgets
    /* feature sub widgets */
  feature_page.dart
  feature_body.dart
  feature_listener.dart // if needed

Pages

Page widgets contains the higher configuration of a page: Provider / Scaffold / SafeArea / Listeners / Inherited — it all goes there. The UI of the page itself goes to the Body widget.

So a page is usually something like this:

return XProvider<MyController>(
  create: (_) => injector(),
  child: Builder(
    // To access MyController from the listener.
    builder: (context) {
      return const Scaffold(
        body: SafeArea(
        child: MyControllerListener(
          listenable: context.myController,
          child: MyBody(),
        );
      },
    },
  ),
);

Reasons:

  • The page configuration is easily found in one file;
  • Imagine the tree from the example below will be put above instead of just creating MyBody. That would create a terrible waterfall widget.

Bodies

Body widgets contain all the UI of the page.

❗️ One important requirement is to decompose widgets making them as small as possible. So instead of:

build:

return Column(
  children: [
    Text(
      someText,
      style: AppStyle.first,
    ),
    TextButton(
      onPressed: () {
        /* Do something */
      },
      child: Text(
        pressMeToDoSomething,
        style: AppStyle.first,
      ),
    ),
    Column(
      // More waterfalls ...
    ),
  ],
);

We do this:

build:

return Column(
  children: const [
    FeatureTitleText(),
    FeatureDoSomethingButton(),
    FeatureMoreWaterfalls(),
  ],
);

Reasons:

  • The tree is more readable: in the latter example you can clearly see what the tree contains, e.g. the title, the button and some waterfall widgets. In the example above you would need to get into the details like reading the text to understand what's there in the tree;
  • Moreover, we exclude possible waterfalls from the tree because all the nesting goes to other classes;
  • The tree is less complex on every level;
  • To avoid unnecessary rebuilds (notice: the whole list is const!);
  • To keep the possible widget logic (like animation controllers etc) separately from other stateless things.
Clone this wiki locally