У меня есть состояние гонки данных из потока 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();
Вы, наверное, догадались, что идея состоит в том, чтобы загрузить JSON из веб-службы, а затем предоставить его всему приложению через глобальную переменную. Это работает без ошибок для всего, кроме класса ContentDetail
.
Этот класс ContentDetail
является почти копией другого компонента, который делает почти то же самое, он фильтрует по тегу slug и отображает список Content
. Здесь требуется только 1 элемент из потока, Content
с определенным свойством slug
.
class Content {
// ...
final String slug;
}
Как вы можете видеть, я передаю slug
в конструкторе ContentDetail
и мой_ContentDetailState
получает доступ к свойству через widget.slug
.
Функция _getContent()
класса ContentListByTagSlug
в сравнении:
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 элемент отэто не так (см. 1-й фрагмент кода).
Вот как я определяю FlatButton
, чтобы открыть страницу ContentDetail
:
FlatButton(
child: Text('Read more'),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ContentDetail(slug: content[index].slug))),
),
Это верно, слаг пройденкак показывает результат функции печати.
Я не уверен, что делать, чтобы убедиться, что переменная _content
заполняется перед выполнением функции сборки. В Angular создается распознаватель, который затем заполняет данные перед созданием / инициализацией компонента.
В Flutter?
Примечание. Я не использую пакет bloc, но помещаю потоки ввыделенный класс, затем используйте setState для обновления состояния в прослушивающих классах, поскольку пакет bloc кажется чрезмерным для этого сценария. 3 виджета (4, если считать ящик) 1, отображающий нефильтрованный список 2, отображающий список, отфильтрованный по тегу slug 3, отображающий один элемент, отфильтрованный по тегу slug, только у последнего есть ошибки.
update1: я даже удалил if (this.mounted)
проверьте _getContent()
, и я снова вызываю _getContent()
в компоновщике, если _content
равно нулю. Затем я изменил _content
на Список и получаю _content.first.title
, что приводит к
Bad state: No element
Но виджет по-прежнему отображается правильно. Таким образом, кажется, что есть 2 вызова виджета. Тот, который выбрасывает ошибки, который отбрасывается, и тот, который не, который сохраняется. Я не знаком с внутренностями, так что это мое лучшее предположение.