2

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.

RNani
  • 49
  • 1
  • 5
  • https://stackoverflow.com/questions/59555999/setstate-doesnt-execute-until-the-enclosing-function-returns may help about setState and build – HII Apr 19 '20 at 15:41
  • Thanks @user for the link. The example though, as far I understand, does not really fit with my specific issues: all crucial functions in my example are async, therefore the functions involved do "exit" and let the event loop process pending events. Or am I missing something? – RNani Apr 19 '20 at 15:53
  • this is because everything happens in one single frame - `setState` could be called 100s times but still one rebuild is performed - if you want to "see" the frames check `debugPrintBeginFrameBanner` and/or `debugPrintEndFrameBanner` – pskink Apr 19 '20 at 16:16
  • @pskink, thanks for your answer, and for your patience. But why are you saying that everything happens in a single frame? Doesn't the async nature of the load(StreamController sc) function allow for the event loop to... loop? Isn't the stream built so that its events flow asynchronously? Hmmm... If this is not the case, then I can't really grasp the advantage of the stream approach in reading http data. – RNani Apr 19 '20 at 16:26
  • tried to set `debugPrintBeginFrameBanner = true`? i am pretty sure it happens in one frame but it is better to double check that – pskink Apr 19 '20 at 16:29
  • I'll give these hints a try! :) Thanks – RNani Apr 19 '20 at 16:38
  • @pskink, I tried using debugPrintBeginFrameBanner=true, and you are right. All the stream transforms and messages are managed in one single frame. So: I kind of get the "elegance" of the stream approach, but frankly it is a conceptual overkill for this user case. Don't you agree? – RNani Apr 19 '20 at 17:12

1 Answers1

1

Following @pskink suggestion, it comes out that all activities related to the stream, used in the example, are all managed in a single frame. This somehow explains why setState does not trigger the build method of the PhotoListState element. Thus, question 1 has now an answer.

RNani
  • 49
  • 1
  • 5