Skip to content

Instantly share code, notes, and snippets.

@ValeriusGC
Last active May 9, 2022 10:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ValeriusGC/9a85b831c59041883332dd356b689f2f to your computer and use it in GitHub Desktop.
Save ValeriusGC/9a85b831c59041883332dd356b689f2f to your computer and use it in GitHub Desktop.
GetRxDecorator - UDF for Getx variables
import 'package:equatable/equatable.dart';
import 'package:get/get_rx/get_rx.dart';
/// GetRxDecorator for Rx<T> variables in Get library [https://pub.dev/packages/get]
/// This wrapper lets to apply UDF concept and makes it easier
/// to work with Getx' Rx<T> and Obx.
///
/// ============================================================================
///
/// Why one need to use this decorator? Because of problem with Rx<T> variables:
///
/// Mainly, direct using this reactive variables violates all of known
/// design patterns. Client (some View) send command to change state
/// and get result immediately without Model processing.
///
/// We should to hide variable itself behind accessors, and this decorator
/// makes it in very handy way.
///
/// ============================================================================
///
/// Without GetRxDecorator
///
/// ```dart
/// // declarations:
///
/// /// 1. Variable itself.
/// final _clickCounterUdf = 0.obs;
///
/// /// 2. Getter
/// int get clickCounterUdf => _clickCounterUdf();
///
/// /// 3. Setter
/// set clickCounterUdf(int v) => _clickCounterUdf(_process(v));
///
/// /// 4. Stream
/// Stream<int> get clickCounterStreamUdf => _clickCounterUdf.stream;
///
/// /// This processor drops values above 3 down to zero.
/// int _process(int v) => v > 3 ? 0 : v;
///
/// //=======================================================
///
/// // using:
/// // Somewhere in View
/// return Center(
/// child: ElevatedButton(
/// child: Obx(
/// () => Text('value = ${controller.clickCounterUdf}'),
/// ),
/// onPressed: () => controller.clickCounterUdf++,
/// ),
/// );
/// ```
///
/// With GetRxDecorator
///
/// ```dart
///
/// // declaration:
///
/// /// Encapsulated Rx variable
/// late var clickCounterDecor = 0.obsDeco(setter: (_, newValue, __) =>
/// _process(newValue ?? 0));
///
/// //=========================================================================
///
/// // using:
/// // Somewhere in View
/// return Center(
/// child: ElevatedButton(
/// child: Obx(
/// () => Text('value = ${controller.clickCounterDecor}'),
/// ),
/// onPressed: () => controller.clickCounterDecor++,
/// ),
/// );
///
/// ```
class GetRxDecorator<T> extends Equatable {
GetRxDecorator(T initial, {this.autoRefresh, this.setter})
: _src = Rx<T>(initial);
/// Inner .obs variable.
final Rx<T> _src;
/// Callback for adjust custom setter.
/// [oldValue] parameter allows to apply specific algorithms,
/// e.g. without [newValue], like a Collatz conjecture (see tests).
/// [args] parameter allows using additional arguments in algorithms,
/// e.g. type or instance of variable's sender or something (see tests).
final GetRxDecoratorSetter<T>? setter;
/// Decorates getter.
T get value => _src();
/// Decorates setter.
set value(T? val) => _setValue(newValue: val);
/// Decorates .obs inner stream.
Stream<T> get stream => _src.stream;
/// Force auto refresh.
final bool? autoRefresh;
/// Decorates .call([T? value]) but with additional [args] parameter.
///
/// Use [this] as functional object with parameters:
/// value means new value. Optional.
/// args means additional argument(s) for some use cases. Optional.
T call([T? value, dynamic args]) {
_setValue(newValue: value, args: args);
return this.value;
}
/// Additional setter in cases when no need to change value itself.
/// For example, when every call changes inner value only depending
/// on external arguments (see Collatz conjecture setter test)
T args(dynamic args) => call(_src(), args);
/// We can use either custom setter or default setting mechanism.
///
/// newValue: value to be set. It may be null in some logic cases. Optional.
/// args: optional dynamic parameter. In some cases, you may need
/// an additional call context to select the logic of changing a variable.
void _setValue({T? newValue, dynamic args}) {
// Prepare for adjust latter auto refresh.
final isSameValue = newValue == _src();
if (setter == null) {
_src(newValue);
} else {
// Here we can return as `wrong` either [oldValue] or null.
final candidate = setter!(value, newValue, args);
_src(candidate);
}
if (isSameValue && (autoRefresh ?? false)) {
refresh();
}
}
void refresh() => _src.refresh();
/// Same as `toString()` but using a getter.
String get string => value.toString();
@override
String toString() => value.toString();
@override
List<Object?> get props => [_src];
}
/// Type of callback to change value in UDF manner.
/// [oldValue] passes currentValue.
/// [newValue] passes value to apply.
/// [args] passes additional arguments.
typedef GetRxDecoratorSetter<T> = T? Function(
T oldValue, T? newValue, dynamic args);
////////////////////////////////////////////////////////////////////////////////
///
class GetRxDecoratorInt extends GetRxDecorator<int> {
GetRxDecoratorInt(int initial,
{bool? autoRefresh, GetRxDecoratorSetter<int>? setter})
: super(initial, autoRefresh: autoRefresh, setter: setter);
/// Addition operator.
GetRxDecoratorInt operator +(int add) {
call(_src.value + add);
return this;
}
/// Subtraction operator.
GetRxDecoratorInt operator -(int sub) {
call(_src.value - sub);
return this;
}
}
///
extension IntGetRxDecoratorX on int {
GetRxDecoratorInt obsDeco(
{bool? autoRefresh, GetRxDecoratorSetter<int>? setter}) =>
GetRxDecoratorInt(this, autoRefresh: autoRefresh, setter: setter);
}
///
class GetRxDecoratorDouble extends GetRxDecorator<double> {
GetRxDecoratorDouble(double initial,
{bool? autoRefresh, GetRxDecoratorSetter<double>? setter})
: super(initial, autoRefresh: autoRefresh, setter: setter);
/// Addition operator.
GetRxDecoratorDouble operator +(double add) {
call(_src.value + add);
return this;
}
/// Subtraction operator.
GetRxDecoratorDouble operator -(double sub) {
call(_src.value - sub);
return this;
}
}
///
extension DoubleGetRxDecoratorX on double {
GetRxDecoratorDouble obsDeco(
{bool? autoRefresh, GetRxDecoratorSetter<double>? setter}) =>
GetRxDecoratorDouble(this, autoRefresh: autoRefresh, setter: setter);
}
///
class GetRxDecoratorBool extends GetRxDecorator<bool> {
GetRxDecoratorBool(bool initial,
{bool? autoRefresh, GetRxDecoratorSetter<bool>? setter})
: super(initial, autoRefresh: autoRefresh, setter: setter);
GetRxDecoratorBool toggle() {
call(_src.value = !_src.value);
return this;
}
}
///
extension BoolGetRxDecoratorX on bool {
GetRxDecoratorBool obsDeco(
{bool? autoRefresh, GetRxDecoratorSetter<bool>? setter}) =>
GetRxDecoratorBool(this, autoRefresh: autoRefresh, setter: setter);
}
///
class GetRxDecoratorString extends GetRxDecorator<String>
implements Comparable<String>, Pattern {
GetRxDecoratorString(String initial,
{bool? autoRefresh, GetRxDecoratorSetter<String>? setter})
: super(initial, autoRefresh: autoRefresh, setter: setter);
GetRxDecoratorString operator +(String add) {
call(_src.value + add);
return this;
}
@override
Iterable<Match> allMatches(String string, [int start = 0]) {
return _src.value.allMatches(string, start);
}
@override
Match? matchAsPrefix(String string, [int start = 0]) {
return _src.value.matchAsPrefix(string, start);
}
@override
int compareTo(String other) {
return _src.value.compareTo(other);
}
}
///
extension StringGetRxDecoratorX on String {
GetRxDecoratorString obsDeco(
{bool? autoRefresh, GetRxDecoratorSetter<String>? setter}) =>
GetRxDecoratorString(this, autoRefresh: autoRefresh, setter: setter);
}
void main() {
group('GetRxDecorator tests', () {
///
test('GetRxDecorator: equality test', () async {
{
final v1 = 'a'.obsDeco();
final v2 = 'a'.obsDeco();
expect(v1, equals(v2));
}
{
final v1 = GetRxDecorator(const ['a', 'b']);
final v2 = GetRxDecorator(const ['a', 'b']);
expect(v1, equals(v2));
}
{
final v1 = GetRxDecorator(const {
1: ['a', 'b']
});
final v2 = GetRxDecorator(const {
1: ['a', 'b']
});
expect(v1, equals(v2));
}
{
final v1 = 'a'.obsDeco();
final v2 = 'b'.obsDeco();
expect(v1, isNot(equals(v2)));
}
{
final v1 = GetRxDecorator(const ['a', 'b']);
final v2 = GetRxDecorator(const ['a', 'c']);
expect(v1, isNot(equals(v2)));
}
{
final v1 = GetRxDecorator(const {
1: ['a', 'b']
});
final v2 = GetRxDecorator(const {
1: ['d', 'b']
});
expect(v1, isNot(equals(v2)));
}
{
final v1 = GetRxDecorator(const {
1: ['a', 'b']
});
final v2 = GetRxDecorator(const {
2: ['a', 'b']
});
expect(v1, isNot(equals(v2)));
}
});
/// Here we just use auto setting.
test('GetRxDecorator: with default setter test (No special business logic)',
() async {
var v = 'a'.obsDeco();
expectLater(v.stream, emitsInOrder(['b', 'c', 'cd']));
v('b');
v('c');
v += 'd';
});
/// Here we can use special business logic inside custom setter.
test(
'GetRxDecorator: with custom setter test (with special business logic)',
() async {
var v = 'a'.obsDeco(
setter: (_, newValue, __) =>
// We can return as `wrong` either [oldValue] or null.
newValue?.contains('-') ?? false ? newValue : null);
expectLater(v.stream, emitsInOrder(['b-', '-c', '-c-']));
v('b-');
v('b');
v('c');
v('-c');
v('d');
v += '-';
});
/// Here we does not use autoRefresh.
test('GetRxDecorator: without refresh test', () async {
final v = 'a'.obsDeco(
setter: (_, newValue, __) =>
// We can return as `wrong` either [oldValue] or null.
newValue?.contains('-') ?? false ? newValue : null);
expectLater(v.stream, emitsInOrder(['b-', '-c', 'd-d']));
v('b-');
v('b-');
v('b-');
v('b');
v('c');
v('c');
v('-c');
v('-c');
v('-c');
v('d-d');
});
/// Here we use autoRefresh.
test('GetRxDecorator: with refresh test', () async {
final v = 'a'.obsDeco(
autoRefresh: true,
// We can return as `wrong` either [oldValue] or null.
setter: (_, newValue, __) =>
newValue?.contains('-') ?? false ? newValue : null,
);
expectLater(v.stream, emitsInOrder(['b-', 'b-', '-c', '-c', 'd-d']));
v('b-');
v('b-');
v('-c');
v('-c');
v('d-d');
});
/// Here we use [args] as extra variable.
test('GetRxDecorator: with args test', () async {
final v = 'a'.obsDeco(
autoRefresh: true,
setter: (_, newValue, args) {
if (args is int && args < 2) {
return null;
}
// We can return as `wrong` either [oldValue] or null.
return newValue?.contains('-') ?? false ? newValue : null;
},
);
expectLater(v.stream, emitsInOrder(['b-2', '2-c', '3-d-d']));
v('b-1', 1);
v('b-2', 2);
v('1-c', 1);
v('2-c', 2);
v('3-d-d', 3);
});
/// Here we use [args] as extra variable - variant 2.
test('GetRxDecorator: with args-2 test', () async {
final v = 'a'.obsDeco(
autoRefresh: true,
setter: (oldValue, newValue, args) {
if (args is bool) {
return null;
}
// We can return as `wrong` either [oldValue] or null.
return newValue?.contains('-') ?? false ? newValue : oldValue;
},
);
expectLater(v.stream, emitsInOrder(['b-2', '2-c', '3-d-d']));
v('b-1', false);
v('b-2', 2);
v('1-c', true);
v('2-c', 2);
v('3-d-d', 4.4);
});
/// Test using auto calculate without outer affect ([newValue] == null).
test('GetRxDecorator: Collatz conjecture setter test', () async {
final v = 7.obsDeco(
// Here we use [oldValue] as base of next step
// and does not use [newValue] at all.
setter: (oldValue, _, __) {
if (oldValue.isEven) {
return oldValue ~/ 2;
} else {
return 3 * oldValue + 1;
}
},
);
expectLater(
v.stream,
emitsInOrder([
22,
11,
34,
17,
52,
26,
13,
40,
20,
10,
5,
16,
8,
4,
2,
1,
4,
2,
1,
]));
// Just call [v.value()] without outer variables.
List.generate(19, (_) => v.call());
});
/// Here one can see overridden operation
test('GetRxDecorator: decorate int test', () async {
var v = 0.obsDeco(setter: (_, newValue, __) {
return (newValue ?? 0) * 2;
});
expectLater(v.stream, emitsInOrder([20, 60, 160, 0]));
v += 10;
v += 10;
v += 20;
v -= 160;
});
/// Here one can see overridden operation
test('GetRxDecorator: decorate bool test', () async {
var v = true.obsDeco();
expectLater(v.stream, emitsInOrder([false, true]));
v.toggle();
v.toggle();
});
});
group('GetRxDecorator as Pattern tests', () {
///
test('GetRxDecorator as Pattern: stream test', () async {
var rxVarInt = 1.obsDeco();
expectLater(rxVarInt.stream, emitsInOrder([2]));
rxVarInt(2);
});
///
test('GetRxDecorator as Pattern: value test', () async {
var rxVarInt = 1.obsDeco();
expectLater(rxVarInt.stream, emitsInOrder([2, 3, 2, 10]));
expect(rxVarInt.value, equals(1));
expect(rxVarInt(), equals(1));
rxVarInt += 1;
expect(rxVarInt.value, equals(2));
expect(rxVarInt(), equals(2));
rxVarInt++;
expect(rxVarInt.value, equals(3));
expect(rxVarInt(), equals(3));
rxVarInt--;
expect(rxVarInt.value, equals(2));
expect(rxVarInt(), equals(2));
rxVarInt(10);
expect(rxVarInt.value, equals(10));
expect(rxVarInt(), equals(10));
});
///
test('GetRxDecorator as Pattern: final value test', () async {
final rxVarInt = 1.obsDeco();
expectLater(rxVarInt.stream, emitsInOrder([2, 3, 2, 10]));
expectLater(rxVarInt.value, equals(1));
expectLater(rxVarInt(), equals(1));
rxVarInt.value += 1;
expect(rxVarInt.value, equals(2));
expect(rxVarInt(), equals(2));
rxVarInt.value++;
expectLater(rxVarInt.value, equals(3));
expectLater(rxVarInt(), equals(3));
rxVarInt.value--;
expectLater(rxVarInt.value, equals(2));
expectLater(rxVarInt(), equals(2));
rxVarInt.value = 10;
expectLater(rxVarInt.value, equals(10));
expectLater(rxVarInt(), equals(10));
});
});
}
/// This is sample page for [GetRxDecorator] concepts and howto.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Obs and Decorators',
style: Theme.of(context).textTheme.displaySmall,
textAlign: TextAlign.center,
),
),
GetX<IntController>(
init: IntController(),
builder: (c) {
return Button(
c.clickCounterUdf,
c.clickCounterDecor,
() {
c.clickCounterUdf++;
c.clickCounterDecor++;
},
);
},
),
GetX<DoubleController>(
init: DoubleController(),
initState: (_) {},
builder: (c) {
return Button(
c.valueUdf,
c.valueDecor,
() {
c.valueUdf += 1.1;
c.valueDecor += 1.1;
},
);
},
),
GetX<BoolController>(
init: BoolController(),
initState: (_) {},
builder: (c) {
return Button(
c.checkedUdf,
c.checkedDecor,
() {
// in plain obs .toggle() does not work in UDF pattern ...
c.checkedUdf = !c.checkedUdf;
// ... and in decorator it does
c.checkedDecor.toggle();
},
);
},
),
GetX<StringController>(
init: StringController(),
initState: (_) {},
builder: (c) {
return Button(
c.stringUdf,
c.stringDecor,
() {
c.stringUdf += ', world!';
c.stringDecor += ', world!';
},
);
},
),
GetX<CollatzController>(
init: CollatzController(),
initState: (_) {},
builder: (c) {
return Button(
c.collatzUdf,
c.collatzDecor,
() {
// In plain obs we re forced to pass some dummy value
// an it is confused
c.collatzUdf = 10;
// whereas the decorator has clear semantics
c.collatzDecor();
},
);
},
),
// This widget demonstrates how to use additions args
// in [GetRxDecorator].
// This argument(s) prohibits variable changing.
GetX<CollatzController>(
builder: (c) {
return SizedBox(
width: Get.width - 100,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.red, // background
onPrimary: Colors.yellow, // foreground
),
onPressed: () {
c.collatzDecor.args(this);
},
child: Column(
children: [
Text(
'${c.collatzDecor.runtimeType}: ${c.collatzDecor}'),
],
),
),
);
},
),
const Logger(),
],
),
),
);
}
}
/// Generic button for checking Decorator concepts.
/// We will manipulate with 2 reactive variables at once
/// and both should be equal.
class Button<O, D, C extends GetxController> extends StatelessWidget {
const Button(this.obs, this.obsDecorator, this.onPressed, {Key? key})
: super(key: key);
final O obs;
final D obsDecorator;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: Get.width - 100,
child: ElevatedButton(
child: Column(
children: [
// Here is standard rx-variable
Text((v) {
return '${v.runtimeType}: $v';
}(obs)),
// Here is decorator for rx-variable
Text((v) {
return '${v.runtimeType}: $v';
}(obsDecorator)),
],
),
onPressed: onPressed,
),
),
);
}
}
/// Logger demonstrates that decorators work identically to native `.obs`.
class Logger extends StatelessWidget {
const Logger({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final ic = Get.find<IntController>();
final dc = Get.find<DoubleController>();
final bc = Get.find<BoolController>();
final sc = Get.find<StringController>();
final cc = Get.find<CollatzController>();
return Obx(() {
return SizedBox(
width: Get.width - 100,
child: Card(
color: Theme.of(context).backgroundColor,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
title: const Text('Log:'),
subtitle: Text('int = ${ic.clickCounterDecor}\n'
'double decorator = ${dc.valueDecor},\n'
'bool decorator = ${bc.checkedDecor},\n'
'string decorator = ${sc.stringDecor},\n'
'collatz decorator = ${cc.collatzDecor}'),
),
),
),
);
});
}
}
///
class IntController extends GetxController {
/// Ordinary observable variable.
/// The problem is there is no way to control its value in business layer.
var clickCounter = 0.obs;
/// Observable variable in UDF pattern.
/// Now we can control variable' value through setter [clickCounterUdf].
/// But we have to orchestrate 3 or 4 entity for this pattern:
///
/// 1. Variable itself.
final _clickCounterUdf = 0.obs;
/// 2. Its Getter
int get clickCounterUdf => _clickCounterUdf();
/// 3. Its Setter
set clickCounterUdf(int v) => _clickCounterUdf(_process(v));
/// 4. Its Stream
Stream<int> get clickCounterStreamUdf => _clickCounterUdf.stream;
/// Decorator for observable variable.
/// All advantages in one.
late var clickCounterDecor =
0.obsDeco(setter: (_, newValue, __) => _process(newValue ?? 0));
/// This processor drops values above 3 down to zero.
int _process(int v) => v > 3 ? 0 : v;
}
///
class DoubleController extends GetxController {
static const _startValue = 0.1;
static const _maxValue = 3.1;
final _valueUdf = _startValue.obs;
double get valueUdf => _valueUdf();
set valueUdf(double v) => _valueUdf(_process(v));
///
late var valueDecor = _startValue.obsDeco(
setter: (_, newValue, __) => _process(newValue ?? _startValue));
/// This processor drops values above 3.1 down to 0.1.
double _process(double v) => v > _maxValue ? _startValue : v;
}
///
class BoolController extends GetxController {
final _checkedUdf = false.obs;
bool get checkedUdf => _checkedUdf();
set checkedUdf(bool v) => _checkedUdf(v);
/// Decorator (4-in-1)
/// Setter here makes nothing special.
var checkedDecor = false.obsDeco(setter: (_, newValue, __) => newValue);
}
///
class StringController extends GetxController {
final _stringUdf = 'hello'.obs;
String get stringUdf => _stringUdf();
set stringUdf(String v) => _stringUdf(v.length > 18 ? 'hello' : v);
/// Decorator 4-in-1
late var stringDecor = 'hello'.obsDeco(setter: (_, newValue, __) {
return _process(newValue ?? '');
});
/// This processor cuts oversized value to simple 'hello' one.
String _process(String v) => v.length > 18 ? 'hello' : v;
}
///
class CollatzController extends GetxController {
final _collatzUdf = 7.obs;
int get collatzUdf => _collatzUdf();
/// This setter realizes Collatz conjecture.
/// See that one forces here to pass something as a parameter
/// even if it is not used.
set collatzUdf(int _) {
if (_collatzUdf.value.isEven) {
_collatzUdf.value = _collatzUdf.value ~/ 2;
} else {
_collatzUdf.value = 3 * _collatzUdf.value + 1;
}
}
/// Decorator.
/// This setter realizes Collatz conjecture.
/// As you see that we are free to eliminate passing any parameter
/// if it is not required.
var collatzDecor = 7.obsDeco(setter: (oldValue, _, args) {
// See how context works!
if (args != null) {
Future.delayed(const Duration(milliseconds: 250)).then((_) {
ScaffoldMessenger.of(Get.context!).showSnackBar(SnackBar(
backgroundColor: Colors.red,
content: Text('It is not possible with this args: $args'),
));
});
return oldValue;
}
//
if (oldValue.isEven) {
return oldValue ~/ 2;
} else {
return 3 * oldValue + 1;
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment