Flutter Provider best practice - flutter

I have been experimenting with the provider package and am usually able to get it to do what I want it to do. However, in some cases I am not sure if what I am doing is at all best practice.
For example, suppose I have a settings page with various, unrelated options - say a theming option, a notifications option, some filter options specific to the app etc.
My question is, should each of these options have their own class dedicated to a single value so that only the parts of the widget tree dependent on that single value rebuild. Or should they all be in the same SettingsProvider class, and there is some way of using the fields in this class separately so as to avoid excessive rebuilds?
Or am I missing the bigger picture entirely? Any help would be great thanks!

A solution I've found is to put all the values in a single class eg SettingsProvider. Then, instead of using Provider.of<> or Consumer<>,
use Selector<>. For example, to get/set the notifications option of settings, you could wrap the widget with a Selector like so -
Selector<SettingsProvider, bool>(
builder: (context, notifications, child) {
(notifications)
? return Text('Notifications are on')
: return Text('Notifications are off')
},
selector: (context , settingsPro) => settingsPro.notifications,
),
This should display whether or not the notifications are on, and is only rebuilt when the notification option changes.
Here is the provider doc page
Here is an article about Selector
Let me know if there are any better solutions.

Related

How to structure BLoCs for screens with cross-dependent entities

I am currently trying to learn the BLoC pattern and still struggle with figuring out the best architecture for an app where entities in the model have cross-dependencies that also have implications on screens.
I am building a very simple CRUD Flutter app that allows the user to manage and tag a movie database. The database is implemented using the SQlite plugin and Drift. So far, there are 4 main screens:
MoviesListScreen
MovieDetailsScreen
TagsListScreen
TagDetailsScreen
So obviously, the list screens list all movies / tags in the database, respectively, and allow you to add and delete entities. When you click on an entitiy on a list screen, you reach the details screen with all information on the respective movie / tag.
From what I have read, it is recommended to have one bloc per feature or screen. Following this tutorial, I created two blocs, one for movies and one for tags. Here is the tags state:
enum TagsStatus { initial, success, error, loading, selected }
extension TagsStatusX on TagsStatus {
bool get isInitial => this == TagsStatus.initial;
bool get isSuccess => this == TagsStatus.success;
bool get isError => this == TagsStatus.error;
bool get isLoading => this == TagsStatus.loading;
bool get isSelected => this == TagsStatus.selected;
}
class TagsState extends Equatable {
final TagsStatus status;
final List<Tag> tags;
final Tag selectedTag;
final List<Movie> moviesWithSelectedTag;
const TagsState(
{this.status = TagsStatus.initial, List<Tag> tags, Tag selectedTag, List<Movie> moviesWithSelectedTag})
: tags = tags ?? const [],
selectedTag = selectedTag,
moviesWithSelectedTag = moviesWithSelectedTag;
#override
List<Object> get props => [status, tags, selectedTag];
TagsState copyWith({TagsStatus status, List<Tag> tags, Tag selectedTag, List<Movie> moviesWithSelectedTag}) {
return TagsState(
status: status ?? this.status,
tags: tags ?? this.tags,
selectedTag: selectedTag ?? this.selectedTag,
moviesWithSelectedTag: moviesWithSelectedTag ?? this.moviesWithSelectedTag);
}
}
And the corresponding bloc:
class TagsBloc extends Bloc<TagEvent, TagsState> {
final Repository repository;
TagsBloc({this.repository}) : super(const TagsState()) {
on<GetTags>(_mapGetTagsEventToState);
on<SelectTag>(_mapSelectTagEventToState);
}
void _mapGetTagsEventToState(GetTags event, Emitter<TagsState> emit) async {
emit(state.copyWith(status: TagsStatus.loading));
try {
final tags = await repository.getTags();
emit(
state.copyWith(
status: TagsStatus.success,
tags: tags,
),
);
} catch (error, stacktrace) {
print(stacktrace);
emit(state.copyWith(status: TagsStatus.error));
}
}
void _mapSelectTagEventToState(event, Emitter<TagsState> emit) async {
emit(
state.copyWith(
status: TagsStatus.selected,
selectedTag: event.selectedTag,
),
);
}
}
Now this works perfectly fine to manage the loading of the list screens. (Remark: I could create separate blocs for the details screens, because it feels a bit out of place to have the selectedTag bloc in the state that is used for the TagsListScreen, even though the selected tag will be displayed on a different screen. However, that would create additional boilerplate code, so I am unsure about it.)
What I really struggle with is how to access all tags in the MovieDetailsScreen where I am using the MoviesBloc. I need them there as well in order to display chips with tags that the user can add to the selected movie simply by clicking on them.
I thought about the following possibilities:
Add all tags to the MoviesBloc - that would go against the point of having two separate blocs and I would have to make sure that both blocs stay in sync; moreover, a failure loading the tags would also cause a failure loading the movies, even in widgets that don't even use the tags
Subscribing to the TagsBloc in MoviesBloc - seems error-prone to me, also same as in point 1
Creating separate blocs for the list screens and details screens - lots of redundancy and additional boilerplate code
Nesting two BlocBuilder components in the MovieDetailsScreen - BlocBuilder currently does not support more than one bloc, probably because this is an anti-pattern
Using one single bloc that holds movies as well as tags and use it in all 4 screens - discouraged by the creators of the BLoC package; also I would need two status properties to manage the loading from the database separately for movies and tags, which I feel should be in separate blocs
What would be the recommended way to handle this kind of business logic with blocs?
Option 4
As you're no dought aware, there are no correct and incorrect answers here, it's a matter of design. And if you ask me, I wouldn't consider movies and tags as two separate features, but that highly depends on the project domain and one's definition of a feature, so let's not go there.
Answering your question I'd go with option 4. Nesting BlocBuilders is a quite common pattern in the bloc architecture, I've seen it many times.
Also, in the same thread you've referenced, the author is recommending that idea, so I don't think it's an anti-pattern.
p.s. option 3 is also fine, and maybe you can avoid the redundancy by creating a class that contains the shared logic between the two cubits, thus, you can still maintain them together, while having the perks of two separate blocs.
There is a recommendation from the author of flutter_bloc available on the flutter_bloc documentation page.
https://bloclibrary.dev/#/architecture?id=bloc-to-bloc-communication
Some key-lines from the page:
...it may be tempting to make a bloc which listens to another bloc.
You should not do this.
...no bloc should know about any other bloc.
A bloc should only receive information through events and from injected repositories
So in short, something like your options 3 or 4. Perhaps with a touch of a BlocListener there to trigger events between the blocs.
There is no problem having multiple blocs per screen. Consider having a bloc controlling the state of a button based on some API calls or a Stream of data, while other parts of the screen are determined by another bloc.
It is not a bad thing to have an "outer" bloc determining if a part of a screen should be visible, but that inner part's state is handled by a separate bloc.
Subscribing to the TagsBloc in MoviesBloc - seems error-prone to me, also same as in point 1
This is the cleanest approach. I have been using it and it scales pretty well as number of blocs increases.

Why I should use named routes?

I searched a lot about "what are benefits of using named route to navigate between screens". And I can't find any actual benefits, actually it has many disadvantages and annoying.
1. In flutter document, it said that named routes use to avoid code duplication.
For example, if I want to navigate to SecondRoute with String one argument, it change from this
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute('Some text')),
);
to this
Navigator.pushNamed(context, SecondRoute.routeName, arguments: 'Some text');
and I need to register it in main file
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == SecondRoute.routeName) {
final String text = settings.arguments as String;
return MaterialPageRoute(
builder: (context) => SecondRoute(text),
);
}
},
);
And if I have more routes, I need to handle and assign arguments for every routes. Isn't that more duplication and complexity?
Also setting.arguments has no-type, isn't that very bad?
2. You can't select which constructor to use when using pushNamed.
For example, I have two constructor default and otherConstructor
class SecondRoute extends StatelessWidget {
static const String routeName = '/second';
String? text;
SecondRoute(this.text);
SecondRoute.otherConstructor(String text) {
this.text = 'Other Constructor: ' + text;
}
}
How can I tell pushNamed which one I want to use.
I have an idea to pass constructor name as an argument and check it like this
Navigator.pushNamed(context, SecondRoute.routeName, arguments: ['default' or 'otherConstructor','Some text']);
In main file
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == SecondRoute.routeName) {
final args = settings.arguments as List<String>;
if (args[0] == 'otherConstructor') {
return MaterialPageRoute(
builder: (context) => SecondRoute.otherConstructor(text),
);
} else if (args[0] == 'default') {
return MaterialPageRoute(
builder: (context) => SecondRoute(text),
);
}
}
},
);
But this is very very complicate and obviously not a good way to do.
3. Some anwsers in reddit and stackoverflow said that named route make code more centralize by keep every route in main file.
Surely centralization is good, but why not do it others way?
For example, keep all dart files that contain routes in new folder
Can someone enlighten me why most people use named route? Thanks for any help.
From my point of view, personally I think the root cause of your concern is you keep using pass the data around your screens using constructor or injection.
Reference from Official flutter docs: https://docs.flutter.dev/development/data-and-backend/state-mgmt/declarative
I am an iOS developer with UI kit so I think you has the same problem with me, that I resolved by changing my thinking flow.
Important thing : From the declaratively UI picture: UI = f(state).
For your problem (1):
I think we should not to pass the data around screens, you can use rxdart, provider, mobx, redux... or any state management. It will keep the data of for UI rendering and you no need to check it again in very long switch/if in main file. If you keep passing arguments so you can't build UI from state
For your problem (2):
I think it's the same problem with (1). Instead of using many constructors, you can create your state to reflecting it. So your screen(I mean the Widget reflecting your screen, not their sub widgets) can take data from state and no need to get it from arguments. Again, it about UI = f(state).
For your problem (3):
I definitely agree with you, keep it in another folder is a best choice.
Note: Personally right now I split widgets to 2 types: screen and Presentational widget.
Screen: Never pass arguments to it, it use the data from state(UI = f(state))
Presentational widget: can receive arguments in constructor.
The original idea is from the founder of redux (https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
=> With this idea, It clearly resolved your problem (1) and (2). With (3) by split to another folder and import it should end with some lines in main file like this(without any process arguments on screen changing):
For the benefit:
Personally I think most advantage is:
1 - You can change the screen later without modify working code(open-closed principle): Assuming your client want to change the screen A to screen A', which has alot of navigate in your code.
Example: your app can go to login screen from 3 different logic: from splash, from press logout, from admin banned message.
If you use: MaterialPageRoute(builder: (context) => SecondRoute('Some text')), so you need to replace it many times(3 times in example) inside the code that work perfectly. The most dangerous thing is: maybe the logic "from admin banned message" is # # from other dev
If you usse: Navigator.pushNamed(context, SecondRoute.routeName, arguments: 'Some text'), you change the code in splited folder(that you mention in (3)) by redirect it to another screen(the new login scrren)
Things below I think it less important:
2- (just a note) If you build your project with web version, you will the route name in the url address, so it turn your page to get url.
3- Prevents alot of unnecessary import(example you can see in login, if you use routes, just import 1 file. but you use navigator directly you need to import many screens related: register, forgotpass....). Anyway, it can resolve by create something like screen_index.dart.

Provider hierarchy in flutter

I am working on a toy application, which is an online shop application. I use Provider for state management. As it progresses, I notice patterns which are not easy to implement. I wanted to understand what is your take on it. I will explain the case.
There is a productsScreen which shows a list of products, it gets its list of products from a ProductsProvider. Right now it just fetches all the products from the server.
There is another widget which is MenuBarWidget. This widget hosts a few stuff including the search box. Like the ProductScreen it has a provider called MenuBarProvider. Now when user types in a search term in the search box int the MenuBarWidget, it updates the MenuBarProvider; then somehow the ProductsProvider has to see that change and adjust list of products accordingly. How do you do that? How two providers communicate ?
I know that it can be handled in the widgets, but that seems very ugly. Ideally I want to listen to MenuBarProvider from ProductsProvider; but I am not sure how to do that, and I am not even sure if that is such a great idea.
Any idea is welcome.
to make 2 providers "communicate" with each other you should use the ProxyProvider.
It updates a provider when another provider updates.
ProxyProvider is part of the Provider package and it is meant exactly for this purpose, to provide dependency to Providers; see here for the docs:
https://pub.dev/packages/provider#proxyprovider
Check this article as well to see a full example:
https://dev.to/paulhalliday/how-to-use-proxyprovider-with-flutter-3ifo
You can use like this:
MultiProvider(
providers: [
Provider(
create: (_) => UserService(),
),
ProxyProvider<UserService, GreetingService>(
update: (BuildContext context, UserService userService,
GreetingService greetingService) =>
GreetingService(userService: userService),
),
],
)
When UserService provider is updated, GreetingService will be updated as well so they will always be in sync.

Flutter Provider package reinitializes when saving in VSCode

I have a simple app with which I am using the Provider package. Within the class using Provider, I fetch some data online and build a collection. I then use it on a couple of other screens. I set up the MultiProvider in main.dart.
return MultiProvider(
providers: [
ChangeNotifierProvider<UserStationInfoProvider>.value(
value: UserStationInfoProvider(),
),
ChangeNotifierProvider<UserStationList>.value(
value: UserStationList(),
),
],
child: MaterialApp(
Within UserStationList is list of the items I am getting online and an integer that indicates which one was selected by the user. I use this data in a couple of screens. The problem comes when I have an emulator up and I make a code change and save the code. The code in main.dart is run again.
The providers are set up again and I lose my collection of items and my integer that holds the item that was selected. If I don't fetch my data again, then I don't have any data and regardless of whether I fetch the data or not, I lose the value in the integer.
Is there any way around this? I'm starting to think that maybe I'm using Provider in a way that it shouldn't be used.
This has been working well. Once I get into my
That happens because you are creating your state directly inside the build method.
It is anti-pattern, and will make you loose your state when the widget rebuilds (which happens on hot reload but not only that).
provider also explicitly states that we should not use the .value constructor to create a value, and instead use the default constructor.
So you can change your:
ChangeNotifierProvider.value(
value: MyModel()
)
into:
ChangeNotifierProvider(
builder: (_) => MyModel()
)

How to make Provider and Navigator work together?

I use Provider for state management and for separate business logic from UI.
I have put some Provider above MaterialApp so can access everywhere in app (for example user info).
But You don’t want to place ChangeNotifierProvider higher than necessary (because you don’t want to pollute the scope).
So I try put some Provider which are only use on some pages lower in widget tree.
For example in my purchase item flow I can have: SelectItemPage => ConfirmItemPage => CheckOutPage.
But my issue is Navigator.push() create separate widget tree every time. So if I initialise a Provider in SelectItemPage this cannot be accessed in ConfirmItemPage or CheckOutPage.
I want Provider be scope to this flow, but I cannot see how.
How I can solve this?
Or my approach is wrong?
Edit: I know I can use prop drilling for pass data from SelectItemPage => ConfirmItemPage => CheckOutPage but this hard to maintain. I want use Provider instead of prop drilling.
I think it's no other way, you mush initialize the provider on each class
try use like this on you app
on you main app
MultiProvider(
providers: ChangeNotifierProvider<yourstate>(
builder: (context) => yourstate,
),
child: MaterialApp...
on each page, you can initialize using below code
final state = Provider.of<yourstate>(context);
and use like this if you don't want to listen any change of the state
final state = Provider.of<yourstate>(context, listen: false);