如何在颤动中从父小部件访问所有孩子的状态?

Posted

技术标签:

【中文标题】如何在颤动中从父小部件访问所有孩子的状态?【英文标题】:How to access all of child's state from Parent Widget in flutter? 【发布时间】:2020-04-23 12:55:17 【问题描述】:

我有一个名为 createRoutineScreen 的父小部件,它有 7 个类似的名为 RoutineFormCard 的子小部件。 RoutineFormCard 是一个表单,它有一个布尔类型的状态 _isPostSuccesful 来判断表单是否保存到数据库中。现在,只有当所有 7 个孩子的 _isPostSuccesful 为真时,我才必须从 createRoutine 移动到另一个屏幕。如何从 createRoutineScreen 小部件访问所有孩子的状态?

我的代码是:

 class CreateRoutineScreen extends StatefulWidget 


  final String userID;

  CreateRoutineScreen(this.userID);
  //TITLE TEXT
  final Text titleSection = Text(
      'Create a Routine',
      style: TextStyle(
        color: Colors.white,
        fontSize: 25,
      )
  );

  final List<Map> weekDays = [
    "name":"Sunday", "value":1,
    "name":"Monday", "value":2,
    "name":"Tuesday", "value":3,
    "name":"Wednesday", "value":4,
    "name":"Thursday", "value":5,
    "name":"Friday", "value":6,
    "name":"Saturday", "value":7,
  ];

  @override
  _CreateRoutineScreenState createState() => _CreateRoutineScreenState();




class _CreateRoutineScreenState extends State<CreateRoutineScreen> 


  Routine routine;
  Future<List<dynamic>> _exercises;
  dynamic selectedDay;
  int _noOfRoutineSaved;
  List _keys = [];


   Future<List<dynamic>>_loadExercisesData()async
    String url = BASE_URL+ "exercises";
    var res = await http.get(url);
    var exercisesList = Exercises.listFromJSON(res.body);
    //var value = await Future.delayed(Duration(seconds: 5));
    return exercisesList;
  


  @override
  void initState()
    super.initState();
    _exercises = _loadExercisesData();
    _noOfRoutineSaved = 0;
    for (int i = 0; i< 7; i++)
      _keys.add(UniqueKey());
    
  

  void _changeNoOfRoutineSaved(int a)
    setState(() 
      _noOfRoutineSaved= _noOfRoutineSaved + a;
    );
  



  @override
  Widget build(BuildContext context) 
    print(_noOfRoutineSaved);
    return Scaffold(
        appBar: AppBar(
          title:Text("Create a Routine"),
          centerTitle: true,
          actions: <Widget>[
            FlatButton(
              child: Text("Done"),
              onPressed: ()
              ,
            ),
          ],
        ),
        body: Container(
          color: Theme.of(context).primaryColor,
          padding: EdgeInsets.only(top:5.0,left: 10,right: 10,bottom: 10),
          child: FutureBuilder(
            future: _exercises,
            builder: (context, snapshot)
              if(snapshot.hasData)
                return ListView.builder(
                  itemCount: widget.weekDays.length,
                  itemBuilder: (context,index)
                    return RoutineFormCard(
                      weekDay: widget.weekDays[index]["name"],
                      exerciseList: snapshot.data,
                      userID : widget.userID,
                      changeNoOfRoutineSaved:_changeNoOfRoutineSaved,
                      key:_keys[index]
                    );
                  ,
                );
              
            else if(snapshot.hasError)
              return SnackBar(
              content: Text(snapshot.error),
              );
            
            else
              return Center(
                child: CircularProgressIndicator(
                  backgroundColor: Colors.grey,
                )
              );
            
          , 
        )
      ),
    );
  

我的子小部件是:

class RoutineFormCard extends StatefulWidget 

  final Function createRoutineState;
  final String weekDay;
  final List<dynamic> exerciseList;
  final String userID;
  final Function changeNoOfRoutineSaved;

  RoutineFormCard(this.createRoutineState, 
    this.weekDay, this.exerciseList, this.changeNoOfRoutineSaved,
    this.userID, Key key):super(key:key);

  @override
  _RoutineFormCardState createState() => _RoutineFormCardState();


class _RoutineFormCardState extends State<RoutineFormCard> 

  bool _checkBoxValue= false;
  List<int> _selectedExercises;
  bool _inAsyncCall;
  bool   _successfulPost;

  @override
  void initState()
    super.initState();
    _selectedExercises = [];
    _inAsyncCall = false;
    _successfulPost= false;


  

  void onSaveClick()async

    setState(() 
     _inAsyncCall = true; 
    );

    String url = BASE_URL + "users/routine";

    List selectedExercises = _selectedExercises.map((item)
      return widget.exerciseList[item].value;
    ).toList();

    String dataToSubmit = jsonEncode(
      "weekDay":widget.weekDay,
      "userID": widget.userID==null?"5e9eb190b355c742c887b88d":widget.userID,
      "exercises": selectedExercises
    );

    try
      var res =await  http.post(url, body: dataToSubmit,
        headers: "Content-Type":"application/json");

        if(res.statusCode==200)
          print("Succesful $res.body");
          widget.changeNoOfRoutineSaved(1);
          setState(() 
           _inAsyncCall = false;
           _successfulPost = true; 
          );

        
        else
            print("Not succesful $res.body");
            setState(() 
             _inAsyncCall = false; 
            );
        



    catch(err)
        setState(() 
          _inAsyncCall = false; 
        );
        print(err);

    

  


  Widget saveAndEditButton()
    if(_inAsyncCall)
      return CircularProgressIndicator();
    
    else if(_successfulPost)
    
      return IconButton(
        icon: Icon(Icons.edit, color: Colors.black,),
        onPressed: ()
          widget.changeNoOfRoutineSaved(-1);
          setState(() 
           _successfulPost = false; 
          );
        ,
      );
    
    else
      return FlatButton(child: Text("Save"),
            onPressed: !_checkBoxValue&&_selectedExercises.length==0?null:onSaveClick,);
    
  

  //Card Header
  Widget cardHeader()
    return  AppBar(
      title: Text(widget.weekDay, style: TextStyle(
        fontFamily: "Raleway",
        fontSize: 20,
        color: Colors.black,),
        ),
      actions: <Widget>[
        saveAndEditButton()    
      ],
      backgroundColor: Colors.lime[400],
    );
  


  Widget cardBody()
    return Column(
      children: <Widget>[
          Padding(
              padding: const EdgeInsets.all(8.0),
              child: Row(
                children: <Widget>[
                  Text("Rest Day"),
                  Checkbox(
                    value: _checkBoxValue,
                    onChanged: (value)
                      setState(() 
                        _checkBoxValue = value;
                      );
                    ,
                  )
                ],
              ),
            ),

            _checkBoxValue?Container():
            SearchableDropdown.multiple(
            hint: "Select Exercise",
            style: TextStyle(color: Colors.black),
            items: widget.exerciseList.map<DropdownMenuItem>((item)
              return DropdownMenuItem(
                child: Text(item.name), value: item
              );
            ).toList(),
            selectedItems: _selectedExercises,
            onChanged: (values)
              setState(() 
              _selectedExercises = values;
              );
            ,
            isExpanded: true,
            dialogBox: true,
          ),
      ],
    );
  


  @override
  Widget build(BuildContext context) 
    print("<><><><><><><><><><><>$widget.weekDay called");
    return Card(
      elevation: 8.0,
      child: Form(
        key: GlobalKey(),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
          cardHeader(),
          _successfulPost?Container():cardBody()
          ],
        ),
      ),
    );
  
 

如您所见,我已尝试从父小部件回调,它增加或减少从每个子小部件保存的表单数量。它可以完成工作,但是当保存一个表单时,父状态会被修改,并且所有其他孩子都得到重建,这在我看来是不必要的。最好的方法是什么?

【问题讨论】:

我认为在你的情况下使用 ChangeNotifier 和 Provider 会更优雅 【参考方案1】:

尝试为每个RoutineFormCard 使用GlobalKey 而不是UniqueKey。它将帮助您访问每个RoutineFormCard 的状态。你可以这样做:

// 1. In the top of your CreateRoutineScreen file, add this line (make your RoutineFormCardState class public before)
final List<GlobalKey<RoutineFormCardState>> routineFormCardKeys = <GlobalKey<RoutineFormCardState>>[
  GlobalKey<RoutineFormCardState>(),
  GlobalKey<RoutineFormCardState>(),
  GlobalKey<RoutineFormCardState>(),
  GlobalKey<RoutineFormCardState>(),
  GlobalKey<RoutineFormCardState>(),
  GlobalKey<RoutineFormCardState>(),
  GlobalKey<RoutineFormCardState>(),
];

// 2. Then construct your RoutineFormCard using the right key
RoutineFormCard(
  weekDay: widget.weekDays[index]["name"],
  exerciseList: snapshot.data,
  userID : widget.userID,
  changeNoOfRoutineSaved:_changeNoOfRoutineSaved,
  key: routineFormCardKeys[index]
);

// 3. Now you can create a method in CreateRoutineScreen which will check the state of all RoutineFormCard
bool _allRoutineFormCardsCompleted() 
  bool result = true;
  for (int i = 0; i < 7; i++)
    result = result && routineFormCardKeys[i].currentState.isPostSuccessful;

  return result;



// 4. Finally use the result of the previous method where you want to move on another page

我正在分享一个解决您问题的快速想法,我尚未对其进行测试,但如果需要,我已准备好改进答案

希望这会有所帮助!

【讨论】:

以上是关于如何在颤动中从父小部件访问所有孩子的状态?的主要内容,如果未能解决你的问题,请参考以下文章

在颤振中从父小部件调用 setState 不会更新状态

Flutter - 无法从父小部件更改子状态

在子小部件中,如何在 kivy 中获取父小部件的实例

导航抽屉未在颤动中从 APPBar 小部件打开

如何在颤动的另一个有状态小部件中访问在一个有状态小部件中创建的对象

如何布局 GridView.count 以便小部件从 Flutter 中的父小部件顶部开始构建?