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.
Related
I made an API call and returned a list data and few other values. I made the API call in one screen in the initState now and I need the list and other data in other 2 screens without Navigation. Is this possible. I am also using provider package. I am new to flutter so any help would be highly appreciated. Thank You.
List<YourClassName> list = [];
class ExampleProvider with ChangeNotifier {
getList(){
list = await yourApiCall();
notifyListeners();
}
}
Your provider class should look like this. When your api returns, you need to use that equation. With that way you can call it everywhere your list with
Provider.of<ExampleProvider>(context).list;
I am extremely new to Flutter, so please forgive my ignorance and please explain things to me like I am a toddler.
I would like to implement a to-do list into my app similar to this project: https://github.com/ishrath-raji/todoey-flutter
It's just a basic list where users can add items, cross them out, and delete them. Very simple.
However, I have absolutely no idea how to take the items that users enter into the to-do list and store them in memory so that the user can review them later.
I've tried googling around, but all the answers I've seen are above my understanding and/or written in a way that is difficult to follow.
Any help would be very much appreciated!
I assume you have a view / page, where the user can give all the necessary information for the new ToDo item. This means you have a class representing a ToDoItem.
class ToDoItem {}
You will want to store them in some List.
As you probably want this list of ToDo items to be accessbible everywhere within the map, you should start researching the topic "state management".
As a starting point, just to name the easiest of all solutions, you could use Riverpod and declare one global variable:
final todoListRereference = StateProvider<List<ToDoItem>>((ref) => <ToDoItem>[]);
Now you have a list of ToDoItem which is accessible from everywhere in your app, provided you follow the steps to make Riverpod providers accessible everywhere. For example, in every build method you can use
final todoList = ref.watch(todoListRereference);
and you have access to all the stored ToDoItems.
In the case of your ToDoItem you can create with all the user information, you will have a construction like:
onPressed: () {
final todoItem = ToDoItem(...);
final todoListProvider = ref.read(todoListReference.notifier);
todoListProvider.state = [... todoListProvider.state, todoItem);
}
I just assumed it would happen after the user clicked something and the onPressed method of the according button is triggered.... However, first you create the ToDoItem. Then you access the List we made accessible with the reference. Then we change the "state" of that provider to a new state which is defined as all the old ToDoItems plus the newly created one.
If you have any pages in your app where you can see all the ToDoItems, you will now see one more.
I hope this is okay as a starting point.
I am normally a Laravel frontend dev, and I'm trying to learn flutter and dart as an intro to mobile frontend. In laravel blade's html, when you want to create visual elements for each data you are feeding to the front it's exactly this: a for each loop that repeats the element but with different data. I undertsand Dart uses a different paradigm, and I am too stuck in the "laravel cone" and finding it difficult to implement solutions as it should be done in dart.
The way this is working now is: I've got some data I got in a post request, saving that in a Todo class and passing it as parameters to the renderer of the next view:
The Class
class Todo {
String name;
String lastacc;
String created_at;
String farmDoc;
String farmName;
String clientId;
List farms;
Todo(this.name, this.lastacc, this.created_at, this.farmDoc, this.farmName, this.farms, this.clientId);
}
The Function
Todo infoAcc = Todo(name, lastAcc, createdAcc, farmDoc, farmName, farms, id );
Future.delayed(
const Duration(milliseconds: 500),
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Farmlist(),
settings: RouteSettings(arguments: infoAcc)));
},
);
This is working nice, before the function I managed to get the variables correctly set and by debugging I know it's working fine; Now i have this other view, the Farmlist, where it should get de List farms and use it to generate several Tiles in a ListTile, each one with info from a different farm. It should be dynamic, as to show always the amount of tiles equivalent to the amount of elements in the array I pass, as different users will have a different amount of farms.
Any example or documentation i could read, or alternative solution is welcome. I've been struggling with this for quite the time now.
You can use the ListView.builder widget, you simply need to pass the array that you want to build and with the builder function you can create a widget that depends on the given data
For more information:
https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html
https://flutter.dev/docs/cookbook/lists/long-lists
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.
I have a problem with some dynamically generated forms and passing values to them. I feel like someone must have solved this, or I’m missing something obvious, but I can't find any mention of it.
So for example, I have three components, a parent, a child, and then a child of that child. For names, I’ll go with, formComponent, questionComponent, textBoxComponent. Both of the children are using changeDetection.OnPush.
So form component passes some values down to questionComponent through the inputs, and some are using the async pipe to subscribe to their respective values in the store.
QuestionComponent dynamically creates different components, then places them on the page if they match (so many types of components, but each questionComponent only handles on one component.
some code:
#Input() normalValue
#Input() asyncPipedValue
#ViewChild('questionRef', {read: ViewContainerRef}) public questionRef: any;
private textBoxComponent: ComponentFactory<TextBoxComponent>;
ngOnInit() {
let component =
this.questionRef.createComponent(this.checkboxComponent);
component.instance.normalValue = this.normalValue;
component.instance. asyncPipedValue = this. asyncPipedValue;
}
This works fine for all instances of normalValues, but not for asyncValues. I can confirm in questionComponent’s ngOnChanges that the value is being updated, but that value is not passed to textBoxComponent.
What I basically need is the async pipe, but not for templates. I’ve tried multiple solutions to different ways to pass asyncValues, I’ve tried detecting when asyncPipeValue changes, and triggering changeDetectionRef.markForChanges() on the textBoxComponent, but that only works when I change the changeDetectionStrategy to normal, which kinda defeats the performance gains I get from using ngrx.
This seems like too big of an oversight to not already have a solution, so I’m assuming it’s just me not thinking of something. Any thoughts?
I do something similar, whereby I have forms populated from data coming from my Ngrx Store. My forms aren't dynamic so I'm not 100% sure if this will also work for you.
Define your input with just a setter, then call patchValue(), or setValue() on your form/ form control. Your root component stays the same, passing the data into your next component with the async pipe.
#Input() set asyncPipedValue(data) {
if (data) {
this.textBoxComponent.patchValue(data);
}
}
patchValue() is on the AbstractControl class. If you don't have access to that from your question component, your TextBoxComponent could expose a similar method, that can be called from your QuestionComponent, with the implementation performing the update of the control.
One thing to watch out for though, if you're also subscribing to valueChanges on your form/control, you may want to set the second parameter so the valueChanges event doesn't fire immediately.
this.textBoxComponent.patchValue(data, { emitEvent: false });
or
this.textBoxComponent.setValue(...same as above);
Then in your TextBoxComponent
this.myTextBox.valueChanges
.debounceTime(a couple of seconds maybe)
.distinctUntilChanged()
.map(changes => {
this.store.dispatch(changes);
})
.subscribe();
This approach is working pretty well, and removes the need to have save/update buttons everywhere.
I believe I have figured out a solution (with some help from the gitter.com/angular channel).
Since the values are coming in to the questionComponent can change, and trigger it's ngOnChanges to fire, whenever there is an event in ngOnChanges, it needs to parse through the event, and bind and changes to the dynamic child component.
ngOnChanges(event) {
if (this.component) {
_.forEach(event, (value, key) => {
if (value && value.currentValue) {
this.component.instance[key] = value.currentValue;
}
});
}
}
This is all in questionComponent, it resets the components instance variables if they have changed. The biggest problem with this so far, is that the child's ngOnChanges doesn't fire, so this isn't a full solution. I'll continue to dig into it.
Here are my thoughts on the question, taking into account limited code snippet.
First, provided example doesn't seem to have anything to do with ngrx. In this case, it is expected that ngOnInit runs only once and at that time this.asyncPipedValue value is undefined. Consequently, if changeDetection of this.checkboxComponent is ChangeDetection.OnPush the value won't get updated. I recommend reading one excellent article about change detection and passing async inputs. That article also contains other not less great resources on change detection. In addition, it seems that the same inputs are passed twice through the component tree which is not a good solution from my point of view.
Second, another approach would be to use ngrx and then you don't need to pass any async inputs at all. Especially, this way is good if two components do not have the parent-child relationship in the component tree. In this case, one component dispatches action to put data to Store and another component subscribes to that data from Store.
export class DataDispatcherCmp {
constructor(private store: Store<ApplicationState>) {
}
onNewData(data: SomeData) {
this.store.dispatch(new SetNewDataAction(data));
}
}
export class DataConsumerCmp implements OnInit {
newData$: Observable<SomeData>;
constructor(private store: Store<ApplicationState>) {
}
ngOnInit() {
this.newData$ = this.store.select('someData');
}
}
Hope this helps or gives some clues at least.