Agora que você conhece a programação declarativa da interface do usuário e a diferença entre o estado efêmero e o estado do aplicativo , está pronto para aprender como gerenciar facilmente o estado do aplicativo.
Usaremos o pacote
provider. Se você é novo no Flutter e não tem uma razão convincente para escolher uma abordagem diferente (Redux, Rx, ganchos, etc.), esta é provavelmente a melhor abordagem para começar. O pacote provider é fácil de aprender e não requer muitos códigos. Ele também opera com conceitos que são aplicáveis em todas as outras abordagens.
No entanto, se você já tem muita experiência com gerenciamento de estado de outras estruturas reativas, pode procurar outros pacotes e tutoriais listados na página de opções .
Exemplo
Considere o seguinte aplicativo simples como exemplo.
O aplicativo possui duas telas distintas: catálogo e carrinho de compras (representados por widgets
MyCataloge MyCartrespectivamente). Neste caso, este é um aplicativo de compras, mas você pode imaginar a mesma estrutura em um aplicativo de rede social simples (substitua o catálogo por “parede” e carrinho por “favoritos”).
A tela do catálogo inclui uma barra de aplicativos personalizável (
MyAppBar) e uma exibição de rolagem de vários itens de lista ( MyListItems).
Aqui está o aplicativo na forma de uma árvore de widgets:
Portanto, temos pelo menos 5 subclasses
Widget. Muitos deles precisam de acesso para afirmar que não os possuem. Por exemplo, cadaMyListItemdeve ser capaz de se adicionar ao carrinho. Eles também podem precisar verificar se o item exibido atualmente está no carrinho.
Isso nos leva à nossa primeira pergunta: onde devemos colocar o estado atual do balde?
Condição crescente
No Flutter, faz sentido posicionar o estado acima dos widgets que o utilizam.
Pelo que? Em estruturas declarativas como o Flutter, se você deseja alterar a interface do usuário, é necessário reconstruí-la. Você não pode simplesmente ir e escrever
MyCart.updateWith(somethingNew). Em outras palavras, é difícil forçar a mudança do widget de fora, chamando um método nele. E mesmo se você pudesse fazê-lo funcionar, você estaria lutando contra a estrutura em vez de deixar que ela o ajudasse.
// :
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
Mesmo se você fizer o código acima funcionar, você terá que lidar
MyCartcom o seguinte no widget :
// :
Widget build(BuildContext context) {
return SomeWidget(
// .
);
}
void updateWith(Item item) {
// - UI.
}
Você precisará levar em consideração o estado atual da IU e aplicar os novos dados a ela. Será difícil evitar erros aqui.
No Flutter, você cria um novo widget toda vez que seu conteúdo muda. Em vez de
MyCart.updateWith(somethingNew)(chamada de método), você usa MyCart(contents)(construtor). Visto que você só pode criar novos widgets nos métodos de construção do pai, se quiser alterá- contentslo, deve ser no pai MyCartou superior.
//
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
Agora
MyCarttem apenas um caminho de código para criar qualquer versão da interface do usuário.
//
Widget build(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
return SomeWidget(
// , .
// ···
);
}
Em nosso exemplo,
contentsdeveria estar em MyApp. Cada vez que ele muda, ele reconstrói o MyCart no topo (mais sobre isso depois). Dessa MyCartforma, você não precisa se preocupar com o ciclo de vida - ele apenas declara o que mostrar para qualquer conteúdo fornecido. Quando ele mudar, o widget antigo MyCartdesaparecerá e será completamente substituído pelo novo.
Isso é o que queremos dizer quando afirmamos que os widgets são imutáveis. Eles não mudam - eles são substituídos.
Agora que sabemos onde colocar o estado do balde, vamos ver como acessá-lo.
Acesso estatal
Quando um usuário clica em um dos itens do catálogo, ele é adicionado ao carrinho. Mas já que o carrinho acabou
MyListItem, como faremos isso?
Uma opção simples é fornecer um retorno de chamada que
MyListItempode ser invocado com um clique. As funções do Dart são objetos de primeira classe, então você pode passá-las da maneira que quiser. Portanto, internamente, MyCatalogvocê pode definir o seguinte:
@override
Widget build(BuildContext context) {
return SomeWidget(
// , .
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}
Isso funciona bem, mas para o estado do aplicativo que você precisa alterar em muitos lugares diferentes, você terá que passar muitos retornos de chamada, o que se torna enfadonho rapidamente.
Felizmente, o Flutter tem mecanismos para permitir que os widgets forneçam dados e serviços aos seus descendentes (em outras palavras, não apenas aos seus descendentes, mas a quaisquer widgets downstream). Como seria de esperar de um Flutter, onde tudo é um Widget , esses mecanismos são os tipos meramente especiais de widgets:
InheritedWidget, InheritedNotifier, InheritedModele outros. Não os descreveremos aqui porque estão um pouco fora de linha com o que estamos tentando fazer.
Em vez disso, vamos usar um pacote que funciona com widgets de baixo nível, mas é fácil de usar. É chamado
provider.
Com,
providervocê não precisa se preocupar com retornos de chamada ou InheritedWidgets. Mas você precisa entender 3 conceitos:
- ChangeNotifier
- ChangeNotifierProvider
- Consumidor
ChangeNotifier
ChangeNotifierÉ uma classe simples incluída no Flutter SDK que fornece notificação de mudança de estado para seus ouvintes. Em outras palavras, se algo for ChangeNotifier, você pode se inscrever para receber suas alterações. (Esta é uma forma de Observable - para aqueles que não estão familiarizados com o termo.)
ChangeNotifierIn provideré uma maneira de encapsular o estado do aplicativo. Para aplicativos muito simples, você pode usar um ChangeNotifier. Nos mais complexos, você terá vários modelos e, portanto, vários ChangeNotifiers. (Você não precisa usar ChangeNotifiercom provider, mas esta classe é fácil de trabalhar.)
Em nosso aplicativo de compras de exemplo, queremos gerenciar o estado do carrinho em
ChangeNotifier. Criamos uma nova classe que o estende, por exemplo:
class CartModel extends ChangeNotifier {
/// .
final List<Item> _items = [];
/// .
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// ( , 42 ).
int get totalPrice => _items.length * 42;
/// [item] . [removeAll] - .
void add(Item item) {
_items.add(item);
// , , .
notifyListeners();
}
/// .
void removeAll() {
_items.clear();
// , , .
notifyListeners();
}
}
O único código específico para
ChangeNotifieré a chamada notifyListeners(). Chame esse método sempre que o modelo mudar de forma que possa ser refletido na IU do seu aplicativo. Todo o resto CartModelé o próprio modelo e sua lógica de negócios.
ChangeNotifierfaz parte flutter:foundatione não depende de nenhuma classe de nível superior no Flutter. É fácil de testar (você nem precisa usar o teste de widget para isso). Por exemplo, aqui está um teste de unidade simples CartModel:
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
});
cart.add(Item('Dash'));
});
ChangeNotifierProvider
ChangeNotifierProviderÉ um widget que fornece uma instância para ChangeNotifierseus filhos. Ele vem em um pacote provider.
Já sabemos onde colocá-lo
ChangeNotifierProvider: acima dos widgets que precisam de acesso a ele. Caso CartModelisso implique algo acima MyCarte MyCatalog.
Você não quer postar
ChangeNotifierProvidermais alto do que o necessário (porque você não quer poluir o escopo). Mas, no nosso caso, o único widget que acabou MyCarte MyCatalog- isso MyApp.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MyApp(),
),
);
}
Observe que estamos definindo um construtor que cria uma nova instância
CartModel. ChangeNotifierProviderinteligente o suficiente para não reconstruí-la, a CartModelmenos que seja absolutamente necessário. Ele também chama dispose () automaticamente no CartModel quando a instância não é mais necessária.
Se você deseja fornecer mais de uma aula, você pode usar
MultiProvider:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: MyApp(),
),
);
}
Consumidor
Agora que ele foi
CartModelfornecido aos widgets em nosso aplicativo por meio da declaração ChangeNotifierProviderna parte superior, podemos começar a usá-lo.
Isso é feito por meio de um widget
Consumer.
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
Temos que especificar o tipo de modelo que queremos acessar. Nesse caso, precisamos disso
CartModel, então escrevemos Consumer<CartModel>. Se você não especificar generic ( <CartModel>), o pacote providernão será capaz de ajudá-lo. provideré baseado em tipo e sem o tipo não entenderá o que você deseja.
O único argumento necessário para o widget
Consumeré builder. Builder é uma função que é chamada na mudança ChangeNotifier. (Em outras palavras, quando você chama notifyListeners()seu modelo, todos os métodos do construtor de todos os widgets relevantes são Consumerchamados.)
O construtor é chamado com três argumentos. A primeira é
context, que você também obtém em todos os métodos de construção.
O segundo argumento para a função builder é uma instância
ChangeNotifier... Isso é o que pedimos desde o início. Você pode usar os dados do modelo para determinar como a interface do usuário deve parecer em qualquer ponto.
O terceiro argumento é
child, é necessário para otimização. Se você tiver uma grande subárvore de widget sob a sua Consumerque não muda quando o modelo muda, você pode construí-la uma vez e obtê-la por meio do construtor.
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// SomeExhibitedWidget, .
child,
Text("Total price: ${cart.totalPrice}"),
],
),
// .
child: SomeExpensiveWidget(),
);
É melhor colocar os widgets do Consumidor o mais profundo possível na árvore. Você não deseja reconstruir grandes partes da interface do usuário apenas porque alguns detalhes foram alterados em algum lugar.
//
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
Em vez disso:
//
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
Provider.of
Às vezes, você realmente não precisa dos dados no modelo para alterar a interface do usuário, mas ainda precisa ter acesso a eles. Por exemplo, um botão
ClearCartpermite que o usuário remova tudo do carrinho. Não é necessário exibir o conteúdo do carrinho, basta chamar o método clear().
Poderíamos usá-lo
Consumer<CartModel>para isso, mas seria um desperdício. Solicitaríamos que o framework reconstruísse o widget, que não precisa ser reconstruído.
Para este caso de uso, podemos usar
Provider.ofcom o parâmetro listendefinido como false.
Provider.of<CartModel>(context, listen: false).removeAll();
Usar a linha acima no método de construção não reconstruirá este widget quando chamado
notifyListeners .
Juntando tudo
Você pode verificar o exemplo discutido neste artigo. Se você precisa de algo um pouco mais simples, verifique a aparência de um aplicativo Contador simples criado com o provedor .
Quando estiver pronto para brincar com
providervocê mesmo, lembre-se de adicionar a dependência dele à sua primeiro pubspec.yaml.
name: my_name
description: Blah blah blah.
# ...
dependencies:
flutter:
sdk: flutter
provider: ^3.0.0
dev_dependencies:
# ...
Agora você pode
'package:provider/provider.dart'; e comece a construir ...
