I'm new to flutter/dart and reactive programming and I've been trying to figure this out for a while. I built the attached test code in DartPad to figure out what I'd like to do in my app. I have created a Stateful Widget (DataRow) and within its State object (_dataRowState) I have a function (setAndRefresher). I am trying to access this function from outside the widget from within a different widget (in the onPressed: of an ElevatedButton):
datums[0].setAndRefresh!(datums[0].count + 1.0);
I have built a global list of data objects I call Datum which have a callback member called setAndRefresh. I pass the index of the particular Datum in the list into the Stateful Widget when it is created and, using that, I try to store the callback function into that data object:
datums[_datumIndex].setAndRefresh = setAndRefresher;
I'm sure I'm missing something, but don't know what. Is what I'm trying to do prevented and, if so, why? I am as much trying to learn the language as I am trying to solve this problem.
import 'package:flutter/material.dart';
class Datum extends Object {
/// data storage object
double count = 0;
String word = 'Uninitialized';
Function? setAndRefresh;
void prt() {
print('word: $word – count: $count – callback: $setAndRefresh');
}
void incrementCount() {
print('incrementCount() -> $count');
count = count + 1;
print(' -> $count');
}
}
List<Datum> datums = [];
/// =============================================
void main() {
// Create three datum in a list
datums.add(Datum());
datums.add(Datum());
datums.add(Datum());
for (Datum dt in datums) {
dt.prt();
}
datums[0].word = 'All buttons total';
datums[1].word = 'Button 1 count';
datums[2].word = 'Button 2 count';
for (Datum dt in datums) {
dt.prt();
}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Testing',
theme: ThemeData(
// primarySwatch: Colors.blue,
backgroundColor: Colors.green,
),
home: Scaffold(
appBar: AppBar(
brightness: Brightness.dark,
backgroundColor: Colors.green,
),
body: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RichText(
text: const TextSpan(
text: 'Test Buttons',
),
),
const DataRow(datumIndex: 0),
const DataRow(datumIndex: 1),
const DataRow(datumIndex: 2),
],
),
),
);
}
}
/// =============================================
class DataRow extends StatefulWidget {
const DataRow({required this.datumIndex});
final int datumIndex;
@override
_DataRowState createState() => _DataRowState();
}
class _DataRowState extends State<DataRow> {
late int _datumIndex;
double count = 0;
@override
void initState() {
super.initState();
_datumIndex = widget.datumIndex;
print('Init setAndRefresh for $_datumIndex – callback: ${datums[_datumIndex].setAndRefresh}');
datums[_datumIndex].setAndRefresh = setAndRefresher;
print('Done init setAndRefresh for $_datumIndex – callback: ${datums[_datumIndex].setAndRefresh}');
}
void setAndRefresher({required double count}) {
print('Did set $count and refresh.');
setState(() {
datums[_datumIndex].count = count;
this.count = count;
});
}
@override
dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return lineItem(
name: '${datums[_datumIndex].word} is now: ${datums[_datumIndex].count} ',
color: Colors.orange,
);
}
Widget lineItem({required String name, required Color color}) {
return Container(
width: 500,
height: 50,
color: color,
child: Row(
children: <Widget>[
RichText(
text: TextSpan(text: name),
),
ElevatedButton(
onPressed: () {
print('Pressed Button $_datumIndex – callback: ${datums[_datumIndex].setAndRefresh}');
datums[_datumIndex].incrementCount();
setState(() {
count = datums[_datumIndex].count;
});
if (_datumIndex != 0 && datums[0].setAndRefresh != null) {
print('Now, increase All buttons total!()'); // This does print
datums[0].setAndRefresh!(datums[0].count + 1.0); // This is problem line?
print('Did it'); // this never prints
}
},
child: Text('Button ${widget.datumIndex}'),
),
],
),
);
}
}
This test code creates three buttons. Clicking the first button seems to work, but clicking either of the other buttons works once and then the button seems to freeze. Here is the console output resulting from clicking Button1, Button0, and then Button0 again:
word: Uninitialized – count: 0 – callback: null
word: Uninitialized – count: 0 – callback: null
word: Uninitialized – count: 0 – callback: null
word: All buttons total – count: 0 – callback: null
word: Button 1 count – count: 0 – callback: null
word: Button 2 count – count: 0 – callback: null
Init setAndRefresh for 0 – callback: null
Done init setAndRefresh for 0 – callback: Closure: ({required double count}) => void from: function setAndRefresher() {
[native code]
}
Init setAndRefresh for 1 – callback: null
Done init setAndRefresh for 1 – callback: Closure: ({required double count}) => void from: function setAndRefresher() {
[native code]
}
Init setAndRefresh for 2 – callback: null
Done init setAndRefresh for 2 – callback: Closure: ({required double count}) => void from: function setAndRefresher() {
[native code]
}
Script error.
Pressed Button 1 – callback: Closure: ({required double count}) => void from: function setAndRefresher() {
[native code]
}
incrementCount() -> 0
-> 1
Now, increase All buttons total!()
Script error.
Pressed Button 0 – callback: Closure: ({required double count}) => void from: function setAndRefresher() {
[native code]
}
incrementCount() -> 0
-> 1
Pressed Button 0 – callback: Closure: ({required double count}) => void from: function setAndRefresher() {
[native code]
}
incrementCount() -> 1
-> 2
Instead of using a global variable for doing this stuff. Flutter provides you a Key class which can be used to do what you are intending to do.
import 'package:flutter/material.dart';
class Datum extends Object {
/// data storage object
double count = 0;
String word = 'Uninitialized';
Function? setAndRefresh;
void prt() {
print('word: $word – count: $count – callback: $setAndRefresh');
}
void incrementCount() {
print('incrementCount() -> $count');
count = count + 1;
print(' -> $count');
}
}
List<Datum> datums = [];
/// =============================================
void main() {
// Create three datum in a list
datums.add(Datum());
datums.add(Datum());
datums.add(Datum());
for (Datum dt in datums) {
dt.prt();
}
datums[0].word = 'All buttons total';
datums[1].word = 'Button 1 count';
datums[2].word = 'Button 2 count';
for (Datum dt in datums) {
dt.prt();
}
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final zeroKey = GlobalKey<_DataRowState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Testing',
theme: ThemeData(
// primarySwatch: Colors.blue,
backgroundColor: Colors.green,
),
home: Scaffold(
appBar: AppBar(
brightness: Brightness.dark,
backgroundColor: Colors.green,
),
body: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RichText(
text: const TextSpan(
text: 'Test Buttons',
),
),
DataRow(datumIndex: 0, key: zeroKey),
DataRow(datumIndex: 1, zeroKey: zeroKey),
DataRow(datumIndex: 2, zeroKey: zeroKey),
],
),
),
);
}
}
/// =============================================
class DataRow extends StatefulWidget {
final int datumIndex;
final GlobalKey<_DataRowState>? zeroKey;
const DataRow({Key? key, this.zeroKey, required this.datumIndex}) : super(key: key);
@override
_DataRowState createState() => _DataRowState();
}
class _DataRowState extends State<DataRow> {
late int _datumIndex;
double count = 0;
@override
void initState() {
super.initState();
_datumIndex = widget.datumIndex;
print('Init setAndRefresh for $_datumIndex – callback: ${datums[_datumIndex].setAndRefresh}');
datums[_datumIndex].setAndRefresh = setAndRefresher;
print('Done init setAndRefresh for $_datumIndex – callback: ${datums[_datumIndex].setAndRefresh}');
}
void setAndRefresher({required double count}) {
print('Did set $count and refresh.');
setState(() {
datums[_datumIndex].count = count;
this.count = count;
});
}
@override
dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return lineItem(
name: '${datums[_datumIndex].word} is now: ${datums[_datumIndex].count} ',
color: Colors.orange,
);
}
Widget lineItem({required String name, required Color color}) {
return Container(
width: 500,
height: 50,
color: color,
child: Row(
children: <Widget>[
RichText(
text: TextSpan(text: name),
),
ElevatedButton(
onPressed: () {
print('Pressed Button $_datumIndex – callback: ${datums[_datumIndex].setAndRefresh}');
datums[_datumIndex].incrementCount();
setState(() {
count = datums[_datumIndex].count;
});
if(widget.zeroKey != null) {
print('Now, increase All buttons total!()'); // This does print
widget.zeroKey?.currentState?.setAndRefresher(count: datums[0].count + 1.0); // This is problem line?
print('Did it');
}
// if (_datumIndex != 0 && datums[0].setAndRefresh != null) {
// // this never prints
// }
},
child: Text('Button ${widget.datumIndex}'),
),
],
),
);
}
}
The zero key passed to first DataRow() will act as a key to control the state of that widget. Then for 2nd and 3rd DataRow() we again pass the zeroKey but as a different parameter indicating we don't want to control their state. Then instead of using the Global variable we can use the zeroKey if it is not null from within the _DataRowState().
Notice how the zeroKey is passed differently :
[
...
DataRow(datumIndex: 0, key: zeroKey),
DataRow(datumIndex: 1, zeroKey: zeroKey),
DataRow(datumIndex: 2, zeroKey: zeroKey),
...
]
DataRow(datumIndex: 0, key: zeroKey)
the key parameter for the 1st DataRow is to say that zeroKey is bound to the first widget.
DataRow(datumIndex: 1,zeroKey: zeroKey)
the zeroKey parameter for the 2nd and 3rd indicates it is just another parameter and the zeroKey should not be bound to the state of those widgets.