BLoC — A magia da classe de estado único

Tempo de leitura: 4 minutes

Todos nós usamos o BLoC como uma ferramenta de gerenciamento de estado para nossos aplicativos Flutter. Normalmente, usamos várias classes de estado em nosso BLoC. Neste artigo, vamos explorar como você pode usar apenas uma única classe de estado com métodos copyWith para otimizar seu gerenciamento de estado!

 

Introdução

O BLoC é um dos gerenciamentos de estado amplamente usados na Comunidade Flutter e em aplicativos de nível de produção. No entanto, a maioria dos desenvolvedores usa várias classes de estado para BLoC. Por classe de estado múltiplo, queremos dizer ter um LoadingState, LoadedState, ErrorState, etc.

Mas, existem algumas desvantagens para esta abordagem. Uma desvantagem é o aumento potencial na complexidade e na duplicação de código. Com várias classes de estado, os desenvolvedores precisam gerenciar e coordenar as atualizações de estado em diferentes componentes, o que pode levar a uma lógica de comunicação mais complexa e a possíveis erros. Além disso, ter várias classes de estado pode resultar em uma base de código maior, dificultando a navegação e a manutenção. Também pode apresentar desafios quando se trata de compartilhar o estado entre os componentes, pois podem exigir mecanismos de sincronização adicionais. No geral, o uso de várias classes de estado no BLoC pode diminuir a simplicidade do código e aumentar a carga cognitiva dos desenvolvedores.

Vamos ver como você pode fazer o melhor uso de uma Single State Class em seu BLoC para superar essas desvantagens!

 

Modo Tradicional/Classes de Múltiplos Estados BLoC

Vamos pegar um exemplo de uma simples chamada de API que é feita através do BLoC. Vamos primeiro ver como fica com várias classes de estado:

home_state.dart

import 'package:api_service/api_service.dart';
import 'package:equatable/equatable.dart';

abstract class HomeState extends Equatable {
  @override
  List<Object?> get props => [];
}

class HomeInitialState extends HomeState {}

class HomeLoadingState extends HomeState {}

class HomeLoadedState extends HomeState {
  final PhotosModel? photosModel;

  HomeLoadedState({this.photosModel});

  @override
  List<Object?> get props => [photosModel];
}

class HomeErrorState extends HomeState {}

home_bloc.dart

import 'package:api_service/api_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'home_event.dart';
part 'home_state.dart';

class HomeBloc extends Bloc<HomeEvent, HomeState> {
  HomeBloc({required this.apiService}) : super(HomeInitialState()) {
    on<HomeApiCallEvent>(_onHomeApiCallEvent);
  }

  final ApiService apiService;

  Future<void> _onHomeApiCallEvent(
    HomeApiCallEvent event,
    Emitter<HomeState> emit,
  ) async {
    try {
      emit(HomeLoadingState());
      final photosModel = await apiService.getPhotos();
      emit(HomeLoadedState(photosModel: photosModel!));
    } catch (e) {
      emit(HomeErrorState());
    }
  }
}

E aqui está como estaríamos usando isso em nossa interface do usuário:

import 'package:bloc_single_state_class_example/home/home.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomeView extends StatelessWidget {
  const HomeView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
        centerTitle: true,
      ),
      body: const _ScaffoldBody(),
    );
  }
}

class _ScaffoldBody extends StatelessWidget {
  const _ScaffoldBody();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<HomeBloc, HomeState>(
      bloc: context.read<HomeBloc>(),
      builder: (context, state) {
        if (state is HomeLoadingState) {
          return const Center(child: CircularProgressIndicator());
        } else if (state is HomeLoadedState) {
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 10.0,
                mainAxisSpacing: 10.0,
              ),
              itemCount: state.photosModel?.photos.length ?? 0,
              itemBuilder: (context, index) {
                return Container(
                  decoration: BoxDecoration(
                    image: DecorationImage(
                      image: CachedNetworkImageProvider(
                        state.photosModel!.photos[index].url,
                      ),
                      fit: BoxFit.cover,
                    ),
                    borderRadius: BorderRadius.circular(20.0),
                  ),
                );
              },
            ),
          );
        } else if (state is HomeErrorState) {
          return const Center(
            child: Text('Something went wrong!'),
          );
        }
        return const SizedBox.shrink();
      },
    );
  }
}

Bem simples, certo? Mas e se você quiser acessar photosModel quando houver um erro também? Existem muitos casos de uso em que você deseja acessar determinadas variáveis em um tipo de estado que não possui o escopo dessa variável.

Então, para isso, seria uma opção melhor ter uma única classe de estado que tivesse o escopo de todas as variáveis presentes 🤩

Vamos agora trabalhar em nossa classe de estado único, que fará a mesma coisa, mas com uma classe de estado único e uma enumeração para o status de nosso aplicativo, como inicial, carregando, carregado e erro.

 

BLoC de classe de estado único

Vamos agora ver como podemos manipular o BLoC para usar uma Single State Class com um método enum e copyWith.

home_state.dart

part of 'home_bloc.dart';

enum HomeStatus {
  initial,
  loading,
  loaded,
  error,
}

class HomeState extends Equatable {
  final HomeStatus status;
  final PhotosModel? photosModel;

  const HomeState({required this.status, required this.photosModel});

  static HomeState initial() => const HomeState(
        status: HomeStatus.initial,
        photosModel: null,
      );

  HomeState copyWith({
    HomeStatus? status,
    PhotosModel? photosModel,
  }) =>
      HomeState(
        status: status ?? this.status,
        photosModel: photosModel ?? this.photosModel,
      );

  @override
  List<Object?> get props => [status, photosModel];
}

Portanto, no trecho de código acima, temos uma única classe chamada HomeState. Existe um método estático chamado initial() que apenas retorna um objeto de HomeState com valores iniciais. Existe outro método chamado método copyWith que atualiza o valor com base no valor passado no método. Se para um determinado parâmetro nenhum valor for passado, ele leva o valor passado para o construtor HomeState. Então, por exemplo, quando usamos o método copyWith e não passamos nenhum valor para status, ele vai atribuir HomeStatus.initial porque foi isso que passamos no método inicial.

Agora, vamos ver como ficará o arquivo do bloco:

home_bloc.dart

import 'package:api_service/api_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'home_event.dart';
part 'home_state.dart';

class HomeBloc extends Bloc<HomeEvent, HomeState> {
  HomeBloc({required this.apiService}) : super(HomeState.initial()) {
    on<HomeApiCallEvent>(_onHomeApiCallEvent);
  }

  final ApiService apiService;

  Future<void> _onHomeApiCallEvent(
    HomeApiCallEvent event,
    Emitter<HomeState> emit,
  ) async {
    try {
      emit(state.copyWith(status: HomeStatus.loading));
      final photosModel = await apiService.getPhotos();
      emit(state.copyWith(photosModel: photosModel, status: HomeStatus.loaded));
    } catch (e) {
      emit(state.copyWith(status: HomeStatus.error));
    }
  }
}

No trecho de código acima, você pode ver que emitimos o objeto de estado atualizando o valor de status e, posteriormente, quando a API foi bem-sucedida, emitimos o objeto de estado atualizando o valor de status e photosModel.

Isso é simples, certo? Agora, na interface do usuário, precisamos apenas verificar o valor do status. Vejamos como fica:

import 'package:bloc_single_state_class_example/home/home.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomeView extends StatelessWidget {
  const HomeView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
        centerTitle: true,
      ),
      body: const _ScaffoldBody(),
    );
  }
}

class _ScaffoldBody extends StatelessWidget {
  const _ScaffoldBody();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<HomeBloc, HomeState>(
      bloc: context.read<HomeBloc>(),
      builder: (context, state) {
        if (state.status == HomeStatus.loading) {
          return const Center(child: CircularProgressIndicator());
        } else if (state.status == HomeStatus.loaded) {
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 10.0,
                mainAxisSpacing: 10.0,
              ),
              itemCount: state.photosModel?.photos.length ?? 0,
              itemBuilder: (context, index) {
                return Container(
                  decoration: BoxDecoration(
                    image: DecorationImage(
                      image: CachedNetworkImageProvider(
                        state.photosModel!.photos[index].url,
                      ),
                      fit: BoxFit.cover,
                    ),
                    borderRadius: BorderRadius.circular(20.0),
                  ),
                );
              },
            ),
          );
        } else if (state.status == HomeStatus.error) {
          return const Center(
            child: Text('Something went wrong!'),
          );
        }
        return const SizedBox.shrink();
      },
    );
  }
}

Edddd, é isso 🥳

Agora podemos usar apenas uma única classe de estado com uma enumeração para diferentes status do aplicativo!

Confira o exemplo completo no repositório GitHub

Não deixe de conhecer meus Ebooks de Flutter/Dart