## Description
-This is a home to a family of Elementary library packages.
+This repository is a home to packages and tools from the Elementary library family.
-| Package | Version |
-| --------------|:-------------:|
-| [elementary](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary) | [![Pub Version](https://img.shields.io/pub/v/elementary?logo=dart&logoColor=white)](https://pub.dev/packages/elementary) |
-| [elementary_test](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary_test) | [![Pub Version](https://img.shields.io/pub/v/elementary_test?logo=dart&logoColor=white)](https://pub.dev/packages/elementary_test) |
-| [elementary_cli](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary_tools/elementary_cli) | [![Pub Version](https://img.shields.io/pub/v/elementary_cli?logo=dart&logoColor=white)](https://pub.dev/packages/elementary_cli) |
+| Package | Version |
+|----------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------:|
+| [elementary](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary) | [![Pub Version](https://img.shields.io/pub/v/elementary?logo=dart&logoColor=white)](https://pub.dev/packages/elementary) |
+| [elementary_helper](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary_helper) | [![Pub Version](https://img.shields.io/pub/v/elementary_helper?logo=dart&logoColor=white)](https://pub.dev/packages/elementary_helper) |
+| [elementary_test](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary_test) | [![Pub Version](https://img.shields.io/pub/v/elementary_test?logo=dart&logoColor=white)](https://pub.dev/packages/elementary_test) |
+| [elementary_cli](https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary_tools/elementary_cli) | [![Pub Version](https://img.shields.io/pub/v/elementary_cli?logo=dart&logoColor=white)](https://pub.dev/packages/elementary_cli) |
## Elementary overview
-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.
+Elementary is a simple and reliable way to build applications with MVVM in Flutter.
+Benefits from using:
-## Sponsor
+- maximum Flutter-like, you don't need to spend a lot of time learning the library if you are already familiar with the
+ standard Flutter approaches;
+- splitting code into different layers by responsibility, that bring low coupling, make code simpler as well as more
+ readable;
+- high testability of all layers from widgets to business logic;
+- speed boost for a team consisting of more than one person, due to the easy sharing of independent task-parts among
+ team members.
-Our main sponsor is [Surf](https://surf.ru/).
+## Environment
+
+For reduce amount of boilerplate and the manual work, for Elementary there are few options:
+
+- [elementary_cli](https://pub.dev/packages/elementary_cli) - command line util helps with boilerplate generation;
+- [plugin for IntelliJ](https://plugins.jetbrains.com/plugin/18099-elementary) - plugin for IntelliJ IDE family,
+based on elementary_cli;
+- [plugin for VSCode](https://marketplace.visualstudio.com/items?itemName=ElementaryTeam.elementary) - plugin for VSCode,
+based on elementary_cli;
+- [elementary brick](https://brickhub.dev/bricks/elementary) - command line util helps with boilerplate generation,
+based on mason.
+
+## Examples
+
+[Country](https://github.com/Elementary-team/flutter-elementary/tree/main/examples/country) - general example how to use elementary for development;
+
+[Elementary with Redux](https://github.com/Elementary-team/flutter-elementary/tree/main/examples/elementary_redux) - example how to use elementary + redux;
+
+[Profile](https://github.com/Elementary-team/flutter-elementary/tree/main/examples/profile) - example shows the feature with the process that spread by separate screens, and also how to use elementary + bloc;
## Maintainer
-[Mikhail Zotyev](https://github.com/MbIXjkee)
+
+
## 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.
+The primary goal of the library is to help write application in a simple and reliable way, as well as make the codebase
+easier readable and more testable. This approach is based on splitting code into different layers depends on
+responsibilities, those as business logic, presentation logic and declarative description for UI.
+Due to this layer separation, the library brings additional performance boost for teams, because a few persons can
+work on the same feature at the same time but on different layers. This library was inspired by Flutter itself,
+MVVM architecture pattern, Business Logic Component architecture pattern, as well by fundamental principles
+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.
+Thanks to elaborately separated responsibilities, Elementary makes it easier to manage whatever is displayed at a
+particular moment based on concrete conditions and business logic state of the app. Let's check the graphic schema
+of how it works internally for a simple screen, and what the user sees every moment.
## 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.
+Elementary uses a classical layers from the MVVM pattern, such as View, View Model, and Model. There are special
+entities which represent these layers: ElementaryWidget as a View layer, WidgetModel as a View Mode layer, and
+ElementaryModel as a Model layer.
-And all of that put together works pretty similar to Flutter itself.
+At the same time Elementary follows the Flutter-similar approach, so all these things are managed by Element.
### 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. You can use changeNotifiers (like a valueNotifier or stateNotifier, etc.), streams, getters
-or something else as 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.
+The key part in the chain of responsibilities is the WidgetModel layer that connects all other layers together and
+provides to ElementaryWidget a set of parameters which are describe the current presentation state. It is a working
+horse of the internal processes of Elementary, and the only source of truth for building a presentation
+(by the MVVM concept).
-Due to this, the WidgetModel is the only place where presentation logic is described: what interaction took place and
-what occurred as a result.
+#### WidgetModel's properties
-For example, when data is loaded from the network, the WidgetModel looks like this:
+MVVM is very efficient when possible to use binding properties. It is easy with Flutter, and Elementary does it.
+Elementary is sharped to use one side binding properties, which based on the design pattern Publisher-Subscriber.
+There is no mandatory requirement which one to use - you can decide based on circumstances for your concrete case.
+You can use ChangeNotifiers (like a ValueNotifier or StateNotifier, etc.), Streams, or any other.
+For properties which are not supposed to change, or initiate a visual change when they are changed, common getters also
+appropriate.
-```dart
-/// Widget Model for [CountryListScreen]
-class CountryListScreenWidgetModel extends WidgetModel
- implements ICountryListWidgetModel {
- final _countryListState = EntityStateNotifier>();
+#### Lifecycle
- @override
- ListenableState>> get countryListState =>
- _countryListState;
+Due to Widget Model is a central entity that binds all layers between and at the same time connected with Element,
+Widget Model has its own lifecycle. If you are familiar with the State lifecycle for StatefulWidget it should be
+as well simple for you.
- /// Some special wm working code this
- /// ...............................................................
+`initWidgetModel` is called only once for lifecycle of the WidgetModel in the really beginning before the first build.
+It can be used for initiate a starting state of the WidgetModel.
- Future _loadCountryList() async {
- final previousData = _countryListState.value?.data;
- _countryListState.loading(previousData);
+`didUpdateWidget` called whenever widget instance in the tree has been updated. Common case where rebuild comes from
+the top. This method is a good place for update state of the WidgetModel based on the new configuration of widget.
+When this method is called is just a signal for decide what exactly should be updated or rebuilt. The fact of update
+doesn't mean that build method of the widget will be called. Set new values to publishers for rebuild concrete
+parts of the UI.
- try {
- final res = await model.loadCountries();
- _countryListState.content(res);
- } on Exception catch (e) {
- _countryListState.error(e, previousData);
- }
- }
-}
-```
+`didChangeDependencies` called whenever dependencies which WidgetModel subscribed with BuildContext change. When this
+method is called is just a signal for decide what exactly should be updated or rebuilt. The fact of the call doesn't
+mean that build method of the widget will be called. Set new values to publishers for rebuild concrete parts of the UI.
+
+`deactivate` called when the WidgetModel with Element removed from the tree.
+
+`activate` called when WidgetModel with Elementary are reinserted into the tree after having been removed via deactivate.
+
+`dispose` called when WidgetModel is going to be permanently destroyed.
-Interface for this WidgetModel looks like this:
+`reassemble` called whenever the application is reassembled during debugging, for example during the hot reload.
+
+`onErrorHandle` called when the ElementaryModel handle error with the ElementaryModel.handleError method.
+Can be useful for general handling errors such as showing snack-bars.
+
+#### Contract
+
+It is a good to use an interface for a Widget Model, to make the code more testable and describe the contract
+in explicit way.
```dart
-/// Interface of [CountryListScreenWidgetModel]
-abstract class ICountryListWidgetModel extends IWidgetModel {
- ListenableState>> get countryListState;
+/// An interface for [ExampleScreenWidgetModel]
+abstract interface class IExampleScreenWidgetModel implements IWidgetModel {
+ ListenableState> get exampleState;
}
```
-_The only place where we have access to BuildContext and need to interact with it is WidgetModel._
+#### I = f(S) or a discrete state of UI
+
+A bit scary name of the section, but really simple meaning. In Flutter, we target to use declarative description of UI.
+We use to have a `build` method for component style widgets to describe a part of UI when Flutter needs it.
+Elementary Widget isn't an exception, but to describe a part of UI it uses Widget Model as a source of truth.
+A Widget Model instance is provided right into the build method and guaranty by its interface (contract) that everything
+that is needed to describe UI part is provided by Widget Model's properties.
+
+#### Context
+
+_The only place where we have access to BuildContext and need to interact with it is Widget Model._
+
+There are a few reasons to this.
+- ElementaryModel is already a business logic layer. Business logic should be pure and independent. BuildContext is not
+appropriate here.
+- ElementaryWidget has source of truth in form of WidgetModel. Using context there, we spread responsibilities
+and break the fact of being a source of truth for WidgetModel.
+- WidgetModel has tight bound with the Element.
-WidgetModel has access to Widget at any time, e.g. for using its properties like a configuration. It can be useful for
-initiate or update WidgetModel's properties like this:
+It is important to note that this fact applies only to the triad of entities ElementaryWidget-WidgetModel-ElementaryModel,
+widgets which are used by ElementaryWidget in the build method can have access to context.
+#### Widget - base immutable configuration
+
+Based on general Flutter approach, widget is an immutable configuration. WidgetModel has access to ElementaryWidget
+at any time. It can be useful for initiate or update WidgetModel's properties:
```dart
@override
@@ -106,19 +148,59 @@ void didUpdateWidget(TestPageWidget oldWidget) {
}
```
+#### Example
+
+This is a simple example shows loading data from the network with providing previous data while loading:
+
+```dart
+/// Widget Model for [ExampleScreen]
+class ExampleWidgetModel extends WidgetModel implements IExampleWidgetModel {
+ final _exampleState = EntityStateNotifier();
+
+ @override
+ ListenableState> get exampleState => _countryListState;
+
+ @override
+ void initWidgetModel() {
+ super.initWidgetModel();
+
+ _loadData();
+ }
+
+ Future _loadData() async {
+ final previousData = _exampleState.value?.data;
+ _exampleState.loading(previousData);
+
+ try {
+ final res = await model.loadData();
+ _exampleState.content(res);
+ } on Exception catch (e) {
+ _exampleState.error(e, previousData);
+ }
+ }
+}
+
+/// An interface for [ExampleWidgetModel]
+abstract interface class IExampleWidgetModel implements IWidgetModel {
+ ListenableState> get exampleState;
+}
+```
+
### 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.
+ElementaryModel is the only point of interaction with business logic for WidgetModel. It provides a contract of
+available business logic interaction in one entity. Based on this ElementaryModel is the only WidgetModel's dependency
+related to business logic. ElementaryModel can be implemented in a free style: as a bunch of simple methods,
+proxy that redirect to internal implementations, or combine with any other approaches.
### 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.
+For Elementary as well as for Flutter, Widget is a simple configuration firstly. It describes the starting params,
+provides the factory for Widget Model and is a delegate for describe UI part represented by this Widget. The main
+difference from other composition widgets is the simplified build process - since business logic and presentation
+logic are encapsulated in the Model and Widget Model, it is only left to the widget to follow the UI=f(s) principle and
+describe this UI based on the Widget Model contract. Therefore, the build method doesn't have context and accepts only
+Widget Model contract as an argument.
It looks like this:
@@ -149,169 +231,47 @@ Since the layers are well-separated from each other, they are easy to test with
* Use unit tests for Model layer;
* Use widget and golden tests for Widget layer;
-* Use widget model test from elementary_test library for WidgetModel.
-
-## Tooling
-
-To make Elementary easier to use, some tools have been added.
-
-### StateNotifier
-
-In order to establish a quicker response to any changes in properties of a state object, you can use a StateNotifier. It
-is a subclass of ChangeNotifier. StateNotifier's subscribers are notified whenever a state change occurs. Also, the
-subscriber will be called for the first time at the moment of subscription. The initialization of the StateNotifier does
-not require an initial value to be specified, for this reason the value returned by it may be null.
-
-```dart
-
-final _somePropertyWithIntegerValue = StateNotifier();
-
-void someFunctionChangeValue() {
- // do something, get new value
- // ...............................................................
- final newValue = 10;
- // and then change value of property
- _somePropertyWithIntegerValue.accept(10);
-}
-```
+* Use widget-model tests from elementary_test library for WidgetModel.
+* Use integration tests to check all together.
-### EntityStateNotifier
+## Utils
-Variant of StateNotifier that uses a special EntityState object as the state value. EntityState has three states:
-content, loading, error.
+To make Elementary easier to use, some helpers have been added. As it was mentioned previously, you can use any
+types of properties follows the Publisher-Subscriber pattern. Additionally, to available by default in Dart and Flutter
+Elementary contains a bunch of personal and builders for them.
+Check [elementary_helper](https://pub.dev/packages/elementary_helper) to find more.
-```dart
-
-final _countryListState = EntityStateNotifier>();
-
-Future _loadCountryList() async {
- final previousData = _countryListState.value?.data;
-
- // set property to loading state and use previous data for this state
- _countryListState.loading(previousData);
-
- try {
- // await the result
- final res = await model.loadCountries();
- // set property to content state, use new data
- _countryListState.content(res);
- } on Exception catch (e) {
- // set property to error state
- _countryListState.error(e, previousData);
- }
-}
-```
-
-### StateNotifierBuilder
-
-The StateNotifierBuilder is a widget that uses a StateNotifier as its data source. A builder function of the
-StateNotifierBuilder must return the widget based on the current value passed.
-
-```dart
-void somewhereInTheBuildFunction() {
- // ......
- StateNotifierBuilder(
- listenableState: someListenableState,
- builder: (ctx, value) {
- return Text(value);
- },
- );
- // ......
-}
-```
-
-### EntityStateNotifierBuilder
+## Maintainer
-The EntityStateNotifierBuilder is a widget that uses a EntityStateNotifier as its data source.
-Depending on the state, different builders will be called. It will be errorBuilder for error, loadingBuilder for error,
-builder for content.
+
+
+
-```dart
-@override
-Widget build(ICountryListWidgetModel wm) {
- return Scaffold(
- appBar: AppBar(
- title: const Text('Country List'),
- ),
- body: EntityStateNotifierBuilder>(
- listenableEntityState: wm.countryListState,
- loadingBuilder: (_, __) => const _LoadingWidget(),
- errorBuilder: (_, __, ___) => const _ErrorWidget(),
- builder: (_, countries) =>
- _CountryList(
- countries: countries,
- nameStyle: wm.countryNameStyle,
- ),
- ),
- );
-}
-```
+## Contributors thanks
-### DoubleSourceBuilder
-One of the multi-sources builders. It uses two ListenableStates as sources of data.
-The builder function will be called when any of the ListenableStates changes.
+Big thanks to all these people, who put their effort to help the project.
-```dart
-void somewhereInTheBuildFunction() {
- // ......
- DoubleSourceBuilder(
- firstSource: captionListenableState,
- secondSource: captionStyleListenableState,
- builder: (ctx, value, style) {
- return Text(value, style: style);
- },
- );
- // ......
-}
-```
+![contributors](https://contributors-img.firebaseapp.com/image?repo=Elementary-team/flutter-elementary)
+
-### TripleSourceBuilder
-One of the multi-sources builders. It uses three ListenableStates as sources of data.
-The builder function will be called when any of the ListenableStates changes.
+Special thanks to:
-```dart
-void somewhereInTheBuildFunction() {
- // ......
- TripleSourceBuilder(
- firstSource: captionListenableState,
- secondSource: valueListenableState,
- thirdSource: captionStyleListenableState,
- builder: (ctx, title, value, style) {
- return Text('$title: ${value ?? 0}', style: style);
- },
- );
- // ......
-}
-```
+[Dmitry Krutskikh](https://github.com/dkrutskikh), [Konoshenko Vlad](https://github.com/vlkonoshenko),
+[Denis Grafov](https://github.com/grafovdenis) for the early adoption and the first production feedback;
-### MultiListenerRebuilder
-Widget that rebuild part of the ui when one of Listenable changes. The builder function in this widget has no values,
-and you need to get the values directly in function's body.
+[Alex Bukin](https://github.com/AlexeyBukin) for IDE plugins;
-```dart
-void somewhereInTheBuildFunction() {
- // ......
- MultiListenerRebuilder(
- listenableList: [
- firstListenable,
- secondListenable,
- thirdListenable,
- ],
- builder: (ctx) {
- final title = firstListenable.value;
- final value = secondListenable.value;
- final style = thirdListenable.value;
- return Text('$title: ${value ?? 0}', style: style);
- },
- );
- // ......
-}
-```
+All members of the Surf Flutter Team for actively using and providing feedback.
-## Sponsor
+## Sponsorship
-Our main sponsor is [Surf](https://surf.ru/).
+Special sponsor of the project:
-## Maintainer
+
+
+
-[Mikhail Zotyev](https://github.com/MbIXjkee)
+For all questions regarding sponsorship/collaboration connect with [Mikhail Zotyev](https://github.com/MbIXjkee).
diff --git a/packages/elementary/documentation/quick_start.md b/packages/elementary/documentation/quick_start.md
new file mode 100644
index 00000000..c78a332b
--- /dev/null
+++ b/packages/elementary/documentation/quick_start.md
@@ -0,0 +1,82 @@
+# Document describes a simple guideline how to start with Elementary [WIP]
+
+### Choose tooling:
+
+Depends on your IDE and personal preferences choose and install one of those tools:
+
+- [plugin for IntelliJ](https://plugins.jetbrains.com/plugin/18099-elementary)
+- [plugin for VSCode](https://marketplace.visualstudio.com/items?itemName=ElementaryTeam.elementary)
+- [elementary brick](https://brickhub.dev/bricks/elementary)
+- [elementary_cli](https://pub.dev/packages/elementary_cli);
+
+Or can skip this step and create Widgets, Widget Models, Models manually further.
+
+### Depend on elementary:
+
+Run this command
+
+```flutter pub add elementary```
+
+or manually add dependency to your project pubspec.yaml
+
+```
+dependencies:
+ elementary: $actual_version
+```
+### Depend on elementary_helper:
+
+Run this command
+
+```flutter pub add elementary_helper```
+
+or manually add dependency to your project pubspec.yaml
+
+```
+dependencies:
+ elementary_helper: $actual_version
+```
+
+### Depend on elementary_test:
+
+Run this command
+
+```flutter pub add elementary_test```
+
+or manually add dependency to your project pubspec.yaml
+
+```
+dependencies:
+ elementary_test: $actual_version
+```
+
+### Create classes:
+
+Using a chosen tool create classes for ElementaryModel, WidgetModel, ElementaryWidget.
+
+TODO: describe manual creation.
+
+### ElementaryModel:
+
+Implement all business logic for current feature in model, free style.
+
+TODO: more details, examples.
+
+### WidgetModel:
+
+Implement all presentation logic for current feature in widget model.
+
+TODO: more details, examples.
+
+### ElementaryWidget:
+
+Based on the contract of Widget Model, describe UI.
+
+TODO: more details, examples.
+
+### Testing:
+
+Write unit tests for Model.
+Write tests for Widget Model using elementary_test.
+Write widget test, golden tests for ElementaryWidget.
+
+TODO: more details, examples.
\ No newline at end of file
diff --git a/packages/elementary/example/ios/Runner.xcodeproj/project.pbxproj b/packages/elementary/example/ios/Runner.xcodeproj/project.pbxproj
index d7afaaed..d3db3781 100644
--- a/packages/elementary/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/elementary/example/ios/Runner.xcodeproj/project.pbxproj
@@ -176,6 +176,7 @@
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
diff --git a/packages/elementary/example/lib/impl/screen/test_page_widget.dart b/packages/elementary/example/lib/impl/screen/test_page_widget.dart
index a4e31cdf..01b7fded 100644
--- a/packages/elementary/example/lib/impl/screen/test_page_widget.dart
+++ b/packages/elementary/example/lib/impl/screen/test_page_widget.dart
@@ -1,5 +1,6 @@
import 'package:counter/impl/screen/test_page_widget_model.dart';
import 'package:elementary/elementary.dart';
+import 'package:elementary_helper/elementary_helper.dart';
import 'package:flutter/material.dart';
/// Widget for demo.
diff --git a/packages/elementary/example/lib/impl/screen/test_page_widget_model.dart b/packages/elementary/example/lib/impl/screen/test_page_widget_model.dart
index 5bb3fbfe..37679734 100644
--- a/packages/elementary/example/lib/impl/screen/test_page_widget_model.dart
+++ b/packages/elementary/example/lib/impl/screen/test_page_widget_model.dart
@@ -4,6 +4,8 @@ import 'package:counter/impl/screen/test_page_model.dart';
import 'package:counter/impl/screen/test_page_widget.dart';
import 'package:counter/main.dart';
import 'package:elementary/elementary.dart';
+import 'package:elementary_helper/elementary_helper.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -17,7 +19,7 @@ TestPageWidgetModel testPageWidgetModelFactory(BuildContext context) {
class TestPageWidgetModel extends WidgetModel
implements ITestPageWidgetModel {
@override
- ListenableState> get valueState => _valueController;
+ ValueListenable> get valueState => _valueController;
late EntityStateNotifier _valueController;
@@ -47,8 +49,8 @@ class TestPageWidgetModel extends WidgetModel
}
}
-abstract class ITestPageWidgetModel extends IWidgetModel {
- ListenableState> get valueState;
+abstract interface class ITestPageWidgetModel implements IWidgetModel {
+ ValueListenable> get valueState;
Future increment();
}
diff --git a/packages/elementary/example/pubspec.yaml b/packages/elementary/example/pubspec.yaml
index 3586a8f7..3a21905b 100644
--- a/packages/elementary/example/pubspec.yaml
+++ b/packages/elementary/example/pubspec.yaml
@@ -4,18 +4,20 @@ version: 1.0.0+1
publish_to: none
environment:
- sdk: ">=2.12.0 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
dependencies:
elementary:
path: ../
+ elementary_helper:
+ path: ../../elementary_helper
flutter:
sdk: flutter
provider: ^6.0.2
dev_dependencies:
- mjk_lint_rules: ^1.0.1
+ mjk_lint_rules: ^2.0.0
flutter:
uses-material-design: true
diff --git a/packages/elementary/lib/elementary.dart b/packages/elementary/lib/elementary.dart
index 8322ef14..c5c5c229 100644
--- a/packages/elementary/lib/elementary.dart
+++ b/packages/elementary/lib/elementary.dart
@@ -3,12 +3,4 @@ library elementary;
export 'package:elementary/src/core.dart' hide Elementary;
export 'package:elementary/src/error/error_handler.dart';
export 'package:elementary/src/keep_alive/automatic_keep_alive_widget_model_mixin.dart';
-export 'package:elementary/src/relations/builder/multi_sources/double_source_builder.dart';
-export 'package:elementary/src/relations/builder/multi_sources/double_value_listenable_builder.dart';
-export 'package:elementary/src/relations/builder/multi_sources/multi_listener_rebuilder.dart';
-export 'package:elementary/src/relations/builder/multi_sources/triple_source_builder.dart';
-export 'package:elementary/src/relations/builder/multi_sources/triple_value_listenable_builder.dart';
-export 'package:elementary/src/relations/builder/state_notifier_builder.dart';
-export 'package:elementary/src/relations/notifier/state_notifier.dart';
export 'package:elementary/src/ticker/ticker_providers.dart';
-export 'package:elementary/src/wrappers/theme_wrapper.dart';
diff --git a/packages/elementary/lib/src/core.dart b/packages/elementary/lib/src/core.dart
index 61f54b10..36d3df54 100644
--- a/packages/elementary/lib/src/core.dart
+++ b/packages/elementary/lib/src/core.dart
@@ -2,63 +2,303 @@ import 'package:elementary/elementary.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-/// Factory function for creating Widget Model.
+/// Type for a factory function that is used to create
+/// an instance of [WidgetModel].
+///
+/// ## The part of Elementary Lifecycle
+/// The [WidgetModel] instance is created for the [ElementaryWidget] when it
+/// instantiate into the tree. Only in this moment the factory is called once(*)
+/// to get the instance of the [WidgetModel] and associate it with
+/// the [Elementary]. After this the [WidgetModel] will alive while
+/// the [Elementary] is alive. If [Elementary] updates the widget associated
+/// with it, the [WidgetModel] will not be recreated and continue work with
+/// the same state that has before the update. But the [WidgetModel] will be
+/// notified about the update by calling the method
+/// [WidgetModel.didUpdateWidget]. All needed state adjustments can be
+/// handle inside this method.
+///
+/// (*) Once per insert into the tree. As all other widgets, an instance of
+/// [ElementaryWidget] can be inserted many times. Every time the element and
+/// separate [WidgetModel] will be created for manage different state for every
+/// concrete inserts.
+///
+/// ## Examples
+/// {@tool snippet}
+///
+/// The following is a an example for the top-level factory function
+/// that creates dependencies right on the spot.
+///
+/// ```dart
+/// ExampleWidgetModel exampleWidgetModelFactory(BuildContext context) {
+/// final modelDependency = ModelDependency();
+/// final exampleModel = ExampleModel(modelDependency);
+/// return ExampleWidgetModel(exampleModel);
+/// }
+/// ```
+/// {@end-tool}
+///
+/// {@tool snippet}
+///
+/// The following is a an example for the top-level factory function
+/// that creates dependencies using one of the DI containers.
+///
+/// ```dart
+/// ExampleWidgetModel exampleWidgetModelFactory(BuildContext context) {
+/// final exampleModel = ContainerInstance.createExampleModel();
+/// return ExampleWidgetModel(exampleModel);
+/// }
+/// ```
+/// {@end-tool}
+///
+/// {@tool snippet}
+///
+/// The following is a an example for the top-level factory function
+/// that get dependencies using passed BuildContext.
+///
+/// ```dart
+/// ExampleWidgetModel exampleWidgetModelFactory(BuildContext context) {
+/// final modelDependency = ModelDependency();
+/// final exampleModel = ExampleModel(modelDependency);
+/// final widgetModelAnotherDependency = SomeWidget.of(context).getSomething;
+/// return ExampleWidgetModel(exampleModel, widgetModelAnotherDependency);
+/// }
+/// ```
+/// {@end-tool}
typedef WidgetModelFactory = T Function(
BuildContext context,
);
-/// Base interface for all Widget Model.
-abstract class IWidgetModel {}
-
-/// A widget that use WidgetModel for build.
+/// A basic interface for every [WidgetModel].
+///
+/// The general approach for the [WidgetModel] is implement interface that
+/// expanded from [IWidgetModel]. This expanded interface describes the contract
+/// which the inheritor of the [WidgetModel] implemented this interface
+/// is providing for the [ElementaryWidget]. The contract includes all
+/// properties and methods which the [ElementaryWidget] can use for build a
+/// part of the tree it describes.
+///
+/// {@tool snippet}
+///
+/// The following is a an example for the top-level factory function
+/// that get dependencies using passed BuildContext.
+///
+/// ```dart
+/// abstract interface class IExampleWidgetModel implements IWidgetModel {
+/// ListenableState get somePublisher;
+/// Stream get anotherPublisher;
+/// Color get justProperty;
+///
+/// Future doSomething();
+/// Future anotherOptionOfInteract();
+/// }
+/// ```
+/// {@end-tool}
+abstract interface class IWidgetModel {}
+
+/// A widget that uses state of [WidgetModel] properties to
+/// build a part of the user interface described by this widget.
+///
+/// For inheritors of this widget is common to be parameterized by
+/// the interface expanded from [IWidgetModel] that provides all special
+/// information which needed for this widget for correctly describe the subtree.
+///
+/// Only one thing should be implemented in inheritors of this widget is a
+/// build method. Build method in this case is pure function which gets the
+/// [WidgetModel] as argument and based on this [WidgetModel] returns
+/// the [Widget]. There isn't additional interactions with anything external,
+/// everything needed for describe should be provided by [WidgetModel].
+///
+/// {@tool snippet}
+///
+/// The following widget shows a default example of Flutter app, which created
+/// with a new Flutter project.
+///
+/// ```dart
+/// class ExampleWidget extends ElementaryWidget {
+/// const ExampleWidget({
+/// Key? key,
+/// WidgetModelFactory wmFactory = exampleWidgetModelFactory,
+/// }) : super(wmFactory, key: key);
+///
+/// @override
+/// Widget build(IExampleWidgetModel wm) {
+/// return Scaffold(
+/// appBar: AppBar(
+/// title: const Text('Test'),
+/// ),
+/// body: Center(
+/// child: Column(
+/// mainAxisAlignment: MainAxisAlignment.center,
+/// children: [
+/// const Text('You have pushed the button this many times:'),
+/// ValueListenableBuilder(
+/// valueListenable: wm.pressCountPublisher,
+/// builder: (_, value, __) {
+/// return Text(value.toString());
+/// },
+/// ),
+/// ],
+/// ),
+/// ),
+/// floatingActionButton: FloatingActionButton(
+/// onPressed: wm.increment,
+/// tooltip: 'Increment',
+/// child: const Icon(Icons.add),
+/// ),
+/// );
+/// }
+/// }
+/// ```
+/// {@end-tool}
///
-/// You must provide [wmFactory] factory function to the constructor
-/// to instantiate WidgetModel. For testing, you can replace
-/// this function for returning mock.
+/// ## wmFactory
+/// An instance of the [WidgetModelFactory] must be provided to the widget
+/// constructor as [wmFactory] property. This is required to create
+/// the [WidgetModel] instance for this widget.
+/// You can use additional factories for different purpose. For example create
+/// a special one that returns a mock [WidgetModel] for the tests.
+///
+/// ## The part of Elementary Lifecycle
+/// This widget is a starting and updating configuration for the [WidgetModel].
+/// More details about using it while life cycle see in the methods docs of the
+/// [WidgetModel].
+///
+/// ## Internal details
+/// This widget doesn't have its own [RenderObject], and just describes a part
+/// of the user interface using other widgets. It is a common approach for the
+/// widgets those inflate to [ComponentElement].
+///
+/// See also: [StatelessWidget], [StatefulWidget], [InheritedWidget].
abstract class ElementaryWidget extends Widget {
- /// Factory-function for creating WidgetModel
+ /// The factory function used to create a [WidgetModel].
final WidgetModelFactory wmFactory;
- /// Create an instance of ElementaryWidget.
+ /// Creates an instance of ElementaryWidget.
const ElementaryWidget(
this.wmFactory, {
Key? key,
}) : super(key: key);
- /// Creates a [Elementary] to manage this widget's location
- /// in the tree.
+ /// Creates a [Elementary] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
- Element createElement() {
+ Elementary createElement() {
return Elementary(this);
}
- /// Describes the part of the user interface represented by this widget.
+ /// Describes the part of the user interface represented by this widget,
+ /// based on the state of the [WidgetModel] passed to the [wm] argument.
///
- /// You can use all properties and methods provided by Widget Model.
- /// You should not use [BuildContext] or something else, all you need
- /// must contains in Widget Model.
+ /// There is no access to the [BuildContext] in this method,
+ /// because all needed in this method should be provided by
+ /// [WidgetModel] which has direct access to the [BuildContext].
+ /// It is uncommon to use [BuildContext] here, possibly in this case you need
+ /// to improve implementation of the [WidgetModel].
+ /// But [BuildContext] CAN be used in all builder functions and all widgets
+ /// used here.
Widget build(I wm);
}
-/// Entity that contains all presentation logic of the widget.
+/// The basic implementation of the entity responsible for all
+/// presentation logic, providing properties and data for the widget,
+/// and keep relations with the business logic. Business logic represented in
+/// the form of [ElementaryModel].
+///
+/// [WidgetModel] is a working horse of the Elementary library. It unites the
+/// trio of 'widget - widget model - model'. So the inheritors of [WidgetModel]
+/// parameterized by an inheritor of the ElementaryWidget and an inheritor
+/// of the ElementaryModel. This mean that this WidgetModel subclass encapsulate
+/// all required logic for the concrete ElementaryWidget subclass that
+/// mentioned as parameter and only for it. Also this WidgetModel subclass uses
+/// exactly the mentioned ElementaryModel subclass as an available contract of
+/// business logic.
+///
+/// It is common for inheritors to implement the expanded from [IWidgetModel]
+/// interface that describes special contract for the relevant
+/// [ElementaryWidget] subclass. Moreover using the contract is preferable way
+/// because this interface explicitly shows available properties and methods
+/// for declarative part (for ElementaryWidget).
+///
+/// ## Approach to update
+/// It is a rare case when [ElementaryWidget] completely rebuild. The most
+/// common case is a partial rebuild of UI parts. In order to get this, using
+/// publishers can be helpful. Declare any publishers which you prefer and
+/// update their values in suitable conditions. In the declarative part just
+/// use these publishers for describe parts of the UI, which depends on them.
+/// Here is a far from complete list of options for use as publishers:
+/// [Stream], [ChangeNotifier], StateNotifier, EntityStateNotifier.
+///
+/// ## The part of Elementary Lifecycle
+/// Base class contains all internal mechanisms and process that need to
+/// guarantee the conceived behavior for the Elementary library.
+///
+/// [initWidgetModel] is called only once for lifecycle
+/// of the [WidgetModel] in the really beginning before the first build.
+/// It can be used for initiate a starting state of the [WidgetModel].
+///
+/// [didUpdateWidget] called whenever widget instance in the tree has been
+/// updated. Common case where rebuild comes from the top. This method is a good
+/// place for update state of the [WidgetModel] based on the new configuration
+/// of widget. When this method is called is just a signal for decide what
+/// exactly should be updated or rebuilt. The fact of update doesn't mean that
+/// build method of the widget will be called. Set new values to publishers
+/// for rebuild concrete parts of the UI.
+///
+/// [didChangeDependencies] called whenever dependencies which [WidgetModel]
+/// subscribed with [BuildContext] change.
+/// When this method is called is just a signal for decide what
+/// exactly should be updated or rebuilt. The fact of the call doesn't mean that
+/// build method of the widget will be called. Set new values to publishers
+/// for rebuild concrete parts of the UI.
+///
+/// [deactivate] called when the [WidgetModel] with [Elementary] removed from
+/// the tree.
+///
+/// [activate] called when [WidgetModel] with [Elementary] are reinserted into
+/// the tree after having been removed via [deactivate].
+///
+/// [dispose] called when [WidgetModel] is going to be permanently destroyed.
+///
+/// [reassemble] called whenever the application is reassembled during
+/// debugging, for example during the hot reload.
+///
+/// [onErrorHandle] called when the [ElementaryModel] handle error with the
+/// [ElementaryModel.handleError] method. Can be useful for general handling
+/// errors such as showing snack-bars.
abstract class WidgetModel with Diagnosticable implements IWidgetModel {
final M _model;
- /// [ElementaryModel] for this WidgetModel.
- /// Only one of business-logic dependencies, that WidgetModel needs.
+ /// Instance of [ElementaryModel] for this [WidgetModel].
+ ///
+ /// The only business logic dependency that is needed for the [WidgetModel].
@protected
@visibleForTesting
M get model => _model;
- /// Widget that use WidgetModel for build.
+ /// Widget that uses this [WidgetModel] for building part of the user interface.
+ ///
+ /// The [WidgetModel] has an associated [ElementaryWidget].
+ /// This relation is managed by [Elementary], and in every time of lifecycle
+ /// in this property is an actual widget. Before the first update this field
+ /// contains a widget that created this [WidgetModel]. This instance can be
+ /// changed during the lifecycle, and [didUpdateWidget] will be called each
+ /// time it is changed.
@protected
@visibleForTesting
W get widget => _widget!;
- /// A handle to the location of a WidgetModel in the tree.
+ /// The location in the tree where this widget builds.
+ ///
+ /// The [WidgetModel] will be associated with the [BuildContext] by
+ /// [Elementary] after creating and before calling [initWidgetModel].
+ /// The association is permanent: the [WidgetModel] object will never
+ /// change its [BuildContext]. However, the [BuildContext] itself can be
+ /// moved around the tree.
+ ///
+ /// After calling [dispose] the association between the [WidgetModel] and
+ /// the [BuildContext] will be broken.
@protected
@visibleForTesting
BuildContext get context {
@@ -71,18 +311,21 @@ abstract class WidgetModel _element != null;
- Elementary? _element;
+ BuildContext? _element;
W? _widget;
- /// Create an instance of WidgetModel.
+ /// Creates an instance of the [WidgetModel].
WidgetModel(this._model);
- /// Called at first build for initialization of this Widget Model.
+ /// Called while the first build for initialization of this [WidgetModel].
+ ///
+ /// This method is called only ones for the instance of [WidgetModel]
+ /// during its lifecycle.
@protected
@mustCallSuper
@visibleForTesting
@@ -92,33 +335,30 @@ abstract class WidgetModel super.widget as ElementaryWidget;
@@ -293,16 +534,22 @@ class Elementary extends ComponentElement {
}
}
-/// Class that contains a business logic for Widget.
+/// The base class for an entity that contains a business logic required for
+/// a specific inheritor of [ElementaryWidget].
+///
+/// You can implement this class freestyle. It may be a bunch of methods,
+/// streams or something else. Also it is not a mandatory for business logic
+/// to be implemented right inside the class, this model can be proxy for other
+/// responsible entities with business logic.
///
-/// You can write this freestyle. It may be collection of methods,
-/// streams or something else.
+/// This class can take [ErrorHandler] as dependency for centralize handling
+/// error (for example logging). The [handleError] method can be used for it.
+/// When the [handleError] is called passed [ErrorHandler] handles exception.
+/// Also the [WidgetModel] is notified about this exception with
+/// [WidgetModel.onErrorHandle] method.
+///
+/// ## The part of Elementary Lifecycle
///
-/// This class can take [ErrorHandler] for handling caught error
-/// like a logging or something else. This realize by using
-/// [handleError] method. This method also notifies the Widget Model about the
-/// error that has occurred. You can use onErrorHandle method of Widget Model
-/// to handle on UI like show snackbar or something else.
abstract class ElementaryModel {
final ErrorHandler? _errorHandler;
void Function(Object)? _wmHandler;
@@ -310,8 +557,8 @@ abstract class ElementaryModel {
/// Create an instance of ElementaryModel.
ElementaryModel({ErrorHandler? errorHandler}) : _errorHandler = errorHandler;
- /// Should be used for report error Error Handler if it was set and notify
- /// Widget Model about error.
+ /// Can be used for send [error] to [ErrorHandler] if it defined and notify
+ /// [WidgetModel].
@protected
@mustCallSuper
@visibleForTesting
@@ -320,36 +567,41 @@ abstract class ElementaryModel {
_wmHandler?.call(error);
}
- /// Method for initialize this Model.
+ /// Initializes [ElementaryModel].
///
- /// Will be call at first build when Widget Model created.
+ /// Called once before the first build of the [ElementaryWidget].
void init() {}
- /// Called when Widget Model disposing.
+ /// Prepares the [ElementaryModel] to be completely destroyed.
+ ///
+ /// Called once when [Elementary] going to be destroyed. Should be used for
+ /// clearing links, subscriptions, and preparation to be garbage collected.
void dispose() {}
/// Method for setup ElementaryModel for testing.
/// This method can be used to WidgetModels error handler.
@visibleForTesting
// ignore: use_setters_to_change_properties
- void setupWmHandler(Function(Object)? function) {
+ void setupWmHandler(void Function(Object)? function) {
_wmHandler = function;
}
}
-/// Mock that helps to prevent [NoSuchMethodError] exception when we mock ElementaryModel
+/// Mock that helps to prevent [NoSuchMethodError] exception when the
+/// ElementaryModel is mocked.
@visibleForTesting
mixin MockElementaryModelMixin implements ElementaryModel {
@override
- set _wmHandler(Function(Object)? _) {}
+ set _wmHandler(void Function(Object)? _) {}
}
-/// Mock that helps to prevent [NoSuchMethodError] exception when we mock WidgetModel
+/// Mock that helps to prevent [NoSuchMethodError] exception when the
+/// WidgetModel is mocked.
@visibleForTesting
mixin MockWidgetModelMixin implements WidgetModel {
@override
- set _element(Elementary? _) {}
+ set _element(BuildContext? _) {}
@override
set _widget(W? _) {}
diff --git a/packages/elementary/lib/src/error/error_handler.dart b/packages/elementary/lib/src/error/error_handler.dart
index bd41189a..c6e2bc37 100644
--- a/packages/elementary/lib/src/error/error_handler.dart
+++ b/packages/elementary/lib/src/error/error_handler.dart
@@ -1,8 +1,23 @@
+import 'package:flutter/foundation.dart';
+
/// Interface for handle error in business logic.
/// It may be something like write log or else.
///
-/// !!! This not for Presentation Layer handling.
-abstract class ErrorHandler {
+/// !!! This is not meant to be used in the presentation layer for handling
+/// errors.
+abstract interface class ErrorHandler {
/// This method have to handle of passed error and optional [StackTrace].
void handleError(Object error, {StackTrace? stackTrace});
}
+
+/// The simplest error handler that just prints error to the console in
+/// debug mode.
+class DefaultDebugErrorHandler implements ErrorHandler {
+ @override
+ void handleError(Object error, {StackTrace? stackTrace}) {
+ debugPrint(error.toString());
+ if (stackTrace != null) {
+ debugPrintStack(stackTrace: stackTrace);
+ }
+ }
+}
diff --git a/packages/elementary/lib/src/relations/builder/state_notifier_builder.dart b/packages/elementary/lib/src/relations/builder/state_notifier_builder.dart
deleted file mode 100644
index c7bc66b5..00000000
--- a/packages/elementary/lib/src/relations/builder/state_notifier_builder.dart
+++ /dev/null
@@ -1,130 +0,0 @@
-import 'package:elementary/src/relations/notifier/state_notifier.dart';
-import 'package:flutter/material.dart';
-
-/// Builder for presentation ui part by [StateNotifier] property.
-class StateNotifierBuilder extends StatefulWidget {
- /// State that used to detect change and rebuild.
- final ListenableState listenableState;
-
- /// Function that used to describe the part of the user interface
- /// represented by this widget.
- final Widget Function(BuildContext context, T? value) builder;
-
- /// Create an instance of StateNotifierBuilder.
- const StateNotifierBuilder({
- Key? key,
- required this.listenableState,
- required this.builder,
- }) : super(key: key);
-
- @override
- StateNotifierBuilderState createState() => StateNotifierBuilderState();
-}
-
-/// State for the [StateNotifierBuilder].
-class StateNotifierBuilderState extends State> {
- /// Value defining the state.
- T? value;
-
- @override
- void initState() {
- super.initState();
- value = widget.listenableState.value;
- widget.listenableState.addListener(_valueChanged);
- }
-
- @override
- void didUpdateWidget(StateNotifierBuilder oldWidget) {
- if (oldWidget.listenableState != widget.listenableState) {
- oldWidget.listenableState.removeListener(_valueChanged);
- value = widget.listenableState.value;
- widget.listenableState.addListener(_valueChanged);
- }
- super.didUpdateWidget(oldWidget);
- }
-
- @override
- void dispose() {
- widget.listenableState.removeListener(_valueChanged);
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return widget.builder(context, value);
- }
-
- void _valueChanged() {
- setState(() {
- value = widget.listenableState.value;
- });
- }
-}
-
-/// Builder for presentation ui part by [EntityStateNotifier] property.
-class EntityStateNotifierBuilder extends StatelessWidget {
- /// State that used to detect change and rebuild.
- final ListenableState> listenableEntityState;
-
- /// Builder that used to describe user interface when get data.
- final DataWidgetBuilder builder;
-
- /// Builder that used to describe user interface when loading.
- final LoadingWidgetBuilder? loadingBuilder;
-
- /// Builder that used to describe user interface when get error.
- final ErrorWidgetBuilder? errorBuilder;
-
- /// Create an instance of EntityStateNotifierBuilder.
- const EntityStateNotifierBuilder({
- Key? key,
- required this.listenableEntityState,
- required this.builder,
- this.loadingBuilder,
- this.errorBuilder,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return StateNotifierBuilder>(
- listenableState: listenableEntityState,
- builder: (ctx, state) {
- final entity = state!;
-
- final eBuilder = errorBuilder;
- if (entity.hasError && eBuilder != null) {
- return eBuilder(ctx, entity.error, entity.data);
- }
-
- final lBuilder = loadingBuilder;
- if (entity.isLoading && lBuilder != null) {
- return lBuilder(ctx, entity.data);
- }
-
- return builder(ctx, entity.data);
- },
- );
- }
-}
-
-/// Builder function for loading state.
-/// See also:
-/// [EntityState] - State of some logical entity.
-typedef LoadingWidgetBuilder = Widget Function(
- BuildContext context,
- T? data,
-);
-
-/// Builder function for content state.
-/// See also:
-/// [EntityState] - State of some logical entity.
-typedef DataWidgetBuilder = Widget Function(BuildContext context, T? data);
-
-/// Builder function for error state.
-/// See also:
-/// [EntityState] - State of some logical entity.
-typedef ErrorWidgetBuilder = Widget Function(
- BuildContext context,
- Exception? e,
- T? data,
-);
diff --git a/packages/elementary/lib/src/relations/notifier/state_notifier.dart b/packages/elementary/lib/src/relations/notifier/state_notifier.dart
deleted file mode 100644
index 4b027b42..00000000
--- a/packages/elementary/lib/src/relations/notifier/state_notifier.dart
+++ /dev/null
@@ -1,102 +0,0 @@
-import 'package:flutter/material.dart';
-
-/// Source of some listenable property.
-///
-/// You can set initial value by pass initValue to constructor.
-class StateNotifier extends ChangeNotifier implements ListenableState {
- @override
- T? get value => _value;
-
- T? _value;
-
- /// Create an instance of StateNotifier.
- StateNotifier({T? initValue}) : _value = initValue;
-
- @override
- void addListener(VoidCallback listener) {
- super.addListener(listener);
-
- listener.call();
- }
-
- /// Accept new value.
- void accept(T? newValue) {
- if (_value == newValue) return;
-
- _value = newValue;
- notifyListeners();
- }
-}
-
-/// An interface that can be listened and return current value.
-abstract class ListenableState extends Listenable {
- /// Return current state
- T? get value;
-}
-
-/// Change notifier with value that presented by [EntityState].
-///
-/// Empty initial value create empty EntityState for initial value.
-class EntityStateNotifier extends StateNotifier> {
- /// Create an instance of EntityStateNotifier.
- EntityStateNotifier([EntityState? initialData])
- : super(initValue: initialData ?? EntityState());
-
- /// Constructor for easy set initial value.
- EntityStateNotifier.value(T initialData)
- : super(
- initValue: EntityState(data: initialData),
- );
-
- /// Accept state with content.
- void content(T data) => super.accept(EntityState.content(data));
-
- /// Accept state with error.
- void error([Exception? exception, T? data]) =>
- super.accept(EntityState.error(exception, data));
-
- /// Accept loading state.
- void loading([T? previousData]) =>
- super.accept(EntityState.loading(previousData));
-}
-
-/// State of some logical entity.
-class EntityState {
- /// Data of entity.
- final T? data;
-
- /// State is loading.
- final bool isLoading;
-
- /// State has error.
- final bool hasError;
-
- /// Exception from state.
- final Exception? error;
-
- /// Create an instance of EntityState.
- const EntityState({
- this.data,
- this.isLoading = false,
- this.hasError = false,
- this.error,
- }) : assert(error == null || hasError),
- assert(!hasError && !isLoading || hasError != isLoading);
-
- /// Loading constructor
- const EntityState.loading([this.data])
- : isLoading = true,
- hasError = false,
- error = null;
-
- /// Error constructor
- const EntityState.error([this.error, this.data])
- : isLoading = false,
- hasError = true;
-
- /// Content constructor
- const EntityState.content(this.data)
- : isLoading = false,
- hasError = false,
- error = null;
-}
diff --git a/packages/elementary/pubspec.yaml b/packages/elementary/pubspec.yaml
index 79bf2f9b..e40d087a 100644
--- a/packages/elementary/pubspec.yaml
+++ b/packages/elementary/pubspec.yaml
@@ -1,13 +1,13 @@
name: elementary
description: This is architecture library with the main goal to split code between different responsibility layers, make code clear, simple, readable and easy testable.
-version: 2.0.0
+version: 3.0.0
repository: "https://github.com/Elementary-team/flutter-elementary/tree/main/packages/elementary"
issue_tracker: "https://github.com/Elementary-team/flutter-elementary/issues"
homepage: "https://github.com/Elementary-team/flutter-elementary"
environment:
- sdk: ">=2.12.0 <3.0.0"
- flutter: ">=1.17.0"
+ sdk: ">=3.0.0 <4.0.0"
+ flutter: ">=3.10.0"
dependencies:
flutter:
@@ -18,8 +18,8 @@ dev_dependencies:
flutter_test:
sdk: flutter
- mocktail: ^0.3.0
- mjk_lint_rules: ^1.0.1
+ mocktail: ^1.0.0
+ mjk_lint_rules: ^2.0.0
flutter:
uses-material-design: true
\ No newline at end of file
diff --git a/packages/elementary/test/automatic_keep_alive_widget_model_mixin/automatic_keep_alive_widget_model_mixin_test.dart b/packages/elementary/test/automatic_keep_alive_widget_model_mixin/automatic_keep_alive_widget_model_mixin_test.dart
index 875f787a..af6dab53 100644
--- a/packages/elementary/test/automatic_keep_alive_widget_model_mixin/automatic_keep_alive_widget_model_mixin_test.dart
+++ b/packages/elementary/test/automatic_keep_alive_widget_model_mixin/automatic_keep_alive_widget_model_mixin_test.dart
@@ -257,7 +257,7 @@ void main() {
);
}
-class IElementaryWidgetModelTest extends IWidgetModel {}
+class IElementaryWidgetModelTest implements IWidgetModel {}
class TestWidgetModel
extends WidgetModel
diff --git a/packages/elementary/test/core/elementary_test.dart b/packages/elementary/test/core/elementary_test.dart
index 10171f53..677a2ffb 100644
--- a/packages/elementary/test/core/elementary_test.dart
+++ b/packages/elementary/test/core/elementary_test.dart
@@ -163,8 +163,8 @@ void main() {
widget,
],
),
- Row(
- key: const ValueKey('secondRow'),
+ const Row(
+ key: ValueKey('secondRow'),
),
],
),
@@ -174,8 +174,8 @@ void main() {
MaterialApp(
home: Column(
children: [
- Row(
- key: const ValueKey('firstRow'),
+ const Row(
+ key: ValueKey('firstRow'),
),
Row(
key: const ValueKey('secondRow'),
@@ -303,7 +303,7 @@ class ElementaryWidgetTest
}
}
-class IElementaryWidgetModelMock extends IWidgetModel {}
+class IElementaryWidgetModelMock implements IWidgetModel {}
class ElementaryWidgetModelMock extends DiagnosticableMock
with MockWidgetModelMixin
diff --git a/packages/elementary/test/core/widget_model_test.dart b/packages/elementary/test/core/widget_model_test.dart
index c8e8deba..3225ca09 100644
--- a/packages/elementary/test/core/widget_model_test.dart
+++ b/packages/elementary/test/core/widget_model_test.dart
@@ -199,7 +199,7 @@ class ElementaryWidgetTest
}
}
-class IElementaryWidgetModelTest extends IWidgetModel {}
+class IElementaryWidgetModelTest implements IWidgetModel {}
class ElementaryWidgetModelTest
extends WidgetModel
diff --git a/packages/elementary/test/error/error_handler_test.dart b/packages/elementary/test/error/error_handler_test.dart
new file mode 100644
index 00000000..7b7d94a4
--- /dev/null
+++ b/packages/elementary/test/error/error_handler_test.dart
@@ -0,0 +1,37 @@
+import 'package:elementary/elementary.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+
+/// Tests for [DefaultDebugErrorHandler].
+void main() {
+ late DefaultDebugErrorHandler debugErrorHandler;
+ late MockDebugPrintCallback callback;
+
+ setUp(() {
+ debugErrorHandler = DefaultDebugErrorHandler();
+ callback = MockDebugPrintCallback();
+ debugPrint = callback.call;
+ });
+
+ test('Call handing error should not throw exception', () {
+ expect(
+ () => debugErrorHandler.handleError(
+ Exception('test'),
+ stackTrace: StackTrace.empty,
+ ),
+ returnsNormally,
+ );
+ });
+
+ test('Call handing error should print debug', () {
+ final exception = Exception('test');
+ debugErrorHandler.handleError(exception);
+
+ verify(() => callback.call(exception.toString())).called(1);
+ });
+}
+
+class MockDebugPrintCallback extends Mock {
+ void call(String? message, {int? wrapWidth});
+}
diff --git a/packages/elementary/test/relations/notifier/entity_state_test.dart b/packages/elementary/test/relations/notifier/entity_state_test.dart
deleted file mode 100644
index 2527f277..00000000
--- a/packages/elementary/test/relations/notifier/entity_state_test.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-import 'package:elementary/elementary.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-/// Tests for [EntityState].
-void main() {
- test('Data should be equal to init data', () {
- const data = 1;
- const state = EntityState(data: data);
- expect(state.data, equals(data));
- });
-
- test('Error should be equal to init error', () {
- final error = Exception('test');
- final state = EntityState(error: error, hasError: true);
- expect(state.error, equals(error));
- });
-
- test('Has error should be equal to init has error', () {
- const state = EntityState(hasError: true);
- expect(state.hasError, isTrue);
- });
-
- test('Loading should be equal to init loading', () {
- const state = EntityState(isLoading: true);
- expect(state.isLoading, isTrue);
- });
-
- test('Data should be equal to init data', () {
- const state = EntityState(data: 1);
- expect(state.data, 1);
- });
-
- test('Data should be null if not init', () {
- const state = EntityState();
- expect(state.data, isNull);
- });
-
- test('Error should be null if not init', () {
- const state = EntityState();
- expect(state.error, isNull);
- });
-
- test('Loading should be false if not init', () {
- const state = EntityState();
- expect(state.isLoading, isFalse);
- });
-
- test('Has error should be false if not init', () {
- const state = EntityState();
- expect(state.hasError, isFalse);
- });
-
- test('Error without hasError should throw assert exception', () {
- final error = Exception('test');
- expect(() => EntityState(error: error), throwsAssertionError);
- });
-
- test('Loading and Error in same time should throw assert exception', () {
- expect(
- () => EntityState(hasError: true, isLoading: true),
- throwsAssertionError,
- );
- });
-
- test('Loading constructor should create correct entity', () {
- const state = EntityState.loading();
- expect(state.isLoading, isTrue);
- expect(state.hasError, isFalse);
- expect(state.error, isNull);
- });
-
- test('Error constructor should create correct entity', () {
- const state = EntityState.error();
- expect(state.isLoading, isFalse);
- expect(state.hasError, isTrue);
- });
-
- test('Content constructor should create correct entity', () {
- const state = EntityState.content(1);
- expect(state.isLoading, isFalse);
- expect(state.hasError, isFalse);
- expect(state.data, 1);
- });
-}
diff --git a/packages/elementary/test/ticker_providers/single_ticker_provider_widget_model_mixin_test.dart b/packages/elementary/test/ticker_providers/single_ticker_provider_widget_model_mixin_test.dart
index 8be1be10..db3055f1 100644
--- a/packages/elementary/test/ticker_providers/single_ticker_provider_widget_model_mixin_test.dart
+++ b/packages/elementary/test/ticker_providers/single_ticker_provider_widget_model_mixin_test.dart
@@ -129,7 +129,7 @@ void main() {
);
}
-class IElementaryWidgetModelTest extends IWidgetModel {}
+class IElementaryWidgetModelTest implements IWidgetModel {}
class TestWidgetModel
extends WidgetModel
diff --git a/packages/elementary/test/ticker_providers/ticker_provider_widget_model_mixin_test.dart b/packages/elementary/test/ticker_providers/ticker_provider_widget_model_mixin_test.dart
index 004a7f12..1cadf484 100644
--- a/packages/elementary/test/ticker_providers/ticker_provider_widget_model_mixin_test.dart
+++ b/packages/elementary/test/ticker_providers/ticker_provider_widget_model_mixin_test.dart
@@ -96,7 +96,7 @@ void main() {
);
}
-class IElementaryWidgetModelTest extends IWidgetModel {}
+class IElementaryWidgetModelTest implements IWidgetModel {}
class TestWidgetModel
extends WidgetModel
diff --git a/packages/elementary_helper/.gitignore b/packages/elementary_helper/.gitignore
new file mode 100644
index 00000000..761d64cb
--- /dev/null
+++ b/packages/elementary_helper/.gitignore
@@ -0,0 +1,78 @@
+# Miscellaneous
+*.class
+*.lock
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Visual Studio Code related
+.classpath
+.project
+.settings/
+.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Coverage
+coverage/
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/packages/elementary_helper/CHANGELOG.md b/packages/elementary_helper/CHANGELOG.md
new file mode 100644
index 00000000..010d54cd
--- /dev/null
+++ b/packages/elementary_helper/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+## Unreleased
+
+## 1.0.1
+### Changed
+* documentation improvement;
+
+## 1.0.0
+### Added
+* implementations have been moved from the main package;
\ No newline at end of file
diff --git a/packages/elementary_helper/LICENSE b/packages/elementary_helper/LICENSE
new file mode 100644
index 00000000..65004d32
--- /dev/null
+++ b/packages/elementary_helper/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Mikhail Zotyev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/elementary_helper/README.md b/packages/elementary_helper/README.md
new file mode 100644
index 00000000..769dc526
--- /dev/null
+++ b/packages/elementary_helper/README.md
@@ -0,0 +1,245 @@
+# Elementary-helper
+###
+
+
+
+
+###
+
+
+
+
+
+
+
+
+
+
+
+
+## Description
+
+To make Elementary easier to use, some helpers have been added. Among them are custom implementations of
+the Pub-Sub pattern and wrappers to make easier testable interaction with Flutter from Widget Model
+
+### StateNotifier
+
+Behaviour is similar to ValueNotifier, but with no requirement to set initial value.
+Due to this the returned value is Nullable. StateNotifier's subscribers are notified whenever a state change occurs.
+Also, the subscriber is called first time at the moment of subscription. For emit new value there is a method accept.
+
+```dart
+final _somePropertyWithIntegerValue = StateNotifier();
+
+void someFunctionChangeValue() {
+ // do something, get new value
+ // ...............................................................
+ final newValue = 10;
+ // and then change value of property
+ _somePropertyWithIntegerValue.accept(10);
+}
+```
+
+### EntityStateNotifier
+
+Variant of ValueNotifier that uses a special EntityState object as the state value. EntityState has three states:
+content, loading, error. All states can contain data, for cases when you want to keep previous values, e.g. pagination.
+
+```dart
+final _countryListState = EntityStateNotifier>();
+
+Future _loadCountryList() async {
+ final previousData = _countryListState.value?.data;
+
+ // set property to loading state and use previous data for this state
+ _countryListState.loading(previousData);
+
+ try {
+ // await the result
+ final res = await model.loadCountries();
+ // set property to content state, use new data
+ _countryListState.content(res);
+ } on Exception catch (e) {
+ // set property to error state
+ _countryListState.error(e, previousData);
+ }
+}
+```
+
+### StateNotifierBuilder
+
+The StateNotifierBuilder is a widget that uses a StateNotifier as its data source. A builder function of the
+StateNotifierBuilder must return the widget based on the current value passed.
+
+```dart
+void somewhereInTheBuildFunction() {
+ // ......
+ StateNotifierBuilder(
+ listenableState: someListenableState,
+ builder: (ctx, value) {
+ return Text(value);
+ },
+ );
+ // ......
+}
+```
+
+### EntityStateNotifierBuilder
+
+The EntityStateNotifierBuilder is a widget that uses a EntityStateNotifier as its data source.
+Depending on the state, different builders are called: errorBuilder for error, loadingBuilder for error,
+builder for content.
+
+```dart
+@override
+Widget build(ICountryListWidgetModel wm) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Country List'),
+ ),
+ body: EntityStateNotifierBuilder>(
+ listenableEntityState: wm.countryListState,
+ loadingBuilder: (_, __) => const _LoadingWidget(),
+ errorBuilder: (_, __, ___) => const _ErrorWidget(),
+ builder: (_, countries) =>
+ _CountryList(
+ countries: countries,
+ nameStyle: wm.countryNameStyle,
+ ),
+ ),
+ );
+}
+```
+
+### DoubleSourceBuilder
+One of the multi-sources builders. It uses two ListenableStates as sources of data.
+The builder function is called when any of sources changes.
+
+```dart
+void somewhereInTheBuildFunction() {
+ // ......
+ DoubleSourceBuilder(
+ firstSource: captionListenableState,
+ secondSource: captionStyleListenableState,
+ builder: (ctx, value, style) {
+ return Text(value, style: style);
+ },
+ );
+ // ......
+}
+```
+
+### DoubleValueListenableBuilder
+One of the multi-sources builders. It uses two ValueListenable as sources of data.
+The builder function is called when any of sources changes.
+
+```dart
+void somewhereInTheBuildFunction() {
+ // ......
+ DoubleValueListenableBuilder(
+ firstValue: captionListenableState,
+ secondValue: captionStyleListenableState,
+ builder: (ctx, value, style) {
+ return Text(value, style: style);
+ },
+ );
+ // ......
+}
+```
+
+### TripleSourceBuilder
+One of the multi-sources builders. It uses three ListenableStates as sources of data.
+The builder function is called when any of sources changes.
+
+```dart
+void somewhereInTheBuildFunction() {
+ // ......
+ TripleSourceBuilder(
+ firstSource: captionListenableState,
+ secondSource: valueListenableState,
+ thirdSource: captionStyleListenableState,
+ builder: (ctx, title, value, style) {
+ return Text('$title: ${value ?? 0}', style: style);
+ },
+ );
+ // ......
+}
+```
+
+### TripleValueListenableBuilder
+One of the multi-sources builders. It uses three ValueListenable as sources of data.
+The builder function is called when any of sources changes.
+
+```dart
+void somewhereInTheBuildFunction() {
+ // ......
+ TripleSourceBuilder(
+ firstSource: captionListenableState,
+ secondSource: valueListenableState,
+ thirdSource: captionStyleListenableState,
+ builder: (ctx, title, value, style) {
+ return Text('$title: ${value ?? 0}', style: style);
+ },
+ );
+ // ......
+}
+```
+
+### MultiListenerRebuilder
+Widget that rebuild part of the ui when one of Listenable changes. The builder function in this widget has no values,
+and you need to get the values directly in function's body.
+
+```dart
+void somewhereInTheBuildFunction() {
+ // ......
+ MultiListenerRebuilder(
+ listenableList: [
+ firstListenable,
+ secondListenable,
+ thirdListenable,
+ ],
+ builder: (ctx) {
+ final title = firstListenable.value;
+ final value = secondListenable.value;
+ final style = thirdListenable.value;
+ return Text('$title: ${value ?? 0}', style: style);
+ },
+ );
+ // ......
+}
+```
+
+## Maintainer
+
+
+