Unlocking Your Flutter Widgets With Keys
Learn how using the right keys in your Flutter widgets can help you avoid UI bugs and improve the performance of your app. By Michael Malak.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Unlocking Your Flutter Widgets With Keys
25 mins
- Getting Started
- Setting up the Starter Project
- Understanding Keys
- Making Swaps With Stateless Widgets
- Handling Swaps in Stateful Widgets
- Using Keys to Avoid Unexpected Results
- Reordering TODOs
- Enabling Drag and Drop
- Adding a Key
- Using Global Keys
- Adding a TODO
- Adding a TODO to the List
- Preserving the Scroll Position
- Preserving the News State
- Refetching News Articles
- Fixing a Bug in the State
- Adding Dividers
- Preserving the Expanded State of the News Items
- Where to Go From Here?
Preserving the News State
You've now added PageStorage
with PageStorageKey
s to HomePage
. However, every time you open the news tab, the app shows a loading indicator while it fetches news articles from HackerNews. This could change the order of the news articles in the list.
To fix this, use PageStorage
to store the entire news list, in addition to the current scroll position of the list.
To do so, go to lib/ui/news/news_page.dart and replace //TODO: Preserve News State on tab change
with the following snippet:
// 1
void updateNewsState() {
// 2
final fetchedNews =
PageStorage.of(context)!.readState(context, identifier: widget.key);
// 3
if (fetchedNews != null) {
setNewsState(fetchedNews);
} else {
fetchNews();
}
}
// 4
void saveToPageStorage(List<NewsModel> newNewsState) {
PageStorage.of(context)!
.writeState(context, newNewsState, identifier: widget.key);
}
Here, you:
- Add
updateNewsState
, which checks if there's a cached news list by usingPageStorage
instead of always fetching the news from the network. - Read the state of the stored news from
PageStorage
, which is an inherited widget. - Choose what to do, depending on whether
PageStorage
has a cached news list. - Add
saveToPageStorage
. This caches the news list the app fetched from the network inPageStorage
.
In fetchNews
, add the following statement after you call setNewsState
:
saveToPageStorage(shuffledNews);
This saves the fetched news articles list in PageStorage
.
Finally, in the initState
of _NewsPageState
, replace fetchNews();
with updateNewsState();
so you no longer fetch news articles from the network on every initialization of NewsPage
.
Hot restart. Now, when you switch to the News tab, the app preserves the scroll state and fetches the news articles from the network only on the first initialization of the NewsPage
, as you expect.
But what if you've already read everything interesting? You'll handle getting new articles next.
Refetching News Articles
PageStorage
now allows you to cache news articles, but there isn't a way to fetch the latest news. To do that, you'll implement pull-to-refresh functionality to refetch news articles. This behavior is common on mobile apps.
In NewsPage
, replace the Scaffold
's body
to the following:
body: isLoading
? const ProgressWidget()
:
// 1
RefreshIndicator(
child: buildNewsList(),
// 2
onRefresh: fetchNews,
color: AppColors.primary,
),
Here, you:
- Use Flutter's
RefreshIndicator
as a wrapper around theListView
containing the news articles. - Call
fetchNews
when the user pulls on the top of the news list.
Hot reload. The app will now refetch the news articles using the refresh indicator:
Tap a news article to view metadata including the author's name and the number of votes and comments. This is the expanded state of the news article in the list view.
Fixing a Bug in the State
Notice that when you expand a news article then refresh the list, the news article at that same position expands after the refresh. This is a bit weird — you'd expect the expanded state to be tied to the news article, not to a specific position on the list.
This happens because NewsItemWidget
is a StatefulWidget
that holds the state of the news item — in this case, whether it's expanded or collapsed.
As you read above, you need to add a key to NewsItemWidget
to fix this problem. So add the following key in buildNewsList
:
(newsItem) => NewsItemWidget(
key: ValueKey<int>(newsItem.id),
...
),
Since each news item has its own unique ID, you can use a ValueKey
to help Flutter compare different NewsItemWidget
s.
Hot reload. You've fixed the issue and preserved the expanded state to match the news article instead of its position in the list of articles.
Adding Dividers
Now, you want a way to easily see where one item in the list ends and the next one begins. To do this, you'll add dividers at the bottom of each news item.
To do this, add the following code in the buildNewsList()
method inside news_page.dart:
...
.map(
(newsItem) => Column(
children: [
NewsItemWidget(
key: ValueKey<int>(newsItem.id),
newsItem: newsItem,
),
const Divider(
color: Colors.black54,
height: 0,
),
],
),
)
...
In buildNewsList
, you currently map each newsItem
directly to a NewsItemWidget
. To make your change, you need to first wrap NewsItemWidget
in a Column
. You'll then add a Divider
after NewsItemWidget
in the Column
.
Hot reload. Now, you'll see the divider. However, on every pull to refresh action, observe that the app loses the expanded state of the news articles in the list.
You'll fix that next.
Preserving the Expanded State of the News Items
This happens because Flutter checks for keys of elements that are on the same level. In this case, Flutter compares both NewsItemWidget
s. However, these don't contain the Divider
.
A key should always be at the topmost level of the widget subtree you want Flutter to compare. Therefore, you want your app to compare the Column
s wrapping both NewsItemWidget
and Divider
.
To do so, in buildNewsList()
, move the key so it belongs to the Column
instead of NewsItemWidget
, as follows:
.map(
(newsItem) => Column(
key: ValueKey<int>(newsItem.id),
children: [
NewsItemWidget(
newsItem: newsItem,
),
...
],
),
)
Flutter now compares the Column
s, as you intended.
Hot reload and confirm that you once again preserve the expanded state for news articles across refreshes. Mission accomplished!
Where to Go From Here?
Download the final project files by clicking the Download Materials button at the top or bottom of the tutorial.
You now have a deeper understanding of widget keys, and more importantly, when and how to use them. You used ValueKey
s, ObjectKey
s and PageStorageKey
s to help Flutter preserve the state of your widgets.
Check out the following links to learn more about some of the concepts in this tutorial:
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!