BloC 8.1.1+ — Por que eu amo Hydrated BloC e por que você provavelmente também deveria
Leia isto se estiver interessado em refatorar rapidamente seu código para persistir no estado.
Quem não gosta de simples? Afinal, esse não é um dos mantras mais amados da programação – Keep It Simple? A vantagem de usar flutter_bloc em seu código para gerenciamento de estado vem com muitos benefícios, incluindo a liberdade da dívida técnica associada a StatefulWidgets E é a oportunidade perfeita para facilmente persistir o estado usando Hydrad_bloc. Para mim, usar o pacote Hydrated Bloc é quase tão fácil quanto cair de um tronco e é um acéfalo. Espero abrir sua mente para usá-lo também. Primeiro, mostrarei as etapas que demonstram como é fácil refatorar seu código para usar bloc e depois hydrate. Persistir dados de estado nunca foi tão fácil!
Conteudo
Intenções de aprendizagem
- Você aprenderá sobre Bloc e Hydrated Bloc.
- Você aprenderá a refatorar o código para usar o Bloc.
- Você aprenderá a refatorar o código Bloc para usar o Hydrated Bloc.
- Você aprenderá a persistir dados facilmente.
O código inicial do GitHub (Link)
O Código Padrão de Ações (sem BloC)
Primeiramente, vamos entender o app que iremos converter para bloc.
Vamos imaginar que este aplicativo rastreie um item de um videogame. O item pode dar ao usuário bônus numéricos dependendo se é mágico, afiado ou pesado. O item pode ter qualquer combinação dos três (ou seja, mágico e/ou afiado e/ou pesado). Ao pressionar uma das opções de bônus, um valor numérico pode ser adicionado ou removido do total que é exibido dentro do círculo azul.
Vejamos o código usando StatefulWidget (sem BLoC)
Apresento o código em diferentes arquivos. Pode não fazer sentido dividir um código tão simples, mas é melhor compartimentá-lo antes de adicionar e alterar o código.
main.dart
void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: MyHomePage(), ); } }
home_page.dart
import 'package:app_enum_example/enums/item_enum.dart'; import 'package:flutter/material.dart'; class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { List<Item> items = [Item.magical, Item.sharp, Item.heavy]; List<Item> selected = []; int _getSum(item) => item.fold(0, (previousValue, element) => previousValue + element.bonus()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('App: Decorators-like Enum'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircleAvatar( radius: 100, child: Text('${_getSum(selected)}'), ), Wrap( children: [ ...items .map((item) => ActionChip( backgroundColor: selected.contains(item) ? Colors.blue : Colors.grey, label: Text(item.name.toString()), onPressed: () { setState(() { selected.contains(item) ? selected.remove(item) : selected.add(item); }); }, )) .toList(), ], ), ], ), ), ); } }
item_enum.dart
enum Item { magical(7), sharp(5), heavy(3); const Item(this.damage); final int damage; int bonus() => damage; }
Para referência, um diagrama de código roxo mostra onde os widgets aparecem no aplicativo.
Para facilitar a compreensão, o estado importante e o código de modificação de estado acima são truncados e exibidos abaixo. A var selecionada é uma List que contém qualquer número de itens de enumeração. A selected var é o ponto focal do estado. O widget CircleAvatar possui um widget Text que acessa os valores numéricos passando a var selecionada para o método _getSum, que é um método .fold que tabula e retorna o total dos valores passados. Por fim, o estado pode ser modificado pressionando um widget ActionChip em uníssono com setState. Pressionar um widget ActionChip também alterna sua cor.
Como não estamos usando bloc, precisamos usar um StatefulWidget e seu método setState para atualizar os dados quando o estado mudar.
... // state List<Item> selected = []; // adds all of the state values together and returns the sum int _getSum(item) => item.fold(0, (previousValue, element) => previousValue + element.bonus()); ... ... // exibe a soma total do state. CircleAvatar( radius: 100, child: Text( '${_getSum(selected)}', style: const TextStyle(fontSize: 50), ), ), ... ... // modifica o xstate. onPressed: () { setState(() { selected.contains(item) ? selected.remove(item) : selected.add(item); }); }, ...
Parte 1. Refatoração para BLoC State Management
Adicione as seguintes bibliotecas ao arquivo ‘pubspec.yaml‘.
dependencies: flutter: sdk: flutter flutter_bloc: ^8.1.3 equatable: ^2.0.5 cupertino_icons: ^1.0.5
Etapa 1 – Conversão do gerenciamento de estado para BLoC
Como estamos trabalhando com flutter_bloc, a melhor prática é trabalhar com events, states e blocs.
Adicione estes três arquivos à pasta lib do seu projeto:
bloc_events.dart
abstract class SelectedEvents {} class AddItem extends SelectedEvents { AddItem(this.item); final Item item; } class RemoveItem extends SelectedEvents { RemoveItem(this.item); final Item item; }
bloc_state.dart
class SelectedState { SelectedState({required this.item, required this.selectedItems}); final List<Item> item; final List<Item> selectedItems; } class InitialState extends SelectedState { InitialState() : super(item: [Item.magical, Item.sharp, Item.heavy], selectedItems: []); }
Observe que começamos implementando nosso estado inicial aqui, onde uma Lista de Itens é passada para a superclasse (SelectedState) que contém todos os elementos que queremos incluir no widget Wrap, e uma Lista de itens vazia que usamos para o nosso valor exibido.
item_bloc.dart
class SelectedBloc extends Bloc<SelectedEvents, SelectedState> { SelectedBloc() : super(InitialState()) { on<AddItem>(((event, emit) => emit(SelectedState( item: state.item, selectedItems: state.selectedItems..add(event.item), )))); on<RemoveItem>(((event, emit) => emit(SelectedState( item: state.item, selectedItems: state.selectedItems..remove(event.item), )))); } }
O método emit é usado para alterar, monitorar e atualizar o estado. Por fim, o comando emit indica aos StatelessWidgets para redesenhar. Observe que ..add e ..remove têm pontos duplos. Os pontos duplos são necessários aqui.
Etapa 2 – Adicionar BlocProvider ao main.dart
main.dart
void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { // add bloc provider return BlocProvider( create: (context) => SelectedBloc(), child: const MaterialApp( home: MyHomePage(), ), ); } }
Etapa 3 – Modifique a página inicial
- Altere o StatefulWidget para StatelssWidget.
- Adicionar um BlocBuilder.
- Altere o código para funcionar com o estado com um BlocProvider em home_page.dart
home_page.dart
**Converter de StateFullWidget para StatelessWidget** class MyHomePage extends StatelessWidget { const MyHomePage({Key? key}) : super(key: key); int _getSum(item) => item.fold(0, (previousValue, element) => previousValue + element.bonus()); @override Widget build(BuildContext context) { **Add bloc builder** return BlocBuilder<SelectedBloc, SelectedState>( builder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('App Enum with BLoC'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CircleAvatar( radius: 100, child: Text( _getSum(state.selectedItems).toString(), style: const TextStyle(fontSize: 50), ), ), Wrap( children: [ ...Item.values .map((e) => Padding( padding: const EdgeInsets.symmetric(horizontal: 1), child: ActionChip( backgroundColor: state.selectedItems.contains(e) ? Colors.blue : null, label: Text( e.name.toString(), ), onPressed: () { **// 3) Word with the state with BlocProvider** state.selectedItems.contains(e) ? BlocProvider.of<SelectedBloc>(context) .add(RemoveItem(e)) : BlocProvider.of<SelectedBloc>(context) .add(AddItem(e)); }, ), )) .toList(), ], ), ], ), ), ); }, ); } }
Etapa 4 – Pare e reinicie completamente seu aplicativo
Agora o estado será atualizado sem usar um StatefulWidget.
O GitHub para o código completo Parte 1 (Link)
Parte 2. Refatorando para Hydrated Bloc
Agora vamos para a parte emocionante! É assim que é fácil tornar seu estado persistente!
Obviamente, é aqui que você começaria se já usasse flutter_bloc em seus aplicativos. Facilitando ainda mais para você!
Adicione as seguintes bibliotecas ao arquivo pubspec.yaml.
pubspec.yaml
hydrated_bloc: ^9.1.2 path_provider: ^2.0.15
Etapa 1 – Modifique bloc_state.dart
Agora estamos adicionando os elementos necessários para o Hydrated Bloc. Resumindo, hydrated bloc usa toJson e fromJson para tornar o estado persistente. Normalmente, os aplicativos já usam as funcionalidades toJson e fromJson para interações com o banco de dados. Ser tão comum significa que bloc hydrated é ainda mais fácil de usar para a maioria de nós. Apenas uma observação especial para o código abaixo, usei uma função auxiliar getItemType para converter a string retornada em um enum. Sem converter para um enum dessa maneira, experimentei um bug difícil de detectar.
Em bloc_state.dart faça os passos abaixo. As etapas 1 a 3 adicionam código, enquanto a etapa 4 remove e consolida o código. As etapas 3 a 4 nem sempre são necessárias, dependendo de como você inicializa o estado do bloco. Eu os adiciono aqui porque eles limpam o código.
- Adicionar ao método Json.
- Adicionar da fábrica Json.
- Adicione uma fábrica de estado inicial.
- Remova a classe de estado inicial.
bloc_state.dart
import 'package:new_item_experiment/enums/item_enum.dart'; class SelectedState { SelectedState({required this.item, required this.selectedItems}); final List<Item> item; final List<Item> selectedItems; ); // 1) adicionar ao método Json Map<String, dynamic> toJson() => { 'items': item.map((e) => e.toString()).toList(), 'selectedItems': selectedItems.map((e) => e.toString()).toList(), }; // 2) adicionar de Json factory factory SelectedState.fromJson(Map<String, dynamic> json) => SelectedState( item: (json['items'].map<Item>((e) => getItemType(e)).toList()), selectedItems: json['selectedItems'].map<Item>((e) => getItemType(e)).toList(), ); // 3) adicione um estado inicial factory factory SelectedState.initial() => SelectedState( item: Item.values, selectedItems: [], ); } // 4) remova a classe de estado inicial, agora que ela é tratada no factory (etapa 3) // class InitialState extends SelectedState { // InitialState() // : super(item: [Item.magical, Item.sharp, Item.heavy], selectedItems: []); // } Item getItemType(String item) { switch (item) { case 'Item.magical': return Item.magical; case 'Item.sharp': return Item.sharp; case 'Item.heavy': return Item.heavy; default: return Item.heavy; } }
Etapa 2 – Modifique o ItemBloc para Hydrated Bloc, corrija a super injeção e adicione Overrides
As alterações anteriores criam um erro porque removemos a classe InitialState. Siga estas etapas no arquivo item_bloc.dart.
- Mude Bloc para Hydrated Bloc.
- Altere o que é injetado na superclasse para SelectedState.initial( ).
- Adicione fromJson ao arquivo overrides.
- Adicione toJson ao overrides.
item_bloc.dart
// 1) Alterar Bloc para Hydrated Bloc class SelectedBloc extends HydratedBloc<SelectedEvents, SelectedState> { // 2) Mude para usar a nova fonte de estado SelectedBloc() : super(SelectedState.initial()) { on<AddItem>(((event, emit) => emit(SelectedState( item: state.item, selectedItems: state.selectedItems..add(event.item), )))); on<RemoveItem>(((event, emit) => emit(SelectedState( item: state.item, selectedItems: state.selectedItems..remove(event.item), )))); } // 3) adicione o fromJson override @override SelectedState? fromJson(Map<String, dynamic> json) { return SelectedState.fromJson(json); } // 4) adicione o toJson override @override Map<String, dynamic>? toJson(SelectedState state) { return state.toJson(); } }
Etapa 3 – Configure o arquivo main.dart para o Hydrated Bloc Data Storage
Atualize todo o arquivo main.dart
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(const DIMultiWidgetSubTree()); } class DIMultiWidgetSubTree extends StatelessWidget { const DIMultiWidgetSubTree({ super.key, }); @override Widget build(BuildContext context) { return RepositoryProvider( create: (context) => SelectedBloc(), child: const MyApp()); } } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => SelectedBloc(), child: const MaterialApp( home: MyHomePage(), ), ); } }
É neste código que o Hydrated Bloc faz todo o trabalho pesado. Sempre que uma mudança de estado é acionada, uma variável de armazenamento é armazenada na memória persistente.
Etapa 4 – Pare e reinicie completamente seu aplicativo
Agora o estado é persistente. Tente clicar no botão mágico para realçá-lo e, em seguida, pressione o botão hot reload. Continua em destaque!
Código final do GitHub (link)
Conclusão
É isso, agora temos memória persistente em pleno funcionamento que basicamente vem de graça quando você usa o BLoC. Acabamos de ver as etapas simples para converter do gerenciamento de estado Stateful para BloC e, em seguida, vimos como torná-lo persistente com Hydrated BloC.
Não deixe de conhecer meus Ebooks de Flutter/Dart