Por que o gerenciamento de estado é necessário?
Em primeiro lugar, por que o gerenciamento de estado é necessário? Isso ocorre porque o Flutter constrói a interface do usuário com base no estilo declarativo . Um dos propósitos de usar o gerenciamento de estado é resolver os problemas causados pelo desenvolvimento "declarativo".
O desenvolvimento "declarativo" é uma forma diferente do desenvolvimento nativo, por isso nunca ouvimos falar de gerenciamento de estado no desenvolvimento nativo. Como entendemos o desenvolvimento "declarativo"?
Análise "Declarativa" VS "Imperativa"
Analise com o contra-exemplo mais clássico
// 一、定义展示的内容
private int mCount =0;
// 二、中间展示数字的控件 TextView
private TextView mTvCount;
// 三、关联 TextView 与 xml 中的组件
mTvCount = findViewById(R.id.tv_count)
// 四、点击按钮控制组件更新
private void increase( ){
mCount++;
mTvCounter.setText(mCount.toString());
}
No Flutter, só precisamos chamar depois que a variável for incrementada setState((){})
. setState
A página inteira será atualizada, fazendo com que o valor exibido no meio seja alterado.
// 一、声明变量
int _counter =0;
// 二、展示变量
Text('$_counter')
// 三、变量增加,更新界面
setState(() {
_counter++;
});
Pode-se constatar que apenas _counter
as propriedades são modificadas no Flutter, e nenhuma operação é realizada no componente Text.Toda a interface muda conforme o estado muda.
Portanto, existe um ditado no Flutter: UI = f(state) :
No exemplo acima, o estado é _counter
o valor de , e setState
o f
método de compilação do driver é chamado para gerar uma nova interface do usuário.
Então, quais são as vantagens do declarativo e quais problemas ele traz?
Vantagens: permite que os desenvolvedores se livrem do controle complicado de componentes e se concentrem no processamento de estado
Depois de se acostumar com o desenvolvimento do Flutter, retornar ao desenvolvimento da plataforma nativa, você descobrirá que, quando vários componentes estão relacionados entre si, é muito problemático controlar o View.
No Flutter, precisamos apenas lidar com o estado (a complexidade é transferida para o mapeamento do estado -> UI, ou seja, a construção do Widget). Os desenvolvimentos mais recentes, incluindo Jetpack Compose, Swift e outras tecnologias, também estão evoluindo na direção do "declarativo".
Problemas com desenvolvimento declarativo
Ao desenvolver diretamente sem usar o gerenciamento de estado, existem três problemas encontrados:
- A lógica é acoplada à interface do usuário da página, resultando na incapacidade de reutilização/teste de unidade, confusão de modificações, etc.
- Difícil acessar dados entre componentes (páginas cruzadas)
- Não é possível controlar facilmente o intervalo de atualização (a alteração da página setState causará a alteração da página global)
Em seguida, levarei você a entender esses problemas um por um.O próximo capítulo descreverá em detalhes como a estrutura de gerenciamento de estado resolve esses problemas.
1) A lógica é acoplada à interface do usuário da página, resultando na incapacidade de reutilização/teste de unidade, confusão de modificações, etc.
Quando o negócio não era complicado no início, todos os códigos eram escritos diretamente no widget. À medida que o negócio iterava, os arquivos se tornavam cada vez maiores, tornando difícil para outros desenvolvedores entender intuitivamente a lógica de negócios interna. E alguma lógica geral, como o processamento do status da solicitação de rede, paginação, etc., é colada para frente e para trás em páginas diferentes.
Esse problema também existe no original, e ideias como o padrão de projeto MVP foram derivadas posteriormente para resolvê-lo.
2) Difícil acessar dados entre componentes (páginas cruzadas)
O segundo ponto é a interação entre componentes, por exemplo, na estrutura Widget, se um componente filho quiser exibir os campos do componente pai name
, pode ser necessário passar camada por camada.
Ou é necessário compartilhar dados de triagem entre duas páginas, e não há um mecanismo muito elegante para resolver esse acesso de dados entre páginas.
3) É impossível controlar facilmente o intervalo de atualização (a alteração da página setState levará à alteração da página global)
O último problema também é a vantagem mencionada acima: em muitos cenários, modificamos apenas parte do estado, como a cor do botão. Mas a página inteira setState
fará com que outros locais que não precisam ser alterados sejam reconstruídos, trazendo sobrecarga desnecessária.
Provedor, obter análise de design de estrutura de gerenciamento de estado
O núcleo da estrutura de gerenciamento de estado no Flutter está nas soluções para esses três problemas. Vamos dar uma olhada em como o Provider e o Get os resolvem:
Resolva o problema de acoplamento da lógica e da interface do usuário da página
Esse problema também existe no desenvolvimento nativo tradicional. Os arquivos de atividade também podem se tornar difíceis de manter com as iterações. Esse problema pode ser desacoplado por meio do modo MVP.
Simplificando, é extrair o código lógico da View para a camada do Presenter, sendo que a View é responsável apenas pela construção da view
Esta também é a solução para quase todos os frameworks de gerenciamento de estado no Flutter.Você pode pensar no Presenter na figura acima como sendo Get GetController
, Provider ChangeNotifier
ou Bloc Bloc
. Vale ressaltar que a abordagem específica é diferente entre o Flutter e o framework MVP nativo.
Sabemos que no modo MVP clássico, geralmente o View e o Presenter definem seu próprio comportamento (ação) com as interfaces e mantêm as interfaces para chamar uma à outra .
No entanto, isso não é adequado para Flutter. Do ponto de vista do relacionamento Presenter → View, View corresponde a Widget no Flutter, mas no Flutter, Widget é apenas a configuração da interface do usuário declarada pelo usuário e não é uma boa prática para controlar diretamente a instância do Widget.
Na relação View → Presenter, o Widget pode de fato segurar o Presenter diretamente, mas isso trará o problema de difícil comunicação de dados .
Diferentes estruturas de gestão do estado têm soluções diferentes para este ponto. Do ponto de vista da implementação, elas podem ser divididas em duas categorias:
- Resolvido pelo mecanismo de árvore do Flutter, como Provider;
- Por meio de injeção de dependência, como Get.
1) Processar a aquisição de V → P através do mecanismo Flutter tree
abstract class Element implements BuildContext {
/// 当前 Element 的父节点
Element? _parent;
}
abstract class BuildContext {
/// 查找父节点中的T类型的State
T findAncestorState0fType<T extends State>( );
/// 遍历子元素的element对象
void visitChildElements(ElementVisitor visitor);
/// 查找父节点中的T类型的 InheritedWidget 例如 MediaQuery 等
T dependOnInheritedWidget0fExactType<T extends InheritedWidget>({
Object aspect });
……
}
O elemento implementa o método de manipulação da estrutura da árvore na classe pai BuildContext
Sabemos que existem três árvores em Flutter, Widget, Element e RenderObject. A chamada árvore Widget é, na verdade, apenas uma maneira de descrevermos o relacionamento de aninhamento de componentes e é uma estrutura virtual . No entanto, Element e RenderObject realmente existem em tempo de execução.Você pode ver que o componente Element contém _parent
propriedades para armazenar seu nó pai. E implementa BuildContext
a interface, incluindo muitos métodos para operações de estrutura de árvore, por exemplo findAncestorStateOfType
, procurando o nó pai; visitChildElements
percorrendo os nós filhos.
No exemplo inicial, podemos encontrar os objetos de Elemento necessários camada por camada, obter o Widget ou Estado e, em seguida, recuperar as variáveis necessárias.context.findAncestorStateOfType
O provedor também usa esse mecanismo para concluir a aquisição de View -> Presenter. Obtendo Provider.of
o objeto Present no componente Provider de nível superior. Obviamente, todos os nós do Widget abaixo do Provider podem acessar o Presenter no Provider através de seu próprio contexto, o que resolve o problema de comunicação entre componentes.
2) Resolva V → P por meio de injeção de dependência
O mecanismo da árvore é bom, mas depende do contexto, que às vezes pode ser enlouquecedor. Sabemos que o Dart é um modelo de encadeamento único, portanto, não há condição de corrida para acesso a objetos em multiencadeamento. Com base nesse Get, use um mapa singleton global para armazenar objetos. Por meio da injeção de dependência, é realizada a aquisição da camada Presenter. Desta forma, o Presenter pode ser obtido em qualquer aula.
A chave correspondente a este Map é runtimeType
+ tag
, onde tag é um parâmetro opcional, e value corresponde Object
, ou seja, podemos armazenar qualquer tipo de objeto e obtê-lo em qualquer posição.
Resolva o problema de que é difícil acessar dados entre componentes (páginas cruzadas)
Este problema é basicamente semelhante ao pensamento da parte anterior, pelo que podemos resumir as características das duas soluções:
Fornecedor
- O mecanismo da árvore de dependência deve ser baseado no contexto
- Fornece a capacidade de subcomponentes acessarem a camada superior
Pegar
- Singleton global, acessível em qualquer lugar
- Há duplicação de tipos e problemas de recuperação de memória
Resolva o problema de atualização desnecessária causada por setState de alto nível
Por fim, setState
o problema de atualização desnecessária causado pelo alto nível que mencionamos, o Flutter resolve adotando o modo observador, a chave está em duas etapas:
- O observador se inscreve no objeto observado;
- O objeto observado notifica o observador.
O sistema também prevê ValueNotifier
a implementação de outros componentes:
/// 声明可能变化的数据
ValueNotifier<int> _statusNotifier = ValueNotifier(0);
ValueListenableBuilder<int>(
// 建立与 _statusNotifier 的绑定关系
valueListenable: _statusNotifier,
builder: (c, data, _) {
return Text('$data');
})
///数据变化驱动 ValueListenableBuilder 局部刷新
_statusNotifier.value += 1;
Depois de entender o padrão de observador mais básico, observe os componentes fornecidos em diferentes estruturas:
Por exemplo, o provedor fornece ChangeNotifierProvider
:
class Counter extend ChangeNotifier {
int count = 0;
/// 调用此方法更新所有观察节点
void increment() {
count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
/// 返回一个实现 ChangeNotifier 接口的对象
create: (_) => Counter(),
child: const MyApp( ),
),
);
}
/// 子节点通过 Consumer 获取 Counter 对象
Consumer<Counter>(
builder:(_, counter, _) => Text(counter.count.toString())
Ainda o exemplo do contador anterior, aqui Counter
ele é herdado ChangeNotifier
por meio do provedor de nível superior para armazenamento. O nó filho pode obter a instância através do Consumer, após chamar increment
o método, apenas o componente Text correspondente será alterado.
A mesma função, em Get, só precisa chamar Get.put
o Counter
objeto de armazenamento do método com antecedência e GetBuilder
especificá-lo como um tipo genérico para o componente Counter
. Como Get é baseado em um singleton, GetBuilder
o objeto armazenado pode ser obtido diretamente por meio de genéricos e exposto no método construtor. Desta Counter
forma, estabelece-se uma relação de monitorização com o componente, sendo que Counter
as alterações posteriores irão apenas conduzir GetBuilder
à atualização do componente que o utiliza como genérico.
class Counter extends GetxController {
int count = 0;
void increase() {
count++;
update();
}
}
/// 提前进行存储
final counter = Get.put(Counter( ));
/// 直接通过泛型获取存储好的实例
GetBuilder<Counter>(
builder: (Counter counter) => Text('${counter.count}') );
Perguntas Frequentes na Prática
No processo de uso dessas estruturas, você pode encontrar os seguintes problemas:
O nível de contexto no provedor é muito alto
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => const Count(),
child: MaterialApp(
home: Scaffold(
body: Center(child: Text('${Provider.of<Counter>(context).count}')),
),
),
);
}
}
Conforme mostrado no código, quando aninhamos diretamente o provedor e o componente no mesmo nível, o Provider.of(context)
tempo de execução no código o lança ProviderNotFoundException
. Como o contexto que usamos aqui vem de MyApp, mas o nó do elemento Provider está localizado abaixo de MyApp, portanto, o Provider.of(context)
nó Provider não pode ser obtido. Há duas maneiras de corrigir esse problema, conforme mostrado no código a seguir:
Melhoria 1: ao aninhar o componente Builder, use o contexto do nó filho para acessar:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => const Count(),
child: MaterialApp(
home: Scaffold(
body: Center(
child: Builder(builder: (builderContext) {
return Text('${Provider.of<Counter>(builderContext).count}');
}),
),
),
),
);
}
}
Melhoria 2: Traga o Provedor para o nível superior:
void main() {
runApp(
Provider(
create: (_) => Counter(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(child: Text('${Provider.of<Counter>(context).count}')),
),
);
}
}
Get tem problemas com singletons globais
Conforme mencionado acima, Get usa o singleton global e o padrão runtimeType
é armazenar objetos como chaves. Em alguns cenários, os objetos obtidos podem não atender às expectativas, como pular entre as páginas de detalhes do produto. Como diferentes instâncias da página de detalhes correspondem à mesma classe, elas são runtimeType
iguais. Se você não adicionar o parâmetro tag, chamar em uma determinada página Get.find
obterá os objetos que foram armazenados em outras páginas. Ao mesmo tempo, você deve prestar atenção à reciclagem de objetos em Get, caso contrário, é provável que cause vazamentos de memória. Faça dispose
isso manualmente no tempo da página delete
ou use totalmente o componente fornecido no Get, por exemplo GetBuilder
, ele será dispose
lançado no .
GetBuilder
A reciclagem é realizada em dispose
fases:
@override
void dispose() {
super.dispose();
widget.dispose?.call(this);
if (_isCreator! || widget.assignId) {
if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
GetInstance().delete<T>(tag: widget.tag);
}
}
_remove?.call();
controller = null;
_isCreator = null;
_remove = null;
_filter = null;
}
Resumo das vantagens e desvantagens de Get e Provider
Através deste artigo, apresentei a você a necessidade do gerenciamento de estado, quais problemas ele resolve no desenvolvimento do Flutter e como resolvê-los. Ao mesmo tempo, também resumi problemas comuns na prática, etc., veja aqui Você ainda pode ter alguns dúvidas, precisa usar gerenciamento de estado?
Na minha opinião, frameworks existem para resolver problemas. Então depende se você também está passando por essas questões que foram levantadas no início. Se houver, você pode tentar usar o gerenciamento de estado para resolvê-lo; caso contrário, não há necessidade de superdimensioná-lo, use-o para uso.
Em segundo lugar, se o gerenciamento de estado for usado, o que é melhor, Get ou Provider?
Essas duas estruturas têm suas próprias vantagens e desvantagens. Acho que, se você ou sua equipe são novos no Flutter, usar o Provider pode ajudá-lo a entender o mecanismo principal do Flutter mais rapidamente. E se você já tem uma compreensão dos princípios do Flutter, as funções avançadas e a API concisa do Get podem ajudá-lo a melhorar sua eficiência de desenvolvimento.