Skip to content

Commit fbfd485

Browse files
HayesGordonHayesGordon
andcommitted
feat(flutter): add a value stream to observable data binding properties (#11480) d020d4554c
Co-authored-by: Gordon <pggordonhayes@gmail.com>
1 parent dd9acea commit fbfd485

File tree

4 files changed

+225
-23
lines changed

4 files changed

+225
-23
lines changed

.rive_head

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
f88b98828badf06410ccb84df6b0876004c0f178
1+
d020d4554c576557edfc6cb87a0634efb6f8913e

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
## Upcoming
22

33
- Add `enumType` string getter to `ViewModelInstanceEnum` which returns the name of the enum (not the property name).
4+
- Add `valueStream<T>` to data binding properties that are observable (implementations of `ViewModelInstanceObservableValue`), which returns a `Stream<T>`. For example:
5+
6+
```
7+
ViewModelInstanceNumber? numberProperty = viewModelInstance!.number('age');
8+
Stream<double> stream = numberProperty!.valueStream;
9+
```
410

511
## 0.14.1
612

example/assets/rewards.riv

4.38 KB
Binary file not shown.

example/lib/examples/databinding.dart

Lines changed: 218 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:rive_example/main.dart' show RiveExampleApp;
77
/// Example using Rive data binding at runtime.
88
///
99
/// See: https://rive.app/docs/runtimes/data-binding
10+
/// Rive Editor file: https://rive.app/marketplace/25475-47540-data-binding-demo/
1011
class ExampleDataBinding extends StatefulWidget {
1112
const ExampleDataBinding({super.key});
1213

@@ -21,8 +22,17 @@ class _ExampleDataBindingState extends State<ExampleDataBinding> {
2122
late ViewModelInstance viewModelInstance;
2223
late ViewModelInstance coinItemVM;
2324
late ViewModelInstance gemItemVM;
24-
late ViewModelInstanceNumber coinValue;
25-
late ViewModelInstanceNumber gemValue;
25+
late ViewModelInstanceNumber coinProperty;
26+
late ViewModelInstanceNumber gemProperty;
27+
late ViewModelInstanceNumber energyBarProperty;
28+
late ViewModelInstanceNumber energyBarLivesProperty;
29+
late ViewModelInstanceColor energyBarColorProperty;
30+
late ViewModelInstanceString buttonTitleProperty;
31+
late ViewModelInstanceTrigger buttonPressedProperty;
32+
33+
double _sliderValue = 0.5;
34+
late Stream<double> _energyBarStream;
35+
Color _selectedColor = const Color(0xFF4CAF50);
2636

2737
@override
2838
void initState() {
@@ -36,71 +46,257 @@ class _ExampleDataBindingState extends State<ExampleDataBinding> {
3646
riveFactory: RiveExampleApp.getCurrentFactory,
3747
);
3848
controller = RiveWidgetController(file!);
39-
_initViewModel();
49+
_setupDataBinding();
4050
setState(() {});
4151
}
4252

43-
void _initViewModel() {
53+
void _setupDataBinding() {
54+
// Bind the default view model instance of the artboard to the controller
4455
viewModelInstance = controller!.dataBind(DataBind.auto());
56+
57+
// Set a random token to reward
4558
_selectRandomToken();
59+
4660
// Print the view model instance properties
4761
debugPrint(viewModelInstance.properties.toString());
62+
4863
// Get the rewards view model
4964
coinItemVM = viewModelInstance.viewModel('Coin')!;
5065
gemItemVM = viewModelInstance.viewModel('Gem')!;
51-
debugPrint(
52-
coinItemVM.toString()); // Print the view model instance properties
5366

54-
coinValue = coinItemVM.number('Item_Value')!;
55-
gemValue = gemItemVM.number('Item_Value')!;
56-
// Listen to the changes on the Item_Value input
57-
coinValue.addListener(_onCoinValueChange);
58-
coinValue.value = 1000; // set the initial coin value to 1000
67+
// Print the view model instance properties
68+
debugPrint(coinItemVM.toString());
69+
70+
// Get the Item_Value number properties for the coin and gem
71+
coinProperty = coinItemVM.number('Item_Value')!;
72+
gemProperty = gemItemVM.number('Item_Value')!;
73+
74+
// Listen to the changes on the Item_Value for the gen and coin
75+
coinProperty.addListener(_onCoinValueChange);
76+
gemProperty.addListener(_onGemValueChange);
77+
78+
// Set the initial values for the coin and gem
79+
coinProperty.value = 1000;
80+
gemProperty.value = 4000;
81+
82+
// Get the Energy_Bar/Energy_Bar number property
83+
energyBarProperty = viewModelInstance.number('Energy_Bar/Energy_Bar')!;
84+
// Create a stream for the energy bar
85+
_energyBarStream = energyBarProperty.valueStream;
86+
87+
// Get the Energy_Bar/Energy_Bar lives number property
88+
energyBarLivesProperty = viewModelInstance.number('Energy_Bar/Lives')!;
89+
90+
// Get the Energy_Bar/Energy_Bar color property
91+
energyBarColorProperty = viewModelInstance.color('Energy_Bar/Bar_Color')!;
5992

60-
gemValue.addListener(_onGemValueChange);
61-
gemValue.value = 4000; // set the initial gem value to 4000
93+
// Get the Button/Button_Title string property
94+
buttonTitleProperty = viewModelInstance.string('Button/State_1')!;
95+
96+
// Get the Button/Button_Trigger trigger property
97+
buttonPressedProperty = viewModelInstance.trigger('Button/Pressed')!;
98+
99+
// Listen to the changes on the Button/Pressed trigger
100+
buttonPressedProperty.addListener(_onButtonPressed);
62101
}
63102

103+
// Randomly select to reward either coins or gems
64104
void _selectRandomToken() {
65105
final random = Random.secure().nextBool() ? 'Coin' : 'Gem';
66-
// We randomly select to reward either coins or gems
67106
viewModelInstance
68107
.viewModel('Item_Selection')!
69108
.enumerator('Item_Selection')!
70109
.value = random;
71110
}
72111

112+
// Listener for the changes on the Item_Value for the coin
73113
void _onCoinValueChange(double value) {
74114
debugPrint('New coin value: $value');
75115
}
76116

117+
// Listener for the changes on the Item_Value for the gem
77118
void _onGemValueChange(double value) {
78119
debugPrint('New gem value: $value');
79120
}
80121

122+
void _onButtonPressed(bool value) {
123+
debugPrint('Button pressed');
124+
}
125+
81126
@override
82127
void dispose() {
83-
coinValue.removeListener(_onCoinValueChange);
84-
gemValue.removeListener(_onGemValueChange);
85-
coinValue.dispose();
86-
gemValue.dispose();
128+
// Listeners must be removed
129+
coinProperty.removeListener(_onCoinValueChange);
130+
gemProperty.removeListener(_onGemValueChange);
131+
132+
// Disposing the properties would also remove the listeners.
133+
// This is a best practice to immediately remove allocated native resources.
134+
// However, they will get garbage collected by the Dart runtime as they are
135+
// Finalizers, and it's not strictly necessary to dispose here.
136+
coinProperty.dispose();
137+
gemProperty.dispose();
87138
coinItemVM.dispose();
88139
gemItemVM.dispose();
140+
energyBarProperty.dispose();
141+
energyBarLivesProperty.dispose();
142+
energyBarColorProperty.dispose();
143+
buttonTitleProperty.dispose();
144+
buttonPressedProperty.dispose();
145+
89146
controller?.dispose();
90147
file?.dispose();
91148
super.dispose();
92149
}
93150

151+
void _showConfigSheet(BuildContext context) {
152+
if (controller == null) return;
153+
154+
showModalBottomSheet(
155+
context: context,
156+
builder: (context) => StatefulBuilder(
157+
builder: (context, setSheetState) => Container(
158+
padding: const EdgeInsets.all(24),
159+
child: SingleChildScrollView(
160+
child: Column(
161+
mainAxisSize: MainAxisSize.min,
162+
crossAxisAlignment: CrossAxisAlignment.start,
163+
children: [
164+
Text(
165+
'Configuration',
166+
style: Theme.of(context).textTheme.titleLarge,
167+
),
168+
const SizedBox(height: 24),
169+
// This is just an example of using a value stream
170+
StreamBuilder<double>(
171+
stream: _energyBarStream,
172+
builder: (context, snapshot) => Text(
173+
'Energy: ${snapshot.data?.toStringAsFixed(2) ?? '0.00'}',
174+
style: Theme.of(context).textTheme.bodyMedium,
175+
),
176+
),
177+
Slider(
178+
value: _sliderValue,
179+
onChanged: (value) {
180+
setSheetState(() => _sliderValue = value);
181+
energyBarProperty.value = value * 100;
182+
},
183+
),
184+
const SizedBox(height: 24),
185+
Text(
186+
'Bar Color',
187+
style: Theme.of(context).textTheme.bodyMedium,
188+
),
189+
const SizedBox(height: 12),
190+
Wrap(
191+
spacing: 8,
192+
runSpacing: 8,
193+
children: [
194+
const Color(0xFF4CAF50), // Green
195+
const Color(0xFF2196F3), // Blue
196+
const Color(0xFFF44336), // Red
197+
const Color(0xFFFF9800), // Orange
198+
const Color(0xFF9C27B0), // Purple
199+
const Color(0xFFFFEB3B), // Yellow
200+
const Color(0xFF00BCD4), // Cyan
201+
const Color(0xFFE91E63), // Pink
202+
].map((color) {
203+
// ignore: deprecated_member_use
204+
final isSelected = _selectedColor.value == color.value;
205+
return GestureDetector(
206+
onTap: () {
207+
setSheetState(() => _selectedColor = color);
208+
energyBarColorProperty.value = color;
209+
},
210+
child: Container(
211+
width: 40,
212+
height: 40,
213+
decoration: BoxDecoration(
214+
color: color,
215+
shape: BoxShape.circle,
216+
border: Border.all(
217+
color:
218+
isSelected ? Colors.white : Colors.transparent,
219+
width: 3,
220+
),
221+
boxShadow: isSelected
222+
? [
223+
BoxShadow(
224+
// ignore: deprecated_member_use
225+
color: color.withOpacity(0.6),
226+
blurRadius: 8,
227+
spreadRadius: 2,
228+
),
229+
]
230+
: null,
231+
),
232+
),
233+
);
234+
}).toList(),
235+
),
236+
const SizedBox(height: 16),
237+
Text(
238+
'Lives: ${energyBarLivesProperty.value}',
239+
style: Theme.of(context).textTheme.bodyMedium,
240+
),
241+
const SizedBox(height: 16),
242+
Slider(
243+
value: energyBarLivesProperty.value,
244+
onChanged: (value) {
245+
energyBarLivesProperty.value = value;
246+
setSheetState(() {});
247+
},
248+
min: 0,
249+
max: 10,
250+
divisions: 10,
251+
label: 'Lives',
252+
),
253+
const SizedBox(height: 16),
254+
Text(
255+
'Button Title: ${buttonTitleProperty.value}',
256+
style: Theme.of(context).textTheme.bodyMedium,
257+
),
258+
const SizedBox(height: 16),
259+
TextField(
260+
onChanged: (value) {
261+
buttonTitleProperty.value = value;
262+
setSheetState(() {});
263+
},
264+
decoration: const InputDecoration(
265+
labelText: 'Button Title',
266+
border: OutlineInputBorder(),
267+
),
268+
),
269+
],
270+
),
271+
),
272+
),
273+
),
274+
);
275+
}
276+
94277
@override
95278
Widget build(BuildContext context) {
96279
final controller = this.controller;
97280
if (controller == null) {
98281
return const Center(child: CircularProgressIndicator());
99282
}
100-
return RiveWidget(
101-
controller: controller,
102-
fit: Fit.layout, // for responsive layouts
103-
layoutScaleFactor: 1 / 2.0,
283+
return Stack(
284+
children: [
285+
RiveWidget(
286+
controller: controller,
287+
fit: Fit.layout, // for responsive layouts
288+
layoutScaleFactor: 1 / 2.0,
289+
),
290+
Positioned(
291+
bottom: 16,
292+
right: 16,
293+
child: IconButton.filled(
294+
icon: const Icon(Icons.tune),
295+
onPressed: () => _showConfigSheet(context),
296+
tooltip: 'Configuration',
297+
),
298+
),
299+
],
104300
);
105301
}
106302
}

0 commit comments

Comments
 (0)