flutter_rearch
애플리케이션 설계 및 아키텍처에 대한 재구성된 선언형 접근 방식
응용 프로그램 설계 및 아키텍처에 대한 재구상된 접근 방식
^7.0.0^9.0.0아래는 영문 원문 스냅샷입니다. 최신 내용은 GitHub에서 확인하세요.
CI Status Github Stars MIT License
Banner
ReArch = re-imagined approach to application design and architecture
We must state definitions and provide for priorities and descriptions of data. We must state relationships, not procedures.
-- Grace Murray Hopper, Management and the Computer of the Future (1962)
Specifically, ReArch is a novel solution to:
And with those, come:
Define your "capsules" (en-capsulated pieces of data) at the top level:
// This particular capsule manages a count from a classic example counter app,
// using the state side effect.
final Capsule<(int, void Function())> countManager = capsule((CapsuleHandle use) {
final (count, setCount) = use.state(0);
return (count, () => setCount(count + 1));
});
// This capsule provides the current count, plus one.
final Capsule<int> countPlusOneCapsule = capsule((use) => use(countManager).$1 + 1);
Capsules are simply functions that consume a CapsuleHandle.
The CapsuleHandle lets you get the data of capsules via use(someCapsule),
in addition to using a large variety of side effects via use.sideEffect().
And then, if you are using Flutter, define some widgets:
// Widgets are just like a special kind of capsule!
// Instead of a CapsuleHandle, they consume a WidgetHandle.
// They also live at the top level.
class CounterAppBody extends RearchConsumer {
const CounterAppBody({super.key});
@override
Widget build(BuildContext context, WidgetHandle use) {
final (count, incrementCount) = use(countManager);
final countPlusOne = use(countPlusOneCapsule);
return Scaffold(
appBar: AppBar(title: Text('ReArch Demo')),
floatingActionButton: FloatingActionButton(
onPressed: incrementCount,
tooltip: 'Increment',
child: Icon(Icons.add),
),
body: Center(
child: Text(
'$count + 1 = $countPlusOne',
style: TextTheme.of(context).headlineLarge,
),
),
);
}
}
Simply run:
flutter pub add rearch flutter_rearch
And (this part is important), wrap your application widget
with a RearchBootstrapper widget in your main.dart:
void main() {
runApp(RearchBootstrapper(
child: MaterialApp(...),
));
}
Simply run:
dart pub add rearch
And then just create one container for your application:
void main() {
final container = CapsuleContainer();
// Use the container.
}
Now go take a look at the documentation!
ReArch is different than other approaches to build applications because it enables feature composition and also acts upon two key observations:
Accordingly, ReArch allows you to simply define functions of state + side effects for creating both state and UI, and in doing so allows you to create applications of great scale. Also because of this insight, I'd argue that ReArch is the most testable approach to building applications today. With ReArch, all of your capsule/widget code will be pure functions (despite having arbitrary side effects!), and you will never need complicated mocks.
While ReArch can be used for state management, it is also much more; it provides a solution to build any application by borrowing from the fields of component-based software engineering and incremental computation.
ReArch was also the the subject of my master's thesis.
Further, ReArch has an extremely powerful side effects system, and that is not an understatement. You will never have to wait on a new feature; you can just create a side effect! Side effects enable you to have extremely high code reuse between your state and widget logic, and allow you to think in the same manner for both.
Worth mentioning here, ReArch's persistence side effect also provides the (arguably) best mechanism to cache/persist state out of any Flutter state management framework. Don't believe me? Take a peak at the examples in the documentation.
If the reasons listed above are not enough for you, then here are some more reasons, based on some other popular solutions.
It's maintenance-only and has fundamental problems; Riverpod is its successor.
I actually created ReArch after being mostly happy with Riverpod. The core principles behind Riverpod are incredbily smart, and I never would have thought of ReArch without them.
However, it can be argued that Riverpod has some design problems, in addition to some other grievances:
@riverpod.future and stream that are not declarative.dependencies parameter. I have seen quite a few beginners stumble across it and get confused.
You shouldn't need to specify dependencies for scoped providers
when you already do a ref.watch in the provider; that is just redundant and error-prone.You will notice a lot of carry over from Riverpod when using ReArch (because I cherry-picked many of the ideas I liked), but there are a notable few things missing (on purpose): Family and AutoDispose providers. While working on the initial version of ReArch for quite some time, and going through many design revisions, I realized how these two core parts of Riverpod are actually flawed, to an extent. While they work at the surface level for some users, the ideas backing Family/AutoDispose promote bad practices (keep reading!).
family?TL;DR: ReArch embraces the factory pattern, which solves all the issues with
family.
Families have two problems:
To best explain that first point, think for a second: where are family providers used? There are two possible answers:
hashCode and ==.
That is a lot of effort for something that could be done simply with the factory pattern and no additional overhead!For some quick context, factories are a way to create an object on demand based on some dynamic arguments, such as those provided by a user or some external mechanism. Factories allow you to create an object for however long you need it, and gracefully handle its state and disposal.
For that reason, ReArch exposes a way to make working with the factory pattern easier; see the documentation for more.
autoDispose?TL;DR: ReArch is smart enough to know which capsules it can dispose automatically. (Cool, right?)
AutoDispose, in my opinion, is a broken concept.
When you have to rely on hacks like a timer to keep something from being disposed (disposeDelay)
when navigating around an app, it should be clear that something is wrong in the implementation and/or idea.
Plus, in a mobile application, you either have global state or disposable local state.
If you need some state to automatically dispose, chances are you actually need ephemeral state.
In cases where a global cache is exceedingly large for some reason (like for app-wide image(s)/video(s)), I'd argue clearing that cache should often be taken care of on a case-per-case basis due to differing application requirements.
Consequently, AutoDispose has all sorts of parameters and traces of it scattered throughout the core framework
to accomodate one-off situations, all in addition to a barrage of types to support the different combinations
(i.e., AutoDisposeAsyncNotifierProviderElement, AutoDisposeAsyncNotifierProviderFamily,
AutoDispose..., etc.).
Instead of AutoDispose, ReArch:
I actually love flutter_hooks! Just a few grievances:
Hooks are akin to ReArch's side effects; in fact, some work very similar to or exactly the same across the libraries.
BLoC = Boilerplate + Lots of Code.
Aside from the fact that bloc is just a heavy weight wrapper around the reducer pattern
(which can be more elegantly done with useReducer from flutter_hooks or use.reducer in ReArch),
bloc is designed in a manner that thwarts any form of useful composition
and then proceeds to blame that limitation on proper application design!
What is suggested there (as of December 2023) is arguably incorrect; proper application design should encourage the use of composition, and this is nothing new. Composition is a design pattern that has been known about for many, many years and can be achieved via dependency inversion (which the docs explicitly say not to do). If you disagree with this assertion, let me counter with a question: why should you jump to UI code to connect pieces of app layer state that naturally need to interact with each other? Such a suggestion is clearly a workaround as you are then putting app layer logic in your UI code, and consequently results in unmaintainability as your app states independently evolve over time.
The underlying issue with bloc and a lot of other state management approaches is that they do one thing really well (in bloc's case, this is the reducer pattern), but then force the rest of your application to try to use this methodology even when it doesn't fit. You can read more about this phenomenon found across state management libraries here.
Tirade aside, individual blocs and cubits do work just fine.
Bloc was in fact my first state management solution some years ago,
and I often coupled it with get_it and Rx.
The earlier days of Bloc also exposed Stream transformers with async * functions,
which I did think was a clever use of language features to provide state decoupling.
get_it?There is a bit of overhead in having to set up dependency injection
with pre-Dart 3 packages like get_it
(which is actually, technically speaking, a "service locater").
Coming from get_it, you should welcome ReArch,
since all dependency inversion is done for you automatically with zero boilerplate!
Further, ReArch is a reactive dependency injection framework;
get_it lacks inter-state reactivity (note: just UI updates may be achieved through watch_it).
But, if you don't want ReArch's reactivity; no problem!
You don't have to use it, but it is there in case you do.
I was a decent fan of using Rx and streams for state management and used to do so myself.
However, there are a few main issues with Rx and streams:
Signals only constitute a subset of ReArch's functionality. If you took ReArch and removed:
.value
Then you'd arrive at Signals.
So, if you like Signals, you'll likely love ReArch, because you'll get all of the benefits of Signals but with many more features (mostly due to ReArch's side effects model). The only main difference is in the API, but that is easy enough to adjust to--signals and capsules tend to map one-to-one.
If you must have the easier-to-learn Signals API, just add this line to your project:
/// Quick capsule wrapper to enable Signals-like syntax.
Capsule<T> capsule<T>(Capsule<T> c) => c;
// And in the rest of your code:
final myCapsule = capsule((use) {
final someOtherData = use(someOtherCapsule);
return use.data(123); // returns a myValue.value for updating state
});
Note that I personally don't recommend the capsule() syntax
since anyone reading such code without an IDE will have no idea what the types are.
But, if you're doing solo work on a project and you always have an editor with LSP support,
then knock yourself out.
Your code will look like 🍝.
Giving credit where credit is due:
I got the original idea for ReArch (although, it was much different back then!)
after using riverpod, flutter_hooks, and functional_widget.
ReArch would not have been possible if not for these stellar, role-model projects.
ReArch took dozens (not an exaggeration) of design overhauls to arrive at where it is today,
incorporating many ideas from these 3 pacakges along the way.
As much as I have done with ReArch, it always seems like there is more to do. One person can only do so much!
If you would like to contribute, here are some areas where I would really appreciate help!
AsyncValue and Option are easy "first issues"
You can become a sponsor of my work here!
https://raw.githubusercontent.com/GregoryConrad/GregoryConrad/main/sponsorkit/sponsors.svg