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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Preserving the News State

You've now added PageStorage with PageStorageKeys 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:

  1. Add updateNewsState, which checks if there's a cached news list by using PageStorage instead of always fetching the news from the network.
  2. Read the state of the stored news from PageStorage, which is an inherited widget.
  3. Choose what to do, depending on whether PageStorage has a cached news list.
  4. Add saveToPageStorage. This caches the news list the app fetched from the network in PageStorage.

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.

Preserving News State

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:

  1. Use Flutter's RefreshIndicator as a wrapper around the ListView containing the news articles.
  2. 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:

Pull to refresh News Page

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.

Bug in saving the state of expanded news

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 NewsItemWidgets.

Note: When choosing a key, don't choose a random value or a value that you generate from inside the widget. For instance, don't use the index of the item in the list as the key for the widget.

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.

Expanded state stays with the news article now, not the position in the list

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.

Expanded state lost bug on adding a divider in each news article item

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 NewsItemWidgets. However, these don't contain the Divider.

Key not top of sub tree

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 Columns 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 Columns, as you intended.

Key at the top of sub tree

Hot reload and confirm that you once again preserve the expanded state for news articles across refreshes. Mission accomplished!

Final divider in News Page

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 ValueKeys, ObjectKeys and PageStorageKeys 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!