Search code examples
flutterflutter-futurebuilder

How can I load Flutter text assets from a build context without reloading each build?


I'm confused about the Flutter documentation on loading text assets.

It states:

Each Flutter app has a rootBundle object for easy access to the main asset bundle. It is possible to load assets directly using the rootBundle global static from package:flutter/services.dart.

However, it’s recommended to obtain the AssetBundle for the current BuildContext using DefaultAssetBundle, rather than the default asset bundle that was built with the app; this approach enables a parent widget to substitute a different AssetBundle at run time, which can be useful for localization or testing scenarios.

I don't understand how I can implement text asset loading the recommended way, while at the same time avoiding to load the assets each time the widget is built.

Consider the following naive example that uses a FutureBuilder to display some text:

@override
Widget build(BuildContext context) {
  var greeting = DefaultAssetBundle.of(context).loadString('assets/greeting');
  return FutureBuilder<String>(
    future: greeting,
    builder (BuildContext context, AsyncSnapshot<String> snapshot) {
      if (snapshot.hasData) {
        return Text(snapshot.requireData);
      } else {
        return Text('Loading greeting...');
      }
    }
  );
}

In the example, loadString is called whenever the widget is built. This seems inefficient to me.

Also, it goes explicitly against the FutureBuilder documentation, which tells me that:

The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.

Now, I could go ahead and load my assets in any of the recommended methods, but none of them has a BuildContext. Meaning I'd have to use the rootBundle, which wasn't recommended.

Since I'm new to Flutter, I'm unsure if the documentation is contradicting itself or if there's some obvious thing I'm missing here. Any clarification would be much appreciated.


Solution

  • I came up with the following solution for loading assets the way it's recommended in the Flutter docs.

    Load the assets in a Widget's State and assign the Future to a nullable instance variable. This works in conjunction with FutureBuilder. Here's an example:

    class MyWidgetState extends State<MyWidget> {
    
      Future<String>? _greeting;
    
      @override
      Widget build(BuildContext context) {
        // Wrapping `loadString` in a condition such as the following ensures that
        // the asset is loaded no more than once. Seems kinda crude, though.
        if (_greeting == null) {
          _greeting = DefaultAssetBundle.of(context).loadString('assets/greeting');
        }
    
        return FutureBuilder<String>(
            future: greeting,
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.requireData);
              } else {
                return Text('Loading greeting...');
              }
            });
      }
    }
    

    This approach ensures that both recommendations from the Flutter docs are honored:

    • Assets are loaded from the Bundle for the current BuildContext.
    • Assets are loaded only once, avoiding that FutureBuilder restarts each build.