Repository navigation
rive_native: standalone Factory.decodeImage() entries block unrelated File.decode() calls (hangs after a test ends) #658
Description
Summary
With the Flutter software renderer (Factory.flutter), a standalone Factory.decodeImage() call makes every later
File.decode() wait for that image, even though the two are unrelated. If that image decode never completes, which
happens whenever it was started in a Flutter widget test's fake-async zone that has since ended, every later Rive file
decode in the isolate hangs forever.
We hit this in a large widget-test suite where many test files run in one isolate: one test that decoded images for
data binding and then ended made unrelated Rive file loads in later tests time out.
Cause
In lib/src/ffi/flutter_renderer_ffi.dart:
FFIFlutterFactory.decodeImage()adds itsFFIFlutterFactoryImageto the static_loadingImageslist.completedDecodingFile()snapshots_loadingImages, clears it, and awaits every entry.
_loadingImages exists so a file decode can wait for the images embedded in that file. Because standalone
decodeImage() calls use the same list, a file decode also awaits images that belong to unrelated callers, possibly
from long before. Awaiting a future resumes in the zone that created it, so an entry created in a zone that no longer
runs (a finished fake-async test zone) blocks the file decode permanently.
The same code is present in rive_native 0.1.11 and 0.2.0-dev.2.
Reproduction
rive 0.14.11, rive_native 0.1.11, Flutter 3.47.5, macOS (flutter test). Any small .riv file works; we used a
121-byte placeholder.
import 'dart:io' as io;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rive/rive.dart';
Future<Uint8List> _png() async {
final recorder = ui.PictureRecorder();
ui.Canvas(recorder).drawRect(const Rect.fromLTWH(0, 0, 4, 4), ui.Paint()..color = const Color(0xFFFF0000));
final image = await recorder.endRecording().toImage(4, 4);
return (await image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List();
}
void main() {
late Uint8List png;
setUpAll(() async {
await RiveNative.init();
png = await _png();
});
testWidgets('1: a standalone image decode is started and the test ends', (tester) async {
// Not awaited: its completion needs real time, which this fake-async test never gives it.
Factory.flutter.decodeImage(png);
});
testWidgets('2: a later, unrelated file decode never completes', (tester) async {
final bytes = io.File('path/to/any.riv').readAsBytesSync();
final file = await tester.runAsync(
() => File.decode(bytes, riveFactory: Factory.flutter).timeout(const Duration(seconds: 10)),
);
expect(file, isNotNull);
});
}- Test 2 run on its own passes immediately.
- Run after test 1, test 2 fails with
TimeoutException after 0:00:10.000000: Future not completed, even though it
decodes insiderunAsyncin the real zone.
Expected
A file decode waits only for the images embedded in that file. Standalone decodeImage() calls already return their
own future to their caller, so they should not join the list that file decodes await.
Possible fix
Do not add standalone decodeImage() results to _loadingImages, or have each file decode track only the images
created by _decodeImageFromNative while it runs.
Workaround
In tests, we decode a tiny placeholder .riv right after each decodeImage() call in the same zone. Because
completedDecodingFile() empties the list at the start of every file decode, this hands the entry to a decode owned by
the same test.
Activity
HayesGordon commentedon Oct 19, 2026
Thanks for the report. Confirmed and a fix is in flight, a
File.decode()now waits only for images decoded during its own load, so standalonedecodeImage()calls no longer join it.