跳到主要内容

迁移指南

了解在升级 Rive Flutter 运行时主要版本时如何迁移你的 Flutter 应用,包括破坏性变更和新功能。

版本变更内容
0.15.0创建控制器时即完成数据绑定;Rive 渲染器在原生平台使用延迟渲染(deferred rendering)
0.14.0Rive 用 C++ 运行时替换 Dart 运行时,组件、控制器和文件加载 API 随之变化

版本 0.15.0​

此版本有两处需要迁移:

  • 创建 RiveWidgetController 时即完成数据绑定,单独的 dataBind 调用已移除。
  • Rive 渲染器(Factory.rive)在原生平台使用延迟渲染。这只影响高级集成。

0.15.0 是开发版本(dev release,当前为 0.15.0-dev.2)。请按下方 API 迁移,但在稳定版发布前仍可能有变化。

要求​

最低 Flutter 版本为 3.32.0(Dart 3.8.0)。0.14.x 曾误将要求声明为 3.28.0,因此更旧的 Flutter 版本能解析到该包,随后编译失败。参见 rive-flutter issue #643。

数据绑定​

构造时即绑定​

RiveWidgetController 在构造时完成绑定。除非你通过新的 main 和 globals 参数另行指定,画板的主 View Model 以及文件中每一个全局 View Model 都会获得默认实例:

// Default instances for the main and every global View Model
final controller = RiveWidgetController(file);

// Or choose what gets bound
final controller = RiveWidgetController(
file,
main: DataBind.byName('My Instance'),
globals: {'Theme': DataBind.byInstance(themeInstance)},
);

对不含数据绑定的内容,此次绑定是空操作(no-op)。

⚠️

行为变化:以前以未绑定状态渲染的、带有 View Model 的内容,现在会渲染其在编辑器中编写的默认值,因为构造时会自动绑定。

通过 controller.viewModelInstance 和 controller.globalViewModelInstance(name) 读回已绑定实例。两者都是实时读取,每次返回同一个对象。

之后若要重新绑定,调用 controller.bind(main: ..., globals: {...})。每次调用都是对当前绑定的增量更新。未写出的槽位会保留已有实例;无论你设置多少个槽位,重新绑定只执行一次。若 bind 在控制器首次 advance 之前运行,debug 构建会警告一次,因为构造时的绑定会在任何内容渲染前被丢弃。请改为把配置传给构造函数。

已移除的 API​

已移除替换为
controller.dataBind(x)RiveWidgetController(file, main: x),通过 controller.viewModelInstance 读回
RiveWidgetBuilder(dataBind: DataBind.auto())删除该参数。构造时会自动绑定默认值
RiveWidgetBuilder(dataBind: DataBind.byInstance(x))controller: (file) => RiveWidgetController(file, main: DataBind.byInstance(x))
state.viewModelInstance(在 RiveLoaded 上)state.controller.viewModelInstance

新 API​

final controller = RiveWidgetController(
file,
main: DataBind.byName('My Instance'),
);
final viewModelInstance = controller.viewModelInstance;

旧 API​

final controller = RiveWidgetController(file);
final viewModelInstance = controller.dataBind(
DataBind.byName('My Instance'),
);

旧版桥接​

若要在迁移期间让现有调用点继续编译:

import 'package:rive/legacy.dart';

这会以已弃用的扩展形式恢复 controller.dataBind 和 RiveLoaded.viewModelInstance。旧的所有权约定一并回来,因此你需要 dispose dataBind 的返回值。该库已冻结,将在未来版本中移除,请把这次 import 当作迁移 TODO。

实例所有权​

控制器拥有它为你解析的每一个实例(DataBind.auto、byName、byIndex 和 empty),并在自身 dispose 时一并释放它们。请去掉对这些实例的手动 dispose。

你自行创建并通过 DataBind.byInstance 传入的实例,运行时永远不会 dispose。RiveWidgetBuilder 也不再 dispose 它们,因此调用方拥有的实例现在会在组件销毁后继续存活。若你曾让某个实例在其控制器之后继续使用,请自己创建它并通过 instance 绑定:

final vm = file.viewModelByName('My View Model')!;
final instance = vm.createInstanceByName('My Instance')!;
final controller = RiveWidgetController(
file,
main: DataBind.byInstance(instance),
);

读回结果不能再次绑定​

controller.viewModelInstance 和 controller.globalViewModelInstance(name) 返回普通的 ViewModelInstance 视图。可以照常读取、写入和监听,但不能把它传给 DataBind.byInstance。后者只接受 BindableViewModelInstance,也就是 ViewModel.createInstance 及其同类方法的返回值。传入读回结果是编译错误;强制转换会在运行时抛出。若要在多个槽位或控制器之间共享状态,请创建你自己的实例。

延迟渲染​

Rive 渲染器(Factory.rive)现在在原生平台使用延迟渲染,并且这是唯一的原生渲染模式。每一帧都在 UI 线程上录制为紧凑的命令流,再在专用渲染线程上回放,因此 GPU 工作不再阻塞 UI 线程。用 Factory.rive 创建的资源(path、paint、image、text)是轻量录制代理,在回放时才真正解析。

对典型的 RiveWidget、RivePanel 以及基于文件的用法,渲染行为和输出没有变化。以下条目只影响高级集成。Factory.flutter 不受影响。

线程​

所有 Rive 调用都留在调用方(UI)线程。命令流每帧只有一个写入者,因此不支持从其他线程 advance 或绘制。这样做会损坏命令流,控制台会看到 rive replay ABORT 消息。

已移除批量 advance API​

破坏性变更: Rive.batchAdvance 和 Rive.batchAdvanceAndRender 已移除,连同持有它们的 Rive 类。它们的工作线程会并发写入单写入者命令流并损坏它。请改为在调用线程上逐个 advance 并绘制每个 State Machine:

// Old API
Rive.batchAdvance(stateMachines, elapsedSeconds);

// New API
for (final stateMachine in stateMachines) {
stateMachine.advanceAndApply(elapsedSeconds);
}

对于 Rive.batchAdvanceAndRender,请像以前一样,在调用线程上 advance 对应的 State Machine 后绘制每个 Artboard。

自定义渲染纹理绘制器​

RenderTexturePainter.riveFactory 指明所绘制内容使用的工厂,以便纹理可以挂上其录制会话。没有会话的纹理在原生平台上不会绘制任何内容,并会一次性打印控制台警告:texture has no deferred session, nothing will draw。

该 getter 默认为 Factory.rive,对在其上解码和创建的内容是正确的,因此大多数自定义绘制器无需改动。当内容在其他地方录制时,用已解码文件的 File.riveFactory 覆盖它;若你自行管理纹理会话,则覆盖为 null。在 Web 上,会话按文件绑定到一块纹理。因此由内容推导出的覆盖只适用于单文件内容,此时它会让纹理走按文件的会话 worker 路径。把多个文件绘制到同一块纹理的绘制器应保持默认,并通过即时回退路径渲染。

SharedTexturePainter 同样增加了 riveFactory getter(默认 null),用于 RivePanel 的绘制过程。实现 SharedTexturePainter 的类必须添加该 getter。继承它的类会获得默认实现。

每块纹理一个 Artboard 实例​

在两个 RiveWidget 中显示同一个 Artboard 实例,会使后一个组件空白并打印 debug 消息。请为每个组件创建一个 Artboard 实例,例如对每个组件调用 file.artboard('MyArtboard')。

Factory.rive 与录制会话​

在原生平台上,Factory.rive 解析为渲染上下文的录制会话。file.riveFactory == Factory.rive 仍然成立,直接在 Factory.rive 上创建的资源也会正确录制和渲染。

高级会话控制​

package:rive_native/rive_deferred.dart 提供手动会话控制:通过 RenderTexture.useDeferredSession 附加或分离纹理会话、会话辅助方法,以及渲染线程统计。典型应用不需要它。

版本 0.14.0​

这是 Rive Flutter 的一次重大更新。我们完全移除了之前用于 Rive 运行时的所有 Dart 代码,并用我们的底层 C++ 运行时取而代之。请参见 Flutter 版 Rive Native 页面了解更多详情。

这导致了底层 API 的许多变更,之前可通过 Dart 访问的大部分代码库现在通过 FFI 在 C++ 中实现。

0.14.0 的新变化​

此版本的 Rive Flutter 增加了对以下内容的支持:

现在 Rive Flutter 使用了核心 Rive C++ 运行时,你可以期待新的 Rive 功能更快地在 Rive Flutter 中得到支持。

你所有的 Rive 图形仍然会像以前一样显示和运行。

要求​

Dart 和 Flutter 版本​

此版本将版本要求提升至:

sdk: ">=3.5.0 <4.0.0"
flutter: ">=3.3.0"

必需的设置​

重要提示: 你必须在应用启动时或使用 Rive 之前调用 RiveNative.init。例如,在 main.dart 中:

import 'package:rive/rive.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RiveNative.init(); // 在使用 Rive 之前调用 init
runApp(const MyApp());
}

迁移指南​

快速迁移检查清单​

  1. ✅ 更新 pubspec.yaml 依赖为 0.14.0 或更高版本
    dependencies:
    rive: ^0.15.0-dev.1
  2. ✅ 在 main() 函数中添加 RiveNative.init(),或在使用 Rive 之前调用。
  3. ✅ 将 Rive 和 RiveAnimation 组件替换为 RiveWidget 或 RiveWidgetBuilder
  4. ✅ 更新控制器以使用新的 API,参见 RiveWidgetController
  5. ✅ 审查并更新任何自定义资源加载代码
  6. ✅ 测试你的图形和交互

已移除的类​

以下类已完全移除:

  • Rive 和 RiveAnimation 组件 → 使用 RiveWidget 和 RiveWidgetBuilder
  • RiveAnimationController 及其子类 → 使用 RiveWidgetController、SingleAnimationPainter 和 StateMachinePainter
  • OneShotAnimation 和 SimpleAnimation → 使用 SingleAnimationPainter 播放单个动画
  • StateMachineController → 改用 StateMachine(可通过 RiveWidgetController.stateMachine 访问)
  • RiveEvent → 替换为 Event
  • SMITrigger → 替换为 TriggerInput
  • SMIBool → 替换为 BooleanInput
  • SMINumber → 替换为 NumberInput
  • FileAssetLoader → 替换为创建 File 时的可选回调

加载 Rive 文件​

RiveFile 已移除,替换为 File。重要变更:

新 API​

final file = await File.decode(bytes, factory: Factory.rive);
final artboard = file.defaultArtboard();
final artboard = file.artboard('MyArtboard');

旧 API​

final file = await RiveFile.import(bytes);
final artboard = file.mainArtboard;
final artboard = file.artboardByName('MyArtboard');

提供的 Factory 决定了将使用的渲染器。使用 Factory.rive 使用 Rive 渲染器,或使用 Factory.flutter 使用内置的 Flutter 渲染器(Skia 或 Impeller)。

⚠️ 警告:矢量羽化仅适用于 Rive 渲染器。

关键变更:

  • 创建 Rive File 现在需要工厂参数(Factory.rive 或 Factory.flutter)
  • 将 RiveFile.import 替换为 File.decode(),返回 Future<File>
  • 将 mainArtboard 替换为 defaultArtboard()
  • 将 artboardByName(name) 替换为 artboard(name)
  • 将 RiveFile.network 替换为 File.url
  • 将 RiveFile.file 替换为 File.path

组件迁移​

参见更新的示例应用以获取完整的迁移指南,包括如何使用新的 RiveWidget 和 RiveWidgetBuilder API。

旧组件新组件备注
Rive/RiveAnimationRiveWidget/RiveWidgetBuilder直接替换

新 API - 选项 1​

class SimpleAssetAnimation extends StatefulWidget {
const SimpleAssetAnimation({Key? key}) : super(key: key);

@override
State<SimpleAssetAnimation> createState() => _SimpleAssetAnimationState();
}

class _SimpleAssetAnimationState extends State<SimpleAssetAnimation> {
late final fileLoader = FileLoader.fromAsset(
'assets/off_road_car.riv',
riveFactory: Factory.rive,
);

@override
void dispose() {
fileLoader.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: Center(
child: RiveWidgetBuilder(
fileLoader: fileLoader,
builder: (context, state) => switch (state) {
RiveLoading() => const CircularProgressIndicator(),
RiveFailed() => Text('Failed to load: ${state.error}'),
RiveLoaded() => RiveWidget(
controller: state.controller,
fit: Fit.cover,
),
},
),
),
);
}
}

新 API - 选项 2​

class SimpleAssetAnimation extends StatefulWidget {
const SimpleAssetAnimation({Key? key}) : super(key: key);

@override
State<SimpleAssetAnimation> createState() => _SimpleAssetAnimationState();
}

class _SimpleAssetAnimationState extends State<SimpleAssetAnimation> {
File? file;
RiveWidgetController? controller;
bool isInitialized = false;

@override
void initState() {
super.initState();
initRive();
}

void initRive() async {
file = (await File.asset('assets/off_road_car.riv', riveFactory: Factory.rive))!;
controller = RiveWidgetController(file!);
setState(() => isInitialized = true);
}

@override
void dispose() {
controller?.dispose();
file?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: Center(
child: isInitialized && controller != null
? RiveWidget(
controller: controller!,
fit: Fit.cover,
)
: const CircularProgressIndicator(),
),
);
}
}

旧 API​

class SimpleAssetAnimation extends StatelessWidget {
const SimpleAssetAnimation({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: const Center(
child: RiveAnimation.asset(
'assets/off_road_car.riv',
fit: BoxFit.cover,
),
),
);
}
}

控制器迁移​

旧控制器新控制器备注
RiveAnimationControllerRiveWidgetController组件的主控制器
StateMachineControllerStateMachine直接访问状态机
OneShotAnimation 和 SimpleAnimationSingleAnimationPainter用于单个动画

使用新的 RiveWidgetController 的示例:

final file = await File.asset('assets/off_road_car.riv', riveFactory: Factory.rive);
final controller = RiveWidgetController(file!);
final artboard = controller.artboard; // 访问已加载的画板
final viewModelInstance = controller.dataBind(DataBind.auto()); // 自动数据绑定

可选择指定要使用的画板和状态机:

final file = await File.asset('assets/off_road_car.riv', riveFactory: Factory.rive);
final controller = RiveWidgetController(
file,
artboardSelector: ArtboardSelector.byName('Main'),
stateMachineSelector: StateMachineSelector.byName('State Machine 1'),
);

播放动画​

⚠️ 警告:此功能已弃用。我们强烈建议通过状态机来播放和混合动画。

在之前的版本中,你可以通过向 RiveAnimation 传递 animations: ['myAnimation'] 来直接播放动画。

要在新版本中实现相同的效果,请使用 SingleAnimationPainter 和 RiveArtboardWidget 代替 RiveWidgetController 和 RiveWidget。

import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:rive_example/main.dart' show RiveExampleApp;

/// 这是一个替代控制器(绘制器),用于代替 [RiveWidgetController]。
///
/// 此绘制器用于绘制/推进状态机。功能上与 [RiveWidgetController] 非常相似,
/// 我们推荐大多数用例使用 [RiveWidgetController]。
class ExampleSingleAnimationPainter extends StatefulWidget {
const ExampleSingleAnimationPainter({super.key});

@override
State<ExampleSingleAnimationPainter> createState() =>
_ExampleSingleAnimationPainterState();
}

class _ExampleSingleAnimationPainterState
extends State<ExampleSingleAnimationPainter> {
late File file;
Artboard? artboard;
late SingleAnimationPainter painter;

@override
void initState() {
super.initState();
init();
}

void init() async {
file = (await File.asset(
'assets/off_road_car.riv',
riveFactory: RiveExampleApp.getCurrentFactory,
))!;
painter = SingleAnimationPainter('idle');
artboard = file.defaultArtboard();
setState(() {});
}

@override
void dispose() {
painter.dispose();
artboard?.dispose();
file.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
if (artboard == null) {
return const Center(child: CircularProgressIndicator());
}
return RiveArtboardWidget(
artboard: artboard!,
painter: painter,
);
}
}

要播放和混合多个动画,你需要创建自己的绘制器。参见 SingleAnimationPainter 的实现并扩展它来创建和推进多个动画。

处理状态机输入​

对于更高级的用例,建议考虑使用数据绑定

StateMachineController 已移除,替换为 StateMachine。重要变更:

状态机输入:新 API​

stateMachine.trigger('myTrigger');
stateMachine.boolean('myBool');
stateMachine.number('myNumber');

状态机输入:旧 API​

controller.getTriggerInput('myTrigger');
controller.getBooleanInput('myBool');
controller.getNumberInput('myNumber');

你可以从 RiveWidgetController 访问 stateMachine:

final controller = RiveWidgetController(file);
final stateMachine = controller.stateMachine;

建议在不再需要时手动释放输入:input.dispose()

嵌套输入​

你可以通过提供可选的 path 参数访问嵌套输入:

stateMachine.boolean('myBool', path: 'nested/path');
stateMachine.number('myNumber', path: 'nested/path');
stateMachine.trigger('myTrigger', path: 'nested/path');

处理 Rive 事件​

对于更高级的用例,建议考虑使用数据绑定来代替事件。

RiveEvent 已移除,替换为 Event。Event 是一个密封类,有两个选项:

  • OpenUrlEvent
  • GeneralEvent

注册事件监听器:

Rive 事件:新 API​

// 新 API
final controller = RiveWidgetController(_riveFile!);
controller?.stateMachine.addEventListener(_onRiveEvent);

void _onRiveEvent(Event event) {
// 对事件执行某些操作
}

Rive 事件:旧 API​

// 旧 API
final controller =
StateMachineController.fromArtboard(artboard, 'State Machine 1')!;
controller.addEventListener(_onRiveEvent);

void _onRiveEvent(RiveEvent event) {
// 对事件执行某些操作
}

访问 properties 返回 Map<String, CustomProperty>。CustomProperty 也是一个密封类,有以下选项:

  • CustomNumberProperty
  • CustomBooleanProperty
  • CustomStringProperty

所有这些都具有 value 字段。在 Event 类上,有便捷的访问器:

// 便捷访问器
event.property(name); // 返回 CustomProperty
event.numberProperty(name); // 返回 CustomNumberProperty
event.booleanProperty(name); // 返回 CustomBooleanProperty
event.stringProperty(name); // 返回 CustomStringProperty

布局变更​

BoxFit → Fit​

之前我们使用 Flutter 的 BoxFit 类。现在我们使用自己的 Fit,它包含一个额外的选项:

// 旧 API
BoxFit.contain

// 新 API
Fit.contain
Fit.layout // 用于基于布局的适配的新选项

资源加载变更​

FileAssetLoader 类及其所有子类已移除:

  • CDNAssetLoader
  • LocalAssetLoader
  • CallbackAssetLoader
  • FallbackAssetLoader
带外资源加载​

新 API​

资源类型:FontAsset、ImageAsset 和 AudioAsset。

参见以下演示加载随机字体的示例:

// 新 API
final fontFile = await File.asset(
'assets/acqua_text_out_of_band.riv',
riveFactory: Factory.rive,
assetLoader: (asset, bytes) {
// 替换未内嵌在 rive 文件中的字体资源
if (asset is FontAsset && bytes == null) {
final urls = [
'https://cdn.rive.app/runtime/flutter/IndieFlower-Regular.ttf',
'https://cdn.rive.app/runtime/flutter/comic-neue.ttf',
'https://cdn.rive.app/runtime/flutter/inter.ttf',
'https://cdn.rive.app/runtime/flutter/inter-tight.ttf',
'https://cdn.rive.app/runtime/flutter/josefin-sans.ttf',
'https://cdn.rive.app/runtime/flutter/send-flowers.ttf',
];

// 从字体 URL 列表中随机选择一个
http.get(Uri.parse(urls[Random().nextInt(urls.length)])).then((res) {
if (mounted) {
asset.decode(
Uint8List.view(res.bodyBytes.buffer),
);
setState(() {
// 强制重建,以防 Rive 图形不再推进
});
}
});
return true; // 告诉运行时不要自动加载该资源
} else {
return false; // 告诉运行时继续尝试加载该资源(如果存在)
}
},
);

你也可以手动创建资源类型并设置它们。这在你想预加载资源时非常有用:

Future<void?> updateImageAsset(ImageAsset asset, Uint8List bytes) async {
final renderImage = await Factory.rive.decodeImage(bytes);
if (renderImage != null) {
asset.renderImage(renderImage);
}
}

Future<void?> updateFontAsset(FontAsset asset, Uint8List bytes) async {
final font = await Factory.rive.decodeFont(bytes);
if (font != null) {
asset.font(font);
}
}

Future<void?> updateAudioAsset(AudioAsset asset, Uint8List bytes) async {
final audioSource = await Factory.rive.decodeAudio(bytes);
if (audioSource != null) {
asset.audio(audioSource);
}
}

旧 API​

// 旧 API
assetLoader: (asset, bytes) async {
/* 异步操作 */
final someImage = await ImageAsset.parseBytes(bytes)
asset.image = someImage;
}

关键变更:

  • assetLoader 不能再是异步 lambda
  • ImageAsset.parseBytes(bytes) → riveFactory.decodeImage(bytes) 或 asset.decode(bytes)
  • FontAsset.parseBytes(bytes) → riveFactory.decodeFont(bytes) 或 asset.decode(bytes)
  • AudioAsset.parseBytes(bytes) → riveFactory.decodeAudio(bytes) 或 asset.decode(bytes)
  • ImageAsset.image = value → ImageAsset.renderImage(value)(返回 boolean)
  • FontAsset.font = value → FontAsset.font(value)(返回 boolean)
  • AudioAsset.audio = value → AudioAsset.audio(value)(返回 boolean)

文本运行更新​

我们建议使用数据绑定来代替在运行时更新文本。

不再可以直接访问 TextValueRun 对象。改用以下方法访问字符串值:

final controller = RiveWidgetController(riveFile);
final artboard = controller.artboard;

// 获取文本运行值
artboard.getText('textRunName')
artboard.getText('textRunName', path: 'nested/path')

// 设置文本运行值
artboard.setText('textRunName', 'new value')
artboard.setText('textRunName', 'new value', path: 'nested/path')

已知的缺失功能​

以下功能在 v0.14.0 中不可用,但可能会在未来的版本中添加:

  • 自动 Rive CDN 资源加载
  • speedMultiplier
  • useArtboardSize
  • clipRect
  • isTouchScrollEnabled
  • dynamicLibraryHelper

已移除的代码路径​

以下路径中的所有"运行时"Dart 代码已被移除:

  • src/controllers
  • src/core
  • src/generated
  • rive_core
  • utilities

获取帮助​

如果在迁移过程中遇到问题:

  1. 查阅 Rive Flutter 文档
  2. 查阅数据绑定指南
  3. 访问 Rive 社区论坛
  4. 在 GitHub 仓库上报告问题
看完还有疑问?进群交流下!
与众多 Rive 创作者、开发者一起交流探讨与答疑解惑。
加入交流群