Search code examples
flutterdartinherited-widget

Flutter trigger action in child with InheritedWidget


I have a dice-rolling MaterialApp. When the floatingButton is pressed I want to trigger a "dice-roll" on the children. I'm trying to use the InheritedWidget, but most examples I've seen appear to do the opposite, trigger a parent change from the child.

The Stateless Parent:

class DiceApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Container(
          child: Center(
            child: Row(
              children: <Widget>[
                RollTrigger(
                  roller: Roller(),
                  child: Die(),
                ),
                RollTrigger(
                  roller: Roller(),
                  child: Die(),
                ),
              ],
            ),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.casino),
          onPressed: () {
            // Trigger the dice roll on each
          },
        ),
      ),
    );
  }
}
}

The InheritedWidget:

class RollTrigger extends InheritedWidget {
  RollTrigger({this.roller, this.child});

  final roller;
  final Widget child;

  static RollTrigger of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<RollTrigger>();
  }

  @override
  bool updateShouldNotify(RollTrigger oldWidget) => true;
}

I'm trying using a Roller class to trigger the roll action:

class Roller {
  bool rolling = false;

  void roll(_DieState die) {
    die.roll();
  }
}

And finally the Stateful Die:

class Die extends StatefulWidget {

  @override
  _DieState createState() => _DieState();
}

class _DieState extends State<Die> {

  int value = 0;
  final _random = new Random();
  roll() {
    this.value = 1 + _random.nextInt(6 - 1);
  }

  _DieState();

  @override
  Widget build(BuildContext context) {
    var roller = RollTrigger.of(context).roller;

    return Text(this.value.toString());
  }
}

This seems like it should be simpler, but I'm tying myself in knots here.

Edit: I'm updating by putting the Roller at the top level per suggestion. I'm still not sure how to trigger a rebuild for the bottom widget:

final Roller roller = Roller();
...
  RollTrigger(
    roller: this.roller,
    child: Die(),
)

And then I put the roll method in the Roller:

class Roller {

  int value = 0;

  final _random = new Random();

  void roll() {

    this.value = 1 + _random.nextInt(6 - 1);

    print(this.value);

  }
}

Assign the value from the Roller:

class _DieState extends State<Die> {
  @override
  Widget build(BuildContext context) {
    final roller = RollTrigger.of(context).roller;

    return Text(roller.value.toString());
  }
}

And finally, call the roll method at the top level:

        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.casino),
          onPressed: () {
            this.roller.roll();
          },
        ),

This updates the Roller's value, but never changes to value of the _DieState... which is the problem I'm trying to solve. There are a lot of interesting patterns out there, but I think what I'm struggling with is the basic implementation.


Solution

  • This seems like it should be simpler, but I'm tying myself in knots here.

    Yes. It should be much simpler!

    You need to take advantage of the observer design pattern. In Flutter, an observable is a ChangeNotifier.

    Package provider will help a great deal. All the widget classes you create can be StatelessWidget.

    import 'dart:math';
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    class Die with ChangeNotifier {
      //
      // _value can be one of null, 1, 2, 3, 4, 5, 6
      // A value of null signifies the die is currently rolling.
      //
      int _value = 1;
    
      final Random _random = Random();
    
      int get value {
        return this._value;
      }
    
      Future<void> roll() async {
        if (this._value == null) {
          // already rolling
          return;
        }
    
        this._value = null;
        this.notifyListeners();
    
        await Future.delayed(Duration(seconds: 1));
    
        this._value = 1 + this._random.nextInt(6);
        this.notifyListeners();
      }
    }
    
    // ---------------------------------------------------------
    
    void main() {
      return runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(final BuildContext context) {
        return MaterialApp(
          title: 'Die Rolling App',
          theme: ThemeData(primarySwatch: Colors.blue),
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(final BuildContext context) {
        return MultiProvider(
          providers: [
            ChangeNotifierProvider<Die>(
              create: (final BuildContext context) {
                return Die();
              },
            ),
          ],
          child: Scaffold(
            appBar: AppBar(title: const Text("Die Rolling Game")),
            body: Center(child: DieDisplay()),
            floatingActionButton: RollFAB(),
          ),
        );
      }
    }
    
    class DieDisplay extends StatelessWidget {
      @override
      Widget build(final BuildContext context) {
        return Consumer<Die>(
          builder: (final BuildContext context, final Die die, final Widget child) {
            return Text((die.value == null) ? 'rolling' : die.value.toString());
          },
        );
      }
    }
    
    class RollFAB extends StatelessWidget {
      @override
      Widget build(final BuildContext context) {
        return FloatingActionButton(
          onPressed: () {
            final Die die = Provider.of<Die>(context, listen: false);
            die.roll();
          },
          tooltip: 'Roll',
          child: const Icon(Icons.casino),
        );
      }
    }