flutter_gadgets_2 0.3.0 copy "flutter_gadgets_2: ^0.3.0" to clipboard
flutter_gadgets_2: ^0.3.0 copied to clipboard

Flutter Gadgets 2, a library for simplified state (model) management and service location in Flutter.

example/README.md

Flutter Gadgets Examples #

A collection of Flutter Gadget examples.

Each example shows some of the key elements of the library. For additional information about the gadgets provided by the library, refer to the guide contained in the README.

Counter 🧮 #

AlienCounter example

The classic Flutter counter example.

void main() => runApp(AppGadget(
      init: (model) => model.putValue<int>(value: 0),
      child: const MyApp(),
    ));

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Counter'),
        ),
        body: Center(
          child: SubscriberGadget<int>(
            builder: (_, value) => Text('Count: $value'),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<ObservableModel>().updateValue<int>(
                updateFunction: (value) => value + 1,
              ),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

During the initialization, a new int instance is registered in the ObservableModel using the init function of AppGadget.

To bind the value of the counter instance to a widget, a Text widget is wrapped in a SubscriberGadget: in this way, it gets access to the int object and will update when necessary.

The task of updating the model instances and the interface is delegated to the onPressedcallback: it will increment the current int value and will trigger the rebuild of the relative subscriber.

Rock Paper Scissor 🖐️ #

RockPaperScissor example

The Flutter Gadgets edition of the iconic Rock Paper Scissor game.

In this example, the business logic is represented by two objects:

  • Turn
  • Match
class Turn {
  static final random = Random();

  late HandSign _playerChoice;
  late HandSign _cpuChoice;
  late TurnState state;

  TurnState play(HandSign playerChoice) {
    _playerChoice = playerChoice;
    _cpuChoice = _generateCpuChoice();
    state = _currentTurnState();
    return state;
  }

  HandSign _generateCpuChoice() {
    int index = random.nextInt(HandSign.values.length);
    return HandSign.values[index];
  }

  TurnState _currentTurnState() {
    if (_playerChoice == _cpuChoice) return TurnState.tie;
    if (_playerWon()) {
      return TurnState.playerWon;
    } else {
      return TurnState.cpuWon;
    }
  }

  bool _playerWon() {
    bool firstCase =
        _playerChoice == HandSign.paper && _cpuChoice == HandSign.rock;
    bool secondCase =
        _playerChoice == HandSign.rock && _cpuChoice == HandSign.scissor;
    bool thirdCase =
        _playerChoice == HandSign.scissor && _cpuChoice == HandSign.paper;
    return firstCase || secondCase || thirdCase;
  }

  HandSign get playerChoice => _playerChoice;

  HandSign get cpuChoice => _cpuChoice;
}
class Match {
  int cpuWins = 0;
  int playerWins = 0;
  int ties = 0;
  Turn? lastTurn;

  void nextTurn(HandSign playerChoice) {
    lastTurn = Turn();
    lastTurn!.play(playerChoice);
    _updateWins();
  }

  void _updateWins() {
    if (lastTurnState == TurnState.playerWon) {
      playerWins++;
    } else if (lastTurnState == TurnState.cpuWon) {
      cpuWins++;
    } else {
      ties++;
    }
  }

  MatchState get matchState {
    if (cpuWins == turnsToWin) return MatchState.cpuWon;
    if (playerWins == turnsToWin) return MatchState.playerWon;
    return MatchState.playing;
  }

  String get matchStateDescription {
    if (cpuWins == turnsToWin) return 'Cpu won!';
    if (playerWins == turnsToWin) return 'Player won!';
    return 'Playing...';
  }

  TurnState get lastTurnState => lastTurn!.state;

  String get lastTurnStateDescription {
    if (lastTurn == null) return 'Your turn.';
    if (lastTurnState == TurnState.playerWon) return 'You won this turn!';
    if (lastTurnState == TurnState.cpuWon) return 'Cpu won this turn!';
    return 'Tie!';
  }
}

When the application is created, a new Match instance is added to the ObservableModel by using the init function in AppGadget:

AppGadget(
  init: (model) => model.putValue<Match>(value: Match()),
  child: const MaterialApp(
    title: 'Rock Paper Scissor',
    home: MyGameView(),
  ),
);

The portion of the view that requires a Match instance to be built is wrapped in a SubscriberGadget that grants access to the instance and rebuilds the screen whenever the state of the match changes. Furthermore, in order to having access to the callbacks provided by the associated view controller MyGameViewController, the whole view is wrapped in a ViewGadget<MyGameViewControl>:

class MyGameView extends StatelessWidget {
  const MyGameView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ViewGadget<MyGameViewControl>(
      create: () => MyGameViewControl(),
      builder: (_, control) => Scaffold(
        appBar: AppBar(
          title: const Text('Rock Paper Scissor'),
          centerTitle: false,
          actions: [
            IconButton(
              icon: const Icon(Icons.refresh),
              onPressed: control.onRefreshPressed,
            )
          ],
        ),
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: SubscriberGadget<Match>(
            builder: (_, match) => Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                const Text(
                  'Play a match!',
                  style: TextStyle(
                    fontWeight: FontWeight.w700,
                  ),
                ),
                Text(match.matchStateDescription),
                Text(match.lastTurnStateDescription),
                const SizedBox(height: 16),
                RowText('Player wins:', match.playerWins.toString()),
                RowText('Ties:', match.ties.toString()),
                RowText('Cpu wins:', match.cpuWins.toString()),
                const SizedBox(height: 16),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    TextButton(
                      'ROCK',
                      () => control.onButtonPressed(HandSign.rock),
                      match.matchState == MatchState.playing,
                    ),
                    TextButton(
                      'PAPER',
                      () => control.onButtonPressed(HandSign.paper),
                      match.matchState == MatchState.playing,
                    ),
                    TextButton(
                      'SCISSOR',
                      () => control.onButtonPressed(HandSign.scissor),
                      match.matchState == MatchState.playing,
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                const HandSignRow()
              ],
            ),
          ),
        ),
      ),
    );
  }
}

The MyGameViewControl can update the global state of the app and rebuild the view using the putValue and notifyFor methods respectively:

class MyGameViewControl extends ControllerGadget {
  void onRefreshPressed() => model.putValue<Match>(value: Match());

  void onButtonPressed(HandSign playerChoice) {
    final match = model.getValue<Match>()!;
    match.nextTurn(playerChoice);
    model.notifyFor<Match>();
  }
}

Alien Counter 👽 #

AlienCounter example

A modified version of the classical flutter Counter.

In this example, the business logic is represented by an AlienCounter object. It is defined as follows:

class AlienCounter {
  final livingThings = 1000;
  int aliens = 0;
  int humans = 0;

  String get alienWarning => '$aliens aliens invaded the earth! 😱';

  String get humanWarning => '$humans humans have been kidnapped! 👽';

  String get livingCreature =>
      'There are ${(livingThings - humans) + aliens} $_what on earth 🌍';

  String get _what => aliens == 0 ? 'humans' : 'living... things';

  void addAlien() => aliens += 1;

  void addHuman() => humans += 1;
}

The three text widgets are wrapped in three different SubscriberGadget, each observing a different property of the model. Let's consider the first one as an example:

SubscriberGadget<AlienCounter>(
  propertiesSelector: (counter) => [counter.aliens],
  builder: (context, counter) {
    dev.log('Building for AlienCounter.aliens');
    return Text(counter.alienWarning);
  },
)

It is observing an AlienCounter object contained in the ObservableModel: in particular, it only rebuilds only when the aliens property of the watched object changes.

As the previous example, the whole view is wrapped in a ViewGadget: the onPressed callback of each button has the task of modifying the state of the AlienCounter and triggering the rebuild of the view. The associated control is defined as follows:

class MyHomeViewController extends ControllerGadget {
  void onAddAlien() => model.updateValue<AlienCounter>(
      updateFunction: (counter) => counter..addAlien());

  void onAddHuman() => model.updateValue<AlienCounter>(
      updateFunction: (counter) => counter..addHuman());
}

When the button is pressed, a reference to the counter can be obtained from the model and its state can be changed. The updateValue method updates the current state of the counter and triggers the rebuild of the view.

Number Guess ❓ #

RockPaperScissor example

Guess a random number between 1 and 100.

In this example, both views have a local internal state.

Let's look at the screen from the image: there is a TextField widget with an associated TextEditingController. To be able to dispose the TextEditingController and manipulate the internal state of the TextField (the errorText property for example), a ViewModelGadget encapsulates the view.

By defining a MyGameViewModel and by using a ViewModelGadget, an instance of the MyGameViewModel is automatically added to the ObservableModel and removed when the view is destroyed.

class MyGameViewModel {
  late TextEditingController controller;
  String? errorMessage;
  bool canPlay = true;
}
ViewModelGadget<MyGameViewModel>(
  create: () {
    final viewModel = MyGameViewModel();
    viewModel.controller = TextEditingController();
    return viewModel;
  },
  dispose: (viewModel) => viewModel.controller.dispose(),
  // subscribeToViewModel: false,
  builder: ...
)

Notice that the subscribeToViewModel property is set to false (default value): in this way, the ViewModelGadget doesn't automatically start observing the MyGameViewModel instance, so widgets that require the MyGameViewModel object can use the SubscriberGadget to access it and start listening for eventual changes.

If the subscribeToViewModel property is set to true, the ViewModelGadget automatically registers itself as a subscriber for the MyGameViewModel instance. Please be careful: in this way the whole ViewModelGadget subtree will rebuild each time the associated view-model changes.

The internal state of the view model instance can be updated as usual. Let's look at the onResetPressed callback of MyGameViewControl:

void onResetPressed() {
  final game = model.getValue<Game>()!;
  game.newGame();
  final viewModel = model.getValue<MyGameViewModel>()!;
  viewModel.controller.clear();
  viewModel.errorMessage = null;
  viewModel.canPlay = true;
  model.notifyFor<MyGameViewModel>();
  model.notifyFor<Game>();
}
12
likes
130
pub points
35%
popularity

Publisher

verified publishersvelto.tech

Flutter Gadgets 2, a library for simplified state (model) management and service location in Flutter.

Repository

Documentation

API reference

License

BSD-3-Clause (LICENSE)

Dependencies

collection, flutter, provider, rxdart

More

Packages that depend on flutter_gadgets_2