【问题标题】:Race condition in Stateful widget with parameter带参数的有状态小部件中的竞争条件
【发布时间】:2020-02-14 11:41:00
【问题描述】:

我遇到了来自 BehaviorSubject 流的数据竞争条件,在构建函数返回之前没有填充/更新状态。

content-detail.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:domain_flutter/application-bloc.dart';
import 'package:domain_flutter/content.dart';
import 'package:domain_flutter/tag_chips.dart';
import 'package:cached_network_image/cached_network_image.dart';

class ContentDetail extends StatefulWidget {
  final String slug;

  ContentDetail({Key key, this.slug}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _ContentDetailState();
}

class _ContentDetailState extends State<ContentDetail> {
  Content _content = Content();

  _getContent() {
    print(widget.slug);
      applicationBloc.contentOutput.map( (contents) =>
      contents.where((item) => item.slug == widget.slug).toList())
          .listen((data) => {
                if (this.mounted) {
                  setState(() => _content = data.first)
                }
              });
  }

  @override
  void initState() {
    super.initState();
    _getContent();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: _content != null ? Text(_content.title) : Text(''),
      ),
      body:
      SafeArea(
        child: Card(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              Container(
                  child: ListTile(
                    contentPadding: EdgeInsets.all(0),
                title: Text(_content.title,style: TextStyle(fontWeight: FontWeight.bold)),
                subtitle: Text(
                  DateFormat('dd.MM.yyyy').format(_content.changed),
                  style: TextStyle(fontSize: 12),
                ),
              )),
              TagChips(_content.tags),
              CachedNetworkImage(
                  placeholder: (context, url) => CircularProgressIndicator(),
                  imageUrl: 'https://domain.tld/files/${_content.image}'),
            ],
          ),
        ),
      ),
    );
  }
}

小部件呈现,但在呈现之前我收到一个错误。 如果内容未初始化,我会收到另一个错误。

我所说的内容初始化是指

Content _content = Content();

内容初始化:

The following assertion was thrown building ContentDetail(dirty, state: _ContentDetailState#15727):
A non-null String must be provided to a Text widget.
'package:flutter/src/widgets/text.dart':
Failed assertion: line 269 pos 10: 'data != null'


Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=BUG.md

User-created ancestor of the error-causing widget was: 
  MaterialApp file:///home/darko/AndroidStudioProjects/domain_flutter/lib/main.dart:24:12
When the exception was thrown, this was the stack: 
#2      new Text (package:flutter/src/widgets/text.dart:269:10)
#3      _ContentDetailState.build (package:domain_flutter/content_detail.dart:41:35)
#4      StatefulElement.build (package:flutter/src/widgets/framework.dart:4047:27)
#5      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3941:15)
#6      Element.rebuild (package:flutter/src/widgets/framework.dart:3738:5)
...

内容未初始化:

The following NoSuchMethodError was thrown building ContentDetail(dirty, state: _ContentDetailState#2be0b):
The getter 'title' was called on null.
Receiver: null
Tried calling: title

User-created ancestor of the error-causing widget was: 
  MaterialApp file:///home/darko/AndroidStudioProjects/domain_flutter/lib/main.dart:24:12
When the exception was thrown, this was the stack: 
#0      Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5)
#1      _ContentDetailState.build (package:domain_flutter/content_detail.dart:53:38)
#2      StatefulElement.build (package:flutter/src/widgets/framework.dart:4047:27)
#3      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3941:15)
#4      Element.rebuild (package:flutter/src/widgets/framework.dart:3738:5)
...

所以是的,当它试图渲染组件时没有数据......或者是 Widget/s。

我应该显示我的applicationBloc

import 'dart:async';
import 'package:domain_flutter/application-entities.dart';
import 'package:domain_flutter/content.dart';
import 'package:domain_flutter/tag.dart';
import 'package:rxdart/rxdart.dart';

class ApplicationBloc {
  final _applicationEntities = ApplicationEntities();

  Sink<List<Content>> get contentInput => _contentInputController.sink;
  Sink<List<Tag>> get tagInput => _tagInputController.sink;

  Stream<List<Content>> get contentOutput => _contentOutputSubject.stream;
  Stream<List<Tag>> get tagOutput => _tagOutputSubject.stream;

  final _contentInputController = StreamController<List<Content>>();
  final _tagInputController = StreamController<List<Tag>>();

  final _contentOutputSubject = BehaviorSubject<List<Content>>();
  final _tagOutputSubject = BehaviorSubject<List<Tag>>();

  ApplicationBloc() {
    _contentInputController.stream.listen(_handleContentInput);
    _tagInputController.stream.listen(_handleTagInput);
  }

  void dispose() {
    _contentInputController.close();
    _contentOutputSubject.close();
    _tagInputController.close();
    _tagOutputSubject.close();
  }

  void _handleContentInput(List<Content> contentList) {
    _applicationEntities.updateContent(contentList);
    _contentOutputSubject.add(contentList);
  }
  void _handleTagInput(List<Tag> tagList) {
    _applicationEntities.updateTags(tagList);
    _tagOutputSubject.add(tagList);
  }
}

final applicationBloc = ApplicationBloc();

您可能猜到了,这个想法是从 Web 服务加载 JSON,然后通过全局变量在应用程序范围内提供它。 除了 ContentDetail 类之外,这对所有内容都没有错误。

这个ContentDetail 类几乎是另一个功能几乎相同的组件的副本,它通过标签段过滤并呈现Content 的列表。 这里只需要流中的 1 个项目,Content 具有特定的 slug 属性。

class Content {
// ...
  final String slug;
}

如您所见,我在ContentDetail 构造函数中传递了slug,而我的_ContentDetailState 正在通过widget.slug 访问其属性。

比较ContentListByTagSlug类的_getContent()函数:

  void _getContent() {
    applicationBloc.contentOutput.map(
            (contents) => contents.where(
                    (item) => item.tags.any((tag) => tag.slug == widget.tag.slug)
            ).toList()
    )
        .listen((data) => {
              if (this.mounted) {
                  setState(() => {content = data})
                }
            });
  }

这行得通,但只能从中获取 1 个项目,但不能(参见第一个代码 sn-p)。

这就是我定义FlatButton 以打开ContentDetail 页面的方式:

            FlatButton(
              child: Text('Read more'),
              onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) =>
                          ContentDetail(slug: content[index].slug))),
            ),

有效,slug按照print函数的结果传递。

我不确定在执行构建函数之前如何确定 _content 变量已填充。 在 Angular 中创建一个解析器,然后在创建/初始化组件之前填充数据。

在 Flutter 中?

请注意,我没有使用 bloc 包,而是将流放在专用类中,然后使用 setState 更新侦听类中的状态,因为 bloc 包在这种情况下似乎有点矫枉过正。 3 个小部件(如果算上抽屉,则为 4 个) 1 显示未过滤的列表 2 显示由标签 slug 过滤的列表 3 显示按标签 slug 过滤的一项 只有后者有错误。

update1:​​我什至删除了_getContent() 中的if (this.mounted) 签入,如果_content 为空,我将在构建器中再次调用_getContent()。 然后我将 _content 更改为 List 并得到 _content.first.title 导致

Bad state: No element

但小部件仍然正确呈现。 所以看起来小部件有 2 次调用。一个抛出错误,被丢弃,一个没有,被保留。我不熟悉内部结构,所以这是我的最佳猜测。

【问题讨论】:

    标签: flutter dart rxdart


    【解决方案1】:

    this answer 回答了这个问题。

    我从中得到的是

    • 不要初始化打算由流填充的变量
    • 在builder中提供检查值是否要填写null
    • 如果 null 呈现加载屏幕,否则呈现具有填充状态的所需小部件

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-11-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多