Painful Truth Most Flutter State Solutions Suck
Want to get hired as a Flutter app designer, then make sure to do reactive flutter state management!
Google's decision to make one Widget stateless and one stateful had another consequence in that not enough architecture limits were applied to state management. And in the wild west of state solutions we got a mess of absolute horrible design architectures.
If you make the mistake of blindly picking up provider, riverpod, BLoC, etc. then sooner or later the lack of applied architecture limits in state management will bite you in a lot of code maintenance and app rewriting to fix app state management errors. How about we find out why those solutions suck and how to choose to architecture state management early to avoid that crap!
So why share this post and subscribe? Because, I am charting a different Flutter App architecture trek where state management is architecured early and is full reactive programming.
Elephant In The Room, Stateless and Stateful Widgets
Early in the Flutter project, Google determined that they could not implement reactive as full functions. There solution was to make two data structures and build functions that create those two data structures then to compose into the 3rd virtual rendered widget tree.
That meant that they had to make one widget stateless and one stateful and part of that effect was that for stateful the controller (i.e. State class) is combined with the view, in this case the Stateful widget. Now, having a controller and view combined should point towards something and in fact it does.
Now let's delve into the state solution mess.
Widget Based Dependency Management As A State Solution
So we have many widget based state solutions including in the SDK itself InheritedWidget, InheritedModel, InheritedNotifiers; and outside the SDK in the form of Provider and Riverpod.
While these solutions do not violate SOLID principles they only deal with distribution of the state. Which means that they are not state managment as in the other non-widget DI solutions are not state management either.
The problem will be that we can not easily use the backing logic of the stateful widget in the case of the widget DI solutions. A better way would be to instead use Flutter Hooks as the dependency injection tool as then any stateful logic re-use is supported in the Flutter Hooks.
Now, we have not dealt with the other problem of the wild-west state management architecture before we get to the DI. Let's do that now, via BLoC.
The BLoC Mess
My apologies to the team lead of the Flutter project, but BLoC sucks and so does his independent ReBloc project. BLoC violates SOLID priciples by both storing state and reducing state by a business rule.
By violating SOLID principles when we have multiple BLoCs we get this mess:
Every time we have multiple BLoCs we have to set up facades which present synch problems.
And that is true of any MVVM construct not just BLoC itself. Thus, its just not viable for large flutter apps.
What about Redux?
Redux
Redux and Flux and Elm before obviously use reactive programming principles. Here is why we do not have the same problem in Redux:
Redux's concept was to make one app global state store and views and actions would then set up reducers to compute the full app state. But, there are challenges to having such a big immutable object especially on mobile.
Facebook's solution was a revamp of single state into a project called recoil:
In recoil the single state is one view, one atom, and one reducer. In other words, app state is divided up into individual view stores that have their own reactivity via the atom and reducer constructs.
So let me lay on some terms. The whole way of this programming is called Transparent Functional Reactive Programming. The concept of multiple singles states is called Segmented State Standard.
How do we get this in Flutter? The trick is what can we re-use to act as the atom. Since each notifier is a listener, we can re-use value notifier once it has been made reactive as the atom.
The Brazilian group Flutterodo has created Atomic State Patterm ASP library to do this:
https://pub.dev/packages/asp
The Brazilian Flutterodo group developed ASP, Flutter Triple, and MobX for that set of tasks. ASP is the abstraction of SSS implementing TFRP which can be used outside of MobX and Flutter Triple. Flutter Triple offers some other stuff that can be used on top of ASP. While MobX offers a reactive observer pattern to use on top.
That also means its fully reachable for beginners as ASP redefines the ValueNotifier to be reactive.
Here, let me show you an example.
ASP Counter Example
First the Atom:
import 'package:asp/asp.dart'; | |
// atom | |
final counterState = Atom<int>(0); | |
// computed | |
String get countText => counterState.value.isEven ? 'Even' : 'Odd'; |
Each changeable state in our app is an atom.
The Counter And Other Page
import 'package:asp/asp.dart'; | |
import 'package:asp_begin/pages/other_page.dart'; | |
import 'package:flutter/material.dart'; | |
import '../atoms/counter.dart'; | |
class CounterPage extends StatelessWidget { | |
const CounterPage({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
final value = context.select(() => counterState.value); | |
return Scaffold( | |
body: Center( | |
child: GestureDetector( | |
onTap: () { | |
Navigator.of(context).push( | |
MaterialPageRoute<void>( | |
builder: (BuildContext context) => OtherPage(), | |
), | |
); | |
}, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text( | |
countText, | |
style: const TextStyle(fontSize: 40), | |
), | |
Text( | |
'$value', | |
style: const TextStyle(fontSize: 23), | |
), | |
], | |
), | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
child: const Icon(Icons.add), | |
onPressed: () => counterState.value++, | |
), | |
); | |
} | |
} |
import 'package:asp/asp.dart'; | |
import 'package:flutter/material.dart'; | |
import '../atoms/counter.dart'; | |
class OtherPage extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
final value = context.select(() => counterState.value); | |
return Scaffold( | |
appBar: AppBar(), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text( | |
countText, | |
style: TextStyle(fontSize: 40), | |
), | |
Text( | |
'$value', | |
style: TextStyle(fontSize: 23), | |
), | |
], | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
child: Icon(Icons.remove), | |
onPressed: () => counterState.value--, | |
), | |
); | |
} | |
} |
So how am I getting the model via DI then since once does not see any Flutter Hooks? Simple to be fully reachable with beginners the ASP package comes with an InheritedWdget to use, let me show you:
import 'package:asp/asp.dart'; | |
import 'package:flutter/material.dart'; | |
import 'pages/counter_page.dart'; | |
void main() { | |
runApp(const RxRoot(child: MyApp())); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
// home: MyHomePage(title: 'Flutter Demo Home Page'), | |
home: const CounterPage(), | |
); | |
} | |
} |
That RxRoot also can accept reducer parameters as well.
Obviously, since I want to stop using Provider and Riverpod is for me to write a set of flutter hooks to integrate with MobX so that I get the full power of the TFRP approach including the MobX reactive observer pattern.