Gerenciamento de estado em aplicativos Flutter


Princípios gerais


Flutter é uma estrutura reativa e, para um desenvolvedor especializado em desenvolvimento nativo, sua filosofia pode ser incomum. Portanto, começamos com uma breve revisão.


A interface do usuário no Flutter, como nas estruturas mais modernas, consiste em uma árvore de componentes (widgets). Quando um componente é alterado, este e todos os seus componentes filhos são renderizados novamente (com otimizações internas, descritas abaixo). Quando a exibição muda globalmente (por exemplo, girando a tela), toda a árvore de widgets é redesenhada.


Essa abordagem pode parecer ineficaz, mas, na verdade, fornece ao programador controle sobre a velocidade do trabalho. Se você atualizar a interface no nível mais alto sem a necessidade, tudo funcionará lentamente, mas com o layout correto dos widgets, os aplicativos no Flutter podem ser muito rápidos.


O Flutter possui dois tipos de widgets - Stateless e Stateful. O primeiro (análogo ao Pure Components in React) não possui estado e é completamente descrito por seus parâmetros. Se as condições de exibição não mudarem (digamos, o tamanho da área em que o widget deve ser exibido) e seus parâmetros, o sistema reutiliza a representação visual criada anteriormente do widget, portanto, o uso de widgets Stateless tem um bom efeito no desempenho. Ao mesmo tempo, de qualquer maneira, toda vez que o widget é redesenhado, um novo objeto é criado formalmente e o construtor é iniciado.


Widgets com estado mantêm algum estado entre renderizações. Para fazer isso, eles são descritos por duas classes. A primeira das classes, o próprio widget, descreve os objetos criados durante cada renderização. A segunda classe descreve o estado do widget e seus objetos são transferidos para os objetos criados. Os widgets de estado com estado são uma das principais fontes de redesenho de interface. Para fazer isso, você precisa alterar suas propriedades dentro da chamada para o método SetState. Portanto, diferente de muitas outras estruturas, o Flutter não possui rastreamento implícito de estado - qualquer alteração nas propriedades do widget fora do método SetState não leva ao redesenho da interface.


Agora, depois de descrever o básico, você pode começar com um aplicativo simples que usa os widgets Stateless e Stateful:


Aplicativo base
import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } } 

Exemplo completo


Resultado


Se você precisar de condições mais tenazes


Vamos seguir em frente. O estado de estado dos widgets é mantido entre redesenho de interfaces, mas apenas enquanto o widget for necessário, ou seja, realmente localizado na tela. Vamos realizar um experimento simples - coloque nossa lista na guia:


Aplicativo guia
 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } 

Exemplo completo


Resultado


Na inicialização, você pode ver que, ao alternar entre guias, o estado é excluído (o método dispose () é chamado); quando retornado, é criado novamente (o método initState ()). Isso é razoável, pois o armazenamento do estado de widgets não exibidos consumirá recursos do sistema. No caso em que o estado do widget deve sobreviver à sua ocultação completa, várias abordagens são possíveis:


Primeiro, você pode usar objetos separados (ViewModel) para armazenar o estado. O dardo no nível do idioma suporta construtores de fábrica que podem ser usados ​​para criar fábricas e singletones que armazenam os dados necessários.


Eu gosto mais dessa abordagem, porque Permite isolar a lógica de negócios da interface do usuário. Isso é especialmente verdade devido ao fato de o Flutter Release Preview 2 ter adicionado a capacidade de criar interfaces perfeitas para pixels para iOS, mas é necessário fazer isso, é claro, nos widgets correspondentes.


Em segundo lugar, é possível usar a abordagem de aumento de estado, familiar aos programadores do React, quando os dados são armazenados em componentes localizados a montante. Como o Flutter redesenha a interface apenas quando o método setState () é chamado, esses dados podem ser alterados e usados ​​sem renderização. Essa abordagem é um pouco mais complexa e aumenta a conectividade dos widgets na estrutura, mas permite especificar o nível de armazenamento de dados no sentido do ponto.


Por fim, existem bibliotecas de armazenamento de estado como flutter_redux .


Para simplificar, usamos a primeira abordagem. Vamos criar uma classe ListData separada, singleton, que armazena os valores para nossa lista. Ao exibir, usaremos esta classe.


Aplicativo de recuperação de dados com guias
 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } } 

Exemplo completo


Resultado


Salvando uma posição de rolagem


Se você rolar a lista do exemplo anterior e alternar entre as guias, é fácil perceber que a posição de rolagem não é salva. Isso é lógico, pois não é armazenado em nossa classe ListData e o próprio estado do widget não sobrevive à alternância entre guias. Implementamos o armazenamento do estado de rolagem manualmente, mas, por diversão, não o adicionaremos a uma classe separada e não ao ListData, mas a um estado de nível superior para mostrar como trabalhar com ele.


Preste atenção aos widgets ScrollController e NotificationListener (assim como o DefaultTabController usado anteriormente). O conceito de widgets que não possuem exibição própria deve ser familiar para os desenvolvedores que trabalham com o React / Redux - os componentes do contêiner são usados ​​ativamente neste pacote. No Flutter, widgets que não são de exibição são comumente usados ​​para adicionar funcionalidade a widgets filhos. Isso permite que você deixe os widgets visuais leves e não processe eventos do sistema onde eles não são necessários.


O código é baseado na solução proposta por Marcin Szałek no Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ). O plano é o seguinte:


  1. Adicione um ScrollController à lista para trabalhar com a posição de rolagem.
  2. Adicione NotificationListener à lista para passar o estado de rolagem.
  3. Salvamos a posição de rolagem em _MyHomePageState (que fica um nível acima das guias) e a associamos à rolagem da lista.

Aplicativo com salvando a posição de rolagem
 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } } 

Exemplo completo


Resultado


Experimentando o desligamento do aplicativo


Salvar informações para a duração do aplicativo é bom, mas geralmente você deseja salvá-las entre as sessões, especialmente considerando o hábito dos sistemas operacionais de fechar aplicativos em segundo plano quando não há memória suficiente. As principais opções para armazenamento persistente de dados no Flutter são:


  1. As preferências compartilhadas ( https://pub.dartlang.org/packages/shared_preferences ) são um invólucro em torno de NSUserDefaults (no iOS) e SharedPreferences (no Android) e permitem armazenar um pequeno número de pares de valores-chave. Ótimo para armazenar configurações.
  2. O sqflite ( https://pub.dartlang.org/packages/sqflite ) é um plug-in para trabalhar com o SQLite (com algumas limitações). Suporta consultas de baixo nível e auxiliares. Além disso, por analogia com o Room, ele permite trabalhar com versões de esquema de banco de dados e definir o código para atualizar o esquema ao atualizar o aplicativo.
  3. O Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) faz parte da família oficial de plug-ins do FireBase.

Para demonstrar, salvaremos o estado da rolagem nas Preferências compartilhadas. Para fazer isso, adicione a restauração da posição de rolagem ao inicializar o estado _MyHomePageState e salve ao rolar.


Aqui, precisamos nos concentrar no modelo assíncrono Flutter / Dart, pois todos os serviços externos funcionam em chamadas assíncronas. O princípio de operação desse modelo é semelhante ao node.js - há um encadeamento principal de execução (encadeamento), que é interrompido por chamadas assíncronas. Em cada interrupção subseqüente (e a interface do usuário as faz constantemente), os resultados das operações assíncronas concluídas são processados ​​e, ao mesmo tempo, é possível executar cálculos pesados ​​nos encadeamentos em segundo plano (por meio da função de computação).


Portanto, a gravação e a leitura em SharedPreferences são feitas de forma assíncrona (embora a biblioteca permita leitura síncrona do cache). Para começar, trataremos da leitura. A abordagem padrão para recuperação de dados assíncrona se parece com isso - inicie o processo assíncrono e, após a conclusão, execute SetState, escrevendo os valores recebidos. Como resultado, a interface do usuário será atualizada usando os dados recebidos. No entanto, neste caso, não estamos trabalhando com dados, mas com a posição de rolagem. Não precisamos atualizar a interface, basta chamar o método jumpTo no ScrollController. O problema é que o resultado do processamento de uma solicitação assíncrona pode retornar a qualquer momento e não será necessariamente o que e para onde rolar. Para garantir a execução de uma operação em uma interface totalmente inicializada, precisamos ... ainda rolar dentro de setState.


Temos algo parecido com este código:


Configuração de estado
  @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); } 

Com o registro, tudo é mais interessante. O fato é que, no processo de rolagem, os eventos que relatam isso acontecem constantemente. Iniciar uma gravação assíncrona toda vez que o valor for alterado pode causar erros no aplicativo. Precisamos processar apenas o último evento da cadeia. Em termos de programação reativa, isso se chama debounce e vamos usá-lo. O Dart suporta os principais recursos da programação reativa por meio de fluxos de dados, portanto, precisaremos criar um fluxo a partir de atualizações de posição de rolagem e se inscrever nele, convertendo-o usando o Debounce. Para converter, precisamos da biblioteca stream_transform . Como uma abordagem alternativa, você pode usar o RxDart e trabalhar em termos do ReactiveX.


Acontece o seguinte código:


Registro de status
  StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); } 

Exemplo completo

Source: https://habr.com/ru/post/pt424765/


All Articles