Aplicativos reativos em Flutter - Parte II

Como podemos lidar com o state passando-o para vários widgets sem ter que colocá-lo em todos os widgets que não precisam usá-lo?

Flutter tem um widget herdado que mantém o state e permite que vocẽ propague de forma mais eficiente o state na árvore de widgets que é o Inherite Widget.

Basicamente, quando se tem um InheritedWidget ao invés de ter que passar referências a ele, podemos usar o contexto de compilação para podermos ter acesso à instância desse estado e usá-lo diretmente dentro de nossos próprios widgets. Vejamos:

class MyInheritedWidget extends InheritedWidget {
final Data state;
...
}

Aqui, nós temos um MyInheritedWidget realmente inteligente onde foi colocado um state, mas como eu posso acessá-lo? posso colocá-lo em minha árvore, dando-lhe algum estado e, em seguida, construindo a árvore de widgets abaixo dela. 

Widget build(BuildContext context) =
return MyInheritedWidget(
state: mState,
child: WidgetTree(...)
);

E então, depois disso, eu posso acessá-lo simplesmente usando o contexto de compilação para obter uma referência a esse widget,  e então eu posso acessar o estado. 

Widget build(BuildContext context) {
final state = MyInheritedWidget.of(context).state;
...
}

Bem, agora posso acessar o estado em qualquer lugar da árvore e manter o resto do meu código mais enxuto. Mas, como você percebeu, meu estado foi final. Isso não é muito útil se você realmente quer mudar seu estado. 

Agora, não é difícil colocar uma camada em mutação com InheritedWidget. Então iremos dar uma olhada na biblioteca chamada ScopedModel que é um pacote externo construído em cima de InheritedWidget e o mais importe é que nos trás uma maneira mais refinada de reconstruir partes de nossa árvore e permite também mudar o estado.

OBS. Devemos importar esta biblioteca para podermos usá-la: https://pub.dartlang.org/packages/scoped_model

Assim, veremos que ScopedModel equivale ao InheritedWidget:

MyInheritedWidget(
state: mState,
child: ...
);

ScopedModel(
state: mState,
child: ...
);

E nós temos o Descendant que é a forma que nos dá acesso ao estado:

ScopedModelDescendantT(
builder:
(context, child, state) {
...
}
);

Seria o equivalente ao:

final state = MyInheritedWidget.of(cotext).state;

Vamos agora partir para o código, para ver na prática, e usaremos o exemplo de uma ideia de um aplicativo de carrinho de compras. Primeiramente iremos definir nosso modelo de carrinho:

class CartModel extends Model {
final _cart = Cart();

List[CartItem] get items = _cart.items;

int get itemCount = _cart.itemCount;

void add(Product product) {
_cart.add(product);
notifyListeners();

}
}

ATENÇÃO! Tendo em vista que o nosso editor bloqueia alguns caracteres, na parte onde há [ substituir pelo símbolo de maior e ] substituir pelo símbulo de menor.

Primeiramente criamos um objeto de carrinho em _cart que está com nosso estado e nós temos três maneiras diferentes de interagir com isso:

  • a) temos um getter para pegar a lista de itens do carrinho;
  • b) temos um contador de itens itemCount que nos dá o número de itens que estão no carrinho;
  • c) temos um função add para adicinar um produto no nosso carrinho de compras.

Na função add observe que quando é feito a mudança do estado chama-se os ouvintes de modificação com o notifyListeners() e a maneira como o ScopedModel funciona é que sempre você tem um widget descendente que está observando as mudanças e quando o estado muda, nós simplesmente chamamos o notifyListeners() e todos os widgets descendentes serão notificados na mudança e irão se reconstruir, então só os widgetes que estão ouvindo as mudanças irão reconstruir.

Então vamos ver como podemos usar este modelo para inserir algum estado no meu aplicativo. O que podemos fazer é inserir o nosso estado no nível mais alto de nosso aplicativo para facilitar, basta colocar o widget ScopedModel no início dos seus widgets:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScopedModel(
model: CartModel(),
child: MaterialApp(
title: 'Carrinho de Compras',
home: MyHomePage(
routes: String, WidgetBuilder{
CartPage.routName: (context) = CartPage()
},
),
)
);
}
}

OBS. O ScopedModel requer um model e um child como vimos acima.

Então agora eu tenho estado no topo da minha árvore. Eu preciso acessar esse estado, então como podemos fazer isso? 

class MyHomePage extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Catálogo de Produtos'),
actions: Widget[
ScopedModelDescendant +CartModel+ (
builder: (context, _, model) = CartButton(
itemCount: model.itemCount,
onPressed: () {
Navigator.of(context).pushNamed(CartPage.routeName);
},
),
)
],
),
body: ProductGrid(),
);
}
}

ATENÇÃO! Tendo em vista que o nosso editor bloqueia alguns caracteres, na parte onde há o caracter + no código ScopedModelDescendant +CartModel+ colocar o símbolo de maior e menor antes e depois de CartModel.

Iremos embrulhar nosso botão em um ScopedModelDescendant CartModel que leva um contrutor builder: (context, _, model) que nos fornecerá um modelo. Ele precisa de um contexto de construção porque é assim que InheritedWidget funciona para obter a referência.

Agora, para poder funcionar, devemos poder adicionar alguns produtos e vamos fazer da mesma forma que dizemos acima, colocar o nosso catálogo de produtos dentro de um descendente do ScopedModel:

class ProductGrid extends StatelessWidget {
@override
Widget build(BuildContext context) = GridView.count(
crossAxisCount: 2,
children: catalog.products.map((product) {
return ScopedModelDescendant CartModel (
builder: (context, _, model) = ProductSquare(
product: product,
onTap: () = model.add(product),
),
);
}).toList(),
);
}

ATENÇÃO! Tendo em vista que o nosso editor bloqueia alguns caracteres, na parte onde há espaços no código ScopedModelDescendant CartModel colocar o símbolo de maior e menor antes e depois de CartModel.

Pronto, vemos que não precisamos mudar a estrutura de nosso aplicativo, basta adicionar alguns widgets extras e tudo irá funcionar.

Muito bom, lembrando que os códigos acima só servem para lhe mostrar como fazer a aplicação do InheritedWidget e ScopedModel.

Mas, preciso lhes dizer que sempre que você notificar os ouvintes, você notifica todos os ouvines descendentes, ou seja, estaremos reconstruindo o botão que criamos, mas também estamos reconstruindo todos os  produtos, o que é ruim.

Portanto, podemos concertar esse pequeno probleminha? Sim, o ScopedModelDescendant tem uma função chamada rebuildOnChange que podemos desabilitar e impedir que ela se reconstrua quando houver mudanças:

class ProductGrid extends StatelessWidget {
@override
Widget build(BuildContext context) = GridView.count(
crossAxisCount: 2,
children: catalog.products.map((product) {
return ScopedModelDescendantCartModel(
rebuildOnChange: false,
builder: (context, _, model) = ProductSquare(
product: product,
onTap: () = model.add(product),
),
);
}).toList(),
);
}

Assim, ele não irá reconstruir o nosso botão toda vez que eu mudar os dados.

Ótimo, o que aprendemos hoje?

  • a) Acessando o state podemos acessar o estado arbitrariamente pela árvore sem ter que mexer com nossos widgets intermediários;
  • b) Nós temos um meio razoavelmente elegante de notificar nossos widgets agora por nossos descendentes observando as mudanças de estado e, em seguida, a reconstrução.

Entretanto, temos um pequena questão sobre a quantidade de código que devemos criar para fazer estas mudanças, pois precisaremos saber quais ítens devem ou não ser reconstruídos e é por isso que no próximo e último artigo iremos estudar sobre fluxos, clique aqui para começar a lê-lo.

186 Visualizações