Search code examples
androidiosflutterdartstatefulwidget

Appearance of rebuilding in Flutter


The Context

I have a flutter application and the main screen of the application is a Scaffold with a PageView body a BottomNavigationBar with four children:

// page declaration
@override
void initState() {
  super.initState();
  _children = [
    const Page1(),
    const Page2(),
    const Page3(),
    const Page4(),
  ];
}

// body
SafeArea(
  child: PageView(
    physics: NeverScrollableScrollPhysics(),
    controller: _pageController, // just a PageController
    children: _children,
    onPageChanged: _onPagechanged,
  ),
)
// bottom nav bar
BottomNavigationBar(
  showSelectedLabels: false,
  showUnselectedLabels: false,
  backgroundColor: Colors.white,
  type: BottomNavigationBarType.fixed,
  items: [
    icon1, icon2, icon3, icon4,
  ],
  currentIndex: _selectedIndex,
  onTap: _onItemTapped,
)

int _selectedIndex = 0; // starts at 0

void _onItemTapped(int index) {
  _pageController.jumpToPage(index);
}

void _onPagechanged(int index) {
  setState(() => _selectedIndex = index);
}

Each child page is constructed using a const constructor, is a stateful widget, and uses with AutomaticKeepAliveClientMixin and this code bit:

@override
bool get wantKeepAlive => true;

There are some stateful widgets down the tree that use an API call to load an image from the web into a Uint8List state variable. That variable is initially null, and I use the initState override to make the call and then update the variable:

@override
void initState() {
  _loadImage();
  super.initState();
}

Future _loadImage() async {
  final resourcePath = widget.item.imageUrl;
  final up = Provider.of<UserProvider>(context, listen: false);

  final fbss = FirebaseStorageService(up.uid);
  final image = await fbss.getResourceWithCache(resourcePath, isItem: true, saveToCache: false);
  setState(() {
    _image = image;
    _isLoaded = true;
  });
}

While the state variable is null (and the API call is running), I show a circular progress indicator; when the image is loaded and no longer null, I use the image in a container as a decoration image for a BoxDecoration widget. The color of the BoxDecoration widget is Colors.grey.

The Problem

Whenever I navigate away from one screen to the next, and then go back, those widgets that are making API calls and loading images flash grey, and then the image is painted back onto the widget. Why is that? Is there a way I should be loading it to make it so that doesn't happen? I'm aware that whenever I change the screen I'm looking at the widget that holds the scaffold with the bottom nav bar rebuilds because of a state change, but why does that make the images appear to be rebuilding? I've verified that those widgets with the images don't in fact rebuild, but it looks like they do. I'd like to prevent this behavior. Do I need to use routes and a Navigator with my pages? What will help to prevent the appearance of reloading every time a users switches between screens?


Solution

  • There was nothing technically wrong with the way that I was using the Stack widget. The problem was that the Stack widget paints renders each child widget from first to last, or from bottom to top

    The stack paints its children in order with the first child being at the bottom (link)

    Because I was using higher resolution images, each time the children in the Stack were rendered it looked like they were reloading the image, but they were just repainting to the UI and it was noticeable because of the resolution. I found that despite using AutomaticKeepAliveClientMixin, when I changed the tab in my app there was an inevitable state change as the _selectedIndex value changed. The build context of the widgets down the tree changed and therefore caused a re-render.

    I fixed this by only loading 2 child items at most in the Stack instead of 5 like I had it set to previously.