1

I appear to have "lost the bubble" regarding re-rendering! I'm not sure what is wrong with the current implementation of my app. It was derived with the help of a number of SO members.

What it's supposed to do: Render 9 green circles and then, one after another, render each circle in yellow in either increasing or decreasing order (Circles widget). Display the current value of count (Counter widget). In the AppBar (Home_Page widget): recognize a tap of + and increment count, recognize a tap of - and decrement count. In both cases, perform the increment/decrement in the body of a setState method. Both the Circles and the Counter widgets are expected to re-render. In this implementation, all circles participate in the green-to-yellow-to-green color changes (effectively count is ignored).

What it does: The initial rendering of the Circles and the Counter widgets display as desired. But the AppBar icons (+ and -), although recognized, do not cause a re-rendering of the Circles widget. The Counter widget does re-render its display of count. In the Circles widget is a RaisedButton that when tapped does cause the re-rendering of the Circles widget. But that button is not desired in a final implementation and is only present for testing.

What has me perplexed is that the template used for the Circles widget is the same as that used for the Counter widget. Yet they appear to execute differently.

The source code for the whole application follows. It is a single .dart file (sorry it's so long but in the past leaving something out caused questions).

Thoughts?

// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: non_constant_identifier_names

import 'package:flutter/material.dart';

import 'dart:async';
import 'dart:math';

const int     NUMBER_TILES = 9;
final int     CROSS_AXIS_COUNT = (sqrt(NUMBER_TILES)).toInt();
const double  CROSS_AXIS_SPACING = 4.0;
const int     INITIAL_COUNT = 9;        // for testing; should be 1
const double  MAIN_AXIS_SPACING = CROSS_AXIS_SPACING;
const int     MILLISECOND_MULTIPLIER = 500;

// ************************************************************** main

void main() {
  final AppState app_state = new AppState(counter: INITIAL_COUNT);
  runApp(new Home_Page(app_state: app_state));
} // main

// **************************************************** class AppState

class AppState {
  int       counter = 0;
  List<int> flash_indices = [];
  bool      forward = false;

  AppState({this.counter});             // AppState

  String toString() {                   // toString
    return ( 'AppState{' +
             'counter: $counter, ' +
             'flash_indices: $flash_indices}');
  } // toString
                                        // following is a mock
  void randomize_flash_indices ( ) {    // randomize_flash_indices

    forward = !forward;
    if ( forward){
      flash_indices = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, -1];
    }
    else {
      flash_indices = [ 8, 7, 6, 5, 4, 3, 2, 1, 0, -1];
    }
    flash_indices.add (-1);             // restore to normal colors
  } // randomize_flash_indices

} // class AppState

// *************************************************** class Home_Page

class Home_Page
      extends StatefulWidget {
  final AppState app_state;

  Home_Page({
    @required this.app_state,
    Key key,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return Home_Page_State();
  }

} // class Home_Page

// ********************************************* class Home_Page_State

class Home_Page_State extends State<Home_Page>{

  Home_Page_State();

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Periodic',
      theme: new ThemeData(primarySwatch: Colors.indigo),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Periodic'),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.add),    // increment counter
              onPressed: () {
                if (widget.app_state.counter < NUMBER_TILES){
                  setState(() {
                    widget.app_state.counter++;
                  });
                }
              }
            ),
            IconButton(
              icon: Icon(Icons.remove), // decrement counter
              onPressed: () {
                if (widget.app_state.counter > 1){
                  setState(() {
                    widget.app_state.counter--;
                  });
                }
              }
            ),
          ]
        ),
        body: Column(
          children: [
            Circles (
              app_state: widget.app_state,
            ),
            Counter (
              app_state: widget.app_state,
            )
          ],
        ),
      ),

    );
  } // Home_Page_State build

} // class Home_Page_State

// ***************************************************** class Circles

class Circles extends StatefulWidget {
  final AppState app_state;

  Circles({
    @required this.app_state,
    Key key,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return Circles_State();
  }
} // class Circles

// *********************************************** class Circles_State

class Circles_State extends State<Circles>{

  Circles_State();

  int                     flash_tile = -1;
  List<GridTile>          grid_tiles = <GridTile>[];
  StreamController<int>   tick_controller;
  StreamSubscription<int> tick_listener;

  Stream<int> start_ticking() {         // start_ticking
    tick_controller = new StreamController();

    for ( int tick = 0; (tick < widget.app_state.counter); tick++ ) {
      Future.delayed(Duration(milliseconds:
                     MILLISECOND_MULTIPLIER * tick),() {

print('start_ticking() tick: $tick');

        tick_controller.add(tick);
      });
    }
    return tick_controller.stream;
  } // start_ticking

  @override
  void initState() {                    // initState
    super.initState();
    widget.app_state.randomize_flash_indices();
    tick_listener = start_ticking().listen(on_tick);
  } // initState

  @override
  void dispose() {                      // dispose
    if (tick_listener != null) {
      tick_listener.cancel();
      tick_listener = null;
    }
    super.dispose();
  } // dispose

  on_tick(int tick) async { // on_tick

print('listen_for_tick() tick: $tick');

    this.setState(() => this.flash_tile =
                        widget.app_state.
                               flash_indices[tick]);
  } // on_tick

  GridTile new_circle_tile(             // new_circle_tile
                    Color tile_color,
                    int   index) {
    GridTile tile = GridTile(
        child: GestureDetector(
          child: Container(
            decoration: BoxDecoration(
              color: tile_color,
              shape: BoxShape.circle,
            ),
          ),
        )
      );
    return (tile);
  } // new_circle_tile

  List<GridTile> create_circle_tiles() {// create_circle_tiles
    grid_tiles = new List<GridTile>();

    for (int i = 0; (i < NUMBER_TILES); i++) {
      Color tile_color =
              ( this.flash_tile == i) ?
                        Colors.yellow :
                        Colors.green;

      grid_tiles.add(new_circle_tile(tile_color, i));
    }
    return (grid_tiles);
  } // create_circle_tiles

  @override // Circles_State
  Widget build(BuildContext context) {

print('Circles_State Build ' +
      widget.app_state.toString() +
      ' flash_tile: $flash_tile');

    return Column(
      children: [
        GridView.count(
          shrinkWrap: true,
          crossAxisCount: CROSS_AXIS_COUNT,
          childAspectRatio: 1.0,
          padding: const EdgeInsets.all(4.0),
          mainAxisSpacing: MAIN_AXIS_SPACING,
          crossAxisSpacing: CROSS_AXIS_SPACING,
          children: create_circle_tiles(),
        ),
        RaisedButton(
          child: Text("restart"),
          onPressed: () {
            widget.app_state.randomize_flash_indices();
            tick_listener = start_ticking().listen(on_tick);
          }
        ),
      ] // children
    );
  } // Circles_State build

} // class Circles_State

// ***************************************************** class Counter

class Counter extends StatefulWidget {
  final AppState app_state;

  Counter({
    @required this.app_state,
    Key key,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return Counter_State();
  }
} // class Counter

// *********************************************** class Counter_State

class Counter_State extends State<Counter> {

  Counter_State();

  @override // Counter_State
  Widget build(BuildContext context) {
    int counter_value = widget.app_state.counter;
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Expanded(
          child: SizedBox(
            width: 24.0,
            child: Center(
              child: Text(
                'Counter $counter_value',
                style: TextStyle(
                  color: Colors.blue,
                  fontWeight: FontWeight.bold,
                  fontSize: 24.0,
                ),
              ),
            ),
          ),
        ),
      ],
    );
  } // Counter_State build

} // class Counter_State
Gus
  • 1,383
  • 2
  • 12
  • 23
  • That's kind of a lot to sift through for useful information (in my opinion). If you could clearly point out what the problem is you are more likely to get help. That said, widgets not re-rendering is usually because of a missing call to `setState()` when changing a property of a `StatefulWidget`. – Jacob Phillips Oct 16 '18 at 01:40
  • In Home_Page_State, the two Icon on_pressed event handlers are both declared in setState bodies. This is where I think I am going astray. I expected both Circles and Counter widgets to be re=rendered, but only the Counter widget is re-rendered. – Gus Oct 16 '18 at 03:12
  • `this.setState(() => this.flash_tile = widget.app_state.flash_indices[tick]);` Is this where you're setting the circles' state? – Jacob Phillips Oct 16 '18 at 03:19
  • Just leaving this here... https://www.dartlang.org/guides/language/effective-dart/style – Jacob Phillips Oct 16 '18 at 03:21
  • @JacobPhilips There are three locations at which I thought I was setting Circles state: one in the on_tick method in class Circles_State (that you identified) and two in the Home_Page_State AppBar actions where I coded "setState((){widget.app_state.counter++;});" for the increment of counter and "setState((){widget.app_state.counter--;});" for its decrement. Note that these last two are not inside class Circles_State but were to communicate the change through the variable counter in the class AppState. – Gus Oct 16 '18 at 17:24
  • You'll have to call `setState` in the Circles class too. Somehow – Jacob Phillips Oct 16 '18 at 18:34

1 Answers1

0

I am very disappointed with the solution I found - combining all widgets into a single class. A skeleton of the original architecture looked like:

void main(){
  :
} // main()

class AppState {
  :
} // class AppState

class HomePage extends StatefulWidget {
  :
} // class HomePage

class HomePageState extends State<HomePage>{ 
  :
} // class HomePageState

class Circles extends StatefulWidget {
  :
} // class Circles

class CirclesState extends State<Circles>{}
  :
} // class CirclesState

class Counter extends StatefulWidget {
  :
} // class Counter

class CounterState extends State<Counter>{}
  :
} // class CounterState

Each class could be placed into its own .dart file. However, in order to achieve the results I desired, I had to eliminate all classes except for HomePage and HomePageState. The contents of all of the other classes (variables, methods, and functions) were required to be placed into HomePageState so that, when the state was changed, the widgets would update correctly. A skeleton of the revised implementation looks like:

void main(){
  :
} // main()

class HomePage extends StatefulWidget {
  :
} // class HomePage

class HomePageState extends State<HomePage>{ 
  :
  void randomize_flash_indices ( ) {...

  Stream<int> start_ticking() { ...

  @override
  void initState() { ...

  @override
  void dispose() { ...

  on_tick(int tick) async { ...

  Circles(){ ...

  Counter(){ ...

  @override
  Widget build(BuildContext context) {...

  :
} // class HomePageState

This implementation is monolithic and flies in the face of good programming (and design) (and architectural) principles. This type of software cannot be maintained in a production environment.

It appears that the architects of the Dart language missed an important point about setState() or I totally missed the correct coding practice. Since I am an experienced programmer (more than 42 years) I tend to doubt the latter (of course, because I am an experienced programmer, I recognize that I may be missing an important point about setState).

I am disappointed in flutter/dart. I had hoped for relief from Xamarin - I'm guessing that I will not get it from flutter/dart.

Gus
  • 1,383
  • 2
  • 12
  • 23
  • In Flutter, perhaps the most straightforward way to efficiently pass state down the widget tree is to subclass `InheritedWidget`. It looks like `InheritedModel` just hit the beta channel of Flutter too, which is like an updated version of InheritedWidget. https://stackoverflow.com/questions/49491860/flutter-how-to-correctly-use-an-inherited-widget – Jacob Phillips Oct 16 '18 at 21:14
  • Here's an example of `InheritedModel` https://gist.github.com/jifalops/8a59c7c8ae64daf6c02cd7ecca1aed0c – Jacob Phillips Oct 16 '18 at 21:22