elementary 1.0.0 elementary: ^1.0.0 copied to clipboard
This is architecture library with the main goal to split code between different responsibility layers, make code clear, simple, readable and easy testable.
Elementary #
Description #
The primary goal of this library is to split code into different responsibility layers, thus making it clearer, simpler as well as more readable and testable. This approach is based on an architectural pattern called MVVM and the fundamentals of Clean Architecture.
Overview #
Thanks to its elaborately separated concerns, Elementary makes it easier to manage whatever is displayed at a particular moment and bind it with the business logic of your app. Now, before we get into detail about its structure, let's look at the graph so that you can see for yourself how easy it is to work with Elementary.
Technical Overview #
This library applies a classic principle of an MVVM pattern, which is layering. In it, we have Widget acting as a View layer, WidgetModel as a ViewModel layer, and Model as a Model layer.
And all of that put together works pretty similar to Flutter itself.
WidgetModel #
The key part in this chain of responsibility is the WidgetModel layer that connects the rest of the layers together and describes state to Widget via a set of parameters. Moreover, it is the only source of truth when you build an image. By the time build is called on the widget, the WidgetModel should provide it with all the data needed for a build. The class representing this layer in the library has the same name – WidgetModel. You can describe a state to be rendered as a set of various properties. In order to determine the properties required, you should specify them in the IWidgetModel interface, the subclasses of which, in turn, determine what properties are used in this or that situation. In order to establish a quicker response to any changes in properties of a state object, you can use StateNotifier. Subscribers are then notified whenever a change occurs in a state.
Due to this, the WidgetModel is the only place where presentation logic is described: what interaction took place and what occurred as a result.
For example, when data is loaded from the network, the WidgetModel looks like this:
/// Widget Model for [CountryListScreen]
class CountryListScreenWidgetModel
extends WidgetModel<CountryListScreen, CountryListScreenModel>
implements ICountryListWidgetModel {
final _countryListState = EntityStateNotifier<Iterable<Country>>();
@override
ListenableState<EntityState<Iterable<Country>>> get countryListState =>
_countryListState;
/// Some special wm working code this
/// ...............................................................
Future<void> _loadCountryList() async {
final previousData = _countryListState.value?.data;
_countryListState.loading(previousData);
try {
final res = await model.loadCountries();
_countryListState.content(res);
} on Exception catch (e) {
_countryListState.error(e, previousData);
}
}
}
Interface for this WidgetModel looks like this:
/// Interface of [CountryListScreenWidgetModel]
abstract class ICountryListWidgetModel extends IWidgetModel {
ListenableState<EntityState<Iterable<Country>>> get countryListState;
}
The only place where we have access to BuildContext and need to interact with it is WidgetModel.
Model #
The only WidgetModel dependency related to business logic is Model. The class representing this layer in the library is called ElementaryModel. There is no declared way to define this one, meaning you can choose whichever way works best for your project. One of the reasons behind that is to provide an easy way to combine elementary with other approaches related specifically to business logic.
Widget #
Since all logic is already described in the WidgetModel and Model, Widget only needs to declare what a certain part of the interface should look like at a particular moment based on the WidgetModel properties. The class representing the Widget layer in the library is called ElementaryWidget. The build method called to display a widget only has one argument – the IWidgetModel interface.
It looks like this:
@override
Widget build(ICountryListWidgetModel wm) {
return Scaffold(
appBar: AppBar(
title: const Text('Country List'),
),
body: EntityStateNotifierBuilder<Iterable<Country>>(
listenableEntityState: wm.countryListState,
loadingBuilder: (_, __) => const _LoadingWidget(),
errorBuilder: (_, __, ___) => const _ErrorWidget(),
builder: (_, countries) => _CountryList(
countries: countries,
nameStyle: wm.countryNameStyle,
),
),
);
}
How to test #
Since the layers are well-separated from each other, they are easy to test with a number of options available.
- Use unit tests for Model layer;
- Use widget and golden tests for Widget layer;
- Use widget model test from elementary_test library for WidgetModel.
Sponsor #
Our main sponsor is Surf.