refena

Creator: coderz1093

Last updated:

Add to Cart

Description:

refena

A state management library for Dart and Flutter.
Inspired by Riverpod and async_redux.
Preview #
Define a provider:
final counterProvider = NotifierProvider<Counter, int>((ref) => Counter());

class Counter extends Notifier<int> {
@override
int init() => 10;

void increment() => state++;
}
copied to clipboard
Use context.watch to access the provider:
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
int counterState = context.watch(counterProvider);
return Scaffold(
body: Center(
child: Text('Counter state: $counterState'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.notifier(counterProvider).increment(),
child: const Icon(Icons.add),
),
);
}
}
copied to clipboard
Or in Redux style:
final counterProvider = ReduxProvider<ReduxCounter, int>((ref) => Counter());

class ReduxCounter extends ReduxNotifier<int> {
@override
int init() => 10;
}

class AddAction extends ReduxAction<ReduxCounter, int> {
final int amount;

AddAction(this.amount);

@override
int reduce() => state + amount;
}

class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
int counterState = context.watch(counterProvider);
return Scaffold(
body: Center(
child: Text('Counter state: $counterState'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.redux(counterProvider).dispatch(AddAction(2)),
child: const Icon(Icons.add),
),
);
}
}
copied to clipboard
Offering high traceability with RefenaDebugObserver and RefenaTracingObserver:
[Refena] Action dispatched: [ReduxCounter.AddAction] by [MyPage]
[Refena] Change by [ReduxCounter] triggered by [AddAction]
- Prev: 5
- Next: 8
- Rebuild (1): [MyPage]
copied to clipboard
With a feature-rich Refena Inspector:

Table of Contents #

Refena vs Riverpod

Key differences
Similarities
Downsides
Migration


Refena vs async_redux
Getting Started
Access the state
Container and Scope
Providers

Provider
FutureProvider
FutureFamilyProvider
StateProvider
StreamProvider
ChangeNotifierProvider
NotifierProvider
AsyncNotifierProvider
ReduxProvider
ViewProvider
ViewFamilyProvider


Notifiers

Lifecycle
Notifier vs PureNotifier
Access BuildContext inside a notifier


Using ref

ref.read
ref.watch
ref.accessor
ref.stream
ref.future
ref.rebuild
ref.notifier
ref.redux
ref.dispose
ref.message
ref.container
BuildContext Extensions


What to choose?
Performance Optimization
ensureRef
defaultRef
Observer
Tools

Event Tracing
Dependency Graph
Inspector
Sentry


Testing

Override providers
Testing without Flutter
Testing ReduxProvider
Testing Notifiers
Access the state within tests
State events
Example test


Add-ons

Snackbars
Navigation
Actions


Dart only
Deep Dives

Refena vs Riverpod #
Refena is aimed to be more notifier focused than Riverpod.
Refena also includes a comprehensive Redux implementation
that can be used for crucial parts of your app.
➤ Key differences #
Flutter native:
No ConsumerWidget or ConsumerStatefulWidget. You still use StatefulWidget or StatelessWidget as usual.
To access ref, you can either add with Refena (only in StatefulWidget) or call context.ref.
Common super class:
WatchableRef extends Ref.
You can use Ref as a parameter type to implement util functions that need access to ref.
These functions can be called by providers and also by widgets.
No Scheduler:
If a provider changes, listeners will be notified in the next micro task.
That's all.
There is no scheduler that is running in the background.
Use ref anywhere, anytime:
Don't worry that the ref within providers or notifiers becomes invalid.
They live as long as the RefenaScope.
Notifier first:
With Notifier, AsyncNotifier, PureNotifier, and ReduxNotifier,
you can choose the right notifier for your use case.
➤ Similarities #
Testable:
Providers are stateless.
The state is stored in the RefenaContainer.
This makes providers testable.
Type-safe:
Working with providers and notifiers are type-safe and null-safe.
Auto register:
Don't worry that you forget to register a provider.
They are automatically registered when you use them.
➤ Downsides #
No autodispose:
Since there is no ConsumerWidget, providers are never disposed automatically.
Refena encourages you to use ref.dispose to dispose providers explicitly.
For view models, you can use ViewModelBuilder which disposes the provider automatically.
➤ Migration #
Use refena_riverpod_extension
to use Riverpod and Refena at the same time.
// Riverpod -> Refena
ref.refena.read(myRefenaProvider);

// Refena -> Riverpod
ref.riverpod.read(myRiverpodProvider);
copied to clipboard
Checkout Refena for Riverpod developers for more information.
Refena vs async_redux #
Compared to async_redux,
Refena encourages you to split the state into multiple providers,
making it easier to scale your app.
➤ Key differences #
More scalable:
Since actions are bound to a ReduxProvider,
you can split your app into multiple providers with each provider having its own set of actions.
This makes it easier to scale your app.
You can also view the dependency graph without any setup.
No StoreConnector:
You don't need to use a StoreConnector to access the state.
If you wish, you can use a ViewModelBuilder instead, but it's not required.
More type-safety:
Refena is more type-safe than async_redux since it differentiates between synchronous and asynchronous actions.
Getting started #
Step 1: Add dependency
Add refena_flutter for Flutter projects,
or refena for Dart projects.
# pubspec.yaml
dependencies:
refena_flutter: <version>
copied to clipboard
Step 2: Add RefenaScope
void main() {
runApp(
RefenaScope(
child: const MyApp(),
),
);
}
copied to clipboard
Step 3: Define a provider
final myProvider = Provider((ref) => 42);
copied to clipboard
Step 4: Use the provider
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final myValue = context.watch(myProvider);
return Scaffold(
body: Center(
child: Text('The value is $myValue'),
),
);
}
}
copied to clipboard
Access the state #
The state should be accessed via ref.
You can get the ref right from the context:
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ref = context.ref;
final myValue = ref.watch(myProvider);
final mySecondValue = ref.watch(mySecondProvider);
return Scaffold(
body: Column(
children: [
Text('The value is $myValue'),
Text('The second value is $mySecondValue'),
],
),
);
}
}
copied to clipboard
To make your life easier, you can also skip the ref part: Just call context.watch.
In a StatefulWidget, you can use with Refena to access the ref directly.
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _CounterState();
}

class _MyPageState extends State<MyPage> with Refena {
@override
Widget build(BuildContext context) {
final myValue = ref.watch(myProvider);
return Scaffold(
body: Center(
child: Text('The value is $myValue'),
),
);
}
}
copied to clipboard
You can also use Consumer to access the state. This is useful to rebuild only a part of the widget tree:
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Consumer(
builder: (context, ref) {
final myValue = ref.watch(myProvider);
return Text('The value is $myValue');
}
),
),
);
}
}
copied to clipboard
Container and Scope #
The RefenaContainer is the root of your app. It holds the state of all providers.
In Flutter, you should use RefenaScope instead which provides easy access to the state from a BuildContext.
A RefenaScope is just a wrapper around a RefenaContainer.
If no container is provided, a new one is implicitly created.
void main() {
runApp(
RefenaScope( // <-- creates an implicit container
child: const MyApp(),
),
);
}
copied to clipboard
You can also create a container explicitly. Use RefenaScope.withContainer to provide the container:
void main() {
final container = RefenaContainer();

// pre init procedure
container.set(databaseProvider.overrideWithValue(DatabaseService()));
container.read(analyticsService).appStarted();

runApp(
RefenaScope.withContainer(
container: container, // <-- explicit container
child: const MyApp(),
),
);
}
copied to clipboard
More about initialization here.
Providers #
There are many types of providers. Choose the right one for your use case.



Provider
Usage
Notifier API
Can watch*
Has family*




Provider
Constants or services
-
No
No


ViewProvider
View models
-
Yes
Yes


FutureProvider
Async values
-
Yes
Yes


StreamProvider
Streams
-
Yes
Yes


StateProvider
Simple states
setState
No
No


ChangeNotifierProvider
More rebuild control
Custom methods
No
No


NotifierProvider
Regular services
Custom methods
No
No


AsyncNotifierProvider
Services with futures
Custom methods
No
No


ReduxProvider
Action based services
Custom actions
No
No



* watch means that you can use ref.watch inside the provider build lambda.
* family means that you can use <Provider>.family to create a parameterized collection of a provider.
➤ Provider #
The Provider is the most basic provider.
Use this provider for immutable values (constants or stateless services).
final databaseProvider = Provider((ref) => DatabaseService());
copied to clipboard
To access the value:
// Everywhere
DatabaseService a = ref.read(databaseProvider);

// Inside a build method
DatabaseService a = ref.watch(databaseProvider);
copied to clipboard
This type of provider can replace your entire dependency injection solution.
➤ FutureProvider #
Use this provider for asynchronous values.
The value is cached and only fetched once.
import 'package:package_info_plus/package_info_plus.dart';

final versionProvider = FutureProvider((ref) async {
final info = await PackageInfo.fromPlatform();
return '${info.version} (${info.buildNumber})';
});
copied to clipboard
Access:
build(BuildContext context) {
AsyncValue<String> versionAsync = ref.watch(versionProvider);
return versionAsync.when(
data: (version) => Text('Version: $version'),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error: $error'),
);
}
copied to clipboard
More about AsyncValue here.
➤ FutureFamilyProvider #
Use this provider for multiple asynchronous values. Use FutureProvider.family for better readability.
final userProvider = FutureProvider.family<User, String>((ref, id) async {
final api = ref.read(apiProvider);
return api.fetchUser(id);
});
copied to clipboard
Access:
build(BuildContext context) {
AsyncValue<User> userAsync = ref.watch(userProvider('123'));
}
copied to clipboard
➤ StateProvider #
The StateProvider is handy for simple use cases where you only need a setState method.
final myProvider = StateProvider((ref) => 10);
copied to clipboard
Update the state:
ref.notifier(myProvider).setState((old) => old + 1);
copied to clipboard
➤ StreamProvider #
Use this provider to listen to a stream.
final myProvider = StreamProvider<int>((ref) async* {
yield 1;
await Future.delayed(const Duration(seconds: 1));
yield 2;
await Future.delayed(const Duration(seconds: 1));
yield 3;
});
copied to clipboard
Access:
build(BuildContext context) {
AsyncValue<int> streamAsync = ref.watch(myProvider);
return streamAsync.when(
data: (value) => Text('The value is $value'),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error: $error'),
);
}
copied to clipboard
➤ ChangeNotifierProvider #
Use this provider if you have many rebuilds and need to optimize performance (e.g., progress indicator).
final myProvider = ChangeNotifierProvider((ref) => MyNotifier());

class MyNotifier extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;

void increment() {
_counter++;
notifyListeners();
}
}
copied to clipboard
Use ref.watch to listen and ref.notifier to call methods:
build(BuildContext context) {
final counter = ref.watch(myProvider).counter;
return Scaffold(
body: Center(
child: Column(
children: [
Text('The value is $counter'),
ElevatedButton(
onPressed: ref.notifier(myProvider).increment,
child: const Text('Increment'),
),
],
),
),
);
}
copied to clipboard
➤ NotifierProvider #
Use this provider for mutable values.
This provider can be used in an MVC-like pattern.
The notifiers are never disposed. You may have custom logic to delete values within a state.
final counterProvider = NotifierProvider<Counter, int>((ref) => Counter());

class Counter extends Notifier<int> {
@override
int init() => 10;

void increment() => state++;
}
copied to clipboard
To access the value:
// Everywhere
int a = ref.read(counterProvider);

// Inside a build method
int a = ref.watch(counterProvider);
copied to clipboard
To access the notifier:
Counter counter = ref.notifier(counterProvider);
copied to clipboard
Or within a click handler:
ElevatedButton(
onPressed: () {
ref.notifier(counterProvider).increment();
},
child: const Text('+ 1'),
)
copied to clipboard
➤ AsyncNotifierProvider #
Use this provider for mutable async values.
final counterProvider = AsyncNotifierProvider<Counter, int>((ref) => Counter());

class Counter extends AsyncNotifier<int> {
@override
Future<int> init() async {
await Future.delayed(const Duration(seconds: 1));
return 0;
}

void increment() async {
// Set `future` to update the state.
future = ref.notifier(apiProvider).fetchAsyncNumber();

// Use `setState` to also access the old value.
setState((snapshot) async => (snapshot.curr ?? 0) + 1);

// Set `state` directly if you want more control.
final old = state.data ?? 0;
state = AsyncValue.loading(old);
await Future.delayed(const Duration(seconds: 1));
state = AsyncValue.data(old + 1);
}
}
copied to clipboard
Notice that AsyncValue.loading() can take an optional previous value.
This is handy if you want to show the previous value while loading.
When using when, it will automatically use the previous value by default.
build(BuildContext context) {
final (counter, loading) = ref.watch(counterProvider.select((s) => (s, s.isLoading)));
return counter.when(
// also shows previous value even when it's loading
data: (value) => Text('The value is $value (loading: $loading)'),

// only shown initially when there is no previous value
loading: () => Text('Loading...'),

// always shows the error if the future fails (configurable)
error: (error, stackTrace) => Text('Error: $error'),
);
}
copied to clipboard
➤ ReduxProvider #

Redux full documentation.
The ReduxProvider is the strictest option. The state is solely altered by actions.
You need to provide other notifiers via constructor
(dependency injection)
making the ReduxNotifier self-contained and testable.
Redux has two main benefits:

Tracing: With RefenaDebugObserver or RefenaTracingObserver, you can see every state transition.
Testing: You can test the state transitions.

final counterProvider = ReduxProvider<Counter, int>((ref) {
return Counter(ref.notifier(providerA), ref.notifier(providerB));
});

class Counter extends ReduxNotifier<int> {
final ServiceA serviceA;
final ServiceB serviceB;

Counter(this.serviceA, this.serviceB);

@override
int init() => 0;
}

class AddAction extends ReduxAction<Counter, int> {
final int amount;

AddAction(this.amount);

@override
int reduce() => state + amount;
}

class SubtractAction extends ReduxAction<Counter, int> {
final int amount;

SubtractAction(this.amount);

@override
int reduce() => state - amount;

// This is called after the state transition
@override
void after() {
// dispatch actions in the same notifier
dispatch(AddAction(amount - 1));

// dispatch actions of other notifiers
external(notifier.serviceA).dispatch(SomeAction());

// access the state of other notifiers
if (notifier.serviceB.state == 3) {
// ...
}
}
}
copied to clipboard
The widget can trigger actions with ref.redux(provider).dispatch(action):
class MyPage extends StatelessWidget {
const MyPage({super.key});

@override
Widget build(BuildContext context) {
final state = context.watch(counterProvider);
return Scaffold(
body: Column(
children: [
Text(state.toString()),
ElevatedButton(
onPressed: () => context.redux(counterProvider).dispatch(AddAction(2)),
child: const Text('Increment'),
),
ElevatedButton(
onPressed: () => context.redux(counterProvider).dispatch(SubtractAction(3)),
child: const Text('Decrement'),
),
],
),
);
}
}
copied to clipboard
Here is how the console output could look like with RefenaDebugObserver:
[Refena] Action dispatched: [ReduxCounter.SubtractAction] by [MyPage]
[Refena] Change by [ReduxCounter] triggered by [SubtractAction]
- Prev: 8
- Next: 5
- Rebuild (1): [MyPage]
copied to clipboard
You can additionally override before, after, and wrapReduce in a ReduxAction to add custom logic.
The redux feature is quite complex.
You can read more about it here.
➤ ViewProvider #
The ViewProvider is the only provider that can watch other providers.
This is useful for view models that depend on multiple providers.
This requires more code but makes your app more testable.
class SettingsVm {
final String firstName;
final String lastName;
final ThemeMode themeMode;
final void Function() logout;

// insert constructor
}

final settingsVmProvider = ViewProvider((ref) {
final auth = ref.watch(authProvider);
final themeMode = ref.watch(themeModeProvider);
return SettingsVm(
firstName: auth.firstName,
lastName: auth.lastName,
themeMode: themeMode,
logout: () => ref.notifier(authProvider).logout(),
);
});
copied to clipboard
The widget:
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.watch(settingsVmProvider);
return Scaffold(
body: Center(
child: Column(
children: [
Text('First name: ${vm.firstName}'),
Text('Last name: ${vm.lastName}'),
Text('Theme mode: ${vm.themeMode}'),
ElevatedButton(
onPressed: vm.logout,
child: const Text('Logout'),
),
],
),
),
);
}
}
copied to clipboard
To automatically dispose the view model, you can use the ViewModelBuilder widget.
Nice to know: The ViewModelBuilder not only works with ViewProvider but with any provider.
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewModelBuilder(
provider: settingsVmProvider,
builder: (context, vm) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('First name: ${vm.firstName}'),
Text('Last name: ${vm.lastName}'),
Text('Theme mode: ${vm.themeMode}'),
ElevatedButton(
onPressed: vm.logout,
child: const Text('Logout'),
),
],
),
),
);
},
);
}
}
copied to clipboard
You can also add lifecycle hooks to the view model:
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewModelBuilder(
provider: settingsVmProvider,
init: (context) => context.notifier(authProvider).init(),
dispose: (ref) => ref.notifier(authProvider).dispose(),
placeholder: (context) => Text('Loading...'), // while init is running
error: (context, error, stackTrace) => Text('Error: $error'), // when init fails
builder: (context, vm) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('First name: ${vm.firstName}'),
Text('Last name: ${vm.lastName}'),
Text('Theme mode: ${vm.themeMode}'),
ElevatedButton(
onPressed: vm.logout,
child: const Text('Logout'),
),
],
),
),
);
},
);
}
}
copied to clipboard
➤ ViewFamilyProvider #
Similar to ViewProvider but with a parameter. Use ViewProvider.family for better readability.
final settingsVmProvider = ViewProvider.family<SettingsVm, String>((ref, userId) {
final auth = ref.watch(authProvider(userId));
final themeMode = ref.watch(themeModeProvider);
return SettingsVm(
firstName: auth.firstName,
lastName: auth.lastName,
themeMode: themeMode,
logout: () => ref.notifier(authProvider).logout(),
);
});
copied to clipboard
A ViewModelBuilder would dispose the whole family provider.
You should use FamilyViewModelBuilder (or ViewModelBuilder.family) instead.
This will only dispose a member of the family on widget disposal.
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
int userId = 123;
return ViewModelBuilder.family(
provider: settingsVmProvider(userId),
builder: (context, vm) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('First name: ${vm.firstName}'),
Text('Last name: ${vm.lastName}'),
Text('Theme mode: ${vm.themeMode}'),
ElevatedButton(
onPressed: vm.logout,
child: const Text('Logout'),
),
],
),
),
);
},
);
}
}
copied to clipboard
Notifiers #
A notifier holds the actual state and triggers rebuilds on widgets listening to them.



Provider
Usage
Provider
Exposes ref




Notifier
For any use case
NotifierProvider
Yes


ChangeNotifier
For performance critical services
ChangeNotifierProvider
Yes


AsyncNotifier
For async values
AsyncNotifierProvider
Yes


PureNotifier
For clean architectures
NotifierProvider
No


ReduxNotifier
For very clean architectures
ReduxProvider
No



➤ Lifecycle #
Inside the init method, you can access the ref to read other providers.
The main goal is to return the initial state. Avoid any additional logic here.
class MyNotifier extends Notifier<int> {
@override
int init() {
final persistenceService = ref.read(persistenceProvider);
return persistenceService.getNumber();
}
}
copied to clipboard
To do initial work, you can override postInit.
At this point, the state is already set, and you can access or modify it via state.
class MyNotifier extends Notifier<int> {
@override
int init() => 10;

@override
void postInit() {
print('The initial value is $state');
state++;
}
}
copied to clipboard
When ref.dispose(provider) is called, you can hook into the disposing process by overriding dispose.
class MyNotifier extends Notifier<int> {
@override
int init() => 10;

@override
void dispose() {
// custom cleanup logic
// everything is still accessible until the end of this method
}
}
copied to clipboard
➤ Notifier vs PureNotifier #
Notifier and PureNotifier are very similar.
The difference is that the Notifier has access to ref and the PureNotifier does not.
// You need to specify the generics (<..>) to have the correct type inference
// Waiting for https://github.com/dart-lang/language/issues/524
final counterProvider = NotifierProvider<Counter, int>((ref) => Counter());

class Counter extends Notifier<int> {
@override
int init() => 10;

void increment() {
final anotherValue = ref.read(anotherProvider);
state++;
}
}
copied to clipboard
A PureNotifier has no access to ref making this notifier self-contained.
This is often used in combination with dependency injection,
where you provide the dependencies via constructor.
final counterProvider = NotifierProvider<PureCounter, int>((ref) {
final persistenceService = ref.read(persistenceProvider);
return PureCounter(persistenceService);
});

class PureCounter extends PureNotifier<int> {
final PersistenceService _persistenceService;

PureCounter(this._persistenceService);

@override
int init() => 10;

void increment() {
counter++;
_persistenceService.persist();
}
}
copied to clipboard
➤ Access BuildContext inside a notifier #
To access BuildContext inside a notifier, you need to bind the lifetime of the notifier to the widget.
This is done by adding the ViewBuildContext mixin to the notifier.
final counterProvider = NotifierProvider<Counter, int>((ref) => Counter());

class Counter extends Notifier<int> with ViewBuildContext {
@override
int init() => 10;

void increment() {
state++;
ScaffoldMessenger.of(context).showSnackBar( // <-- access BuildContext
SnackBar(
content: Text('Counter: $state'),
),
);
}
}
copied to clipboard
This provider needs to be initialized with ViewModelBuilder.
Not doing so will throw an error.
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewModelBuilder(
provider: counterProvider, // <-- Binds the lifetime of the provider to the widget
builder: (context, vm) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('The value is $vm'),
ElevatedButton(
onPressed: () => context.notifier(counterProvider).increment(),
child: const Text('Increment'),
),
],
),
),
);
},
);
}
}
copied to clipboard
Using ref #
With ref, you can access the providers and notifiers.
➤ ref.read #
Read the value of a provider.
int a = ref.read(myProvider);
copied to clipboard
➤ ref.watch #
Read the value of a provider and rebuild the widget / provider when the value changes.
This should be used within a build method of a widget or inside a provider build lambda.
Warning: Watching outside the build method can lead to inconsistent rebuilds.
Widget build(BuildContext context) {
final ref = context.ref;
final currentValue = ref.watch(myProvider);

// ...
}
copied to clipboard
You may add an optional listener callback:
build(BuildContext context) {
final currentValue = ref.watch(myProvider, listener: (prev, next) {
print('The value changed from $prev to $next');
});

// ...
}
copied to clipboard
➤ ref.accessor #
Similar to Ref.read, but instead of returning the state right away,
it returns a StateAccessor to get the state later.
This is useful if you need to read the latest state of a provider (ViewProvider in particular),
but you can't use Ref.watch when building a notifier.
More about dependency injection.
final myViewProvider = ViewProvider((ref) {
final counter = ref.watch(counterProvider);
return MyView(counter);
});

final myProvider = NotifierProvider<MyNotifier, int>((ref) {
// If we just use ref.read, then we don't have the latest state
// of the myViewProvider.
final view = ref.accessor(myViewProvider);
return MyNotifier(view);
});

class MyNotifier extends Notifier<int> {
final StateAccessor<MyView> _view;

MyNotifier(this._view);

@override
int init() => 0;

void add() {
state += _view.state.counter;
}
}
copied to clipboard
➤ ref.stream #
Similar to ref.watch with listener, but you need to manage the subscription manually.
The subscription will not be disposed automatically.
Use this outside a build method.
final subscription = ref.stream(myProvider).listen((value) {
print('The value changed from ${value.prev} to ${value.next}');
});
copied to clipboard
➤ ref.future #
Get the Future of a FutureProvider, a StreamProvider, or an AsyncNotifierProvider.
Future<String> version = ref.future(versionProvider);
copied to clipboard
➤ ref.rebuild #
Reruns the build method of a provider and triggers a rebuild on all listeners.
This is useful if you want to manually rebuild a provider.
Only available for rebuildable providers: ViewProvider, FutureProvider, StreamProvider.
Returns the result of the build method: T, Future<T>, Stream<T>.
Future<T> result = ref.rebuild(myFutureProvider);
copied to clipboard
➤ ref.notifier #
Get the notifier of a provider.
Counter counter = ref.notifier(counterProvider);

// or

ref.notifier(counterProvider).increment();
copied to clipboard
➤ ref.redux #
Dispatches an action to a ReduxProvider.
ref.redux(myReduxProvider).dispatch(MyAction());

await ref.redux(myReduxProvider).dispatchAsync(MyAsyncAction());
copied to clipboard
➤ ref.dispose #
Providers are never disposed automatically.
Instead, you should create a custom "cleanup" logic.
To make your life easier, you can dispose a provider by calling this method:
ref.dispose(myProvider);
copied to clipboard
This can be called in a StatefulWidget's dispose method and is safe to do so because ref.dispose does not trigger a rebuild.
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> with Refena {
@override
void dispose() {
ref.dispose(myProvider); // <-- dispose the provider
super.dispose();
}
}
copied to clipboard
In a notifier, you can hook into the disposing process by overriding dispose.
class MyNotifier extends Notifier<int> {
@override
int init() => 10;

@override
void dispose() {
// custom cleanup logic
// everything is still accessible until the end of this method
}
}
copied to clipboard
➤ ref.message #
Emits a message to the observer.
This might be handy if you use one of the built-in observers like RefenaDebugObserver.
The messages are also shown in the inspector.
ref.message('Hello World');
copied to clipboard
Inside a ReduxAction, this method is available as emitMessage.
➤ ref.container #
Returns the backing container.
The container exposes more advanced methods for edge cases like post-init overrides.
RefenaContainer container = ref.container;
copied to clipboard
➤ BuildContext Extensions #
Some frequently used methods are available as extensions on BuildContext.
context.watch(myProvider);
context.read(myProvider);
context.notifier(myProvider).increment();
context.redux(myReduxProvider).dispatch(MyAction());
copied to clipboard
What to choose? #
There are lots of providers and notifiers. Which one should you choose?
For most use cases, Provider and Notifier are more than enough.
If you work in an environment where clean architecture is important,
you may want to use ReduxProvider and ViewProvider.
Be aware that you will need to write more boilerplate code.



Providers & Notifiers
Boilerplate
Testability




Provider, StateProvider

Low


Provider, NotifierProvider
notifiers
Medium


Provider, ReduxProvider
notifiers, actions
High


Provider, ViewProvider, NotifierProvider
notifiers, view models
Very high


Provider, ViewProvider, ReduxProvider
notifiers, view models, actions
Maximum



➤ Can I use different providers & notifiers together? #
Yes. You can use any combination of providers and notifiers.
The cool thing about notifiers is that they are self-contained.
Usually, you don't need to create a view model for each page.
This refactoring can be done later when your page gets too complex.
➤ What layers should an app have? #
Let's start from the UI (Widgets):

Widgets: The UI layer. This is where you build your UI. It should not contain any business logic.
View Models: This layer provides the data for the UI. It should not contain any business logic. Use ViewProvider for this layer.
Controllers: This layer contains the business logic for one specific view (page). It should not contain any business logic shared between multiple views. Possible providers: NotifierProvider, ReduxProvider.
Services: This layer contains the business logic that is shared between multiple views. A "service" is essentially a "feature" of your app. Possible providers: NotifierProvider, ReduxProvider.

As always, you don't need to use all layers. It depends on the complexity of your app.

Create widgets and services first.
If you notice that your widgets get too complex, you can add controllers or view models to avoid StatefulWidgets.

Simple StatefulWidgets are fine because they are still self-contained.
The problem starts when you want to test them.
View models makes it easier to test the UI.
This is a very pragmatic approach. You can also write the full architecture from the beginning.
Additional types like "repositories" can be added if you need them.
Usually, they can be treated as services (but more specialized) in this model.
You can open the dependency graph to see how the layers are connected.
Performance Optimization #
➤ Selective watching #
You may restrict the rebuilds to only a subset of the state with provider.select.
Here, the == operator is used to compare the previous and next value.
build(BuildContext context) {
final themeMode = ref.watch(
settingsProvider.select((settings) => settings.themeMode),
);

// ...
}
copied to clipboard
For more complex logic, you can use rebuidWhen.
build(BuildContext context) {
final currentValue = ref.watch(
myProvider,
rebuildWhen: (prev, next) => prev.attribute != next.attribute,
);

// ...
}
copied to clipboard
You can use both select and rebuildWhen at the same time.
The select will be applied, when rebuildWhen returns true.
Do not execute ref.watch of the same provider multiple times
as only the last one will be used for the rebuild condition.
Instead, you should use Records to combine multiple values.
build(BuildContext context) {
final (themeMode, locale) = ref.watch(settingsProvider.select((settings) {
return (settings.theme, settings.locale);
}));
}
copied to clipboard
➤ Notify Strategy #
A more global approach than select is to set a defaultNotifyStrategy.
By default, NotifyStrategy.equality is used to reduce rebuilds.
You can change it to NotifyStrategy.identity to use identical instead of ==.
This avoids comparing deeply nested objects.
void main() {
runApp(
RefenaScope(
defaultNotifyStrategy: NotifyStrategy.identity,
child: const MyApp(),
),
);
}
copied to clipboard
You probably noticed the default- prefix.
Of course, you can override updateShouldNotify for each notifier individually.
class MyNotifier extends Notifier<int> {
@override
int init() => 10;

@override
bool updateShouldNotify(int old, int next) => old != next;
}
copied to clipboard
ensureRef #
In a StatefulWidget, you can use ensureRef to access the providers and notifiers within initState.
You may also use ref inside dispose because ref is guaranteed to be initialized.
Please note that you need with Refena.
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> with Refena {
@override
void initState() {
super.initState();
ensureRef((ref) {
ref.read(myProvider);
});

// or
ensureRef();
}

@override
void dispose() {
ensureRef((ref) {
// This is safe now because we called `ensureRef` in `initState`
ref.read(myProvider);
ref.notifier(myNotifierProvider).doSomething();
});
super.dispose();
}
}
copied to clipboard
defaultRef #
If you are unable to access ref, there is a pragmatic solution for that.
You can use RefenaScope.defaultRef to access the providers and notifiers.
Remember that this is only for edge cases, and you should always use the accessible ref if possible.
void someFunction() {
Ref ref = RefenaScope.defaultRef;
ref.read(myProvider);
ref.notifier(myNotifierProvider).doSomething();
}
copied to clipboard
Observer #
With observers, you access the events emitted by providers.
To add one, specify observers in the RefenaScope / RefenaContainer constructor.
You can implement one yourself or just use one of the built-in observers.
void main() {
runApp(
RefenaScope(
observers: [
if (kDebugMode) ...[
RefenaDebugObserver(), // <-- built-in observer
],
],
child: const MyApp(),
),
);
}
copied to clipboard
Now you will see useful information printed into the console:
[Refena] Provider initialized: [Counter]
- Reason: INITIAL ACCESS
- Value: 10
[Refena] Listener added: [SecondPage] on [Counter]
[Refena] Change by [Counter]
- Prev: 10
- Next: 11
- Rebuild (2): [HomePage], [SecondPage]
copied to clipboard
Example implementation of a custom observer. Note that you also have access to ref.
class MyObserver extends RefenaObserver {
@override
void init() {
// optional initialization logic
ref.read(crashReporterProvider).init();
}

@override
void handleEvent(RefenaEvent event) {
if (event is ActionErrorEvent) {
Object error = event.error;
StackTrace stackTrace = event.stackTrace;

ref.read(crashReporterProvider).report(error, stackTrace);

if (error is ConnectionException) {
// show snackbar
ref.global.dispatch(ShowSnackBarAction(message: 'No internet connection'));
}
}
}
}
copied to clipboard
➤ Built-in Observers #



Observer
Description
Package




RefenaDebugObserver
Prints all events to the console. Useful for debugging.
refena


RefenaHistoryObserver
Stores events in a list. Useful for testing.
refena


RefenaTracingObserver
Stores recent events in a list to be read by a tracing view.
refena


RefenaInspectorObserver
Sends the state of the app to an inspector server.
refena_inspector_client


RefenaSentryObserver
Populates the Sentry breadcrumbs.
refena_sentry



Tools #
➤ Event Tracing #
Refena includes a ready-to-use UI to trace the state changes.
First, you need to add the RefenaTracingObserver to the RefenaScope.
void main() {
runApp(
RefenaScope(
observers: [
RefenaTracingObserver(), // <-- add this observer
],
child: const MyApp(),
),
);
}
copied to clipboard
Then, you can use the RefenaTracingPage to show the state changes.
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const RefenaTracingPage(), // <-- open the page
),
);
},
child: const Text('Show tracing'),
),
);
}
}
copied to clipboard
Here is how it looks like:

Side note:
The RefenaTracingObserver itself is quite performant as events are added and removed with O(1) complexity.
To build the tree, it uses O(n^2) complexity, where n is the number of events.
That's why you see a loading indicator when you open the tracing UI.
➤ Dependency Graph #
You can open the RefenaGraphPage to see the dependency graph. It requires no setup.
Be aware that Refena only tracks dependencies during the build phase of a notifier,
hence, always prefer dependency injection!
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const RefenaGraphPage(), // <-- open the page
),
);
},
child: const Text('Show graph'),
),
);
}
}
copied to clipboard
Here is how it looks like:

➤ Inspector #
Both the RefenaTracingPage and the RefenaGraphPage are included in the Refena Inspector.

➤ Sentry #
Use refena_sentry to add breadcrumbs to Sentry.
This is especially useful if you use Redux since you can see every action in Sentry.

Testing #
➤ Override providers #
You can override any provider in your tests.
void main() {
testWidgets('My test', (tester) async {
await tester.pumpWidget(
RefenaScope(
overrides: [
myProvider.overrideWithValue(42),
myNotifierProvider.overrideWithNotifier((ref) => MyNotifier(42)),
],
child: const MyApp(),
),
);
});
}
copied to clipboard
➤ Testing without Flutter #
You can use RefenaContainer to test your providers without Flutter.
void main() {
test('My test', () {
final ref = RefenaContainer();

expect(ref.read(myCounter), 0);
ref.notifier(myCounter).increment();
expect(ref.read(myCounter), 1);
});
}
copied to clipboard
➤ Testing ReduxProvider #
For simple tests, you can use ReduxNotifier.test. It provides a state getter, a setState method and optionally allows you to specify an initial state.
void main() {
test('My test', () {
final counter = ReduxNotifier.test(
redux: Counter(),
initialState: 11,
);

expect(counter.state, 11);

counter.dispatch(IncrementAction());
expect(counter.state, 12);

counter.setState(42); // set state directly
expect(counter.state, 42);
});
}
copied to clipboard
To quickly override a ReduxProvider, you can use overrideWithReducer.
void main() {
test('Override test', () {
final ref = RefenaContainer(
overrides: [
counterProvider.overrideWithReducer(
reducer: {
IncrementAction: (state) => state + 20,
DecrementAction: null, // do nothing
},
),
],
);

expect(ref.read(counterProvider), 0);

// Should use the overridden reducer
ref.redux(counterProvider).dispatch(IncrementAction());
expect(ref.read(counterProvider), 20);

// Should not change the state
ref.redux(counterProvider).dispatch(DecrementAction());
expect(ref.read(counterProvider), 20);
});
}
copied to clipboard
➤ Testing Notifiers #
You can use Notifier.test (or AsyncNotifier.test) to test your notifiers in isolation.
This is only useful if your notifier is not dependent on other providers,
or if you have specified all dependencies via constructor (dependency injection).
void main() {
test('My test', () {
final counter = Notifier.test(
notifier: Counter(),
initialState: 11,
);

expect(counter.state, 11);

counter.notifier.increment();
expect(counter.state, 12);
});
}
copied to clipboard
➤ Access the state within tests #
A RefenaScope is a Ref, so you can access the state directly.
void main() {
testWidgets('My test', (tester) async {
final ref = RefenaScope(
child: const MyApp(),
);
await tester.pumpWidget(ref);

// ...
ref.notifier(myNotifier).increment();
expect(ref.read(myNotifier), 2);
});
}
copied to clipboard
➤ State events #
Use RefenaHistoryObserver to keep track of every state change.
void main() {
testWidgets('My test', (tester) async {
final observer = RefenaHistoryObserver();
await tester.pumpWidget(
RefenaScope(
observers: [observer],
child: const MyApp(),
),
);

// ...
expect(observer.history, [
ProviderInitEvent(
provider: myProvider,
notifier: myNotifier,
cause: ProviderInitCause.access,
value: 1,
),
ChangeEvent(
notifier: myNotifier,
action: null,
prev: 1,
next: 2,
rebuild: [WidgetRebuildable<MyLoginPage>()],
),
]);
});
}
copied to clipboard
To test Redux, you can access RefenaHistoryObserver.dispatchedActions:
final observer = RefenaHistoryObserver.only(
actionDispatched: true,
);

// ...

expect(observer.dispatchedActions.length, 2);
expect(observer.dispatchedActions, [
isA<IncrementAction>(),
isA<DecrementAction>(),
]);
copied to clipboard
➤ Example test #
There is an example test that shows how to test a counter app.
See the example test.
Add-ons #
Add-ons are features implemented on top of Refena,
so you don't have to write the boilerplate code yourself.
The add-ons are entirely optional, of course.
To get started, add the following import:
import 'package:refena_flutter/addons.dart';
copied to clipboard
The core library never imports the add-ons, so we don't need to publish an additional package
as the add-ons are tree-shaken away if you don't use them.
➤ Snackbars #
Show snackbar messages.
First, set up the snackBarProvider:
MaterialApp(
scaffoldMessengerKey: ref.watch(snackBarProvider).key,
home: MyPage(),
)
copied to clipboard
Then show a message:
class MyNotifier extends Notifier<int> {
@override
int init() => 10;

void increment() {
state++;
ref.read(snackbarProvider).showMessage('Incremented');
}
}
copied to clipboard
Optionally, you can also dispatch a ShowSnackBarAction:
ref.global.dispatch(ShowSnackBarAction(message: 'Hello World from Action!'));
copied to clipboard
➤ Navigation #
Manage your navigation stack.
First, set up the navigationProvider:
MaterialApp(
navigatorKey: ref.watch(navigationProvider).key,
home: MyPage(),
)
copied to clipboard
Then navigate:
class MyNotifier extends Notifier<int> {
@override
int init() => 10;

void someMethod() async {
state++;
ref.read(navigationProvider).push(MyPage());

// or wait for the result
final result = await ref.read(navigationProvider).push<DateTime>(DatePickerPage());
}
}
copied to clipboard
Optionally, you can also dispatch a NavigateAction:
ref.global.dispatchAsync(NavigateAction.push(SecondPage()));

// or wait for the result
final result = await ref.global.dispatchAsync<DateTime?>(
NavigateAction.push(DatePickerPage()),
);
copied to clipboard
➤ Actions #
The add-on actions are all implemented as GlobalAction.
So you can dispatch them from anywhere.
ref.global.dispatch(ShowSnackBarAction(message: 'Hello World from Action!'));
copied to clipboard
Please read the full documentation about global actions here.
You can easily customize the given add-ons by extending their respective "Base-" classes.
class CustomizedNavigationAction<T> extends BaseNavigationPushAction<T> {
@override
Future<T?> navigate() async {
// get the key
GlobalKey<NavigatorState> key = ref.read(navigationProvider).key;

// navigate
T? result = await key.currentState!.push<T>(
MaterialPageRoute(
builder: (_) => _SecondPage(),
),
);

return result;
}
}
copied to clipboard
Then, you can use your customized action:
class MyAction extends ReduxAction<Counter, int> with GlobalActions {
@override
int reduce() => state + 1;

@override
void after() {
global.dispatchAsync(CustomizedNavigationAction());
}
}
copied to clipboard
Dart only #
You can use Refena without Flutter.
# pubspec.yaml
dependencies:
refena: <version>
copied to clipboard
void main() {
final ref = RefenaContainer();
ref.read(myProvider);
ref.notifier(myNotifier).doSomething();
ref.stream(myProvider).listen((value) {
print('The value changed from ${value.prev} to ${value.next}');
});
}
copied to clipboard
Deep dives #
This README is a high-level overview of Refena.
To learn more about each topic, checkout the topics.
License #
MIT License
Copyright (c) 2023-2024 Tien Do Nam
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Files:

Customer Reviews

There are no reviews.