No universo Flutter, encontramos diversas formas de lidar com gerência de estado e de aplicar as melhores práticas da linguagem em determinados contextos, nesse artigo vou lhe apresentar mais uma forma para essa coleção! Vamos conhecer um pouco sobre o State Pattern, que promete obedecer os princípios de responsabilidade única e composição de uma forma mais organizada, limpa e de fácil manutenção.
Tópicos
- O que é o State Pattern?
- Quando devo usar StatePattern?
- State Pattern na prática
- Conclusão
- Referências
O que é o State Pattern?
No site de design patterns in Dart, podemos encontrar a seguinte definição, que é uma maneira simples e direta de dizer do que se trata o nosso State Pattern:
O State pattern é utilizado na programação para encapsular comportamentos diferentes para o mesmo objeto, com base no seu estado interno. Esta pode ser uma forma mais limpa de um objeto alterar o seu comportamento em tempo de execução sem recorrer a declarações condicionais, melhorando assim a manutenção.
Isso quer dizer que cada estado do nosso objeto estará separado por classes, que serão extensões/variações de uma classe de estado principal de um determinado objeto.
Mas como assim?
Imagine que você tem um objeto Agua, para representar o estado desta Agua você cria um EstadoDaAgua, logo as variações de estado de Agua poderiam ser Solido, Liquido e Gasoso, ou seja:
abstract class EstadoDaAgua {
//
}
class Solido extends EstadoDaAgua {
//
}
class Liquido extends EstadoDaAgua {
//
}
class Gasoso extends EstadoDaAgua {
//
}
O State Pattern é usado inclusive no padrão BLoC. Se você usa padrão BLoC, obrigatoriamente já estará usando State Pattern.
Quando devo usar State Pattern?
- Quando seu objeto se comporta diferente dependendo do seu estado.
- Quando o número de estados é grande e o código do estado muda frequentemente.
- Quando sua classe tiver uma quantidade massiva de condicionais que alteram a forma como a classe se comporta de acordo com os valores dos campos que ela contém.
- Quando tiver muito código duplicado de estados e transições semelhantes.
State Pattern na prática
Agora, vamos ver um exemplo de uso de State Pattern na prática. Observando a classe abaixo, percebemos que o estado está sendo definido a partir de variáveis usando ChangeNotifier:
class CategoryStore extends ChangeNotifier {
List<String> categories = [];
bool isLoading = false;
String error = '';
IApiDatasource apiDatasource = ApiDatasource();
void getCategories() async {
isLoading = true;
await apiDatasource.getCategories().then((response) {
response.fold(
(left) => error = left.message;
(right) => categories = right;
)
});
isLoading = false;
notifyListeners();
}
}
O método
fold()
utilizado no exemplo pertence ao Either Type, um elemento da programação funcional utilizado para representar um valor que tem qualquer um dos dois tipos especificados. O Either é comumente usado para representar um valor de sucesso ou um valor de falha, assim como exemplificado acima ondeleft
representa o valor de erro e oright
o valor de sucesso.
A classe acima contém uma função que inicia com o carregamento (isLoading
) e, durante esse processo, ela aguarda que os dados provenientes de uma API sejam armazenados na variável categories
. Quando o carregamento é concluído, a função recebe o valor falso.
Poderíamos organizar esse código usando State Pattern da seguinte forma:
- Classe de Estados
Declaramos os estados com suas respectivas classes.
abstract class CategoryState {}
class CategoryInitial extends CategoryState {}
class CategoryLoading extends CategoryState {}
class CategoryLoaded extends CategoryState {
final List<String> categories;
CategoryLoaded(this.categories);
}
class CategoryError extends CategoryState {
final String message;
CategoryError(this.message);
}
- Classe de Store
Na função getCategories()
descartamos a variável isLoading
e utilizaremos a variável value
(variável que representa o estado no ValueNotifier, que por sua vez é do tipo CategoryState
) setando a classe CategoryLoading
para receber nosso estado.
Depois disso, no método fold()
se minha requisição deu erro ,value
receberá o estado de Error com a mensagem (left
) e se estiver dado tudo certo receberá o Loaded com seus dados carregados na variável right
e assim, acabamos por descartar as variáveis categories
e èrror
class CategoryStore extends ValueNotifier<CategoryState> {
CategoryStore() : super(CategoryInitial());
IApiDatasource apiDatasource = ApiDatasource();
void getCategories() async {
value = CategoryLoading();
await apiDatasource.getCategories().then((response) {
response.fold(
(left) => value = CategoryError(left.message);
(right) => value = CategoryLoaded(right);
)
});
}
}
value é um get de Value Notifier, onde contém o estado atual da nossa classe Store
- Exemplo em página
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
//Instancia de CategoryStore criada
CategoryStore store = CategoryStore();
// iniciando função ao carregar a página
@override
void initState() {
store.getCategories();
super.initState();
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
store.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
// "Escutando a store"
child: ValueListenableBuilder(
valueListenable: store,
builder: (context, value, child) {
//se o value receber erro retorna a mensagem
if (value is CharacterError) {
return Center(
child: Text(
'Erro ao carregar categorias: ${value.message}',
),
);
}
// se o value receber sucesso/loaded retorna a lista
// de categorias
if (value is CharacterLoaded) {
return ListView.builder(
controller: store.scroll,
itemCount: value.categories.length,
itemBuilder: (context, index) {
final category = value.categories[index];
return Text(category);
}
);
}
// em estado de loading ou inicial ficará carregando
return const Center(child: CircularProgressIndicator());
}
),
);
}
}
E pronto, já implementamos nosso State Pattern! E como vantagem: seguimos o Princípio de Responsabilidade Única e Open/Closed e simplificamos o código eliminando condicionais que poderiam deixar o nosso código poluído.
Conclusão
Muito obrigada por ter lido até aqui, como este é um artigo sobre o State Pattern, acabei por não falar muito sobre o ValueNotifier e outras coisas que escolhi utilizar no exemplo, porém vou estar deixando abaixo alguns links que podem ajudar a entender, além de também um vídeo do meu sensei @redrodrigoc explicando direitinho como o State Pattern funciona, vale a pena dar uma olhada. Espero que tenham gostado, até a próxima! 💙
Referências
- Vídeo REDRODRIGO - State Pattern
- State Pattern com ValueNotifier - bwolf
- Design patterns in Dart
- Refactoring Guru - Design patterns - State
- Either Type
- Better Error Handling with Either type in Dart
- Package para uso de Either
- Understanding Flutter ValueNotifier
Meus agradecimentos ao @redrodrigoc e a @cherryramatis por todo apoio e ajuda com este artigo 💙
Top comments (2)
Ótimo conteúdo, parabéns.
Muito obrigada, tive um bom professor kk 💙