跳到主要内容

迁移指南

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

版本 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.14.0-dev.8 # 或最新版本
  2. ✅ 在 main() 函数中添加 RiveNative.init(),或在使用 Rive 之前调用。
  3. ✅ 将 RiveRiveAnimation 组件替换为 RiveWidgetRiveWidgetBuilder
  4. ✅ 更新控制器以使用新的 API,参见 RiveWidgetController
  5. ✅ 审查并更新任何自定义资源加载代码
  6. ✅ 测试你的图形和交互

已移除的类

以下类已完全移除:

  • RiveRiveAnimation 组件 → 使用 RiveWidgetRiveWidgetBuilder
  • RiveAnimationController 及其子类 → 使用 RiveWidgetControllerSingleAnimationPainterStateMachinePainter
  • OneShotAnimationSimpleAnimation → 使用 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.riveFactory.flutter
  • RiveFile.import 替换为 File.decode(),返回 Future<File>
  • mainArtboard 替换为 defaultArtboard()
  • artboardByName(name) 替换为 artboard(name)
  • RiveFile.network 替换为 File.url
  • RiveFile.file 替换为 File.path

组件迁移

参见更新的示例应用以获取完整的迁移指南,包括如何使用新的 RiveWidgetRiveWidgetBuilder 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直接访问状态机
OneShotAnimationSimpleAnimationSingleAnimationPainter用于单个动画

使用新的 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'] 来直接播放动画。

要在新版本中实现相同的效果,请使用 SingleAnimationPainterRiveArtboardWidget 代替 RiveWidgetControllerRiveWidget

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 已移除,替换为 EventEvent 是一个密封类,有两个选项:

  • 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

资源类型:FontAssetImageAssetAudioAsset

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

// 新 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 = valueImageAsset.renderImage(value)(返回 boolean)
  • FontAsset.font = valueFontAsset.font(value)(返回 boolean)
  • AudioAsset.audio = valueAudioAsset.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 仓库上报告问题