From a381dd9602e2ab199f487ea8302757d82095b0f0 Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Tue, 21 Apr 2020 20:22:34 +0100 Subject: [PATCH 01/10] refactor to use states rebuilder v1.15.0 --- states_rebuilder/lib/app.dart | 7 +- .../lib/data_source/todo_repository.dart | 11 +- .../lib/domain/entities/todo.dart | 47 +++- states_rebuilder/lib/main.dart | 3 +- states_rebuilder/lib/main_web.dart | 2 +- .../exceptions/persistance_exception.dart | 5 +- .../lib/service/filtered_todos.dart | 22 ++ .../lib/service/stats_service.dart | 10 + .../lib/service/todos_service.dart | 55 ++-- .../lib/ui/common/helper_methods.dart | 10 +- .../lib/ui/exceptions/error_handler.dart | 19 ++ .../add_edit_screen.dart/add_edit_screen.dart | 26 +- .../ui/pages/detail_screen/detail_screen.dart | 47 ++-- .../home_screen/extra_actions_button.dart | 18 +- .../ui/pages/home_screen/filter_button.dart | 24 +- .../lib/ui/pages/home_screen/home_screen.dart | 94 ++++--- .../ui/pages/home_screen/stats_counter.dart | 80 +++--- .../lib/ui/pages/home_screen/todo_item.dart | 15 +- .../lib/ui/pages/home_screen/todo_list.dart | 27 +- .../shared_widgets/check_favorite_box.dart | 45 +++ states_rebuilder/pubspec.yaml | 3 +- states_rebuilder/test/fake_repository.dart | 48 ++-- states_rebuilder/test/home_screen_test.dart | 257 ++++++++++++++++++ 23 files changed, 638 insertions(+), 237 deletions(-) create mode 100644 states_rebuilder/lib/service/filtered_todos.dart create mode 100644 states_rebuilder/lib/service/stats_service.dart create mode 100644 states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart create mode 100644 states_rebuilder/test/home_screen_test.dart diff --git a/states_rebuilder/lib/app.dart b/states_rebuilder/lib/app.dart index 83ea8fcb..d79c7280 100644 --- a/states_rebuilder/lib/app.dart +++ b/states_rebuilder/lib/app.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; +import 'package:states_rebuilder_sample/service/interfaces/i_todo_repository.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 '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); @@ -25,8 +25,9 @@ class StatesRebuilderApp extends StatelessWidget { ArchSampleLocalizationsDelegate(), StatesRebuilderLocalizationsDelegate(), ], + home: HomeScreen(), routes: { - ArchSampleRoutes.home: (context) => HomeScreen(), + // ArchSampleRoutes.home: (context) => HomeScreen(), ArchSampleRoutes.addTodo: (context) => AddEditPage(), }, ), diff --git a/states_rebuilder/lib/data_source/todo_repository.dart b/states_rebuilder/lib/data_source/todo_repository.dart index d215055e..1248a371 100644 --- a/states_rebuilder/lib/data_source/todo_repository.dart +++ b/states_rebuilder/lib/data_source/todo_repository.dart @@ -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 @@ -27,16 +27,17 @@ class StatesBuilderTodosRepository implements ITodosRepository { } @override - Future saveTodos(List todos) { + Future saveTodos(List todos) async { try { var todosEntities = []; + // await Future.delayed(Duration(milliseconds: 200)); + // 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'); } } } diff --git a/states_rebuilder/lib/domain/entities/todo.dart b/states_rebuilder/lib/domain/entities/todo.dart index c149058b..3b95b4c7 100644 --- a/states_rebuilder/lib/domain/entities/todo.dart +++ b/states_rebuilder/lib/domain/entities/todo.dart @@ -5,19 +5,21 @@ 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. class Todo { - String id; - bool complete; - String note; - String task; + String _id; + String get id => _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(); + : _id = id ?? flutter_arch_sample_app.Uuid().generateV4(); - Todo.fromJson(Map 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 map) { + return Todo( + map['task'] as String, + note: map['note'] as String, + complete: map['complete'] as bool, + ); } // toJson is called just before persistance. @@ -32,7 +34,7 @@ class Todo { } void _validation() { - if (id == null) { + if (_id == null) { // Custom defined error classes throw ValidationException('This todo has no ID!'); } @@ -41,11 +43,30 @@ 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 - int get hashCode => id.hashCode; + int get hashCode => _id.hashCode; @override bool operator ==(Object other) => identical(this, other) || - other is Todo && runtimeType == other.runtimeType && id == other.id; + other is Todo && runtimeType == other.runtimeType && _id == other._id; + + @override + String toString() { + return 'Todo(id: $id,task:$task, complete: $complete)'; + } } diff --git a/states_rebuilder/lib/main.dart b/states_rebuilder/lib/main.dart index 63784241..8a16c84e 100644 --- a/states_rebuilder/lib/main.dart +++ b/states_rebuilder/lib/main.dart @@ -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', diff --git a/states_rebuilder/lib/main_web.dart b/states_rebuilder/lib/main_web.dart index 97eae9a0..3d5cf765 100644 --- a/states_rebuilder/lib/main_web.dart +++ b/states_rebuilder/lib/main_web.dart @@ -15,7 +15,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); runApp( StatesRebuilderApp( - repository: StatesBuilderTodosRepository( + repository: StatesRebuilderTodosRepository( todosRepository: LocalStorageRepository( localStorage: KeyValueStorage( 'states_rebuilder', diff --git a/states_rebuilder/lib/service/exceptions/persistance_exception.dart b/states_rebuilder/lib/service/exceptions/persistance_exception.dart index fdc86d9e..d5a76c8d 100644 --- a/states_rebuilder/lib/service/exceptions/persistance_exception.dart +++ b/states_rebuilder/lib/service/exceptions/persistance_exception.dart @@ -1,5 +1,8 @@ class PersistanceException extends Error { final String message; - PersistanceException(this.message); + @override + String toString() { + return message.toString(); + } } diff --git a/states_rebuilder/lib/service/filtered_todos.dart b/states_rebuilder/lib/service/filtered_todos.dart new file mode 100644 index 00000000..444dba62 --- /dev/null +++ b/states_rebuilder/lib/service/filtered_todos.dart @@ -0,0 +1,22 @@ +import 'package:states_rebuilder_sample/domain/entities/todo.dart'; +import 'package:states_rebuilder_sample/service/common/enums.dart'; + +class FilteredTodos { + final List _todos; + + VisibilityFilter activeFilter = VisibilityFilter.all; + + FilteredTodos(this._todos); + + List get todos { + return _todos.where((todo) { + if (activeFilter == VisibilityFilter.all) { + return true; + } else if (activeFilter == VisibilityFilter.active) { + return !todo.complete; + } else { + return todo.complete; + } + }).toList(); + } +} diff --git a/states_rebuilder/lib/service/stats_service.dart b/states_rebuilder/lib/service/stats_service.dart new file mode 100644 index 00000000..f00f58a9 --- /dev/null +++ b/states_rebuilder/lib/service/stats_service.dart @@ -0,0 +1,10 @@ +import 'package:states_rebuilder_sample/domain/entities/todo.dart'; + +class Stats { + final List _todos; + + Stats(this._todos); + + int get numActive => _todos.where((todo) => !todo.complete).toList().length; + int get numCompleted => _todos.where((todo) => todo.complete).toList().length; +} diff --git a/states_rebuilder/lib/service/todos_service.dart b/states_rebuilder/lib/service/todos_service.dart index 49d80081..10deb5c3 100644 --- a/states_rebuilder/lib/service/todos_service.dart +++ b/states_rebuilder/lib/service/todos_service.dart @@ -35,39 +35,58 @@ class TodosService { bool get allComplete => _activeTodos.isEmpty; //methods for CRUD - void loadTodos() async { - _todos = await _todoRepository.loadTodos(); + Future loadTodos() async { + return _todos = await _todoRepository.loadTodos(); } - void addTodo(Todo todo) { + Future addTodo(Todo todo) async { _todos.add(todo); - _todoRepository.saveTodos(_todos); + await _todoRepository.saveTodos(_todos); } - void updateTodo(Todo todo) { - final index = _todos.indexOf(todo); - if (index == -1) return; + Future updateTodo(Todo todo) async { + final oldTodo = _todos.firstWhere((t) => t.id == todo.id); + final index = _todos.indexOf(oldTodo); _todos[index] = todo; - _todoRepository.saveTodos(_todos); + await _todoRepository.saveTodos(_todos).catchError((error) { + _todos[index] = oldTodo; + throw error; + }); } - void deleteTodo(Todo todo) { - if (_todos.remove(todo)) { - _todoRepository.saveTodos(_todos); - } + Future deleteTodo(Todo todo) async { + final index = _todos.indexOf(todo); + _todos.removeAt(index); + return _todoRepository.saveTodos(_todos).catchError((error) { + _todos.insert(index, todo); + throw error; + }); } - void toggleAll() { + Future toggleAll() async { final allComplete = _todos.every((todo) => todo.complete); + var beforeTodos = []; - for (final todo in _todos) { - todo.complete = !allComplete; + for (var i = 0; i < _todos.length; i++) { + beforeTodos.add(_todos[i]); + _todos[i] = _todos[i].copyWith(complete: !allComplete); } - _todoRepository.saveTodos(_todos); + return _todoRepository.saveTodos(_todos).catchError( + (error) { + _todos = beforeTodos; + throw error; + }, + ); } - void clearCompleted() { + Future clearCompleted() async { + var beforeTodos = List.from(_todos); _todos.removeWhere((todo) => todo.complete); - _todoRepository.saveTodos(_todos); + await _todoRepository.saveTodos(_todos).catchError( + (error) { + _todos = beforeTodos; + throw error; + }, + ); } } diff --git a/states_rebuilder/lib/ui/common/helper_methods.dart b/states_rebuilder/lib/ui/common/helper_methods.dart index eba8136c..2528935e 100644 --- a/states_rebuilder/lib/ui/common/helper_methods.dart +++ b/states_rebuilder/lib/ui/common/helper_methods.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; +import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:todos_app_core/todos_app_core.dart'; import '../../domain/entities/todo.dart'; import '../../service/todos_service.dart'; class HelperMethods { - static void removeTodo(Todo todo) { - final todosServiceRM = Injector.getAsReactive(); + static void removeTodo(BuildContext context, Todo todo) { + final todosServiceRM = RM.get(); todosServiceRM.setState( - (s) => s.deleteTodo(todo), - onSetState: (context) { + (s) async { Scaffold.of(context).showSnackBar( SnackBar( key: ArchSampleKeys.snackbar, @@ -28,7 +28,9 @@ class HelperMethods { ), ), ); + return s.deleteTodo(todo); }, + onError: ErrorHandler.showErrorSnackBar, ); } } diff --git a/states_rebuilder/lib/ui/exceptions/error_handler.dart b/states_rebuilder/lib/ui/exceptions/error_handler.dart index 4be1dc88..749a5982 100644 --- a/states_rebuilder/lib/ui/exceptions/error_handler.dart +++ b/states_rebuilder/lib/ui/exceptions/error_handler.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:states_rebuilder_sample/domain/exceptions/validation_exception.dart'; import 'package:states_rebuilder_sample/service/exceptions/persistance_exception.dart'; @@ -13,4 +14,22 @@ class ErrorHandler { throw (error); } + + static void showErrorSnackBar(BuildContext context, dynamic error) { + Scaffold.of(context).hideCurrentSnackBar(); + Scaffold.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Text(ErrorHandler.getErrorMessage(error)), + Spacer(), + Icon( + Icons.error_outline, + color: Colors.yellow, + ) + ], + ), + ), + ); + } } diff --git a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart index c3089261..3e520ba8 100644 --- a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart +++ b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart @@ -29,7 +29,7 @@ class _AddEditPageState extends State { String _task; String _note; bool get isEditing => widget.todo != null; - final todosService = Injector.get(); + final todosServiceRM = RM.get(); @override Widget build(BuildContext context) { return Scaffold( @@ -86,19 +86,17 @@ class _AddEditPageState extends State { if (form.validate()) { form.save(); - if (isEditing) { - widget.todo - ..task = _task - ..note = _note; - todosService.updateTodo(widget.todo); - } else { - todosService.addTodo( - Todo( - _task, - note: _note, - ), - ); - } + todosServiceRM.setState( + (s) { + if (isEditing) { + return s.updateTodo(widget.todo.copyWith( + task: _task, + note: _note, + )); + } + return s.addTodo(Todo(_task, note: _note)); + }, + ); Navigator.pop(context); } diff --git a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart index ea3f244b..6785d754 100644 --- a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart +++ b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart @@ -3,37 +3,34 @@ // in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; import 'package:states_rebuilder_sample/ui/common/helper_methods.dart'; import 'package:states_rebuilder_sample/ui/pages/add_edit_screen.dart/add_edit_screen.dart'; +import 'package:states_rebuilder_sample/ui/pages/shared_widgets/check_favorite_box.dart'; import 'package:todos_app_core/todos_app_core.dart'; class DetailScreen extends StatelessWidget { DetailScreen(this.todo) : super(key: ArchSampleKeys.todoDetailsScreen); final Todo todo; - //use Injector.get because DetailScreen need not be reactive - final todosService = Injector.get(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(ArchSampleLocalizations.of(context).todoDetails), actions: [ - IconButton( - key: ArchSampleKeys.deleteTodoButton, - tooltip: ArchSampleLocalizations.of(context).deleteTodo, - icon: Icon(Icons.delete), - onPressed: () { - //This is one particularity of states_rebuilder - //We have the ability to call a method form an injected model without notify observers - //This can be done by consuming the injected model using Injector.get and call the method we want. - todosService.deleteTodo(todo); - //When navigating back to home page, rebuild is granted by flutter framework. - Navigator.pop(context, todo); - //delegate to the static method HelperMethods.removeTodo to remove todo - HelperMethods.removeTodo(todo); + Builder( + builder: (context) { + return IconButton( + key: ArchSampleKeys.deleteTodoButton, + tooltip: ArchSampleLocalizations.of(context).deleteTodo, + icon: Icon(Icons.delete), + onPressed: () { + //When navigating back to home page, rebuild is granted by flutter framework. + Navigator.pop(context, todo); + //delegate to the static method HelperMethods.removeTodo to remove todo + HelperMethods.removeTodo(context, todo); + }, + ); }, ) ], @@ -47,19 +44,9 @@ class DetailScreen extends StatelessWidget { children: [ Padding( padding: EdgeInsets.only(right: 8.0), - child: StateBuilder( - //getting a new ReactiveModel of TodosService to optimize rebuild of widgets - builder: (_, todosServiceRM) { - return Checkbox( - value: todo.complete, - key: ArchSampleKeys.detailsTodoItemCheckbox, - onChanged: (complete) { - todo.complete = !todo.complete; - //only this checkBox will rebuild - todosServiceRM.setState((s) => s.updateTodo(todo)); - }, - ); - }, + child: CheckFavoriteBox( + todo: todo, + key: ArchSampleKeys.detailsTodoItemCheckbox, ), ), Expanded( diff --git a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart index aca10542..6661f715 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart @@ -2,22 +2,28 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; import 'package:states_rebuilder_sample/ui/common/enums.dart'; +import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:todos_app_core/todos_app_core.dart'; class ExtraActionsButton extends StatelessWidget { ExtraActionsButton({Key key}) : super(key: key); - final todosServiceRM = Injector.getAsReactive(); + final todosServiceRM = RM.get(); @override Widget build(BuildContext context) { return PopupMenuButton( key: ArchSampleKeys.extraActionsButton, onSelected: (action) { - if (action == ExtraAction.toggleAllComplete) { - todosServiceRM.setState((s) => s.toggleAll()); - } else if (action == ExtraAction.clearCompleted) { - todosServiceRM.setState((s) => s.clearCompleted()); - } + todosServiceRM.setState( + (s) async { + if (action == ExtraAction.toggleAllComplete) { + return s.toggleAll(); + } else if (action == ExtraAction.clearCompleted) { + return s.clearCompleted(); + } + }, + onError: ErrorHandler.showErrorSnackBar, + ); }, itemBuilder: (BuildContext context) { return >[ diff --git a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart index 0687f5af..e33abe9f 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart @@ -7,17 +7,16 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/service/common/enums.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/ui/common/enums.dart'; import 'package:todos_app_core/todos_app_core.dart'; class FilterButton extends StatelessWidget { - const FilterButton({this.isActive, Key key}) : super(key: key); - final bool isActive; - + const FilterButton({this.activeTabRM, Key key}) : super(key: key); + final ReactiveModel activeTabRM; @override Widget build(BuildContext context) { //context is used to register FilterButton as observer in todosServiceRM - final todosServiceRM = - Injector.getAsReactive(context: context); + final todosServiceRM = RM.get(context: context); final defaultStyle = Theme.of(context).textTheme.body1; final activeStyle = Theme.of(context) @@ -33,11 +32,16 @@ class FilterButton extends StatelessWidget { defaultStyle: defaultStyle, ); - return AnimatedOpacity( - opacity: isActive ? 1.0 : 0.0, - duration: Duration(milliseconds: 150), - child: isActive ? button : IgnorePointer(child: button), - ); + return StateBuilder( + observe: () => activeTabRM, + builder: (context, activeTabRM) { + final _isActive = activeTabRM.value == AppTab.todos; + return AnimatedOpacity( + opacity: _isActive ? 1.0 : 0.0, + duration: Duration(milliseconds: 150), + child: _isActive ? button : IgnorePointer(child: button), + ); + }); } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart index 69655b82..68e60df5 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart @@ -10,35 +10,37 @@ import 'filter_button.dart'; import 'stats_counter.dart'; import 'todo_list.dart'; -class HomeScreen extends StatefulWidget { - HomeScreen({Key key}) : super(key: key ?? ArchSampleKeys.homeScreen); - - @override - _HomeScreenState createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - // Here we use a StatefulWidget to store the _activeTab state which is private to this class - - AppTab _activeTab = AppTab.todos; +// RM.printActiveRM = true; +class HomeScreen extends StatelessWidget { + //create a local ReactiveeModel to deal with the state of the tabs. + final _activeTabRMKey = RMKey(AppTab.todos); @override Widget build(BuildContext context) { + print('rebuild HomeScreen'); return Scaffold( appBar: AppBar( title: Text(StatesRebuilderLocalizations.of(context).appTitle), actions: [ - FilterButton(isActive: _activeTab == AppTab.todos), + FilterButton(activeTabRM: _activeTabRMKey), ExtraActionsButton(), ], ), - body: StateBuilder( - models: [Injector.getAsReactive()], - initState: (_, todosServiceRM) { - //update state and notify observer - return todosServiceRM.setState((s) => s.loadTodos()); + body: WhenRebuilderOr( + observeMany: [ + () => RM.getFuture((t) => t.loadTodos()), + () => _activeTabRMKey, + ], + onWaiting: () { + return Center( + child: CircularProgressIndicator( + key: ArchSampleKeys.todosLoading, + ), + ); }, - builder: (_, todosServiceRM) { - return _activeTab == AppTab.todos ? TodoList() : StatsCounter(); + builder: (context, _activeTabRM) { + return _activeTabRM.value == AppTab.todos + ? TodoList() + : StatsCounter(); }, ), floatingActionButton: FloatingActionButton( @@ -49,31 +51,37 @@ class _HomeScreenState extends State { child: Icon(Icons.add), tooltip: ArchSampleLocalizations.of(context).addTodo, ), - bottomNavigationBar: BottomNavigationBar( - key: ArchSampleKeys.tabs, - currentIndex: AppTab.values.indexOf(_activeTab), - onTap: (index) { - //mutate the state of the private field _activeTab and use Flutter setState because - setState(() => _activeTab = AppTab.values[index]); - }, - items: AppTab.values.map( - (tab) { - return BottomNavigationBarItem( - icon: Icon( - tab == AppTab.todos ? Icons.list : Icons.show_chart, - key: tab == AppTab.stats - ? ArchSampleKeys.statsTab - : ArchSampleKeys.todoTab, - ), - title: Text( - tab == AppTab.stats - ? ArchSampleLocalizations.of(context).stats - : ArchSampleLocalizations.of(context).todos, - ), + bottomNavigationBar: StateBuilder( + observe: () => RM.create(AppTab.todos), + rmKey: _activeTabRMKey, + builder: (context, _activeTabRM) { + return BottomNavigationBar( + key: ArchSampleKeys.tabs, + currentIndex: AppTab.values.indexOf(_activeTabRM.value), + onTap: (index) { + //mutate the value of the private field _activeTab, + //observing widget will be notified to rebuild + _activeTabRM.value = AppTab.values[index]; + }, + items: AppTab.values.map( + (tab) { + return BottomNavigationBarItem( + icon: Icon( + tab == AppTab.todos ? Icons.list : Icons.show_chart, + key: tab == AppTab.stats + ? ArchSampleKeys.statsTab + : ArchSampleKeys.todoTab, + ), + title: Text( + tab == AppTab.stats + ? ArchSampleLocalizations.of(context).stats + : ArchSampleLocalizations.of(context).todos, + ), + ); + }, + ).toList(), ); - }, - ).toList(), - ), + }), ); } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart index 1b134031..e636f848 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart @@ -5,53 +5,57 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; +import 'package:states_rebuilder_sample/service/stats_service.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; import 'package:todos_app_core/todos_app_core.dart'; class StatsCounter extends StatelessWidget { - //use Injector.get, because this class need not to be reactive and its rebuild is ensured by its parent. - final todosService = Injector.get(); - StatsCounter() : super(key: ArchSampleKeys.statsCounter); @override Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: Text( - ArchSampleLocalizations.of(context).completedTodos, - style: Theme.of(context).textTheme.title, - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 24.0), - child: Text( - '${todosService.numCompleted}', - key: ArchSampleKeys.statsNumCompleted, - style: Theme.of(context).textTheme.subhead, - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: Text( - ArchSampleLocalizations.of(context).activeTodos, - style: Theme.of(context).textTheme.title, - ), + return StateBuilder( + observe: () => RM.get(), + builder: (_, todosServiceRM) { + final stats = Stats(todosServiceRM.state.todos); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + ArchSampleLocalizations.of(context).completedTodos, + style: Theme.of(context).textTheme.title, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 24.0), + child: Text( + '${stats.numCompleted}', + key: ArchSampleKeys.statsNumCompleted, + style: Theme.of(context).textTheme.subhead, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + ArchSampleLocalizations.of(context).activeTodos, + style: Theme.of(context).textTheme.title, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 24.0), + child: Text( + '${stats.numActive}', + key: ArchSampleKeys.statsNumActive, + style: Theme.of(context).textTheme.subhead, + ), + ) + ], ), - Padding( - padding: EdgeInsets.only(bottom: 24.0), - child: Text( - '${todosService.numActive}', - key: ArchSampleKeys.statsNumActive, - style: Theme.of(context).textTheme.subhead, - ), - ) - ], - ), + ); + }, ); } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index 4091623d..44ae233b 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -4,11 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; import 'package:states_rebuilder_sample/ui/common/helper_methods.dart'; import 'package:states_rebuilder_sample/ui/pages/detail_screen/detail_screen.dart'; +import 'package:states_rebuilder_sample/ui/pages/shared_widgets/check_favorite_box.dart'; import 'package:todos_app_core/todos_app_core.dart'; class TodoItem extends StatelessWidget { @@ -19,15 +18,13 @@ class TodoItem extends StatelessWidget { @required this.todo, }) : super(key: key); - final todosServiceRM = Injector.getAsReactive(); - @override Widget build(BuildContext context) { return Dismissible( key: ArchSampleKeys.todoItem(todo.id), onDismissed: (direction) { //delegate removing todo to the static method HelperMethods.removeTodo. - HelperMethods.removeTodo(todo); + HelperMethods.removeTodo(context, todo); }, child: ListTile( onTap: () { @@ -39,13 +36,9 @@ class TodoItem extends StatelessWidget { ), ); }, - leading: Checkbox( + leading: CheckFavoriteBox( + todo: todo, key: ArchSampleKeys.todoItemCheckbox(todo.id), - value: todo.complete, - onChanged: (complete) { - todo.complete = !todo.complete; - todosServiceRM.setState((state) => state.updateTodo(todo)); - }, ), title: Text( todo.task, diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart index 449b1ef1..1ad2df63 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart @@ -5,38 +5,29 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; -import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; +import 'package:states_rebuilder_sample/ui/common/enums.dart'; import 'package:todos_app_core/todos_app_core.dart'; import 'todo_item.dart'; class TodoList extends StatelessWidget { - TodoList() : super(key: ArchSampleKeys.todoList); - final todosServiceRM = Injector.getAsReactive(); + TodoList(); @override Widget build(BuildContext context) { - //use whenConnectionState method to go through all the possible status of the ReactiveModel - return todosServiceRM.whenConnectionState( - onIdle: () => Container(), - onWaiting: () => Center( - child: CircularProgressIndicator( - key: ArchSampleKeys.todosLoading, - ), - ), - onData: (todosService) { + return StateBuilder( + observe: () => RM.get(), + tag: AppTab.todos, + builder: (context, todosServiceRM) { + print('rebuild of todoList'); return ListView.builder( key: ArchSampleKeys.todoList, - itemCount: todosService.todos.length, + itemCount: todosServiceRM.state.todos.length, itemBuilder: (BuildContext context, int index) { - final todo = todosService.todos[index]; + final todo = todosServiceRM.state.todos[index]; return TodoItem(todo: todo); }, ); }, - onError: (error) { - //Delegate error handling to the static method ErrorHandler.getErrorMessage - return Center(child: Text(ErrorHandler.getErrorMessage(error))); - }, ); } } diff --git a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart new file mode 100644 index 00000000..4dc77209 --- /dev/null +++ b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:states_rebuilder/states_rebuilder.dart'; + +import '../../../domain/entities/todo.dart'; +import '../../../service/todos_service.dart'; + +import '../../exceptions/error_handler.dart'; + +class CheckFavoriteBox extends StatelessWidget { + const CheckFavoriteBox({ + Key key, + @required this.todo, + }) : _key = key; + final Key _key; + final Todo todo; + + @override + Widget build(BuildContext context) { + return StateBuilder( + observe: () => RM.create(todo.complete), + builder: (context, completeRM) { + return Checkbox( + key: _key, + value: completeRM.value, + onChanged: (value) { + completeRM.value = value; + RM + .getFuture( + (t) => t.updateTodo( + todo.copyWith(complete: value), + ), + ) + .subscription + .onError( + (error) { + completeRM.value = !value; + ErrorHandler.showErrorSnackBar(context, error); + }, + ); + }, + ); + }, + ); + } +} diff --git a/states_rebuilder/pubspec.yaml b/states_rebuilder/pubspec.yaml index 10e92960..4ee9704d 100644 --- a/states_rebuilder/pubspec.yaml +++ b/states_rebuilder/pubspec.yaml @@ -19,7 +19,8 @@ environment: dependencies: flutter: sdk: flutter - states_rebuilder: ^1.11.2 + states_rebuilder: + path: E:\flutter\_libraries\_states_rebuilder\states_rebuilder\states_rebuilder_package key_value_store_flutter: key_value_store_web: shared_preferences: diff --git a/states_rebuilder/test/fake_repository.dart b/states_rebuilder/test/fake_repository.dart index d8fa18ff..e0057801 100644 --- a/states_rebuilder/test/fake_repository.dart +++ b/states_rebuilder/test/fake_repository.dart @@ -1,31 +1,41 @@ import 'package:states_rebuilder_sample/domain/entities/todo.dart'; +import 'package:states_rebuilder_sample/service/exceptions/persistance_exception.dart'; import 'package:states_rebuilder_sample/service/interfaces/i_todo_repository.dart'; class FakeRepository implements ITodosRepository { @override - Future> loadTodos() { - return Future.value( - [ - Todo( - 'task1', - id: '1', - note: 'note1', - complete: true, - ), - Todo( - 'task2', - id: '2', - note: 'note2', - complete: false, - ), - ], - ); + Future> loadTodos() async { + await Future.delayed(Duration(milliseconds: 20)); + return [ + Todo( + 'Task1', + id: '1', + note: 'Note1', + ), + Todo( + 'Task2', + id: '2', + note: 'Note2', + complete: false, + ), + Todo( + 'Task3', + id: '3', + note: 'Note3', + complete: true, + ), + ]; } + bool throwError = false; bool isSaved = false; @override - Future saveTodos(List todos) { + Future saveTodos(List todos) async { + await Future.delayed(Duration(milliseconds: 50)); + if (throwError) { + throw PersistanceException('There is a problem in saving todos'); + } isSaved = true; - return Future.value(true); + return true; } } diff --git a/states_rebuilder/test/home_screen_test.dart b/states_rebuilder/test/home_screen_test.dart new file mode 100644 index 00000000..6753ce8d --- /dev/null +++ b/states_rebuilder/test/home_screen_test.dart @@ -0,0 +1,257 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:states_rebuilder/states_rebuilder.dart'; +import 'package:states_rebuilder_sample/app.dart'; +import 'package:states_rebuilder_sample/domain/entities/todo.dart'; +import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/ui/common/enums.dart'; +import 'package:todos_app_core/todos_app_core.dart'; + +import 'fake_repository.dart'; + +/// Demonstrates how to test Widgets +void main() { + group('HomeScreen', () { + final todoListFinder = find.byKey(ArchSampleKeys.todoList); + final todoItem1Finder = find.byKey(ArchSampleKeys.todoItem('1')); + final todoItem2Finder = find.byKey(ArchSampleKeys.todoItem('2')); + final todoItem3Finder = find.byKey(ArchSampleKeys.todoItem('3')); + + testWidgets('should render loading indicator at first', (tester) async { + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository(), + ), + ); + await tester.pump(Duration.zero); + expect(find.byKey(ArchSampleKeys.todosLoading), findsOneWidget); + await tester.pumpAndSettle(); + //check the reactive model that causes the rebuild of the widget + var notifiedRM = RM.notified; + expect( + notifiedRM.isA>() && + notifiedRM.hasData && + notifiedRM.state is List && + notifiedRM.state.length == 3, + isTrue, + ); + }); + + testWidgets('should display a list after loading todos', (tester) async { + final handle = tester.ensureSemantics(); + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository(), + ), + ); + await tester.pumpAndSettle(); + + final checkbox1 = find.descendant( + of: find.byKey(ArchSampleKeys.todoItemCheckbox('1')), + matching: find.byType(Focus), + ); + final checkbox2 = find.descendant( + of: find.byKey(ArchSampleKeys.todoItemCheckbox('2')), + matching: find.byType(Focus), + ); + final checkbox3 = find.descendant( + of: find.byKey(ArchSampleKeys.todoItemCheckbox('3')), + matching: find.byType(Focus), + ); + + expect(todoListFinder, findsOneWidget); + expect(todoItem1Finder, findsOneWidget); + expect(find.text('Task1'), findsOneWidget); + expect(find.text('Note1'), findsOneWidget); + expect(tester.getSemantics(checkbox1), isChecked(false)); + expect(todoItem2Finder, findsOneWidget); + expect(find.text('Task2'), findsOneWidget); + expect(find.text('Note2'), findsOneWidget); + expect(tester.getSemantics(checkbox2), isChecked(false)); + expect(todoItem3Finder, findsOneWidget); + expect(find.text('Task3'), findsOneWidget); + expect(find.text('Note3'), findsOneWidget); + expect(tester.getSemantics(checkbox3), isChecked(true)); + + handle.dispose(); + }); + + testWidgets('should remove todos using a dismissible', (tester) async { + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository(), + ), + ); + await tester.pumpAndSettle(); + await tester.drag(todoItem1Finder, Offset(-1000, 0)); + await tester.pumpAndSettle(); + + //check the reactive model that causes the rebuild of the widget + var notifiedRM = RM.notified; + expect( + notifiedRM.isA() && + notifiedRM.hasData && + notifiedRM.state.todos is List && + notifiedRM.state.todos.length == 2, + isTrue, + ); + + expect(todoItem1Finder, findsNothing); + expect(todoItem2Finder, findsOneWidget); + expect(todoItem3Finder, findsOneWidget); + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('Undo'), findsOneWidget); + }); + + testWidgets( + 'should remove todos using a dismissible ane insert back the removed element if throws', + (tester) async { + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository()..throwError = true, + ), + ); + + await tester.pumpAndSettle(); + await tester.drag(todoItem1Finder, Offset(-1000, 0)); + await tester.pumpAndSettle(); + + //check the reactive model that causes the rebuild of the widget + var notifiedRM = RM.notified; + expect( + notifiedRM.isA() && + notifiedRM.hasError && + notifiedRM.error.message == 'There is a problem in saving todos', + isTrue, + ); + + //Removed item in inserted back to the list + expect(todoItem1Finder, findsOneWidget); + expect(todoItem2Finder, findsOneWidget); + expect(todoItem3Finder, findsOneWidget); + //SnackBar with error message + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('There is a problem in saving todos'), findsOneWidget); + }); + + testWidgets('should display stats when switching tabs', (tester) async { + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(ArchSampleKeys.statsTab)); + await tester.pump(); + + //check the reactive model that causes the rebuild of the widget + var notifiedRM = RM.notified; + expect( + notifiedRM.isA() && + notifiedRM.hasData && + notifiedRM.value == AppTab.stats, + isTrue, + ); + + expect(find.byKey(ArchSampleKeys.statsNumActive), findsOneWidget); + expect(find.byKey(ArchSampleKeys.statsNumActive), findsOneWidget); + }); + + testWidgets('should toggle a todo', (tester) async { + final handle = tester.ensureSemantics(); + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository(), + ), + ); + await tester.pumpAndSettle(); + + final checkbox1 = find.descendant( + of: find.byKey(ArchSampleKeys.todoItemCheckbox('1')), + matching: find.byType(Focus), + ); + expect(tester.getSemantics(checkbox1), isChecked(false)); + + await tester.tap(checkbox1); + await tester.pump(); + //check the reactive model that causes the rebuild of the widget + var notifiedRM = RM.notified; + expect( + notifiedRM.isA() && + notifiedRM.hasData && + notifiedRM.value == true, + isTrue, + ); + expect(tester.getSemantics(checkbox1), isChecked(true)); + + await tester.pumpAndSettle(); + handle.dispose(); + }); + + testWidgets('should toggle a todo and toggle back if throws', + (tester) async { + final handle = tester.ensureSemantics(); + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository()..throwError = true, + ), + ); + await tester.pumpAndSettle(); + + final checkbox1 = find.descendant( + of: find.byKey(ArchSampleKeys.todoItemCheckbox('1')), + matching: find.byType(Focus), + ); + expect(tester.getSemantics(checkbox1), isChecked(false)); + + await tester.tap(checkbox1); + await tester.pump(); + //check the reactive model that causes the rebuild of the widget + var notifiedRM = RM.notified; + print(notifiedRM); + expect( + notifiedRM.isA() && + notifiedRM.hasData && + notifiedRM.value == true, + isTrue, + ); + expect(tester.getSemantics(checkbox1), isChecked(true)); + //NO Error, + expect(find.byType(SnackBar), findsNothing); + //expect that the saveTodos method is still running + //and the reactive model of todosService is waiting; + // expect(RM.get().isWaiting, isTrue); + RM.printActiveRM = true; + // + await tester.pumpAndSettle(); + notifiedRM = RM.notified; + print(notifiedRM); + expect( + notifiedRM.isA() && + notifiedRM.hasData && + notifiedRM.value == false, + isTrue, + ); + expect(tester.getSemantics(checkbox1), isChecked(false)); + //expect that the saveTodos is ended with error + //and the reactive model of todosService has en error; + // expect(RM.get().hasError, isTrue); + + //SnackBar with error message + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('There is a problem in saving todos'), findsOneWidget); + handle.dispose(); + }); + }); +} + +Matcher isChecked(bool isChecked) { + return matchesSemantics( + isChecked: isChecked, + hasCheckedState: true, + hasEnabledState: true, + isEnabled: true, + isFocusable: true, + hasTapAction: true, + ); +} From 30c6a45564333f23a991bb2aef9197594f77fe75 Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Wed, 22 Apr 2020 16:31:58 +0100 Subject: [PATCH 02/10] minimize rebuild --- states_rebuilder/lib/app.dart | 4 +- .../lib/data_source/todo_repository.dart | 2 +- .../lib/domain/entities/todo.dart | 16 +++- .../lib/service/todos_service.dart | 5 +- .../lib/ui/common/helper_methods.dart | 35 ------- .../add_edit_screen.dart/add_edit_screen.dart | 62 +++++++++---- .../ui/pages/detail_screen/detail_screen.dart | 93 +++++++++---------- .../ui/pages/home_screen/stats_counter.dart | 1 - .../lib/ui/pages/home_screen/todo_item.dart | 53 ++++++++--- .../lib/ui/pages/home_screen/todo_list.dart | 18 +++- .../shared_widgets/check_favorite_box.dart | 46 +++++---- 11 files changed, 183 insertions(+), 152 deletions(-) diff --git a/states_rebuilder/lib/app.dart b/states_rebuilder/lib/app.dart index d79c7280..6669b52e 100644 --- a/states_rebuilder/lib/app.dart +++ b/states_rebuilder/lib/app.dart @@ -14,6 +14,7 @@ class StatesRebuilderApp extends StatelessWidget { @override Widget build(BuildContext context) { + RM.printActiveRM = true; //Injecting the TodoService globally before MaterialApp widget. //It will be available throughout all the widget tree even after navigation. return Injector( @@ -25,9 +26,8 @@ class StatesRebuilderApp extends StatelessWidget { ArchSampleLocalizationsDelegate(), StatesRebuilderLocalizationsDelegate(), ], - home: HomeScreen(), routes: { - // ArchSampleRoutes.home: (context) => HomeScreen(), + ArchSampleRoutes.home: (context) => HomeScreen(), ArchSampleRoutes.addTodo: (context) => AddEditPage(), }, ), diff --git a/states_rebuilder/lib/data_source/todo_repository.dart b/states_rebuilder/lib/data_source/todo_repository.dart index 1248a371..c09098c3 100644 --- a/states_rebuilder/lib/data_source/todo_repository.dart +++ b/states_rebuilder/lib/data_source/todo_repository.dart @@ -30,7 +30,7 @@ class StatesRebuilderTodosRepository implements ITodosRepository { Future saveTodos(List todos) async { try { var todosEntities = []; - // await Future.delayed(Duration(milliseconds: 200)); + // await Future.delayed(Duration(milliseconds: 500)); // throw Exception(); for (var todo in todos) { todosEntities.add(TodoEntity.fromJson(todo.toJson())); diff --git a/states_rebuilder/lib/domain/entities/todo.dart b/states_rebuilder/lib/domain/entities/todo.dart index 3b95b4c7..de2c1e95 100644 --- a/states_rebuilder/lib/domain/entities/todo.dart +++ b/states_rebuilder/lib/domain/entities/todo.dart @@ -58,12 +58,20 @@ class Todo { } @override - int get hashCode => _id.hashCode; + 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 - bool operator ==(Object other) => - identical(this, other) || - other is Todo && runtimeType == other.runtimeType && _id == other._id; + int get hashCode { + return _id.hashCode ^ complete.hashCode ^ note.hashCode ^ task.hashCode; + } @override String toString() { diff --git a/states_rebuilder/lib/service/todos_service.dart b/states_rebuilder/lib/service/todos_service.dart index 10deb5c3..8406550e 100644 --- a/states_rebuilder/lib/service/todos_service.dart +++ b/states_rebuilder/lib/service/todos_service.dart @@ -41,7 +41,10 @@ class TodosService { Future addTodo(Todo todo) async { _todos.add(todo); - await _todoRepository.saveTodos(_todos); + await _todoRepository.saveTodos(_todos).catchError((error) { + _todos.remove(todo); + throw error; + }); } Future updateTodo(Todo todo) async { diff --git a/states_rebuilder/lib/ui/common/helper_methods.dart b/states_rebuilder/lib/ui/common/helper_methods.dart index 2528935e..8b137891 100644 --- a/states_rebuilder/lib/ui/common/helper_methods.dart +++ b/states_rebuilder/lib/ui/common/helper_methods.dart @@ -1,36 +1 @@ -import 'package:flutter/material.dart'; -import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; -import 'package:todos_app_core/todos_app_core.dart'; -import '../../domain/entities/todo.dart'; -import '../../service/todos_service.dart'; - -class HelperMethods { - static void removeTodo(BuildContext context, Todo todo) { - final todosServiceRM = RM.get(); - todosServiceRM.setState( - (s) async { - Scaffold.of(context).showSnackBar( - SnackBar( - key: ArchSampleKeys.snackbar, - duration: Duration(seconds: 2), - content: Text( - ArchSampleLocalizations.of(context).todoDeleted(todo.task), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - action: SnackBarAction( - label: ArchSampleLocalizations.of(context).undo, - onPressed: () { - todosServiceRM.setState((s) => s.addTodo(todo)); - }, - ), - ), - ); - return s.deleteTodo(todo); - }, - onError: ErrorHandler.showErrorSnackBar, - ); - } -} diff --git a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart index 3e520ba8..67bd6944 100644 --- a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart +++ b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart @@ -9,14 +9,15 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/domain/entities/todo.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:todos_app_core/todos_app_core.dart'; class AddEditPage extends StatefulWidget { - final Todo todo; + final ReactiveModel todoRM; AddEditPage({ Key key, - this.todo, + this.todoRM, }) : super(key: key ?? ArchSampleKeys.addTodoScreen); @override @@ -28,8 +29,8 @@ class _AddEditPageState extends State { // Here we use a StatefulWidget to hold local fields _task and _note String _task; String _note; - bool get isEditing => widget.todo != null; - final todosServiceRM = RM.get(); + bool get isEditing => widget.todoRM != null; + Todo get todo => widget.todoRM?.value; @override Widget build(BuildContext context) { return Scaffold( @@ -49,7 +50,7 @@ class _AddEditPageState extends State { child: ListView( children: [ TextFormField( - initialValue: widget.todo != null ? widget.todo.task : '', + initialValue: todo != null ? todo.task : '', key: ArchSampleKeys.taskField, autofocus: isEditing ? false : true, style: Theme.of(context).textTheme.headline, @@ -61,7 +62,7 @@ class _AddEditPageState extends State { onSaved: (value) => _task = value, ), TextFormField( - initialValue: widget.todo != null ? widget.todo.note : '', + initialValue: todo != null ? todo.note : '', key: ArchSampleKeys.noteField, maxLines: 10, style: Theme.of(context).textTheme.subhead, @@ -85,20 +86,41 @@ class _AddEditPageState extends State { final form = formKey.currentState; if (form.validate()) { form.save(); - - todosServiceRM.setState( - (s) { - if (isEditing) { - return s.updateTodo(widget.todo.copyWith( - task: _task, - note: _note, - )); - } - return s.addTodo(Todo(_task, note: _note)); - }, - ); - - Navigator.pop(context); + final todosServiceRM = RM.get(); + if (isEditing) { + final oldTodo = todo; + final newTodo = todo.copyWith( + task: _task, + note: _note, + ); + widget.todoRM.setState( + (s) async { + await todosServiceRM.setState( + (s) { + widget.todoRM.value = newTodo; + Navigator.pop(context, newTodo); + return s.updateTodo(newTodo); + }, + onError: (context, error) { + throw error; + }, + ); + }, + watch: (todoRM) => widget.todoRM.hasError, + onError: (context, error) { + widget.todoRM.value = oldTodo; + ErrorHandler.showErrorSnackBar(context, error); + }, + ); + } else { + todosServiceRM.setState( + (s) { + Navigator.pop(context); + return s.addTodo(Todo(_task, note: _note)); + }, + onError: ErrorHandler.showErrorSnackBar, + ); + } } }, ), diff --git a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart index 6785d754..66242d1b 100644 --- a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart +++ b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart @@ -3,34 +3,29 @@ // in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:states_rebuilder/states_rebuilder.dart'; + import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/ui/common/helper_methods.dart'; import 'package:states_rebuilder_sample/ui/pages/add_edit_screen.dart/add_edit_screen.dart'; import 'package:states_rebuilder_sample/ui/pages/shared_widgets/check_favorite_box.dart'; import 'package:todos_app_core/todos_app_core.dart'; class DetailScreen extends StatelessWidget { - DetailScreen(this.todo) : super(key: ArchSampleKeys.todoDetailsScreen); - final Todo todo; + DetailScreen(this.todoRM) : super(key: ArchSampleKeys.todoDetailsScreen); + final ReactiveModel todoRM; + Todo get todo => todoRM.value; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(ArchSampleLocalizations.of(context).todoDetails), actions: [ - Builder( - builder: (context) { - return IconButton( - key: ArchSampleKeys.deleteTodoButton, - tooltip: ArchSampleLocalizations.of(context).deleteTodo, - icon: Icon(Icons.delete), - onPressed: () { - //When navigating back to home page, rebuild is granted by flutter framework. - Navigator.pop(context, todo); - //delegate to the static method HelperMethods.removeTodo to remove todo - HelperMethods.removeTodo(context, todo); - }, - ); + IconButton( + key: ArchSampleKeys.deleteTodoButton, + tooltip: ArchSampleLocalizations.of(context).deleteTodo, + icon: Icon(Icons.delete), + onPressed: () { + Navigator.pop(context, true); }, ) ], @@ -39,41 +34,45 @@ class DetailScreen extends StatelessWidget { padding: EdgeInsets.all(16.0), child: ListView( children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(right: 8.0), - child: CheckFavoriteBox( - todo: todo, - key: ArchSampleKeys.detailsTodoItemCheckbox, - ), - ), - Expanded( - child: Column( + StateBuilder( + observe: () => todoRM, + builder: (_, __) { + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.only( - top: 8.0, - bottom: 16.0, + padding: EdgeInsets.only(right: 8.0), + child: CheckFavoriteBox( + todoRM: todoRM, + key: ArchSampleKeys.detailsTodoItemCheckbox, ), - child: Text( - todo.task, - key: ArchSampleKeys.detailsTodoItemTask, - style: Theme.of(context).textTheme.headline, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + top: 8.0, + bottom: 16.0, + ), + child: Text( + todo.task, + key: ArchSampleKeys.detailsTodoItemTask, + style: Theme.of(context).textTheme.headline, + ), + ), + Text( + todo.note, + key: ArchSampleKeys.detailsTodoItemNote, + style: Theme.of(context).textTheme.subhead, + ) + ], ), ), - Text( - todo.note, - key: ArchSampleKeys.detailsTodoItemNote, - style: Theme.of(context).textTheme.subhead, - ) ], - ), - ), - ], - ), + ); + }), ], ), ), @@ -81,13 +80,13 @@ class DetailScreen extends StatelessWidget { tooltip: ArchSampleLocalizations.of(context).editTodo, child: Icon(Icons.edit), key: ArchSampleKeys.editTodoFab, - onPressed: () { - Navigator.of(context).push( + onPressed: () async { + await Navigator.of(context).push( MaterialPageRoute( builder: (context) { return AddEditPage( key: ArchSampleKeys.editTodoScreen, - todo: todo, + todoRM: todoRM, ); }, ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart index e636f848..87795c6c 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by the MIT license that can be found // in the LICENSE file. -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/service/stats_service.dart'; diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index 44ae233b..fe68d2e8 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -2,20 +2,22 @@ // Use of this source code is governed by the MIT license that can be found // in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:states_rebuilder/states_rebuilder.dart'; + import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/ui/common/helper_methods.dart'; +import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:states_rebuilder_sample/ui/pages/detail_screen/detail_screen.dart'; import 'package:states_rebuilder_sample/ui/pages/shared_widgets/check_favorite_box.dart'; import 'package:todos_app_core/todos_app_core.dart'; class TodoItem extends StatelessWidget { - final Todo todo; - + final ReactiveModel todoRM; + Todo get todo => todoRM.value; TodoItem({ Key key, - @required this.todo, + @required this.todoRM, }) : super(key: key); @override @@ -23,21 +25,23 @@ class TodoItem extends StatelessWidget { return Dismissible( key: ArchSampleKeys.todoItem(todo.id), onDismissed: (direction) { - //delegate removing todo to the static method HelperMethods.removeTodo. - HelperMethods.removeTodo(context, todo); + removeTodo(context, todo); }, child: ListTile( - onTap: () { - Navigator.of(context).push( + onTap: () async { + final shouldDelete = await Navigator.of(context).push( MaterialPageRoute( builder: (_) { - return DetailScreen(todo); + return DetailScreen(todoRM); }, ), ); + if (shouldDelete == true) { + removeTodo(context, todo); + } }, leading: CheckFavoriteBox( - todo: todo, + todoRM: todoRM, key: ArchSampleKeys.todoItemCheckbox(todo.id), ), title: Text( @@ -55,4 +59,31 @@ class TodoItem extends StatelessWidget { ), ); } + + void removeTodo(BuildContext context, Todo todo) { + final todosServiceRM = RM.get(); + todosServiceRM.setState( + (s) async { + Scaffold.of(context).showSnackBar( + SnackBar( + key: ArchSampleKeys.snackbar, + duration: Duration(seconds: 2), + content: Text( + ArchSampleLocalizations.of(context).todoDeleted(todo.task), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + action: SnackBarAction( + label: ArchSampleLocalizations.of(context).undo, + onPressed: () { + todosServiceRM.setState((s) => s.addTodo(todo)); + }, + ), + ), + ); + return s.deleteTodo(todo); + }, + onError: ErrorHandler.showErrorSnackBar, + ); + } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart index 1ad2df63..f8ed4d3c 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart @@ -4,27 +4,35 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; +import 'package:states_rebuilder_sample/domain/entities/todo.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; -import 'package:states_rebuilder_sample/ui/common/enums.dart'; import 'package:todos_app_core/todos_app_core.dart'; import 'todo_item.dart'; class TodoList extends StatelessWidget { - TodoList(); + TodoList() : super(key: Key('__todoList__')) { + print(Key('__todoList__')); + } @override Widget build(BuildContext context) { return StateBuilder( observe: () => RM.get(), - tag: AppTab.todos, + watch: (todoServiceRM) => todoServiceRM.state.todos.length, builder: (context, todosServiceRM) { print('rebuild of todoList'); return ListView.builder( - key: ArchSampleKeys.todoList, + // key: ArchSampleKeys.todoList, itemCount: todosServiceRM.state.todos.length, itemBuilder: (BuildContext context, int index) { final todo = todosServiceRM.state.todos[index]; - return TodoItem(todo: todo); + return StateBuilder( + observe: () => RM.create(todo), + key: Key(todo.id), + builder: (context, todoRM) { + return TodoItem(todoRM: todoRM); + }, + ); }, ); }, diff --git a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart index 4dc77209..2e25f03b 100644 --- a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart +++ b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart @@ -3,40 +3,36 @@ import 'package:states_rebuilder/states_rebuilder.dart'; import '../../../domain/entities/todo.dart'; import '../../../service/todos_service.dart'; - import '../../exceptions/error_handler.dart'; class CheckFavoriteBox extends StatelessWidget { const CheckFavoriteBox({ Key key, - @required this.todo, + @required this.todoRM, }) : _key = key; final Key _key; - final Todo todo; - + final ReactiveModel todoRM; + Todo get todo => todoRM.value; @override Widget build(BuildContext context) { - return StateBuilder( - observe: () => RM.create(todo.complete), - builder: (context, completeRM) { - return Checkbox( - key: _key, - value: completeRM.value, - onChanged: (value) { - completeRM.value = value; - RM - .getFuture( - (t) => t.updateTodo( - todo.copyWith(complete: value), - ), - ) - .subscription - .onError( - (error) { - completeRM.value = !value; - ErrorHandler.showErrorSnackBar(context, error); - }, - ); + return Checkbox( + key: _key, + value: todo.complete, + onChanged: (value) { + final oldTodo = todo; + final newTodo = todo.copyWith( + complete: value, + ); + todoRM.value = newTodo; + RM + .getFuture( + (t) => t.updateTodo(newTodo), + ) + .subscription + .onError( + (error) { + todoRM.value = oldTodo; + ErrorHandler.showErrorSnackBar(context, error); }, ); }, From 17398506d9e2b9f733a36241d76ca85fd614b839 Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Wed, 22 Apr 2020 23:25:02 +0100 Subject: [PATCH 03/10] update stats --- .../lib/service/filtered_todos.dart | 22 ------------------- .../lib/service/stats_service.dart | 10 --------- .../ui/pages/home_screen/stats_counter.dart | 6 ++--- 3 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 states_rebuilder/lib/service/filtered_todos.dart delete mode 100644 states_rebuilder/lib/service/stats_service.dart diff --git a/states_rebuilder/lib/service/filtered_todos.dart b/states_rebuilder/lib/service/filtered_todos.dart deleted file mode 100644 index 444dba62..00000000 --- a/states_rebuilder/lib/service/filtered_todos.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/common/enums.dart'; - -class FilteredTodos { - final List _todos; - - VisibilityFilter activeFilter = VisibilityFilter.all; - - FilteredTodos(this._todos); - - List get todos { - return _todos.where((todo) { - if (activeFilter == VisibilityFilter.all) { - return true; - } else if (activeFilter == VisibilityFilter.active) { - return !todo.complete; - } else { - return todo.complete; - } - }).toList(); - } -} diff --git a/states_rebuilder/lib/service/stats_service.dart b/states_rebuilder/lib/service/stats_service.dart deleted file mode 100644 index f00f58a9..00000000 --- a/states_rebuilder/lib/service/stats_service.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:states_rebuilder_sample/domain/entities/todo.dart'; - -class Stats { - final List _todos; - - Stats(this._todos); - - int get numActive => _todos.where((todo) => !todo.complete).toList().length; - int get numCompleted => _todos.where((todo) => todo.complete).toList().length; -} diff --git a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart index 87795c6c..462fc330 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/service/stats_service.dart'; import 'package:states_rebuilder_sample/service/todos_service.dart'; import 'package:todos_app_core/todos_app_core.dart'; @@ -16,7 +15,6 @@ class StatsCounter extends StatelessWidget { return StateBuilder( observe: () => RM.get(), builder: (_, todosServiceRM) { - final stats = Stats(todosServiceRM.state.todos); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -31,7 +29,7 @@ class StatsCounter extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: 24.0), child: Text( - '${stats.numCompleted}', + '${todosServiceRM.state.numCompleted}', key: ArchSampleKeys.statsNumCompleted, style: Theme.of(context).textTheme.subhead, ), @@ -46,7 +44,7 @@ class StatsCounter extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: 24.0), child: Text( - '${stats.numActive}', + '${todosServiceRM.state.numActive}', key: ArchSampleKeys.statsNumActive, style: Theme.of(context).textTheme.subhead, ), From dae2e16bfa25e1babf7c7debfeb4729d43199d6f Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Sat, 25 Apr 2020 11:44:36 +0100 Subject: [PATCH 04/10] add comments --- states_rebuilder/README.sr.md | 274 ++++++++++++++++++ states_rebuilder/android/settings_aar.gradle | 1 + states_rebuilder/lib/app.dart | 3 + .../lib/data_source/todo_repository.dart | 5 +- .../lib/domain/entities/todo.dart | 3 +- .../exceptions/validation_exception.dart | 4 + .../lib/service/todos_service.dart | 14 + .../lib/ui/common/helper_methods.dart | 1 - .../lib/ui/exceptions/error_handler.dart | 20 ++ .../add_edit_screen.dart/add_edit_screen.dart | 16 +- .../ui/pages/detail_screen/detail_screen.dart | 4 +- .../home_screen/extra_actions_button.dart | 80 +++-- .../ui/pages/home_screen/filter_button.dart | 119 ++++---- .../lib/ui/pages/home_screen/home_screen.dart | 45 ++- .../lib/ui/pages/home_screen/todo_item.dart | 15 +- .../lib/ui/pages/home_screen/todo_list.dart | 49 +++- .../shared_widgets/check_favorite_box.dart | 12 +- 17 files changed, 545 insertions(+), 120 deletions(-) create mode 100644 states_rebuilder/README.sr.md create mode 100644 states_rebuilder/android/settings_aar.gradle delete mode 100644 states_rebuilder/lib/ui/common/helper_methods.dart diff --git a/states_rebuilder/README.sr.md b/states_rebuilder/README.sr.md new file mode 100644 index 00000000..7f8516a7 --- /dev/null +++ b/states_rebuilder/README.sr.md @@ -0,0 +1,274 @@ +# `states_rebuilder` + +[![pub package](https://img.shields.io/pub/v/states_rebuilder.svg)](https://pub.dev/packages/states_rebuilder) +[![CircleCI](https://circleci.com/gh/GIfatahTH/states_rebuilder.svg?style=svg)](https://circleci.com/gh/GIfatahTH/states_rebuilder) +[![codecov](https://codecov.io/gh/GIfatahTH/states_rebuilder/branch/master/graph/badge.svg)](https://codecov.io/gh/GIfatahTH/states_rebuilder) + + +A Flutter state management combined with dependency injection solution that allows : + * a 100% separation of User Interface (UI) representation from your logic classes + * an easy control on how your widgets rebuild to reflect the actual state of your application. +Model classes are simple vanilla dart classes without any need for inheritance, notification, streams or annotation and code generation. + + +`states_rebuilder` is built on the observer pattern for state management and on the service locator pattern for dependency injection. + +> **Intent of observer pattern** +>Define a one-to-many dependency between objects so that when one object changes state (observable object), all its dependents (observer objects) are notified and updated automatically. + +>**Intent of service locator pattern** +>The purpose of the Service Locator pattern is to return the service instances on demand. This is useful for decoupling service consumers from concrete classes. It uses a central container which on request returns the request instance. + +`states_rebuilder` state management solution is based on what is called the `ReactiveModel`. + +## What is a `ReactiveModel` +* It is an abstract class. +* In the context of observer model (or observable-observer couple), it is the observable part. observer widgets can subscribe to it so that they can be notified to rebuild. +* It exposes two getters to get the latest state (`state` , `value`) +* It offers two methods to mutate the state and notify observer widgets (`setState` and `setValue`). `state`-`stateState` is used for mutable objects, whereas `value`-`setValue` is more convenient for primitives and immutable objects. +* It exposes four getters to track its state status, (`isIdle`, `isWaiting`, `hasError`, and `hasData`). +* And many more ... + +In `states_rebuilder`, you write your business logic part with pure dart classes, without worrying on how the UI will interact with it and get notification. +`states_rebuilder` decorate your plain old dart class with a `ReactiveModel` model using the decorator pattern. + +> **Intent of decorator pattern** +> Adds new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. + +`ReactiveModel` decorates your plain old dart class with the following behaviors: + +The getters are : +* **state**: returns the registered raw singleton of the model. +* **value**: returns the registered raw singleton of the model. +* **connectionState** : It is of type `ConnectionState` (a Flutter defined enumeration). It takes three values: + 1- `ConnectionState.none`: Before executing any method of the model. + 2- `ConnectionState.waiting`: While waiting for the end of an asynchronous task. + 3- `ConnectionState.done`: After running a synchronous method or the end of a pending asynchronous task. +* **isIdle** : It's of bool type. it is true if `connectionState` is `ConnectionState.none` +* **isWaiting**: It's of bool type. it is true if `connectionState` is `ConnectionState.waiting` +* **hasError**: It's of bool type. it is true if the asynchronous task ends with an error. +* **error**: Is of type dynamic. It holds the thrown error. +* **hasData**: It is of type bool. It is true if the connectionState is done without any error. + +The fields are: +* **joinSingletonToNewData** : It is of type dynamic. It holds data sent from a new reactive instance to the reactive singleton. +* **subscription** : it is of type `StreamSubscription`. It is not null if you inject streams using `Inject.stream` constructor. It is used to control the injected stream. + +The methods are: +* **setState**: return a `Future`. It is used to mutate the state and notify listeners after state mutation. +* **setValue**: return a `Future` It is used to mutate the state and notify listeners after state mutation. It is equivalent to `setState` with the parameter `setValue` set to true. **setValue** is most suitable for immutables whereas **setState** is more convenient for mutable objects. +* **whenConnectionState** Exhaustively switch over all the possible statuses of [connectionState]. Used mostly to return [Widget]s. It has four required parameters (`onIdle`, `onWaiting`, `onData` and `onError`). +* **restToIdle** used to reset the async connection state to `isIdle`. +* **restToHasData** used to reset the async connection state to `hasData`. + +## Local and Global `ReactiveModel`: + +ReactiveModels are either local or global. +In local `ReactiveModel`, the creation of the `ReactiveModel` and subscription and notification are all limited in one place (widget). +In Global `ReactiveModel`, the `ReactiveModel` is created once, and it is available for subscription and notification throughout all the widget tree. + +### Local ReactiveModels + +Let's start by building the simplest counter app you have ever seen: + +```dart +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + //StateBuilder is used to subscribe to ReactiveModel + home: StateBuilder( + //Creating a local ReactiveModel that decorate an int value + //with initial value of 0 + observe: () => RM.create(0), + //The builder exposes the BuildContext and the instance of the created ReactiveModel + builder: (context, counterRM) { + return Scaffold( + appBar: AppBar(), + body: Center( + //use the value getter to get the latest state stored in the ReactiveModel + child: Text('${counterRM.value}'), + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + //get and increment the value of the counterRM. + //on mutating the state using the value setter the observers are automatically notified + onPressed: () => counterRM.value++, + ), + ); + }, + ), + ); + } +} +``` +* The Observer pattern: + * `StateBuilder` widget is one of four observer widgets offered by `states_rebuilder` to subscribe to a `ReactiveModel`. + * in `observer` parameter we created and subscribed to a local ReactiveModel the decorated an integer value with initial value of 0. + With states_rebuilder we can created ReactiveModels form primitives, pure dart classes, futures or streams: + ```dart + //create for objects + final fooRM = RM.create(Foo()); + //create from Future + final futureRM = RM.future(myFuture); + //create from stream + final streamRM = RM.stream(myStream); + //the above statement are shortcuts of the following + final fooRM = ReactiveModel.create(Foo()); + final futureRM = ReactiveModel.future(futureRM); + final streamRM = ReactiveModel.stream(streamRM); + ``` + * The `builder` parameter exposes the BuildContext and the the created instance of the `ReactiveModel`. + * To notify the subscribed widgets (we have one StateBuilder here), we just incremented the value of the counterRM + ```dart + onPressed: () => counterRM.value++, + ``` +* The decorator pattern: + * ReactiveModel decorates a primitive integer of 0 initial value and adds the following functionality: + * The 0 is an observable ReactiveModel and widget can subscribe to it. + * The value getter and setter to increment the 0 and notify observers + + +In the example above the rebuild is not optimized, because the whole Scaffold rebuild to only change a little text at the center fo the screen. + +Let's optimize the rebuild and introduce the concept of ReactiveModel keys (`RMKey`). + +```dart +class MyApp extends StatelessWidget { + //define a ReactiveModel model key of type int and optional initial value + final RMKey counterRMKey = RMKey(0); + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(), + body: Center( + child: StateBuilder( + observe: () => RM.create(0), + //associate this StateBuilder to the defined key + rmKey: counterRMKey, + builder: (context, counterRM) { + return Text('${counterRM.value}'); + }), + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + //increment the counter value and notify observers using the ReactiveModel key + onPressed: () => counterRMKey.value++, + ), + ), + ); + } +} +``` + +In a similar fashion on How global key are used in Flutter, we use ReactiveModel key to control a local ReactiveModel from outside its builder method of the widget where it is first created. +First,we instantiate a RMKey : +```dart +final RMKey counterRMKey = RMKey(0); +``` +Unlike Flutter global keys, you do not have to use StatefulWidget, because in states_rebuilder the state of RMKey is preserved even if the widget is rebuild. + +The next step is to associate the RMKey with a ReactiveModel, as done through the rmKey parameter of the StateBuilder widget. + +RMKey has all the functionality of the ReactiveModel is is associate with. You can call setState, setValue, get the state and its status. +```dart +onPressed: () => counterRMKey.value++, +``` +For more details on RMKey see here. + +As I said, ReactiveModel is a decorator over an object. Among the functionalities add, is the ability to track the asynchronous status of the state. +Let's see with an example: + +```dart +//create an immutable object +@immutable +class Counter { + final int count; + + Counter(this.count); + + Future increment() async { + //simulate a delay + await Future.delayed(Duration(seconds: 1)); + //simulate an error + if (Random().nextBool()) { + throw Exception('A Custom Message'); + } + return Counter(count + 1); + } +} + +class MyApp extends StatelessWidget { + //use a RMKey of type Counter + final counterRMKey = RMKey(); + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(), + body: Center( + //WhenRebuilder is a widget that is used to subscribe to an observable model. + //It Exhaustively goes throw the four possible status of the state and define the corresponding widget. + child: WhenRebuilder( + observe: () => RM.create(Counter(0)), + rmKey: counterRMKey, + //Before and action + onIdle: () => Text('Tap to increment the counter'), + //While waiting for and asynchronous task to end + onWaiting: () => Center( + child: CircularProgressIndicator(), + ), + //If the asynchronous task ends with error + onError: (error) => Center(child: Text('$error')), + //If data is available + onData: (counter) { + return Text('${counter.count}'); + }, + ), + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + //We use setValue to change the Counter state and notify observers + onPressed: () async => counterRMKey.setValue( + () => counterRMKey.value.increment(), + //set catchError to true + catchError: true, + ), + ), + ), + ); + } +} +``` +`WhenRebuilder` is the second observer widget after `StateBuilder`. It helps you to define the corresponding view for each of the four state status (`onIdle`, `onWaiting`, `onError` and `onData`). + +Notice that we used setValue method to mutate the state. If you use setValue, states_rebuilder automatically handle the asynchronous event for you. +* This is the rule, if you want to change the state synchronously, use the value setter: +```dart +counterRMKey.value = await counterRMKey.value.increment(); +``` +whereas if you want to let `states_rebuilder` handle the asynchronous events use `setValue`: +```dart +counterRMKey.setValue( + () => counterRMKey.value.increment(), + catchError: true, +), +``` + +Local `ReactiveModel` are the first choice when dealing with flutter's Input and selections widgets (Checkbox, Radio, switch,...), +here is an example of Slider : +```dart +StateBuilder( + observe: () => RM.create(0), + builder: (context, sliderRM) { + return Slider( + value: sliderRM.value, + onChanged: (value) { + sliderRM.value = value; + }, + ); + }, +), +``` + diff --git a/states_rebuilder/android/settings_aar.gradle b/states_rebuilder/android/settings_aar.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/states_rebuilder/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/states_rebuilder/lib/app.dart b/states_rebuilder/lib/app.dart index 6669b52e..ea6ceb57 100644 --- a/states_rebuilder/lib/app.dart +++ b/states_rebuilder/lib/app.dart @@ -14,7 +14,10 @@ class StatesRebuilderApp extends StatelessWidget { @override Widget build(BuildContext context) { + //uncomment this line to consol log and see the notification timeline RM.printActiveRM = true; + + // //Injecting the TodoService globally before MaterialApp widget. //It will be available throughout all the widget tree even after navigation. return Injector( diff --git a/states_rebuilder/lib/data_source/todo_repository.dart b/states_rebuilder/lib/data_source/todo_repository.dart index c09098c3..1b5f40b9 100644 --- a/states_rebuilder/lib/data_source/todo_repository.dart +++ b/states_rebuilder/lib/data_source/todo_repository.dart @@ -30,8 +30,9 @@ class StatesRebuilderTodosRepository implements ITodosRepository { Future saveTodos(List todos) async { try { var todosEntities = []; - // await Future.delayed(Duration(milliseconds: 500)); - // throw Exception(); + //// 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())); } diff --git a/states_rebuilder/lib/domain/entities/todo.dart b/states_rebuilder/lib/domain/entities/todo.dart index de2c1e95..1c68d755 100644 --- a/states_rebuilder/lib/domain/entities/todo.dart +++ b/states_rebuilder/lib/domain/entities/todo.dart @@ -2,8 +2,6 @@ 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. class Todo { String _id; String get id => _id; @@ -17,6 +15,7 @@ class Todo { factory Todo.fromJson(Map map) { return Todo( map['task'] as String, + id: map['id'] as String, note: map['note'] as String, complete: map['complete'] as bool, ); diff --git a/states_rebuilder/lib/domain/exceptions/validation_exception.dart b/states_rebuilder/lib/domain/exceptions/validation_exception.dart index e348fcd7..6f60b379 100644 --- a/states_rebuilder/lib/domain/exceptions/validation_exception.dart +++ b/states_rebuilder/lib/domain/exceptions/validation_exception.dart @@ -2,4 +2,8 @@ class ValidationException extends Error { final String message; ValidationException(this.message); + @override + String toString() { + return message; + } } diff --git a/states_rebuilder/lib/service/todos_service.dart b/states_rebuilder/lib/service/todos_service.dart index 8406550e..058078ed 100644 --- a/states_rebuilder/lib/service/todos_service.dart +++ b/states_rebuilder/lib/service/todos_service.dart @@ -1,4 +1,5 @@ import 'package:states_rebuilder_sample/domain/entities/todo.dart'; +import 'package:states_rebuilder_sample/service/exceptions/persistance_exception.dart'; import 'common/enums.dart'; import 'interfaces/i_todo_repository.dart'; @@ -36,6 +37,8 @@ class TodosService { //methods for CRUD Future loadTodos() async { + // await Future.delayed(Duration(seconds: 5)); + // throw PersistanceException('net work error'); return _todos = await _todoRepository.loadTodos(); } @@ -47,12 +50,20 @@ class TodosService { }); } + //on updating todos, states_rebuilder will instantly update the UI, + //Meanwhile the asynchronous method saveTodos is executed in the background. + //If an error occurs, the old state is returned and states_rebuilder update the UI + //to display the old state and shows a snackBar informing the user of the error. + Future updateTodo(Todo todo) async { final oldTodo = _todos.firstWhere((t) => t.id == todo.id); final index = _todos.indexOf(oldTodo); _todos[index] = todo; + //here states_rebuild will update the UI to display the new todos await _todoRepository.saveTodos(_todos).catchError((error) { + //on error return to the initial state _todos[index] = oldTodo; + //for states_rebuild to be informed of the error, we rethrow the error throw error; }); } @@ -61,6 +72,7 @@ class TodosService { final index = _todos.indexOf(todo); _todos.removeAt(index); return _todoRepository.saveTodos(_todos).catchError((error) { + //on error reinsert the deleted todo _todos.insert(index, todo); throw error; }); @@ -76,6 +88,7 @@ class TodosService { } return _todoRepository.saveTodos(_todos).catchError( (error) { + //on error return to the initial state _todos = beforeTodos; throw error; }, @@ -87,6 +100,7 @@ class TodosService { _todos.removeWhere((todo) => todo.complete); await _todoRepository.saveTodos(_todos).catchError( (error) { + //on error return to the initial state _todos = beforeTodos; throw error; }, diff --git a/states_rebuilder/lib/ui/common/helper_methods.dart b/states_rebuilder/lib/ui/common/helper_methods.dart deleted file mode 100644 index 8b137891..00000000 --- a/states_rebuilder/lib/ui/common/helper_methods.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/states_rebuilder/lib/ui/exceptions/error_handler.dart b/states_rebuilder/lib/ui/exceptions/error_handler.dart index 749a5982..39c260d4 100644 --- a/states_rebuilder/lib/ui/exceptions/error_handler.dart +++ b/states_rebuilder/lib/ui/exceptions/error_handler.dart @@ -32,4 +32,24 @@ class ErrorHandler { ), ); } + + static void showErrorDialog(BuildContext context, dynamic error) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: Colors.yellow, + ), + Text(ErrorHandler.getErrorMessage(error)), + ], + ), + ); + }, + ); + } } diff --git a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart index 67bd6944..38379457 100644 --- a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart +++ b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart @@ -86,7 +86,6 @@ class _AddEditPageState extends State { final form = formKey.currentState; if (form.validate()) { form.save(); - final todosServiceRM = RM.get(); if (isEditing) { final oldTodo = todo; final newTodo = todo.copyWith( @@ -95,16 +94,9 @@ class _AddEditPageState extends State { ); widget.todoRM.setState( (s) async { - await todosServiceRM.setState( - (s) { - widget.todoRM.value = newTodo; - Navigator.pop(context, newTodo); - return s.updateTodo(newTodo); - }, - onError: (context, error) { - throw error; - }, - ); + widget.todoRM.value = newTodo; + Navigator.pop(context, newTodo); + await IN.get().updateTodo(todo); }, watch: (todoRM) => widget.todoRM.hasError, onError: (context, error) { @@ -113,7 +105,7 @@ class _AddEditPageState extends State { }, ); } else { - todosServiceRM.setState( + RM.getSetState( (s) { Navigator.pop(context); return s.addTodo(Todo(_task, note: _note)); diff --git a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart index 66242d1b..498ad3c4 100644 --- a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart +++ b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart @@ -80,8 +80,8 @@ class DetailScreen extends StatelessWidget { tooltip: ArchSampleLocalizations.of(context).editTodo, child: Icon(Icons.edit), key: ArchSampleKeys.editTodoFab, - onPressed: () async { - await Navigator.of(context).push( + onPressed: () { + Navigator.of(context).push( MaterialPageRoute( builder: (context) { return AddEditPage( diff --git a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart index 6661f715..d95bc520 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart @@ -7,40 +7,56 @@ import 'package:todos_app_core/todos_app_core.dart'; class ExtraActionsButton extends StatelessWidget { ExtraActionsButton({Key key}) : super(key: key); - final todosServiceRM = RM.get(); @override Widget build(BuildContext context) { - return PopupMenuButton( - key: ArchSampleKeys.extraActionsButton, - onSelected: (action) { - todosServiceRM.setState( - (s) async { - if (action == ExtraAction.toggleAllComplete) { - return s.toggleAll(); - } else if (action == ExtraAction.clearCompleted) { - return s.clearCompleted(); - } - }, - onError: ErrorHandler.showErrorSnackBar, - ); - }, - itemBuilder: (BuildContext context) { - return >[ - PopupMenuItem( - key: ArchSampleKeys.toggleAll, - value: ExtraAction.toggleAllComplete, - child: Text(todosServiceRM.state.allComplete - ? ArchSampleLocalizations.of(context).markAllIncomplete - : ArchSampleLocalizations.of(context).markAllComplete), - ), - PopupMenuItem( - key: ArchSampleKeys.clearCompleted, - value: ExtraAction.clearCompleted, - child: Text(ArchSampleLocalizations.of(context).clearCompleted), - ), - ]; - }, - ); + //This is an example of local ReactiveModel + return StateBuilder( + //Create a reactiveModel of type ExtraAction and set its initialValue to ExtraAction.clearCompleted) + observe: () => RM.create(ExtraAction.clearCompleted), + builder: (context, extraActionRM) { + return PopupMenuButton( + key: ArchSampleKeys.extraActionsButton, + onSelected: (action) { + //first set the value to the new action + //See FilterButton where we use setValue there. + extraActionRM.value = action; + //then we use the getSetState to get the global registered ReactiveModel of TodosService + //and call setState method. + //There is one widget registered to the global ReactiveModel of TodosService, it is the + //StateBuilder in the TodoList Widget. + RM.getSetState( + (s) async { + if (action == ExtraAction.toggleAllComplete) { + return s.toggleAll(); + } else if (action == ExtraAction.clearCompleted) { + return s.clearCompleted(); + } + }, + //If and error happens, the global ReactiveModel of TodosService will notify listener widgets, + //so that these widgets will display the origin state before calling onSelected method + //and call showErrorSnackBar to show a snackBar + onError: ErrorHandler.showErrorSnackBar, + ); + }, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: ArchSampleKeys.toggleAll, + value: ExtraAction.toggleAllComplete, + child: Text(IN.get().allComplete + ? ArchSampleLocalizations.of(context).markAllIncomplete + : ArchSampleLocalizations.of(context).markAllComplete), + ), + PopupMenuItem( + key: ArchSampleKeys.clearCompleted, + value: ExtraAction.clearCompleted, + child: + Text(ArchSampleLocalizations.of(context).clearCompleted), + ), + ]; + }, + ); + }); } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart index e33abe9f..30c63d24 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart @@ -5,29 +5,24 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/service/common/enums.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; -import 'package:states_rebuilder_sample/ui/common/enums.dart'; import 'package:todos_app_core/todos_app_core.dart'; +import '../../../service/common/enums.dart'; +import '../../../service/todos_service.dart'; +import '../../common/enums.dart'; + class FilterButton extends StatelessWidget { + //Accept the activeTabRM defined in the HomePage const FilterButton({this.activeTabRM, Key key}) : super(key: key); final ReactiveModel activeTabRM; @override Widget build(BuildContext context) { - //context is used to register FilterButton as observer in todosServiceRM - final todosServiceRM = RM.get(context: context); - final defaultStyle = Theme.of(context).textTheme.body1; final activeStyle = Theme.of(context) .textTheme .body1 .copyWith(color: Theme.of(context).accentColor); final button = _Button( - onSelected: (filter) { - todosServiceRM.setState((s) => s.activeFilter = filter); - }, - activeFilter: todosServiceRM.state.activeFilter, activeStyle: activeStyle, defaultStyle: defaultStyle, ); @@ -48,56 +43,76 @@ class FilterButton extends StatelessWidget { class _Button extends StatelessWidget { const _Button({ Key key, - @required this.onSelected, - @required this.activeFilter, @required this.activeStyle, @required this.defaultStyle, }) : super(key: key); - final PopupMenuItemSelected onSelected; - final VisibilityFilter activeFilter; final TextStyle activeStyle; final TextStyle defaultStyle; @override Widget build(BuildContext context) { - return PopupMenuButton( - key: ArchSampleKeys.filterButton, - tooltip: ArchSampleLocalizations.of(context).filterTodos, - onSelected: onSelected, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - key: ArchSampleKeys.allFilter, - value: VisibilityFilter.all, - child: Text( - ArchSampleLocalizations.of(context).showAll, - style: activeFilter == VisibilityFilter.all - ? activeStyle - : defaultStyle, - ), - ), - PopupMenuItem( - key: ArchSampleKeys.activeFilter, - value: VisibilityFilter.active, - child: Text( - ArchSampleLocalizations.of(context).showActive, - style: activeFilter == VisibilityFilter.active - ? activeStyle - : defaultStyle, - ), - ), - PopupMenuItem( - key: ArchSampleKeys.completedFilter, - value: VisibilityFilter.completed, - child: Text( - ArchSampleLocalizations.of(context).showCompleted, - style: activeFilter == VisibilityFilter.completed - ? activeStyle - : defaultStyle, - ), - ), - ], - icon: Icon(Icons.filter_list), - ); + //This is an example of Local ReactiveModel + return StateBuilder( + //Create and subscribe to a ReactiveModel of type VisibilityFilter + observe: () => RM.create(VisibilityFilter.all), + builder: (context, activeFilterRM) { + return PopupMenuButton( + key: ArchSampleKeys.filterButton, + tooltip: ArchSampleLocalizations.of(context).filterTodos, + onSelected: (filter) { + //Compere this onSelected callBack with that of the ExtraActionsButton widget. + // + //In ExtraActionsButton, we did not use the setValue. + //Here we use the setValue (although we can use activeFilterRM.value = filter ). + + // + //The reason we use setValue is to minimize the rebuild process. + //If the use select the same option, the setValue method will not notify observers. + //and onData will not invoked. + activeFilterRM.setValue( + () => filter, + onData: (_, __) { + //get and setState of the global ReactiveModel TodosService + RM.getSetState((s) => s.activeFilter = filter); + }, + ); + }, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + key: ArchSampleKeys.allFilter, + value: VisibilityFilter.all, + child: Text( + ArchSampleLocalizations.of(context).showAll, + style: activeFilterRM.value == VisibilityFilter.all + ? activeStyle + : defaultStyle, + ), + ), + PopupMenuItem( + key: ArchSampleKeys.activeFilter, + value: VisibilityFilter.active, + child: Text( + ArchSampleLocalizations.of(context).showActive, + style: activeFilterRM.value == VisibilityFilter.active + ? activeStyle + : defaultStyle, + ), + ), + PopupMenuItem( + key: ArchSampleKeys.completedFilter, + value: VisibilityFilter.completed, + child: Text( + ArchSampleLocalizations.of(context).showCompleted, + style: activeFilterRM.value == VisibilityFilter.completed + ? activeStyle + : defaultStyle, + ), + ), + ], + icon: Icon(Icons.filter_list), + ); + }); } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart index 68e60df5..532278dc 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart @@ -5,31 +5,55 @@ import 'package:todos_app_core/todos_app_core.dart'; import '../../../localization.dart'; import '../../../service/todos_service.dart'; import '../../common/enums.dart'; +import '../../exceptions/error_handler.dart'; import 'extra_actions_button.dart'; import 'filter_button.dart'; import 'stats_counter.dart'; import 'todo_list.dart'; -// RM.printActiveRM = true; +//states_rebuilder is based on the concept fo ReactiveModels. +//ReactiveModels can be local or globe. class HomeScreen extends StatelessWidget { - //create a local ReactiveeModel to deal with the state of the tabs. + //Create a reactive model key to handle app tab navigation. + //ReactiveModel keys are used for local ReactiveModels final _activeTabRMKey = RMKey(AppTab.todos); @override Widget build(BuildContext context) { - print('rebuild HomeScreen'); return Scaffold( appBar: AppBar( title: Text(StatesRebuilderLocalizations.of(context).appTitle), actions: [ + //As FilterButton should respond to the active tab, the activeTab reactiveModel is + //injected throw the constructor using the ReactiveModels key, FilterButton(activeTabRM: _activeTabRMKey), ExtraActionsButton(), ], ), + //WhenRebuilderOr is one of four widget used by states_rebuilder to subscribe to observable ReactiveModels body: WhenRebuilderOr( + //subscribe this widget to many observables. + //This widget will rebuild when the loadTodos future method resolves and, + //when the state of the active AppTab is changed observeMany: [ - () => RM.getFuture((t) => t.loadTodos()), + //This means get the injected TodosService and create a ReactiveModel to + //handle the loadTodos async methods. + () => RM.get().future((s) => s.loadTodos()) + //using the cascade operator, + //Invoke the error callBack to handle the error + //In states_rebuild there are three level of error handling: + //1- global such as this one : (This is considered the default error handler). + //2- semi-global : for onError defined in setState and setValue methods. + // When defined it will override the gobble error handler. + //3- local-global, for onError defined in the StateBuilder and OnSetStateListener widgets. + // they override the global and semi-global error for the widget where it is defined + ..onError( + (error) => ErrorHandler.showErrorDialog(context, error), + ), + //Her we subscribe to the activeTab ReactiveModel key () => _activeTabRMKey, ], + //When any of the observed model is waiting for a future to resolve or stream to begin, + //this onWaiting method is called, onWaiting: () { return Center( child: CircularProgressIndicator( @@ -37,6 +61,8 @@ class HomeScreen extends StatelessWidget { ), ); }, + //WhenRebuilderOr has other optional callBacks (onData, onIdle, onError). + //the builder is the default one. builder: (context, _activeTabRM) { return _activeTabRM.value == AppTab.todos ? TodoList() @@ -51,16 +77,25 @@ class HomeScreen extends StatelessWidget { child: Icon(Icons.add), tooltip: ArchSampleLocalizations.of(context).addTodo, ), + //StateBuilder is the second of three widget used to subscribe to observables bottomNavigationBar: StateBuilder( + //Here we create a local ReactiveModel of type AppTab with default state of AppTab.todos) observe: () => RM.create(AppTab.todos), + //To control or use the value of this local ReactiveModel outside this Widget, + //we use key in the same fashion Flutter gobble key is used. + //Here we associated the already defined ReactiveModel key with this widget. rmKey: _activeTabRMKey, + //The builder method exposes the BuildContext and the ReactiveModel model of type defined in + // the generic type of the StateBuilder builder: (context, _activeTabRM) { return BottomNavigationBar( key: ArchSampleKeys.tabs, currentIndex: AppTab.values.indexOf(_activeTabRM.value), onTap: (index) { - //mutate the value of the private field _activeTab, + //mutate the value of the private field _activeTabRM, //observing widget will be notified to rebuild + //We have three observing widgets : this StateBuilder, the WhenRebuilderOr, + //ond the StateBuilder defined in the FilterButton widget _activeTabRM.value = AppTab.values[index]; }, items: AppTab.values.map( diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index fe68d2e8..7714bf15 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -15,6 +15,7 @@ import 'package:todos_app_core/todos_app_core.dart'; class TodoItem extends StatelessWidget { final ReactiveModel todoRM; Todo get todo => todoRM.value; + //Accept the todo ReactiveModel from the TodoList widget TodoItem({ Key key, @required this.todoRM, @@ -40,6 +41,8 @@ class TodoItem extends StatelessWidget { removeTodo(context, todo); } }, + //Because checkbox for favorite is use here and in the detailed screen, and the both share the same logic, + //we isolate the widget in a dedicated widget in the shared_widgets folder leading: CheckFavoriteBox( todoRM: todoRM, key: ArchSampleKeys.todoItemCheckbox(todo.id), @@ -61,6 +64,7 @@ class TodoItem extends StatelessWidget { } void removeTodo(BuildContext context, Todo todo) { + //get the global ReactiveModel, because we want to update the view of the list after removing a todo final todosServiceRM = RM.get(); todosServiceRM.setState( (s) async { @@ -76,13 +80,22 @@ class TodoItem extends StatelessWidget { action: SnackBarAction( label: ArchSampleLocalizations.of(context).undo, onPressed: () { + //another nested setState to voluntary add the todo back todosServiceRM.setState((s) => s.addTodo(todo)); }, ), ), ); - return s.deleteTodo(todo); + //await for the to do to delete from the persistent storage + await s.deleteTodo(todo); }, + //another watch, there are tow watch in states_rebuild: + // 1- This one, in setState, the notification will not be sent unless the watched parameters changes + // 2- The watch in StateBuilder which is more local, it prevent the watch StateBuilder from rebuilding + // even after a ReactiveModel sends a notification + watch: (todosService) => [todosService.todos.length], + //Handling the error. + //Error handling is centralized id the ErrorHandler class onError: ErrorHandler.showErrorSnackBar, ); } diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart index f8ed4d3c..69caf4e5 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart @@ -4,32 +4,63 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; import 'package:todos_app_core/todos_app_core.dart'; +import '../../../domain/entities/todo.dart'; +import '../../../service/todos_service.dart'; import 'todo_item.dart'; class TodoList extends StatelessWidget { - TodoList() : super(key: Key('__todoList__')) { - print(Key('__todoList__')); - } @override Widget build(BuildContext context) { return StateBuilder( + //As this is the main list of todos, and this list can be update from + //many widgets and screens (FilterButton, ExtraActionsButton, AddEditScreen, ..) + //We register this widget with the global injected ReactiveModel. + //any where in the widget tree if setState of TodosService is called this StatesRebuild + // will rebuild + //In states_rebuild global ReactiveModel is the model that can be invoked all across the widget tree + //and local ReactiveModel is a model that is meant to be called only locally in the widget where it is created observe: () => RM.get(), - watch: (todoServiceRM) => todoServiceRM.state.todos.length, + //The watch parameter is used to limit the rebuild of this StateBuilder. + //Even if TodosService emits a notification this widget will rebuild only if: + // 1- the length of the todo list is changed (add / remove a todo) + // 2- the active filter changes (From FilterButton widget) + // 3- The number of active todos changes (From ExtraActionsButton widget) + // + //Notice that if we edit one todo this StateBuilder will not update + watch: (todosServiceRM) => [ + todosServiceRM.state.todos.length, + todosServiceRM.state.activeFilter, + todosServiceRM.state.numActive, + ], builder: (context, todosServiceRM) { - print('rebuild of todoList'); + //The builder exposes the BuildContext and the ReactiveModel of TodosService return ListView.builder( - // key: ArchSampleKeys.todoList, + key: ArchSampleKeys.todoList, itemCount: todosServiceRM.state.todos.length, itemBuilder: (BuildContext context, int index) { final todo = todosServiceRM.state.todos[index]; + //This is important. + //As we want to limit the rebuild of the listView, we want to rebuild only the listTile + //of the todo that changed. + //For this reason, we Wrapped each todo with a StateBuilder and subscribe it to + //a ReactiveModel model created from the todo return StateBuilder( + //here we created a local ReactiveModel from one todo of the list observe: () => RM.create(todo), - key: Key(todo.id), + //This didUpdateWidget is used because if we mark all complete from the ExtraActionsButton, + //The listBuilder will update, but the StateBuilder for single todo will still have the old todo. + //In the didUpdateWidget, we check if the todo is modified, we set it and notify + //the StateBuilder to change + didUpdateWidget: (context, todoRM, oldWidget) { + if (todoRM.value != todo) { + //set and notify the observer this StateBuilder to rebuild + todoRM.value = todo; + } + }, builder: (context, todoRM) { + //render TodoItem and pass the local ReactiveModel through the constructor return TodoItem(todoRM: todoRM); }, ); diff --git a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart index 2e25f03b..23f3d37a 100644 --- a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart +++ b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart @@ -6,6 +6,7 @@ import '../../../service/todos_service.dart'; import '../../exceptions/error_handler.dart'; class CheckFavoriteBox extends StatelessWidget { + //accept the todo ReactiveModel from the TodoList or DetailedScreen widgets const CheckFavoriteBox({ Key key, @required this.todoRM, @@ -19,19 +20,26 @@ class CheckFavoriteBox extends StatelessWidget { key: _key, value: todo.complete, onChanged: (value) { + //hold the old todo final oldTodo = todo; final newTodo = todo.copyWith( complete: value, ); + //set todo to th new todo and notify observer (the todo tile) todoRM.value = newTodo; + + //Here we get the global ReactiveModel and from it we create a new Local ReactiveModel. + //The created ReactiveModel is based of the future of updateTodo method. RM - .getFuture( + .get() + .future( (t) => t.updateTodo(newTodo), ) - .subscription .onError( (error) { + //on Error set the todo value to the old value todoRM.value = oldTodo; + //show SnackBar to display the error message ErrorHandler.showErrorSnackBar(context, error); }, ); From 4e1f8390f831763d346d14a8d43627f4e5f52d0a Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Sun, 26 Apr 2020 12:39:35 +0100 Subject: [PATCH 05/10] refactor and add key --- states_rebuilder/README.sr.md | 901 +++++++++++++++++- .../lib/data_source/todo_repository.dart | 4 +- .../lib/ui/pages/home_screen/home_screen.dart | 13 +- .../lib/ui/pages/home_screen/todo_item.dart | 42 +- .../lib/ui/pages/home_screen/todo_list.dart | 5 + .../shared_widgets/check_favorite_box.dart | 9 +- 6 files changed, 930 insertions(+), 44 deletions(-) diff --git a/states_rebuilder/README.sr.md b/states_rebuilder/README.sr.md index 7f8516a7..4c268197 100644 --- a/states_rebuilder/README.sr.md +++ b/states_rebuilder/README.sr.md @@ -11,14 +11,11 @@ A Flutter state management combined with dependency injection solution that allo Model classes are simple vanilla dart classes without any need for inheritance, notification, streams or annotation and code generation. -`states_rebuilder` is built on the observer pattern for state management and on the service locator pattern for dependency injection. +`states_rebuilder` is built on the observer pattern for state management. > **Intent of observer pattern** >Define a one-to-many dependency between objects so that when one object changes state (observable object), all its dependents (observer objects) are notified and updated automatically. ->**Intent of service locator pattern** ->The purpose of the Service Locator pattern is to return the service instances on demand. This is useful for decoupling service consumers from concrete classes. It uses a central container which on request returns the request instance. - `states_rebuilder` state management solution is based on what is called the `ReactiveModel`. ## What is a `ReactiveModel` @@ -60,6 +57,9 @@ The methods are: * **whenConnectionState** Exhaustively switch over all the possible statuses of [connectionState]. Used mostly to return [Widget]s. It has four required parameters (`onIdle`, `onWaiting`, `onData` and `onError`). * **restToIdle** used to reset the async connection state to `isIdle`. * **restToHasData** used to reset the async connection state to `hasData`. +* **onError** a global error handler callback. +* **stream** listen to a stream from the state and notify observer widgets when it emits a data. +* **future** link to a future from the state and notify observers when it resolves. ## Local and Global `ReactiveModel`: @@ -209,7 +209,7 @@ class MyApp extends StatelessWidget { appBar: AppBar(), body: Center( //WhenRebuilder is a widget that is used to subscribe to an observable model. - //It Exhaustively goes throw the four possible status of the state and define the corresponding widget. + //It Exhaustively goes through the four possible status of the state and define the corresponding widget. child: WhenRebuilder( observe: () => RM.create(Counter(0)), rmKey: counterRMKey, @@ -255,6 +255,74 @@ counterRMKey.setValue( catchError: true, ), ``` +Later in this readme, I will talk about error handling with states_rebuilder. Here I only want to show you how easy error handling is with states_rebuilder. +Let's use the last example and imagine a real scenario where we want to persist the counter value in a database. We want to instantly display the incremented counter, and call an asynchronous method in the background to store the counter. If en error is thrown we want to go back to the last counter value and show a snackBar informing us about the error. + +```dart +@immutable +class Counter { + final int count; + + Counter(this.count); + + //Use stream generator instead of future + Stream increment() async* { + //Yield the new counter state + //states_rebuilder rebuilds the UI to display the new state + yield Counter(count + 1); + try { + await fetchSomeThing(); + } catch (e) { + //If an error, yield the old state + //states_rebuilder rebuilds the UI to display the old state + yield this; + //We have to throw the error to let states_rebuilder handle the error + throw e; + } + } +} + +class MyApp extends StatelessWidget { + final counterRMKey = RMKey(); + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(), + body: Center( + child: StateBuilder( + observe: () => RM.create(Counter(0)), + rmKey: counterRMKey, + builder: (context, counterRM) { + return Text('${counterRM.value.count}'); + }, + ), + ), + floatingActionButton: Builder(builder: (context) { + return FloatingActionButton( + child: Icon(Icons.add), + onPressed: () async { + //invoking the stream method from the ReactiveModel. + //states_rebuild subscribe to the stream and rebuild the observer widget whenever the stream emits + counterRMKey.stream((c) => c.increment()).onError( + (context, error) { + //side effects here + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text('$error.message'), + ), + ); + }, + ); + }, + ); + }), + ), + ); + } +} +``` +states_rebuild automatically subscribe to a stream and unsubscribe from it if the StateBuilder is disposed. Local `ReactiveModel` are the first choice when dealing with flutter's Input and selections widgets (Checkbox, Radio, switch,...), here is an example of Slider : @@ -272,3 +340,826 @@ StateBuilder( ), ``` +### Global ReactiveModel +There is no difference between local and global ReactiveModel expect of the span of the availability of the ReactiveModel in the widget tree. + +Global ReactiveModels are cross pages available model, they can be subscribed to in one part of the widget tree and can make send notification from the other side of the widget tree. + +Regardless of the effectiveness of the state management solution, it must rely on a reliable dependency injection system. + +`states_rebuilder` uses the service locator pattern, but using it in a way that makes it aware of the widget's lifecycle. This means that models are registered when needed in the `initState` method of a` StatefulWidget` and are unregistered when they are no longer needed in the `dispose` method. +Models once registered are available throughout the widget tree as long as the StatefulWidget that registered them is not disposed. The StatefulWidget used to register and unregister models is the `Injector` widget. + + +```dart +@immutable +class Counter { + final int count; + + Counter(this.count); + + Future increment() async { + await fetchSomeThing(); + return Counter(count + 1); + } +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + //We use Injector widget to provide a model to the widget tree + return Injector( + //inject a list of injectable + inject: [Inject(() => Counter(0))], + builder: (context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(), + body: Center( + child: WhenRebuilder( + //Consume the ReactiveModel of the injected Counter model + observe: () => RM.get(), + onIdle: () => Text('Tap to increment the counter'), + onWaiting: () => Center( + child: CircularProgressIndicator(), + ), + onError: (error) => Center(child: Text('$error')), + onData: (counter) { + return Text('${counter.count}'); + }, + ), + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + //The ReactiveModel of Counter is available any where is the widget tree. + onPressed: () async => RM.get().setValue( + () => RM.get().value.increment(), + catchError: true, + ), + ), + ), + ); + }, + ); + } +} +``` + +`states_rebuilder` uses the service locator pattern for injecting dependencies using the` injector` with is a StatefulWidget. + +>**Intent of service locator pattern** +>The purpose of the Service Locator pattern is to return the service instances on demand. This is useful for decoupling service consumers from concrete classes. It uses a central container which on request returns the request instance. + +To understand the principle of DI, it is important to consider the following principles: + +1. `Injector` adds classes to the container of the service locator in` initState` and deletes them in the `dispose` state. This means that if `Injector` is removed and re-inserted in the widget tree, a new singleton is registered for the injected models. If you injected streams or futures using `Inject.stream` or `Inject.future` and when the `Injector` is disposed and re-inserted, the streams and futures are disposed and reinitialized by `states_rebuilder` and do not fear of any memory leakage. + +2. You can use nested injectors. As the `Injector` is a simple StatefulWidget, it can be added anywhere in the widget tree. Typical use is to insert the `Injector` deeper in the widget tree just before using the injected classes. + +3. Injected classes are registered lazily. This means that they are not instantiated after injection until they are consumed for the first time. + +4. For each injected class, you can consume the registered instance using : + * `Injector.get` to get the registered raw vanilla dart instance. + ```dart + final T model = Injector.get() + //If the model is registered with custom name : + final T model = Injector.get(name:'customName'); + ``` + > As a shortcut you can use: + ```dart + //feature add in version 1.15.0 + final T model = IN.get() // IN stands for Injector + ``` + * or the ReactiveModel wrapper of the injected instance using: + ```dart + ReactiveModel modelRM = Injector.getAsReactive() + ``` + As a shortcut you can use: + ```dart + ReactiveModel modelRM = ReactiveModel(); + // Or simply (add in version 1.15.0) + ReactiveModel modelRM = RM.get(); + //I will use the latter, but the three are equivalent. + //In tutorials you expect to find any of these. + ``` + +5. Both the raw instance and the reactive instance are registered lazily, if you consume a class using only `Injector.get` and not` Injector.getAsReactive`, the reactive instance will never be instantiated. + +6. You can register classes with concrete types or abstract classes. + +7. You can register under different devolvement environments. This can be done by the help of `Inject.interface` named constructor and by setting the environment flavor `Injector.env` before calling the runApp method. see example below. + +That said: +> It is possible to register a class as a singleton, as a lazy singleton or as a factory simply by choosing where to insert it in the widget tree. + +* To save a singleton that will be available for all applications, insert the `Injector` widget in the top widget tree. It is possible to set the `isLazy` parameter to false to instantiate the injected class the time of injection. + +* To save a singleton that will be used by a branch of the widget tree, insert the `Injector` widget just above the branch. Each time you get into the branch, a singleton is registered and when you get out of it, the singleton will be destroyed. Making a profit of the behavior, you can clean injected models by defining a `dispose()` method inside them and set the parameter `disposeModels` of the `Injector`to true. + +It is important to understand that `states_rebuilder` caches two singletons. +* The raw singleton of the registered model, obtained using `Injector.get` method. +* The reactive singleton of the registered model (the raw model decorated with reactive environment), obtained using `Injector.getAsReactive`. + +With `states_rebuilder`, you can create, at any time, a new reactive instance, which is the same raw cashed singleton but decorated with a new reactive environment. + +To create a new reactive instance of an injected model use `StateBuilder` with generic type and without defining `models` property. +```dart +StateBuilder( + builder:(BuildContext context, ReactiveModel newReactiveModel){ + return YourWidget(); + } +) +``` + +You can also use `ReactiveModel.asNew([dynamic seed])` method: + +```dart +final reactiveModel = RM.get(); +final newReactiveModel = reactiveModel.asNew('mySeed'); + +// or directly + +final newReactiveModel = RM.get().asNew('mySeed'); +``` +By setting the seed parameter of the `asNew` method your are sure to get the same new reactive instance even after the widget rebuilds. + +The seed parameter is optional, and if not provided, `states_rebuilder` uses a default seed. + +>seed here has a similar meaning in random number generator. That is for the same seed we get the same new reactive instance. + + +**Important notes about reactive singleton and new reactive instances:** +* The reactive singleton and all the new reactive instances share the same raw singleton of the model, but each one decorates it with a different environment. +* Unlike the reactive singleton, new reactive instances are not cached so they are not accessible outside the widget in which they were instantiated. +* To make new reactive instances accessible throughout the widget tree, you have to register it with the `Injector` with a custom name: + +```dart +return Injector( +inject: [ + Inject( () => modelNewRM, name: 'newModel1'), +], +) +// Or +Injector( +inject: [ + Inject(() => Counter()), + Inject( + () => modelNewRM, + name: Enum.newModel1, + ), +], +``` +At later time if you want to consume the injected new reactive instance you use: + +```dart +// get the injected new reactive instance +ReactiveModel modelRM2 = RM.get(name : 'newModel1'); +//Or +ReactiveModel modelRM2 = RM.get(name : Enum.newModel1); +``` +* You can not get a new reactive model by using `getAsReactive(context: context)` with a defined context. It will throw because only the reactive singleton that can subscribe a widget using the context. + +* With the exception of the raw singleton they share, the reactive singleton and the new reactive instances have an independent reactive environment. That is when a particular reactive instance issues a notification with an error or with `ConnectionState.awaiting`, it will not affect other reactive environments. + +* `states_rebuilder` allows reactive instances to share their notification or state with the reactive singleton. This can be done by: +1- `notifyAllReactiveInstances` parameter of `setState` method. If true, each time a notification is issued by the reactive instance in which `setState` is called, all other reactive instances are notified. +2- `joinSingletonWith` parameter of `Inject` class. This time, new reactive instances, when issuing a notification, can clone their state to the reactive singleton. + * If `joinSingletonWith` is set to` JoinSingleton.withNewReactiveInstance`, this means that the reactive singleton will have the state of the new reactive instance issuing the notification. + * If `joinSingletonWith` is set to `JoinSingleton.withCombinedReactiveInstances`, this means that the singleton will hold a combined state of all the new reactive instances. + The combined state priority logic is: + Priority 1- The combined `ReactiveModel.hasError` is true if at least one of the new instances has an error + Priority 2- The combined `ReactiveModel.connectionState` is awaiting if at least one of the new instances is awaiting. + Priority 3- The combined `ReactiveModel.connectionState` is 'none' if at least one of the new instances is 'none'. + Priority 4- The combined `ReactiveModel.hasDate` is true if it has no error, it isn't awaiting and it is not in 'none' state. +* New reactive instances can send data to the reactive singleton. `joinSingletonToNewData` parameter of reactive environment hold the sending message. + + +# StateBuilder +In addition to its state management responsibility, `StateBuilder` offers a facade that facilitates the management of the widget's lifecycle. +```dart +StateBuilder( + onSetState: (BuildContext context, ReactiveModel exposedModel){ + /* + Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar,... + + It is similar to 'onSetState' parameter of the 'setState' method. The difference is that the `onSetState` of the 'setState' method is called once after executing the 'setState'. But this 'onSetState' is executed each time a notification is sent from one of the observable models this 'StateBuilder' is subscribing. + + You can use another nested setState here. + */ + }, + onRebuildState: (BuildContext context, ReactiveModel exposedModel){ + // The same as in onSetState but called after the end rebuild process. + }, + initState: (BuildContext context, ReactiveModel exposedModel){ + // Function to execute in initState of the state. + }, + dispose: (BuildContext context, ReactiveModel exposedModel){ + // Function to execute in dispose of the state. + }, + didChangeDependencies: (BuildContext context, ReactiveModel exposedModel){ + // Function to be executed when a dependency of state changes. + }, + didUpdateWidget: (BuildContext context, ReactiveModel exposedModel, StateBuilder oldWidget){ + // Called whenever the widget configuration changes. + }, + afterInitialBuild: (BuildContext context, ReactiveModel exposedModel){ + // Called after the widget is first inserted in the widget tree. + }, + afterRebuild: (BuildContext context, ReactiveModel exposedModel){ + /* + Called after each rebuild of the widget. + + The difference between onRebuildState and afterRebuild is that the latter is called each time the widget rebuilds, regardless of the origin of the rebuild. + Whereas onRebuildState is called only after rebuilds after notifications from the models to which the widget is subscribed. + */ + }, + // If true all model will be disposed when the widget is removed from the widget tree + disposeModels: true, + + // A list of observable objects to which this widget will subscribe. + models: [model1, model2] + + // Tag to be used to filer notification from observable classes. + // It can be any type of data, but when it is a List, + // this widget will be saved with many tags that are the items in the list. + tag: dynamic + + //Similar to the concept of global key in Flutter, with ReactiveModel key you + //can control the observer widget associated with it from outside. + ///[see here for more details](changelog/v-1.15.0.md) + rmKey : RMKey(); + + watch: (ReactiveModel exposedModel) { + //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes + }, + + builder: (BuildContext context, ReactiveModel exposedModel){ + /// [BuildContext] can be used as the default tag of this widget. + + /// The model is the first instance (model1) in the list of the [models] parameter. + /// If the parameter [models] is not provided then the model will be a new reactive instance. + }, + builderWithChild: (BuildContext context, ReactiveModel model, Widget child){ + ///Same as [builder], but can take a child widget exposedModel the part of the widget tree that we do not want to rebuild. + /// If both [builder] and [builderWithChild] are defined, it will throw. + + }, + + //The child widget that is used in [builderWithChild]. + child: MyWidget(), + +) +``` + +`states_rebuilder` uses the observer pattern. Notification can be filtered so that only widgets meeting the filtering criteria will be notified to rebuild. Filtration is done through tags. `StateBuilder` can register with one or more tags and `StatesRebuilder` can notify the observer widgets with a specific list of tags, so that only widgets registered with at least one of these tags will rebuild. + +```dart +class Counter extends StatesRebuilder { + int count = 0; + increment() { + count++; + //notifying the observers with 'Tag1' + rebuildStates(['Tag1']); + } +} + +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final Counter counterModel = Injector.get(context: context); + return Column( + children: [ + StateBuilder( // This StateBuilder will be notified + models: [counterModel], + tag: ['tag1, tag2'], + builder: (_, __) => Text('${counterModel.count}'), + ), + StateBuilder( + models: [counterModel], + tag: 'tag2', + builder: (_, __) => Text('${counterModel.count}'), + ), + StateBuilder( + models: [counterModel], + tag: MyEnumeration.tag1,// You can use enumeration + builder: (_, __) => Text('${counterModel.count}'), + ) + ], + ); + } +} +``` +# WhenRebuilder and WhenRebuilderOr +`states_rebuilder` offers the the `WhenRebuilder` widget which is a a combination of `StateBuilder` widget and `ReactiveModel.whenConnectionState` method. + +instead of verbosely: +```dart +Widget build(BuildContext context) { + return StateBuilder( + models: [RM.get()], + builder: (_, plugin1RM) { + return plugin1RM.whenConnectionState( + onIdle: () => Text('onIDle'), + onWaiting: () => CircularProgressIndicator(), + onError: (error) => Text('plugin one has an error $error'), + onData: (plugin1) => Text('plugin one is ready'), + ); + }, + ); +} +``` + +You use : + +```dart +@override +Widget build(BuildContext context) { + return WhenRebuilder( + models: [RM.get()], + onIdle: () => Text('onIdle'), + onWaiting: () => CircularProgressIndicator(), + onError: (error) => Text('plugin one has an error $error'), + onData: (plugin1) => Text('plugin one is ready'), + ); +} +``` + +Also with `WhenRebuilder` you can listen to a list of observable models and go throw all the possible combination statuses of the observable models: + +```dart +WhenRebuilder( + //List of observable models + models: [reactiveModel1, reactiveModel1], + onIdle: () { + //Will be invoked if : + //1- None of the observable models is in the error state, AND + //2- None of the observable models is in the waiting state, AND + //3- At least one of the observable models is in the idle state. + }, + onWaiting: () => { + //Will be invoked if : + //1- None of the observable models is in the error state, AND + //2- At least one of the observable models is in the waiting state. + }, + onError: (error) => { + //Will be invoked if : + //1- At least one of the observable models is in the error state. + + //The error parameter holds the thrown error of the model that has the error + }, + onData: (data) => { + //Will be invoked if : + //1- None of the observable models is in the error state, AND + //2- None of the observable models is in the waiting state, AND + //3- None of the observable models is in the idle state, AND + //4- All the observable models have data + + //The data parameter holds the state of the first model in the models' list. + }, + + // Tag to be used to filer notification from observable classes. + // It can be any type of data, but when it is a List, + // this widget will be saved with many tags that are the items in the list. + tag: dynamic + + initState: (BuildContext context, ReactiveModel exposedModel){ + // Function to execute in initState of the state. + }, + dispose: (BuildContext context, ReactiveModel exposedModel){ + // Function to execute in dispose of the state. + }, +), +``` +`WhenRebuilderOr` is just like `WhenRebuilder` but with optional `onIdle`, `onWaiting` and `onError` parameters and with required default `builder`.. + +# OnSetStateListener +`OnSetStateListener` is useful when you want to globally control the notification flow of a list of observable models and execute side effect calls. + +```dart +OnSetStateListener( + //List of observable models + models: [reactiveModel1, reactiveModel1], + onSetState: (context, reactiveModel1) { + _onSetState = 'onSetState'; + }, + onWaiting: (context, reactiveModel1) { + //Will be invoked if : + //At least one of the observable models is in the waiting state. + }, + onData: (context, reactiveModel1) { + //Will be invoked if : + //All of the observable models are in the hasData state. + }, + onError: (context, error) { + //Will be invoked if : + //At least one of the observable models is in the error state. + //The error parameter holds the thrown error of the model that has the error + }, + // Tag to be used to filer notification from observable classes. + // It can be any type of data, but when it is a List, + // this widget will be saved with many tags that are the items in the list. + tag: dynamic + + watch: (ReactiveModel exposedModel) { + //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes + }, + //Wether to execute [onSetState],[onWaiting], [onError], and/or [onData] in the [State.initState] + shouldOnInitState:false, + //It has a child parameter, not a builder parameter. + child: Container(), +) +``` +What makes `OnSetStateListener` different is the fact that is has a child parameter rather than a builder parameter. This means that the child parameter will not rebuild even if observable models send notifications. + +# Note on the exposedModel +`StateBuilder`, `WhenRebuilder` and `OnSetStateListener` observer widgets can be set to observer many observable reactive models. The exposed model instance depends on the generic parameter `T`. +ex: +```dart +//first case : generic model is ModelA +StateBuilder( + models:[modelA, modelB], + builder:(context, exposedModel){ + //exposedModel is an instance of ReactiveModel. + } +) +//second case : generic model is ModelB +StateBuilder( + models:[modelA, modelB], + builder:(context, exposedModel){ + //exposedModel is an instance of ReactiveModel. + } +) +//third case : generic model is dynamic +StateBuilder( + models:[modelA, modelB], + builder:(context, exposedModel){ + //exposedModel is dynamic and it will change over time to hold the instance of model that emits a notification. + + //If modelA emits a notification the exposedModel == ReactiveModel. + //Wheres if modelB emits a notification the exposedModel == ReactiveModel. + } +) +``` +# Injector +With `Injector` you can inject multiple dependent or independent models (BloCs, Services) at the same time. Also you can inject stream and future. +```dart +Injector( + inject: [ + //The order is not mandatory even for dependent models. + Inject(() => ModelA()), + Inject(() => ModelB()),//Generic type in inferred. + Inject(() => ModelC(Injector.get())),// Directly inject ModelA in ModelC constructor + Inject(() => ModelC(Injector.get())),// Type in inferred. + Inject(() => ModelD()),// Register with Interface type. + Inject({ //Inject through interface with environment flavor. + 'prod': ()=>ModelImplA(), + 'test': ()=>ModelImplB(), + }), // you have to set the `Inject.env = 'prod'` before `runApp` method + //You can inject streams and future and make them accessible to all the widget tree. + Inject.future(() => Future(), initialValue:0),// Register a future. + Inject.stream(() => Stream()),// Register a stream. + Inject(() => ModelD(),name:"customName"), // Use custom name + + //Inject and reinject with previous value provided. + Inject.previous((ModelA previous){ + return ModelA(id: previous.id); + }) + ], + //reinjectOn takes a list of StatesRebuilder models, if any of those models emits a notification all the injected model will be disposed and re-injected. + reinjectOn : [models] + // Whether to notify listeners of injected model using 'previous' constructor + shouldNotifyOnReinjectOn: true; + . + . +); +``` + +* Models are registered lazily by default. That is, they will not be instantiated until they are first used. To instantiate a particular model at the time of registration, you can set the `isLazy` variable of the class `Inject` to false. + +* `Injector` is a simple `StatefulWidget`, and models are registered in the `initState` and unregistered in the `dispose` state. (Registered means add to the service locator container). +If you wish to unregister and re-register the injected models after each rebuild you can set the key parameter to be `UniqueKey()`. +In a better alternative is to use the `reinjectOn` parameter which takes a list of `StatesRebuilder` models and unregister and re-register the injected models if any of the latter models emits a notification. + +* For more detailed on the use of `Inject.previous` and `reinject` and `shouldNotifyOnReinjectOn` [see more details here](changelog/v-1.15.0.md) + +* In addition to its injection responsibility, the `Injector` widget gives you a convenient facade to manage the life cycle of the widget as well as the application: + +```dart +Injector( + initState: (){ + // Function to execute in initState of the state. + }, + dispose: (){ + // Function to execute in dispose of the state. + }, + afterInitialBuild: (BuildContext context){ + // Called after the widget is first inserted in the widget tree. + }, + appLifeCycle: (AppLifecycleState state){ + /* + Function to track app life cycle state. It takes as parameter the AppLifeCycleState + In Android (onCreate, onPause, ...) and in IOS (didFinishLaunchingWithOptions, + applicationWillEnterForeground ..) + */ + }, + // If true all model will be disposed when the widget is removed from the widget tree + disposeModels: true, + . + . +); +``` + + +The `Injector.get` method searches for the registered singleton using the service locator pattern. For this reason, `BuildContext` is not required. The `BuildContext` is optional and it is useful if you want to subscribe to the widget that has the `BuildContext` to the obtained model. + +In the `HomePage` class of the example, we can remove `StateBuilder` and use the `BuildContext` to subscribe the widget. + +```dart +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + // The BuildContext of this widget is subscribed to the Counter class. + // Whenever the Counter class issues a notification, this widget will be rebuilt. + final Counter counterModel = Injector.get(context: context); + return Center( + child: Text('${counterModel.count}'), + ); + } +} +``` +Once the context is provided, `states_rebuilder` searches up in the widget tree to find the nearest `Injector` widget that has registered an `Inject` of the type provided and register the context (`Inject` class is associated with `InheritedWidget`). So be careful in case the `InheritedWidget` is not available, especially after navigation. + +To deal with such a situation, you can remove the `context` parameter and use the `StateBuilder` widget, or in case you want to keep using the `context` you can use the `reinject` parameter of the `Injector`. +```dart +Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Injector( + //reinject an already injected model + reinject: [counterModel], + builder: (context) { + return PageTwo(); + }, + ), + ), +); +``` + +# setState +`setState` is used whenever you want to trigger an event or an action that will mutate the state of the model and ends by issuing a notification to the observers. + +```dart +reactiveModel.setState( + (state) => state.increment(), + //Filter notification with tags + filterTags: ['Tag1', Enumeration.Tag2], + + //onData, trigger notification from new reactive models with the seeds in the list, + seeds:['seed1',Enumeration.Seed2 ], + + //set to true, you want to catch error, and not break the app. + catchError: true + + watch: (Counter counter) { + //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes + return counter.count; //if count value is not changed, no notification will be emitted. + }, + onSetState: (BuildContext context) { + /* + Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar , .. + + You can use another nested setState here. + */ + }, + onRebuildState: (BuildContext context) { + //The same as in onSetState but called after the end rebuild process. + }, + + onData: (BuildContext context, T model){ + //Callback to be executed if the reactive model has data. + } + + onError: (BuildContext context, dynamic error){ + //Callback to be executed if the reactive model throws an error. + //You do not have to set the parameter catchError to true. By defining onError parameter + //states_rebuilder catches the error by default. + } + + //When a notification is issued, whether to notify all reactive instances of the model + notifyAllReactiveInstances: true, + /* + If defined, when a new reactive instance issues a notification, it will change the state of the reactive singleton. + */ + joinSingleton: true, + + //message to be sent to the reactive singleton + dynamic joinSingletonToNewData, + + //Whether to set value or not + bool setValue:false, +), +``` + +# `value` getter and `setValue` method. +With `states_rebuilder` you can inject with primitive values or enums and make them reactive so that you can mutate their values and notify observer widgets that have subscribed to them. + +With `RM.create(T value)` you can create a `ReactiveModel` from a primitive value. The created `ReactiveModel` has the full power the other reactive models created using `Injector` have as en example you can wrap the primitive value with many reactive model instances. + +If you change the value (counterRM.value++), setValue will implicitly called to notify observers. + +`setValue` watches the change of the value and will not notify observers only if the value has changed. +`setValue` has `onSetState`, `onRebuildState`, `onError`, `catchError`, `filterTags` , `seeds` and `notifyAllReactiveInstances` the same way they are defined in `setState`: +```dart +reactiveModel.setValue( + ()=> newValue, + filterTags: ['Tag1', Enumeration.Tag2], + //onData, trigger notification from new reactive models with the seeds in the list, + seeds:['seed1',Enumeration.Seed2 ], + + onSetState: (BuildContext context) { + /* + Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar , .. + + You can use another nested setState here. + */ + }, + onRebuildState: (BuildContext context) { + //The same as in onSetState but called after the end rebuild process. + }, + + //set to true, you want to catch error, and not break the app. + catchError: true, + + onError: (BuildContext context, dynamic error){ + //Callback to be executed if the reactive model throws an error. + //You do not have to set the parameter catchError to true. By defining onError parameter + //states_rebuilder catches the error by default. + } + + //When a notification is issued, whether to notify all reactive instances of the model + notifyAllReactiveInstances: true, +), +``` + +# StateWithMixinBuilder +`StateWithMixinBuilder` is similar to `StateBuilder` and extends it by adding mixin (practical case is an animation), +```Dart +StateWithMixinBuilder( { + Key key, + dynamic tag, // you define the tag of the state. This is the first way + List models, // You give a list of the logic classes (BloC) you want this widget to listen to. + initState: (BuildContext, String,T) { + // for code to be executed in the initState of a StatefulWidget + }, + dispose (BuildContext, String,T) { + // for code to be executed in the dispose of a StatefulWidget + }, + didChangeDependencies: (BuildContext, String,T) { + // for code to be executed in the didChangeDependencies of a StatefulWidget + }, + didUpdateWidget: (BuildContext, String,StateBuilder, T){ + // for code to be executed in the didUpdateWidget of a StatefulWidget + }, + didChangeAppLifecycleState: (String, AppLifecycleState){ + // for code to be executed depending on the life cycle of the app (in Android : onResume, onPause ...). + }, + afterInitialBuild: (BuildContext, String,T){ + // for code to be executed after the widget is inserted in the widget tree. + }, + afterRebuild: (BuildContext, String) { + // for code to be executed after each rebuild of the widget. + }, + @required MixinWith mixinWith +}); +``` +Available mixins are: singleTickerProviderStateMixin, tickerProviderStateMixin, AutomaticKeepAliveClientMixin and WidgetsBindingObserver. + + +# Dependency Injection and development flavor +example of development flavor: +```dart +//abstract class +abstract class ConfigInterface { + String get appDisplayName; +} + +// first prod implementation +class ProdConfig implements ConfigInterface { + @override + String get appDisplayName => "Production App"; +} + +//second dev implementation +class DevConfig implements ConfigInterface { + @override + String get appDisplayName => "Dev App"; +} + +//Another abstract class +abstract class IDataBase{} + +// first prod implementation +class RealDataBase implements IDataBase {} + +// Second prod implementation +class FakeDataBase implements IDataBase {} + + +//enum for defined flavor +enum Flavor { Prod, Dev } + + +void main() { + //Choose yor environment flavor + Injector.env = Flavor.Prod; + + runApp( + Injector( + inject: [ + //Register against an interface with different flavor + Inject.interface({ + Flavor.Prod: ()=>ProdConfig(), + Flavor.Dev:()=>DevConfig(), + }), + Inject.interface({ + Flavor.Prod: ()=>RealDataBase(), + Flavor.Dev:()=>FakeDataBase(), + }), + ], + builder: (_){ + return MyApp( + appTitle: Injector.get().appDisplayName; + dataBaseRepo : Injector.get(), + ); + }, + ) + ); +} +``` + +# Widget unit texting + +The test is an important step in the daily life of a programmer; if not the most important part! + +With `Injector`, you can isolate any widget by mocking its dependencies and test it. + +Let's suppose we have the widget. +```dart +Import 'my_real_model.dart'; +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Injector( + inject: [Inject(() => MyRealModel())], + builder: (context) { + final myRealModelRM = RM.get(); + + // your widget + }, + ); + } +} +``` +The `MyApp` widget depends on` MyRealModel`. At first glance, this may seem to violate DI principles. How can we mock the "MyRealModel" which is not injected into the constructor of "MyApp"? + +To mock `MyRealModel` and test MyApp we set `Injector.enableTestMode` to true : + +```dart +testWidgets('Test MyApp class', (tester) async { + //set enableTestMode to true + Injector.enableTestMode = true; + + await tester.pumpWidget( + Injector( + //Inject the fake model and register it with the real model type + inject: [Inject(() => MyFakeModel())], + builder: (context) { + //In my MyApp, Injector.get or Inject.getAsReactive will return the fake model instance + return MyApp(); + }, + ), + ); + + //My test +}); + +//fake model implement real model +class MyFakeModel extends MyRealModel { + // fake implementation +} +``` +You can see a real test of the [counter_app_with_error]((states_rebuilder_package/example/test)) and [counter_app_with_refresh_indicator]((states_rebuilder_package/example/test)). + +# For further reading: + +> [List of article about `states_rebuilder`](https://medium.com/@meltft/states-rebuilder-and-animator-articles-4b178a09cdfa?source=friends_link&sk=7bef442f49254bfe7adc2c798395d9b9) + + +# Updates +To track the history of updates and feel the context when those updates are introduced, See the following links. + +* [1.15.0 update](changelog/v-1.15.0.md) : + * Example of using `Inject.previous` with `reinjectOn`; + * The add shortcuts (`IN.get()`, `RM.get()`, ...); + * Example of how to use states_rebuilder observer widgets instead of `FutureBuilder` and `StreamBuilder` Flutter widgets; + * The concept of ReactiveModel keys. diff --git a/states_rebuilder/lib/data_source/todo_repository.dart b/states_rebuilder/lib/data_source/todo_repository.dart index 1b5f40b9..591630d8 100644 --- a/states_rebuilder/lib/data_source/todo_repository.dart +++ b/states_rebuilder/lib/data_source/todo_repository.dart @@ -31,8 +31,8 @@ class StatesRebuilderTodosRepository implements ITodosRepository { try { var todosEntities = []; //// to simulate en error uncomment these lines. - await Future.delayed(Duration(milliseconds: 500)); - throw Exception(); + // await Future.delayed(Duration(milliseconds: 500)); + // throw Exception(); for (var todo in todos) { todosEntities.add(TodoEntity.fromJson(todo.toJson())); } diff --git a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart index 532278dc..3b6fe82a 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart @@ -12,10 +12,10 @@ import 'stats_counter.dart'; import 'todo_list.dart'; //states_rebuilder is based on the concept fo ReactiveModels. -//ReactiveModels can be local or globe. +//ReactiveModels can be local or global. class HomeScreen extends StatelessWidget { //Create a reactive model key to handle app tab navigation. - //ReactiveModel keys are used for local ReactiveModels + //ReactiveModel keys are used for local ReactiveModels (similar to Flutter global key) final _activeTabRMKey = RMKey(AppTab.todos); @override Widget build(BuildContext context) { @@ -35,9 +35,8 @@ class HomeScreen extends StatelessWidget { //This widget will rebuild when the loadTodos future method resolves and, //when the state of the active AppTab is changed observeMany: [ - //This means get the injected TodosService and create a ReactiveModel to - //handle the loadTodos async methods. - () => RM.get().future((s) => s.loadTodos()) + //Here we are creating a local ReactiveModel form the future of loadTodos method. + () => RM.future(IN.get().loadTodos()) //using the cascade operator, //Invoke the error callBack to handle the error //In states_rebuild there are three level of error handling: @@ -46,9 +45,7 @@ class HomeScreen extends StatelessWidget { // When defined it will override the gobble error handler. //3- local-global, for onError defined in the StateBuilder and OnSetStateListener widgets. // they override the global and semi-global error for the widget where it is defined - ..onError( - (error) => ErrorHandler.showErrorDialog(context, error), - ), + ..onError(ErrorHandler.showErrorDialog), //Her we subscribe to the activeTab ReactiveModel key () => _activeTabRMKey, ], diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index 7714bf15..3b3cea9a 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -67,28 +67,7 @@ class TodoItem extends StatelessWidget { //get the global ReactiveModel, because we want to update the view of the list after removing a todo final todosServiceRM = RM.get(); todosServiceRM.setState( - (s) async { - Scaffold.of(context).showSnackBar( - SnackBar( - key: ArchSampleKeys.snackbar, - duration: Duration(seconds: 2), - content: Text( - ArchSampleLocalizations.of(context).todoDeleted(todo.task), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - action: SnackBarAction( - label: ArchSampleLocalizations.of(context).undo, - onPressed: () { - //another nested setState to voluntary add the todo back - todosServiceRM.setState((s) => s.addTodo(todo)); - }, - ), - ), - ); - //await for the to do to delete from the persistent storage - await s.deleteTodo(todo); - }, + (s) => s.deleteTodo(todo), //another watch, there are tow watch in states_rebuild: // 1- This one, in setState, the notification will not be sent unless the watched parameters changes // 2- The watch in StateBuilder which is more local, it prevent the watch StateBuilder from rebuilding @@ -98,5 +77,24 @@ class TodoItem extends StatelessWidget { //Error handling is centralized id the ErrorHandler class onError: ErrorHandler.showErrorSnackBar, ); + + Scaffold.of(context).showSnackBar( + SnackBar( + key: ArchSampleKeys.snackbar, + duration: Duration(seconds: 2), + content: Text( + ArchSampleLocalizations.of(context).todoDeleted(todo.task), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + action: SnackBarAction( + label: ArchSampleLocalizations.of(context).undo, + onPressed: () { + //another nested setState to voluntary add the todo back + todosServiceRM.setState((s) => s.addTodo(todo)); + }, + ), + ), + ); } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart index 69caf4e5..452f91d7 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart @@ -47,19 +47,24 @@ class TodoList extends StatelessWidget { //For this reason, we Wrapped each todo with a StateBuilder and subscribe it to //a ReactiveModel model created from the todo return StateBuilder( + //Key here is very important because StateBuilder is a StatefulWidget (this is a Flutter concept) + key: Key(todo.id), //here we created a local ReactiveModel from one todo of the list observe: () => RM.create(todo), //This didUpdateWidget is used because if we mark all complete from the ExtraActionsButton, //The listBuilder will update, but the StateBuilder for single todo will still have the old todo. //In the didUpdateWidget, we check if the todo is modified, we set it and notify //the StateBuilder to change + didUpdateWidget: (context, todoRM, oldWidget) { if (todoRM.value != todo) { + print('didUpdateWidget (${todoRM.value} $todo'); //set and notify the observer this StateBuilder to rebuild todoRM.value = todo; } }, builder: (context, todoRM) { + print("builder"); //render TodoItem and pass the local ReactiveModel through the constructor return TodoItem(todoRM: todoRM); }, diff --git a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart index 23f3d37a..a76b0092 100644 --- a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart +++ b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart @@ -30,13 +30,8 @@ class CheckFavoriteBox extends StatelessWidget { //Here we get the global ReactiveModel and from it we create a new Local ReactiveModel. //The created ReactiveModel is based of the future of updateTodo method. - RM - .get() - .future( - (t) => t.updateTodo(newTodo), - ) - .onError( - (error) { + RM.future(IN.get().updateTodo(newTodo)).onError( + (context, error) { //on Error set the todo value to the old value todoRM.value = oldTodo; //show SnackBar to display the error message From 45e763e5869540e0e34c444c32eb46430b1a858b Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Thu, 30 Apr 2020 00:56:37 +0100 Subject: [PATCH 06/10] refactor to use version 1.15.0 and immutable state --- states_rebuilder/README.md | 119 ++++++++---- states_rebuilder/lib/app.dart | 23 ++- .../lib/domain/entities/todo.dart | 13 +- .../lib/service/todos_service.dart | 109 ----------- states_rebuilder/lib/service/todos_state.dart | 139 ++++++++++++++ .../add_edit_screen.dart/add_edit_screen.dart | 42 ++-- .../ui/pages/detail_screen/detail_screen.dart | 89 ++++++--- .../home_screen/extra_actions_button.dart | 35 ++-- .../ui/pages/home_screen/filter_button.dart | 8 +- .../lib/ui/pages/home_screen/home_screen.dart | 26 +-- .../ui/pages/home_screen/stats_counter.dart | 12 +- .../lib/ui/pages/home_screen/todo_item.dart | 54 +++--- .../lib/ui/pages/home_screen/todo_list.dart | 60 ++---- .../shared_widgets/check_favorite_box.dart | 44 ----- states_rebuilder/pubspec.yaml | 3 +- .../test/detailed_screen_test.dart | 85 +++++++++ states_rebuilder/test/fake_repository.dart | 5 +- states_rebuilder/test/home_screen_test.dart | 78 +------- states_rebuilder/test/todo_service_test.dart | 119 ------------ states_rebuilder/test/todo_state_test.dart | 179 ++++++++++++++++++ 20 files changed, 670 insertions(+), 572 deletions(-) delete mode 100644 states_rebuilder/lib/service/todos_service.dart create mode 100644 states_rebuilder/lib/service/todos_state.dart delete mode 100644 states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart create mode 100644 states_rebuilder/test/detailed_screen_test.dart delete mode 100644 states_rebuilder/test/todo_service_test.dart create mode 100644 states_rebuilder/test/todo_state_test.dart diff --git a/states_rebuilder/README.md b/states_rebuilder/README.md index 7b65b4b4..3dc85ec4 100644 --- a/states_rebuilder/README.md +++ b/states_rebuilder/README.md @@ -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()` method; -```dart -final Foo foo = Injector.get(); -``` -5. To get the registered singleton wrapped with a reactive environment, you use the static method -`Injector.getAsReactive()` method: -```dart -final ReactiveModel foo = Injector.getAsReactive(); -``` -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(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). \ No newline at end of file +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 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( + observe: () => RM.get(), + 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().value = + RM.get().value.copyWith(activeFilter: filter); + }, + ); + ``` + * for async future method: use future method. + ```dart + body: WhenRebuilderOr( + observeMany: [ + () => RM.get().asNew(HomeScreen) + ..future((t) => t.loadTodos()) + .onError(ErrorHandler.showErrorDialog), + () => _activeTabRMKey, + ] + + //... + ) + ``` + * for async stream method: use stream method. + ```dart + onSelected: (action) { + + RM.get() + .stream( + (action == ExtraAction.toggleAllComplete) + ? (t) => t.toggleAll() + : (t) => t.clearCompleted(), + ) + .onError(ErrorHandler.showErrorSnackBar); + } + ``` + diff --git a/states_rebuilder/lib/app.dart b/states_rebuilder/lib/app.dart index ea6ceb57..946cb5c1 100644 --- a/states_rebuilder/lib/app.dart +++ b/states_rebuilder/lib/app.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/service/interfaces/i_todo_repository.dart'; import 'package:todos_app_core/todos_app_core.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'; @@ -14,14 +16,23 @@ class StatesRebuilderApp extends StatelessWidget { @override Widget build(BuildContext context) { - //uncomment this line to consol log and see the notification timeline - RM.printActiveRM = true; + ////uncomment this line to consol log and see the notification timeline + //RM.printActiveRM = true; // - //Injecting the TodoService globally before MaterialApp widget. + //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, diff --git a/states_rebuilder/lib/domain/entities/todo.dart b/states_rebuilder/lib/domain/entities/todo.dart index 1c68d755..e493ae70 100644 --- a/states_rebuilder/lib/domain/entities/todo.dart +++ b/states_rebuilder/lib/domain/entities/todo.dart @@ -1,16 +1,17 @@ +import 'package:flutter/cupertino.dart'; import 'package:todos_app_core/todos_app_core.dart' as flutter_arch_sample_app; import '../exceptions/validation_exception.dart'; +@immutable class Todo { - String _id; - String get id => _id; + 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(); + : id = id ?? flutter_arch_sample_app.Uuid().generateV4(); factory Todo.fromJson(Map map) { return Todo( @@ -33,7 +34,7 @@ class Todo { } void _validation() { - if (_id == null) { + if (id == null) { // Custom defined error classes throw ValidationException('This todo has no ID!'); } @@ -61,7 +62,7 @@ class Todo { if (identical(this, o)) return true; return o is Todo && - o._id == _id && + o.id == id && o.complete == complete && o.note == note && o.task == task; @@ -69,7 +70,7 @@ class Todo { @override int get hashCode { - return _id.hashCode ^ complete.hashCode ^ note.hashCode ^ task.hashCode; + return id.hashCode ^ complete.hashCode ^ note.hashCode ^ task.hashCode; } @override diff --git a/states_rebuilder/lib/service/todos_service.dart b/states_rebuilder/lib/service/todos_service.dart deleted file mode 100644 index 058078ed..00000000 --- a/states_rebuilder/lib/service/todos_service.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/exceptions/persistance_exception.dart'; - -import 'common/enums.dart'; -import 'interfaces/i_todo_repository.dart'; - -//`TodosService` is a pure dart class that can be easily tested (see test folder). - -class TodosService { - //Constructor injection of the ITodoRepository abstract class. - TodosService(ITodosRepository todoRepository) - : _todoRepository = todoRepository; - - //private fields - final ITodosRepository _todoRepository; - List _todos = const []; - - //public field - VisibilityFilter activeFilter = VisibilityFilter.all; - - //getters - List get todos { - if (activeFilter == VisibilityFilter.active) { - return _activeTodos; - } - if (activeFilter == VisibilityFilter.completed) { - return _completedTodos; - } - return _todos; - } - - List get _completedTodos => _todos.where((t) => t.complete).toList(); - List get _activeTodos => _todos.where((t) => !t.complete).toList(); - int get numCompleted => _completedTodos.length; - int get numActive => _activeTodos.length; - bool get allComplete => _activeTodos.isEmpty; - - //methods for CRUD - Future loadTodos() async { - // await Future.delayed(Duration(seconds: 5)); - // throw PersistanceException('net work error'); - return _todos = await _todoRepository.loadTodos(); - } - - Future addTodo(Todo todo) async { - _todos.add(todo); - await _todoRepository.saveTodos(_todos).catchError((error) { - _todos.remove(todo); - throw error; - }); - } - - //on updating todos, states_rebuilder will instantly update the UI, - //Meanwhile the asynchronous method saveTodos is executed in the background. - //If an error occurs, the old state is returned and states_rebuilder update the UI - //to display the old state and shows a snackBar informing the user of the error. - - Future updateTodo(Todo todo) async { - final oldTodo = _todos.firstWhere((t) => t.id == todo.id); - final index = _todos.indexOf(oldTodo); - _todos[index] = todo; - //here states_rebuild will update the UI to display the new todos - await _todoRepository.saveTodos(_todos).catchError((error) { - //on error return to the initial state - _todos[index] = oldTodo; - //for states_rebuild to be informed of the error, we rethrow the error - throw error; - }); - } - - Future deleteTodo(Todo todo) async { - final index = _todos.indexOf(todo); - _todos.removeAt(index); - return _todoRepository.saveTodos(_todos).catchError((error) { - //on error reinsert the deleted todo - _todos.insert(index, todo); - throw error; - }); - } - - Future toggleAll() async { - final allComplete = _todos.every((todo) => todo.complete); - var beforeTodos = []; - - for (var i = 0; i < _todos.length; i++) { - beforeTodos.add(_todos[i]); - _todos[i] = _todos[i].copyWith(complete: !allComplete); - } - return _todoRepository.saveTodos(_todos).catchError( - (error) { - //on error return to the initial state - _todos = beforeTodos; - throw error; - }, - ); - } - - Future clearCompleted() async { - var beforeTodos = List.from(_todos); - _todos.removeWhere((todo) => todo.complete); - await _todoRepository.saveTodos(_todos).catchError( - (error) { - //on error return to the initial state - _todos = beforeTodos; - throw error; - }, - ); - } -} diff --git a/states_rebuilder/lib/service/todos_state.dart b/states_rebuilder/lib/service/todos_state.dart new file mode 100644 index 00000000..024f7674 --- /dev/null +++ b/states_rebuilder/lib/service/todos_state.dart @@ -0,0 +1,139 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:states_rebuilder_sample/domain/entities/todo.dart'; + +import 'common/enums.dart'; +import 'interfaces/i_todo_repository.dart'; + +//`TodosState` is a pure dart immutable class that can be easily tested (see test folder). +@immutable +class TodosState { + //Constructor injection of the ITodoRepository abstract class. + TodosState({ + ITodosRepository todoRepository, + List todos, + VisibilityFilter activeFilter, + }) : _todoRepository = todoRepository, + _todos = todos, + _activeFilter = activeFilter; + + //private fields + final ITodosRepository _todoRepository; + final List _todos; + final VisibilityFilter _activeFilter; + + //public getters + List get todos { + if (_activeFilter == VisibilityFilter.active) { + return _activeTodos; + } + if (_activeFilter == VisibilityFilter.completed) { + return _completedTodos; + } + return _todos; + } + + int get numCompleted => _completedTodos.length; + int get numActive => _activeTodos.length; + bool get allComplete => _activeTodos.isEmpty; + //private getter + List get _completedTodos => _todos.where((t) => t.complete).toList(); + List get _activeTodos => _todos.where((t) => !t.complete).toList(); + + //methods for CRUD + + //When we want to await for the future and display something in the screen, + //we use future. + Future loadTodos() async { + ////If you want to simulate loading failure uncomment theses lines + // await Future.delayed(Duration(seconds: 5)); + // throw PersistanceException('net work error'); + final _todos = await _todoRepository.loadTodos(); + return copyWith( + todos: _todos, + activeFilter: VisibilityFilter.all, + ); + } + + //We use stream generator when we want to instantly display the update, and execute the the saveTodos in the background, + //and if the saveTodos fails we want to display the old state and a snackbar containing the error message + Stream addTodo(Todo todo) async* { + final newTodos = List.from(_todos)..add(todo); + //_saveTodos is common to all crud operation + yield* _saveTodos(newTodos); + } + + Stream updateTodo(Todo todo) async* { + final newTodos = _todos.map((t) => t.id == todo.id ? todo : t).toList(); + yield* _saveTodos(newTodos); + } + + Stream deleteTodo(Todo todo) async* { + final newTodos = List.from(_todos)..remove(todo); + yield* _saveTodos(newTodos); + } + + Stream toggleAll() async* { + final newTodos = _todos + .map( + (t) => t.copyWith(complete: !allComplete), + ) + .toList(); + yield* _saveTodos(newTodos); + } + + Stream clearCompleted() async* { + final newTodos = List.from(_todos) + ..removeWhere( + (t) => t.complete, + ); + yield* _saveTodos(newTodos); + } + + Stream _saveTodos(List newTodos) async* { + //Yield the new state, and states_rebuilder will rebuild observer widgets + yield copyWith( + todos: newTodos, + ); + try { + await _todoRepository.saveTodos(newTodos); + } catch (e) { + //on error yield the old state, states_rebuilder will rebuild the UI to display the old state + yield this; + //rethrow the error so that states_rebuilder can display the snackbar containing the error message + rethrow; + } + } + + TodosState copyWith({ + ITodosRepository todoRepository, + List todos, + VisibilityFilter activeFilter, + }) { + final filter = todos?.isEmpty == true ? VisibilityFilter.all : activeFilter; + return TodosState( + todoRepository: todoRepository ?? _todoRepository, + todos: todos ?? _todos, + activeFilter: filter ?? _activeFilter, + ); + } + + @override + String toString() => + 'TodosState(_todoRepository: $_todoRepository, _todos: $_todos, activeFilter: $_activeFilter)'; + + @override + bool operator ==(Object o) { + if (identical(this, o)) return true; + + return o is TodosState && + o._todoRepository == _todoRepository && + listEquals(o._todos, _todos) && + o._activeFilter == _activeFilter; + } + + @override + int get hashCode => + _todoRepository.hashCode ^ _todos.hashCode ^ _activeFilter.hashCode; +} diff --git a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart index 38379457..c3525f1f 100644 --- a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart +++ b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart @@ -8,16 +8,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/service/todos_state.dart'; import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:todos_app_core/todos_app_core.dart'; class AddEditPage extends StatefulWidget { - final ReactiveModel todoRM; + final Todo todo; AddEditPage({ Key key, - this.todoRM, + this.todo, }) : super(key: key ?? ArchSampleKeys.addTodoScreen); @override @@ -29,8 +29,7 @@ class _AddEditPageState extends State { // Here we use a StatefulWidget to hold local fields _task and _note String _task; String _note; - bool get isEditing => widget.todoRM != null; - Todo get todo => widget.todoRM?.value; + bool get isEditing => widget.todo != null; @override Widget build(BuildContext context) { return Scaffold( @@ -50,7 +49,7 @@ class _AddEditPageState extends State { child: ListView( children: [ TextFormField( - initialValue: todo != null ? todo.task : '', + initialValue: widget.todo != null ? widget.todo.task : '', key: ArchSampleKeys.taskField, autofocus: isEditing ? false : true, style: Theme.of(context).textTheme.headline, @@ -62,7 +61,7 @@ class _AddEditPageState extends State { onSaved: (value) => _task = value, ), TextFormField( - initialValue: todo != null ? todo.note : '', + initialValue: widget.todo != null ? widget.todo.note : '', key: ArchSampleKeys.noteField, maxLines: 10, style: Theme.of(context).textTheme.subhead, @@ -87,31 +86,18 @@ class _AddEditPageState extends State { if (form.validate()) { form.save(); if (isEditing) { - final oldTodo = todo; - final newTodo = todo.copyWith( + final newTodo = widget.todo.copyWith( task: _task, note: _note, ); - widget.todoRM.setState( - (s) async { - widget.todoRM.value = newTodo; - Navigator.pop(context, newTodo); - await IN.get().updateTodo(todo); - }, - watch: (todoRM) => widget.todoRM.hasError, - onError: (context, error) { - widget.todoRM.value = oldTodo; - ErrorHandler.showErrorSnackBar(context, error); - }, - ); + Navigator.pop(context, newTodo); } else { - RM.getSetState( - (s) { - Navigator.pop(context); - return s.addTodo(Todo(_task, note: _note)); - }, - onError: ErrorHandler.showErrorSnackBar, - ); + Navigator.pop(context); + + RM + .get() + .stream((t) => t.addTodo(Todo(_task, note: _note))) + .onError(ErrorHandler.showErrorSnackBar); } } }, diff --git a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart index 498ad3c4..94cd626f 100644 --- a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart +++ b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart @@ -4,16 +4,17 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; - import 'package:states_rebuilder_sample/domain/entities/todo.dart'; +import 'package:states_rebuilder_sample/service/todos_state.dart'; +import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:states_rebuilder_sample/ui/pages/add_edit_screen.dart/add_edit_screen.dart'; -import 'package:states_rebuilder_sample/ui/pages/shared_widgets/check_favorite_box.dart'; import 'package:todos_app_core/todos_app_core.dart'; class DetailScreen extends StatelessWidget { - DetailScreen(this.todoRM) : super(key: ArchSampleKeys.todoDetailsScreen); - final ReactiveModel todoRM; - Todo get todo => todoRM.value; + DetailScreen(this.todo) : super(key: ArchSampleKeys.todoDetailsScreen); + final Todo todo; + final todoRMKey = RMKey(); + @override Widget build(BuildContext context) { return Scaffold( @@ -34,19 +35,27 @@ class DetailScreen extends StatelessWidget { padding: EdgeInsets.all(16.0), child: ListView( children: [ - StateBuilder( - observe: () => todoRM, - builder: (_, __) { + StateBuilder( + //create a local ReactiveModel for the todo + observe: () => RM.create(todo), + //associate ti with todoRMKey + rmKey: todoRMKey, + builder: (context, todosStateRM) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.only(right: 8.0), - child: CheckFavoriteBox( - todoRM: todoRM, - key: ArchSampleKeys.detailsTodoItemCheckbox, - ), - ), + padding: EdgeInsets.only(right: 8.0), + child: Checkbox( + key: ArchSampleKeys.detailsTodoItemCheckbox, + value: todosStateRM.value.complete, + onChanged: (value) { + final newTodo = todosStateRM.value.copyWith( + complete: value, + ); + _updateTodo(context, newTodo); + }, + )), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -57,13 +66,13 @@ class DetailScreen extends StatelessWidget { bottom: 16.0, ), child: Text( - todo.task, + todosStateRM.value.task, key: ArchSampleKeys.detailsTodoItemTask, style: Theme.of(context).textTheme.headline, ), ), Text( - todo.note, + todosStateRM.value.note, key: ArchSampleKeys.detailsTodoItemNote, style: Theme.of(context).textTheme.subhead, ) @@ -76,23 +85,43 @@ class DetailScreen extends StatelessWidget { ], ), ), - floatingActionButton: FloatingActionButton( - tooltip: ArchSampleLocalizations.of(context).editTodo, - child: Icon(Icons.edit), - key: ArchSampleKeys.editTodoFab, - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return AddEditPage( - key: ArchSampleKeys.editTodoScreen, - todoRM: todoRM, - ); - }, - ), + floatingActionButton: Builder( + builder: (context) { + return FloatingActionButton( + tooltip: ArchSampleLocalizations.of(context).editTodo, + child: Icon(Icons.edit), + key: ArchSampleKeys.editTodoFab, + onPressed: () async { + final newTodo = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return AddEditPage( + key: ArchSampleKeys.editTodoScreen, + todo: todoRMKey.value, + ); + }, + ), + ); + if (newTodo == null) { + return; + } + _updateTodo(context, newTodo); + }, ); }, ), ); } + + void _updateTodo(BuildContext context, Todo newTodo) { + final oldTodo = todoRMKey.value; + todoRMKey.value = newTodo; + RM + .get() + .stream((t) => t.updateTodo(newTodo)) + .onError((ctx, error) { + todoRMKey.value = oldTodo; + ErrorHandler.showErrorSnackBar(context, error); + }); + } } diff --git a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart index d95bc520..7020c257 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; -import 'package:states_rebuilder_sample/ui/common/enums.dart'; -import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:todos_app_core/todos_app_core.dart'; +import '../../../service/todos_state.dart'; +import '../../common/enums.dart'; +import '../../exceptions/error_handler.dart'; + class ExtraActionsButton extends StatelessWidget { ExtraActionsButton({Key key}) : super(key: key); @@ -21,30 +22,22 @@ class ExtraActionsButton extends StatelessWidget { //first set the value to the new action //See FilterButton where we use setValue there. extraActionRM.value = action; - //then we use the getSetState to get the global registered ReactiveModel of TodosService - //and call setState method. - //There is one widget registered to the global ReactiveModel of TodosService, it is the - //StateBuilder in the TodoList Widget. - RM.getSetState( - (s) async { - if (action == ExtraAction.toggleAllComplete) { - return s.toggleAll(); - } else if (action == ExtraAction.clearCompleted) { - return s.clearCompleted(); - } - }, - //If and error happens, the global ReactiveModel of TodosService will notify listener widgets, - //so that these widgets will display the origin state before calling onSelected method - //and call showErrorSnackBar to show a snackBar - onError: ErrorHandler.showErrorSnackBar, - ); + + RM + .get() + .stream( + (action == ExtraAction.toggleAllComplete) + ? (t) => t.toggleAll() + : (t) => t.clearCompleted(), + ) + .onError(ErrorHandler.showErrorSnackBar); }, itemBuilder: (BuildContext context) { return >[ PopupMenuItem( key: ArchSampleKeys.toggleAll, value: ExtraAction.toggleAllComplete, - child: Text(IN.get().allComplete + child: Text(IN.get().allComplete ? ArchSampleLocalizations.of(context).markAllIncomplete : ArchSampleLocalizations.of(context).markAllComplete), ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart index 30c63d24..ce1afebc 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart @@ -8,7 +8,7 @@ import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:todos_app_core/todos_app_core.dart'; import '../../../service/common/enums.dart'; -import '../../../service/todos_service.dart'; +import '../../../service/todos_state.dart'; import '../../common/enums.dart'; class FilterButton extends StatelessWidget { @@ -28,6 +28,7 @@ class FilterButton extends StatelessWidget { ); return StateBuilder( + //register to activeTabRM observe: () => activeTabRM, builder: (context, activeTabRM) { final _isActive = activeTabRM.value == AppTab.todos; @@ -73,8 +74,9 @@ class _Button extends StatelessWidget { activeFilterRM.setValue( () => filter, onData: (_, __) { - //get and setState of the global ReactiveModel TodosService - RM.getSetState((s) => s.activeFilter = filter); + //get and set the value of the global ReactiveModel TodosStore + RM.get().value = + RM.get().value.copyWith(activeFilter: filter); }, ); }, diff --git a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart index 3b6fe82a..8a29f229 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart @@ -3,7 +3,7 @@ import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:todos_app_core/todos_app_core.dart'; import '../../../localization.dart'; -import '../../../service/todos_service.dart'; +import '../../../service/todos_state.dart'; import '../../common/enums.dart'; import '../../exceptions/error_handler.dart'; import 'extra_actions_button.dart'; @@ -35,17 +35,19 @@ class HomeScreen extends StatelessWidget { //This widget will rebuild when the loadTodos future method resolves and, //when the state of the active AppTab is changed observeMany: [ - //Here we are creating a local ReactiveModel form the future of loadTodos method. - () => RM.future(IN.get().loadTodos()) - //using the cascade operator, - //Invoke the error callBack to handle the error - //In states_rebuild there are three level of error handling: - //1- global such as this one : (This is considered the default error handler). - //2- semi-global : for onError defined in setState and setValue methods. - // When defined it will override the gobble error handler. - //3- local-global, for onError defined in the StateBuilder and OnSetStateListener widgets. - // they override the global and semi-global error for the widget where it is defined - ..onError(ErrorHandler.showErrorDialog), + //Here get a new reactiveModel of the injected TodosStore + //we use the HomeScreen seed so that if other pages emits a notification this widget will not be notified + () => RM.get().asNew(HomeScreen) + //using the cascade operator, we call the todosLoad method informing states_rebuilder that is is a future + ..future((t) => t.loadTodos()) + //Invoke the error callBack to handle the error + //In states_rebuild there are three level of error handling: + //1- global such as this one : (This is considered the default error handler). + //2- semi-global : for onError defined in setState and setValue methods. + // When defined it will override the gobble error handler. + //3- local-global, for onError defined in the StateBuilder and OnSetStateListener widgets. + // they override the global and semi-global error for the widget where it is defined + .onError(ErrorHandler.showErrorDialog), //Her we subscribe to the activeTab ReactiveModel key () => _activeTabRMKey, ], diff --git a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart index 462fc330..4d0dbbc3 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/service/todos_state.dart'; import 'package:todos_app_core/todos_app_core.dart'; class StatsCounter extends StatelessWidget { @@ -12,9 +12,9 @@ class StatsCounter extends StatelessWidget { @override Widget build(BuildContext context) { - return StateBuilder( - observe: () => RM.get(), - builder: (_, todosServiceRM) { + return StateBuilder( + observe: () => RM.get(), + builder: (_, todosStateRM) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -29,7 +29,7 @@ class StatsCounter extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: 24.0), child: Text( - '${todosServiceRM.state.numCompleted}', + '${todosStateRM.value.numCompleted}', key: ArchSampleKeys.statsNumCompleted, style: Theme.of(context).textTheme.subhead, ), @@ -44,7 +44,7 @@ class StatsCounter extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: 24.0), child: Text( - '${todosServiceRM.state.numActive}', + '${todosStateRM.value.numActive}', key: ArchSampleKeys.statsNumActive, style: Theme.of(context).textTheme.subhead, ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index 3b3cea9a..5556f163 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -4,21 +4,19 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; - import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; +import 'package:states_rebuilder_sample/service/todos_state.dart'; import 'package:states_rebuilder_sample/ui/exceptions/error_handler.dart'; import 'package:states_rebuilder_sample/ui/pages/detail_screen/detail_screen.dart'; -import 'package:states_rebuilder_sample/ui/pages/shared_widgets/check_favorite_box.dart'; import 'package:todos_app_core/todos_app_core.dart'; class TodoItem extends StatelessWidget { - final ReactiveModel todoRM; - Todo get todo => todoRM.value; - //Accept the todo ReactiveModel from the TodoList widget + final Todo todo; + + //Accept the todo from the TodoList widget TodoItem({ Key key, - @required this.todoRM, + @required this.todo, }) : super(key: key); @override @@ -33,7 +31,7 @@ class TodoItem extends StatelessWidget { final shouldDelete = await Navigator.of(context).push( MaterialPageRoute( builder: (_) { - return DetailScreen(todoRM); + return DetailScreen(todo); }, ), ); @@ -41,11 +39,20 @@ class TodoItem extends StatelessWidget { removeTodo(context, todo); } }, - //Because checkbox for favorite is use here and in the detailed screen, and the both share the same logic, - //we isolate the widget in a dedicated widget in the shared_widgets folder - leading: CheckFavoriteBox( - todoRM: todoRM, + leading: Checkbox( key: ArchSampleKeys.todoItemCheckbox(todo.id), + value: todo.complete, + onChanged: (value) { + final newTodo = todo.copyWith( + complete: value, + ); + //Here we get the global ReactiveModel, and use the stream method to call the updateTodo. + //states_rebuilder will subscribe to this stream and notify observer widgets to rebuild when data is emitted. + RM.get().stream((t) => t.updateTodo(newTodo)).onError( + //on Error we want to display a snackbar + ErrorHandler.showErrorSnackBar, + ); + }, ), title: Text( todo.task, @@ -65,18 +72,11 @@ class TodoItem extends StatelessWidget { void removeTodo(BuildContext context, Todo todo) { //get the global ReactiveModel, because we want to update the view of the list after removing a todo - final todosServiceRM = RM.get(); - todosServiceRM.setState( - (s) => s.deleteTodo(todo), - //another watch, there are tow watch in states_rebuild: - // 1- This one, in setState, the notification will not be sent unless the watched parameters changes - // 2- The watch in StateBuilder which is more local, it prevent the watch StateBuilder from rebuilding - // even after a ReactiveModel sends a notification - watch: (todosService) => [todosService.todos.length], - //Handling the error. - //Error handling is centralized id the ErrorHandler class - onError: ErrorHandler.showErrorSnackBar, - ); + final todosStateRM = RM.get(); + + todosStateRM + .stream((t) => t.deleteTodo(todo)) + .onError(ErrorHandler.showErrorSnackBar); Scaffold.of(context).showSnackBar( SnackBar( @@ -90,8 +90,10 @@ class TodoItem extends StatelessWidget { action: SnackBarAction( label: ArchSampleLocalizations.of(context).undo, onPressed: () { - //another nested setState to voluntary add the todo back - todosServiceRM.setState((s) => s.addTodo(todo)); + //another nested call of stream method to voluntary add the todo back + todosStateRM + .stream((t) => t.addTodo(todo)) + .onError(ErrorHandler.showErrorSnackBar); }, ), ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart index 452f91d7..ff9559fb 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart @@ -6,69 +6,31 @@ import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:todos_app_core/todos_app_core.dart'; -import '../../../domain/entities/todo.dart'; -import '../../../service/todos_service.dart'; +import '../../../service/todos_state.dart'; import 'todo_item.dart'; class TodoList extends StatelessWidget { @override Widget build(BuildContext context) { - return StateBuilder( + return StateBuilder( //As this is the main list of todos, and this list can be update from //many widgets and screens (FilterButton, ExtraActionsButton, AddEditScreen, ..) //We register this widget with the global injected ReactiveModel. - //any where in the widget tree if setState of TodosService is called this StatesRebuild + //Anywhere in the widget tree if setValue of todosStore is called this StatesRebuild // will rebuild //In states_rebuild global ReactiveModel is the model that can be invoked all across the widget tree //and local ReactiveModel is a model that is meant to be called only locally in the widget where it is created - observe: () => RM.get(), - //The watch parameter is used to limit the rebuild of this StateBuilder. - //Even if TodosService emits a notification this widget will rebuild only if: - // 1- the length of the todo list is changed (add / remove a todo) - // 2- the active filter changes (From FilterButton widget) - // 3- The number of active todos changes (From ExtraActionsButton widget) - // - //Notice that if we edit one todo this StateBuilder will not update - watch: (todosServiceRM) => [ - todosServiceRM.state.todos.length, - todosServiceRM.state.activeFilter, - todosServiceRM.state.numActive, - ], - builder: (context, todosServiceRM) { - //The builder exposes the BuildContext and the ReactiveModel of TodosService + observe: () => RM.get(), + + builder: (context, todosStoreRM) { + //The builder exposes the BuildContext and the ReactiveModel of todosStore + final todos = todosStoreRM.value.todos; return ListView.builder( key: ArchSampleKeys.todoList, - itemCount: todosServiceRM.state.todos.length, + itemCount: todos.length, itemBuilder: (BuildContext context, int index) { - final todo = todosServiceRM.state.todos[index]; - //This is important. - //As we want to limit the rebuild of the listView, we want to rebuild only the listTile - //of the todo that changed. - //For this reason, we Wrapped each todo with a StateBuilder and subscribe it to - //a ReactiveModel model created from the todo - return StateBuilder( - //Key here is very important because StateBuilder is a StatefulWidget (this is a Flutter concept) - key: Key(todo.id), - //here we created a local ReactiveModel from one todo of the list - observe: () => RM.create(todo), - //This didUpdateWidget is used because if we mark all complete from the ExtraActionsButton, - //The listBuilder will update, but the StateBuilder for single todo will still have the old todo. - //In the didUpdateWidget, we check if the todo is modified, we set it and notify - //the StateBuilder to change - - didUpdateWidget: (context, todoRM, oldWidget) { - if (todoRM.value != todo) { - print('didUpdateWidget (${todoRM.value} $todo'); - //set and notify the observer this StateBuilder to rebuild - todoRM.value = todo; - } - }, - builder: (context, todoRM) { - print("builder"); - //render TodoItem and pass the local ReactiveModel through the constructor - return TodoItem(todoRM: todoRM); - }, - ); + final todo = todos[index]; + return TodoItem(todo: todo); }, ); }, diff --git a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart b/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart deleted file mode 100644 index a76b0092..00000000 --- a/states_rebuilder/lib/ui/pages/shared_widgets/check_favorite_box.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:states_rebuilder/states_rebuilder.dart'; - -import '../../../domain/entities/todo.dart'; -import '../../../service/todos_service.dart'; -import '../../exceptions/error_handler.dart'; - -class CheckFavoriteBox extends StatelessWidget { - //accept the todo ReactiveModel from the TodoList or DetailedScreen widgets - const CheckFavoriteBox({ - Key key, - @required this.todoRM, - }) : _key = key; - final Key _key; - final ReactiveModel todoRM; - Todo get todo => todoRM.value; - @override - Widget build(BuildContext context) { - return Checkbox( - key: _key, - value: todo.complete, - onChanged: (value) { - //hold the old todo - final oldTodo = todo; - final newTodo = todo.copyWith( - complete: value, - ); - //set todo to th new todo and notify observer (the todo tile) - todoRM.value = newTodo; - - //Here we get the global ReactiveModel and from it we create a new Local ReactiveModel. - //The created ReactiveModel is based of the future of updateTodo method. - RM.future(IN.get().updateTodo(newTodo)).onError( - (context, error) { - //on Error set the todo value to the old value - todoRM.value = oldTodo; - //show SnackBar to display the error message - ErrorHandler.showErrorSnackBar(context, error); - }, - ); - }, - ); - } -} diff --git a/states_rebuilder/pubspec.yaml b/states_rebuilder/pubspec.yaml index 4ee9704d..4c585945 100644 --- a/states_rebuilder/pubspec.yaml +++ b/states_rebuilder/pubspec.yaml @@ -19,8 +19,7 @@ environment: dependencies: flutter: sdk: flutter - states_rebuilder: - path: E:\flutter\_libraries\_states_rebuilder\states_rebuilder\states_rebuilder_package + states_rebuilder: ^1.15.0 key_value_store_flutter: key_value_store_web: shared_preferences: diff --git a/states_rebuilder/test/detailed_screen_test.dart b/states_rebuilder/test/detailed_screen_test.dart new file mode 100644 index 00000000..bd93e9be --- /dev/null +++ b/states_rebuilder/test/detailed_screen_test.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:states_rebuilder_sample/app.dart'; +import 'package:states_rebuilder_sample/ui/pages/home_screen/todo_item.dart'; +import 'package:todos_app_core/todos_app_core.dart'; + +import 'fake_repository.dart'; + +void main() { + final todoItem1Finder = find.byKey(ArchSampleKeys.todoItem('1')); + + testWidgets('delete item from the detailed screen', (tester) async { + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository(), + ), + ); + + await tester.pumpAndSettle(); + //expect to see three Todo items + expect(find.byType(TodoItem), findsNWidgets(3)); + + //tap to navigate to detail screen + await tester.tap(todoItem1Finder); + await tester.pumpAndSettle(); + + //expect we are in the detailed screen + expect(find.byKey(ArchSampleKeys.todoDetailsScreen), findsOneWidget); + + // + await tester.tap(find.byKey(ArchSampleKeys.deleteTodoButton)); + await tester.pumpAndSettle(); + + //expect we are back in the home screen + expect(find.byKey(ArchSampleKeys.todoList), findsOneWidget); + //expect to see two Todo items + expect(find.byType(TodoItem), findsNWidgets(2)); + //expect to see a SnackBar to reinsert the deleted todo + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('Undo'), findsOneWidget); + + //reinsert the deleted todo + await tester.tap(find.byType(SnackBarAction)); + await tester.pump(); + //expect to see three Todo items + expect(find.byType(TodoItem), findsNWidgets(3)); + await tester.pumpAndSettle(); + }); + + testWidgets('delete item from the detailed screen and reinsert it on error', + (tester) async { + await tester.pumpWidget( + StatesRebuilderApp( + repository: FakeRepository() + ..throwError = true + ..delay = 1000, + ), + ); + + await tester.pumpAndSettle(); + + //tap to navigate to detail screen + await tester.tap(todoItem1Finder); + await tester.pumpAndSettle(); + + // + await tester.tap(find.byKey(ArchSampleKeys.deleteTodoButton)); + await tester.pumpAndSettle(); + + //expect we are back in the home screen + expect(find.byKey(ArchSampleKeys.todoList), findsOneWidget); + //expect to see two Todo items + expect(find.byType(TodoItem), findsNWidgets(2)); + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('Undo'), findsOneWidget); + + // + await tester.pump(Duration(milliseconds: 1000)); + await tester.pumpAndSettle(); + + expect(find.byType(TodoItem), findsNWidgets(3)); + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('There is a problem in saving todos'), findsOneWidget); + }); +} diff --git a/states_rebuilder/test/fake_repository.dart b/states_rebuilder/test/fake_repository.dart index e0057801..34cea9af 100644 --- a/states_rebuilder/test/fake_repository.dart +++ b/states_rebuilder/test/fake_repository.dart @@ -5,7 +5,7 @@ import 'package:states_rebuilder_sample/service/interfaces/i_todo_repository.dar class FakeRepository implements ITodosRepository { @override Future> loadTodos() async { - await Future.delayed(Duration(milliseconds: 20)); + await Future.delayed(Duration(milliseconds: delay ?? 20)); return [ Todo( 'Task1', @@ -28,10 +28,11 @@ class FakeRepository implements ITodosRepository { } bool throwError = false; + int delay; bool isSaved = false; @override Future saveTodos(List todos) async { - await Future.delayed(Duration(milliseconds: 50)); + await Future.delayed(Duration(milliseconds: delay ?? 50)); if (throwError) { throw PersistanceException('There is a problem in saving todos'); } diff --git a/states_rebuilder/test/home_screen_test.dart b/states_rebuilder/test/home_screen_test.dart index 6753ce8d..eb965a28 100644 --- a/states_rebuilder/test/home_screen_test.dart +++ b/states_rebuilder/test/home_screen_test.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:states_rebuilder/states_rebuilder.dart'; import 'package:states_rebuilder_sample/app.dart'; -import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; -import 'package:states_rebuilder_sample/ui/common/enums.dart'; import 'package:todos_app_core/todos_app_core.dart'; - import 'fake_repository.dart'; /// Demonstrates how to test Widgets @@ -26,15 +21,6 @@ void main() { await tester.pump(Duration.zero); expect(find.byKey(ArchSampleKeys.todosLoading), findsOneWidget); await tester.pumpAndSettle(); - //check the reactive model that causes the rebuild of the widget - var notifiedRM = RM.notified; - expect( - notifiedRM.isA>() && - notifiedRM.hasData && - notifiedRM.state is List && - notifiedRM.state.length == 3, - isTrue, - ); }); testWidgets('should display a list after loading todos', (tester) async { @@ -86,16 +72,6 @@ void main() { await tester.drag(todoItem1Finder, Offset(-1000, 0)); await tester.pumpAndSettle(); - //check the reactive model that causes the rebuild of the widget - var notifiedRM = RM.notified; - expect( - notifiedRM.isA() && - notifiedRM.hasData && - notifiedRM.state.todos is List && - notifiedRM.state.todos.length == 2, - isTrue, - ); - expect(todoItem1Finder, findsNothing); expect(todoItem2Finder, findsOneWidget); expect(todoItem3Finder, findsOneWidget); @@ -104,7 +80,7 @@ void main() { }); testWidgets( - 'should remove todos using a dismissible ane insert back the removed element if throws', + 'should remove todos using a dismissible and insert back the removed element if throws', (tester) async { await tester.pumpWidget( StatesRebuilderApp( @@ -116,15 +92,6 @@ void main() { await tester.drag(todoItem1Finder, Offset(-1000, 0)); await tester.pumpAndSettle(); - //check the reactive model that causes the rebuild of the widget - var notifiedRM = RM.notified; - expect( - notifiedRM.isA() && - notifiedRM.hasError && - notifiedRM.error.message == 'There is a problem in saving todos', - isTrue, - ); - //Removed item in inserted back to the list expect(todoItem1Finder, findsOneWidget); expect(todoItem2Finder, findsOneWidget); @@ -144,15 +111,6 @@ void main() { await tester.tap(find.byKey(ArchSampleKeys.statsTab)); await tester.pump(); - //check the reactive model that causes the rebuild of the widget - var notifiedRM = RM.notified; - expect( - notifiedRM.isA() && - notifiedRM.hasData && - notifiedRM.value == AppTab.stats, - isTrue, - ); - expect(find.byKey(ArchSampleKeys.statsNumActive), findsOneWidget); expect(find.byKey(ArchSampleKeys.statsNumActive), findsOneWidget); }); @@ -174,14 +132,6 @@ void main() { await tester.tap(checkbox1); await tester.pump(); - //check the reactive model that causes the rebuild of the widget - var notifiedRM = RM.notified; - expect( - notifiedRM.isA() && - notifiedRM.hasData && - notifiedRM.value == true, - isTrue, - ); expect(tester.getSemantics(checkbox1), isChecked(true)); await tester.pumpAndSettle(); @@ -206,36 +156,14 @@ void main() { await tester.tap(checkbox1); await tester.pump(); - //check the reactive model that causes the rebuild of the widget - var notifiedRM = RM.notified; - print(notifiedRM); - expect( - notifiedRM.isA() && - notifiedRM.hasData && - notifiedRM.value == true, - isTrue, - ); + expect(tester.getSemantics(checkbox1), isChecked(true)); //NO Error, expect(find.byType(SnackBar), findsNothing); - //expect that the saveTodos method is still running - //and the reactive model of todosService is waiting; - // expect(RM.get().isWaiting, isTrue); - RM.printActiveRM = true; + // await tester.pumpAndSettle(); - notifiedRM = RM.notified; - print(notifiedRM); - expect( - notifiedRM.isA() && - notifiedRM.hasData && - notifiedRM.value == false, - isTrue, - ); expect(tester.getSemantics(checkbox1), isChecked(false)); - //expect that the saveTodos is ended with error - //and the reactive model of todosService has en error; - // expect(RM.get().hasError, isTrue); //SnackBar with error message expect(find.byType(SnackBar), findsOneWidget); diff --git a/states_rebuilder/test/todo_service_test.dart b/states_rebuilder/test/todo_service_test.dart deleted file mode 100644 index ae91c27f..00000000 --- a/states_rebuilder/test/todo_service_test.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:states_rebuilder_sample/domain/entities/todo.dart'; -import 'package:states_rebuilder_sample/service/common/enums.dart'; -import 'package:states_rebuilder_sample/service/interfaces/i_todo_repository.dart'; -import 'package:states_rebuilder_sample/service/todos_service.dart'; - -import 'fake_repository.dart'; - -//TodoService class is a pure dart class, you can test it just as you test a plain dart class. -void main() { - group( - 'TodosService', - () { - ITodosRepository todosRepository; - TodosService todoService; - setUp( - () { - todosRepository = FakeRepository(); - todoService = TodosService(todosRepository); - }, - ); - - test( - 'should load todos works', - () async { - expect(todoService.todos.isEmpty, isTrue); - await todoService.loadTodos(); - expect(todoService.todos.length, equals(2)); - }, - ); - - test( - 'should filler todos works', - () async { - await todoService.loadTodos(); - //all todos - expect(todoService.todos.length, equals(2)); - //active todos - todoService.activeFilter = VisibilityFilter.active; - expect(todoService.todos.length, equals(1)); - //completed todos - todoService.activeFilter = VisibilityFilter.completed; - expect(todoService.todos.length, equals(1)); - }, - ); - - test( - 'should add todo works', - () async { - await todoService.loadTodos(); - expect(todoService.todos.length, equals(2)); - final todoToAdd = Todo('addTask'); - await todoService.addTodo(todoToAdd); - expect(todoService.todos.length, equals(3)); - expect(await (todosRepository as FakeRepository).isSaved, isTrue); - }, - ); - - test( - 'should update todo works', - () async { - await todoService.loadTodos(); - final beforeUpdate = - todoService.todos.firstWhere((todo) => todo.id == '1'); - expect(beforeUpdate.task, equals('task1')); - await todoService.updateTodo(Todo('updateTodo', id: '1')); - expect(await (todosRepository as FakeRepository).isSaved, isTrue); - final afterUpdate = - todoService.todos.firstWhere((todo) => todo.id == '1'); - expect(afterUpdate.task, equals('updateTodo')); - }, - ); - - test( - 'should delete todo works', - () async { - await todoService.loadTodos(); - expect(todoService.todos.length, equals(2)); - await todoService.deleteTodo(Todo('updateTodo', id: '1')); - expect(await (todosRepository as FakeRepository).isSaved, isTrue); - expect(todoService.todos.length, equals(1)); - }, - ); - - test( - 'should toggleAll todos works', - () async { - await todoService.loadTodos(); - expect(todoService.numActive, equals(1)); - expect(todoService.numCompleted, equals(1)); - - await todoService.toggleAll(); - expect(await (todosRepository as FakeRepository).isSaved, isTrue); - expect(todoService.numActive, equals(0)); - expect(todoService.numCompleted, equals(2)); - - await todoService.toggleAll(); - expect(todoService.numActive, equals(2)); - expect(todoService.numCompleted, equals(0)); - }, - ); - - test( - 'should clearCompleted todos works', - () async { - await todoService.loadTodos(); - expect(todoService.numActive, equals(1)); - expect(todoService.numCompleted, equals(1)); - - await todoService.clearCompleted(); - expect(await (todosRepository as FakeRepository).isSaved, isTrue); - expect(todoService.todos.length, equals(1)); - expect(todoService.numActive, equals(1)); - expect(todoService.numCompleted, equals(0)); - }, - ); - }, - ); -} diff --git a/states_rebuilder/test/todo_state_test.dart b/states_rebuilder/test/todo_state_test.dart new file mode 100644 index 00000000..b6dbb10f --- /dev/null +++ b/states_rebuilder/test/todo_state_test.dart @@ -0,0 +1,179 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:states_rebuilder_sample/domain/entities/todo.dart'; +import 'package:states_rebuilder_sample/service/common/enums.dart'; +import 'package:states_rebuilder_sample/service/exceptions/persistance_exception.dart'; +import 'package:states_rebuilder_sample/service/interfaces/i_todo_repository.dart'; +import 'package:states_rebuilder_sample/service/todos_state.dart'; + +import 'fake_repository.dart'; + +//TodoService class is a pure dart class, you can test it just as you test a plain dart class. +void main() { + group( + 'TodosState', + () { + ITodosRepository todosRepository; + TodosState todosState; + setUp( + () { + todosRepository = FakeRepository(); + todosState = TodosState( + todos: [], + activeFilter: VisibilityFilter.all, + todoRepository: todosRepository, + ); + }, + ); + + test( + 'should load todos works', + () async { + final todosNewState = await todosState.loadTodos(); + expect(todosNewState.todos.length, equals(3)); + }, + ); + + test( + 'should filler todos works', + () async { + var todosNewState = await todosState.loadTodos(); + //all todos + expect(todosNewState.todos.length, equals(3)); + //active todos + todosNewState = + todosNewState.copyWith(activeFilter: VisibilityFilter.active); + expect(todosNewState.todos.length, equals(2)); + //completed todos + todosNewState = + todosNewState.copyWith(activeFilter: VisibilityFilter.completed); + expect(todosNewState.todos.length, equals(1)); + }, + ); + + test( + 'should add todo works', + () async { + var startingTodosState = await todosState.loadTodos(); + + final todoToAdd = Todo('addTask'); + final expectedTodosState = startingTodosState.copyWith( + todos: List.from(startingTodosState.todos)..add(todoToAdd), + ); + + expect( + startingTodosState.addTodo(todoToAdd), + emitsInOrder([expectedTodosState, emitsDone]), + ); + }, + ); + + test( + 'should add todo and remove it on error', + () async { + var startingTodosState = await todosState.loadTodos(); + + final todoToAdd = Todo('addTask'); + + (todosRepository as FakeRepository).throwError = true; + final expectedTodosState = startingTodosState.copyWith( + todos: List.from(startingTodosState.todos)..add(todoToAdd), + ); + + expect( + startingTodosState.addTodo(todoToAdd), + emitsInOrder([ + expectedTodosState, + startingTodosState, + emitsError(isA()), + emitsDone, + ]), + ); + }, + ); + + test( + 'should update todo works', + () async { + var startingTodosState = await todosState.loadTodos(); + + final updatedTodo = + startingTodosState.todos.first.copyWith(task: 'updated task'); + + final expectedTodos = List.from(startingTodosState.todos); + expectedTodos[0] = updatedTodo; + final expectedTodosState = startingTodosState.copyWith( + todos: expectedTodos, + ); + + expect( + startingTodosState.updateTodo(updatedTodo), + emitsInOrder([expectedTodosState, emitsDone]), + ); + }, + ); + + test( + 'should delete todo works', + () async { + var startingTodosState = await todosState.loadTodos(); + + final expectedTodosState = startingTodosState.copyWith( + todos: List.from(startingTodosState.todos)..removeLast(), + ); + + expect( + startingTodosState.deleteTodo(startingTodosState.todos.last), + emitsInOrder([expectedTodosState, emitsDone]), + ); + }, + ); + + test( + 'should toggleAll todos works', + () async { + var startingTodosState = await todosState.loadTodos(); + + expect(startingTodosState.numActive, equals(2)); + expect(startingTodosState.numCompleted, equals(1)); + + var expectedTodosState = startingTodosState.copyWith( + todos: startingTodosState.todos + .map( + (t) => + t.copyWith(complete: !startingTodosState.allComplete), + ) + .toList()); + + expect( + startingTodosState.toggleAll(), + emitsInOrder([expectedTodosState, emitsDone]), + ); + expect(expectedTodosState.numActive, equals(0)); + expect(expectedTodosState.numCompleted, equals(3)); + }, + ); + + test( + 'should clearCompleted todos works', + () async { + var startingTodosState = await todosState.loadTodos(); + + expect(startingTodosState.numActive, equals(2)); + expect(startingTodosState.numCompleted, equals(1)); + + var expectedTodosState = startingTodosState.copyWith( + todos: startingTodosState.todos + .where( + (t) => !t.complete, + ) + .toList()); + + expect( + startingTodosState.clearCompleted(), + emitsInOrder([expectedTodosState, emitsDone]), + ); + }, + ); + }, + ); +} From d2fa95e4830e55f7891d658d5efbebc0a27757b0 Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Thu, 30 Apr 2020 00:58:59 +0100 Subject: [PATCH 07/10] remvoe unused file --- states_rebuilder/README.sr.md | 1165 --------------------------------- 1 file changed, 1165 deletions(-) delete mode 100644 states_rebuilder/README.sr.md diff --git a/states_rebuilder/README.sr.md b/states_rebuilder/README.sr.md deleted file mode 100644 index 4c268197..00000000 --- a/states_rebuilder/README.sr.md +++ /dev/null @@ -1,1165 +0,0 @@ -# `states_rebuilder` - -[![pub package](https://img.shields.io/pub/v/states_rebuilder.svg)](https://pub.dev/packages/states_rebuilder) -[![CircleCI](https://circleci.com/gh/GIfatahTH/states_rebuilder.svg?style=svg)](https://circleci.com/gh/GIfatahTH/states_rebuilder) -[![codecov](https://codecov.io/gh/GIfatahTH/states_rebuilder/branch/master/graph/badge.svg)](https://codecov.io/gh/GIfatahTH/states_rebuilder) - - -A Flutter state management combined with dependency injection solution that allows : - * a 100% separation of User Interface (UI) representation from your logic classes - * an easy control on how your widgets rebuild to reflect the actual state of your application. -Model classes are simple vanilla dart classes without any need for inheritance, notification, streams or annotation and code generation. - - -`states_rebuilder` is built on the observer pattern for state management. - -> **Intent of observer pattern** ->Define a one-to-many dependency between objects so that when one object changes state (observable object), all its dependents (observer objects) are notified and updated automatically. - -`states_rebuilder` state management solution is based on what is called the `ReactiveModel`. - -## What is a `ReactiveModel` -* It is an abstract class. -* In the context of observer model (or observable-observer couple), it is the observable part. observer widgets can subscribe to it so that they can be notified to rebuild. -* It exposes two getters to get the latest state (`state` , `value`) -* It offers two methods to mutate the state and notify observer widgets (`setState` and `setValue`). `state`-`stateState` is used for mutable objects, whereas `value`-`setValue` is more convenient for primitives and immutable objects. -* It exposes four getters to track its state status, (`isIdle`, `isWaiting`, `hasError`, and `hasData`). -* And many more ... - -In `states_rebuilder`, you write your business logic part with pure dart classes, without worrying on how the UI will interact with it and get notification. -`states_rebuilder` decorate your plain old dart class with a `ReactiveModel` model using the decorator pattern. - -> **Intent of decorator pattern** -> Adds new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. - -`ReactiveModel` decorates your plain old dart class with the following behaviors: - -The getters are : -* **state**: returns the registered raw singleton of the model. -* **value**: returns the registered raw singleton of the model. -* **connectionState** : It is of type `ConnectionState` (a Flutter defined enumeration). It takes three values: - 1- `ConnectionState.none`: Before executing any method of the model. - 2- `ConnectionState.waiting`: While waiting for the end of an asynchronous task. - 3- `ConnectionState.done`: After running a synchronous method or the end of a pending asynchronous task. -* **isIdle** : It's of bool type. it is true if `connectionState` is `ConnectionState.none` -* **isWaiting**: It's of bool type. it is true if `connectionState` is `ConnectionState.waiting` -* **hasError**: It's of bool type. it is true if the asynchronous task ends with an error. -* **error**: Is of type dynamic. It holds the thrown error. -* **hasData**: It is of type bool. It is true if the connectionState is done without any error. - -The fields are: -* **joinSingletonToNewData** : It is of type dynamic. It holds data sent from a new reactive instance to the reactive singleton. -* **subscription** : it is of type `StreamSubscription`. It is not null if you inject streams using `Inject.stream` constructor. It is used to control the injected stream. - -The methods are: -* **setState**: return a `Future`. It is used to mutate the state and notify listeners after state mutation. -* **setValue**: return a `Future` It is used to mutate the state and notify listeners after state mutation. It is equivalent to `setState` with the parameter `setValue` set to true. **setValue** is most suitable for immutables whereas **setState** is more convenient for mutable objects. -* **whenConnectionState** Exhaustively switch over all the possible statuses of [connectionState]. Used mostly to return [Widget]s. It has four required parameters (`onIdle`, `onWaiting`, `onData` and `onError`). -* **restToIdle** used to reset the async connection state to `isIdle`. -* **restToHasData** used to reset the async connection state to `hasData`. -* **onError** a global error handler callback. -* **stream** listen to a stream from the state and notify observer widgets when it emits a data. -* **future** link to a future from the state and notify observers when it resolves. - -## Local and Global `ReactiveModel`: - -ReactiveModels are either local or global. -In local `ReactiveModel`, the creation of the `ReactiveModel` and subscription and notification are all limited in one place (widget). -In Global `ReactiveModel`, the `ReactiveModel` is created once, and it is available for subscription and notification throughout all the widget tree. - -### Local ReactiveModels - -Let's start by building the simplest counter app you have ever seen: - -```dart -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - //StateBuilder is used to subscribe to ReactiveModel - home: StateBuilder( - //Creating a local ReactiveModel that decorate an int value - //with initial value of 0 - observe: () => RM.create(0), - //The builder exposes the BuildContext and the instance of the created ReactiveModel - builder: (context, counterRM) { - return Scaffold( - appBar: AppBar(), - body: Center( - //use the value getter to get the latest state stored in the ReactiveModel - child: Text('${counterRM.value}'), - ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - //get and increment the value of the counterRM. - //on mutating the state using the value setter the observers are automatically notified - onPressed: () => counterRM.value++, - ), - ); - }, - ), - ); - } -} -``` -* The Observer pattern: - * `StateBuilder` widget is one of four observer widgets offered by `states_rebuilder` to subscribe to a `ReactiveModel`. - * in `observer` parameter we created and subscribed to a local ReactiveModel the decorated an integer value with initial value of 0. - With states_rebuilder we can created ReactiveModels form primitives, pure dart classes, futures or streams: - ```dart - //create for objects - final fooRM = RM.create(Foo()); - //create from Future - final futureRM = RM.future(myFuture); - //create from stream - final streamRM = RM.stream(myStream); - //the above statement are shortcuts of the following - final fooRM = ReactiveModel.create(Foo()); - final futureRM = ReactiveModel.future(futureRM); - final streamRM = ReactiveModel.stream(streamRM); - ``` - * The `builder` parameter exposes the BuildContext and the the created instance of the `ReactiveModel`. - * To notify the subscribed widgets (we have one StateBuilder here), we just incremented the value of the counterRM - ```dart - onPressed: () => counterRM.value++, - ``` -* The decorator pattern: - * ReactiveModel decorates a primitive integer of 0 initial value and adds the following functionality: - * The 0 is an observable ReactiveModel and widget can subscribe to it. - * The value getter and setter to increment the 0 and notify observers - - -In the example above the rebuild is not optimized, because the whole Scaffold rebuild to only change a little text at the center fo the screen. - -Let's optimize the rebuild and introduce the concept of ReactiveModel keys (`RMKey`). - -```dart -class MyApp extends StatelessWidget { - //define a ReactiveModel model key of type int and optional initial value - final RMKey counterRMKey = RMKey(0); - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(), - body: Center( - child: StateBuilder( - observe: () => RM.create(0), - //associate this StateBuilder to the defined key - rmKey: counterRMKey, - builder: (context, counterRM) { - return Text('${counterRM.value}'); - }), - ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - //increment the counter value and notify observers using the ReactiveModel key - onPressed: () => counterRMKey.value++, - ), - ), - ); - } -} -``` - -In a similar fashion on How global key are used in Flutter, we use ReactiveModel key to control a local ReactiveModel from outside its builder method of the widget where it is first created. -First,we instantiate a RMKey : -```dart -final RMKey counterRMKey = RMKey(0); -``` -Unlike Flutter global keys, you do not have to use StatefulWidget, because in states_rebuilder the state of RMKey is preserved even if the widget is rebuild. - -The next step is to associate the RMKey with a ReactiveModel, as done through the rmKey parameter of the StateBuilder widget. - -RMKey has all the functionality of the ReactiveModel is is associate with. You can call setState, setValue, get the state and its status. -```dart -onPressed: () => counterRMKey.value++, -``` -For more details on RMKey see here. - -As I said, ReactiveModel is a decorator over an object. Among the functionalities add, is the ability to track the asynchronous status of the state. -Let's see with an example: - -```dart -//create an immutable object -@immutable -class Counter { - final int count; - - Counter(this.count); - - Future increment() async { - //simulate a delay - await Future.delayed(Duration(seconds: 1)); - //simulate an error - if (Random().nextBool()) { - throw Exception('A Custom Message'); - } - return Counter(count + 1); - } -} - -class MyApp extends StatelessWidget { - //use a RMKey of type Counter - final counterRMKey = RMKey(); - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(), - body: Center( - //WhenRebuilder is a widget that is used to subscribe to an observable model. - //It Exhaustively goes through the four possible status of the state and define the corresponding widget. - child: WhenRebuilder( - observe: () => RM.create(Counter(0)), - rmKey: counterRMKey, - //Before and action - onIdle: () => Text('Tap to increment the counter'), - //While waiting for and asynchronous task to end - onWaiting: () => Center( - child: CircularProgressIndicator(), - ), - //If the asynchronous task ends with error - onError: (error) => Center(child: Text('$error')), - //If data is available - onData: (counter) { - return Text('${counter.count}'); - }, - ), - ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - //We use setValue to change the Counter state and notify observers - onPressed: () async => counterRMKey.setValue( - () => counterRMKey.value.increment(), - //set catchError to true - catchError: true, - ), - ), - ), - ); - } -} -``` -`WhenRebuilder` is the second observer widget after `StateBuilder`. It helps you to define the corresponding view for each of the four state status (`onIdle`, `onWaiting`, `onError` and `onData`). - -Notice that we used setValue method to mutate the state. If you use setValue, states_rebuilder automatically handle the asynchronous event for you. -* This is the rule, if you want to change the state synchronously, use the value setter: -```dart -counterRMKey.value = await counterRMKey.value.increment(); -``` -whereas if you want to let `states_rebuilder` handle the asynchronous events use `setValue`: -```dart -counterRMKey.setValue( - () => counterRMKey.value.increment(), - catchError: true, -), -``` -Later in this readme, I will talk about error handling with states_rebuilder. Here I only want to show you how easy error handling is with states_rebuilder. -Let's use the last example and imagine a real scenario where we want to persist the counter value in a database. We want to instantly display the incremented counter, and call an asynchronous method in the background to store the counter. If en error is thrown we want to go back to the last counter value and show a snackBar informing us about the error. - -```dart -@immutable -class Counter { - final int count; - - Counter(this.count); - - //Use stream generator instead of future - Stream increment() async* { - //Yield the new counter state - //states_rebuilder rebuilds the UI to display the new state - yield Counter(count + 1); - try { - await fetchSomeThing(); - } catch (e) { - //If an error, yield the old state - //states_rebuilder rebuilds the UI to display the old state - yield this; - //We have to throw the error to let states_rebuilder handle the error - throw e; - } - } -} - -class MyApp extends StatelessWidget { - final counterRMKey = RMKey(); - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(), - body: Center( - child: StateBuilder( - observe: () => RM.create(Counter(0)), - rmKey: counterRMKey, - builder: (context, counterRM) { - return Text('${counterRM.value.count}'); - }, - ), - ), - floatingActionButton: Builder(builder: (context) { - return FloatingActionButton( - child: Icon(Icons.add), - onPressed: () async { - //invoking the stream method from the ReactiveModel. - //states_rebuild subscribe to the stream and rebuild the observer widget whenever the stream emits - counterRMKey.stream((c) => c.increment()).onError( - (context, error) { - //side effects here - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text('$error.message'), - ), - ); - }, - ); - }, - ); - }), - ), - ); - } -} -``` -states_rebuild automatically subscribe to a stream and unsubscribe from it if the StateBuilder is disposed. - -Local `ReactiveModel` are the first choice when dealing with flutter's Input and selections widgets (Checkbox, Radio, switch,...), -here is an example of Slider : -```dart -StateBuilder( - observe: () => RM.create(0), - builder: (context, sliderRM) { - return Slider( - value: sliderRM.value, - onChanged: (value) { - sliderRM.value = value; - }, - ); - }, -), -``` - -### Global ReactiveModel -There is no difference between local and global ReactiveModel expect of the span of the availability of the ReactiveModel in the widget tree. - -Global ReactiveModels are cross pages available model, they can be subscribed to in one part of the widget tree and can make send notification from the other side of the widget tree. - -Regardless of the effectiveness of the state management solution, it must rely on a reliable dependency injection system. - -`states_rebuilder` uses the service locator pattern, but using it in a way that makes it aware of the widget's lifecycle. This means that models are registered when needed in the `initState` method of a` StatefulWidget` and are unregistered when they are no longer needed in the `dispose` method. -Models once registered are available throughout the widget tree as long as the StatefulWidget that registered them is not disposed. The StatefulWidget used to register and unregister models is the `Injector` widget. - - -```dart -@immutable -class Counter { - final int count; - - Counter(this.count); - - Future increment() async { - await fetchSomeThing(); - return Counter(count + 1); - } -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - //We use Injector widget to provide a model to the widget tree - return Injector( - //inject a list of injectable - inject: [Inject(() => Counter(0))], - builder: (context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(), - body: Center( - child: WhenRebuilder( - //Consume the ReactiveModel of the injected Counter model - observe: () => RM.get(), - onIdle: () => Text('Tap to increment the counter'), - onWaiting: () => Center( - child: CircularProgressIndicator(), - ), - onError: (error) => Center(child: Text('$error')), - onData: (counter) { - return Text('${counter.count}'); - }, - ), - ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - //The ReactiveModel of Counter is available any where is the widget tree. - onPressed: () async => RM.get().setValue( - () => RM.get().value.increment(), - catchError: true, - ), - ), - ), - ); - }, - ); - } -} -``` - -`states_rebuilder` uses the service locator pattern for injecting dependencies using the` injector` with is a StatefulWidget. - ->**Intent of service locator pattern** ->The purpose of the Service Locator pattern is to return the service instances on demand. This is useful for decoupling service consumers from concrete classes. It uses a central container which on request returns the request instance. - -To understand the principle of DI, it is important to consider the following principles: - -1. `Injector` adds classes to the container of the service locator in` initState` and deletes them in the `dispose` state. This means that if `Injector` is removed and re-inserted in the widget tree, a new singleton is registered for the injected models. If you injected streams or futures using `Inject.stream` or `Inject.future` and when the `Injector` is disposed and re-inserted, the streams and futures are disposed and reinitialized by `states_rebuilder` and do not fear of any memory leakage. - -2. You can use nested injectors. As the `Injector` is a simple StatefulWidget, it can be added anywhere in the widget tree. Typical use is to insert the `Injector` deeper in the widget tree just before using the injected classes. - -3. Injected classes are registered lazily. This means that they are not instantiated after injection until they are consumed for the first time. - -4. For each injected class, you can consume the registered instance using : - * `Injector.get` to get the registered raw vanilla dart instance. - ```dart - final T model = Injector.get() - //If the model is registered with custom name : - final T model = Injector.get(name:'customName'); - ``` - > As a shortcut you can use: - ```dart - //feature add in version 1.15.0 - final T model = IN.get() // IN stands for Injector - ``` - * or the ReactiveModel wrapper of the injected instance using: - ```dart - ReactiveModel modelRM = Injector.getAsReactive() - ``` - As a shortcut you can use: - ```dart - ReactiveModel modelRM = ReactiveModel(); - // Or simply (add in version 1.15.0) - ReactiveModel modelRM = RM.get(); - //I will use the latter, but the three are equivalent. - //In tutorials you expect to find any of these. - ``` - -5. Both the raw instance and the reactive instance are registered lazily, if you consume a class using only `Injector.get` and not` Injector.getAsReactive`, the reactive instance will never be instantiated. - -6. You can register classes with concrete types or abstract classes. - -7. You can register under different devolvement environments. This can be done by the help of `Inject.interface` named constructor and by setting the environment flavor `Injector.env` before calling the runApp method. see example below. - -That said: -> It is possible to register a class as a singleton, as a lazy singleton or as a factory simply by choosing where to insert it in the widget tree. - -* To save a singleton that will be available for all applications, insert the `Injector` widget in the top widget tree. It is possible to set the `isLazy` parameter to false to instantiate the injected class the time of injection. - -* To save a singleton that will be used by a branch of the widget tree, insert the `Injector` widget just above the branch. Each time you get into the branch, a singleton is registered and when you get out of it, the singleton will be destroyed. Making a profit of the behavior, you can clean injected models by defining a `dispose()` method inside them and set the parameter `disposeModels` of the `Injector`to true. - -It is important to understand that `states_rebuilder` caches two singletons. -* The raw singleton of the registered model, obtained using `Injector.get` method. -* The reactive singleton of the registered model (the raw model decorated with reactive environment), obtained using `Injector.getAsReactive`. - -With `states_rebuilder`, you can create, at any time, a new reactive instance, which is the same raw cashed singleton but decorated with a new reactive environment. - -To create a new reactive instance of an injected model use `StateBuilder` with generic type and without defining `models` property. -```dart -StateBuilder( - builder:(BuildContext context, ReactiveModel newReactiveModel){ - return YourWidget(); - } -) -``` - -You can also use `ReactiveModel.asNew([dynamic seed])` method: - -```dart -final reactiveModel = RM.get(); -final newReactiveModel = reactiveModel.asNew('mySeed'); - -// or directly - -final newReactiveModel = RM.get().asNew('mySeed'); -``` -By setting the seed parameter of the `asNew` method your are sure to get the same new reactive instance even after the widget rebuilds. - -The seed parameter is optional, and if not provided, `states_rebuilder` uses a default seed. - ->seed here has a similar meaning in random number generator. That is for the same seed we get the same new reactive instance. - - -**Important notes about reactive singleton and new reactive instances:** -* The reactive singleton and all the new reactive instances share the same raw singleton of the model, but each one decorates it with a different environment. -* Unlike the reactive singleton, new reactive instances are not cached so they are not accessible outside the widget in which they were instantiated. -* To make new reactive instances accessible throughout the widget tree, you have to register it with the `Injector` with a custom name: - -```dart -return Injector( -inject: [ - Inject( () => modelNewRM, name: 'newModel1'), -], -) -// Or -Injector( -inject: [ - Inject(() => Counter()), - Inject( - () => modelNewRM, - name: Enum.newModel1, - ), -], -``` -At later time if you want to consume the injected new reactive instance you use: - -```dart -// get the injected new reactive instance -ReactiveModel modelRM2 = RM.get(name : 'newModel1'); -//Or -ReactiveModel modelRM2 = RM.get(name : Enum.newModel1); -``` -* You can not get a new reactive model by using `getAsReactive(context: context)` with a defined context. It will throw because only the reactive singleton that can subscribe a widget using the context. - -* With the exception of the raw singleton they share, the reactive singleton and the new reactive instances have an independent reactive environment. That is when a particular reactive instance issues a notification with an error or with `ConnectionState.awaiting`, it will not affect other reactive environments. - -* `states_rebuilder` allows reactive instances to share their notification or state with the reactive singleton. This can be done by: -1- `notifyAllReactiveInstances` parameter of `setState` method. If true, each time a notification is issued by the reactive instance in which `setState` is called, all other reactive instances are notified. -2- `joinSingletonWith` parameter of `Inject` class. This time, new reactive instances, when issuing a notification, can clone their state to the reactive singleton. - * If `joinSingletonWith` is set to` JoinSingleton.withNewReactiveInstance`, this means that the reactive singleton will have the state of the new reactive instance issuing the notification. - * If `joinSingletonWith` is set to `JoinSingleton.withCombinedReactiveInstances`, this means that the singleton will hold a combined state of all the new reactive instances. - The combined state priority logic is: - Priority 1- The combined `ReactiveModel.hasError` is true if at least one of the new instances has an error - Priority 2- The combined `ReactiveModel.connectionState` is awaiting if at least one of the new instances is awaiting. - Priority 3- The combined `ReactiveModel.connectionState` is 'none' if at least one of the new instances is 'none'. - Priority 4- The combined `ReactiveModel.hasDate` is true if it has no error, it isn't awaiting and it is not in 'none' state. -* New reactive instances can send data to the reactive singleton. `joinSingletonToNewData` parameter of reactive environment hold the sending message. - - -# StateBuilder -In addition to its state management responsibility, `StateBuilder` offers a facade that facilitates the management of the widget's lifecycle. -```dart -StateBuilder( - onSetState: (BuildContext context, ReactiveModel exposedModel){ - /* - Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar,... - - It is similar to 'onSetState' parameter of the 'setState' method. The difference is that the `onSetState` of the 'setState' method is called once after executing the 'setState'. But this 'onSetState' is executed each time a notification is sent from one of the observable models this 'StateBuilder' is subscribing. - - You can use another nested setState here. - */ - }, - onRebuildState: (BuildContext context, ReactiveModel exposedModel){ - // The same as in onSetState but called after the end rebuild process. - }, - initState: (BuildContext context, ReactiveModel exposedModel){ - // Function to execute in initState of the state. - }, - dispose: (BuildContext context, ReactiveModel exposedModel){ - // Function to execute in dispose of the state. - }, - didChangeDependencies: (BuildContext context, ReactiveModel exposedModel){ - // Function to be executed when a dependency of state changes. - }, - didUpdateWidget: (BuildContext context, ReactiveModel exposedModel, StateBuilder oldWidget){ - // Called whenever the widget configuration changes. - }, - afterInitialBuild: (BuildContext context, ReactiveModel exposedModel){ - // Called after the widget is first inserted in the widget tree. - }, - afterRebuild: (BuildContext context, ReactiveModel exposedModel){ - /* - Called after each rebuild of the widget. - - The difference between onRebuildState and afterRebuild is that the latter is called each time the widget rebuilds, regardless of the origin of the rebuild. - Whereas onRebuildState is called only after rebuilds after notifications from the models to which the widget is subscribed. - */ - }, - // If true all model will be disposed when the widget is removed from the widget tree - disposeModels: true, - - // A list of observable objects to which this widget will subscribe. - models: [model1, model2] - - // Tag to be used to filer notification from observable classes. - // It can be any type of data, but when it is a List, - // this widget will be saved with many tags that are the items in the list. - tag: dynamic - - //Similar to the concept of global key in Flutter, with ReactiveModel key you - //can control the observer widget associated with it from outside. - ///[see here for more details](changelog/v-1.15.0.md) - rmKey : RMKey(); - - watch: (ReactiveModel exposedModel) { - //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes - }, - - builder: (BuildContext context, ReactiveModel exposedModel){ - /// [BuildContext] can be used as the default tag of this widget. - - /// The model is the first instance (model1) in the list of the [models] parameter. - /// If the parameter [models] is not provided then the model will be a new reactive instance. - }, - builderWithChild: (BuildContext context, ReactiveModel model, Widget child){ - ///Same as [builder], but can take a child widget exposedModel the part of the widget tree that we do not want to rebuild. - /// If both [builder] and [builderWithChild] are defined, it will throw. - - }, - - //The child widget that is used in [builderWithChild]. - child: MyWidget(), - -) -``` - -`states_rebuilder` uses the observer pattern. Notification can be filtered so that only widgets meeting the filtering criteria will be notified to rebuild. Filtration is done through tags. `StateBuilder` can register with one or more tags and `StatesRebuilder` can notify the observer widgets with a specific list of tags, so that only widgets registered with at least one of these tags will rebuild. - -```dart -class Counter extends StatesRebuilder { - int count = 0; - increment() { - count++; - //notifying the observers with 'Tag1' - rebuildStates(['Tag1']); - } -} - -class HomePage extends StatelessWidget { - @override - Widget build(BuildContext context) { - final Counter counterModel = Injector.get(context: context); - return Column( - children: [ - StateBuilder( // This StateBuilder will be notified - models: [counterModel], - tag: ['tag1, tag2'], - builder: (_, __) => Text('${counterModel.count}'), - ), - StateBuilder( - models: [counterModel], - tag: 'tag2', - builder: (_, __) => Text('${counterModel.count}'), - ), - StateBuilder( - models: [counterModel], - tag: MyEnumeration.tag1,// You can use enumeration - builder: (_, __) => Text('${counterModel.count}'), - ) - ], - ); - } -} -``` -# WhenRebuilder and WhenRebuilderOr -`states_rebuilder` offers the the `WhenRebuilder` widget which is a a combination of `StateBuilder` widget and `ReactiveModel.whenConnectionState` method. - -instead of verbosely: -```dart -Widget build(BuildContext context) { - return StateBuilder( - models: [RM.get()], - builder: (_, plugin1RM) { - return plugin1RM.whenConnectionState( - onIdle: () => Text('onIDle'), - onWaiting: () => CircularProgressIndicator(), - onError: (error) => Text('plugin one has an error $error'), - onData: (plugin1) => Text('plugin one is ready'), - ); - }, - ); -} -``` - -You use : - -```dart -@override -Widget build(BuildContext context) { - return WhenRebuilder( - models: [RM.get()], - onIdle: () => Text('onIdle'), - onWaiting: () => CircularProgressIndicator(), - onError: (error) => Text('plugin one has an error $error'), - onData: (plugin1) => Text('plugin one is ready'), - ); -} -``` - -Also with `WhenRebuilder` you can listen to a list of observable models and go throw all the possible combination statuses of the observable models: - -```dart -WhenRebuilder( - //List of observable models - models: [reactiveModel1, reactiveModel1], - onIdle: () { - //Will be invoked if : - //1- None of the observable models is in the error state, AND - //2- None of the observable models is in the waiting state, AND - //3- At least one of the observable models is in the idle state. - }, - onWaiting: () => { - //Will be invoked if : - //1- None of the observable models is in the error state, AND - //2- At least one of the observable models is in the waiting state. - }, - onError: (error) => { - //Will be invoked if : - //1- At least one of the observable models is in the error state. - - //The error parameter holds the thrown error of the model that has the error - }, - onData: (data) => { - //Will be invoked if : - //1- None of the observable models is in the error state, AND - //2- None of the observable models is in the waiting state, AND - //3- None of the observable models is in the idle state, AND - //4- All the observable models have data - - //The data parameter holds the state of the first model in the models' list. - }, - - // Tag to be used to filer notification from observable classes. - // It can be any type of data, but when it is a List, - // this widget will be saved with many tags that are the items in the list. - tag: dynamic - - initState: (BuildContext context, ReactiveModel exposedModel){ - // Function to execute in initState of the state. - }, - dispose: (BuildContext context, ReactiveModel exposedModel){ - // Function to execute in dispose of the state. - }, -), -``` -`WhenRebuilderOr` is just like `WhenRebuilder` but with optional `onIdle`, `onWaiting` and `onError` parameters and with required default `builder`.. - -# OnSetStateListener -`OnSetStateListener` is useful when you want to globally control the notification flow of a list of observable models and execute side effect calls. - -```dart -OnSetStateListener( - //List of observable models - models: [reactiveModel1, reactiveModel1], - onSetState: (context, reactiveModel1) { - _onSetState = 'onSetState'; - }, - onWaiting: (context, reactiveModel1) { - //Will be invoked if : - //At least one of the observable models is in the waiting state. - }, - onData: (context, reactiveModel1) { - //Will be invoked if : - //All of the observable models are in the hasData state. - }, - onError: (context, error) { - //Will be invoked if : - //At least one of the observable models is in the error state. - //The error parameter holds the thrown error of the model that has the error - }, - // Tag to be used to filer notification from observable classes. - // It can be any type of data, but when it is a List, - // this widget will be saved with many tags that are the items in the list. - tag: dynamic - - watch: (ReactiveModel exposedModel) { - //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes - }, - //Wether to execute [onSetState],[onWaiting], [onError], and/or [onData] in the [State.initState] - shouldOnInitState:false, - //It has a child parameter, not a builder parameter. - child: Container(), -) -``` -What makes `OnSetStateListener` different is the fact that is has a child parameter rather than a builder parameter. This means that the child parameter will not rebuild even if observable models send notifications. - -# Note on the exposedModel -`StateBuilder`, `WhenRebuilder` and `OnSetStateListener` observer widgets can be set to observer many observable reactive models. The exposed model instance depends on the generic parameter `T`. -ex: -```dart -//first case : generic model is ModelA -StateBuilder( - models:[modelA, modelB], - builder:(context, exposedModel){ - //exposedModel is an instance of ReactiveModel. - } -) -//second case : generic model is ModelB -StateBuilder( - models:[modelA, modelB], - builder:(context, exposedModel){ - //exposedModel is an instance of ReactiveModel. - } -) -//third case : generic model is dynamic -StateBuilder( - models:[modelA, modelB], - builder:(context, exposedModel){ - //exposedModel is dynamic and it will change over time to hold the instance of model that emits a notification. - - //If modelA emits a notification the exposedModel == ReactiveModel. - //Wheres if modelB emits a notification the exposedModel == ReactiveModel. - } -) -``` -# Injector -With `Injector` you can inject multiple dependent or independent models (BloCs, Services) at the same time. Also you can inject stream and future. -```dart -Injector( - inject: [ - //The order is not mandatory even for dependent models. - Inject(() => ModelA()), - Inject(() => ModelB()),//Generic type in inferred. - Inject(() => ModelC(Injector.get())),// Directly inject ModelA in ModelC constructor - Inject(() => ModelC(Injector.get())),// Type in inferred. - Inject(() => ModelD()),// Register with Interface type. - Inject({ //Inject through interface with environment flavor. - 'prod': ()=>ModelImplA(), - 'test': ()=>ModelImplB(), - }), // you have to set the `Inject.env = 'prod'` before `runApp` method - //You can inject streams and future and make them accessible to all the widget tree. - Inject.future(() => Future(), initialValue:0),// Register a future. - Inject.stream(() => Stream()),// Register a stream. - Inject(() => ModelD(),name:"customName"), // Use custom name - - //Inject and reinject with previous value provided. - Inject.previous((ModelA previous){ - return ModelA(id: previous.id); - }) - ], - //reinjectOn takes a list of StatesRebuilder models, if any of those models emits a notification all the injected model will be disposed and re-injected. - reinjectOn : [models] - // Whether to notify listeners of injected model using 'previous' constructor - shouldNotifyOnReinjectOn: true; - . - . -); -``` - -* Models are registered lazily by default. That is, they will not be instantiated until they are first used. To instantiate a particular model at the time of registration, you can set the `isLazy` variable of the class `Inject` to false. - -* `Injector` is a simple `StatefulWidget`, and models are registered in the `initState` and unregistered in the `dispose` state. (Registered means add to the service locator container). -If you wish to unregister and re-register the injected models after each rebuild you can set the key parameter to be `UniqueKey()`. -In a better alternative is to use the `reinjectOn` parameter which takes a list of `StatesRebuilder` models and unregister and re-register the injected models if any of the latter models emits a notification. - -* For more detailed on the use of `Inject.previous` and `reinject` and `shouldNotifyOnReinjectOn` [see more details here](changelog/v-1.15.0.md) - -* In addition to its injection responsibility, the `Injector` widget gives you a convenient facade to manage the life cycle of the widget as well as the application: - -```dart -Injector( - initState: (){ - // Function to execute in initState of the state. - }, - dispose: (){ - // Function to execute in dispose of the state. - }, - afterInitialBuild: (BuildContext context){ - // Called after the widget is first inserted in the widget tree. - }, - appLifeCycle: (AppLifecycleState state){ - /* - Function to track app life cycle state. It takes as parameter the AppLifeCycleState - In Android (onCreate, onPause, ...) and in IOS (didFinishLaunchingWithOptions, - applicationWillEnterForeground ..) - */ - }, - // If true all model will be disposed when the widget is removed from the widget tree - disposeModels: true, - . - . -); -``` - - -The `Injector.get` method searches for the registered singleton using the service locator pattern. For this reason, `BuildContext` is not required. The `BuildContext` is optional and it is useful if you want to subscribe to the widget that has the `BuildContext` to the obtained model. - -In the `HomePage` class of the example, we can remove `StateBuilder` and use the `BuildContext` to subscribe the widget. - -```dart -class HomePage extends StatelessWidget { - @override - Widget build(BuildContext context) { - // The BuildContext of this widget is subscribed to the Counter class. - // Whenever the Counter class issues a notification, this widget will be rebuilt. - final Counter counterModel = Injector.get(context: context); - return Center( - child: Text('${counterModel.count}'), - ); - } -} -``` -Once the context is provided, `states_rebuilder` searches up in the widget tree to find the nearest `Injector` widget that has registered an `Inject` of the type provided and register the context (`Inject` class is associated with `InheritedWidget`). So be careful in case the `InheritedWidget` is not available, especially after navigation. - -To deal with such a situation, you can remove the `context` parameter and use the `StateBuilder` widget, or in case you want to keep using the `context` you can use the `reinject` parameter of the `Injector`. -```dart -Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Injector( - //reinject an already injected model - reinject: [counterModel], - builder: (context) { - return PageTwo(); - }, - ), - ), -); -``` - -# setState -`setState` is used whenever you want to trigger an event or an action that will mutate the state of the model and ends by issuing a notification to the observers. - -```dart -reactiveModel.setState( - (state) => state.increment(), - //Filter notification with tags - filterTags: ['Tag1', Enumeration.Tag2], - - //onData, trigger notification from new reactive models with the seeds in the list, - seeds:['seed1',Enumeration.Seed2 ], - - //set to true, you want to catch error, and not break the app. - catchError: true - - watch: (Counter counter) { - //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes - return counter.count; //if count value is not changed, no notification will be emitted. - }, - onSetState: (BuildContext context) { - /* - Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar , .. - - You can use another nested setState here. - */ - }, - onRebuildState: (BuildContext context) { - //The same as in onSetState but called after the end rebuild process. - }, - - onData: (BuildContext context, T model){ - //Callback to be executed if the reactive model has data. - } - - onError: (BuildContext context, dynamic error){ - //Callback to be executed if the reactive model throws an error. - //You do not have to set the parameter catchError to true. By defining onError parameter - //states_rebuilder catches the error by default. - } - - //When a notification is issued, whether to notify all reactive instances of the model - notifyAllReactiveInstances: true, - /* - If defined, when a new reactive instance issues a notification, it will change the state of the reactive singleton. - */ - joinSingleton: true, - - //message to be sent to the reactive singleton - dynamic joinSingletonToNewData, - - //Whether to set value or not - bool setValue:false, -), -``` - -# `value` getter and `setValue` method. -With `states_rebuilder` you can inject with primitive values or enums and make them reactive so that you can mutate their values and notify observer widgets that have subscribed to them. - -With `RM.create(T value)` you can create a `ReactiveModel` from a primitive value. The created `ReactiveModel` has the full power the other reactive models created using `Injector` have as en example you can wrap the primitive value with many reactive model instances. - -If you change the value (counterRM.value++), setValue will implicitly called to notify observers. - -`setValue` watches the change of the value and will not notify observers only if the value has changed. -`setValue` has `onSetState`, `onRebuildState`, `onError`, `catchError`, `filterTags` , `seeds` and `notifyAllReactiveInstances` the same way they are defined in `setState`: -```dart -reactiveModel.setValue( - ()=> newValue, - filterTags: ['Tag1', Enumeration.Tag2], - //onData, trigger notification from new reactive models with the seeds in the list, - seeds:['seed1',Enumeration.Seed2 ], - - onSetState: (BuildContext context) { - /* - Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar , .. - - You can use another nested setState here. - */ - }, - onRebuildState: (BuildContext context) { - //The same as in onSetState but called after the end rebuild process. - }, - - //set to true, you want to catch error, and not break the app. - catchError: true, - - onError: (BuildContext context, dynamic error){ - //Callback to be executed if the reactive model throws an error. - //You do not have to set the parameter catchError to true. By defining onError parameter - //states_rebuilder catches the error by default. - } - - //When a notification is issued, whether to notify all reactive instances of the model - notifyAllReactiveInstances: true, -), -``` - -# StateWithMixinBuilder -`StateWithMixinBuilder` is similar to `StateBuilder` and extends it by adding mixin (practical case is an animation), -```Dart -StateWithMixinBuilder( { - Key key, - dynamic tag, // you define the tag of the state. This is the first way - List models, // You give a list of the logic classes (BloC) you want this widget to listen to. - initState: (BuildContext, String,T) { - // for code to be executed in the initState of a StatefulWidget - }, - dispose (BuildContext, String,T) { - // for code to be executed in the dispose of a StatefulWidget - }, - didChangeDependencies: (BuildContext, String,T) { - // for code to be executed in the didChangeDependencies of a StatefulWidget - }, - didUpdateWidget: (BuildContext, String,StateBuilder, T){ - // for code to be executed in the didUpdateWidget of a StatefulWidget - }, - didChangeAppLifecycleState: (String, AppLifecycleState){ - // for code to be executed depending on the life cycle of the app (in Android : onResume, onPause ...). - }, - afterInitialBuild: (BuildContext, String,T){ - // for code to be executed after the widget is inserted in the widget tree. - }, - afterRebuild: (BuildContext, String) { - // for code to be executed after each rebuild of the widget. - }, - @required MixinWith mixinWith -}); -``` -Available mixins are: singleTickerProviderStateMixin, tickerProviderStateMixin, AutomaticKeepAliveClientMixin and WidgetsBindingObserver. - - -# Dependency Injection and development flavor -example of development flavor: -```dart -//abstract class -abstract class ConfigInterface { - String get appDisplayName; -} - -// first prod implementation -class ProdConfig implements ConfigInterface { - @override - String get appDisplayName => "Production App"; -} - -//second dev implementation -class DevConfig implements ConfigInterface { - @override - String get appDisplayName => "Dev App"; -} - -//Another abstract class -abstract class IDataBase{} - -// first prod implementation -class RealDataBase implements IDataBase {} - -// Second prod implementation -class FakeDataBase implements IDataBase {} - - -//enum for defined flavor -enum Flavor { Prod, Dev } - - -void main() { - //Choose yor environment flavor - Injector.env = Flavor.Prod; - - runApp( - Injector( - inject: [ - //Register against an interface with different flavor - Inject.interface({ - Flavor.Prod: ()=>ProdConfig(), - Flavor.Dev:()=>DevConfig(), - }), - Inject.interface({ - Flavor.Prod: ()=>RealDataBase(), - Flavor.Dev:()=>FakeDataBase(), - }), - ], - builder: (_){ - return MyApp( - appTitle: Injector.get().appDisplayName; - dataBaseRepo : Injector.get(), - ); - }, - ) - ); -} -``` - -# Widget unit texting - -The test is an important step in the daily life of a programmer; if not the most important part! - -With `Injector`, you can isolate any widget by mocking its dependencies and test it. - -Let's suppose we have the widget. -```dart -Import 'my_real_model.dart'; -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Injector( - inject: [Inject(() => MyRealModel())], - builder: (context) { - final myRealModelRM = RM.get(); - - // your widget - }, - ); - } -} -``` -The `MyApp` widget depends on` MyRealModel`. At first glance, this may seem to violate DI principles. How can we mock the "MyRealModel" which is not injected into the constructor of "MyApp"? - -To mock `MyRealModel` and test MyApp we set `Injector.enableTestMode` to true : - -```dart -testWidgets('Test MyApp class', (tester) async { - //set enableTestMode to true - Injector.enableTestMode = true; - - await tester.pumpWidget( - Injector( - //Inject the fake model and register it with the real model type - inject: [Inject(() => MyFakeModel())], - builder: (context) { - //In my MyApp, Injector.get or Inject.getAsReactive will return the fake model instance - return MyApp(); - }, - ), - ); - - //My test -}); - -//fake model implement real model -class MyFakeModel extends MyRealModel { - // fake implementation -} -``` -You can see a real test of the [counter_app_with_error]((states_rebuilder_package/example/test)) and [counter_app_with_refresh_indicator]((states_rebuilder_package/example/test)). - -# For further reading: - -> [List of article about `states_rebuilder`](https://medium.com/@meltft/states-rebuilder-and-animator-articles-4b178a09cdfa?source=friends_link&sk=7bef442f49254bfe7adc2c798395d9b9) - - -# Updates -To track the history of updates and feel the context when those updates are introduced, See the following links. - -* [1.15.0 update](changelog/v-1.15.0.md) : - * Example of using `Inject.previous` with `reinjectOn`; - * The add shortcuts (`IN.get()`, `RM.get()`, ...); - * Example of how to use states_rebuilder observer widgets instead of `FutureBuilder` and `StreamBuilder` Flutter widgets; - * The concept of ReactiveModel keys. From 265a007ca95de0f9af35cf629a9bf6b94a7b4967 Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Mon, 4 May 2020 18:53:33 +0100 Subject: [PATCH 08/10] use static pure function in TodosState class --- states_rebuilder/lib/service/todos_state.dart | 56 +++++++++++-------- .../add_edit_screen.dart/add_edit_screen.dart | 3 +- .../ui/pages/detail_screen/detail_screen.dart | 2 +- .../home_screen/extra_actions_button.dart | 4 +- .../lib/ui/pages/home_screen/home_screen.dart | 2 +- .../lib/ui/pages/home_screen/todo_item.dart | 9 ++- states_rebuilder/test/todo_state_test.dart | 29 +++++----- 7 files changed, 59 insertions(+), 46 deletions(-) diff --git a/states_rebuilder/lib/service/todos_state.dart b/states_rebuilder/lib/service/todos_state.dart index 024f7674..4208720d 100644 --- a/states_rebuilder/lib/service/todos_state.dart +++ b/states_rebuilder/lib/service/todos_state.dart @@ -45,12 +45,12 @@ class TodosState { //When we want to await for the future and display something in the screen, //we use future. - Future loadTodos() async { + static Future loadTodos(TodosState todosState) async { ////If you want to simulate loading failure uncomment theses lines // await Future.delayed(Duration(seconds: 5)); // throw PersistanceException('net work error'); - final _todos = await _todoRepository.loadTodos(); - return copyWith( + final _todos = await todosState._todoRepository.loadTodos(); + return todosState.copyWith( todos: _todos, activeFilter: VisibilityFilter.all, ); @@ -58,49 +58,57 @@ class TodosState { //We use stream generator when we want to instantly display the update, and execute the the saveTodos in the background, //and if the saveTodos fails we want to display the old state and a snackbar containing the error message - Stream addTodo(Todo todo) async* { - final newTodos = List.from(_todos)..add(todo); - //_saveTodos is common to all crud operation - yield* _saveTodos(newTodos); + // + //Notice that this method is static pure function, it is already isolated to be tested easily + static Stream addTodo(TodosState todosState, Todo todo) async* { + final newTodos = List.from(todosState._todos)..add(todo); + + yield* _saveTodos(todosState, newTodos); } - Stream updateTodo(Todo todo) async* { - final newTodos = _todos.map((t) => t.id == todo.id ? todo : t).toList(); - yield* _saveTodos(newTodos); + static Stream updateTodo( + TodosState todosState, Todo todo) async* { + final newTodos = + todosState._todos.map((t) => t.id == todo.id ? todo : t).toList(); + yield* _saveTodos(todosState, newTodos); } - Stream deleteTodo(Todo todo) async* { - final newTodos = List.from(_todos)..remove(todo); - yield* _saveTodos(newTodos); + static Stream deleteTodo( + TodosState todosState, Todo todo) async* { + final newTodos = List.from(todosState._todos)..remove(todo); + yield* _saveTodos(todosState, newTodos); } - Stream toggleAll() async* { - final newTodos = _todos + static Stream toggleAll(TodosState todosState) async* { + final newTodos = todosState._todos .map( - (t) => t.copyWith(complete: !allComplete), + (t) => t.copyWith(complete: !todosState.allComplete), ) .toList(); - yield* _saveTodos(newTodos); + yield* _saveTodos(todosState, newTodos); } - Stream clearCompleted() async* { - final newTodos = List.from(_todos) + static Stream clearCompleted(TodosState todosState) async* { + final newTodos = List.from(todosState._todos) ..removeWhere( (t) => t.complete, ); - yield* _saveTodos(newTodos); + yield* _saveTodos(todosState, newTodos); } - Stream _saveTodos(List newTodos) async* { + static Stream _saveTodos( + TodosState todosState, + List newTodos, + ) async* { //Yield the new state, and states_rebuilder will rebuild observer widgets - yield copyWith( + yield todosState.copyWith( todos: newTodos, ); try { - await _todoRepository.saveTodos(newTodos); + await todosState._todoRepository.saveTodos(newTodos); } catch (e) { //on error yield the old state, states_rebuilder will rebuild the UI to display the old state - yield this; + yield todosState; //rethrow the error so that states_rebuilder can display the snackbar containing the error message rethrow; } diff --git a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart index c3525f1f..51117355 100644 --- a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart +++ b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart @@ -96,7 +96,8 @@ class _AddEditPageState extends State { RM .get() - .stream((t) => t.addTodo(Todo(_task, note: _note))) + .stream( + (t) => TodosState.addTodo(t, Todo(_task, note: _note))) .onError(ErrorHandler.showErrorSnackBar); } } diff --git a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart index 94cd626f..2d44e829 100644 --- a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart +++ b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart @@ -118,7 +118,7 @@ class DetailScreen extends StatelessWidget { todoRMKey.value = newTodo; RM .get() - .stream((t) => t.updateTodo(newTodo)) + .stream((t) => TodosState.updateTodo(t, newTodo)) .onError((ctx, error) { todoRMKey.value = oldTodo; ErrorHandler.showErrorSnackBar(context, error); diff --git a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart index 7020c257..469c0278 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart @@ -27,8 +27,8 @@ class ExtraActionsButton extends StatelessWidget { .get() .stream( (action == ExtraAction.toggleAllComplete) - ? (t) => t.toggleAll() - : (t) => t.clearCompleted(), + ? (t) => TodosState.toggleAll(t) + : (t) => TodosState.clearCompleted(t), ) .onError(ErrorHandler.showErrorSnackBar); }, diff --git a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart index 8a29f229..770597e4 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart @@ -39,7 +39,7 @@ class HomeScreen extends StatelessWidget { //we use the HomeScreen seed so that if other pages emits a notification this widget will not be notified () => RM.get().asNew(HomeScreen) //using the cascade operator, we call the todosLoad method informing states_rebuilder that is is a future - ..future((t) => t.loadTodos()) + ..future((t) => TodosState.loadTodos(t)) //Invoke the error callBack to handle the error //In states_rebuild there are three level of error handling: //1- global such as this one : (This is considered the default error handler). diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index 5556f163..1a830ded 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -48,7 +48,10 @@ class TodoItem extends StatelessWidget { ); //Here we get the global ReactiveModel, and use the stream method to call the updateTodo. //states_rebuilder will subscribe to this stream and notify observer widgets to rebuild when data is emitted. - RM.get().stream((t) => t.updateTodo(newTodo)).onError( + RM + .get() + .stream((t) => TodosState.updateTodo(t, newTodo)) + .onError( //on Error we want to display a snackbar ErrorHandler.showErrorSnackBar, ); @@ -75,7 +78,7 @@ class TodoItem extends StatelessWidget { final todosStateRM = RM.get(); todosStateRM - .stream((t) => t.deleteTodo(todo)) + .stream((t) => TodosState.deleteTodo(t, todo)) .onError(ErrorHandler.showErrorSnackBar); Scaffold.of(context).showSnackBar( @@ -92,7 +95,7 @@ class TodoItem extends StatelessWidget { onPressed: () { //another nested call of stream method to voluntary add the todo back todosStateRM - .stream((t) => t.addTodo(todo)) + .stream((t) => TodosState.addTodo(t, todo)) .onError(ErrorHandler.showErrorSnackBar); }, ), diff --git a/states_rebuilder/test/todo_state_test.dart b/states_rebuilder/test/todo_state_test.dart index b6dbb10f..d9d2c791 100644 --- a/states_rebuilder/test/todo_state_test.dart +++ b/states_rebuilder/test/todo_state_test.dart @@ -28,7 +28,7 @@ void main() { test( 'should load todos works', () async { - final todosNewState = await todosState.loadTodos(); + final todosNewState = await TodosState.loadTodos(todosState); expect(todosNewState.todos.length, equals(3)); }, ); @@ -36,7 +36,7 @@ void main() { test( 'should filler todos works', () async { - var todosNewState = await todosState.loadTodos(); + var todosNewState = await TodosState.loadTodos(todosState); //all todos expect(todosNewState.todos.length, equals(3)); //active todos @@ -53,7 +53,7 @@ void main() { test( 'should add todo works', () async { - var startingTodosState = await todosState.loadTodos(); + var startingTodosState = await TodosState.loadTodos(todosState); final todoToAdd = Todo('addTask'); final expectedTodosState = startingTodosState.copyWith( @@ -61,7 +61,7 @@ void main() { ); expect( - startingTodosState.addTodo(todoToAdd), + TodosState.addTodo(startingTodosState, todoToAdd), emitsInOrder([expectedTodosState, emitsDone]), ); }, @@ -70,7 +70,7 @@ void main() { test( 'should add todo and remove it on error', () async { - var startingTodosState = await todosState.loadTodos(); + var startingTodosState = await TodosState.loadTodos(todosState); final todoToAdd = Todo('addTask'); @@ -80,7 +80,7 @@ void main() { ); expect( - startingTodosState.addTodo(todoToAdd), + TodosState.addTodo(startingTodosState, todoToAdd), emitsInOrder([ expectedTodosState, startingTodosState, @@ -94,7 +94,7 @@ void main() { test( 'should update todo works', () async { - var startingTodosState = await todosState.loadTodos(); + var startingTodosState = await TodosState.loadTodos(todosState); final updatedTodo = startingTodosState.todos.first.copyWith(task: 'updated task'); @@ -106,7 +106,7 @@ void main() { ); expect( - startingTodosState.updateTodo(updatedTodo), + TodosState.updateTodo(startingTodosState, updatedTodo), emitsInOrder([expectedTodosState, emitsDone]), ); }, @@ -115,14 +115,15 @@ void main() { test( 'should delete todo works', () async { - var startingTodosState = await todosState.loadTodos(); + var startingTodosState = await TodosState.loadTodos(todosState); final expectedTodosState = startingTodosState.copyWith( todos: List.from(startingTodosState.todos)..removeLast(), ); expect( - startingTodosState.deleteTodo(startingTodosState.todos.last), + TodosState.deleteTodo( + startingTodosState, startingTodosState.todos.last), emitsInOrder([expectedTodosState, emitsDone]), ); }, @@ -131,7 +132,7 @@ void main() { test( 'should toggleAll todos works', () async { - var startingTodosState = await todosState.loadTodos(); + var startingTodosState = await TodosState.loadTodos(todosState); expect(startingTodosState.numActive, equals(2)); expect(startingTodosState.numCompleted, equals(1)); @@ -145,7 +146,7 @@ void main() { .toList()); expect( - startingTodosState.toggleAll(), + TodosState.toggleAll(startingTodosState), emitsInOrder([expectedTodosState, emitsDone]), ); expect(expectedTodosState.numActive, equals(0)); @@ -156,7 +157,7 @@ void main() { test( 'should clearCompleted todos works', () async { - var startingTodosState = await todosState.loadTodos(); + var startingTodosState = await TodosState.loadTodos(todosState); expect(startingTodosState.numActive, equals(2)); expect(startingTodosState.numCompleted, equals(1)); @@ -169,7 +170,7 @@ void main() { .toList()); expect( - startingTodosState.clearCompleted(), + TodosState.clearCompleted(startingTodosState), emitsInOrder([expectedTodosState, emitsDone]), ); }, From 169bcb5f0045312dbaf85c9d707ef12b91ac1e4e Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Sat, 16 May 2020 06:06:10 +0100 Subject: [PATCH 09/10] update to use v 2.0.0 --- .../add_edit_screen.dart/add_edit_screen.dart | 9 +++--- .../ui/pages/detail_screen/detail_screen.dart | 22 +++++++------- .../home_screen/extra_actions_button.dart | 14 ++++----- .../ui/pages/home_screen/filter_button.dart | 29 ++++++++++--------- .../lib/ui/pages/home_screen/home_screen.dart | 22 ++++++-------- .../ui/pages/home_screen/stats_counter.dart | 4 +-- .../lib/ui/pages/home_screen/todo_item.dart | 22 +++++++------- .../lib/ui/pages/home_screen/todo_list.dart | 4 +-- states_rebuilder/pubspec.yaml | 2 +- 9 files changed, 61 insertions(+), 67 deletions(-) diff --git a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart index 51117355..0c558d28 100644 --- a/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart +++ b/states_rebuilder/lib/ui/pages/add_edit_screen.dart/add_edit_screen.dart @@ -94,11 +94,10 @@ class _AddEditPageState extends State { } else { Navigator.pop(context); - RM - .get() - .stream( - (t) => TodosState.addTodo(t, Todo(_task, note: _note))) - .onError(ErrorHandler.showErrorSnackBar); + RM.get().setState( + (t) => TodosState.addTodo(t, Todo(_task, note: _note)), + onError: ErrorHandler.showErrorSnackBar, + ); } } }, diff --git a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart index 2d44e829..79e3daff 100644 --- a/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart +++ b/states_rebuilder/lib/ui/pages/detail_screen/detail_screen.dart @@ -48,9 +48,9 @@ class DetailScreen extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Checkbox( key: ArchSampleKeys.detailsTodoItemCheckbox, - value: todosStateRM.value.complete, + value: todosStateRM.state.complete, onChanged: (value) { - final newTodo = todosStateRM.value.copyWith( + final newTodo = todosStateRM.state.copyWith( complete: value, ); _updateTodo(context, newTodo); @@ -66,13 +66,13 @@ class DetailScreen extends StatelessWidget { bottom: 16.0, ), child: Text( - todosStateRM.value.task, + todosStateRM.state.task, key: ArchSampleKeys.detailsTodoItemTask, style: Theme.of(context).textTheme.headline, ), ), Text( - todosStateRM.value.note, + todosStateRM.state.note, key: ArchSampleKeys.detailsTodoItemNote, style: Theme.of(context).textTheme.subhead, ) @@ -97,7 +97,7 @@ class DetailScreen extends StatelessWidget { builder: (context) { return AddEditPage( key: ArchSampleKeys.editTodoScreen, - todo: todoRMKey.value, + todo: todoRMKey.state, ); }, ), @@ -114,13 +114,11 @@ class DetailScreen extends StatelessWidget { } void _updateTodo(BuildContext context, Todo newTodo) { - final oldTodo = todoRMKey.value; - todoRMKey.value = newTodo; - RM - .get() - .stream((t) => TodosState.updateTodo(t, newTodo)) - .onError((ctx, error) { - todoRMKey.value = oldTodo; + final oldTodo = todoRMKey.state; + todoRMKey.state = newTodo; + RM.get().setState((t) => TodosState.updateTodo(t, newTodo), + onError: (ctx, error) { + todoRMKey.state = oldTodo; ErrorHandler.showErrorSnackBar(context, error); }); } diff --git a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart index 469c0278..7e757545 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/extra_actions_button.dart @@ -19,18 +19,16 @@ class ExtraActionsButton extends StatelessWidget { return PopupMenuButton( key: ArchSampleKeys.extraActionsButton, onSelected: (action) { - //first set the value to the new action - //See FilterButton where we use setValue there. - extraActionRM.value = action; + //first set the state to the new action + //See FilterButton where we use setState there. + extraActionRM.state = action; - RM - .get() - .stream( + RM.get().setState( (action == ExtraAction.toggleAllComplete) ? (t) => TodosState.toggleAll(t) : (t) => TodosState.clearCompleted(t), - ) - .onError(ErrorHandler.showErrorSnackBar); + onError: ErrorHandler.showErrorSnackBar, + ); }, itemBuilder: (BuildContext context) { return >[ diff --git a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart index ce1afebc..f9586d9b 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/filter_button.dart @@ -31,7 +31,7 @@ class FilterButton extends StatelessWidget { //register to activeTabRM observe: () => activeTabRM, builder: (context, activeTabRM) { - final _isActive = activeTabRM.value == AppTab.todos; + final _isActive = activeTabRM.state == AppTab.todos; return AnimatedOpacity( opacity: _isActive ? 1.0 : 0.0, duration: Duration(milliseconds: 150), @@ -64,19 +64,22 @@ class _Button extends StatelessWidget { onSelected: (filter) { //Compere this onSelected callBack with that of the ExtraActionsButton widget. // - //In ExtraActionsButton, we did not use the setValue. - //Here we use the setValue (although we can use activeFilterRM.value = filter ). + //In ExtraActionsButton, we did not use the setState. + //Here we use the setState (although we can use activeFilterRM.state = filter ). // - //The reason we use setValue is to minimize the rebuild process. - //If the use select the same option, the setValue method will not notify observers. + //The reason we use setState is to minimize the rebuild process. + //If the use select the same option, the setState method will not notify observers. //and onData will not invoked. - activeFilterRM.setValue( - () => filter, + activeFilterRM.setState( + (_) => filter, onData: (_, __) { - //get and set the value of the global ReactiveModel TodosStore - RM.get().value = - RM.get().value.copyWith(activeFilter: filter); + //get and set the state of the global ReactiveModel TodosStore + RM.get().setState( + (currentSate) => currentSate.copyWith( + activeFilter: filter, + ), + ); }, ); }, @@ -87,7 +90,7 @@ class _Button extends StatelessWidget { value: VisibilityFilter.all, child: Text( ArchSampleLocalizations.of(context).showAll, - style: activeFilterRM.value == VisibilityFilter.all + style: activeFilterRM.state == VisibilityFilter.all ? activeStyle : defaultStyle, ), @@ -97,7 +100,7 @@ class _Button extends StatelessWidget { value: VisibilityFilter.active, child: Text( ArchSampleLocalizations.of(context).showActive, - style: activeFilterRM.value == VisibilityFilter.active + style: activeFilterRM.state == VisibilityFilter.active ? activeStyle : defaultStyle, ), @@ -107,7 +110,7 @@ class _Button extends StatelessWidget { value: VisibilityFilter.completed, child: Text( ArchSampleLocalizations.of(context).showCompleted, - style: activeFilterRM.value == VisibilityFilter.completed + style: activeFilterRM.state == VisibilityFilter.completed ? activeStyle : defaultStyle, ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart index 770597e4..95ccf065 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/home_screen.dart @@ -39,15 +39,11 @@ class HomeScreen extends StatelessWidget { //we use the HomeScreen seed so that if other pages emits a notification this widget will not be notified () => RM.get().asNew(HomeScreen) //using the cascade operator, we call the todosLoad method informing states_rebuilder that is is a future - ..future((t) => TodosState.loadTodos(t)) - //Invoke the error callBack to handle the error - //In states_rebuild there are three level of error handling: - //1- global such as this one : (This is considered the default error handler). - //2- semi-global : for onError defined in setState and setValue methods. - // When defined it will override the gobble error handler. - //3- local-global, for onError defined in the StateBuilder and OnSetStateListener widgets. - // they override the global and semi-global error for the widget where it is defined - .onError(ErrorHandler.showErrorDialog), + ..setState( + (t) => TodosState.loadTodos(t), + //Invoke the error callBack to handle the error + onError: ErrorHandler.showErrorDialog, + ), //Her we subscribe to the activeTab ReactiveModel key () => _activeTabRMKey, ], @@ -63,7 +59,7 @@ class HomeScreen extends StatelessWidget { //WhenRebuilderOr has other optional callBacks (onData, onIdle, onError). //the builder is the default one. builder: (context, _activeTabRM) { - return _activeTabRM.value == AppTab.todos + return _activeTabRM.state == AppTab.todos ? TodoList() : StatsCounter(); }, @@ -89,13 +85,13 @@ class HomeScreen extends StatelessWidget { builder: (context, _activeTabRM) { return BottomNavigationBar( key: ArchSampleKeys.tabs, - currentIndex: AppTab.values.indexOf(_activeTabRM.value), + currentIndex: AppTab.values.indexOf(_activeTabRM.state), onTap: (index) { - //mutate the value of the private field _activeTabRM, + //mutate the state of the private field _activeTabRM, //observing widget will be notified to rebuild //We have three observing widgets : this StateBuilder, the WhenRebuilderOr, //ond the StateBuilder defined in the FilterButton widget - _activeTabRM.value = AppTab.values[index]; + _activeTabRM.state = AppTab.values[index]; }, items: AppTab.values.map( (tab) { diff --git a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart index 4d0dbbc3..1842a3cc 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/stats_counter.dart @@ -29,7 +29,7 @@ class StatsCounter extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: 24.0), child: Text( - '${todosStateRM.value.numCompleted}', + '${todosStateRM.state.numCompleted}', key: ArchSampleKeys.statsNumCompleted, style: Theme.of(context).textTheme.subhead, ), @@ -44,7 +44,7 @@ class StatsCounter extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: 24.0), child: Text( - '${todosStateRM.value.numActive}', + '${todosStateRM.state.numActive}', key: ArchSampleKeys.statsNumActive, style: Theme.of(context).textTheme.subhead, ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart index 1a830ded..d7ac990f 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_item.dart @@ -48,12 +48,10 @@ class TodoItem extends StatelessWidget { ); //Here we get the global ReactiveModel, and use the stream method to call the updateTodo. //states_rebuilder will subscribe to this stream and notify observer widgets to rebuild when data is emitted. - RM - .get() - .stream((t) => TodosState.updateTodo(t, newTodo)) - .onError( + RM.get().setState( + (t) => TodosState.updateTodo(t, newTodo), //on Error we want to display a snackbar - ErrorHandler.showErrorSnackBar, + onError: ErrorHandler.showErrorSnackBar, ); }, ), @@ -77,9 +75,10 @@ class TodoItem extends StatelessWidget { //get the global ReactiveModel, because we want to update the view of the list after removing a todo final todosStateRM = RM.get(); - todosStateRM - .stream((t) => TodosState.deleteTodo(t, todo)) - .onError(ErrorHandler.showErrorSnackBar); + todosStateRM.setState( + (t) => TodosState.deleteTodo(t, todo), + onError: ErrorHandler.showErrorSnackBar, + ); Scaffold.of(context).showSnackBar( SnackBar( @@ -94,9 +93,10 @@ class TodoItem extends StatelessWidget { label: ArchSampleLocalizations.of(context).undo, onPressed: () { //another nested call of stream method to voluntary add the todo back - todosStateRM - .stream((t) => TodosState.addTodo(t, todo)) - .onError(ErrorHandler.showErrorSnackBar); + todosStateRM.setState( + (t) => TodosState.addTodo(t, todo), + onError: ErrorHandler.showErrorSnackBar, + ); }, ), ), diff --git a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart index ff9559fb..1cfacabb 100644 --- a/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart +++ b/states_rebuilder/lib/ui/pages/home_screen/todo_list.dart @@ -16,7 +16,7 @@ class TodoList extends StatelessWidget { //As this is the main list of todos, and this list can be update from //many widgets and screens (FilterButton, ExtraActionsButton, AddEditScreen, ..) //We register this widget with the global injected ReactiveModel. - //Anywhere in the widget tree if setValue of todosStore is called this StatesRebuild + //Anywhere in the widget tree if setState of todosStore is called this StatesRebuild // will rebuild //In states_rebuild global ReactiveModel is the model that can be invoked all across the widget tree //and local ReactiveModel is a model that is meant to be called only locally in the widget where it is created @@ -24,7 +24,7 @@ class TodoList extends StatelessWidget { builder: (context, todosStoreRM) { //The builder exposes the BuildContext and the ReactiveModel of todosStore - final todos = todosStoreRM.value.todos; + final todos = todosStoreRM.state.todos; return ListView.builder( key: ArchSampleKeys.todoList, itemCount: todos.length, diff --git a/states_rebuilder/pubspec.yaml b/states_rebuilder/pubspec.yaml index 4c585945..21bcaa45 100644 --- a/states_rebuilder/pubspec.yaml +++ b/states_rebuilder/pubspec.yaml @@ -19,7 +19,7 @@ environment: dependencies: flutter: sdk: flutter - states_rebuilder: ^1.15.0 + states_rebuilder: ^2.0.0 key_value_store_flutter: key_value_store_web: shared_preferences: From d0f469f1d9e62a3b23144495f6240c501d684588 Mon Sep 17 00:00:00 2001 From: MELLATI Fatah Date: Sun, 24 May 2020 19:15:01 +0100 Subject: [PATCH 10/10] update to use states_rebuilder version 2.1.0 --- states_rebuilder/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/states_rebuilder/pubspec.yaml b/states_rebuilder/pubspec.yaml index 21bcaa45..32f3636f 100644 --- a/states_rebuilder/pubspec.yaml +++ b/states_rebuilder/pubspec.yaml @@ -19,7 +19,7 @@ environment: dependencies: flutter: sdk: flutter - states_rebuilder: ^2.0.0 + states_rebuilder: ^2.1.0 key_value_store_flutter: key_value_store_web: shared_preferences: