Я разрабатываю приложение Flutter, которое использует Rive-анимацию для флажков.
Хотя флажки визуально корректно переключаются при нажатии, программное состояние не отражает эти изменения. В частности, список submittedMissions
обновляется неправильно — он не может удалить идентификаторы, если флажки не отмечены, что приводит к неточному представлению состояния в логике приложения.
Несмотря на снятие двух флажков в пользовательском интерфейсе, приложение по-прежнему сообщает, что два флажка отмечены, что демонстрирует сбой в управлении состоянием:
И наоборот, замена флажков Rive собственным виджетом Flutter Checkbox решает эту проблему, подчеркивая проблему, связанную с тем, как Rive обрабатывает изменения состояния:
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,
});
}
Даже если бы я добавил UniqueKey
, данные все равно не меняются правильно.
Как я могу гарантировать, что изменения состояния, вызванные анимацией Rive во Flutter, распознаются и обрабатываются правильно?
Несмотря на визуальные признаки того, что флажки переключаются, внутреннее состояние не отражает эти изменения.
Вы можете скачать этот файл rive (.riv) с официального сайта Rive или с Google Диска.
Не забудьте добавить указанный выше файл в папку с ресурсами и обновить файл pubspec.yaml
.
@MosheDicker Да, я это уже пробовал, это была и моя первоначальная мысль :) Но, к сожалению, это не сработало.
Пробовали ли вы реализовать обратный вызов или прослушиватели, которые привязывают состояние анимации Rive к вашему состоянию Flutter? При этом убедитесь, что setState вызывается для обновления представленных миссий - при изменении состояния. Спасибо
Какая версия Flutter? Потому что ваш пример кода странный...
Поиграв некоторое время, я нашел сам файл Rive, используя ввод Trigger вместо Boolean /-;
Прежде чем заметить, я придумал отдельный контекст для каждого, но есть небольшая ошибка, если пользователи нажимают одну и ту же кнопку в течение активной продолжительности анимации (1 секунда, например)
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'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
...missions.map(
(e) => Text(" Selected ${e.missionName} ${e.isChecked}"),
),
Expanded(
child: ListView.builder(
itemCount: missions.length,
itemBuilder: (context, index) {
MissionItem mission = missions[index];
return ListTile(
title: Text(mission.missionName),
trailing: RiveCheckBox(
initValue: mission.isChecked,
onChanged: (_) {
missions[index] = missions[index].copyWith(
isChecked: !missions[index].isChecked,
);
setState(() {});
}),
);
},
),
),
],
),
);
}
}
class RiveCheckBox extends StatefulWidget {
const RiveCheckBox({
super.key,
this.initValue = false,
required this.onChanged,
});
final bool initValue;
final ValueChanged<bool> onChanged;
@override
State<RiveCheckBox> createState() => _RiveCheckBoxState();
}
class _RiveCheckBoxState extends State<RiveCheckBox> {
Artboard? _artboard;
SMITrigger? smiInput;
late StateMachineController controller;
late final future = _loadRiveFile();
Future _loadRiveFile() async {
print('load Rive file:');
try {
final data = await rootBundle.load('assets/checkbox.riv');
final file = RiveFile.import(data);
final artboard = file.mainArtboard;
controller =
StateMachineController.fromArtboard(artboard, 'State Machine 1')!;
///! while I am certain on null assert
smiInput = controller.findSMI('Tap');
smiInput!.value = widget.initValue;
artboard.addController(controller);
_artboard = artboard;
return artboard;
} catch (e) {
print('Failed to load Rive file: $e');
}
}
@override
Widget build(BuildContext context) {
print("rebuilding RiveCheckBox ");
return FutureBuilder(
future: future,
builder: (context, snapshot) => snapshot.data == null
? const Text("Loading...")
: SizedBox(
width: 32,
height: 32,
child: GestureDetector(
onTap: () async {
smiInput!.value = !smiInput!.value;
widget.onChanged(smiInput!.value);
await Future.delayed(const Duration(seconds: 1));
},
child: Rive(artboard: _artboard!),
),
),
);
}
}
class MissionItem {
final String missionId;
final String missionName;
final bool isChecked;
const MissionItem({
required this.missionId,
required this.missionName,
this.isChecked = false,
});
MissionItem copyWith({
String? missionId,
String? missionName,
bool? isChecked,
}) {
return MissionItem(
missionId: missionId ?? this.missionId,
missionName: missionName ?? this.missionName,
isChecked: isChecked ?? this.isChecked,
);
}
}
Пробовали ли вы использовать ValueKey для каждого ListTiles?