Checkbox animation shows a visual toggle but still fails to update state interally
Description
I am developing a Flutter application using Rive animations for checkboxes. While the checkboxes visually toggle correctly when tapped, the programmatic state does not reflect these changes. Specifically, the submittedMissions list does not update correctly—it fails to remove IDs when checkboxes are unchecked, leading to inaccurate state representation in the application logic.
Steps to Reproduce
- Tap a checkbox to select it; the internal state list
submittedMissionsupdates to include the mission ID. - Tap the checkbox again to deselect it; the
submittedMissionslist fails to remove the mission ID. - The UI shows the checkbox as deselected, but the state still considers it selected.
Expected Behavior
When a checkbox is deselected, the corresponding mission ID should be removed from the submittedMissions list, ensuring the internal state matches the visual representation.
Screenshots
State not updating when checkbox is unchecked - it says there are still 2 checkboxes selected
Expected behavior using Flutter's native Checkbox.
Code to Reproduce
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
void main() {
runApp(
MaterialApp(
home: MissionListScreen(),
debugShowCheckedModeBanner: false,
),
);
}
class MissionListScreen extends StatefulWidget {
@override
_MissionListScreenState createState() => _MissionListScreenState();
}
class _MissionListScreenState extends State<MissionListScreen> {
List<MissionItem> missions = [
MissionItem(missionId: '1', missionName: 'Mission 1'),
MissionItem(missionId: '2', missionName: 'Mission 2'),
MissionItem(missionId: '3', missionName: 'Mission 3'),
];
List<String> submittedMissions = [];
@override
void initState() {
super.initState();
loadRiveFiles();
}
void loadRiveFiles() async {
for (var mission in missions) {
await _loadRiveFile(mission);
}
setState(() {});
}
Future<void> _loadRiveFile(MissionItem missionItem) async {
try {
final data = await rootBundle.load('assets/checkbox.riv');
final file = RiveFile.import(data);
final artboard = file.mainArtboard;
var controller =
StateMachineController.fromArtboard(artboard, 'State Machine 1');
if (controller != null) {
artboard.addController(controller);
missionItem.checkButtonInput = controller.findInput<bool>('Tap');
missionItem.checkButtonArtboard = artboard;
// Set the initial state of the Tap input
missionItem.checkButtonInput?.value =
submittedMissions.contains(missionItem.missionId);
}
} catch (e) {
print('Failed to load Rive file: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
//show the total number of missions checked
Text(
'There are ${submittedMissions.length} boxes checked.',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: missions.length,
itemBuilder: (context, index) {
MissionItem missionItem = missions[index];
return buildMissionListTile(missionItem, index);
},
),
),
],
),
);
}
Widget buildMissionListTile(MissionItem mission, int index) {
return ListTile(
title: Text(mission.missionName),
trailing: GestureDetector(
onTap: () => toggleMission(mission, index),
child: SizedBox(
height: 32,
width: 32,
child: Rive(artboard: mission.checkButtonArtboard!)),
// this code using Checkbox widget DOES work
// child: Checkbox(
// value: mission.checkButtonInput?.value ?? false,
// onChanged: (value) => toggleMission(mission, index),
// )),
),
);
}
void toggleMission(MissionItem mission, int index) {
bool currentState = mission.checkButtonInput?.value ?? false;
setState(() {
mission.checkButtonInput?.value = !currentState;
MissionItem newMission = mission;
if (currentState) {
submittedMissions.removeWhere((id) => id == mission.missionId);
} else {
if (!submittedMissions.contains(mission.missionId)) {
submittedMissions.add(mission.missionId);
}
}
missions.removeAt(index);
missions.insert(index, newMission);
});
print("Submitted Missions: $submittedMissions"); // Debugging
}
}
class MissionItem {
final String missionId;
final String missionName;
SMIInput<bool>? checkButtonInput;
Artboard? checkButtonArtboard;
MissionItem({
required this.missionId,
required this.missionName,
});
}
Environment
- Flutter 3.19.5 • channel stable • https://github.com/flutter/flutter.git
- Framework • revision 300451adae (3 weeks ago) • 2024-03-27 21:54:07 -0500
- Engine • revision e76c956498
- Tools • Dart 3.3.3 • DevTools 2.31.1
Additional Context
- This issue is specific to Rive animations and does not occur with native Flutter widgets.
- Rive file used for testing: Rive Checkbox Animation
- Link to Rive file on Google Drive: Google Drive Link
- I also posted on StackOverflow
Request
Please look into why the Rive animations do not trigger state updates as expected when toggling the state of checkboxes despite the UI changing. Any guidance or workarounds would be greatly appreciated!