BloC 8.1.1+ — Por que eu amo Hydrated BloC e por que você provavelmente também deveria

Tempo de leitura: 7 minutes

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!

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 eventsstates 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

  1. Altere o StatefulWidget para StatelssWidget.
  2. Adicionar um BlocBuilder.
  3. 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.

  1. Adicionar ao método Json.
  2. Adicionar da fábrica Json.
  3. Adicione uma fábrica de estado inicial.
  4. 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