Flutter 2. ValueNotifier, AnimatedBuilder e o ValueListenableBuilder
- 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 values, 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 builders dos AnimatedBuilders. 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 AnimatedBuilders, pede um Listenable que neste caso é o settings.counter$. O AnimatedBuilders 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
AnimatedBuilderporValueListenableBuilder. - em seguida troque o atributo
animationporvalueListenable. Seu argumento é o mesmo. - por fim, adicione o
valuecomo 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.
Deixe uma resposta