flutter_gadgets_2 0.3.0 flutter_gadgets_2: ^0.3.0 copied to clipboard
Flutter Gadgets 2, a library for simplified state (model) management and service location in Flutter.
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 🧮 #
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 onPressed
callback: it will increment the current int value and will trigger the rebuild of the relative subscriber.
Rock Paper Scissor 🖐️ #
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 👽 #
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 ❓ #
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>();
}