Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update states_rebuilder sample to use the latest version and to use immutable state #182

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 85 additions & 34 deletions states_rebuilder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,37 +46,88 @@ Contains service application use cases business logic. It defines a set of API t

1. With states_rebuilder you can achieve a clear separation between UI and business logic;
2. Your business logic is made up of pure dart classes without the need to refer to external packages or frameworks (NO extension, NO notification, NO annotation);
```dart
class Foo {
//Vanilla dart class
//NO inheritance form external libraries
//NO notification
//No annotation
}
```
3. You make a singleton of your logical class available to the widget tree by injecting it using the Injector widget.
```dart
Injector(
inject : [Inject(()=>Foo())]
builder : (context) => MyChildWidget()
)
```
Injector is a StatefulWidget. It can be used any where in the widget tree.
4. From any child widget of the Injector widget, you can get the registered raw singleton using the static method `Injector.get<T>()` method;
```dart
final Foo foo = Injector.get<Foo>();
```
5. To get the registered singleton wrapped with a reactive environment, you use the static method
`Injector.getAsReactive<T>()` method:
```dart
final ReactiveModel<Foo> foo = Injector.getAsReactive<Foo>();
```
In fact, for each injected model, states_rebuilder registers two singletons:
- The raw singleton of the model
- The reactive singleton of the model which is the raw singleton wrapped with a reactive environment:
The reactive environment adds getters, fields, and methods to modify the state, track the state of the reactive environment and notify the widgets which are subscribed to it.
6. To subscribe a widget as observer, we use `StateBuilder` widget or define the context parameter in `Injector.getAsReactive<Foo>(context:context)`.
7. The `setState` method is where actions that mutate the state and send notifications are defined.
What happens is that from the user interface, we use the `setState` method to mutate the state and notify subscribed widgets after the state mutation. In the `setState`, we can define a callback for all the side effects to be executed after the state change and just before rebuilding subscribed widgets using `onSetState`, `onData` and `onError` parameter(or `onRebuild` so that the code executes after the reconstruction). From inside `onSetState`, we can call another `setState` to mutate the state and notify the user interface with another call `onSetState` (`onRebuild`) and so on …

For more information and tutorials on how states_rebuilder work please check out the [official documentation](https://github.com/GIfatahTH/states_rebuilder).
3. With states_rebuilder you can manage immutable as well as mutable state.

In this demo implementation, I choose to use immutable state, you can find the same app implemented with mutable state in the example folder of the [official repo in github](https://github.com/GIfatahTH/states_rebuilder).

In this implementation, I add the requirement that when a todo is added, deleted or updated, it will be instantly displayed in the user interface, so that the user will not notice any delay, and the async method `saveTodo` will be called in the background to persist the change. If the `saveTodo` method fails, the old state is returned and displayed back with a` SnackBar` containing the error message.

The idea is simple:
1- Write your immutable `TodosState` class using pure dart without any use of external libraries.
```dart
@immutable
class TodosState {
TodosState({
ITodosRepository todoRepository,
List<Todo> todos,
VisibilityFilter activeFilter,
}) : _todoRepository = todoRepository,
_todos = todos,
_activeFilter = activeFilter;

//....
}
```
2- Inject the `TodosState` using the `Injector` widget,
```dart
return Injector(
inject: [
Inject(
() => TodosState(
todos: [],
activeFilter: VisibilityFilter.all,
todoRepository: repository,
),
)
],
```
3- use one of the available observer widgets offered by states_rebuilder to subscribed to the `TodosState` `ReactiveModel`.
```dart
return StateBuilder<TodosState>(
observe: () => RM.get<TodosState>(),
builder: (context, todosStoreRM) {
//...

}
)
```
4- to notify the observing widgets use:
* for sync method: use `setValue` method or the `value` setter.
```dart
onSelected: (filter) {

activeFilterRM.setValue(
() => filter,
onData: (context, data) {
RM.get<TodosState>().value =
RM.get<TodosState>().value.copyWith(activeFilter: filter);
},
);
```
* for async future method: use future method.
```dart
body: WhenRebuilderOr<AppTab>(
observeMany: [
() => RM.get<TodosState>().asNew(HomeScreen)
..future((t) => t.loadTodos())
.onError(ErrorHandler.showErrorDialog),
() => _activeTabRMKey,
]

//...
)
```
* for async stream method: use stream method.
```dart
onSelected: (action) {

RM.get<TodosState>()
.stream(
(action == ExtraAction.toggleAllComplete)
? (t) => t.toggleAll()
: (t) => t.clearCompleted(),
)
.onError(ErrorHandler.showErrorSnackBar);
}
```

1 change: 1 addition & 0 deletions states_rebuilder/android/settings_aar.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ':app'
25 changes: 20 additions & 5 deletions states_rebuilder/lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'data_source/todo_repository.dart';

import 'localization.dart';
import 'service/todos_service.dart';
import 'service/common/enums.dart';
import 'service/interfaces/i_todo_repository.dart';
import 'service/todos_state.dart';
import 'ui/pages/add_edit_screen.dart/add_edit_screen.dart';
import 'ui/pages/home_screen/home_screen.dart';

class StatesRebuilderApp extends StatelessWidget {
final StatesBuilderTodosRepository repository;
final ITodosRepository repository;

const StatesRebuilderApp({Key key, this.repository}) : super(key: key);

@override
Widget build(BuildContext context) {
//Injecting the TodoService globally before MaterialApp widget.
////uncomment this line to consol log and see the notification timeline
//RM.printActiveRM = true;

//
//Injecting the TodosState globally before MaterialApp widget.
//It will be available throughout all the widget tree even after navigation.
//The initial state is an empty todos and VisibilityFilter.all
return Injector(
inject: [Inject(() => TodosService(repository))],
inject: [
Inject(
() => TodosState(
todos: [],
activeFilter: VisibilityFilter.all,
todoRepository: repository,
),
)
],
builder: (_) => MaterialApp(
title: StatesRebuilderLocalizations().appTitle,
theme: ArchSampleTheme.theme,
Expand Down
12 changes: 7 additions & 5 deletions states_rebuilder/lib/data_source/todo_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import '../domain/entities/todo.dart';
import '../service/exceptions/persistance_exception.dart';
import '../service/interfaces/i_todo_repository.dart';

class StatesBuilderTodosRepository implements ITodosRepository {
class StatesRebuilderTodosRepository implements ITodosRepository {
final core.TodosRepository _todosRepository;

StatesBuilderTodosRepository({core.TodosRepository todosRepository})
StatesRebuilderTodosRepository({core.TodosRepository todosRepository})
: _todosRepository = todosRepository;

@override
Expand All @@ -27,16 +27,18 @@ class StatesBuilderTodosRepository implements ITodosRepository {
}

@override
Future saveTodos(List<Todo> todos) {
Future saveTodos(List<Todo> todos) async {
try {
var todosEntities = <TodoEntity>[];
//// to simulate en error uncomment these lines.
// await Future.delayed(Duration(milliseconds: 500));
// throw Exception();
for (var todo in todos) {
todosEntities.add(TodoEntity.fromJson(todo.toJson()));
}

return _todosRepository.saveTodos(todosEntities);
} catch (e) {
throw PersistanceException('There is a problem in saving todos : $e');
throw PersistanceException('There is a problem in saving todos');
}
}
}
59 changes: 44 additions & 15 deletions states_rebuilder/lib/domain/entities/todo.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import 'package:flutter/cupertino.dart';
import 'package:todos_app_core/todos_app_core.dart' as flutter_arch_sample_app;

import '../exceptions/validation_exception.dart';

//Entity is a mutable object with an ID. It should contain all the logic It controls.
//Entity is validated just before persistance, ie, in toMap() method.
@immutable
class Todo {
String id;
bool complete;
String note;
String task;
final String id;
final bool complete;
final String note;
final String task;

Todo(this.task, {String id, this.note, this.complete = false})
: id = id ?? flutter_arch_sample_app.Uuid().generateV4();

Todo.fromJson(Map<String, Object> map) {
id = map['id'] as String;
task = map['task'] as String;
note = map['note'] as String;
complete = map['complete'] as bool;
factory Todo.fromJson(Map<String, Object> map) {
return Todo(
map['task'] as String,
id: map['id'] as String,
note: map['note'] as String,
complete: map['complete'] as bool,
);
}

// toJson is called just before persistance.
Expand All @@ -41,11 +43,38 @@ class Todo {
}
}

Todo copyWith({
String task,
String note,
bool complete,
String id,
}) {
return Todo(
task ?? this.task,
id: id ?? this.id,
note: note ?? this.note,
complete: complete ?? this.complete,
);
}

@override
bool operator ==(Object o) {
if (identical(this, o)) return true;

return o is Todo &&
o.id == id &&
o.complete == complete &&
o.note == note &&
o.task == task;
}

@override
int get hashCode => id.hashCode;
int get hashCode {
return id.hashCode ^ complete.hashCode ^ note.hashCode ^ task.hashCode;
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Todo && runtimeType == other.runtimeType && id == other.id;
String toString() {
return 'Todo(id: $id,task:$task, complete: $complete)';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ class ValidationException extends Error {
final String message;

ValidationException(this.message);
@override
String toString() {
return message;
}
}
3 changes: 1 addition & 2 deletions states_rebuilder/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import 'data_source/todo_repository.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

runApp(
StatesRebuilderApp(
repository: StatesBuilderTodosRepository(
repository: StatesRebuilderTodosRepository(
todosRepository: LocalStorageRepository(
localStorage: KeyValueStorage(
'states_rebuilder',
Expand Down
2 changes: 1 addition & 1 deletion states_rebuilder/lib/main_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
StatesRebuilderApp(
repository: StatesBuilderTodosRepository(
repository: StatesRebuilderTodosRepository(
todosRepository: LocalStorageRepository(
localStorage: KeyValueStorage(
'states_rebuilder',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
class PersistanceException extends Error {
final String message;

PersistanceException(this.message);
@override
String toString() {
return message.toString();
}
}
73 changes: 0 additions & 73 deletions states_rebuilder/lib/service/todos_service.dart

This file was deleted.

Loading