【问题标题】:Problem when move from first to last tab on TabBarView Flutter在 TabBarView Flutter 上从第一个选项卡移动到最后一个选项卡时出现问题
【发布时间】:2020-05-05 16:13:45
【问题描述】:

当我需要从第一个选项卡跳转到第三个选项卡时,我的颤振应用程序出现问题。我将在下面更好地解释:

当我有一个带有 3 个选项卡的 TabBar 并在 TabBarView 下方填充此选项卡时,我正在使用颤振构建一个屏幕。

类似的东西:

Widget _buildReportAppbar() {
    return AppBar(
      backgroundColor: AppColors.colorPrimary,
      elevation: 5,
      leading: ...
      title: ...
      actions: ...
      bottom: PreferredSize(
        preferredSize: Size.fromHeight(58.0),
        child: Padding(
          padding: EdgeInsets.only(bottom: 10),
          child: TabBar(
            labelColor: AppColors.colorPrimary,
            labelPadding: EdgeInsets.only(left: 8, right: 8),
            labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
            unselectedLabelColor: Colors.white,
            unselectedLabelStyle:
                TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
            indicatorSize: TabBarIndicatorSize.label,
            isScrollable: true,
            indicatorWeight: 0,
            indicator: BoxDecoration(
              borderRadius: BorderRadius.circular(12),
              color: Colors.white,
            ),
            controller: _tabController,
            tabs: _tabs,
          ),
        ),
      ),
    );
  }

  Widget _buildReportBody() {
    return TabBarView(
      controller: _tabController,
      children: _provideTabScreenList(),
    );
  }

我的标签控制器工作起来就像一个魅力,在每个 TabBarView 中我都有一个 statefulWidget 来构建我的报告屏幕。每个 StatefulWidget 都有一个 api 调用,它为我带来报告信息,并使用 SetState(){} 方法将信息放在屏幕上。

List<Widget> _provideTabScreenList() {
_tabScreenList.clear();
_tabScreenList.add(PeriodResumeReport(filterData: currentFilter));
_tabScreenList.add(SaleStatisticReport(filterData: currentFilter));
_tabScreenList.add(SalePerDayReport(filterData: currentFilter));
return _tabScreenList;
}


class PeriodResumeReport extends StatefulWidget {
  final _periodResumeReportState = _PeriodResumeReportState();
  final SelectedFilter filterData;

  PeriodResumeReport({Key key, @required this.filterData}) : super(key: key);

  @override
  _PeriodResumeReportState createState() => _periodResumeReportState;

  //My tabController has a listener that detect the position change and notify the child.
  void isVisible() {
    _periodResumeReportState.isVisible();
  }
}

//I'm using the AutomaticKeepAliveClientMixin to keep the state when i move between the 
childs of the TabBarView
class _PeriodResumeReportState extends State<PeriodResumeReport>
    with AutomaticKeepAliveClientMixin<PeriodResumeReport> {
  var _loadingData = false;
  var _apiErrorMessage = "";
  var _hasResponseFromApi = false;

  var _response = ...api response;

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }

  //Load first data when the screen visible for the first time.
  @override
  void initState() {
    super.initState();
    _reloadData();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      children: <Widget>[
        _loadingData ? LinearProgressIndicator() : Container(),
        _apiErrorMessage.isNotEmpty
            ? Padding(...)
            : Container(),
        _hasResponseFromApi ? _buildTotalOfSalesContainer() : Container(),
        _hasResponseFromApi ? _buildComparativeChart() : Container(),
      ],
    );
  }

  Widget _buildTotalOfSalesContainer() {
    return ...;
  }

  Widget _buildComparativeChart() {
    return ...;
  }

  //reload data if the user return to the screen
  void isVisible() {
    _reloadData();
  }

  Future<void> _reloadData() async {
    //show ProgressBar and clear apiError
    setState(() {
      _loadingData = true;
      _apiErrorMessage = "";
    });

    try {
      var response = .... api call ....

      ....
      .... handle api response
      .... 

      setState(() {
        _response = response;
        _loadingData = false;
        _hasResponseFromApi = true;
      });
  }

  @override
  bool get wantKeepAlive => true;
}

使用此代码,一切正常。但是有一个问题,如果我将第一个选项卡滑动到第二个选项卡,然后再滑动到第三个选项卡,一切正常。如果我在第一个选项卡中并单击以移动到第三个选项卡而不是传递到第二个选项卡,则会出现问题。这样做,第二个选项卡崩溃并出现此错误:

Exception has occurred.
FlutterError (setState() called after dispose(): _SaleStatisticReportState#8c846(lifecycle 
state: defunct)
This error happens if you call setState() on a State object for a widget that no longer 
appears in the widget tree (e.g., whose parent widget no longer includes the widget in its. 
build). This error can occur when code calls setState() from a timer or an animation 
callback.
The preferred solution is to cancel the timer or stop listening to the animation in the 
dispose() callback. Another solution is to check the "mounted" property of this object 
before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object 
is retaining a reference to this State object after it has been removed from the tree. To 
avoid memory leaks, consider breaking the reference to this object during dispose().)

在这一行:

@override
void setState(fn) {
    if(mounted){
      super.setState(fn);
    }
}

我不知道是什么问题,可能是从第一个选项卡直接滑动到第三个选项卡的操作运行 isVisible 方法并在第二个选项卡上启动 api 调用,但是当我在第三个选项卡上时,setState( ){} 第二次崩溃。我该如何解决?

【问题讨论】:

    标签: flutter flutter-layout


    【解决方案1】:

    问题出在 _reloadData 方法中,因为你在 initstate 中调用了这个方法,所以这个方法调用 setState 即使屏幕没有挂载。要解决此问题,请尝试以下解决方法。

    在以下解决方案中,我确保屏幕已安装,如果已安装,则仅调用 setState。

    Future<void> _reloadData() async {
        //show ProgressBar and clear apiError
        if(mounted){
        setState(() {
          _loadingData = true;
          _apiErrorMessage = "";
        });
        }
    
        try {
          var response = .... api call ....
    
          ....
          .... handle api response
          .... 
          if(mounted)
          setState(() {
            _response = response;
            _loadingData = false;
            _hasResponseFromApi = true;
          });
          }
      }
    

    更新:

    class DeleteWidget extends StatefulWidget {
      @override
      _DeleteWidgetState createState() => _DeleteWidgetState();
    }
    
    class _DeleteWidgetState extends State<DeleteWidget> {
      int clockHours = 10;
      int clockMinutes = 10;
      @override
      Widget build(BuildContext context) {
        return DefaultTabController(
          length: 3,
          child: Scaffold(
            backgroundColor: Colors.teal,
            appBar: AppBar(
              title: const Text('Tabbed AppBar'),
              bottom: TabBar(
                isScrollable: true,
                tabs: [
                  Tab(text: "1"),
                  Tab(text: "2"),
                  Tab(text: "3"),
                ],
              ),
            ),
            body: TabBarView(
              children: [
                Home1(),
                Home2(),
                Home3(),
              ],
            ),
          ),
        );
      }
    }
    
    class Home1 extends StatefulWidget {
      @override
      _Home1State createState() => _Home1State();
    }
    
    class _Home1State extends State<Home1> {
      @override
      void initState() {
        super.initState();
        callme();
      }
    
      @override
      void setState(fn) {
        if (mounted) {
          super.setState(fn);
        }
      }
    
      callme() async {
        await Future.delayed(Duration(seconds: 1));
    
        setState(() {});
      }
    
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }
    
    class Home2 extends StatefulWidget {
      @override
      _Home2State createState() => _Home2State();
    }
    
    class _Home2State extends State<Home2>
        with AutomaticKeepAliveClientMixin<Home2> {
      @override
      void initState() {
        super.initState();
    
        callme();
      }
    
      @override
      void setState(fn) {
        if (mounted) {
          super.setState(fn);
        }
      }
    
      callme() async {
        await Future.delayed(Duration(seconds: 1));
        // if (mounted) {
        setState(() {});
        //}
      }
    
      @override
      Widget build(BuildContext context) {
        super.build(context);
        return Container();
      }
    
      @override
      bool get wantKeepAlive => true;
    }
    
    class Home3 extends StatefulWidget {
      @override
      _Home3State createState() => _Home3State();
    }
    
    class _Home3State extends State<Home3>
        with AutomaticKeepAliveClientMixin<Home3> {
      @override
      void initState() {
        super.initState();
        callme();
      }
    
      callme() async {
        await Future.delayed(Duration(seconds: 1));
        setState(() {});
      }
    
      @override
      Widget build(BuildContext context) {
        super.build(context);
        return Container();
      }
    
      @override
      bool get wantKeepAlive => true;
    }
    

    更新:

    如何从父部件调用子方法。

    class DeleteWidget extends StatefulWidget {
      @override
      _DeleteWidgetState createState() => _DeleteWidgetState();
    }
    
    class _DeleteWidgetState extends State<DeleteWidget> {
      GlobalKey<Home1State> _keyChild1;
    
      @override
      void initState() {
        super.initState();
        _keyChild1 = GlobalKey();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              RaisedButton(
                child: Text("press"),
                onPressed: () {
                  _keyChild1.currentState.callme();
                },
              ),
              Home1(
                key: _keyChild1,
              )
            ],
          ),
        );
      }
    }
    
    class Home1 extends StatefulWidget {
      Home1({Key key}) : super(key: key);
    
      @override
      Home1State createState() => Home1State();
    }
    
    class Home1State extends State<Home1> {
      callme() {
        print("method call from parent");
        setState(() {});
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
          child: Text("Home1"),
        );
      }
    }
    

    【讨论】:

    • 同样的问题,我已经重写了setState方法来检查是否已挂载,但直到现在还没有解决方案。
    • 您添加的错误是由_SaleStatisticReportState 类引发的,那么您是否也像在_PeriodResumeReportState 类中那样确保它们?
    • 是的,3个report class几乎都是ctrl c + v,唯一的区别就是api和屏幕体:/
    • 我想你忘了在那个类中覆盖 super ,因为我在我的演示中尝试过,如果我覆盖 super 方法它工作正常。
    • 我添加了我的演示代码,它是否适合您。如果不能,您能告诉我如何重现错误,以便我尝试。当我尝试时,它对我来说很好。
    猜你喜欢
    • 2017-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-03
    • 1970-01-01
    相关资源
    最近更新 更多