├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── reactives.dart └── src │ ├── animation.dart │ ├── async.dart │ ├── disposables.dart │ ├── listenable.dart │ ├── reactive.dart │ └── ticker.dart ├── pubspec.lock ├── pubspec.yaml └── test └── reactives_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/ephemeral 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 615957513eb43b128a16048ab5aa2daaba1656cd 8 | channel: beta 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | * Fix concurrent access bug 4 | 5 | ## 0.1.1 6 | 7 | * Update Docs 8 | 9 | ## 0.1.0 10 | 11 | * Renamed Reactives to start with Reactive. Add support `didUpdate`. Added `ReactiveAnimation` and other controller reactives. 12 | 13 | ## 0.0.1 14 | 15 | * Alpha 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Sri Krishna Paritala 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactives 2 | 3 | A new way for Flutter to reuse/group common logic. Think of them like React hooks but for and of flutter. 4 | 5 | Idea copied from the [lit world](https://lit.dev/docs/composition/controllers/) 6 | 7 | ## Motive 8 | 9 | Have you ever written code like this? 10 | 11 | ```dart 12 | class AwesomeWidget extends StatefulWidget { 13 | const AwesomeWidget({Key? key}) : super(key: key); 14 | 15 | @override 16 | _AwesomeWidgetState createState() => _AwesomeWidgetState(); 17 | } 18 | 19 | class _AwesomeWidgetState extends State 20 | with TickerProviderStateMixin { 21 | late final TextEditingController emailCtrl; 22 | late final TextEditingController passwordCtrl; 23 | late final AnimationController entryAnimation; 24 | late final AnimationController highLightAnimation; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | emailCtrl = TextEditingController(); 30 | passwordCtrl = TextEditingController(); 31 | entryAnimation = AnimationController(vsync: this); 32 | highLightAnimation = AnimationController(vsync: this); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | emailCtrl.dispose(); 38 | entryAnimation.dispose(); 39 | highLightAnimation.dispose(); 40 | passwordCtrl.dispose(); 41 | super.dispose(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return Container(); 47 | } 48 | } 49 | ``` 50 | 51 | There are a couple of problems here. 52 | * Most of the object management logic is generic and can be reused. 53 | * It is extremely easy to forget that one `dispose` call (confession). 54 | * Coupling the logic to the widget makes it harder to test. 55 | 56 | Using reactives this gets transformed to, 57 | ```dart 58 | class AwesomeReactiveWidget extends StatefulWidget { 59 | const AwesomeReactiveWidget({Key? key}) : super(key: key); 60 | 61 | @override 62 | _AwesomeReactiveWidgetState createState() => _AwesomeReactiveWidgetState(); 63 | } 64 | 65 | class _AwesomeReactiveWidgetState extends State with ReactiveHostMixin { 66 | late final emailCtrl = ReactiveTextEditingController(this); 67 | late final passwordCtrl = ReactiveTextEditingController(this); 68 | late final entryAnimation = ReactiveAnimationController(this); 69 | late final exitAnimation = ReactiveAnimationController(this); 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return Container( 74 | // .... 75 | ); 76 | } 77 | } 78 | ``` 79 | 80 | See [Examples](https://pub.dev/packages/reactives/example) for examples of writing a reactive or just browse the [source](https://github.com/ripemango/reactives), it's really small. 81 | 82 | ### Comparision to [flutter_hooks](https://pub.dev/packages/flutter_hooks): 83 | 84 | From a user perspective `flutter_hooks` is a replacement for `StatefulWidget`. `reactives` is not. If you look at the code for `ReactiveHostMixin` it is about 60 lines (blank lines included). Reactives do not try to replace `StatefulWidget`, it just solves the reusability problem inherent due to mixin's "is-a" relationship. Reactives have a "has-a" relationship. 85 | 86 | There are no new rules to writing reactives. See examples of existing reactives. It is basically the same logic isolated in a different class. 87 | For the same reason it doesn't need a lot of the hooks like `useState` which would be needed if tried to replace `StatefulWidget`. 88 | 89 | The learning curve of reactives is next to negligible. Hooks need you to learn a few concepts and [how to/not to](https://pub.dev/packages/flutter_hooks#rules) do things. It also requires you to transform entire widgets. Reactives can be adapted incrementally even for a single widget. -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:reactives/reactives.dart'; 3 | 4 | /// Simple widget showing reactive 5 | class LoginWidget extends StatefulWidget { 6 | const LoginWidget({Key? key}) : super(key: key); 7 | 8 | @override 9 | _LoginWidgetState createState() => _LoginWidgetState(); 10 | } 11 | 12 | // Add the ReactiveHostMixin 13 | class _LoginWidgetState extends State with ReactiveHostMixin { 14 | // Create required reactives 15 | late final emailCtrl = ReactiveTextEditingController(this).ctrl; 16 | late final passwordCtrl = ReactiveTextEditingController(this).ctrl; 17 | 18 | var passwordVisible = false; 19 | 20 | void submit() { 21 | // Login logic 22 | } 23 | 24 | void toggeVisible() { 25 | setState(() { 26 | passwordVisible = !passwordVisible; 27 | }); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Column( 33 | mainAxisSize: MainAxisSize.min, 34 | children: [ 35 | // Use the reactives just like any field 36 | TextField(controller: emailCtrl), 37 | TextField( 38 | controller: passwordCtrl, 39 | decoration: InputDecoration( 40 | suffix: IconButton( 41 | icon: const Icon(Icons.visibility), 42 | onPressed: toggeVisible, 43 | ), 44 | ), 45 | obscureText: !passwordVisible, 46 | ), 47 | ElevatedButton( 48 | child: const Text('Login'), 49 | onPressed: submit, 50 | ), 51 | ], 52 | ); 53 | } 54 | } 55 | 56 | /// ReactiveLogin is a way to extract the login logic out of the widget for 57 | /// testability and resusability 58 | class ReactiveLogin extends Reactive { 59 | final ReactiveTextEditingController _email; 60 | final ReactiveTextEditingController _password; 61 | 62 | TextEditingController get emailCtrl => _email.ctrl; 63 | TextEditingController get passwordCtrl => _password.ctrl; 64 | 65 | bool _passwordVisible = false; 66 | bool get passwordVisible => _passwordVisible; 67 | 68 | ReactiveLogin(ReactiveHost host) 69 | : _email = ReactiveTextEditingController(host), 70 | _password = ReactiveTextEditingController(host), 71 | super(host); 72 | 73 | void submit() { 74 | // Login logic 75 | } 76 | 77 | void toggleVisible() { 78 | _passwordVisible = !_passwordVisible; 79 | host.requestUpdate(); // Calls setState 80 | } 81 | } 82 | 83 | /// The above example can be rewritten using our extracted login logic 84 | /// This reduces the Widget the UI logic 85 | class LoginView extends StatefulWidget { 86 | const LoginView({Key? key}) : super(key: key); 87 | 88 | @override 89 | _LoginViewState createState() => _LoginViewState(); 90 | } 91 | 92 | // Add the ReactiveHostMixin 93 | class _LoginViewState extends State with ReactiveHostMixin { 94 | // Create required login Reactive 95 | late final login = ReactiveLogin(this); 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Column( 100 | mainAxisSize: MainAxisSize.min, 101 | children: [ 102 | // Use the reactives just like any field 103 | TextField(controller: login.emailCtrl), 104 | TextField( 105 | controller: login.passwordCtrl, 106 | decoration: InputDecoration( 107 | suffix: IconButton( 108 | icon: const Icon(Icons.visibility), 109 | onPressed: login.toggleVisible, 110 | ), 111 | ), 112 | obscureText: !login.passwordVisible, 113 | ), 114 | ElevatedButton(child: const Text('Login'), onPressed: login.submit), 115 | ], 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/reactives.dart: -------------------------------------------------------------------------------- 1 | library reactives; 2 | 3 | export 'src/animation.dart'; 4 | export 'src/reactive.dart'; 5 | export 'src/disposables.dart'; 6 | export 'src/listenable.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/animation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:reactives/src/ticker.dart'; 4 | import 'reactive.dart'; 5 | 6 | class ReactiveAnimationController extends Reactive 7 | with SingleTickerProviderReactiveMixin 8 | implements TickerProvider { 9 | final bool _listen; 10 | late final AnimationController ctrl; 11 | 12 | ReactiveAnimationController( 13 | ReactiveHost host, { 14 | AnimationController? ctrl, 15 | bool listen = false, 16 | }) : _listen = listen, 17 | super(host) { 18 | this.ctrl = ctrl ?? AnimationController(vsync: this); 19 | if (_listen) this.ctrl.addListener(_onChange); 20 | } 21 | 22 | void _onChange() { 23 | host.requestUpdate(); 24 | } 25 | 26 | @override 27 | void dispose() { 28 | if (_listen) ctrl.removeListener(_onChange); 29 | ctrl.dispose(); 30 | super.dispose(); 31 | } 32 | } 33 | 34 | class ReactiveAnimation extends Reactive { 35 | final Animation animation; 36 | final VoidCallback? listener; 37 | ReactiveAnimation( 38 | ReactiveHost host, { 39 | required this.animation, 40 | this.listener, 41 | }) : super(host) { 42 | animation.addListener(listener ?? _onChange); 43 | } 44 | 45 | void _onChange() { 46 | host.requestUpdate(); 47 | } 48 | 49 | @override 50 | void dispose() { 51 | animation.removeListener(listener ?? _onChange); 52 | super.dispose(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/async.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:reactives/reactives.dart'; 5 | 6 | class ReactiveFuture extends Reactive { 7 | final Future future; 8 | 9 | AsyncSnapshot _snapshot = const AsyncSnapshot.waiting(); 10 | AsyncSnapshot get snapshot => _snapshot; 11 | 12 | ReactiveFuture( 13 | ReactiveHost host, 14 | this.future, { 15 | T? initialData, 16 | void Function(AsyncSnapshot)? listener, 17 | }) : super(host) { 18 | if (initialData != null) { 19 | _snapshot = AsyncSnapshot.withData(ConnectionState.waiting, initialData); 20 | } 21 | 22 | future.then((data) { 23 | _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); 24 | if (listener != null) { 25 | listener(_snapshot); 26 | } else { 27 | host.requestUpdate(); 28 | } 29 | }, onError: (err, st) { 30 | _snapshot = AsyncSnapshot.withError(ConnectionState.done, err, st); 31 | if (listener != null) { 32 | listener(_snapshot); 33 | } else { 34 | host.requestUpdate(); 35 | } 36 | }); 37 | } 38 | } 39 | 40 | class ReactiveStream extends Reactive { 41 | final Stream stream; 42 | 43 | AsyncSnapshot _snapshot = const AsyncSnapshot.waiting(); 44 | AsyncSnapshot get snapshot => _snapshot; 45 | 46 | StreamSubscription? _subscription; 47 | 48 | @protected 49 | final void Function(AsyncSnapshot)? listener; 50 | 51 | ReactiveStream( 52 | ReactiveHost host, 53 | this.stream, { 54 | T? initialData, 55 | this.listener, 56 | }) : super(host) { 57 | if (initialData != null) { 58 | _snapshot = AsyncSnapshot.withData(ConnectionState.waiting, initialData); 59 | } 60 | 61 | _subscription = stream.listen((T data) { 62 | _snapshot = afterData(_snapshot, data); 63 | _onChange(); 64 | }, onError: (Object error, StackTrace stackTrace) { 65 | _snapshot = afterError(_snapshot, error, stackTrace); 66 | _onChange(); 67 | }, onDone: () { 68 | _snapshot = afterDone(_snapshot); 69 | _onChange(); 70 | }); 71 | } 72 | 73 | @override 74 | void dispose() { 75 | _subscription?.cancel(); 76 | _subscription = null; 77 | super.dispose(); 78 | } 79 | 80 | void _onChange() { 81 | final lis = listener; 82 | if (lis != null) { 83 | lis(_snapshot); 84 | } else { 85 | host.requestUpdate(); 86 | } 87 | } 88 | 89 | AsyncSnapshot afterConnected(AsyncSnapshot current) => 90 | current.inState(ConnectionState.waiting); 91 | 92 | AsyncSnapshot afterData(AsyncSnapshot current, T data) { 93 | return AsyncSnapshot.withData(ConnectionState.active, data); 94 | } 95 | 96 | AsyncSnapshot afterError( 97 | AsyncSnapshot current, Object error, StackTrace stackTrace) { 98 | return AsyncSnapshot.withError( 99 | ConnectionState.active, error, stackTrace); 100 | } 101 | 102 | AsyncSnapshot afterDone(AsyncSnapshot current) => 103 | current.inState(ConnectionState.done); 104 | 105 | AsyncSnapshot afterDisconnected(AsyncSnapshot current) => 106 | current.inState(ConnectionState.none); 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/disposables.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:reactives/reactives.dart'; 4 | import 'package:reactives/src/ticker.dart'; 5 | 6 | import 'reactive.dart'; 7 | 8 | abstract class Disposable { 9 | void dispose(); 10 | } 11 | 12 | /// {@template disposable} 13 | /// Optinally creates and disposes a [FocusNode]. 14 | /// {@endtemplate} 15 | 16 | /// {@macro disposable} [FocusNode]. 17 | class ReactiveFocusNode extends Reactive { 18 | final FocusNode node; 19 | ReactiveFocusNode( 20 | ReactiveHost host, { 21 | FocusNode? node, 22 | }) : node = node ?? FocusNode(), 23 | super(host); 24 | 25 | @override 26 | void dispose() { 27 | node.dispose(); 28 | super.dispose(); 29 | } 30 | } 31 | 32 | /// {@macro disposable} [TextEditingController]. 33 | class ReactiveTextEditingController extends Reactive { 34 | /// The [TextEditingController] that will be disposed. 35 | final TextEditingController ctrl; 36 | 37 | ReactiveTextEditingController( 38 | ReactiveHost host, { 39 | TextEditingController? ctrl, 40 | }) : ctrl = ctrl ?? TextEditingController(), 41 | super(host); 42 | 43 | @override 44 | void dispose() { 45 | ctrl.dispose(); 46 | super.dispose(); 47 | } 48 | } 49 | 50 | /// {@macro disposable} [PageController]. 51 | class ReactivePageController extends ReactiveScrollController { 52 | @override 53 | PageController get ctrl => super.ctrl as PageController; 54 | 55 | ReactivePageController( 56 | ReactiveHost host, { 57 | PageController? ctrl, 58 | bool listen = false, 59 | VoidCallback? listener, 60 | }) : super(host, ctrl: ctrl, listen: listen, listener: listener); 61 | } 62 | 63 | /// Creates and disposes a [TabController]. 64 | class ReactiveTabController extends Reactive 65 | with SingleTickerProviderReactiveMixin { 66 | late final TabController ctrl; 67 | ReactiveTabController( 68 | ReactiveHost host, { 69 | int initialIndex = 0, 70 | required int length, 71 | bool listen = false, 72 | VoidCallback? listener, 73 | }) : super(host) { 74 | ctrl = TabController( 75 | initialIndex: initialIndex, 76 | length: length, 77 | vsync: this, 78 | ); 79 | ReactiveListenable(host, ctrl, listen: listen, listener: listener); 80 | } 81 | 82 | @override 83 | void dispose() { 84 | ctrl.dispose(); 85 | super.dispose(); 86 | } 87 | } 88 | 89 | /// {@macro disposable} [ScrollController]. 90 | class ReactiveScrollController extends Reactive { 91 | final ScrollController ctrl; 92 | ReactiveScrollController( 93 | ReactiveHost host, { 94 | ScrollController? ctrl, 95 | bool listen = false, 96 | VoidCallback? listener, 97 | }) : ctrl = ctrl ?? ScrollController(), 98 | super(host) { 99 | ReactiveListenable(host, this.ctrl, listen: listen, listener: listener); 100 | } 101 | 102 | @override 103 | void dispose() { 104 | ctrl.dispose(); 105 | super.dispose(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/listenable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'reactive.dart'; 4 | 5 | class ReactiveListenable extends Reactive { 6 | final T listenable; 7 | final bool listen; 8 | final VoidCallback? listener; 9 | 10 | ReactiveListenable( 11 | ReactiveHost host, 12 | this.listenable, { 13 | this.listen = false, 14 | this.listener, 15 | }) : super(host) { 16 | if (listen || listener != null) { 17 | listenable.addListener(listener ?? host.requestUpdate); 18 | } 19 | } 20 | 21 | @override 22 | void dispose() { 23 | GlobalKey(); 24 | if (listen || listener != null) { 25 | listenable.removeListener(listener ?? host.requestUpdate); 26 | } 27 | super.dispose(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/reactive.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | /// The base class for reactives. Providing handy callbacks. 7 | /// 8 | /// New reactives need to extend this class and override callbacks 9 | /// as needed 10 | abstract class Reactive with Diagnosticable { 11 | @protected 12 | final ReactiveHost host; 13 | 14 | Reactive(this.host) { 15 | host.addController(this); 16 | } 17 | 18 | @protected 19 | @mustCallSuper 20 | void didChangeDependencies() {} 21 | 22 | @protected 23 | @mustCallSuper 24 | void dispose() {} 25 | 26 | void didUpdateWidget(covariant StatefulWidget oldWidget) {} 27 | 28 | @protected 29 | @mustCallSuper 30 | void activate() {} 31 | 32 | @protected 33 | @mustCallSuper 34 | void deactivate() {} 35 | } 36 | 37 | /// The interafce required for reactive controller hosts 38 | /// See [ReactiveHostMixin] for a mixin that implements this interface. 39 | abstract class ReactiveHost { 40 | addController(Reactive ctrl); 41 | removeController(Reactive ctrl); 42 | requestUpdate(); 43 | Future get updateComplete; 44 | BuildContext get context; 45 | } 46 | 47 | /// Turns any state class to a [ReactiveHost] 48 | /// 49 | /// ```dart 50 | /// class MyWidget extends StatefulWidget { 51 | /// const MyWidget({Key? key}) : super(key: key); 52 | /// 53 | /// @override 54 | /// _MyWidgetState createState() => _MyWidgetState(); 55 | /// } 56 | /// 57 | /// class _MyWidgetState extends State with ReactiveBindings { 58 | /// @override 59 | /// Widget build(BuildContext context) { 60 | /// return Container(); 61 | /// } 62 | /// } 63 | /// ``` 64 | /// 65 | @optionalTypeArgs 66 | mixin ReactiveHostMixin on State 67 | implements ReactiveHost { 68 | // API 69 | @protected 70 | final List reactives = []; 71 | 72 | @override 73 | addController(Reactive ctrl) { 74 | reactives.add(ctrl); 75 | } 76 | 77 | @override 78 | removeController(Reactive ctrl) { 79 | reactives.remove(ctrl); 80 | } 81 | 82 | @override 83 | requestUpdate() { 84 | setState(() {}); 85 | } 86 | 87 | @override 88 | Future get updateComplete { 89 | final completer = Completer(); 90 | WidgetsBinding.instance?.addPostFrameCallback((_) { 91 | completer.complete(); 92 | }); 93 | return completer.future; 94 | } 95 | 96 | @override 97 | void didUpdateWidget(covariant T oldWidget) { 98 | super.didUpdateWidget(oldWidget); 99 | for (final ctrl in reactives) { 100 | ctrl.didUpdateWidget(oldWidget); 101 | } 102 | } 103 | 104 | // Callbacks 105 | @override 106 | void didChangeDependencies() { 107 | super.didChangeDependencies(); 108 | for (final ctrl in reactives) { 109 | ctrl.didChangeDependencies(); 110 | } 111 | } 112 | 113 | @override 114 | void activate() { 115 | super.activate(); 116 | for (final ctrl in reactives) { 117 | ctrl.activate(); 118 | } 119 | } 120 | 121 | @override 122 | void deactivate() { 123 | for (final ctrl in reactives) { 124 | ctrl.deactivate(); 125 | } 126 | super.deactivate(); 127 | } 128 | 129 | @override 130 | void dispose() { 131 | for (final ctrl in reactives) { 132 | ctrl.dispose(); 133 | } 134 | reactives.clear(); 135 | super.dispose(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/src/ticker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:reactives/reactives.dart'; 5 | 6 | /// Port of [SingleTickerProviderStateMixin] on a [Reactive] 7 | mixin SingleTickerProviderReactiveMixin on Reactive implements TickerProvider { 8 | Ticker? _ticker; 9 | 10 | @override 11 | @protected 12 | Ticker createTicker(TickerCallback onTick) { 13 | _ticker = Ticker( 14 | onTick, 15 | debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null, 16 | ); 17 | // We assume that this is called from initState, build, or some sort of 18 | // event handler, and that thus TickerMode.of(context) would return true. We 19 | // can't actually check that here because if we're in initState then we're 20 | // not allowed to do inheritance checks yet. 21 | return _ticker!; 22 | } 23 | 24 | @override 25 | void didChangeDependencies() { 26 | if (_ticker != null) _ticker!.muted = !TickerMode.of(host.context); 27 | super.didChangeDependencies(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | assert(() { 33 | if (_ticker == null || !_ticker!.isActive) return true; 34 | throw FlutterError.fromParts([ 35 | ErrorSummary('$this was disposed with an active Ticker.'), 36 | ErrorDescription( 37 | '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time ' 38 | 'dispose() was called on the mixin, that Ticker was still active. The Ticker must ' 39 | 'be disposed before calling super.dispose().', 40 | ), 41 | ErrorHint( 42 | 'Tickers used by AnimationControllers ' 43 | 'should be disposed by calling dispose() on the AnimationController itself. ' 44 | 'Otherwise, the ticker will leak.', 45 | ), 46 | _ticker!.describeForError('The offending ticker was'), 47 | ]); 48 | }()); 49 | super.dispose(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.7.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_lints: 59 | dependency: "direct dev" 60 | description: 61 | name: flutter_lints 62 | url: "https://pub.dartlang.org" 63 | source: hosted 64 | version: "1.0.4" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | lints: 71 | dependency: transitive 72 | description: 73 | name: lints 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.0.1" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.12.10" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.4.0" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.8.0" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.8.1" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.0" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.0" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.4.0" 145 | typed_data: 146 | dependency: transitive 147 | description: 148 | name: typed_data 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.3.0" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.1.0" 159 | sdks: 160 | dart: ">=2.12.0 <3.0.0" 161 | flutter: ">=1.17.0" 162 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: reactives 2 | description: A new way to extract/group/reuse common logic of StatefulWidget. Think of them like React hooks but for and of flutter. 3 | version: 0.2.0 4 | repository: https://github.com/ripemango/reactives 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | flutter_lints: ^1.0.0 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.dev/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.dev/assets-and-images/#resolution-aware. 34 | # To add custom fonts to your package, add a fonts section here, 35 | # in this "flutter" section. Each entry in this list should have a 36 | # "family" key with the font family name, and a "fonts" key with a 37 | # list giving the asset and other descriptors for the font. For 38 | # example: 39 | # fonts: 40 | # - family: Schyler 41 | # fonts: 42 | # - asset: fonts/Schyler-Regular.ttf 43 | # - asset: fonts/Schyler-Italic.ttf 44 | # style: italic 45 | # - family: Trajan Pro 46 | # fonts: 47 | # - asset: fonts/TrajanPro.ttf 48 | # - asset: fonts/TrajanPro_Bold.ttf 49 | # weight: 700 50 | # 51 | # For details regarding fonts in packages, see 52 | # https://flutter.dev/custom-fonts/#from-packages 53 | -------------------------------------------------------------------------------- /test/reactives_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | 3 | --------------------------------------------------------------------------------