Thomas Braun
Thomas Braun

Reputation: 535

setState not causing UI to refresh

Using a stateful widget, I have this build function:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            Icon(MdiIcons.atom),
            Text("Message Channel")
          ],
        )
      ),

      body: compileWidget(context),

      bottomSheet: Row(
        children: [
          Expanded(
              child: DefaultTextFormField(true, null, hintText: "Message ...", controller: messageField)
          ),

          IconButton(
              icon: Icon(Icons.send),
              onPressed: onSendPressed
          )
        ],
      ),
    );
  }

As you can see in the body, there is a compileWidget function defined as:

  FutureBuilder<Widget> compileWidget(BuildContext context) {
    return FutureBuilder(
      future: createWidget(context),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            print("DONE loading widget ");
            return snapshot.data;
          } else {
            return Container(
              child: Center(
                child: CircularProgressIndicator()
              ),
            );
          }
        }
    );
  }

In turn, the createWidget function looks like this:

List<Widget> bubbles;

  Future<Widget> createWidget(BuildContext context) async {
    await updateChatList();
    // when working, return listview w/ children of bubbles
    return Padding(
      padding: EdgeInsets.all(10),
      child: ListView(
        children: this.bubbles,
      ),
    );
  }

// update chat list below

  Future<void> updateChatList({Message message}) async {
    if (this.bubbles != null) {
      if (message != null) {
        print("Pushing new notification into chat ...");
        this.bubbles.add(bubbleFrom(message));
      } else {
        print("Update called, but nothing to do");
      }
    } else {
      var initMessages = await Message.getMessagesBetween(this.widget.implicatedCnac.implicatedCid, this.widget.peerNac.peerCid);
      print("Loaded ${initMessages.length} messages between ${this.widget.implicatedCnac.implicatedCid} and ${this.widget.peerNac.peerCid}");
      // create init bubble list

      this.bubbles = initMessages.map((e) {
        print("Adding bubble $e");
        return bubbleFrom(e);
      }).toList();
      print("Done loading bubbles ...");
    }
  }

The chat bubbles populate the screen in good order. However, once a new message comes-in and is received by:

  StreamSubscription<Message> listener;

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

    this.listener = Utils.broadcaster.stream.stream.listen((message) async {
      print("{${this.widget.implicatedCnac.implicatedCid} <=> ${this.widget.peerNac.peerCid} streamer received possible message ...");
      if (this.widget.implicatedCnac.implicatedCid == message.implicatedCid && this.widget.peerNac.peerCid == message.peerCid) {
        print("Message meant for this screen!");
        await this.updateChatList(message: message);
        setState(() {});
      }
    });
  }

The "message meant for this screen!" prints, then within updateChatList, "Pushing new notification into chat ..." prints, implying that the bubble gets added to the array. Finally, setState is called, yet, the additional bubble doesn't get rendered. What might be going wrong here?

Upvotes: 2

Views: 1164

Answers (2)

Thomas Braun
Thomas Braun

Reputation: 535

You need to ensure that the ListView has a UniqueKey:

ScrollController _scrollController = ScrollController();
Stream<Widget> messageStream;

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

    // use RxDart to merge 2 streams into one stream
    this.messageStream = Rx.merge<Message>([Utils.broadcaster.stream.stream.where((message) => this.widget.implicatedCnac.implicatedCid == message.implicatedCid && this.widget.peerNac.peerCid == message.peerCid), this.initStream()]).map((message) {
      print("[MERGE] stream recv $message");
      this.widget.bubbles.add(bubbleFrom(message));

      // this line below to ensure that, as new items get added, the listview scrolls to the bottom
      this._scrollController = _scrollController.hasClients ? ScrollController(initialScrollOffset:  _scrollController.position.maxScrollExtent) : ScrollController();

      return ListView(
        key: UniqueKey(),
        controller: this._scrollController,
          keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
          padding: EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 50),
          children: this.widget.bubbles,
      );
    });

  }

Note: As per Erfan's directions, the context is best handled with a StreamBuilder. As such, I changed to a StreamBuilder, thus reducing the amount of code needed

Upvotes: 0

Erfan Jazeb Nikoo
Erfan Jazeb Nikoo

Reputation: 988

First of all, let me explain how setState works according to this link.

Whenever you change the internal state of a State object, make the change in a function that you pass to setState. Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.

In your case, I would say it's just a convention. You can define a message object in your stateful class and update it inside the listener and your setstate like this:

setState(() { this.message = message; });

Caution:

Before any changes, you need to check this question

Usage of FutureBuilder with setState

Because using setstete with FutureBuilder can make an infinite loop in StatefulWidget which reduces performance and flexibility. Probably, you should change your approach to design your screen because FutureBuilder automatically updates the state each time tries to fetch data.

Update:

There is a better solution for your problem. Because you are using a stream to read messages, you can use StreamBuilder instead of the listener and FutureBuilder. It brings less implementation and more reliability for services that provide a stream of data. Every time the StreamBuilder receives a message from the Stream, it will display whatever you want with the snapshot of data.

Upvotes: 2

Related Questions