是否可以从另一个小部件访问有状态小部件内的实例函数?

Is it possible to access instance function that is within Stateful Widget from another widget?

我是 flutter/dart 和响应式编程的新手,一段时间以来我一直在努力解决这个问题。我在 DartPad 中构建了附加的测试代码,以确定我想在我的应用程序中做什么。我创建了一个有状态的小部件 (DataRow),在它的状态对象 (_dataRowState) 中我有一个函数 (setAndRefresher)。我正在尝试从小部件外部从另一个小部件(在 ElevatedButton 的 onPressed: 中)访问此功能:

datums[0].setAndRefresh!(datums[0].count + 1.0);

我已经构建了一个全局数据对象列表,我称之为 Datum,它有一个名为 setAndRefresh 的回调成员。我在创建状态小部件时将列表中特定数据的索引传递给有状态小部件,并使用它尝试将回调函数存储到该数据对象中:

数据[_datumIndex].setAndRefresh = setAndRefresher;

我确定我错过了什么,但不知道是什么。我试图做的事情是否被阻止了,如果是,为什么?我在努力学习这门语言的同时也在努力解决这个问题。

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}'),
          ),
        ],
      ),
    );
  }
}

此测试代码创建了三个按钮。单击第一个按钮似乎可以工作,但是单击其他任何一个按钮都可以工作一次,然后该按钮似乎冻结了。以下是再次单击 Button1、Button0 和 Button0 后的控制台输出:

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

而不是使用全局变量来做这些事情。 Flutter 为你提供了一个 Key class 可以用来做你想做的事情。

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}'),
          ),
        ],
      ),
    );
  }
}

传递给第一个 DataRow() 的零键将充当控制该小部件状态的键。然后对于第二个和第三个 DataRow() 我们再次传递 zeroKey 但作为一个不同的参数表明我们不想控制它们的状态。然后,如果它在 _DataRowState() 中不为空,我们可以使用 zeroKey 而不是使用 Global 变量。

注意 zeroKey 的传递方式有何不同:

[
  ...
  DataRow(datumIndex: 0, key: zeroKey),
  DataRow(datumIndex: 1, zeroKey: zeroKey),
  DataRow(datumIndex: 2, zeroKey: zeroKey),
  ...
]

DataRow(datumIndex: 0, key: zeroKey) 第一个数据行的关键参数是说 zeroKey 绑定到第一个小部件。

DataRow(datumIndex: 1,zeroKey: zeroKey) 第二个和第三个的 zeroKey 参数表示它只是另一个参数,zeroKey 不应绑定到这些小部件的状态。

另一种方法: 可以通过将状态提升到其父级来实现相同的功能,这对于这种情况更加方便。

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'),
          ),
        ],
      ),
    );
  }
}

在这种情况下,呈现按钮的小部件是转储的,所有数据和操作都由其父级处理。

但是这种方法仍然存在一些问题,因为使用的 List<Datum> 是全局的。我们也可以为 List<Datum> 创建一个本地范围。

但就我们的目的而言,上面的代码可以解决问题,而无需任何麻烦的键或任何时髦的逻辑。这也解决了您更新任何其他小部件而无需麻烦的问题。

然而,缺点是这将更新整个父小部件本身,但这是一个合理的权衡。