Clean Provider ð
A simple way to apply best practices such as (separation of concerns) with flutter and provider.
Why This Package? ðĪ
Talat's created this package for many reasons following some:
- Community likes provider ðĻâðŧ
- Provider doesn't try to do everything (forcing your hand and preventing you from using cool packages) ð
- For
Separation Of Concerns
side better, decoupling logic from the UI. ðŠ
Provider
with the help of ChangeNotifier
can do this, but it's not clear and not straightforward since there is no
prefixed pattern, they keep your free, but this can turn into cause.
This package takes the best features from Provider
, and adds its own touch.
Index ð
Installation âŽïļ
Add this to your pubspec.yaml file:
dependencies:
clean_provider: <^last>
Exciting ðĨ
The code below shows min part of the potential of this package.
This library comes with:
View
--> Extends theStatelessWidget
to the next level, offering you a clean approach to build your UI.Screen
--> The responsive version ofView
.ViewModel
--> The business logic or UI level state holder.Page
--> A normalStatelessWidget
returning yourView
orScreen
wrapped withMultiProvider
.
// Remember to set the View generic `Type` in order to accesses your custom properties,
// getters, setters, methods in your Widget. As below HomeViewModel is subclass of ViewModel.
// If you don't specify the View generic `Type` then you'll be able to accesses only predefined stuff. e.g a context getter.
class HomeView extends View<HomeViewModel> {
HomeView({super.key});
// Note: that you can assess the BuildContext globally in your widget.
@override
Widget? builder() {
return Scaffold(
appBar: AppBar(
title: Text(viewModel.title),
),
body: Center(
child: ElevatedButton(
onPressed: viewModel.navToNext, // Your custom method.
child: const Text('Go Next'),
),
),
);
}
}
class HomeViewModel extends ViewModel {
final String title = 'Clean Provider Example'; // can be fetched from an api, no matters you can send any thing to UI from here.
void navToNext() {
Navigator.push(
context, // You can accesses the BuildContext globally in your ViewModel.
MaterialPageRoute(
builder: (context) {
return const SecondPage();
},
),
);
}
}
// Remember what we have side?
// The type parameter on View is optional, so in this case you cannot access custom invocation,
// but still able to get the predefined ones like the global context.
class SecondPage extends View {
SecondPage({super.key});
@override
Widget builder() {
return const Scaffold(
body: Center(
child: Text('Go back'),
),
);
}
}
Getting Started ð
To work properly, you have to follow the ordered steps as follows:
- Create your
ViewModel
- Create your
View
- Create your
Page
ViewModel ð§
- Create
<your>ViewModel
- Define your
Listenable
s fields - Pass the
Listenable
s to thenotifiers
list to auto-update the UI - Define your custom logic.
class CounterViewModel extends ViewModel {
int get counter => _counter.value;
final ValueNotifier<int> _counter = 0.listen;
void increment() => _counter.value++;
void decrement() => _counter.value--;
@override
List<ChangeNotifier> get notifiers =>
<ChangeNotifier>[
_counter,
];
}
Life Cycle ðŠī
The ViewModel comes with life cycle methods such as onInit
which runs after the ViewModel
injection. There is also
the Famous onDispose
method which is needed to kill processes after ViewModel
disposed.
onInit ðą
class MyViewModel extends ViewModel {
@override
void onInit() {
super.onInit();
// TODO: add your implementation.
// initialize variables etc.
}
}
onDispose ðĨ
class MyViewModel extends ViewModel {
int get counter => _counter.value;
final ValueNotifier<int> _counter = 0.listen;
@override
void dispose() {
_counter.dispose();
super.dispose();
}
}
View ðą
A StatelessWidget
extended to the next level.
- Create the
<your>View
and pass the<your>ViewModel
to it as a generic type - Build your UI using the
bulder
method - Use the
context
passed globally - Access your
<your>ViewModel
members usingviewModel.<your_member>
class CounterView extends View<CounterViewModel> {
const CounterView({
required this.title,
super.key,
});
final String title;
@override
Widget builder() {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme
.of(context)
.colorScheme
.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${viewModel.counter}',
style: Theme
.of(context)
.textTheme
.headlineMedium,
),
const SizedBox(height: 100),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: viewModel.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('title', title));
}
}
Screen Info âđïļ
Holds Screen util information such as the width
or height
received from MediaQuery
.
class ExampleView extends View {
ExampleView({super.key});
@override
Widget builder() {
return Container(
width: info.width * .5, // same as MediaQuery.of(context).size.width * .5
height: info.height * .3, // MediaQuery.of(context).size.height * .3
);
}
}
Global BuildContext ð
Thanks to the ScreenInfo
we're able to access the BuildContext
globally (no need to pass context from method to
other like in case of StatelessWidget
).
class HomeView extends View {
// Note: that you can assess the BuildContext globally in your widget.
@override
Widget? builder() {
return Scaffold(
appBar: AppBar(
title: Text(viewModel.title),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context, // Accessing the BuildContext.
MaterialPageRoute(
builder: (context) {
return const SecondPage();
},
),
);
},
child: const Text('Go Next'),
),
),
);
}
}
Page âïļ
You can call it whatever you like, I recommend you to call it as Page
since you'll need it during navigation.
- Create the
<your>Page
as regularStatelessWidget
- Within the
build
method return theMultiProvider
- Pass your
<your>View
as child - Pass your providers to the
MultiProvider
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<CounterViewModel>(
create: (_) => CounterViewModel(),
)
],
child: CounterView(
title: 'Clean Provider Counter',
),
);
}
}
Recommended Folders Structure ðŊ
We highly recommend you to use the clean architecture to achieve the power of this package.
Tip: Try to flow something similar to the provided examples under /example
folder.
<future_name> --> The name of your (micro_service) examples: user, auth, home etc.
|
|_ data --> The data layer.
| |
| |_ data_sources --> Holds the data sources ether local or remote.
| | |
| | |_ remote --> Holds the contract and all implemntation of the remote data source kown as API calls.
| | | |
| | | |_ <future_name>_remote_data_source.dart --> The remote data source contarct.
| | | |_ <future_name>_remote_data_source_impl.dart --> One implementation of the remote data source contarct.
| | | |
| | |_ local --> Holds the contract and all implemntation of the local data source such as data caching.
| | |
| | |_ <future_name>_local_data_source.dart --> The local data source contarct.
| | |_ <future_name>_local_data_source_impl.dart --> One implementation of the local data source contarct.
| |
| |_ models --> Holds the conversation logic.
| | |
| | |_ <entity_name> --> Holds a spesific entity conversation logic.
| | |_ <entity_name>_model.dart
| | |_ <entity_name>_model.g.dart
| |
| |_ <future_name>_repository_impl.dart --> The implementation of <future_name>_repository.dart of domain layer.
|
|
|_ domain --> The independent layer, in this layer you sholde avoid any logic.
| |
| |_ <future_name>_entity.dart --> The entity, this is the blu print of what your logic shold look like.
| |_ <future_name>_repository.dart --> The repository contract, the middle man between the view_model and the data_sources.
|
|
|_ providers --> Holdes all providers related to this future.
| |_ <future_name>_providers.dart
|
|
|_ presentation --> The presentation layer.
|
|_ models --> Contains UI models.
|
|_ view
| |_ <future_name>_scene.dart --> The main future UI.
| |_ widgets --> Holds all utils widgets for this future.
|
|_ view_model --> holds all view_models related to this future.
| |_ <future_name>_view_model.dart --> The main future view_model.
|
|_ utils --> Contains utils reiusable code over this future.