mirror of
https://github.com/rive-app/rive-flutter.git
synced 2025-12-12 04:08:05 +08:00
fix(flutter): make RiveWidgetBuilder file loading cancellable (#11190) 9e48626a96
* fix(flutter): make RiveWidgetBuilder file loading cancellable * docs: update CHANGELOG Update the versions of the spir-v shaders to all be 460 (#11209) f38d54d8e4 Some tooling changed today-ish that is causing the 310 es shaders to fail to build with: ``` runner/_work/rive/rive/packages/runtime/tests/unit_tests/out/android_arm64_debug/include/generated/shaders/glsl.minified.glsl:450: 'token pasting (##)' : not supported with this profile: es ERROR: spirv/blit_texture_as_draw_filtered.main:6: 'stringify (#)' : not supported with this profile: es ERROR: spirv/blit_texture_as_draw_filtered.main:6: '#' : '#' is not followed by a macro parameter. ERROR: spirv/blit_texture_as_draw_filtered.main:6: '' : missing #endif ERROR: spirv/blit_texture_as_draw_filtered.main:6: '' : compilation terminated ERROR: 5 compilation errors. No code generated. ``` This is only a "310 es" issue, and so since most spir-v shaders are already 460, this moves the rest to be the same. fix: list updates correctly when items are added or removed (#11206) 255f1ba3d2 fix: ignore non visible clips when removing redundant operations (#11207) 673820da1a update scripted drawable clipping to new strategy and remove unused function (#11195) 8002b223a3 * update clipping to new strategy fix(scripting): store data on all cases (#11197) 7fae9fb6c8 fix(EA): list index can be compared to numbers (#11194) e2f7d6cde1 feature: apply clippings as separate drawables (#11183) c3237c0283 * feature: apply clippings as separate drawables chore(scripting): add clipping support to scripted drawables (#11184) 27085dfb0c fix(editor): Display runtime errors when Script input VM properties not set (#11177) cd70e30236 Fixes a crash when trying to use pointer events when a ViewModel property input is not setup properly on a scripted drawable instance. The code was checking for ScriptAsset.vm which will always be non-null when bytecode is compiled in the editor. We should be checking instead for ScriptedObject.state because that will only be non-null if the ScriptedObject has validated (and we early out if that is null so we don't crash). Also added some errors when VM properties are not setup correctly as Script inputs so users will be aware that there is an issue with their VM setup or bindings chore: Refactor invalidateEffect for StrokeEffects (#11173) 05630acdb1 Refactors how invalidateEffect in StrokeEffects work. Now there are 2 code paths: invalidateEffectsFromLocal() - this is called from the StrokeEffect itself. It calls out to ShapePaint::invalidateEffects to let it invalidate any other StrokeEffects that may need to do so. It also calls invalidateEffect on itself. invalidateEffects() - this can be called from the StrokeEffect (via invalidateEffectsFromLocal) on itself OR by ShapePaint while in the process of determining if any other StrokeEffects should be invalidated via the call above. This adds paint dirt to the StrokeEffect along with any additional work needed when invalidating. This prevents invalidateEffect from calling ShapePaint::invalidateEffects multiple times. This also resolves an issue with ScriptedPathEffects in Components chore: complete scripted converters implementation (#11166) 55853117b2 chore: finish scripted converters implementation chore: Preprocess transitions conditions on initialization (#11150) bb52cbd4a3 Preprocess transitions conditions on initialization fix: reset interpolator and initialize it on convert (#11157) 0791ee519d * fix: reset interpolator and initialize it on convert feature: goto definition (#11143) 4f005f715e chore: updating to luau_701 (#11142) d4dfc63c3a fix(vk): Implement manual MSAA resolves (#11120) 756dc2db91 * fix(vk): Implement manual MSAA resolves Some Android devices have issues with MSAA resolves when the MSAA color buffer is also read as an input attachment. In the past we've worked around this by adding an empty subpass at the end of the render pass. This PR implements fully manual resolves instead, which we now use when there are blend modes and partial updates. This is hopefully a more stable workaround than a mystery subpass, and will ideally get better performance as well when we don't need to resolve the entire render target. * Fix synchronization validation (had a write/write hazard between the image state transition and the load op) * fix(vk): Implement manual MSAA resolves Some Android devices have issues with MSAA resolves when the MSAA color buffer is also read as an input attachment. In the past we've worked around this by adding an empty subpass at the end of the render pass. This PR implements fully manual resolves instead, which we now use when there are blend modes and partial updates. This is hopefully a more stable workaround than a mystery subpass, and will ideally get better performance as well when we don't need to resolve the entire render target. * Fix synchronization validation (had a write/write hazard between the image state transition and the load op) feature: add path effects for fills (#11136) 376052977b feature: add support for path effects for fills fix(vk): Only rely on implicit PowerVR raster ordering on Vulkan 1.3 (#11132) 4cdb5779cd After testing a statistically significant number of devices (7, specifically!) I have concluded that implicit PowerVR raster ordering only works with Rive on Vulkan 1.3 contexts. Update our renderer accordingly. Update path_fiddle.cpp (#11123) 19be344a5a * Update path_fiddle.cpp * clang format fix(Vulkan) Vulkan synchronization fixes (#11091) 810e208837 This gets (non-atomic) Vulkan rendering to pass the Vulkan synchronization validation layer tests. Nnnnn add support for path effect part 3 (#11107) c27b081319 * WIP * ui * fix errors * remove duplicate code * remove empty switch case * make pathData return required * add web ffi path methods fix: rename Node to NodeData (#11110) c46192f1fe add support for path effect (#11095) ab13b4d1a2 * add support for stacked path effects chore: rename vec2d to vector in scripts (#11097) 4ad14fbe73 * chore: rename vec2d to vector in scripts * chore: replace comments * chore: fix formatting * chore: formatting * fix: bad definition * fix: pointer event chore: Runtime Scripting fixes & tests (#11094) 539bd8c48c - Call reinit on ScriptedObjects in Artboard::initialize as discussed. We still need to reinit in Artboard::internalDataContext, because things like ScriptInputViewModelProperty depends on the ScriptedObject having a datacontext, which we don't necessarily have at initialize time. - Always run the setup code in ScriptedObject::scriptInit, but only run the init function if we verified its implementation. - Added some tests. Still need to add more comprehensive tests once we can get rivs exporting with bytecode. feature: autocomplete requires! (#11090) 6bd796b5f0 * feature: autocomplete requires! * chore: cleanup fix(vk): Never read the resolve attachment (#11081) 2430b66647 When Vulkan expands the renderTarget into the MSAA color buffer for LoadAction::preserveRenderTarget, we've been reading the resolve texture as an input attachment. But it's debatable whether a texture can be an input attachment AND a resolve attachment in the same render pass, and some early Qualcomm devices have struggled with this even if we implement the MSAA resolve manually. For now, always copy out the render target to a separate texture when there's a preserve. chore(scripting): rename scripting (#11084) 85b425bf93 * chore(scripting): make init and draw optional * chore(scripting): rename ScriptType to ScriptProtocol and none to utility * rename Drawing to Node Peon Worker Script Signing (#11063) 8748f53562 * feature: signing and compiling via ffi * feature: script signing * fix ups * Fix tests feature: Script signing (#11016) 9295f20b82 * feature: signing and compiling via ffi * feature: script signing * fix: missed with rive scripting * chore: more fixes * fix: rive_native wasm * fix: missed dispose call for the workspace * feature: optional signing * chore: core collision * feature: doing actual verification * fix: signature verification at runtime * chore: missed files * fix: switch order * fix: with rive scripting flag * fix: core def collisions * chore: unifying enrich assets * chore: remove unnecessary type check * fix: signing context fix(vk): Use rasterOrdering mode on Imagination GPUs (#11072) 69b2a3c643 We already have a codepath that enables rasterOrdering on ARM, even if the extension isn't present, because we know that's how these GPUs work. If we enable this codepath on Imagination, it appears to work as well. This works around an MSAA crash on Pixel 10. Co-authored-by: Gordon <pggordonhayes@gmail.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
2aa57b3d8acfb1ede538d3cc0212e6ce51d01c57
|
||||
9e48626a963ceb704d22308692991b07a5f19497
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
### Fixes
|
||||
|
||||
- Fixed Android build issues in certain environments. See issue [555](https://github.com/rive-app/rive-flutter/issues/555). Build commands are now executed from Gradle instead of CMakeLists, and the setup process automatically skips `rive_native:setup` if the required libraries are already downloaded.
|
||||
- Fixed [570](https://github.com/rive-app/rive-flutter/issues/570) - `setState` called after dispose. Add cancellation checks to prevent stale state updates.
|
||||
|
||||
### Build & Platform Updates
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
|
||||
@@ -78,6 +80,7 @@ class RiveWidgetBuilder extends StatefulWidget {
|
||||
class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
|
||||
RiveState _state = RiveLoading();
|
||||
late File _file;
|
||||
Future<void>? _currentSetup;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -98,10 +101,39 @@ class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
|
||||
}
|
||||
|
||||
Future<void> _setup({required bool withFileLoad}) async {
|
||||
final completer = Completer<void>();
|
||||
_currentSetup = completer.future;
|
||||
|
||||
try {
|
||||
await _setupImpl(completer.future, withFileLoad: withFileLoad);
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete();
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(e, stackTrace);
|
||||
}
|
||||
} finally {
|
||||
// Only clear if this is still the current setup
|
||||
if (identical(_currentSetup, completer.future)) {
|
||||
_currentSetup = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupImpl(
|
||||
Future<void> thisSetup, {
|
||||
required bool withFileLoad,
|
||||
}) async {
|
||||
try {
|
||||
if (withFileLoad) {
|
||||
_file = await widget.fileLoader.file();
|
||||
// Check if this operation was cancelled or the widget was disposed
|
||||
if (!mounted || !identical(_currentSetup, thisSetup)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final controllerBuilder = widget.controller;
|
||||
final controller = controllerBuilder != null
|
||||
? controllerBuilder(_file)
|
||||
@@ -117,6 +149,13 @@ class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
|
||||
vmi = controller.dataBind(dataBind);
|
||||
}
|
||||
|
||||
// Check if this operation was cancelled or the widget was disposed
|
||||
if (!mounted || !identical(_currentSetup, thisSetup)) {
|
||||
controller.dispose();
|
||||
vmi?.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_state = RiveLoaded(
|
||||
file: _file,
|
||||
@@ -126,6 +165,10 @@ class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
|
||||
});
|
||||
widget.onLoaded?.call(_state as RiveLoaded);
|
||||
} on Exception catch (e, stackTrace) {
|
||||
// Check if this operation was cancelled or the widget was disposed
|
||||
if (!mounted || !identical(_currentSetup, thisSetup)) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_state = RiveFailed(e, stackTrace);
|
||||
});
|
||||
|
||||
200
test/widget_builder_test.dart
Normal file
200
test/widget_builder_test.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
|
||||
void main() {
|
||||
late File riveFile;
|
||||
|
||||
setUp(() async {
|
||||
final file = io.File('test/assets/rive_file_controller_test.riv');
|
||||
riveFile = await File.decode(
|
||||
await file.readAsBytes(),
|
||||
riveFactory: Factory.flutter,
|
||||
) as File;
|
||||
});
|
||||
|
||||
testWidgets('RiveWidgetBuilder cancels previous setup when new one starts',
|
||||
(WidgetTester tester) async {
|
||||
// Track state changes from each file loader
|
||||
RiveLoaded? stateFromFirstLoader;
|
||||
RiveLoaded? stateFromSecondLoader;
|
||||
|
||||
// Create a delayed file loader for the first setup
|
||||
final delayedFileLoader1 = _DelayedFileLoader(
|
||||
riveFile,
|
||||
delay: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
// Create a faster file loader for the second setup
|
||||
final fastFileLoader2 = _DelayedFileLoader(
|
||||
riveFile,
|
||||
delay: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
// Track the final loaded state
|
||||
RiveLoaded? finalLoadedState;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: _TestWidget(
|
||||
initialFileLoader: delayedFileLoader1,
|
||||
onStateChanged: (state) {
|
||||
if (state is RiveLoaded) {
|
||||
stateFromFirstLoader = state;
|
||||
finalLoadedState = state;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Start the first setup
|
||||
await tester.pump();
|
||||
|
||||
// Wait a bit to ensure first setup has started loading
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
|
||||
// Change to the second file loader - this should cancel the first setup
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: _TestWidget(
|
||||
initialFileLoader: fastFileLoader2,
|
||||
onStateChanged: (state) {
|
||||
if (state is RiveLoaded) {
|
||||
stateFromSecondLoader = state;
|
||||
finalLoadedState = state;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Wait for the second setup to complete
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Wait a bit more to ensure first setup would have completed if not cancelled
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
// Verify that the second setup completed and is the final state
|
||||
expect(finalLoadedState, isNotNull);
|
||||
expect(stateFromSecondLoader, isNotNull);
|
||||
expect(identical(finalLoadedState, stateFromSecondLoader), isTrue);
|
||||
|
||||
// Verify that the first setup was cancelled - it should not have
|
||||
// updated the state (stateFromFirstLoader should be null or different)
|
||||
// The first file loader may complete, but the setup should be cancelled
|
||||
// before calling setState
|
||||
if (stateFromFirstLoader != null) {
|
||||
// If the first loader did create a state, it should not be the final one
|
||||
expect(identical(finalLoadedState, stateFromFirstLoader), isFalse);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
delayedFileLoader1.dispose();
|
||||
fastFileLoader2.dispose();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'RiveWidgetBuilder cancels setup when widget is disposed during loading',
|
||||
(WidgetTester tester) async {
|
||||
bool stateUpdatedAfterDisposal = false;
|
||||
|
||||
final delayedFileLoader = _DelayedFileLoader(
|
||||
riveFile,
|
||||
delay: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: _TestWidget(
|
||||
initialFileLoader: delayedFileLoader,
|
||||
onStateChanged: (state) {
|
||||
if (state is RiveLoaded) {
|
||||
stateUpdatedAfterDisposal = true;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Start loading
|
||||
await tester.pump();
|
||||
|
||||
// Dispose the widget before loading completes
|
||||
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
|
||||
|
||||
// Wait long enough for the delayed file loader to complete
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
// The file loader may complete, but the setup should be cancelled
|
||||
// and not update the state after disposal (mounted check should prevent setState)
|
||||
// Note: We can't directly verify this without accessing internal state,
|
||||
// but the fact that the widget was disposed means mounted should be false
|
||||
expect(stateUpdatedAfterDisposal, isFalse);
|
||||
|
||||
delayedFileLoader.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/// A file loader that introduces a delay before loading the file.
|
||||
/// This is used to test cancellation behavior.
|
||||
class _DelayedFileLoader extends FileLoader {
|
||||
final File _file;
|
||||
final Duration delay;
|
||||
|
||||
_DelayedFileLoader(
|
||||
this._file, {
|
||||
required this.delay,
|
||||
}) : super.fromFile(_file, riveFactory: Factory.flutter);
|
||||
|
||||
@override
|
||||
Future<File> file() async {
|
||||
await Future.delayed(delay);
|
||||
return _file;
|
||||
}
|
||||
}
|
||||
|
||||
class _TestWidget extends StatefulWidget {
|
||||
final FileLoader initialFileLoader;
|
||||
final void Function(RiveState state) onStateChanged;
|
||||
|
||||
const _TestWidget({
|
||||
required this.initialFileLoader,
|
||||
required this.onStateChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_TestWidget> createState() => _TestWidgetState();
|
||||
}
|
||||
|
||||
class _TestWidgetState extends State<_TestWidget> {
|
||||
late FileLoader _fileLoader;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fileLoader = widget.initialFileLoader;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fileLoader.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RiveWidgetBuilder(
|
||||
fileLoader: _fileLoader,
|
||||
builder: (context, state) {
|
||||
// Notify about state changes
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onStateChanged(state);
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user