I'm new to Flutter and I'm trying to build a chat application and I've watched several tutorials. To view the chat messages list/history, almost every tutorial is doing something like this: (I'm shortening the code to get to the point)
List<Widget> messages = api.listOfMessages();
return Column(children: messages);
Now every time there's a new message, messages is updated and the column is re-built. I gotta say the word "rebuild" sounds an expensive procedure to me. Say 2 users have been chatting 500 lines. Now every time a new message is coming, 500 lines are getting rebuilt over an over.
I thought about putting an empty widget at the end of the list. So when a new message arrives, I just insert it to that empty widget and have that to rebuild only:
List<Widget> messages = api.listOfMessages();
return Column(children: [...messages, EmptyWidgetForNewMessage()];
But that looks like a hack and will cause a lot of nested widgets, because every new message must also insert another EmptyWidget etc...
How can I avoid rebuilding previous messages and only insert the new one to the view? (or rebuilding the entire list is not that big a deal?)
You can use sliver widget to build the messages that are visible in viewport(or within cacheExtent).
Like ListView.build, ListView.separated, from the doc of ListView.build:
if the list view's children are
created in advance, or all at once when the [ListView] itself is created,
it is more efficient to use the [ListView] constructor. Even more efficient, however, is to create the instances on demand using this constructor's itemBuilder callback.
Also, we will not fetch all messages from server at once. Instead we will fetch them in batches with query like ?page=1&size=20.
Note: There is a known issue with this widget, avoid using shrinkWrap: true if possible. See this issue
couple of improvements that you could target
Use ListView builder constructor instead of the column, only the children widget that are currently visible in the screen will be rendered whereas in Column widget all of its children will be rendered. Also ListView should be your preferred widget, because the Column widget is not scrollable, and there are chances of overflow exception, if the message list length is huge.
Use const constructor for all type of widgets returned by api.listOfMessages(), this will allow the compiler to reuse any rendered widget, meaning every time when a state change happens(in you case arrival of a new message) the entire tree is not re-rendered, the renderer will have the luxury of re using previously built message widget.
These two suggestions should take care of any performance bottlenecks, in short we would be rendering only the visible children widgets with ListView and we will be re reusing already rendered widget with the help of const constructor.
Related
I'm building a chat app and I want to achieve a ListView() that loads up like its reverse property is set to true i.e showing the end of the ListView on build. The reason I don't want to set the reverse property to true is because, if the items in the ListView do not fill up the screen, they are aligned to the bottom (cannot use shrinkWrap because of performance), and also because it reverses the order of the items and to show new elements at the bottom of the list, I'd need to use a spread operator and I'm worried about performance since the Map from which I render from can grow significantly.
I have also tried using the ScrollController's jumpTo method inside a SchedulerBinding.instance!.addPostFrameCallback but the user can see the scrolling and
it also causes it to jump like it's attempting to scroll beyond what is contained in the ListView. I have also used ScrollablePositionedList and set the index to which I want to scroll, but this jumps also.
Is there any way I can achieve my aim without any drawbacks visually or performance-wise?
You have the correct idea: in the initState, jump to the end. The end of the list can be obtained from position.maxScrollExtent. When done correctly, this does not create any visual or performance drawbacks. For example:
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_controller.jumpTo(_controller.position.maxScrollExtent);
});
}
The _controller should be the ScrollController passed to the list.
My Question: Can you keep built UI that was built by a state when another state is called?
Please correct me if I'm using the cubit pattern wrong. I have a unique example I'm trying to solve. I have the cubit set up and it's working as per documentation.
I have a screen where I have a horizontal list that is returned through cubit. Then based on the horizontal list returned if you click one of the items the cubit fetches a second list to display vertically underneath the horizontal list.
var _responseCategories = await _repository.postGetRootCategories();
emit(ShowCategories(state, _responseCategories));
var _responseCategoryItems = await _repository.postGetCategoryItems(_responseCategories[0].id);
emit(ShowCategoriesItems(state, _responseCategoryItems));
The ui has a bloc builder and processes the two states but when the one state is changed the UI for the first built state is then not there I understand why this happens but is there a way to stop it until I emit the state again. The UI decodes the states in a builder as follows:
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (state is ShowCategories) buildCategories(context, state),
state is ShowCategoriesItems
? Expanded(child: buildMenuItems(context, state))
: SizedBox.shrink(), //getMenuList(_activeSelection)),
],
);
If I'm using this wrong that's understandable I just want to know if there is a better solution to this to solve the problem as the category items are different per category and I don't need to fetch the categories every time the state changes.
The app is essentially a menu. The horizontal section is the categories of the menu 'I only need to load them once' and the vertical section is the items 'I need to load them when the user selects a category'. The items are different per category but the categories never change unless set by the server.
It seems like you have a master-detail view and your detail state is just a superset of the master-no-detail state.
You could derive your detail state from the master state and add more information to it (master-list, adding selected, details-list to the derived class) or you could just go with one state for both (master-list, selected, details-list) where some things aren't set when nothing is selected yet.
If your details view is really complicated, you could also create a whole new BLoC for it, with it's own logic and builder in the widget tree, then you can have totally different states.
But states from the same BLoC, you cannot opt to only rebuild a part of the tree below the builder.
(well, technically you can do anything, but there is no point in working around a pattern. If it does not fit, use a different one. And I think you can easily make it fit as described above)
I have a ListView that might be big / small, depends on result of API call. What I want to do is if the result is too big, put this ListView inside Expanded.
The client could be anything, phone or tab, all with different sizes, both horizontal and vertical orientation supported.
How do I detect the size of the ListView before it gets built?
if (MyListView.height > this.container.height) { // ListView isn't built yet, so how can I know the size?
return Expanded(child: MyListView);
}
return MyListView();
// MyListView is just a ListView inside a Container
API calls should be made inside initState function so that you don't hit the endpoint with each rebuild. (which can happen many times) You can create an empty future object in your statefull widget and in the initState function assign your api call to the future object. In build method add a FutureBuilder widget to control the state of the api call. (loading, errors? etc.) Once you get the data just check the length of the list and depending on that you can decide what to do:
data.length < 20 ? MyListView() : Expanded (child: MyListView())
I'm fighting with Flutter's provider and can't understand all of it's possibilities.
The thing is that for example I have such widgets structure:
Widget_A
--Widget_B
----Widget_C
----Widget_D
--Widget_E
--Widget_F
Simple example is TODO list. Imagine you have categories, add_field and todos_list. Each is a separate widget. Than you change categories so todos_list should be reloaded to show todos from selected category. Later you add todo using add_field so todos_list also need to be updated because of new record. How can I achieve something like this?
PS: I was trying to separate all the stuff to the different Provider classes, but effect is the same: if widget uses Provider.of(context) it will be updated no matter what. And as I should combine data from 2 providers there will be widget that is connected to the both providers so the result will be endless loop. Still.
I have a ListView and I am using ListView.builder to lazily populate the list when children get visible.
However, the items also need to render additional content which I want to fetch lazily.
To do that, I am constructing an http-get-future in the child-builder and pass it to a FutureBuilder that renders the additional content as soon as the Future completes.
However, even if I cache the http-get-futures, the whole list feels glitchy since the items are rebuild when they get visible again and the FutureBuilder renders always one waiting-frame without any data (“because there is no way to synchronously check if a Future already completed” — as the Flutter docs say).
What should I change?
I found a solution:
Instead of directly using a Future in the FutureBuilder, I put a ValueNotifier in the cache that is set inside of the Future‘s then function and a replace the FutureBuilder by a ListenableBuilder.
The ListenableBuilder does not have the issue with the empty frame...
I just resolved by using "reverse: true"
ListView.builder(
reverse: true,
...
)