Flutter: Isolando estados com Mixins
Carlos Costa • 03/11/2024 Nesse tutorial, vamos explorar como utilizar mixins para isolar o estado de um widget em Flutter, essa técnica pode ser um recurso extremamente útil para manter o código mais organizado e fácil de testar.Introdução
Primeiro vamos entender o que são Mixins no Dart.
No Dart, um mixin é uma forma de reutilizar código em várias classes sem precisar de herança. Mixins permitem definir métodos e propriedades em uma estrutura semelhante a uma classe que pode então ser “misturada” com outras classes.
Exemplo:
mixin Flyable {
void fly() {
print('Flying');
}
}
class Bird with Flyable {
void fly() {
print('Bird is flying');
}
}
void main() {
Bird bird = Bird();
bird.fly();
}
Nesse exemplo, a classe Bird
usa o mixin Flyable
para adicionar o método fly
ao objeto bird
.
StatefulWidget
Vamos criar um simple contador para ilustrar o nosso exemplo:
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
void _decrementCounter() {
setState(() {
_counter--;
});
}
void _resetCounter() {
setState(() {
_counter = 0;
});
}
void _doubleCounter() {
setState(() {
_counter *= 2;
});
}
void _halfCounter() {
setState(() {
_counter ~/= 2;
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Count: $_counter',
),
FilledButton(onPressed: _incrementCounter, child: const Text('Increment')),
FilledButton(onPressed: _decrementCounter, child: const Text('Decrement')),
FilledButton(onPressed: _resetCounter, child: const Text('Reset')),
FilledButton(onPressed: _doubleCounter, child: const Text('Double')),
FilledButton(onPressed: _halfCounter, child: const Text('Half')),
],
),
);
}
}
Aqui temos um StatefulWidget
chamado Counter
que possui um estado interno _counter
e métodos para manipular esse estado. Apesar desse código ser bem simples tem duas responsabilidades importantes:
- • A lógica do contador: incrementar, decrementar, resetar e modificar o valor do contador.
- • A renderização do contador: exibir o contador e os botões para manipular o estado.
Considerando essas duas responsabilidades, podemos criar um mixin chamado CounterController
que será responsável por lidar com a lógica do contador.
Controller
Vamos refatorar o código para ter a seguinte estrutura:
📂 counter — 📄 counter.dart — 📄 counter.controller.dart
- counter: widget que renderiza o contador.
- counter.controller: mixin que lida com a lógica do contador.
Nosso counter.controller
será escrito da seguinte forma:
mixin CounterController<T extends StatefulWidget> on State<T> {
int counter = 0;
void incrementCounter() {
counter++;
}
void decrementCounter() {
counter--;
}
void resetCounter() {
counter = 0;
}
void doubleCounter() {
counter *= 2;
}
void halfCounter() {
counter ~/= 2;
}
}
Nosso CounterController
foi escrito da forma mais “pura” possível, sem usar setState
ou context
. Isso futuramente vai facilitar os nossos testes unitários.
O tipo T
é uma restrição para que o CounterController
seja aplicado apenas a classes que herdam de StatefulWidget
.
on State<T>
permite que o mixin seja aplicado apenas a classes que herdam de State<T>
e fornece acesso a todas as propriedades e métodos.
Já o nosso Counter
será feito da seguinte forma:
import 'package:flutter/material.dart';
import 'counter.controller.dart';
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> with CounterController {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Count: $counter',
),
FilledButton(
onPressed: () {
setState(() {
incrementCounter();
});
},
child: const Text('Increment'),
),
FilledButton(
onPressed: () {
setState(() {
decrementCounter();
});
},
child: const Text('Decrement'),
),
FilledButton(
onPressed: () {
setState(() {
resetCounter();
});
},
child: const Text('Reset'),
),
FilledButton(
onPressed: () {
setState(() {
doubleCounter();
});
},
child: const Text('Double'),
),
FilledButton(
onPressed: () {
setState(() {
halfCounter();
});
},
child: const Text('Half'),
),
],
),
);
}
}
Através do with CounterController
o Counter
agora possui acesso aos métodos e propriedades do CounterController
. Dessa forma, podemos chamar os métodos incrementCounter
, decrementCounter
, resetCounter
, doubleCounter
e halfCounter
diretamente no Counter
.
Nossa estutura ficou mais simples, separamos as responsabilidades e agora podemos ter um código mais organizado e fácil de manter.
Tests
Vamos criar alguns testes unitários para garantir que nosso código está funcionando corretamente.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_articles/counter.controller.dart';
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> with CounterController {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
void main() {
group("Counter", () {
var state = const Counter().createState() as _CounterState;
test('check incrementCounter', () {
expect(state.counter, 0);
state.incrementCounter();
state.incrementCounter();
state.incrementCounter();
expect(state.counter, 3);
});
test('check decrementCounter', () {
state.decrementCounter();
state.decrementCounter();
state.decrementCounter();
expect(state.counter, 0);
});
test('check resetCounter', () {
state.incrementCounter();
state.incrementCounter();
state.incrementCounter();
state.resetCounter();
expect(state.counter, 0);
});
test('check doubleCounter', () {
state.incrementCounter();
state.incrementCounter();
state.incrementCounter();
state.doubleCounter();
expect(state.counter, 6);
});
test('check halfCounter', () {
state.halfCounter();
expect(state.counter, 3);
});
});
}
Primeiro vamos criamos um Counter
que usa o nosso mixin CounterController
, através dessa classe podemos usar o método createState
para criar um objeto _CounterState
que herda de State<Counter>
e possui acesso aos métodos e propriedades do nosso mixin, dessa forma podemos interagir diretamente com o nosso estado e observar todas as mudanças.
Conclusão
Como vimos, o uso de mixins é uma ferramenta poderosa para isolar o estado de um widget, permitindo que você possa manter o código mais organizado e fácil de manter. Também facilita a criação de testes unitários já que temos a opção de não usar setState
ou context
.