Handle memory usage with images / videos and Navigators flutter

302

I think you have already pointed out correctly, the root problem is in the "infinite" nature of pushing lots and lots of pages while keeping all the old pages in memory.

You have not posted any source code, but I don't think it's relevant at this point now. And from the question description, you seem very capable, so we can probably just discuss the problem on a high level. As some comments have pointed out, maybe there are rooms for optimization for each page, but even then, it'd only delay the problem. To put it simply, even if each page is merely a Text widget, it would eventually run into OOM issue.

One way to solve this is to build your own routing stack. This has been made a lot easier in a recent Flutter update, with their introduction of the so-called "Navigator 2.0", you can learn more of it from the official docs.

Essentially, you can have your own "stack" (the data structure) to keep track of what users have clicked, so the "back button" functions correctly. But at each jump, instead of pushing a new page, you literally "replace" the current page. Similarly, when the user goes back, you don't "pop" the navigator because there's nothing under it. Instead, you look up your own "history stack" and reload what the previous page would be. This way, you can delay the problem much further (until your stack overflows, just like the name of this website).

Once that's done, maybe a further optimization you can do, is to keep a handful of the most recent pages alive, instead of immediately destroying them. For example, maybe you can keep the 3-5 most recent pages alive, so users can go back to them faster (without loading). And this optional optimization would cover maybe 95% of the use cases anyway.


Edit:

Since you mentioned in the comments that you would like an example of Navigator 2.0, I pulled out my IDE and did a quick demo for you.

demo gif

As you can see, the user can open an almost infinite amount of new pages, and they can go back to all of them; however, there are only 3 pages being held in the memory at any given time, other pages are completely destroyed and recreated when needed.

I wrote some brief comments in the source code to help you, but you can always learn Navigator 2.0 from the official docs.

Full source code:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // This list holds the real "stack": anything user has ever clicked.
  // But it only holds minimal info - just the page title.
  final List<String> _titles = [
    'Home Page',
  ];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Navigator(
        // The `pages` property here is the most important thing in this demo.
        // It literally represents the stack of pages on the screen.
        // If we make each item in `_titles` into a page, it'll build them all.
        pages: _getPages()
            .map((title) => MaterialPage(
                  key: ValueKey(title),
                  child: _buildPage(title),
                ))
            .toList(),
        onPopPage: (route, result) {
          // When user pops a page, we remove it from our list as well.
          setState(() => _titles.removeLast());
          return route.didPop(result);
        },
      ),
    );
  }

  List<String> _getPages() {
    print("Current list: $_titles");
    // Uncomment the following line, to skip "optimization" altogether.
    // return _titles;

    // Optimization: only build the top-most 3 pages.
    // First, don't optimize if there are only 3 or fewer pages.
    if (_titles.length <= 3) return _titles;
    // Otherwise, we only return the last 3 items, so the navigator
    // will only build those 3 pages, omitting anything beneath them.
    final last3items = [
      _titles[_titles.length - 3],
      _titles[_titles.length - 2],
      _titles[_titles.length - 1],
    ];
    print("Optimizing, only build the top 3 pages: $last3items");
    return last3items;
  }

  Widget _buildPage(title) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: GridView.count(
        crossAxisCount: 4,
        children: [
          for (int i = 1; i <= 10; i++)
            Padding(
              padding: EdgeInsets.all(8),
              child: ElevatedButton(
                onPressed: () {
                  final time = DateTime.now().millisecondsSinceEpoch;
                  setState(() {
                    // When user clicks on a button, we add it to our list
                    _titles.add('Page $i: $time');
                  });
                },
                child: Text('Open Page $i'),
              ),
            ),
        ],
      ),
    );
  }
}
Share:
302
Tom3652
Author by

Tom3652

Updated on December 30, 2022

Comments

  • Tom3652
    Tom3652 over 1 year

    Sorry for the big post in advance...

    My app is a social network that behaves closely to Instagram.

    PATTERN :

    1. User profile (with list of thumbnails, no videos played)
    2. Select photo (open a page with Navigator.push() with photos / playing videos
    3. Select another user profile (in the video's comments for example)
    4. See again list of thumbnails, then posts with photos - playing videos, and so on...

    This is like an infinite "profile - feed" loop. With such logic, i am reaching more or less 30 pages with Navigator.push() before i run into an OutOfMemory error.

    Flutter tools only says lost connection to device but the more i use the Navigator and the more laggy the app becomes and finally crashes, so i am 99% sure this is due to the memory usage.
    This happens at 100% of the time, at 1 page difference more or less due to the scrolling in the post list.

    The memory usage is increasing by 20MB for each page more or less if i don't scroll too much in the picture / video list.
    I already plan to make my images smaller but this does just delay the issue at best.

    QUESTIONS :

    • Is it possible to never run into an OutOfMemory exception with this kind of "infinite pages" ?
    • I know that there is a deactivate() method in StateFul Widgets that could be used after Navigator.push is called (dispose() is not called because we don't remove anything from the tree), maybe some work should be done there ?
    • Should i do something to handle myself the Navigator stack ? I don't want to pop() old pages as i need to go back until the first one opened

    In this situation and this logic it means that if i would go maybe through 100 pages in Instagram, this would also crash at 100%.

    I am not sure anyone goes that far anyway and maybe that's what they are counting on... If there is no way to prevent the OutOfMemory, the only solution might be to delay it until the user saw at least 100 pages for instance...

    WORKAROUND that i have found in theory but not sure that's even possible in code :

    The only solution i have think of until now is to allow the user to push() a certain amount of pages, and keep a 20 pages maximum in Navigator stack, but without removing the first page. So there would always be 20 pages in memory.

    If this is the only solution, can someone provide an example on how to deal with specific pages inside the Navigator stack please ?

    Also, i have noticed that the memory doesn't decrease when you come back with Navigator.pop(), why ?

    EDIT :

    In order to display photos / videos, i am using essentially 2 packages :

    I am also using Slivers in one part of the app but again with SliverStaggeredGrid.countBuilder from the previously mentioned package.