Trying to grasp the usage of streams in Flutter, I followed the interesting example found here https://github.com/tensor-programming/flutter_streams, which shows how to get data from an http call, and fill lazily a (very big) list of photo objects/widgets.
The whole thing boils down to a stream and a subscription to it, which calls setState for every event in the stream, while adding an element to the list of items [for debug, I put a print("ADD!")
statement to be sure that the call is working as intended. And it does].
The stream is "filled" after an http call, which provides the big list of raw demo data.
And to see when Flutter (re)builds the main body of the PhotoList widget, I put a nice print("BUILD!!!")
just before the Scaffold is returned.
The whole code of the flutter program is this:
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Photo Streamer',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: PhotoList(),
);
}
}
class PhotoList extends StatefulWidget {
@override
PhotoListState createState() => PhotoListState();
}
class PhotoListState extends State<PhotoList> {
StreamController<Photo> streamController;
List<Photo> list = [];
@override
void initState() {
super.initState();
streamController = StreamController.broadcast();
streamController.stream.listen((p) => {
setState(() {
list.add(p);
print("ADD!");
})
});
load(streamController);
}
load(StreamController<Photo> sc) async {
String url = "https://jsonplaceholder.typicode.com/photos";
var client = new http.Client();
var req = new http.Request('get', Uri.parse(url));
var streamedRes = await client.send(req);
streamedRes.stream
.transform(utf8.decoder)
.transform(json.decoder)
.expand((e) => e)
.map((map) => Photo.fromJsonMap(map))
.pipe(sc);
}
@override
void dispose() {
super.dispose();
streamController?.close();
streamController = null;
}
@override
Widget build(BuildContext context) {
print("BUILD!!!");
return Scaffold(
appBar: AppBar(
title: Text("Photo Streams"),
),
body: Center(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) => _makeElement(index),
),
),
);
}
Widget _makeElement(int index) {
if (index >= list.length) {
return null;
}
return Container(
padding: EdgeInsets.all(5.0),
child: Padding(
padding: EdgeInsets.only(top: 200.0),
child: Column(
children: <Widget>[
Image.network(list[index].url, width: 150.0, height: 150.0),
Text(list[index].title),
],
),
));
}
}
class Photo {
final String title;
final String url;
Photo.fromJsonMap(Map map)
: title = map['title'],
url = map['url'];
}
I was expecting that each time setState is called (adding an element to the list of objects to be displayed), a new rebuild of the widget tree would be called, with prints looking like:
ADD!
BUILD!!!
ADD!
BUILD!!!
... (etc.)
But this is not happening. What I see is:
BUILD!!!!
ADD!
ADD!
...
...
ADD!
BUILD!!!!
And here come my questions:
1) why does Flutter rebuilds the widgets just twice, even though setState is called every time an item is added to the list objects?
2) if the whole list is filled in one-shot before repainting, what is the point in using a stream?
and, last but not least -in general - :
3) if the http call returns ALL THE DATA at once (as it does), then what's the point in using a stream to fill a (long) list of items, and not fill the list directly - after the async/await event has been triggered?
[I am an old algorithmist, and these things are driving me crazy :) ].
I will be for ever grateful to whoever will shine a light on these issues! Thanks.