- Flutter 2. ValueNotifier, AnimatedBuilder e o ValueListenableBuilder
- Flutter 1. ChangeNotifier
Índice
Este artigo continua a discussão do artigo anterior agora implementando a responsividade com o ValueNotifier
e posteriormente aplicando o AnimatedBuilder
e o ValueListenableBuilder
, o que permite reconstruir apenas a fração da tela alterada pela modificação.
O código base está disponível no arquivo zip counter-01.zip
. Caso deseje iniciar por ele basta criar um novo projeto flutter
e copiar a pasta lib
do arquivo zip para a pasta lib
do novo projeto.
ValueNotifier
A classe ValueNotifier
herda da classe ChangeNotifier
e implementa o ValueListenable
internamente. O construtor da classe ValueNotifier
inicia armazenando o valor passado em this._value
, o atributo interno para armazenar o valor a ser gerenciado, linha 8 no código abaixo. O getter value
, linha 25 à 33 abaixo, fica responsável por alterar o valor do _value
e dispara um notifyListeners()
ao final, notificando a todos os ouvintes uma alteração no seu atributo interno.
/// A [ChangeNotifier] that holds a single value. /// /// When [value] is replaced with something that is not equal to the old /// value as evaluated by the equality operator ==, this class notifies its /// listeners. class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { MemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, className: '$ValueNotifier', object: this, ); } _creationDispatched = true; } /// The current value stored in this notifier. /// /// When the value is replaced with something that is not equal to the old /// value as evaluated by the equality operator ==, this class notifies its /// listeners. @override T get value => _value; T _value; set value(T newValue) { if (_value == newValue) { return; } _value = newValue; notifyListeners(); } @override String toString() => '${describeIdentity(this)}($value)'; }
Desta forma o emprego do ValueNotifier
dispensa as chamadas ao notifyListeners()
, mas impõe que o valor escutado seja o seu _value
, acessível pelo método getter.
Implantando o ValueNotifier no app_settings.dart
A classe AppSettings
não necessita mais estender o ChangeNotifier
, e a declaração dos atributos _counter
, _selection
e _state
se tornam ValueNotifier
como mostrado nas linhas abaixo.
...
class AppSettings {
ValueNotifier<int> counter$ = ValueNotifier(0);
ValueNotifier<bool> selection$ = ValueNotifier(false);
ValueNotifier<ThreeState> state$ = ValueNotifier(ThreeState.first);
...
Como será necessário acessar os métodos addListener
destas variáveis em home_page.dart
, estas não poderão mais serem privadas. É conveniente adicionar um “$
” ao final do nome destas variáveis, uma vez que os getter destas serão úteis para acessar os seus value
s, já que estas agora são ValueNotifier
.
...
int get counter => counter$.value;
bool get selection => selection$.value;
ThreeState get state => state$.value;
...
Como pode observar acima os métodos de alteração agora devem mudar os valores em ‘.value
‘ de cada atributo, e os notifyListeners()
não são mais necessários, visto que as alterações nos valores dos _value
‘s são gerenciados pelo ValueNotifier
. Desta forma os métodos de alteração dos atributos ficam assim:
...
void increment() => counter$.value++;
void toggleSelection() => selection$.value = !selection$.value;
switch (state$.value) {
case ThreeState.first:
state$.value = ThreeState.second;
break;
case ThreeState.second:
state$.value = ThreeState.third;
break;
case ThreeState.third:
state$.value = ThreeState.first;
break;
}
...
O código completo do novo app_settings.dart
é apresentado a seguir:
import 'package:flutter/material.dart'; enum ThreeState { first, second, third } class AppSettings { ValueNotifier<int> counter$ = ValueNotifier(0); ValueNotifier<bool> selection$ = ValueNotifier(false); ValueNotifier<ThreeState> state$ = ValueNotifier(ThreeState.first); int get counter => counter$.value; bool get selection => selection$.value; ThreeState get state => state$.value; void increment() => counter$.value++; void toggleSelection() => selection$.value = !selection$.value; void toggleState() { switch (state$.value) { case ThreeState.first: state$.value = ThreeState.second; break; case ThreeState.second: state$.value = ThreeState.third; break; case ThreeState.third: state$.value = ThreeState.first; break; } } }
As explicitações dos tipos nas linhas 6 à 8 não são necessárias, e as fiz apenas para deixar mais explicito os tipos no código.
Aplicando ao HomePage
A mudança na classe HomePage
ser apenas no initState
. O settings.addListener
será removido e três novos addListener
serão adicionados, visto que agora as alterações ouvidas são ativadas por settings.counter$.addListener
, settings.selection$.addListener
e o settings.state$.addListener
. Bom, isto resulta na alteração do método initState
para o código a seguir:
...
@override
void initState() {
super.initState();
settings.counter$.addListener(() {
setState(() {});
});
settings.selection$.addListener(() {
setState(() {});
});
settings.state$.addListener(() {
setState(() {});
});
}
...
Devido aos getters criados na classe AppSettings
o restante do código permanece inalterado e neste ponto o código deve retornar a funcionar.
O código completo do home_page.dart
é apresentado a seguir.
import 'package:flutter/material.dart'; import '../settings/app_settings.dart'; class HomePage extends StatefulWidget { const HomePage({super.key, required this.title}); final String title; @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final settings = AppSettings(); @override void initState() { super.initState(); settings.counter$.addListener(() { setState(() {}); }); settings.selection$.addListener(() { setState(() {}); }); settings.state$.addListener(() { setState(() {}); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.increment, child: const Icon(Icons.add), ), const SizedBox(width: 30), Text( 'Counter: ${settings.counter}', ), ], ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.toggleSelection, child: const Text('Toggle'), ), const SizedBox(width: 30), Text( 'Selection: ${settings.selection}', style: Theme.of(context).textTheme.titleMedium, ), ], ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.toggleState, child: const Text('Toggle'), ), const SizedBox(width: 30), Text( 'State: ${settings.state.name}', style: Theme.of(context).textTheme.titleMedium, ), ], ), ], ), ), ); } }
Este código está disponível em counter-02.zip.
Além de uma certa simplicidade, da forma como está este código não apresenta nenhuma grande vantagem. No entanto, este possui uma grande diferença com respeito ao código anterior: neste momento as alterações foram individualizadas, gerando notificações independentes a cada atributo do AppSettings
. Isto pode ser bem aproveitado com o emprego do widget AnimatedBuilder
.
AnimatedBuilder
Embora não seja focado para o uso proposto aqui, este builder pode ser empregado para renderizar a porção da tela que foi alterada, mantendo o restante da tela inalterada.
Inserindo no HomePage
Neste caso não necessitará mais de sobrescrever o método initState
na classe HomePage
, de forma que este pode ser totalmente removido do código. O Listener
será passado pelo atributo animation
do AnimatedBuilder
, fazendo-o renderizar a porção da tela em particular.
O passo seguinte é adicionar os AnimatedBuilder
onde estão os widgets Text()
com as mensagens dos valores das variáveis. Estes widgets serão usados como retorno do builder
s dos AnimatedBuilder
s. O código para o atributo settings.counter$
do settings
é apresentado a seguir:
...
AnimatedBuilder(
animation: settings.counter$,
builder: (context, child) {
return Text(
'Counter: ${settings.counter}',
);
},
),
...
O atributo animation
, do AnimatedBuilder
s, pede um Listenable que neste caso é o settings.counter$
. O AnimatedBuilder
s possui um atributo child
que se for passado pode ser empregado pelo builder
para construir o seu retorno. No entanto, como o child
é declarado fora do builder
ele não será renderizado novamente, mantendo o mesmo conteúdo da primeira renderização, o que não serve para o propósito aqui.
Agora é repetir esta mudança para os demais listenables: settings.selection$
e settings.state$
. O código final é apresentado a seguir:
import 'package:flutter/material.dart'; import '../settings/app_settings.dart'; class HomePage extends StatefulWidget { const HomePage({super.key, required this.title}); final String title; @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final settings = AppSettings(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.increment, child: const Icon(Icons.add), ), const SizedBox(width: 30), AnimatedBuilder( animation: settings.counter$, builder: (context, child) { return Text( 'Counter: ${settings.counter}', ); }, ), ], ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.toggleSelection, child: const Text('Toggle'), ), const SizedBox(width: 30), AnimatedBuilder( animation: settings.selection$, builder: (context, child) { return Text( 'Selection: ${settings.selection}', style: Theme.of(context).textTheme.titleMedium, ); }, ), ], ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.toggleState, child: const Text('Toggle'), ), const SizedBox(width: 30), AnimatedBuilder( animation: settings.state$, builder: (context, child) { return Text( 'State: ${settings.state.name}', style: Theme.of(context).textTheme.titleMedium, ); }, ), ], ), ], ), ), ); } }
Do AnimatedBuilder
o que de fato está sendo empregado é a sua capacidade de renderizar apenas uma porção da tela. O foco do AnimatedBuilder é para widgets mais complexos que desejam incluir uma animações: AnimatedBuilder-class.
ValueListenableBuilder
O ValueListenableBuilder
é o mais adequado para o problema aqui apresentado. Como o AnimatedBilder
ele irá renderizar apenas uma porção da tela quando um Value
de um Listenable
for alterado.
Ajustando o HomePage Novamente
Para inserir o ValueListenableBuilder
ao código siga as instruções a seguir:
- primeiro substitua os
AnimatedBuilder
porValueListenableBuilder
. - em seguida troque o atributo
animation
porvalueListenable
. Seu argumento é o mesmo. - por fim, adicione o
value
como o segundo argumento dobuilder
.
Repita isto para cada elemento do AnimatedBuilder
e o código já estará funcional novamente. O home_page.dart
final está abaixo para verificação.
import 'package:flutter/material.dart'; import '../settings/app_settings.dart'; class HomePage extends StatefulWidget { const HomePage({super.key, required this.title}); final String title; @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final settings = AppSettings(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.increment, child: const Icon(Icons.add), ), const SizedBox(width: 30), ValueListenableBuilder<int>( valueListenable: settings.counter$, builder: (context, value, child) { return Text( 'Counter: ${settings.counter}', ); }), ], ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.toggleSelection, child: const Text('Toggle'), ), const SizedBox(width: 30), ValueListenableBuilder<bool>( valueListenable: settings.selection$, builder: (context, value, child) { return Text( 'Selection: ${settings.selection}', style: Theme.of(context).textTheme.titleMedium, ); }, ), ], ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: settings.toggleState, child: const Text('Toggle'), ), const SizedBox(width: 30), ValueListenableBuilder<ThreeState>( valueListenable: settings.state$, builder: (context, value, child) { return Text( 'State: ${settings.state.name}', style: Theme.of(context).textTheme.titleMedium, ); }, ), ], ), ], ), ), ); } }
As explicitações de tipo nas linhas 35, 53 e 73, nas chamadas dos ValueListenableBuilder
não são necessárias, as coloquei apenas para deixar mais evidente o tipo empregado.
Conclusões
Este artigo conclui a ideia inicial desta série que era cobrir o ChangeNotifier
, e sua extensão ValueNotifier
, bem como o AnimatedBuilder
e o ValueListenableBuilder
.
Embora tenha concordado que o ValueListenableBuilder
é mais adequado para a maioria de minhas aplicações, recentemente encontrei uma situação em que o AnimatedBuilder
foi mais adequado. O problema ocorreu quanto houve a necessidade de ouvir vários Listenable
para renderizar uma barra de status no aplicativo. O problema é que o ValueListenableBuilder
aceita apenas um ValueListenable
como argumento no atributo valueListenable
sem a possibilidade de passar uma lista dos Listenable
. Já o AnimatedBuilder
aceita um Listenable
no atributo animation
, e a classe Listenable
possui o construtor nomeado Listenable.merge
que permite passar uma lista de Listenable
, conveniente para ouvir a mais de um Listenable
ao mesmo tempo.
No próximo artigo pretendo estender os atributos do settings
para uma página de configuração e tratá-los como elementos de configuração do aplicativo de fato.