Jonatas

improve framework structure, added StateMixin, prepare to getx 4.0

... ... @@ -3,43 +3,22 @@ import 'package:get/get.dart';
import '../../domain/adapters/repository_adapter.dart';
import '../../domain/entity/cases_model.dart';
enum Status { loading, success, error }
class HomeController extends GetxController {
class HomeController extends GetxController with StatusMixin<CasesModel> {
HomeController({this.homeRepository});
/// inject repo abstraction dependency
final IHomeRepository homeRepository;
/// create a reactive status from request with initial value = loading
final status = Status.loading.obs;
/// create a reactive CasesModel. CasesModel().obs has same result
final cases = Rx<CasesModel>();
/// When the controller is initialized, make the http request
@override
void onInit() {
super.onInit();
fetchDataFromApi();
}
/// fetch cases from Api
Future<void> fetchDataFromApi() async {
/// When the repository returns the value, change the status to success,
/// and fill in "cases"
return homeRepository.getCases().then(
(data) {
cases(data);
status(Status.success);
},
/// In case of error, print the error and change the status
/// to Status.error
onError: (err) {
print("$err");
return status(Status.error);
},
);
// show loading on start, data on success
// and error message on error with 0 boilerplate
homeRepository.getCases().then((data) {
change(data, status: RxStatus.success());
}, onError: (err) {
change(null, status: RxStatus.error(err.toString()));
});
}
}
... ...
... ... @@ -29,9 +29,9 @@ class CountryView extends GetView<HomeController> {
),
body: Center(
child: ListView.builder(
itemCount: controller.cases.value.countries.length,
itemCount: controller.value.countries.length,
itemBuilder: (context, index) {
final country = controller.cases.value.countries[index];
final country = controller.value.countries[index];
return ListTile(
onTap: () {
Get.toNamed('/details', arguments: country);
... ...
... ... @@ -27,11 +27,8 @@ class HomeView extends GetView<HomeController> {
centerTitle: true,
),
body: Center(
child: Obx(
() {
final status = controller.status.value;
if (status == Status.loading) return CircularProgressIndicator();
if (status == Status.error) return Text('Error on connection :(');
child: controller(
(state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
... ... @@ -45,7 +42,7 @@ class HomeView extends GetView<HomeController> {
),
),
Text(
'${controller.cases.value.global.totalConfirmed}',
'${state.global.totalConfirmed}',
style: TextStyle(fontSize: 45, fontWeight: FontWeight.bold),
),
SizedBox(
... ... @@ -58,7 +55,7 @@ class HomeView extends GetView<HomeController> {
),
),
Text(
'${controller.cases.value.global.totalDeaths}',
'${state.global.totalDeaths}',
style: TextStyle(fontSize: 45, fontWeight: FontWeight.bold),
),
SizedBox(
... ...
... ... @@ -59,18 +59,18 @@ void main() {
expect(controller.initialized, true);
/// check initial Status
expect(controller.status.value, Status.loading);
expect(controller.status.isLoading, true);
/// await time request
await Future.delayed(Duration(milliseconds: 100));
if (controller.status.value == Status.error) {
expect(controller.cases.value, null);
if (controller.status.isError) {
expect(controller.value, null);
}
if (controller.status.value == Status.success) {
expect(controller.cases.value.global.totalDeaths, 100);
expect(controller.cases.value.global.totalConfirmed, 200);
if (controller.status.isSuccess) {
expect(controller.value.global.totalDeaths, 100);
expect(controller.value.global.totalConfirmed, 200);
}
});
... ...
import 'package:meta/meta.dart';
import '../../get_core/get_core.dart';
/// Special callable class to keep the contract of a regular method, and avoid
... ... @@ -18,37 +17,34 @@ class _InternalFinalCallback<T> {
/// ```dart
/// class SomeController with GetLifeCycle {
/// SomeController() {
/// initLifeCycle();
/// configureLifeCycle();
/// }
/// }
/// ```
mixin GetLifeCycle {
/// The `initLifeCycle` works as a constructor for the [GetLifeCycle]
///
/// This method must be invoked in the constructor of the implementation
void initLifeCycle() {
onStart._callback = _onStart;
onDelete._callback = _onDelete;
}
mixin GetLifeCycleBase {
/// Called at the exact moment the widget is allocated in memory.
/// It uses an internal "callable" type, to avoid any @overrides in subclases.
/// This method should be internal and is required to define the
/// lifetime cycle of the subclass.
final onStart = _InternalFinalCallback<void>();
// /// The `configureLifeCycle` works as a constructor for the [GetLifeCycle]
// ///
// /// This method must be invoked in the constructor of the implementation
// void configureLifeCycle() {
// if (_initialized) return;
// }
/// Internal callback that starts the cycle of this controller.
final onDelete = _InternalFinalCallback<void>();
/// Called immediately after the widget is allocated in memory.
/// You might use this to initialize something for the controller.
@mustCallSuper
void onInit() {}
/// Called 1 frame after onInit(). It is the perfect place to enter
/// navigation events, like snackbar, dialogs, or a new route, or
/// async request.
@mustCallSuper
void onReady() {}
/// Called before [onDelete] method. [onClose] might be used to
... ... @@ -57,7 +53,6 @@ mixin GetLifeCycle {
/// Or dispose objects that can potentially create some memory leaks,
/// like TextEditingControllers, AnimationControllers.
/// Might be useful as well to persist some data on disk.
@mustCallSuper
void onClose() {}
bool _initialized = false;
... ... @@ -83,6 +78,26 @@ mixin GetLifeCycle {
_isClosed = true;
onClose();
}
void $configureLifeCycle() {
_checkIfAlreadyConfigured();
onStart._callback = _onStart;
onDelete._callback = _onDelete;
}
void _checkIfAlreadyConfigured() {
if (_initialized) {
throw """You can only call configureLifeCycle once.
The proper place to insert it is in your class's constructor
that inherits GetLifeCycle.""";
}
}
}
abstract class GetLifeCycle with GetLifeCycleBase {
GetLifeCycle() {
$configureLifeCycle();
}
}
/// Allow track difference between GetxServices and GetxControllers
... ...
... ... @@ -10,11 +10,7 @@ import '../../../get_instance/src/lifecycle.dart';
/// it is Get.reset().
abstract class GetxService extends DisposableInterface with GetxServiceMixin {}
abstract class DisposableInterface with GetLifeCycle {
DisposableInterface() {
initLifeCycle();
}
abstract class DisposableInterface extends GetLifeCycle {
/// Called immediately after the widget is allocated in memory.
/// You might use this to initialize something for the controller.
@override
... ... @@ -29,7 +25,9 @@ abstract class DisposableInterface with GetLifeCycle {
/// async request.
@override
@mustCallSuper
void onReady() {}
void onReady() {
super.onReady();
}
/// Called before [onDelete] method. [onClose] might be used to
/// dispose resources used by the controller. Like closing events,
... ... @@ -38,5 +36,7 @@ abstract class DisposableInterface with GetLifeCycle {
/// like TextEditingControllers, AnimationControllers.
/// Might be useful as well to persist some data on disk.
@override
void onClose() {}
void onClose() {
super.onClose();
}
}
... ...
... ... @@ -5,29 +5,77 @@ import '../../../instance_manager.dart';
import '../../get_state_manager.dart';
import '../simple/list_notifier.dart';
class Value<T> extends ListNotifier implements ValueListenable<T> {
Value(this._value);
mixin StatusMixin<T> on ListNotifier {
T _value;
RxStatus _status;
bool _isNullOrEmpty(dynamic val) {
if (val == null) return true;
var result = false;
if (val is Iterable) {
result = val.isEmpty;
} else if (val is String) {
result = val.isEmpty;
} else if (val is Map) {
result = val.isEmpty;
}
return result;
}
void _fillEmptyStatus() {
_status = _isNullOrEmpty(_value) ? RxStatus.loading() : RxStatus.success();
}
RxStatus get status {
notifyChildrens();
return _status ??= _status = RxStatus.loading();
}
T get value {
notifyChildrens();
return _value;
}
@override
String toString() => value.toString();
T _value;
set value(T newValue) {
if (_value == newValue) return;
_value = newValue;
updater();
refresh();
}
@protected
void change(T newState, {RxStatus status}) {
var _canUpdate = false;
if (status != null) {
_status = status;
_canUpdate = true;
}
if (newState != _value) {
_value = newState;
_canUpdate = true;
}
if (_canUpdate) {
refresh();
}
}
}
class Value<T> extends ListNotifier
with StatusMixin<T>
implements ValueListenable<T> {
Value(T val) {
_value = val;
_fillEmptyStatus();
}
void update(void fn(T value)) {
fn(value);
updater();
refresh();
}
@override
String toString() => value.toString();
dynamic toJson() => (value as dynamic)?.toJson();
}
extension ReactiveT<T> on T {
... ... @@ -36,10 +84,9 @@ extension ReactiveT<T> on T {
typedef Condition = bool Function();
abstract class GetNotifier<T> extends Value<T> with GetLifeCycle {
abstract class GetNotifier<T> extends Value<T> with GetLifeCycleBase {
GetNotifier(T initial) : super(initial) {
initLifeCycle();
_fillEmptyStatus();
$configureLifeCycle();
}
@override
... ... @@ -48,32 +95,9 @@ abstract class GetNotifier<T> extends Value<T> with GetLifeCycle {
super.onInit();
SchedulerBinding.instance?.addPostFrameCallback((_) => onReady());
}
}
RxStatus _status;
bool get isNullOrEmpty {
if (_value == null) return true;
dynamic val = _value;
var result = false;
if (val is Iterable) {
result = val.isEmpty;
} else if (val is String) {
result = val.isEmpty;
} else if (val is Map) {
result = val.isEmpty;
}
return result;
}
void _fillEmptyStatus() {
_status = isNullOrEmpty ? RxStatus.loading() : RxStatus.success();
}
RxStatus get status {
notifyChildrens();
return _status;
}
extension StateExt<T> on StatusMixin<T> {
Widget call(NotifierBuilder<T> widget, {Widget onError, Widget onLoading}) {
assert(widget != null);
return SimpleBuilder(builder: (_) {
... ... @@ -86,24 +110,6 @@ abstract class GetNotifier<T> extends Value<T> with GetLifeCycle {
}
});
}
@protected
void change(T newState, {RxStatus status}) {
var _canUpdate = false;
if (status != null) {
_status = status;
_canUpdate = true;
}
if (newState != _value) {
_value = newState;
_canUpdate = true;
}
if (_canUpdate) {
updater();
}
}
dynamic toJson() => (value as dynamic)?.toJson();
}
class RxStatus {
... ...
import 'dart:collection';
import 'package:flutter/material.dart';
import '../../../get_core/get_core.dart';
import '../../../get_instance/src/get_instance.dart';
import '../../get_state_manager.dart';
// Changed to VoidCallback.
//typedef Disposer = void Function();
// replacing StateSetter, return if the Widget is mounted for extra validation.
// if it brings overhead the extra call,
typedef GetStateUpdate = void Function();
//typedef GetStateUpdate = void Function(VoidCallback fn);
import 'list_notifier.dart';
/// Complies with [GetStateUpdater]
///
... ... @@ -31,14 +23,8 @@ mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {
}
}
class GetxController extends DisposableInterface {
final _updaters = <GetStateUpdate>[];
// final _updatersIds = HashMap<String, StateSetter>(); //<old>
final _updatersIds = HashMap<String, GetStateUpdate>();
final _updatersGroupIds = HashMap<String, List<GetStateUpdate>>();
// ignore: prefer_mixin
class GetxController extends DisposableInterface with ListNotifier {
/// Rebuilds [GetBuilder] each time you call [update()];
/// Can take a List of [ids], that will only update the matching
/// `GetBuilder( id: )`,
... ... @@ -49,73 +35,13 @@ class GetxController extends DisposableInterface {
return;
}
if (ids == null) {
// _updaters?.forEach((rs) => rs(() {})); //<old>
for (final updater in _updaters) {
updater();
}
refresh();
} else {
// @jonny, remove this commented code if it's not more optimized.
// for (final id in ids) {
// if (_updatersIds[id] != null) _updatersIds[id]();
// if (_updatersGroupIds[id] != null)
// for (final rs in _updatersGroupIds[id]) rs();
// }
for (final id in ids) {
_updatersIds[id]?.call();
// ignore: avoid_function_literals_in_foreach_calls
_updatersGroupIds[id]?.forEach((rs) => rs());
refreshGroup(id);
}
}
}
// VoidCallback addListener(StateSetter listener) {//<old>
VoidCallback addListener(GetStateUpdate listener) {
_updaters.add(listener);
return () => _updaters.remove(listener);
}
// VoidCallback addListenerId(String key, StateSetter listener) {//<old>
VoidCallback addListenerId(String key, GetStateUpdate listener) {
// _printCurrentIds();
if (_updatersIds.containsKey(key)) {
_updatersGroupIds[key] ??= <GetStateUpdate>[];
_updatersGroupIds[key].add(listener);
return () {
_updatersGroupIds[key].remove(listener);
};
} else {
_updatersIds[key] = listener;
return () => _updatersIds.remove(key);
}
}
/// To dispose an [id] from future updates(), this ids are registered
/// by [GetBuilder()] or similar, so is a way to unlink the state change with
/// the Widget from the Controller.
void disposeId(String id) {
_updatersIds.remove(id);
_updatersGroupIds.remove(id);
}
/// Remove this after checking the new implementation makes sense.
/// Uncomment this if you wanna control the removal of ids..
/// bool _debugging = false;
/// Future<void> _printCurrentIds() async {
/// if (_debugging) return;
/// _debugging = true;
/// print('about to debug...');
/// await Future.delayed(Duration(milliseconds: 10));
/// int totalGroups = 0;
/// _updatersGroupIds.forEach((key, value) {
/// totalGroups += value.length;
/// });
/// int totalIds = _updatersIds.length;
/// print(
/// 'Total: ${totalIds + totalGroups},'+
/// 'in groups:$totalGroups, solo ids:$totalIds',);
/// _debugging = false;
/// }
}
typedef GetControllerBuilder<T extends DisposableInterface> = Widget Function(
... ... @@ -187,10 +113,6 @@ class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
controller?.onStart();
}
// if (widget.global && Get.smartManagement ==
//SmartManagement.onlyBuilder) {
// controller?.onStart();
// }
_subscribeToController();
}
... ... @@ -200,20 +122,10 @@ class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
void _subscribeToController() {
remove?.call();
remove = (widget.id == null)
// ? controller?.addListener(setState) //<old>
// : controller?.addListenerId(widget.id, setState); //<old>
? controller?.addListener(getUpdate)
: controller?.addListenerId(widget.id, getUpdate);
}
/// Sample for [GetStateUpdate] when you don't wanna
/// use [GetStateHelper mixin].
/// bool _getUpdater() {
/// final _mounted = mounted;
/// if (_mounted) setState(() {});
/// return _mounted;
/// }
@override
void dispose() {
super.dispose();
... ... @@ -249,26 +161,6 @@ class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
Widget build(BuildContext context) => widget.builder(controller);
}
/// This is a experimental feature.
/// Meant to be used with SimpleBuilder, it auto-registers the variable
/// like Rx() does with Obx().
// class Value<T> extends GetxController {
// Value([this._value]);
// T _value;
// T get value {
// TaskManager.instance.notify(_updaters);
// return _value;
// }
// set value(T newValue) {
// if (_value == newValue) return;
// _value = newValue;
// update();
// }
// }
/// It's Experimental class, the Api can be change
abstract class GetState<T> extends GetxController {
GetState(T initialValue) {
... ...
... ... @@ -146,5 +146,3 @@
// return widget.builder(controller.state);
// }
//}
... ...
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'simple_builder.dart';
// This callback remove the listener on addListener function
typedef Disposer = void Function();
// replacing StateSetter, return if the Widget is mounted for extra validation.
// if it brings overhead the extra call,
typedef GetStateUpdate = void Function();
class ListNotifier implements Listenable {
List<VoidCallback> _listeners = <VoidCallback>[];
List<GetStateUpdate> _updaters = <GetStateUpdate>[];
HashMap<String, List<GetStateUpdate>> _updatersGroupIds =
HashMap<String, List<GetStateUpdate>>();
@protected
void updater() {
void refresh() {
assert(_debugAssertNotDisposed());
for (var element in _listeners) {
for (var element in _updaters) {
element();
}
}
@protected
void refreshGroup(String id) {
assert(_debugAssertNotDisposed());
if (_updatersGroupIds.containsKey(id)) {
for (var item in _updatersGroupIds[id]) {
item();
}
}
}
bool _debugAssertNotDisposed() {
assert(() {
if (_listeners == null) {
if (_updaters == null) {
throw FlutterError('''A $runtimeType was used after being disposed.\n
'Once you have called dispose() on a $runtimeType, it can no longer be used.''');
}
... ... @@ -26,29 +46,87 @@ class ListNotifier implements Listenable {
@protected
void notifyChildrens() {
TaskManager.instance.notify(_listeners);
TaskManager.instance.notify(_updaters);
}
bool get hasListeners {
assert(_debugAssertNotDisposed());
return _listeners.isNotEmpty;
return _updaters.isNotEmpty;
}
@override
void addListener(VoidCallback listener) {
void removeListener(VoidCallback listener) {
assert(_debugAssertNotDisposed());
_listeners.add(listener);
_updaters.remove(listener);
}
@override
void removeListener(VoidCallback listener) {
void removeListenerId(String id, VoidCallback listener) {
assert(_debugAssertNotDisposed());
_listeners.remove(listener);
if (_updatersGroupIds.containsKey(id)) {
_updatersGroupIds[id].remove(listener);
}
_updaters.remove(listener);
}
@mustCallSuper
void dispose() {
assert(_debugAssertNotDisposed());
_listeners = null;
_updaters = null;
_updatersGroupIds = null;
}
@override
Disposer addListener(GetStateUpdate listener) {
assert(_debugAssertNotDisposed());
_updaters.add(listener);
return () => _updaters.remove(listener);
}
Disposer addListenerId(String key, GetStateUpdate listener) {
_updatersGroupIds[key] ??= <GetStateUpdate>[];
_updatersGroupIds[key].add(listener);
return () => _updatersGroupIds[key].remove(listener);
}
/// To dispose an [id] from future updates(), this ids are registered
/// by [GetBuilder()] or similar, so is a way to unlink the state change with
/// the Widget from the Controller.
void disposeId(String id) {
_updatersGroupIds.remove(id);
}
}
class TaskManager {
TaskManager._();
static TaskManager _instance;
static TaskManager get instance => _instance ??= TaskManager._();
GetStateUpdate _setter;
List<VoidCallback> _remove;
void notify(List<GetStateUpdate> _updaters) {
if (_setter != null) {
if (!_updaters.contains(_setter)) {
_updaters.add(_setter);
_remove.add(() => _updaters.remove(_setter));
}
}
}
Widget exchange(
List<VoidCallback> disposers,
GetStateUpdate setState,
Widget Function(BuildContext) builder,
BuildContext context,
) {
_remove = disposers;
_setter = setState;
final result = builder(context);
_remove = null;
_setter = null;
return result;
}
}
... ...
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'get_state.dart';
import 'list_notifier.dart';
typedef ValueBuilderUpdateCallback<T> = void Function(T snapshot);
typedef ValueBuilderBuilder<T> = Widget Function(
... ... @@ -87,7 +88,7 @@ class SimpleBuilder extends StatefulWidget {
class _SimpleBuilderState extends State<SimpleBuilder>
with GetStateUpdaterMixin {
final disposers = <VoidCallback>[];
final disposers = <Disposer>[];
@override
void dispose() {
... ... @@ -107,38 +108,3 @@ class _SimpleBuilderState extends State<SimpleBuilder>
);
}
}
class TaskManager {
TaskManager._();
static TaskManager _instance;
static TaskManager get instance => _instance ??= TaskManager._();
GetStateUpdate _setter;
List<VoidCallback> _remove;
void notify(List<GetStateUpdate> _updaters) {
if (_setter != null) {
if (!_updaters.contains(_setter)) {
_updaters.add(_setter);
_remove.add(() => _updaters.remove(_setter));
}
}
}
Widget exchange(
List<VoidCallback> disposers,
GetStateUpdate setState,
Widget Function(BuildContext) builder,
BuildContext context,
) {
_remove = disposers;
_setter = setState;
final result = builder(context);
_remove = null;
_setter = null;
return result;
}
}
... ...
... ... @@ -9,7 +9,6 @@ environment:
dependencies:
flutter:
sdk: flutter
meta: 1.3.0-nullsafety.3
dev_dependencies:
flutter_test:
... ...
... ... @@ -9,11 +9,7 @@ class Mock {
}
}
class DisposableController with GetLifeCycle {
DisposableController() {
initLifeCycle();
}
}
class DisposableController extends GetLifeCycle {}
// ignore: one_member_abstracts
abstract class Service {
... ...