Problem: I'm working on a dice roller app using Flutter and Rive animation. The dice animation doesn't work on the first button press, but it works fine from the second press onward.
I loaded the Rive file in the initState() method and set up the animation controller using the StateMachineController. However, the issue persists. I suspect the problem might be related to how the Rive file or its state machine is initialized.
Here are screenshots showing the issue:
Before pressing the button:
After the first press:
After the second press:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
class Rolldice extends StatefulWidget {
const Rolldice({super.key});
@override
State<Rolldice> createState() => _RolldiceState();
}
class _RolldiceState extends State<Rolldice> {
Artboard? riveArtboard;
var dicevalue = 0;
SMINumber? diceNumber;
SMITrigger? rollTrigger;
bool isRolling = false;
@override
void initState() {
_loadRiveFile();
super.initState();
}
void _loadRiveFile() async {
try {
await RiveFile.initialize();
final data = await rootBundle.load("assets/images/shakeable_dice.riv");
final file = RiveFile.import(data);
final artboard = file.mainArtboard;
final controller =
StateMachineController.fromArtboard(artboard, 'StateIdle');
if (controller != null) {
artboard.addController(controller);
diceNumber = controller.findSMI('Number');
rollTrigger = controller.findSMI('Trigger');
setState(() => riveArtboard = artboard);
}
} catch (e) {
print("Error loading Rive file: $e");
}
}
void _triggerRoll() {
if (isRolling) return;
isRolling = true;
setState(() {
dicevalue = Random().nextInt(6) + 1;
});
if (diceNumber != null) {
diceNumber!.value = dicevalue.toDouble();
}
if (rollTrigger != null) {
rollTrigger!.fire();
} else {
print('rollTrigger is not initialized');
}
Future.delayed(const Duration(seconds: 1), () {
isRolling = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('roll dice')),
body: Center(
child: Column(
children: [
Expanded(
child: riveArtboard == null
? const SizedBox(child: Text("Loading..."),)
: Center(child: Rive(artboard: riveArtboard!)),
),
Text(dicevalue.toString()),
ElevatedButton(onPressed: _triggerRoll, child: Text("Roll")),
],
)),
);
}
}
1 Answer 1
Any Dart function marked async returns a Future, so _loadRiveFile() actually returns Future<void>.
You don't await _loadRiveFile() in initState. For this reason, initState finishes before _loadRiveFile() has returned and your state variables riveArtboard,diceNumber, and rollTrigger are null when your widget is build.
As a side comment, in general, calling setState from within initState makes little sense.
I would recommend refactoring your code, so that your state variables which represent a future value are defined as a Future and use a FutureBuilder to build your widget.
This question has a different context but deals with the same issue of using a future value to build a widget.