My ApplicationBloc is the root of the widget tree. In the bloc's constructor I'm listening to a stream from a repository that contains models decoded from JSON and forwarding them to another stream which is listened to by StreamBuilder.
I expected that StreamBuilder would receive models one by one and add them to AnimatedList. But there's the problem: StreamBuilder's builder fires only once with the last item in the stream.
For example, several models lay in the local storage with ids 0, 1, 2 and 3. All of these are emitted from repository, all of these are successfully put in the stream controller, but only the last model (with id == 3) appears in the AnimatedList.
Repository:
class Repository {
static Stream<Model> load() async* {
//...
for (var model in models) {
yield Model.fromJson(model);
}
}
}
Bloc:
class ApplicationBloc {
ReplaySubject<Model> _outModelsController = ReplaySubject<Model>();
Stream<Model> get outModels => _outModelsController.stream;
ApplicationBloc() {
TimersRepository.load().listen((model) => _outModelsController.add(model));
}
}
main.dart:
void main() {
runApp(
BlocProvider<ApplicationBloc>(
bloc: ApplicationBloc(),
child: MyApp(),
),
);
}
//...
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
final ApplicationBloc appBloc = //...
return MaterialApp(
//...
body: StreamBuilder(
stream: appBloc.outModels,
builder: (context, snapshot) {
if (snapshot.hasData) {
var model = snapshot.data;
/* inserting model to the AnimatedList */
}
return AnimatedList(/* ... */);
},
),
);
}
}
Interesting notice: in the StreamBuilder's _subscribe() method onData() callback triggers required number of times but build() method fires only once.
You need a Stream that outputs a List<Model instead of a single element. Also, listening to a stream to add it to another ReplaySubject will delay the output stream by 2 (!!!) frames, so it would be better to have a single chain.
class TimersRepository {
// maybe use a Future if you only perform a single http request!
static Stream<List<Model>> load() async* {
//...
yield models.map((json) => Model.fromJson(json)).toList();
}
}
class ApplicationBloc {
Stream<List<Model>> get outModels => _outModels;
ValueConnectableObservable<List<Model>> _outModels;
StreamSubscription _outModelsSubscription;
ApplicationBloc() {
// publishValue is similar to a BehaviorSubject, it always provides the latest value,
// but without the extra delay of listening and adding to another subject
_outModels = Observable(TimersRepository.load()).publishValue();
// do no reload until the BLoC is disposed
_outModelsSubscription = _outModels.connect();
}
void dispose() {
// unsubcribe repo stream on dispose
_outModelsSubscription.cancel();
}
}
class _MyAppState extends State<MyApp> {
ApplicationBloc _bloc;
#override
Widget build(BuildContext context) {
return StreamBuilder<List<Model>>(
stream: _bloc.outModels,
builder: (context, snapshot) {
final models = snapshot.data ?? <Model>[];
return ListView.builder(
itemCount: models.length,
itemBuilder: (context, index) => Item(model: models[index]),
);
},
);
}
}
Related
I have a ListView.builder widget wrapped inside a RefreshIndicator and then a FutureBuilder. Refreshing does not update my list, I have to close the app and open it again but the refresh code does the same as my FutureBuilder.
Please see my code below, when I read it I expect the widget tree to definitely update.
#override
void initState() {
super.initState();
taskListFuture= TaskService().getTasks();
}
#override
Widget build(BuildContext context) {
return Consumer<TaskData>(builder: (context, taskData, child) {
return FutureBuilder(
future: taskListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
taskData.tasks = (snapshot.data as ApiResponseModel).responseBody;
return RefreshIndicator(
onRefresh: () async {
var responseModel = await TaskService().getTasks();
setState(() {
taskData.tasks = responseModel.responseBody;
});
},
child: ListView.builder(
...
...
Let me know if more code is required, thanks in advance!
Points
I am using a StatefulWidget
Task data is a class that extends ChangeNotifier
When I debug the refresh I can see the new data in the list, but the UI does not update
getTasks()
Future<ApiResponseModel> getTasks() async {
try {
var _sharedPreferences = await SharedPreferences.getInstance();
var userId = _sharedPreferences.getString(PreferencesModel.userId);
var response = await http.get(
Uri.parse("$apiBaseUrl/$_controllerRoute?userId=$userId"),
headers: await authorizeHttpRequest(),
);
var jsonTaskDtos = jsonDecode(response.body);
var taskDtos= List<TaskDto>.from(
jsonTaskDtos.map((jsonTaskDto) => TaskDto.fromJson(jsonTaskDto)));
return ApiResponseModel(
responseBody: taskDtos,
isSuccessStatusCode: isSuccessStatusCode(response.statusCode));
} catch (e) {
return null;
}
}
The issue here seems to be that you are updating a property that is not part of your StatefulWidget state.
setState(() {
taskData.tasks = responseModel.responseBody;
});
That sets a property part of TaskData.
My suggestion is to only use the Consumer and refactor TaskService so it controls a list of TaskData or similar. Something like:
Provider
class TaskService extends ChangeNotifier {
List<TaskData> _data;
load() async {
this.data = await _fetchData();
}
List<TaskData> get data => _data;
set data(List<TaskData> data) {
_data = data;
notifyListeners();
}
}
Widget
class MyTaskList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<TaskService>(builder: (context, service, child) {
return RefreshIndicator(
onRefresh: () {
service.getTasks();
},
child: ListView.builder(
itemCount: service.data.length,
itemBuilder: (BuildContext context, int index) {
return MyTaskItem(data:service.data[index]);
},
),
);
});
}
}
and make sure to call notifyListeners() in the service.getTasks() method to make the Consumer rebuild
I think (someone will correct me if I'm wrong) the problem is that you are using the FutureBuilder, once it's built, you need to refresh to whole widget for the FutureBuilder to listen to changes. I can suggest a StreamBuilder that listens to any changes provided from the data model/api/any kind of stream of data. Or better yet, you can use some sort of state management like Provider and use Consumer from the Provider package that notifies the widget of any changes that may occurred.
I have a Flutter widget which gets data from a server and renders a List. After getting the data, I parse the data and convert it to an internal object in my application, so the function is something like this:
Future<List<Data>> getData(Thing thing) async {
var response = await http.get(Uri.parse(MY_URL));
// do some processing
return data;
}
After that, I've defined a stateful widget which calls this function and takes the future to render a List.
class DataList extends StatefulWidget {
const DataList({Key key}) : super(key: key);
#override
_DataListState createState() => _DataListState();
}
class _DataListState extends State<DataList> {
Widget createListView(BuildContext context, AsyncSnapshot snapshot) {
List<Data> values = snapshot.data;
if (values.isEmpty) {
return NoResultsWidget('No results.');
}
return ListView.builder(
itemCount: values.length,
itemBuilder: (BuildContext context, int index) {
return values[index];
},
);
}
#override
Widget build(BuildContext context) {
var data = getSomething().then((thing) => getData(thing));
return FutureBuilder(
future: data,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return CustomErrorWidget('Error');
case ConnectionState.waiting:
return LoadingWidget();
default:
if (snapshot.hasError) {
return CustomErrorWidget('Error.');
} else {
return createListView(context, snapshot);
}
}
},
);
}
}
Now, the code works just fine in this manner. But, when I try to move my data to be a class variable (of type Future<List>) that I update through the initState method, the variable just never updates. Example code below:
class _DataListState extends State<DataList> {
Future<List<Data>> data;
....
#override
void initState() {
super.initState();
updateData();
}
void updateData() {
data = getSomething().then((thing) => getData(thing));
}
....
}
I want to add a refresh indicator to update the data on refresh, and to do that I need to make my data a class variable to update it on refresh, but I can't seem to figure out how to make my data part of the state of the stateful widget and have it work. any help or guides to a github code example would be appreciated.
You need to wrap the assignment of the data variable in setState so that Flutter knows the variable changed and rebuilds your widget.
For example:
void updateData() {
setState(() {
data = getSomething().then((thing) => getData(thing));
});
}
Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});
I'm building an app that includes discussion threads. Each Thread has a subcollection of Replies, and each Reply has a map of Reactions like this. So each Thread's subcollection would look like this if JSONified:
{
'replyDocument1': {
'authorName': 'Steve Dave',
'text': 'this is a reply',
'reactions': {'👍': ['userid1', 'userid2']}
},
...
}
I'm trying to use a Stream/Sink set up to listen to changes in the Replies subcollection. ie: Rebuild the Replies ListView anytime someone adds a Reaction to any Reply in the subcollection.
This is what I've got so far. The models and RepliesApi class don't do anything exciting so I've left them out.
class RepliesBloc {
final String threadId;
RepliesApi _api;
final _controller = StreamController<List<Reply>>.broadcast();
Stream<List<Reply>> get stream =>
_controller.stream.asBroadcastStream();
StreamSink<List<Reply>> get sink => _controller.sink;
RepliesBloc(this.threadId) {
this._api = RepliesApi(this.threadId);
this
._api
.collectionReference
.getDocuments()
.asStream()
.listen(_repliesUpdated);
this._api.collectionReference.snapshots().listen(_repliesUpdated);
}
void _repliesUpdated(QuerySnapshot querySnapshot) {
List<Reply> replies = _api.fromQuerySnapshot(querySnapshot);
this.sink.add(replies);
}
void dispose() {
_controller.close();
}
}
class ThreadReplyList extends StatefulWidget {
ThreadReplyList(this.threadId, {Key key}) : super(key: key);
final String threadId;
#override
_ThreadReplyListState createState() => _ThreadReplyListState();
}
class _ThreadReplyListState extends State<ThreadReplyList> {
RepliesBloc _bloc;
#override
void initState() {
super.initState();
this._bloc = RepliesBloc(this.widget.threadId);
}
#override
Widget build(BuildContext context) {
dev.log('starting ThreadReplyList.build', name: "ThreadReplyList.build");
return StreamBuilder(
stream: this._bloc.stream,
builder: (context, snapshot) {
return ListView.separated(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ThreadReply(snapshot.data[index]);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox.shrink();
},
);
});
}
}
This loads the initial data fine and if I change the collection, eg: add or remove a Reply, the ListView rebuilds. However if I modify a Reply, eg: update the reactions map on a single Reply, nothing happens.
I've seen solutions which add listeners to each document in a QuerySnapshot but it's unclear to me how to implement that here.
I'm trying to write a simple Flutter app using the Google Maps plugin. I need to use multiple BLoC/ChangeNotifier objects in order to manage the object shown on-screen.
The issue comes out when I call notifyListeners() on a ChangeNotifier. The method which calls notifyListeners() completes its execution, and then the app freezes completely (no widget update, unable to interact with existing widgets).
I've tried to understand where's the problem: the only thing I understood is that it works fine while CompaniesData (which is the ChangeNotifier that causes the problem) is empty.
class CompaniesData extends ChangeNotifier {
Map<MarkerId, Company> _companiesMap;
set companies(Set<Company> companies) {
_companiesMap = companies != null
? Map.fromIterable(
companies,
key: (company) => MarkerId(company.id.toString()),
value: (company) => company,
)
: null;
notifyListeners();
;
}
bool get available => _companiesMap != null;
Company companyWithId(MarkerId id) => available ? _companiesMap[id] : null;
Map<MarkerId, Company> get companiesIfAvailable =>
available ? _companiesMap : Map();
Iterable<Company> companiesFromIds(BuildContext context, Set<int> ids) {
Set<int> idsCopy = Set.from(ids);
return companiesIfAvailable.entries
.where((entry) => idsCopy.remove(entry.value.id))
.map<Company>((entry) => entry.value);
}
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Consumer<CompaniesData>(
builder: (context, data, child) {
return BlocBuilder(
bloc: BlocProvider.of<ShownCompaniesBloc>(context),
builder: (context, shownCompaniesState) {
return BlocBuilder(
bloc: BlocProvider.of<FavoriteCompaniesBloc>(context),
builder: (context, favoriteCompaniesState) {
return BlocBuilder(
bloc: BlocProvider.of<MapPropertiesBloc>(context),
builder: (context, mapPropertiesState) {
CompaniesData data =
Provider.of<CompaniesData>(context, listen: false);
// ...
As you can see, the build method contains multiple nested BLoC/Consumer objects.
#override
void initState() {
_fetchCompanies();
super.initState();
}
void _fetchCompanies() {
findUser().then((location) {
Set<Company> companies = Set.from([Company.fake()]);
// CompaniesData.companies is a setter, which calls
// notifyListeners
_companiesData.companies = companies;
});
}
I don't get error messages, exception, my app simply dies after the end of the execution of the callback given to findUser().then().
EDIT:
I changed the code a little bit, and I figured out that the problem isn't notifyListeners (or at least it isn't now).
final Completer<Map<MarkerId, Company>> _companiesData = Completer();
_AeroMainViewState() {
findUser()
.then(_fetchCompanies)
.then((companies) => _companiesData.complete(Map.fromIterable(
companies,
key: (company) => MarkerId(company.id.toString()),
value: (company) => company,
)));
}
Future<Set<Company>> _fetchCompanies(LatLng location) async =>
Set.from([Company.fake()]);
// ...
child: FutureBuilder<Map<MarkerId, Company>>(
future: _companiesData.future,
builder: (context, snapshot) {
// this builder function isn't called at all
// when the Completer _companiesData is completed
if (snapshot.connectionState == ConnectionState.done) {
return Provider<Map<MarkerId, Company>>.value(
value: snapshot.data,
child: // ...
} else {
return Center(child: CircularProgressIndicator());
}
}),
// ...
Removing the ChangeNotifier doesn't fix the issue.
I post my error for future reference. I was doing this in a class:
static Stream<Obj1> stream() async* {
while (true) {
yield Obj1();
}
}
_subscription = Obj1.stream().listen((event) {
// do something...
}
Since the Stream contains potentially an infinite number of objects, the subscription to that stream was blocking the main (and only) thread.