provider_kit 0.0.1
provider_kit: ^0.0.1 copied to clipboard
A tool kit for provider package with predefined builders, listeners, caches, observers and much more with reduced boilerplate.
provider_kit is a state management toolkit for Flutter, built to work seamlessly with the provider
package. It simplifies state handling with predefined widgets, reduces boilerplate, and efficiently manages loading, error, and data states. With built-in async support, state observers, caching, and enhanced notifiers.
Features #
π― Feature | π Description |
---|---|
πReduces Boilerplate | Simplifies state management by minimizing repetitive code. |
π Handles Multiple States | Provides a centralized way to manage loading, error, initial, empty, and data states while supplying predefined widgets for these states to be used within builders. |
ποΈ Builders & Listeners | Enhanced widgets that integrate automatically with state changes and allow customization. |
π Combined Provider States | Supports managing multiple provider states together. |
πΎ State Caching | Provides mixins to store and restore state efficiently. |
π οΈ Provider Observation | Monitors provider lifecycle events for better debugging. |
π§© Immutable Objects | Ensures predictable state management through immutability. |
β‘ Error & Loading Handling | Automatically manages loading and error states with built-in support. |
π¦ Enhances Provider | Extends the functionality of the provider package. |
π TypeDefs Convention | Type definitions use the providerβs name as a prefix for widgets and states, simplifying usage and improving readability. |
Before | After |
---|---|
Index #
- Getting started
- State
- View State
- Nested State Listener
- State Observer
- Templates
- Best Practices for Managing Additional State in provider kit
Getting started #
Add them to your pubspec.yaml
file
dependencies:
provider_kit: ^0.0.1
provider: ^6.1.2 # Replace with the latest version
Make sure to register your provider to gain full advantage of this package.
ChangeNotifierProvider(
create: (_) => MyProvider(),
child: ...
)
For more information and details about registering your provider, see the documentation of provider package.
Alright, now lets dive in !
State #
State management is simplified using StateNotifier
and various widgets designed for listening, building, and consuming state changes efficiently.
StateNotifier #
StateNotifier
acts as the core class of this library, similar to ValueNotifier
but with enhanced capabilities. By extending StateNotifier
, our providers become observable, allowing widgets to listen and react to state changes.
class MyProvider extends StateNotifier<int> {
CounterProvider() : super(0);
void increment() => state++;
void decrement() => state--;
}
State Widgets #
To listen to state changes from our provider, we would use built-in widgets that are designed to interact with the StateNotifier
. Each widget includes an optional provider attribute. By default, state widgets automatically search the widget tree for the corresponding provider type (e.g., MyProvider
). Alternatively, we can pass a specific provider instance using the provider attribute.
- State Widgets includes
StateListener
,StateBuilder
,StateConsumer
.
StateListener #
A widget that listens for state changes and executes side effects without rebuilding the UI.
StateListener<MyProvider, MyDataType>(
provider: provider, // Optional
listenWhen: (previous, current) => previous != current, // Default, optional
shouldCallListenerOnInit: false, // Default, optional
listener: (context, state) {
// Can execute side effects here
},
child: YourWidget(),
);
StateBuilder #
A widget in which the builder will be triggered on state change.
StateBuilder<MyProvider, MyDataType>(
provider: provider, // Optional
rebuildWhen: (previous, current) => previous != current, // Default, optional
builder: (context, state, child) {
return Text('Count: $state');
},
child: YourStaticWidget(), // Optional, won't be rebuilt
);
StateConsumer #
A widget that combines the features of both StateListener
and StateBuilder
.
StateConsumer<MyProvider, MyDataType>(
provider: provider,
listenWhen: (previous, current) => previous != current, // Default, optional
shouldCallListenerOnInit: false, // Default, optional
listener: (context, state) {
// Can execute side effects here
},
rebuildWhen: (previous, current) => previous != current, // Default, optional
builder: (context, state, child) {
return Text('Count: $state');
},
child: YourStaticWidget(), // Optional, won't be rebuilt
);
Multi State Widgets #
With Multi State Widgets, we can listen to multiple providers states with a single widget. However, these widgets won't try to read the provider.
Note: Our providers states can either be of the same type or dynamic.
- Multi State Widgets includes
MultiStateListener
,MultiStateBuilder
andMultiStateConsumer
.
MultiStateListener #
A widget that listens to the state of multiple providers, and a state change in any of the providers will trigger the listener callback.
MultiStateListener<MyDataType>(
providers: [provider1, provider2, provider3],
listenWhen: (previous, current) => previous != current, // Default, optional
shouldCallListenerOnInit: false, // Default, optional
listener: (context, states) {
// Can execute side effects here
},
child: YourWidget(),
);
MultiStateBuilder #
A widget that listens to the state of multiple providers, and a state change in any of the providers will trigger the builder.
MultiStateBuilder<MyDataType>(
providers: [provider1, provider2, provider3],
rebuildWhen: (previous, current) => previous != current, // Default, optional
builder: (context, states, child) => Text(states.toString()),
child: YourStaticWidget(), // Optional, won't be rebuilt
);
MultiStateConsumer #
A widget that combines both the features of MultiStateListener
and MultiStateBuilder
.
MultiStateConsumer<MyDataType>(
providers: [provider1, provider2, provider3],
listenWhen: (previous, current) => previous != current, // Default, optional
shouldCallListenerOnInit: false, // Default, optional
listener: (context, states) {
// Can execute side effects here
},
rebuildWhen: (previous, current) => previous != current, // Default, optional
builder: (context, states, child) {
return Text('Count: $states');
},
child: YourStaticWidget(), // Optional, won't be rebuilt
);
ViewState #
ViewState
is a sealed class representing different states of a view. It supports various states such as Initial, Loading, Data, Empty, and Error. Each state has specific properties and behaviors.
A Typical use case for ViewState
is when fetching data asynchronously. For example, it can be from a server or local storage. It can also be used in operation-based scenarios like authentication features.
State | Description | Properties |
---|---|---|
InitialState |
Represents the initial state of a view. | None |
LoadingState |
Represents a loading state with optional progress and message. | message: String? , progress: double? |
DataState |
Represents a successful data state containing the result object. | dataObject: T |
EmptyState |
Represents an empty state with an optional message. | message: String? |
ErrorState |
Represents an error state with an optional message and retry callback. | message: String? , onRetry: VoidCallback? , exception: dynamic , stackTrace: StackTrace? |
Important Note:
EmptyState
will be used only forIterable
data types. For Example when your T is aList
,Set
etc.
ViewStateNotifier #
ViewStateNotifier
is a StateNotifier
that manages ViewState<T>
. It simplifies state management by handling various states such as loading, empty, data, and error for a given data type.
By default the intial state of
ViewStateNotifier
is LoadingState.
class MyViewStateProvider extends ViewStateNotifier<List<Item>> {
final Repository _repo = Repository();
MyViewStateProvider() : super(const InitialState()) {
init();
}
Future<void> init() async {
try {
state = const LoadingState();
final List<Item> items = await _repo.getItems(10);
if (items.isEmpty) {
state = const EmptyState();
return;
}
state = DataState(items);
} catch (e, s) {
state = ErrorState(e.toString(), e, s, onRefresh);
}
}
void onRefresh() {
state = const LoadingState();
init();
}
}
Tired of manually implementing the same logic for every provider?
No worries! Introducing ProviderKitβa more efficient way to manage our view state.
ProviderKit #
ProviderKit
automates state management, eliminating the need to repeatedly extend ViewStateNotifier
and implement the same boilerplate logic. It streamlines fetching, handling empty states, error management, and retry mechanisms.
By default the intial state of
ProviderKit
is LoadingState.
How does it work? #
Instead of writing the entire MyViewStateProvider
which we seen above, we can simply extend ProviderKit
like this:
class MyViewStateProvider extends ProviderKit<List<Item>> {
@override
FutureOr<List<Item>> fetchData() => Repository().getItems(10);
}
That's it! π
What does ProviderKit
handle for us? #
β
Automatically fetches data upon initialization.
β
Transitions to LoadingState
before fetching.
β
If the data is Iterable
and if its empty, it switches to EmptyState
.
β
Catches exceptions and converts them into ErrorState
.
β
Includes a built-in onRefresh
function, which rebuilds the initialization logic.
β
Passes the onRefresh
function, exception, and stack trace to ErrorState
.
With ProviderKit
, state management becomes cleaner, more efficient, and hassle-free.
Attributes | Type | Description |
---|---|---|
Constructor Params | ||
initialState |
ViewState<T> |
The initial state of the provider. Defaults to LoadingState . |
disableEmptystate |
bool |
By default, if T is an Iterable (like List , Set , etc.), an empty iterable will result in EmptyState . Setting this to true forces an empty iterable to be assigned as DataState . |
Property | ||
state |
ViewState<T> |
The current state of the provider, which can be LoadingState , DataState , EmptyState , or ErrorState . |
Methods | ||
init() |
FutureOr<void> |
Runs on initialization, setting up states and Guared with Try catch. It won't execute again if already initialized unless refresh is called. |
fetchData() |
FutureOr<T> |
Fetches data from an API or database. Must be implemented in subclasses. |
errorStateObject() |
ErrorState<T> |
Helps to customize default ErrorState Object |
loadingStateObject() |
LoadingState<T> |
Helps to customize default LoadingState Object |
emptyStateObject() |
EmptyState<T> |
Helps to customize default EmptyStaet Object instance. |
refresh() |
Future<void> |
Refreshes the provider which will call init with fetchData() again. |
Lets customize our MyViewStateProvider
to the fullest.
class MyViewStateProvider extends ProviderKit<List<Item>> {
// by default `initialState` is `LoadingState`.
// by default `disableEmptystate` is false.
MyViewStateProvider()
: super(initialState: const InitialState(),
//disabling empty state will set the state to `DataState` instead of `EmptyState`
disableEmptystate: true);
@override
FutureOr<void> init() async {
// `init` is internally guarded
// Custom initialization logic goes here
state = const LoadingState();
List<Item> items = await fetchData();
// Additional processing, such as filtering, can be done here
state = DataState(items);
}
@override
FutureOr<List<Item>> fetchData() async {
// Fetch data from an API or database
return [];
}
/// **Custom error state handling**
@override
ErrorState<List<Item>> errorStateObject(Object error, StackTrace stackTrace) {
String message = "Something went wrong";
// Custom error message handling
if (error is MyException) {
message = error.message;
}
return ErrorState<List<Item>>(message, error, stackTrace, refresh);
}
/// **Custom loading state**
@override
LoadingState<List<Item>> loadingStateObject() {
return const LoadingState<List<Item>>('Data is Loading...');
}
/// **Custom empty state**
@override
EmptyState<List<Item>> emptyStateObject() {
return const EmptyState<List<Item>>('No data available.');
}
/// **Optional refresh override**
@override
Future<void> refresh() async {
// Perform any additional refresh logic if needed
super.refresh();
}
}
Note: Even if
refresh
is not passed inside theErrorState
forretry
mechanism, therefresh
will be automatically be read by theView State Widgets
as long as the provider extendsProviderKit
.
ViewStateWidgetsProvider #
In a typical application, most screens fetch data from a server or local storage. On every view screen, we compare the state and display the appropriate widget based on that state. For example:
LoadingWidget
when the state is loadingErrorWidget
when the state is errorEmptyWidget
when the data list is emptyDataWidget
when the data is successfully fetched
Instead of checking the state type and passing the respective widgets for every single screen, we can reuse the same widgets across all screens. We can streamline this process by wrapping our MaterialApp
with ViewStateWidgetsProvider
and supplying custom widgets for each state.
Note: These widgets will be used internally by
ViewStateBuilder
,ViewStateConsumer
,MultiViewStateBuilder
andMultiViewStateConsumer
which weβll explore soon below.
Additionally, we can wrap a specific part of the widget tree with ViewStateWidgetsProvider
to override the state widgets for that section.
ViewStateWidgetsProvider
is simply an inherited widget that provides consistent state based widgets across our app.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ViewStateWidgetsProvider(
//supply your initial state widget
initialStateBuilder: (isSliver) {
const widget = Center(child: Text("Initial State"));
return isSliver ? const SliverToBoxAdapter(child: widget) : widget;
},
//supply your empty state widget
emptyStateBuilder: (message, isSliver) {
Widget widget = Center(child: Text(message ?? "No Data Available"));
return isSliver ? SliverToBoxAdapter(child: widget) : widget;
},
//supply your error state widget
//onRetry will refresh the provider
errorStateBuilder: (errorMessage, onRetry, exception, stackTrace, isSliver) {
final widget = Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(errorMessage ?? "An error occurred",
style: const TextStyle(color: Colors.red)),
TextButton(
onPressed: onRetry, child: const Text("Retry")),
],
),
);
return isSliver ? const SliverToBoxAdapter(child: widget) : widget;
},
//supply your loading state widget
loadingStateBuilder: (message, progress, isSliver) {
const widget = Center(child: CircularProgressIndicator());
return isSliver ? const SliverToBoxAdapter(child: widget) : widget;
},
child: const MaterialApp(
//..
),
);
}
}
Note: In
errorStateBuilder
, theerrorMessage
,onRetry
,exception
, andstackTrace
are automatically passed to the function if your provider isproviderKit
.
View State Widgets #
These widgets are similar to State Widgets but are designed to adapt based on the corresponding ViewState
. They listen to a provider that extends either ViewStateNotifier
or ProviderKit
, ensuring they respond dynamically to state changes.For example MyViewStateProvider
which we learned above.
- View State Widgets includes
ViewStateListener
,ViewStateBuilder
,ViewStateConsumer
.
ViewStateListener #
This widget provides individual listener
callbacks for each ViewState
, allowing customized behavior based on the current state.
ViewStateListener<MyViewStateProvider, MyDataType(
dataStateListener: (data) => context.showToast(data.toString()),
child: YourWidget(),
)
Attribute Name | Type | Required/Optional | Description |
---|---|---|---|
provider |
P? |
Optional | Automatically searches the widget tree for the corresponding provider type (e.g., MyViewStateProvider ) if not provided. |
initialStateListener |
void Function()? |
Optional | Invoked when the state is InitialState . |
loadingStateListener |
void Function(String? message, double? progress)? |
Optional | Invoked when the state is LoadingState . |
dataStateListener |
void Function(T data)? |
Required | Invoked when the state is DataState . |
emptyStateListener |
void Function(String? message)? |
Optional | Invoked when the state is EmptyState . |
errorStateListener |
void Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace)? |
Optional | Invoked when the state is ErrorState . |
listenWhen |
bool Function(ViewState<T> previous, ViewState<T> next)? |
Optional | Determines whether to listen for state changes based on previous and next state comparisons. |
shouldCallListenerOnInit |
bool |
Optional | Determines whether the state listener should be called immediately upon initialization. Defaults to false . |
child |
Widget? |
Required | The child widget wrapped by ViewStateListener . |
Each callback is triggered based on the current ViewState
, allowing dynamic response handling within ViewStateListener
.
ViewStateBuilder #
This widget provides individual builder
for each ViewState
, allowing customized behavior based on the current state.
Important Note:
initialStateBuilder
,loadingStateBuilder
,emptyStateBuilder
anderrorStateBuilder
that we supplied toViewStateWidgetsProvider
will be used by this widget internally by default.
ViewStateBuilder<MyViewStateProvider, MyDataType>(
// Other ViewState builders will be assigned from the `ViewStateWidgetsProvider`.
// We can override them here in `ViewStateBuilder` if needed.
// loadingBuilder: (message, progress, isSliver) => ,
dataBuilder: (data) => Text(data.toString()),
)
The ViewStateBuilder
allows customization of UI rendering for different ViewState
s, enabling dynamic UI updates based on the current state.
Attribute Name | Type | Required/Optional | Description |
---|---|---|---|
provider |
P? |
Optional | Automatically searches the widget tree for the corresponding provider type if not provided. |
rebuildWhen |
bool Function(ViewState<T> previous, ViewState<T> next)? |
Optional | Determines if the builder should rebuild based on state changes. |
initialBuilder |
Widget Function(bool isSliver)? |
Optional | Called when the state is InitialState . |
dataBuilder |
Widget Function(T data) |
Required | Called when the state is DataState , passing the retrieved data. |
errorBuilder |
Widget Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace, bool isSliver)? |
Optional | Called when the state is ErrorState . |
loadingBuilder |
Widget Function(String? message, double? progress, bool isSliver)? |
Optional | Called when the state is LoadingState . |
emptyBuilder |
Widget Function(String? message, bool isSliver)? |
Optional | Called when the state is EmptyState . |
isSliver |
bool |
Optional | Specifies whether the widget is a sliver. Defaults to false . |
child |
Widget? |
Optional | A static child widget that does not depend on the state. |
ViewStateConsumer
#
This widget combines features of both ViewStateListener
and ViewStateBuilder
. We can use this widget when we need both listeners and builders functionality.
Important Note:
initialStateBuilder
,loadingStateBuilder
,emptyStateBuilder
anderrorStateBuilder
that we supplied toViewStateWidgetsProvider
will be used by this widget internally by default.
ViewStateConsumer<MyViewStateProvider, MyDataType(
dataStateListener: (data) {
print(data);
},
dataBuilder: (data) => Text(data.toString()),
)
Attribute Name | Type | Required/Optional | Description |
---|---|---|---|
provider |
P? |
Optional | Automatically searches the widget tree for the corresponding provider type. |
initialStateListener |
void Function()? |
Optional | Invoked when the state is InitialState . |
loadingStateListener |
void Function(String? message, double? progress)? |
Optional | Invoked when the state is LoadingState . |
dataStateListener |
void Function(T data)? |
Optional | Invoked when the state is DataState . |
emptyStateListener |
void Function(String? message)? |
Optional | Invoked when the state is EmptyState . |
errorStateListener |
void Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace)? |
Optional | Invoked when the state is ErrorState . |
listenWhen |
bool Function(ViewState<T> previous, ViewState<T> next)? |
Optional | Determines whether to listen for state changes based on previous and next state comparisons. |
rebuildWhen |
bool Function(ViewState<T> previous, ViewState<T> next)? |
Optional | Determines if the builder should rebuild based on state changes. |
initialBuilder |
Widget Function(bool isSliver)? |
Optional | Called when the state is InitialState . |
loadingBuilder |
Widget Function(String? message, double? progress, bool isSliver)? |
Optional | Called when the state is LoadingState . |
emptyBuilder |
Widget Function(String? message, bool isSliver)? |
Optional | Called when the state is EmptyState . |
dataBuilder |
Widget Function(T data) |
Required | Called when the state is DataState , passing the retrieved data. |
errorBuilder |
Widget Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace, bool isSliver)? |
Optional | Called when the state is ErrorState . |
isSliver |
bool |
Optional | Specifies whether the widget is a sliver. Defaults to false . |
Multi View State Widgets #
Multi View State Widgets allow us to listen to multiple providers' ViewState
s with a single widget. However, these widgets do not read the provider.
Note: Our providers states can either be of the same types or dynamic.
Key Difference: Unlike
ViewStateListener
,ViewStateBuilder
, andViewStateConsumer
, Multi View State Widgets require a list of providers as a mandatory attribute.
- Multi View State Widgets inlcudes
MultiViewStateListener
,MultiViewStateBuilder
andMultiViewStateConsumer
.
How Multi View State Widgets Work #
The behavior of MultiViewStateBuilder
, MultiViewStateListener
, and MultiViewStateConsumer
depends on the collective states of the provided ViewState
s. The highest-priority state in the list determines which builder or listener is triggered.
Priority Order of States #
1οΈβ£ ErrorState
(Highest Priority)
- If any provider is in
ErrorState
, theerrorStateListener
(orerrorBuilder
) will be invoked. -
The first encountered
ErrorState
data will be passed to theerrorStatelistener
orerrorBuilder
.
2οΈβ£ InitialState
- If no
ErrorState
is found, but at least one provider is inInitialState
, theinitialStateListener
(orinitialBuilder
) will be invoked.
3οΈβ£ LoadingState
- If no
ErrorState
orInitialState
exists, but at least one provider is inLoadingState
, theloadingStateListener
(orloadingBuilder
) will be invoked. -
First encountered
LoadingState
message will be passed to theloadingStatelistener
orloadingBuilder
. -
progress
will be aggregated from allLoadingState
s into a single combined value.
4οΈβ£ EmptyState
- If none of the above states are present, but at least one provider is in
EmptyState
, theemptyStateListener
(oremptyBuilder
) will be invoked. -
The first encountered
EmptyState
message will be passed to theemptyStatelistener
oremptybuilder
.
5οΈβ£ DataState<DataType>
(Lowest Priority)
- Only If all providers are in
DataState
, thedataStateListener
(ordataBuilder
) will be invoked.
Additional Notes #
- First encountered state applies to all states except
DataState
. LoadingState
progress is aggregated from all activeLoadingState
s into a single combined value.- Modifying
listenWhen
orrebuildWhen
overrides the default priority logic which will results in triggeringlistener
orbuilder
whenever any provider's state changes.
Handling EmptyState
in MultiViewState Widgets #
If some providers have data while others return empty, triggering
EmptyState
may not be ideal.
Solution: Avoid using EmptyState
in the provider logic. Instead, handle empty cases manually inside dataBuilder
.
This ensures EmptyState
wonβt be triggered unless all providers return an empty state.
MultiViewStateListener #
The MultiViewStateListener
allows listening to multiple ViewState
providers simultaneously. It merges their states into a unified ViewState
, enabling centralized state management without manually handling multiple providers.
Check How Multi View State Widgets Work for more detailed information about how which state is triggered
MultiViewStateListener<MyDataType>(
providers: [viewStateProviderOne, viewStateProviderTwo, viewStateProviderThree],
dataStateListener: (dataStates) {
print(dataStates);
},
child: YourChild(),
);
Attribute Name | Type | Required/Optional | Description |
---|---|---|---|
providers |
List<ViewStateNotifier<T>> |
Required | A list of ViewStateNotifier providers that the listener will observe. |
initialStateListener |
void Function()? |
Optional | Callback triggered when the state transitions to InitialState . |
loadingStateListener |
void Function(String? message, double? progress)? |
Optional | Invoked when the state is LoadingState . Receives a message and an aggregated progress value (if multiple providers are loading). |
emptyStateListener |
void Function(String? message)? |
Optional | Triggered when the state is EmptyState . Uses the first encountered EmptyState 's empty message. |
errorStateListener |
void Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace)? |
Optional | Called when the state transitions to ErrorState , passing the first encountered error details. |
dataStateListener |
void Function(List<DataState<T>> dataStates)? |
Optional | Called when all providers transition to DataState , providing the combined list of data states. |
listenWhen |
bool Function(List<ViewState<T>> previous, List<ViewState<T>> next) |
Optional | Modifying this overrides the default priority logic, triggering listener whenever any provider's state changes |
shouldCallListenerOnInit |
bool |
Optional | Determines whether the listener should be triggered immediately when the widget initializes. Defaults to false . |
child |
Widget? |
Required | The wrapped widget that remains within the listener, receiving state updates. |
MultiViewStateBuilder #
The MultiViewStateBuilder
enables building UI based on multiple ViewState
providers simultaneously. It merges their states into a unified ViewState
.
Important Note:
initialStateBuilder
,loadingStateBuilder
,emptyStateBuilder
anderrorStateBuilder
that we supplied toViewStateWidgetsProvider
will be used by this widget internally by default.
MultiViewStateBuilder<MyDataType>(
providers: [viewStateProviderOne, viewStateProviderTwo, viewStateProviderThree],
dataBuilder: (dataStates) {
return YourWidget(dataStates);
},
);
Attribute Name | Type | Required/Optional | Description |
---|---|---|---|
providers |
List<ViewStateNotifier<T>> |
Required | A list of ViewStateNotifier providers that the builder will observe. |
initialBuilder |
Widget Function(bool isSliver)? |
Optional | Builder triggered when the state transitions to InitialState . |
loadingBuilder |
Widget Function(String? message, double? progress, bool isSliver)? |
Optional | Triggered when the state is LoadingState . Receives a message and an aggregated progress value (if multiple providers are loading). |
emptyBuilder |
Widget Function(String? message, bool isSliver)? |
Optional | Triggered when the state is EmptyState . Uses the first encountered EmptyState 's empty message. |
errorBuilder |
Widget Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace, bool isSliver)? |
Optional | Triggered when the state transitions to ErrorState , passing the first encountered error details. |
dataBuilder |
Widget Function(List<DataState<T>> dataStates)? |
Required | Triggered when all providers transition to DataState , providing the combined list of data states. |
rebuildWhen |
bool Function(List<ViewState<T>> previous, List<ViewState<T>> next)? |
Optional | Modifying this overrides the default priority logic, triggering builder whenever any provider's state changes. |
isSliver |
bool? |
Optional | Determines whether the widget should be a Sliver or a regular widget. Defaults to false . |
MultiViewStateConsumer #
Combines the features of MultiViewStateListener
and MultiViewStateBuilder
in a single widget.
Important Note:
initialStateBuilder
,loadingStateBuilder
,emptyStateBuilder
anderrorStateBuilder
that we supplied toViewStateWidgetsProvider
will be used by this widget internally by default.
MultiViewStateConsumer<MyDataType>(
providers: [viewStateProviderOne, viewStateProviderTwo, viewStateProviderThree],
dataStateListener: (dataStates) {
print(dataStates);
},
dataBuilder: (dataStates) {
return YourWidget(dataStates);
},
);
Attribute Name | Type | Required/Optional | Description |
---|---|---|---|
providers |
List<ViewStateNotifier<T>> |
Required | A list of ViewStateNotifier providers that the consumer will observe. |
initialStateListener |
void Function()? |
Optional | Callback triggered when the state transitions to InitialState . |
loadingStateListener |
void Function(String? message, double? progress)? |
Optional | Invoked when the state is LoadingState . Receives a message and an aggregated progress value (if multiple providers are loading). |
emptyStateListener |
void Function(String? message)? |
Optional | Triggered when the state is EmptyState . Uses the first encountered EmptyState 's empty message. |
errorStateListener |
void Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace)? |
Optional | Called when the state transitions to ErrorState , passing the first encountered error details. |
dataStateListener |
void Function(List<DataState<T>> dataStates)? |
Optional | Called when all providers transition to DataState , providing the combined list of data states. |
listenWhen |
bool Function(List<ViewState<T>> previous, List<ViewState<T>> next) |
Optional | Modifying this overrides the default priority logic, triggering listener whenever any provider's state changes. |
shouldCallListenerOnInit |
bool |
Optional | Determines whether the listener should be triggered immediately when the widget initializes. Defaults to false . |
initialBuilder |
Widget Function(bool isSliver)? |
Optional | Builder triggered when the state transitions to InitialState . |
loadingBuilder |
Widget Function(String? message, double? progress, bool isSliver)? |
Optional | Triggered when the state is LoadingState . Receives a message and an aggregated progress value (if multiple providers are loading). |
emptyBuilder |
Widget Function(String? message, bool isSliver)? |
Optional | Triggered when the state is EmptyState . Uses the first encountered EmptyState 's empty message. |
errorBuilder |
Widget Function(String? message, VoidCallback? onRetry, dynamic exception, StackTrace? stackTrace, bool isSliver)? |
Optional | Triggered when the state transitions to ErrorState , passing the first encountered error details. |
dataBuilder |
Widget Function(List<DataState<T>> dataStates)? |
Required | Triggered when all providers transition to DataState , providing the combined list of data states. |
rebuildWhen |
bool Function(List<ViewState<T>> previous, List<ViewState<T>> next)? |
Optional | Modifying this overrides the default priority logic, triggering builder whenever any provider's state changes. |
isSliver |
bool? |
Optional | Determines whether the widget should be a Sliver or a regular widget. Defaults to false . |
Cache Mixins #
Some mixins to help with ViewState
caching and data caching that will come handy.
ExViewStateCacheMixin #
This mixin can be used on provider with ViewState
support like ViewStateNotifier
or ProviderKit
. It provides caching capabilities for different view states. It keeps track of the most recent state of each type and allows easy retrieval of cached states.
Features
- Stores the last known state for each
ViewState
type. - Allows accessing cached states via getter methods.
- Clears cached states when disposed to free up memory.
class MyViewStateProvider extends ViewStateNotifier<MyDataType> with ExViewStateCacheMixin {
// Your implementation here
}
Name | Type | Description |
---|---|---|
exInitialState |
InitialState<T>? |
Stores the last InitialState . |
exLoadingState |
LoadingState<T>? |
Stores the last LoadingState . |
exEmptyState |
EmptyState<T>? |
Stores the last EmptyState . |
exErrorState |
ErrorState<T>? |
Stores the last ErrorState . |
exDataState |
DataState<T>? |
Stores the last DataState . |
exDataStateObject |
T? |
Stores the last known data object from DataState . |
clearCache() |
void |
Clears all cached states. |
DataStateCopyCacheMixin #
This mixin can be used on provider with ViewState
support like ViewStateNotifier
or ProviderKit
. We can use this mixin to cache original data.
sometimes we do local filtering on data we fetched from server and when user cancel filter we need to show the original data back which is exactly when we should use this mixin.
Features:
- Stores the latest
DataState<T>
and data whensaveDataStateCopy
is called. - Provides access to the cached
DataState<T>
and its data object. - Allows clearing cached state manually using
clearDataStateCopy
.
class MyViewStateProvider extends ProviderKit<List<String>> with DataStateCopyCacheMixin {
void updateDataState(List<String> newData) {
final newState = DataState(newData);
saveDataStateCopy(newState);
state = newState;
}
void clearFilter(){
state = dataStateCopy!;
}
}
Name | Type | Description |
---|---|---|
dataStateCopy |
DataState<T>? |
gets the copy of the saved DataState<T> . |
dataObjectCopy |
T? |
gets the copy of the saved data object from DataState<T> . |
saveDataStateCopy |
(ViewState<T>? newDataState) |
Stores the given DataState<T> and its associated data. |
clearDataStateCopy |
void |
Clears the stored DataState<T> and its associated data. |
NestedStateListener #
NestedStateListener
is a widget that nests multiple state listeners within a single widget. It allows you to combine different types of listeners and manage them together efficiently.
- Supports nesting multiple state listeners.
- Works seamlessly with
StateListener
,ViewStateListener
,MultiStateListener
, andMultiViewStateListener
. - Reduces boilerplate code by combining multiple listeners into a single widget.
NestedStateListener(
listeners: [
StateListener<MyProvider,DataType>(
listener: (context, state) {
// Handle state changes
},
),
MultiStateListener<DataType>(
providers: [ProviderOne(),ProviderTwo()],
listener: (context, states) {
// Handle state changes
},
),
ViewStateListener<MyProvider,DataType(
dataStateListener: (data) {
// Handle view state changes
},
),
MultiViewStateListener<DataType>(
providers: [ProviderOne(),ProviderTwo()],
dataStateListener: (states) {
// Handle state changes
},
),
],
child: MyChildWidget(),
);
Attribute | Type | Description |
---|---|---|
listeners (Required) |
List<SingleChildWidget> |
A list of listeners to be applied. These can include StateListener , ViewStateListener , MultiStateListener , and MultiViewStateListener . |
child (Required) |
Widget |
The child widget that will be wrapped by the listeners. |
Note: Ensure that the
listeners
list contains at least one listener to avoid an empty nesting.
StateObserver #
The StateObserver
helps in monitoring provider activites. It can be used for debugging, for example - by logging lifecycle events such as creation, state changes, errors, and disposal.
void main() {
// Set the state observer
StateNotifier.observer = NotifierLogger();
runApp(const MyApp());
}
class MyStateObserver extends StateObserver {
@override
void onChange(StateNotifierBase stateNotifier, Change change) {
super.onChange(stateNotifier, change);
debugPrint(
'StateNotifier onChange -- \${stateNotifier.runtimeType}, '
'\${change.currentState.runtimeType} ---> \${change.nextState.runtimeType}',
);
}
@override
void onCreate(StateNotifierBase stateNotifier) {
super.onCreate(stateNotifier);
debugPrint('StateNotifier onCreate -- \${stateNotifier.runtimeType}');
}
@override
void onError(
StateNotifierBase stateNotifier, Object error, StackTrace stackTrace) {
debugPrint(
'StateNotifier onError -- \${stateNotifier.runtimeType} '
'Error: \$error StackTrace: \$stackTrace',
);
super.onError(stateNotifier, error, stackTrace);
}
@override
void onDispose(StateNotifierBase stateNotifier) {
super.onDispose(stateNotifier);
debugPrint('StateNotifier onDispose -- \${stateNotifier.runtimeType}');
}
}
Templates #
This guide provides step-by-step instructions for setting up the ProviderKit Template in VS Code and Android Studio/IntelliJ on both Mac & Windows.
VS Code Template Setup #
πΉ Step 1: Open Snippets File #
- Open VS Code.
- Press
Cmd + Shift + P
(Mac) orCtrl + Shift + P
(Windows). - Type
"Snippets: Configure Snippets"
and select it. - Choose
dart.json
to open the Dart snippets file.
πΉ Step 2: Add ProviderKit Snippet #
-
Copy the following snippet:
{ "ProviderKit Template": { "prefix": "pkit", "description": "ProviderKit template for view state provider", "body": [ "import 'dart:async';", "", "import 'package:provider_kit/provider_kit.dart';", "", "class ${1:ProviderName}Provider extends ProviderKit<${2:DataType}> {", "", " @override", " FutureOr<${2:DataType}> fetchData() async {", " return ;", " }", "", "}", "", "typedef ${1:ProviderName}ViewState = ViewState<${2:DataType}>;", "", "typedef ${1:ProviderName}InitialState = InitialState<${2:DataType}>;", "typedef ${1:ProviderName}LoadingState = LoadingState<${2:DataType}>;", "typedef ${1:ProviderName}EmptyState = EmptyState<${2:DataType}>;", "typedef ${1:ProviderName}DataState = DataState<${2:DataType}>;", "typedef ${1:ProviderName}ErrorState = ErrorState<${2:DataType}>;", "", "typedef ${1:ProviderName}ViewStateBuilder = ViewStateBuilder<${1:ProviderName}Provider, ${2:DataType}>;", "typedef ${1:ProviderName}ViewStateListener = ViewStateListener<${1:ProviderName}Provider, ${2:DataType}>;", "typedef ${1:ProviderName}ViewStateConsumer = ViewStateConsumer<${1:ProviderName}Provider, ${2:DataType}>;" ] } }
copied to clipboard -
Paste it inside
dart.json
. -
Save the file (Cmd + S on Mac, Ctrl + S on Windows).
πΉ Step 3: Verify Snippet #
- Open any Dart file.
- Type
"pkit"
and pressTab
to insert the template.
Important Note for VS Code Users #
-
Pressing
Tab
should move the cursor to the next snippet placeholder (e.g., fromProviderName
toDataType
). -
If
Tab
does not move to the next placeholder, follow these troubleshooting steps:Troubleshooting Steps
-
Check your settings:
- Open VS Code Settings (
Ctrl + ,
orCmd + ,
). - Search for
"Tab Completion"
and set it toonlySnippets
.
- Open VS Code Settings (
-
Restart VS Code after changing the setting.
-
Disable conflicting extensions:
- If you have Tabnine VS Code extension, try disabling it.
- Some AI-powered extensions override
Tab
behavior.
-
Android Studio and IntelliJ Template Setup #
πΉ Step 1: Open Live Templates #
- Open IntelliJ IDEA or Android Studio.
- Go to Settings (
Ctrl + Alt + S
on Windows/Linux,Cmd + ,
on Mac). - Navigate to Editor β Live Templates.
πΉ Step 2: Create a New Template Group #
- Click on the + (Add) button.
- Select Template Group.
- Name it ProviderKit.
- Paste the below code after selecting the created group.
<template name="providerkit" value="import 'dart:async'; import 'package:provider_kit/provider_kit.dart'; class $NAME$Provider extends ProviderKit<$DATA_TYPE$> { @override FutureOr<$DATA_TYPE$> fetchData() async { return ; } } typedef $NAME$ViewState = ViewState<$DATA_TYPE$>; typedef $NAME$InitialState = InitialState<$DATA_TYPE$>; typedef $NAME$LoadingState = LoadingState<$DATA_TYPE$>; typedef $NAME$EmptyState = EmptyState<$DATA_TYPE$>; typedef $NAME$DataState = DataState<$DATA_TYPE$>; typedef $NAME$ErrorState = ErrorState<$DATA_TYPE$>; typedef $NAME$ViewStateBuilder = ViewStateBuilder<$NAME$Provider, $DATA_TYPE$>; typedef $NAME$ViewStateListener = ViewStateListener<$NAME$Provider, $DATA_TYPE$>; typedef $NAME$ViewStateConsumer = ViewStateConsumer<$NAME$Provider, $DATA_TYPE$>;" description="ProviderKit template for four-state provider" toReformat="false" toShortenFQNames="true">
<variable name="NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="DATA_TYPE" expression="" defaultValue="" alwaysStopAt="true" />
<context>
<option name="DART" value="true" />
<option name="FLUTTER" value="true" />
</context>
</template>
How to Use the Template #
- Open a Dart file.
- Type
pkit
and press Tab or Enter. - The template expands, allowing you to fill in
NAME
andDATA_TYPE
.
Taking full advantage of templates #
ProviderKit Template #
ProviderKit
provides a structured way to manage view states efficiently. Below is a template that allows you to create a FeedProvider
using ProviderKit
.
import 'dart:async';
import 'package:provider_kit/provider_kit.dart';
class FeedProvider extends ProviderKit<List<Item>> {
@override
FutureOr<List<Item>> fetchData() async {
return []; // Fetch data from an API or database
}
}
typedef FeedViewState = ViewState<List<Item>>;
typedef FeedInitialState = InitialState<List<Item>>;
typedef FeedLoadingState = LoadingState<List<Item>>;
typedef FeedEmptyState = EmptyState<List<Item>>;
typedef FeedDataState = DataState<List<Item>>;
typedef FeedErrorState = ErrorState<List<Item>>;
typedef FeedViewStateBuilder = ViewStateBuilder<FeedProvider, List<Item>>;
typedef FeedViewStateListener = ViewStateListener<FeedProvider, List<Item>>;
typedef FeedViewStateConsumer = ViewStateConsumer<FeedProvider, List<Item>>;
Note: Above
FeedProvider
generated by the template can be used for any View State Widgets and Multi View State Widgets.
Advantages of Typedefs #
Using typedefs in ProviderKit
offers several advantages:
1. Improved Readability #
Instead of writing long generic types, typedefs make it easier to understand what each state represents:
FeedViewState viewState;
compared to:
ViewState<List<Item>> viewState;
2. Consistency in Naming #
By adding Feed
in front of state widgets, it's easier to identify which provider they belong to. Example:
FeedViewStateBuilder(
builder: (context, state) {
// Handle state changes
},
)
instead of:
ViewStateBuilder<FeedProvider, List<Item>>(
builder: (context, state) {
// Handle state changes
},
)
3. Reduced Boilerplate #
Instead of specifying the provider and data type every time, typedefs allow you to use shorter, meaningful names.
4. Type Safety #
By defining typedefs, you ensure that the correct data types are used throughout the app, preventing common mistakes.
Best Practices for Managing Additional State in provider kit #
Since provider_kit
is built with ChangeNotifier
as its base, there are multiple ways to use the provider.
1. Managing State in Providers #
Handling Additional State in provider_kit
#
When managing state in provider_kit
, you may need additional parameters like pagination, filters, or metadata alongside your primary data (e.g., List<Item>
). Here are three structured approaches to handle this efficiently:
1. Using Extra Variables in the Same Provider (β Not Recommended)
Since provider_kit
extends ChangeNotifier
, you can declare additional variables inside the provider and update them using notifyListeners()
. These variables can be listened to via Selector
in the UI.
class MyProvider extends ProviderKit<List<Item>> {
PaginationData? paginationData;
FilterData? filterData;
void updatePagination(PaginationData newData) {
paginationData = newData;
notifyListeners();
}
}
β Avoid this approach as it mixes multiple responsibilities within a single provider, making it harder to maintain and test.
2. Using Dart Records or a Custom Data Object (β Preferred)
A better approach is to store all related data inside DataState
, ensuring clear separation of concerns.
DataState((
pagination: PaginationData(),
filter: FilterData(),
items: List<Item>(),
));
β Advantages:
- Keeps all relevant data encapsulated in a single object.
- Improves maintainability and separation of concerns.
- Easier to test and manage.
3. Using Separate Providers with ProxyProvider
(β
Best Practice)
A scalable approach is to keep pagination and filter logic in separate providers and link them using ProxyProvider
.
class PaginationProvider extends ProviderKit<PaginationData> {}
class FilterProvider extends ProviderKit<FilterData> {}
ProxyProvider<PaginationProvider, MyProvider>(
update: (_, pagination, myProvider) =>
myProvider!..updatePagination(pagination.state),
)
β Advantages:
- Encourages modularity and reusability.
- Keeps providers focused on a single responsibility.
- Enhances performance by updating only necessary state.
Few features of this package were inspired from
flutter_bloc
andflutter_bloc_ease
.
π Features & Bug Reports #
Have a feature request or found a bug? Feel free to open an issue on the GitHub Issue Tracker. Your feedback helps improve ProviderKit!
π’ Connect with Me #
Stay updated and reach out for collaborations!
Website: Ram Prasanth