Dev Journal

  • Custom asset bundle for flutter golden tests

    Custom asset bundle for flutter golden tests

    Tests in a flutter project or game often require assets that you don’t want in the root asset bundle (rootBundle) -w specially if you are writing golden tests. Wouldn’t it be great if you could create a custom asset bundle for your tests?

    Today I ran in to this exact problem. I’ve written an image utility class that resizes images, and supports other transforms at the same time (like scale, rotation and crop). I need some image assets to test this class that I don’t want as part of the rootBundle. Flutter doesn’t currently support “dev” assets – see open issue #5813 – but luckily it is possible to create your own custom asset bundle.

    I based my asset bundle class on this one that a Google Flutter engineer posted in a thread on another issue. I modified it to work better for my purpose.

    Asset bundle class

    This is the code for the custom asset bundle:

    import 'dart:convert';
    import 'dart:io';
    
    import 'package:flutter/services.dart';
    import 'package:glob/glob.dart';
    import 'package:glob/list_local_fs.dart';
    
    /// A custom [AssetBundle] that reads files from a directory.
    ///
    /// This is meant to be used in place of [rootBundle] for testing
    class DiskAssetBundle extends CachingAssetBundle {
      static const _assetManifestDotJson = 'AssetManifest.json';
    
      /// Creates a [DiskAssetBundle] by loading files from [path].
      static Future<AssetBundle> loadFromPath(
        String path, {
        String? from,
      }) async {
        // Prepare the file search pattern
        path = _formatPath(path);
        String pattern = path;
        if (!pattern.endsWith('/')) {
          pattern += '/';
        }
        pattern += '**';
    
        // Load the assets
        final cache = <String, ByteData>{};
        await for (final entity in Glob(pattern).list(root: from)) {
          if (entity is File) {
            final bytes = await (entity as File).readAsBytes();
    
            // Keep only the asset name relative to the folder
            String name = _formatPath(entity.path);
            name = name.substring(name.indexOf(path) + path.length);
            cache[name] = ByteData.view(bytes.buffer);
          }
        }
    
        // Create the asset manifest
        final manifest = <String, List<String>>{};
        cache.forEach((key, _) {
          manifest[key] = [key];
        });
        cache[_assetManifestDotJson] = ByteData.view(
          Uint8List.fromList(jsonEncode(manifest).codeUnits).buffer,
        );
    
        return DiskAssetBundle._(cache);
      }
    
      /// Format a file path to only forward slashes
      static String _formatPath(String path) {
        return path.replaceAll(r'\', '/');
      }
    
      /// The cache of assets
      final Map<String, ByteData> _cache;
    
      /// Private constructor
      DiskAssetBundle._(this._cache);
    
      /// Load an asset from the cache
      @override
      Future<ByteData> load(String key) async {
        return _cache[key]!;
      }
    }
    Dart

    Writing a golden test

    This is how you use it for a golden test:

    import 'dart:io';
    import 'dart:ui';
    
    import 'package:flutter/services.dart';
    import 'package:flutter_test/flutter_test.dart';
    
    import 'disk_asset_bundle.dart';
    
    void main() async {
      // Path to the goldens folder (i.e. project/test/_goldens)
      String goldens =
        '${Directory.current.path.replaceAll(r'\', '/')}/test/_goldens';
    
      // This is required before an asset bundle is available
      TestWidgetsFlutterBinding.ensureInitialized();
    
      // Create the custom assets bundle from the resources folder
      // (i.e. project/test/_resources)
      AssetBundle assets = await DiskAssetBundle.loadFromPath('test/_resources/');
    
      group('Example', () {
    
        /// This test will compare an image to a golden file
        test('Does match golden', () async {
          final image = await assets.load('image.png');
    
          await expectLater(
            image,
            matchesGoldenFile('$goldens/image-no-transform.png'),
          );
        });
      });
    }
    Dart

    Some tips, especially if you are new to Flutter or dart testing:

    • A golden test is a comparison of a generated image against a golden image to ensure that the generated image is correct. Every pixel of the image has to exactly match the same pixel in the golden image.
    • After you have tested your code yourself and it is working as expected, you generate goldens. These are included in the repo. The golden tests then ensure that future changes do not affect how the images are generated.
    • To generate the golden images (just once):
      flutter test --update-goldens
    • To run the tests once you have generated goldens:
      flutter test
    • My file structure is like this:
      project root/
      test/
      _goldens/
      _resources/
      image.png
      example_test.dart
      disk_asset_bundle.dart

    Hopefully you can figure it out from here!

  • Object pooling in dart

    Object pooling in dart improves the memory performance of your Flutter app or game.

    When a large number of objects (such as bullets or enemies) are created over time in a Flutter game, dart has to do a lot of work allocating and then garbage collecting that memory.

    The garbage collector (GC) does not run continuously, so memory usage can increase dramatically between sweeps, resulting in high spikes of memory usage. Read more about dart’s garbage collection.

    Object pooling can help prevent fragmentation and reduce those peaks by re-using objects from a pool instead of creating and destroying them every time. This is especially useful on lower-memory devices such as smart watches or cheaper smart phones.

    Implementation of object pooling

    Here is a simple implementation of object pooling in dart. At it’s core, the pool is simply a list of the same type of object.

    // An object that is pooled should mix the Pooled mixin
    mixin Pooled {
      // Implement this to reset the object ready for re-use
      void reset();
    }
    
    // Object pool implementation
    class Pool<T extends Pooled> {
      // A list that holds the pooled objects
      final List<T> _pool = <T>[];
      // The method called to create a new pooled object
      final T Function() _creator;
    
      // Constructor
      Pool(this._creator);
    
      // Get an object from the pool, or create one if pool is empty
      T get() {
        if (_pool.isEmpty) {
          return _creator();
        } else {
          T obj = _pool.removeLast();
          obj.reset();
          return obj;
        }
      }
    
      // Add an object (back) to the pool
      void add(T obj) {
        _pool.add(obj);
      }
    
      // Clear the pool and release all the pooled objects for GC 
      void clear() {
        _pool.clear();
      }
    }
    Dart

    To use it, create a new pool for every different object type that you want to pool. Bullets are a good choice as they are often created and destroyed in large numbers.

    // Mix the Pooled mixin with the class that should be pooled
    class Bullet with Pooled {
      // Create a static pool, passing in the constructor of the object
      static Pool<Bullet> pool = Pool<Bullet>(Bullet.new);
      
      // These are some example properties on Bullet
      double damage = 1.0;
      double speed = 20.0;
      
      // Set the Bullet back to default values when re-used
      @override
      void reset() {
        damage = 1.0;
        speed = 20.0;
      }
    }
    Dart

    Then, whenever you need a new bullet get it from pool. Also remember to add it back to the pool once it is no longer needed.

    // Instead of this: Bullet b = Bullet();
    Bullet b = Bullet.pool.get();
    
    // Once the bullet is no longer required
    Bullet.pool.add(b);
    
    // When the game is over, free any pooled objects
    Bullet.pool.clear();
    Dart

    Here is the result – these are screengrabs of the memory profiler without and with object pooling enabled.

    Without object pooling, as many as 1000 bullets can remain in memory before the garbage collector sweep removed them.

    Memory usage without object pooling
    Memory usage without object pooling

    With object pooling enabled there are a maximum of 20 bullets in memory. Instead of creating new objects, recycled bullets are re-used from the pool.

    Memory usage with object pooling
    Memory usage with object pooling

  • FPS and battery life

    FPS and battery life

    , , ,

    I’ve been a fan of Flutter and Dart for some time. Recently I purchased a Wear OS smart watch and I’m interested in how well a Flutter game will perform on it.

    My watch is a Mobvoi Ticwatch Pro 5. It’s a beast, with 2GHz of Ram and a low-power 1.7GHz quad-core GPU. The display is a gorgeous 1.43″ 466*466 AMOLED screen. You can read the full specs here.

    My background is in game development for mobile – starting back in the day before iOS and Android existed – when devices had huge limitations in terms of CPU, ram, storage, network, screen size – you name it! I love working within limitations like these. They represent a special challenge for game design and development!

    I’m working on a prototype bullet-storm shooter with lots of bullets and particle effects to really try and see what the limitations of Flutter running on Wear OS are. I’m using a modified version of the Flame engine.

    Limiting the frame rate in Flame

    Flame works by calling an update function as often as possible. On desktop this tends to be around 120fps. On the Ticwatch it’s 60fps. To test the effect of different frame rates I modified the update function to limit the frame rate. Something like this:

    double _renderPeriod = 0.0;
    double _elapsedTime = 0.0;
    
    /// Set the maximum framerate for the game
    set maxFPS(double? fps) {
      if (fps == null) {
        _renderPeriod = 0.0;
        return;
      }
      _renderPeriod = 1.0 / fps;
    }
    
    /// Ensure the framerate doesn't exceed maximum
    @override
    void updateTree(double dt) {
      _elapsedTime += dt;
      if (_elapsedTime >= _renderPeriod) {
        super.updateTree(_elapsedTime);
        _elapsedTime = 0.0;
      }
    }
    Dart

    Testing the effects of different frame rates

    To perform the tests I repeated these steps:

    1. Charge the watch to 100%
    2. Set the framerate
    3. Run the game for exactly 30 minutes
    4. Check the battery level

    Here are the results:

    FPSBattery drain
    6011%
    3011%
    911%
    Effects on battery drain of changing FPS

    Analysis of results

    Changing the frame rate made no difference to the battery drain.

    I was initially surprised, but then I quickly realised that this made sense. My testing is flawed.

    My tests are not changing the fundamental framerate of the app. Under the hood, graphics are still being rendered to the screen at the same rate.

    It’s just my higher level update function that is being limited. It performs physics calculations and moves objects around, but it’s not responsible for the actual rendering. So while the animation is choppy and jumpy, the rendering is still happening at the same rate.

    Here’s an image to illustrate. It shows 10 frames being drawn while the ship is rotating:

    As you can see:

    • In both cases, the underlying code is updating the frames at the same rate
    • But when the fps is being limited (incorrectly), only the animation (rotation) is being limited.

    However, this next image shows what the intended effect of reducing frame rate should be:

    So back to the drawing board in this one.