Skip to content

Architecture

Nikita Sirovskiy edited this page Jul 31, 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 i18n_extension

Architecture

The code base is divided in several modules:

  • leafy_data — all the data related stuff;
  • leafy_domain — domain related things: use cases, services interfaces etc;
  • leafy_launcher — launcher itself: the controllers, the app UI etc;
  • leafy_localization — all the translations and localizations;
  • leafy_resources — application colours, paddings, text styles etc.;
  • leafy_ui_kit — reusable widgets.

// TODO: Add links to the folders when the update is shipped // TODO: Put the detailed description about the packages to the corresponding readmes.

Data

Domain

Use Cases

Services

Launcher (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: const Scaffold(
    body: SafeArea(
      child: MyControllerListener(
        child: MyBody(),
      ),
    ),
  ),
);

Reasons:

  • The page configuration is easily found in one file;
  • Imagine that MyBody in the example above will be replaced by the tree from the example below. 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.