Last active
September 1, 2021 21:34
-
-
Save roipeker/42a6155a72501b091a9e54428cf7b3ac to your computer and use it in GitHub Desktop.
sample reactive ValueNotifier + StateWidget( Widget build() inside Widget)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/scheduler.dart'; | |
import 'package:redo_provider/notifier/notifier.dart'; | |
import 'package:redo_provider/state_widget.dart'; | |
class AboutPage extends StateWidget<AboutPageState> { | |
const AboutPage({Key? key}) : super(key: key); | |
@override | |
createState() => AboutPageState(); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text('Hello ${state.name}!'), | |
const Divider(), | |
Text('hot reloads: ${state.hotReloads}'), | |
const Divider(), | |
Observer(() => Text('ticker: ${state.tickerTime}')), | |
], | |
), | |
); | |
} | |
SnackBar buildSnackbar() { | |
return SnackBar( | |
content: Text('Ticker disposed @${state.tickerTime}'), | |
duration: Duration(milliseconds: 1200), | |
action: SnackBarAction( | |
label: 'Widget mounted?', | |
onPressed: state.onSnackbarAction, | |
), | |
); | |
} | |
} | |
class AboutPageState extends StateController<AboutPage> | |
with SingleTickerProviderStateMixin { | |
final name = 'roi'; | |
final hotReloads = NotifierValue(0); | |
final tickerTime = NotifierValue(Duration.zero); | |
late final Ticker _ticker = createTicker((e) => tickerTime(e)); | |
@override | |
void initState() { | |
_ticker.start(); | |
super.initState(); | |
} | |
@override | |
void reassemble() { | |
hotReloads.value++; | |
super.reassemble(); | |
} | |
late ScaffoldMessengerState _snackbarHolder; | |
@override | |
void didChangeDependencies() { | |
_snackbarHolder = ScaffoldMessenger.of(context); | |
super.didChangeDependencies(); | |
} | |
@override | |
void dispose() { | |
_ticker.dispose(); | |
Future.microtask(_showSnackbar); | |
super.dispose(); | |
} | |
FutureOr _showSnackbar() => | |
_snackbarHolder.showSnackBar(widget.buildSnackbar()); | |
void onSnackbarAction() { | |
print('Widget still mounted? $mounted'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'notifier/notifier.dart'; | |
class ContactPage extends StatefulWidget { | |
const ContactPage({Key? key}) : super(key: key); | |
@override | |
_ContactPageState createState() => _ContactPageState(); | |
} | |
class _ContactPageState extends State<ContactPage> with DisposerMixin { | |
final user2 = NotifierValue<User?>(null); | |
late final user = User('roi', 34).obs( | |
disposer: this, | |
); | |
late final count = 0.obs( | |
disposer: this, | |
onChange: onCountChange, | |
); | |
final active = false.obs(); | |
late Stream<int> counterStream; | |
@override | |
void initState() { | |
/// listen to dispose() | |
user2.hookDispose(this); | |
super.initState(); | |
counterStream = Stream<int>.periodic(const Duration(seconds: 1), (x) => x) | |
.take(15) | |
.asBroadcastStream(); | |
count.bindStream(counterStream); | |
} | |
void onCountChange(int value) { | |
print('count changed: $value'); | |
if (value > 8) { | |
count.closeStream(counterStream); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Observer((){ | |
return Text('user 2 : $user2'); | |
}), | |
Divider(), | |
Observer( | |
() => Switch(value: active(), onChanged: active), | |
), | |
Divider(), | |
Observer( | |
() => Text('Hello contact $count'), | |
), | |
Divider(), | |
Observer( | |
() => Text('Other contact ${count() - 2}'), | |
), | |
Divider(), | |
Observer(() { | |
print('rebuild user obs()'); | |
return Text('$user'); | |
// return Text('hola!'); | |
}), | |
], | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: () { | |
count.value += 2; | |
active.toggle(); | |
user2.value = User('Emilce', 36); | |
user.update((user) => user.copyWith(name: 'Ismail')); | |
}, | |
), | |
); | |
} | |
} | |
class User with IValueNotifier { | |
final String name; | |
final int age; | |
const User(this.name, this.age); | |
User copyWith({String? name, int? age}) { | |
return User(name ?? this.name, age ?? this.age); | |
} | |
@override | |
String toString() => 'User{name: $name, age: $age}'; | |
/// for equality. | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
other is User && | |
runtimeType == other.runtimeType && | |
name == other.name && | |
age == other.age; | |
@override | |
int get hashCode => name.hashCode ^ age.hashCode; | |
Map<String, dynamic> toJson() => {'name': name, 'age': age}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
part of notifier; | |
extension NotifierIntX on int { | |
NotifierValue<int> obs({ | |
DisposerNotifier? disposer, | |
ValueChanged<int>? onChange, | |
}) { | |
final o = NotifierValue<int>(this)..hookDispose(disposer); | |
if (onChange != null) { | |
o.addValueListener(onChange); | |
} | |
return o; | |
} | |
} | |
extension NotifierDoubleX on double { | |
NotifierValue<double> obs({ | |
DisposerNotifier? disposer, | |
ValueChanged<double>? onChange, | |
}) { | |
final o = NotifierValue<double>(this)..hookDispose(disposer); | |
if (onChange != null) { | |
o.addValueListener(onChange); | |
} | |
return o; | |
} | |
} | |
extension NotifierStringX on String { | |
NotifierValue<String> obs({ | |
DisposerNotifier? disposer, | |
ValueChanged<String>? onChange, | |
}) { | |
final o = NotifierValue<String>(this)..hookDispose(disposer); | |
if (onChange != null) { | |
o.addValueListener(onChange); | |
} | |
return o; | |
} | |
} | |
extension NotifierBoolX on bool { | |
NotifierValue<bool> rvn({ | |
DisposerNotifier? disposer, | |
ValueChanged<bool>? onChange, | |
}) { | |
final o = NotifierValue<bool>(this); | |
o.hookDispose(disposer); | |
if (onChange != null) { | |
o.addValueListener(onChange); | |
} | |
return o; | |
} | |
} | |
extension NotifierValueBoolX on NotifierValue<bool> { | |
bool get isTrue => value; | |
bool get isFalse => !isTrue; | |
bool operator &(bool other) => other && value; | |
bool operator |(bool other) => other || value; | |
bool operator ^(bool other) => !other == value; | |
void toggle() { | |
value = !value; | |
} | |
} | |
extension NotifierValueInterfaseX<T extends IValueNotifier> on T { | |
NotifierValue<T> obs({ | |
DisposerNotifier? disposer, | |
ValueChanged<T>? onChange, | |
}) { | |
final o = NotifierValue<T>(this)..hookDispose(disposer); | |
if (onChange != null) { | |
o.addValueListener(onChange); | |
} | |
return o; | |
} | |
} | |
class NotifierValue<T> extends ValueNotifier<T> { | |
static _ObxNotifier? _proxyNotifier; | |
Map<Stream, StreamSubscription> _subscriptions = {}; | |
DisposerNotifier? _disposer; | |
NotifierValue<T> hookDispose(DisposerNotifier? instance) { | |
_disposer = instance; | |
_disposer?.addListener(_onDispose); | |
return this; | |
} | |
void _onDispose() { | |
_disposer?.removeListener(_onDispose); | |
dispose(); | |
} | |
NotifierValue(T value) : super(value); | |
final Expando<VoidCallback> _valueListeners = Expando(); | |
void addValueListener(ValueChanged<T> listener) { | |
_valueListeners[listener] = () { | |
listener(super.value); | |
}; | |
addListener(_valueListeners[listener]!); | |
} | |
void removeValueListener(ValueChanged<T> listener) { | |
if (_valueListeners[listener] != null) { | |
removeListener(_valueListeners[listener]!); | |
_valueListeners[listener] = null; | |
} | |
} | |
FutureOr closeStream(Stream<T> stream) { | |
if (_subscriptions.containsKey(stream)) { | |
return _subscriptions.remove(stream)!.cancel(); | |
} | |
} | |
void bindStream(Stream<T> stream) { | |
late StreamSubscription subscription; | |
subscription = stream.asBroadcastStream().listen((event) { | |
this.value = event; | |
}, onDone: () { | |
subscription.cancel(); | |
_subscriptions.remove(subscription); | |
}); | |
_subscriptions[stream] = subscription; | |
} | |
@override | |
String toString() => '$value'; | |
T call([T? newValue]) { | |
if (newValue != null && newValue != value) { | |
value = newValue; | |
} | |
return value; | |
} | |
@override | |
set value(T newValue) { | |
if (newValue != super.value) { | |
super.value = newValue; | |
} | |
} | |
@override | |
T get value { | |
if (NotifierValue._proxyNotifier != null) { | |
NotifierValue._proxyNotifier!.add(this); | |
} | |
return super.value; | |
} | |
void update(T Function(T value) fn) { | |
value = fn(super.value); | |
} | |
@override | |
void dispose() { | |
if (_proxyNotifier != null) { | |
_proxyNotifier!.remove(this); | |
} | |
for (final subscription in _subscriptions.values) { | |
subscription.cancel(); | |
} | |
_subscriptions.clear(); | |
super.dispose(); | |
} | |
} | |
class NotifierValueInt extends NotifierValue<int> { | |
NotifierValueInt(int value) : super(value); | |
} | |
abstract class IValueNotifier<T> { | |
/// complies with obs(). | |
} | |
mixin DisposerMixin<T extends StatefulWidget> on State<T> | |
implements DisposerNotifier { | |
final _disposer = _DisposerNotifier(); | |
void addListener(VoidCallback fn) => _disposer.addListener(fn); | |
void removeListener(VoidCallback fn) => _disposer.removeListener(fn); | |
void dispose() { | |
_disposer.dispose(); | |
super.dispose(); | |
} | |
} | |
class _DisposerNotifier extends ChangeNotifier { | |
@override | |
void dispose() { | |
notifyListeners(); | |
super.dispose(); | |
} | |
} | |
abstract class DisposerNotifier { | |
final _disposer = _DisposerNotifier(); | |
void addListener(VoidCallback fn) => _disposer.addListener(fn); | |
void removeListener(VoidCallback fn) => _disposer.removeListener(fn); | |
void dispose() { | |
_disposer.dispose(); | |
} | |
} | |
extension ValueNotifierX<T> on ValueListenable<T> { | |
String get string => '$value'; | |
Widget widget<T>({ | |
Widget? child, | |
required ValueWidgetBuilder<T> builder, | |
}) { | |
return ValueListenableBuilder<T>( | |
valueListenable: this as ValueListenable<T>, | |
builder: builder, | |
child: child, | |
); | |
} | |
} | |
class Observer extends StatelessWidget { | |
final Widget Function() builder; | |
const Observer( | |
this.builder, { | |
Key? key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return ObserverBuilder( | |
builder: (_, __) => builder(), | |
); | |
} | |
} | |
class ObserverBuilder extends StatefulWidget { | |
final TransitionBuilder builder; | |
final Widget? child; | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties..add(ObjectFlagProperty<Function>.has('builder', builder)); | |
} | |
const ObserverBuilder({ | |
Key? key, | |
required this.builder, | |
this.child, | |
}) : super(key: key); | |
@override | |
_ObserverBuilderState createState() => _ObserverBuilderState(); | |
} | |
class _ObserverBuilderState extends State<ObserverBuilder> { | |
late _ObxNotifier notifier = _ObxNotifier(update); | |
void update() { | |
if (mounted) { | |
setState(() {}); | |
} | |
} | |
@override | |
void dispose() { | |
notifier.dispose(false); | |
super.dispose(); | |
} | |
Widget notifyChild(BuildContext context, Widget? child) { | |
final oldNotifier = NotifierValue._proxyNotifier; | |
NotifierValue._proxyNotifier = notifier; | |
final result = widget.builder(context, child); | |
if (!notifier.canUpdate) { | |
NotifierValue._proxyNotifier = oldNotifier; | |
throw """ | |
[NotifierValue] improper use of Observer() or ObserverBuilder() detected. | |
Use [NotifierValue](s) directly in the scope of the builder(). | |
If you need to update a parent widget and a child widget, wrap them separately in Observer() or ObserverBuilder(). | |
"""; | |
} | |
NotifierValue._proxyNotifier = oldNotifier; | |
return result; | |
} | |
@override | |
void reassemble() { | |
notifier.dispose(true); | |
super.reassemble(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return AnimatedBuilder( | |
animation: notifier.notifications, | |
builder: (_, child) => notifyChild(context, child), | |
child: widget.child, | |
); | |
} | |
} | |
class _ObxNotifier { | |
static final emptyListener = ChangeNotifier(); | |
Listenable notifications = emptyListener; | |
final VoidCallback stateSetter; | |
final _notis = <NotifierValue>{}; | |
Set<NotifierValue>? _cachedNotis; | |
bool _dirtyWidget = false; | |
_ObxNotifier(this.stateSetter); | |
bool get canUpdate => _notis.isNotEmpty; | |
void add(NotifierValue noti) { | |
if (!_notis.contains(noti)) { | |
if (_cachedNotis != null && _dirtyWidget && _cachedNotis!.isNotEmpty) { | |
final _cached = _cachedNotis!; | |
for (var m in _cached) { | |
m.dispose(); | |
} | |
_notis.removeAll(_cached); | |
_cached.clear(); | |
_cachedNotis = null; | |
} | |
_notis.add(noti); | |
_updateCollection(); | |
} | |
} | |
void remove(NotifierValue motion) { | |
if (_notis.contains(motion)) { | |
_notis.remove(motion); | |
_updateCollection(); | |
} | |
} | |
void _updateCollection() { | |
if (_notis.isEmpty) { | |
notifications = emptyListener; | |
} else { | |
notifications = Listenable.merge(_notis.toList(growable: false)); | |
} | |
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) => stateSetter()); | |
} | |
void dispose([bool reassembling = false]) { | |
final _buffer = List.of(_notis); | |
// _buffer.forEach((e) { | |
// if (e is _ValueNoti && reassembling) { | |
// e.dispose(); | |
// } | |
// }); | |
notifications = emptyListener; | |
_notis.clear(); | |
_buffer.clear(); | |
_cachedNotis?.clear(); | |
_cachedNotis = null; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/widgets.dart'; | |
abstract class StateWidget<T extends State> extends StatefulWidget { | |
const StateWidget({Key? key}) : super(key: key); | |
Widget build(BuildContext context); | |
T get state => StateElement._elements[this] as T; | |
@override | |
StateElement createElement() => StateElement(this); | |
@override | |
T createState(); | |
} | |
class StateElement extends StatefulElement { | |
static final _elements = Expando('State Controllers'); | |
StateElement(StateWidget widget) : super(widget) { | |
_elements[widget] = state; | |
} | |
@override | |
void update(StatefulWidget newWidget) { | |
_elements[newWidget] = state; | |
super.update(newWidget); | |
} | |
@override | |
StateWidget get widget => super.widget as StateWidget; | |
@override | |
Widget build() => widget.build(this); | |
} | |
class StateController<T extends StateWidget> extends State<T> { | |
@protected | |
@override | |
Widget build(BuildContext context) { | |
throw "$runtimeType\.build() is invalid. Use <StateWidget.build()> instead."; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment