Reputation: 43
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
Upvotes: 0
Views: 468
Reputation: 346
An Alternative Approach: The same functionality can be implemented by lifting the state up to its parent which is much more convenient for this case.
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> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Testing',
theme: ThemeData(
// primarySwatch: Colors.blue,
backgroundColor: Colors.green,
),
home: Home(),
);
}
}
class Home extends StatefulWidget {
@override
State<Home> createState() => HomeState();
}
class HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
brightness: Brightness.dark,
backgroundColor: Colors.green,
),
body: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RichText(
text: const TextSpan(
text: 'Test Buttons',
),
),
...List.generate(datums.length, (index) => DataRow(
currentDatum: datums[index],
index: index,
onTap: () {
setState(() {
datums[index].incrementCount();
if(index != 0) datums[0].incrementCount();
});
}
)),
],
),
);
}
}
/// =============================================
class DataRow extends StatelessWidget {
final Datum currentDatum;
final int index;
final void Function()? onTap;
const DataRow({Key? key,required this.index,required this.onTap, required this.currentDatum}) : super(key: key);
@override
Widget build(BuildContext context) {
return lineItem(
name: '${currentDatum.word} is now: ${currentDatum.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: onTap,
child: Text('Button $index'),
),
],
),
);
}
}
In this case, the widget which is rendering the buttons is dump and all of the data and actions are being handled by its parent.
But this approach still has some problems as the List<Datum>
being used is global. We can create a local scope for the List<Datum>
as well.
But for our purposed the above code will do the trick without any hassle of keys or any funky logic. This also solves your problem for any updating any other widget without hassle of a key.
The drawback however is that this will update the whole parent widget itself but it is a reasonable tradeoff.
Upvotes: 0
Reputation: 346
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.
Upvotes: 1