To keep my app as performant as possible and code testable and maintainable i read about normalizing the redux state: https://redux.js.org/recipes/structuringreducers/normalizingstateshape
I was wondering if something like this is possible with redux in flutter. I have this state and view model:
class CompetitionState {
final LoadingStatus status;
final Competition competition;
}
class Competition {
final List<Competitor> competitors;
}
class CompetitionPageViewModel {
final LoadingStatus status;
final Competition competition;
}
A single competitor inside the List of competitors can change dynamically based on user input or socket events. Currently a change inside one competitor would lead to a complete rerender of my ListView as i connect the view model like this:
return StoreConnector<AppState, CompetitionPageViewModel>(
distinct: true,
converter: (store) => CompetitionPageViewModel.fromStore(store),
builder: (context, viewModel) => CompetitionPageContent(viewModel)
);
Is there a solution to this problem? My first approach would be to also normalize the state by introducing an EntityState, which holds all the different normalized entites. Similiar to how it is displayed here:
https://medium.com/statuscode/dissecting-twitters-redux-store-d7280b62c6b1#.2e55tu6wb
But with this i am unsure how to structure the state and the reducers to be able to work on single entities without causing a complete list rerender. And also how to connect a Widget to a single entity inside this state. Has anyone any expierence with this and can guide me in a direction?
First of all, normalizing state is simply a paradigm so it is applicable in any form of state management. The first thing you need to normalize your state is some way to uniquely identify the model you want to normalize (most likely an ID), in this case the Competitor. You can follow the EntityState example that you linked to to accomplish this. Basically you would just store a mapping of id to Competitor somewhere in your state. Then, you would change your Competition class to:
class Competition {
final List<int> competitorIds;
}
And then, in your Flutter ListView you can attach each competitor to its own StoreConnector like so:
ListView.builder(
itemCount: competition.competitorIds.length,
itemBuilder: (context, index) {
int competitorId = competition.competitorIds[index];
return StoreConnector<AppState, Competitor>(
distinct: true,
converter: (store) => store.state.entities.allCompetitors[competitorId],
builder: ...
);
}
)
Related
I've followed various guides to successfully get data out of a Firestore collection with a StreamProvider. What I can't quite seem to figure out is how to get a singular document and its associated fields.
For example, let's say I have a collection 'WeatherObs' with a document called '5-13-21' as shown below:
If I wanted to pull in the whole collection with my StreamProvider, I could easily just put it into a list like so:
class FirestoreService {
FirebaseFirestore _db = FirebaseFirestore.instance;
var random = Random();
Stream<List<Weather>> getWeather() {
return _db.collection('TodaysWeather').snapshots().map((snapshot) => snapshot.docs.map((event) => Weather.fromJson(event.data())).toList());
}
I can't for the life of me, however, figure out how if I wanted to just access a single document the same way. I am able to get the document accessible in a key, value pair so I can use fromJson. But when I access the Provider object that calls the method in another class, it always returns null.
Stream<Weather> getWeather() {
return _db.collection('TodaysWeather').doc('5-13-21').snapshots().map((event) => FastFoodHealthEUser.fromJson(event.data()));
}
In the last example, I am not returning a list, but I don't think a list is necessary as I should be able to access the weather object and get access to its attributes like high (type String). Is accessing a single document in the same manner possible?
You can use where for checking on unique field on your documents like this code
Stream<List<VehicleCommentSessionModel>> getSomeoneCommentsList(
{#required String sellerId}) {
return _fbd
.collection('comments')
.where('sellerId', isEqualTo: sellerId)
.snapshots()
.map((qSnap) => qSnap.docs
.map((doc) => VehicleCommentSessionModel.fromJson(doc.data()))
.toList());
}
I'm starting with flutter as I want to port my swift app to Flutter, but I'm getting stuck understanding the pattern Bloc/Repository/Firebase as I'm following the tutorial https://bloclibrary.dev/#/flutterfirestoretodostutorial dough I use the real time database, not Firestore.
My swift app is basically a map where you can add Alerts at your actual coordinates. The Alert get sent to Firebase and the firebase observer on the map updates the map showing the just added alert.
The above tutorial should help me porting my app. I'm just not sure I do understand the logic behind the code.
My concerns are 2:
First. There is an Entity layer between the model object and the firebase object. It is explained that this will facilitate having different Data providers, but I don't really see it facilitating anything. In the Model class there is a toEntity() and a fromEntity() conversion method, and in the Entity class there is a fromSnapshot() and a toDocument() conversion method. I don't see what's the point here. Is it really necessary? What's wrong with doing the conversion directly in the Model class , having different methods for each Data provider?
Second. Inside the TodoBloc I can't follow the logic.
The first event that is sent to the bloc at AppStart is LoadTodos.
BlocProvider<TodosBloc>(
create: (context) {
return TodosBloc(
todosRepository: FirebaseTodosRepository(),
)..add(LoadTodos());
In the mapEventToState() method of TodoBloc that event gets mapped to this Stream:
Stream<TodosState> _mapLoadTodosToState() async* {
_todosSubscription?.cancel();
_todosSubscription = _todosRepository.todos().listen(
(todos) => add(TodosUpdated(todos)),
);
}
So far so good. As I understand this subscribes to the todos() Stream ()
#override
Stream<List<Todo>> todos() {
return todoCollection.snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
}
and this should be the equivalent of the firebase observer in my swift app. It this part inside the listen closure I'm not sure to understand: (todos) => add(TodosUpdated(todos)) .
This sends to itself (TodoBloc) a TodosUpdated event on which the bloc will map this Stream:
Stream<TodosState> _mapTodosUpdatedToState(TodosUpdated event) async* {
yield TodosLoaded(event.todos);
}
which is this:
class TodosLoaded extends TodosState {
final List<Todo> todos;
const TodosLoaded([this.todos = const []]);
#override
List<Object> get props => [todos];
#override
String toString() => 'TodosLoaded { todos: $todos }';
}
Is this the actual list of Firebase objects? Does the todos() Stream return the entire node every time a new object is added in Firebase?
In my swift app the observer returns only the .childAdded after the first download of the node.
Should I use the firebase_database package that has a FirebaseList class(https://pub.dev/documentation/firebase_database/latest/ui_firebase_list/FirebaseList-class.html) that will just return a List on any change on the node as my observers do in my swift app?
Sorry for this very long and messy question, but I'm quite lost here starting with bloc pattern.
Thank you very much for your time and help.
Ok, I I think I understood the logic behind it, but if you see that I didn't get it right please correct me as at this stage of getting into a new paradigm is very important not to carry any misconceptions.
todos() is the Stream coming from Firebase and returns a List<Todo>.
_mapLoadTodosToState() is the bloc method that attach a bloc listener to todos() and in the .listen(onData) callback, it sends to the bloc an TodosUpdated(todos) event containing the latest list.
TodosUpdated(todos) gets mapped to _mapTodosUpdatedToState, which yields
TodosLoaded(event.todos) , the new state that BlocProvider uses to build the UI.
Thank you and I hope this will help others struggling to master BloC pattern at a more complex level.
Cheers
I have the following situation: there's a button on the screen, which adds data to db. And there's the future builder which has future getDataFromDB. When I add data by button, the future builder doesn't get data from DB. And when I do several swipes on the screen it works correctly. What's the matter? Here's the code:
FutureBuilder(
future: DatabaseManager().findAllCaloriesForSelectedDate(currentDate),
builder: (context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
_caloriesCurrent = snapshot.data;
return AnimatedCircularChart(
size: Size(constraints.maxWidth * 0.8, constraints.maxWidth * 0.8),
initialChartData: <CircularStackEntry>[
CircularStackEntry(
<CircularSegmentEntry>[
CircularSegmentEntry(
currentProgress,
Color(AppColors.brandViolet),
),
CircularSegmentEntry(
100 - currentProgress,
Color(AppColors.layoutBackgroundColor),
),
],
),
],
chartType: CircularChartType.Radial,
edgeStyle: SegmentEdgeStyle.round,
percentageValues: true,
);
} else {
return Container();
}
},
)
I'll appreciate any help. Thanks in advance!
There're two problems with your code:
You're obtaining Future object within build function which isn't right way to do it. Every time your widget rebuilds, you get new Future object (so, maybe, it rebuilds when you touch/scroll). You should store Future object in State of your Widget.
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
You're using FutureBuilder, but you expect FutureBuilder to rebuild after you change some data. FutureBuilder doesn't work like that and rebuilds only once - when Future value is resolved (if you store Future object within State. In your case, it always gets new Future on rebuild). You probably want to use StreamBuilder. It will allow you to add new data to the stream of data. StreamBuilder will trigger on stream changes and automatically rebuild.
You should read about BloC Architecture, in case you haven't.
My goal is to create an edit profile page in Flutter using the bloc pattern.
I've searched for a better/cleaner way to create a user profile page in Flutter using Bloc, but I can't find anything.
Right now I have to list every field out. Here's an example of 2 fields:
final _firstNameController = BehaviorSubject<String>();
final _lastNameController = BehaviorSubject<String>();
Function(String) get firstNameChanged => _firstNameController.sink.add;
Function(String) get lastNameChanged => _lastNameController.sink.add;
Stream<String> get firstNameStream => _firstNameController.stream;
Stream<String> get lastNameStream => _lastNameController.stream;
String get firstName => _firstNameController.value;
String get lastName => _lastNameController.value;
#override
void dispose() {
_firstNameController?.close();
_lastNameController?.close();
}
There are a lot more fields and I don't want to have all this code if I can avoid it.
I'd prefer to only have 1 user bloc and update the specific field of that user. I've added the following to a user bloc.
final _userFetcher = BehaviorSubject<User>();
Observable<User> get userStream => _userFetcher.stream;
User get user => _userFetcher.value;
Function(User) get changeUser => _userFetcher.sink.add;
#override
void dispose() async {
await _userFetcher.drain();
_userFetcher.close();
}
Here's an example of my user model:
class User {
final Name name;
User.fromJson(Map<String, dynamic> json)
: name = Name.fromJson(json);
Map<String, dynamic> toJson() => {
"first": name.first,
"last": name.last,
};
}
The issue is I can't figure out how to use a textfield to edit the "fist name" and "last name" fields in my "User" model.
Is this possible, am I going about this the wrong way, or should I stick to listing every field out individually?
To individually manage all those streams for each individual fields can be cumbersome. I would recommend you to try out this library flutter bloc . It is a really good one that handles the state management pretty well. You just need to define the states and events and then for each event you can generate a state.
So for example you want to validate password field as the user is typing. You define an Event(eg. PasswordChanged). This will call a method in the bloc. In which you can write your business logic to check the validation.
After your validation logic you can yield a new State(error, succcess). This will cause your UI to be rebuilt and then you can update your UI according to your state.
You should checkout the documentation of this library. It also has some very good examples there.
Thank you everyone for your help!
I ended up being able to edit the nested first and last name fields in my user model I listed above by implementing the following in my textField:
onChanged:(text) {
User thisUser = _bloc.user;
thisUser.name.last = text;
}
In a screen, a Firebase list has logic applied to
reactively detect blueness of widgets
hide blue widgets
show non-blue widgets
I want to count the blue widgets.
What is the best way to do this?
return new StreamBuilder<int>(
stream: subscribeMyThings(thing.id),
builder: (context, thingsSnapshot) {
return new FirebaseAnimatedList(
query: getThings(thing.id),
itemBuilder: (context, snapshot, animation, index) {
return new ReactiveWidget<int>(
reactiveRef: getThingBlueness(snapshot.key),
widgetBuilder: (checkBlueness) {
// code to check blueness here
// if Thing is blue, place empty container
if (thingIsBlue) {
return new Container();
// *** how do I count these??? ***
}
// or, if Thing is NOT blue
return new Thing(thing.id);
);
},
);
);
}
I think you are not hitting the problem from the proper point of view, plus the title is misleading.
You might not intend to count the widget shown on the screen, but rather count a subset of Firebase database entries that fit some conditions.
This makes a big difference in Flutter, since counting widgets involves the the render tree, the layouts and graphics in general, while it looks you want to address a data problem.
If I am right then you might consider having an index in your db keeping track of the number of blue items and listen to it in another streambuilder.
Long story short, you might want to keep a map< id, blueness >, update each element at build time, and iterate over the map whenever required to count.
// somewhere in your outer scope
Map<int, bool> _bluenessMap = new Map<int, bool>();
return new StreamBuilder<int>(
stream: subscribeMyThings(thing.id),
builder: (context, thingsSnapshot) {
return new FirebaseAnimatedList(
query: getThings(thing.id),
itemBuilder: (context, snapshot, animation, index) {
return new ReactiveWidget<int>(
reactiveRef: getThingBlueness(snapshot.key),
widgetBuilder: (checkBlueness) {
// code to check blueness here
// ----------------------------
// before return let us update the map
_bluenessMap[thing.id] = thingIsBlue
// ----------------------------
// if Thing is blue, place empty container
if (thingIsBlue) {
return new Container();
}
// or, if Thing is NOT blue
return new Thing(thing.id);
);
},
);
);
}
// call me when you want to count
int fancyBlueCounter(){
int result = 0;
for(bool isBlue in _bluenessMap.values){
if(isBlue) result += 1;
}
return result;
}
Edit
Since the author of the question confirmed it is a "data problem", I feel like suggesting another (maybe better) way to address it.
Aggregates or Indexes are (in my opinion) the proper way to solve your problem. Anytime someone touches a Thing depending on its blueness it refreshes a purposefully created aggregate in the DB (actually also a simple data entry count). You can do this either following any function editing Things with a transaction updating the blue count, or with cloud functions (see doc here) listening for changes events (creation, deletion, and edit) and updating the count as well.