icazevedo icazevedo - 1 month ago 14
Dart Question

setState doesn't update the user interface

I've been facing some problems related to the setState function while using Stateful Widgets that updates itself with the help of Timers. The code below show 2 main classes that replicate how I came to find this error. The Text Widget "Lorem" should be inserted within 10 seconds - and it is - but it's never shown. I tried to debug the array "Items" and it does contain the "lorem" Text Widget after 5 seconds, as it should. The "build" function runs but doesn't make any difference in the UI.

class textList extends StatefulWidget {

@override
State<StatefulWidget> createState() =>
new _textListState();
}

class _textListState extends State<textList>
with TickerProviderStateMixin {

List<Widget> items = new List();
Widget lorem = new textClass("Lorem");
Timer timer;

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

items.add(new textClass("test"));
items.add(new textClass("test"));

timer = new Timer.periodic(new Duration(seconds: 5), (Timer timer) {
setState(() {
items.removeAt(0);
items.add(lorem);
});
});
}

@override
void dispose() {
super.dispose();
timer.cancel();
}

@override
Widget build(BuildContext context) {
Iterable<Widget> content = ListTile.divideTiles(
context: context, tiles: items).toList();

return new Column(
children: content,
);
}
}

class textClass extends StatefulWidget {
textClass(this.word);

final String word;

@override
State<StatefulWidget> createState() =>
new _textClass(word);
}

class _textClass extends State<textClass>
with TickerProviderStateMixin {
_textClass(this.word);

String word;
Timer timer;

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

timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
setState(() {
word += "t";
});
});
}

@override
void dispose() {
super.dispose();
timer.cancel();
}


@override
Widget build(BuildContext context) {
return new Text(word);
}
}


This is not how I came to find this error but this is the simplest way to replicate it. The main idea is: The children texts should keep updating themselves (in this case, adding "t"s in the end) and, after 5 seconds, the last of them should be replaced for the Text Widget "Lorem", what does happen in the list but not in the UI.

Answer Source

Here's what's wrong:

  • A State should never have any constructor arguments. Use the widget property to get access to final properties of the associated StatefulWidget.
  • Flutter is reusing your _textClass instance because the class name and keys match. This is a problem since you only set widget.word in initState so you're not picking up the new word configuration information. You can fix this either by giving the StatefulWidget instances unique keys to disambiguate them and cause the old State to be disposed, or you can keep around the old State and implement didUpdateWidget. The latter approach is shown below.

video

import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    home: new Scaffold(
      appBar: new AppBar(title: new Text('Example App')),
      body: new textList(),
    ),
  ));
}

class textList extends StatefulWidget {

  @override
  State<StatefulWidget> createState() =>
      new _textListState();
}

class _textListState extends State<textList>
    with TickerProviderStateMixin {

  List<Widget> items = new List();
  Widget lorem = new textClass("Lorem");
  Timer timer;

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

    items.add(new textClass("test"));
    items.add(new textClass("test"));

    timer = new Timer.periodic(new Duration(seconds: 5), (Timer timer) {
      setState(() {
        items.removeAt(0);
        items.add(lorem);
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
    timer.cancel();
  }

  @override
  Widget build(BuildContext context) {
    Iterable<Widget> content = ListTile.divideTiles(
        context: context, tiles: items).toList();

    return new Column(
      children: content,
    );
  }
}

class textClass extends StatefulWidget {
  textClass(this.word);

  final String word;

  @override
  State<StatefulWidget> createState() =>
      new _textClass();
}

class _textClass extends State<textClass>
    with TickerProviderStateMixin {
  _textClass();

  String word;
  Timer timer;

  @override
  void didUpdateWidget(textClass oldWidget) {
    if (oldWidget.word != widget.word) {
      word = widget.word;
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  void initState() {
    super.initState();
    word = widget.word;

    timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
      setState(() {
        word += "t";
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
    timer.cancel();
  }


  @override
  Widget build(BuildContext context) {
    return new Text(word);
  }
}