Flutter Stateful Widget 状态未初始化

Posted

技术标签:

【中文标题】Flutter Stateful Widget 状态未初始化【英文标题】:Flutter Stateful Widget State not Initializing 【发布时间】:2018-12-06 13:45:05 【问题描述】:

我正在使用 Flutter 制作命令和控制应用程序,但遇到了一个奇怪的问题。应用程序的主状态页面显示有状态小部件列表,每个小部件都拥有一个 WebSocket 连接,该连接从连接的机器人平台流式传输状态数据。当机器人本身被硬编码时,这很有效。但是现在我正在动态添加它们(通过条形码扫描),只有第一个小部件显示状态。

使用调试器的进一步调查表明,这是由于为列表中的第一个小部件创建状态。随后添加的小部件已成功构建,但没有获得状态。这意味着除了添加的第一个小部件之外,不会调用 createState 。我检查了小部件本身确实被添加到列表中,并且它们每个都有唯一的哈希码。此外,IOWebSocketChannel 具有唯一的哈希码,并且所有小部件数据对于列表中的不同元素都是正确且唯一的。

关于什么可能导致此问题的任何想法?

HomePageState 的代码:

class HomePageState extends State<HomePage> 
  String submittedString = "";
  StateContainerState container;
  List<RobotSummary> robotList = [];
  List<String> robotIps = [];
  final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();

  void addRobotToList(String ipAddress) 
    var channel = new IOWebSocketChannel.connect('ws://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort);
    channel.sink.add("http://" + ipAddress);
    var newConnection = new RobotSummary(key: new UniqueKey(), channel: channel, ipAddress: ipAddress, state: -1, fullAddress: 'http://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort,);
    scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: new Text("Adding robot..."), duration: Duration(seconds: 2),));
    setState(() 
      robotList.add(newConnection);
      robotIps.add(ipAddress);
      submittedString = ipAddress;
    );
  

  void _onSubmit(String val) 

    // Determine the scan data that was entered
    if(Validator.isIP(val)) 
      if(ModalRoute.of(context).settings.name == '/') 
        if (!robotIps.contains(val)) 
          addRobotToList(val);
        
        else 
          scaffoldKey.currentState.showSnackBar(new SnackBar(
            content: new Text("Robot already added..."), duration: Duration(seconds: 5),));
        
      
      else 
        setState(() 
          _showSnackbar("Robot scanned. Go to page?", '/');
        );
      
    
    else if(Validator.isSlotId(val)) 
      setState(() 
        _showSnackbar("Slot scanned. Go to page?", '/slots');
      );
    
    else if(Validator.isUPC(val)) 
      setState(() 
        _showSnackbar("Product scanned. Go to page?", '/products');
      );
    
    else if (Validator.isToteId(val)) 

    
  

  @override
  Widget build(BuildContext context) 
    container = StateContainer.of(context);
    return new Scaffold (
      key: scaffoldKey,
      drawer: Drawer(
        child: CategoryRoute(),
      ),
      appBar: AppBar(
        title: Text(widget.topText),  
      ),
      bottomNavigationBar: BottomAppBar(
        child: new Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
            IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
          ],
        ),
      ),
      body: robotList.length > 0 ? ListView(children: robotList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: Colors.blue),),),
    );
  

  void _showModalSheet() 
    showModalBottomSheet(
        context: context,
        builder: (builder) 
          return _searchBar(context);
        );
  

  void _showSnackbar(String message, String route) 
    scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: new Text(message),
      action: SnackBarAction(
        label: 'Go?', 
        onPressed: () 
          if (route == '/') 
            Navigator.popUntil(context,ModalRoute.withName('/'));
          
          else 
            Navigator.of(context).pushNamed(route); 
          
        ,),
      duration: Duration(seconds: 5),));
  

  Widget _searchBar(BuildContext context) 
    return new Scaffold(
      body: Container(
      height: 75.0,
      color: iam_blue,
      child: Center(
      child: TextField(
        style: TextStyle (color: Colors.white, fontSize: 18.0),
        autofocus: true,
        keyboardType: TextInputType.number,
        onSubmitted: (String submittedStr) 
          Navigator.pop(context);
          _onSubmit(submittedStr);
        ,
        decoration: new InputDecoration(
        border: InputBorder.none,
        hintText: 'Scan a tote, robot, UPC, or slot',
        hintStyle: TextStyle(color: Colors.white70),
        icon: const Icon(Icons.search, color: Colors.white70,)),
      ),
    )));
  

  Future scan() async 
    try 
      String barcode = await BarcodeScanner.scan();
      setState(() => this._onSubmit(barcode));
     on PlatformException catch (e) 
      if (e.code == BarcodeScanner.CameraAccessDenied) 
        setState(() 
          print('The user did not grant the camera permission!');
        );
       else 
        setState(() => print('Unknown error: $e'));
      
     on FormatException
      setState(() => print('null (User returned using the "back"-button before scanning anything. Result)'));
     catch (e) 
      setState(() => print('Unknown error: $e'));
    
  

RobotSummary 类的代码 sn-p:

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:test_app/genericStateSummary_static.dart';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:test_app/StateDecodeJsonFull.dart';
import 'dart:async';
import 'package:test_app/dataValidation.dart';

class RobotSummary extends StatefulWidget 
  final String ipAddress;
  final String _port = '5000';
  final int state;
  final String fullAddress;
  final WebSocketChannel channel;

  RobotSummary(
    Key key,
    @required this.ipAddress,
    @required this.channel,
    this.state = -1,
    this.fullAddress = "http://10.1.10.200:5000",
  ) :  assert(Validator.isIP(ipAddress)),
        super(key: key);

  @override
  _RobotSummaryState createState() => new _RobotSummaryState();


class _RobotSummaryState extends State<RobotSummary> 
  StreamController<StateDecodeJsonFull> streamController;

  @override
  void initState() 
    super.initState();
    streamController = StreamController.broadcast();
  

  @override
  Widget build(BuildContext context) 

    return new Padding(
      padding: const EdgeInsets.all(20.0),
      child: new StreamBuilder(
        stream: widget.channel.stream,
        builder: (context, snapshot) 
          //streamController.sink.add('"autonomyControllerState" : 3,  "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82,   "pickUpcCode" : "00814638", "robotName" : "Adam"');
          return getStateWidget(snapshot);
        ,
      ),
    );
  

  @override
  void dispose() 
    streamController.sink.close();
    super.dispose();
  

【问题讨论】:

将 Scaffold 主体更改为 ListView(children: roboList) 是否有效?或者,如果您在 setState 中重新创建列表,创建一个新列表并从旧列表创建 addAll,会怎样? 我都试过了。不幸的是,他们都没有工作。我喜欢你的想法,也许建造者会认为孩子们不同,并触发状态构建/重建的流动。还使用了第一个选项来稍微清理我的代码并摆脱最终有点无用的类(RobotList)。更新了上面的代码以反映。 【参考方案1】:

根据 Jacob 在最初的 cmets 中所说的话,我想出了一个可行的解决方案,并且结合了他的建议。他上面提出的代码解决方案无法实现(见我的评论),但也许可以尝试修改它的元素。对于我现在正在使用的解决方案,对 HomePageState 的构建器调用如下:

Widget build(BuildContext context) 
    List<RobotSummary> tempList = [];
    if (robotList.length > 0) 
      tempList.addAll(robotList);
    
    container = StateContainer.of(context);
    return new Scaffold (
      key: scaffoldKey,
      drawer: Drawer(
        child: CategoryRoute(),
      ),
      appBar: AppBar(
        title: Text(widget.topText),  
      ),
      bottomNavigationBar: BottomAppBar(
        child: new Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
            IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
          ],
        ),
      ),
      body: robotList.length > 0 ? ListView(children: tempList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: iam_blue),),),
    );
  

【讨论】:

【参考方案2】:

问题是您在build 调用之间保留了StatefulWidgets,因此它们的状态始终相同。尝试将RobotSummary 业务逻辑与视图逻辑分开。类似的东西

class RobotSummary 
  final String ipAddress;
  final String _port = '5000';
  final int state;
  final String fullAddress;
  final WebSocketChannel channel;
  StreamController<StateDecodeJsonFull> streamController;

  RobotSummary(
    @required this.ipAddress,
    @required this.channel,
    this.state = -1,
    this.fullAddress = "http://10.1.10.200:5000",
  ) :  assert(Validator.isIP(ipAddress));

  void init() => streamController = StreamController.broadcast();
  void dispose() => streamController.sink.close();

然后在你的 Scaffold 体内:

...

body: ListView.builder(itemCount: robotList.length, itemBuilder: _buildItem)

...

Widget _buildItem(BuildContext context, int index) 
  return new Padding(
      padding: const EdgeInsets.all(20.0),
      child: new StreamBuilder(
        stream: robotList[index].channel.stream,
        builder: (context, snapshot) 
          //streamController.sink.add('"autonomyControllerState" : 3,  "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82,   "pickUpcCode" : "00814638", "robotName" : "Adam"');
          return getStateWidget(snapshot); // not sure how to change this.
        ,
      ),
    );

【讨论】:

这里的问题是 RobotSummary 不是一个列表,而是一个列表元素。首页正文中展示的是一个RobotSummary小部件的ListView。 是的,ListView 是通过在每个 RobotSummary 上调用 _buildItem 来填充的。

以上是关于Flutter Stateful Widget 状态未初始化的主要内容,如果未能解决你的问题,请参考以下文章

Flutter控件篇(Stateful widget)——ListView

Flutter - Stateful(有状态) 和 stateless(无状态) widgets

跨多个屏幕使用的 Flutter Stateful Widget 正在重建

Flutter Stateful Widget 状态未初始化

Flutter Stateful Widget 重新创建 State

Flutter: Stateful 挂件 vs Stateless 挂件