Управление Состоянием В Приложениях Flutter



Управление состоянием в приложениях Flutter

Общие принципы Flutter — это реактивный фреймворк, и для разработчика, специализирующегося на нативной разработке, его философия может быть необычной.

Итак, начнем с небольшого обзора.

Пользовательский интерфейс во Flutter, как и в большинстве современных фреймворков, состоит из дерева компонентов (виджетов).

Когда какой-либо компонент изменяется, он и все его дочерние компоненты перерисовываются (с внутренней оптимизацией, которая обсуждается ниже).

При глобальном изменении отображения (например, повороте экрана) перерисовывается все дерево виджетов.

Такой подход может показаться неэффективным, но на самом деле он дает программисту контроль над скоростью работы.

Если обновить интерфейс на верхнем уровне без надобности, все будет работать медленно, но при правильном расположении виджетов приложения Flutter могут работать очень быстро.

Во Flutter есть два типа виджетов — без сохранения состояния и с сохранением состояния.

Первые (аналог Pure Components в React) не имеют состояния и полностью описываются своими параметрами.

Если условия отображения (скажем, размер области, в которой должен отображаться виджет) и его параметры не меняются, система повторно использует ранее созданное визуальное представление виджета, поэтому использование виджетов без состояния хорошо влияет на производительность.

При этом каждый раз при перерисовке виджета формально создается новый объект и запускается конструктор.

Виджеты с сохранением состояния сохраняют некоторое состояние между рендерингами.

Для этого они описываются двумя классами.

Первый из классов, сам виджет, описывает объекты, которые создаются при каждом его рендеринге.

Второй класс описывает состояние виджета, и его объекты передаются создаваемым объектам виджета.

Изменение состояния виджетов с состоянием — основной источник перерисовки интерфейса.

Для этого вам необходимо изменить его свойства внутри вызова метода SetState. Таким образом, в отличие от многих других фреймворков, во Flutter нет неявного отслеживания состояния — любое изменение свойств виджета вне метода SetState не приводит к перерисовке интерфейса.

Теперь, когда мы рассмотрели основы, мы можем начать с простого приложения, использующего виджеты без сохранения состояния и с сохранением состояния: Базовое приложение

  
  
  
  
  
   

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)}',); }); } }

Полный пример Результат

Управление состоянием в приложениях Flutter

Если необходимы более прочные государства Вперед, продолжать.

Состояние виджетов с сохранением состояния сохраняется между перерисовками интерфейса, но только до тех пор, пока виджет необходим, т. е.

фактически находится на экране.

Проведем простой эксперимент — разместим наш список на вкладке: Приложение с вкладками

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, ), ); } }

Полный пример Результат

Управление состоянием в приложениях Flutter

При запуске видно, что при переключении между вкладками состояние удаляется (вызывается метод Dispose()); при возврате он создается заново (метод initState()).

Это разумно, поскольку сохранение состояния неотрисованных виджетов будет потреблять системные ресурсы.

В случае, когда состояние виджета должно пережить его полное сокрытие, возможны несколько подходов: Во-первых, вы можете использовать отдельные объекты (ViewModel) для хранения состояния.

Dart поддерживает конструкторы фабрик на уровне языка, которые можно использовать для создания фабрик и синглтонов, хранящих необходимые данные.

Мне этот подход нравится больше, потому что.

он позволяет изолировать бизнес-логику от пользовательского интерфейса.

Особенно это актуально в связи с тем, что во Flutter Release Preview 2 добавлена возможность создавать пиксельные интерфейсы для iOS, но делать это нужно, естественно, на соответствующих виджетах.

Во-вторых, вы можете использовать подход поднятия состояния, знакомый программистам React, где данные хранятся в компонентах выше по дереву.

Поскольку Flutter перерисовывает интерфейс только при вызове setState(), эти данные можно изменять и использовать без рендеринга.

Этот подход несколько сложнее и повышает связность виджетов в структуре, но позволяет указать уровень хранения данных.

Наконец, есть библиотеки хранения состояний, например flutter_redux .

Для простоты мы используем первый подход. Давайте создадим отдельный класс ListData, синглтон, в котором будут храниться значения для нашего списка.

При отображении мы будем использовать этот класс.

Приложение с вкладками и восстановлением данных

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]}',); } }

Полный пример Результат

Управление состоянием в приложениях Flutter

Сохранение позиции прокрутки Если прокрутить список из предыдущего примера вниз, а затем перемещаться между вкладками, легко заметить, что положение прокрутки не сохраняется.

Это логично, поскольку оно не хранится в нашем классе ListData, а собственное состояние виджета не сохраняется при переключении между вкладками.

Мы реализуем сохранение состояния прокрутки вручную, но ради интереса вынесем его не в отдельный класс или в ListData, а в состояние более высокого уровня, чтобы показать, как с ним работать.

Обратите внимание на виджеты ScrollController и NotificationListener (а также на ранее использованный DefaultTabController).

Концепция виджетов, не имеющих собственного дисплея, должна быть знакома разработчикам, работающим с React/Redux — в этой комбинации активно используются компоненты-контейнеры.

Во Flutter неотображаемые виджеты обычно используются для добавления функциональности дочерним виджетам.

Это позволяет сохранить легкость самих визуальных виджетов и не обрабатывать системные события там, где они не нужны.

Код основан на решении, предложенном Марцином Салеком на Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ).

План таков:

  1. Мы добавляем в список ScrollController для работы с позицией прокрутки.

  2. Мы добавляем NotificationListener в список, чтобы передать состояние прокрутки.

  3. Мы сохраняем позицию прокрутки в _MyHomePageState (который находится на один уровень выше вкладок) и связываем ее с прокруткой списка.

Приложение с сохранением положения прокрутки

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); } }, ); } }

Полный пример Результат

Управление состоянием в приложениях Flutter

Завершение работы приложения Сохранение информации во время работы приложения — это хорошо, но часто хочется сохранить ее между сеансами, особенно учитывая привычку операционных систем закрывать фоновые приложения, когда не хватает памяти.

Основными вариантами постоянного хранения данных во Flutter являются:

  1. Общие предпочтения ( https://pub.dartlang.org/packages/shared_preferences ) представляет собой оболочку NSUserDefaults (в iOS) и SharedPreferences (в Android) и позволяет хранить небольшое количество пар ключ-значение.

    Отлично подходит для хранения настроек.

  2. скфлайт ( https://pub.dartlang.org/packages/sqflite ) — плагин для работы с SQLite (с некоторыми ограничениями).

    Поддерживает как низкоуровневые запросы, так и помощники.

    Кроме того, как и Room, он позволяет работать с версиями схемы базы данных и задавать код для обновления схемы при обновлении приложения.

  3. Облачный Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) входит в семейство официальных плагинов для работы с FireBase.
Для демонстрации давайте сохраним состояние прокрутки в общих настройках.

Для этого добавим восстановление позиции прокрутки при инициализации состояния _MyHomePageState и сохранение его при прокрутке.

Здесь нужно немного остановиться на асинхронной модели Flutter/Dart, поскольку все внешние сервисы работают на асинхронных вызовах.

Принцип работы этой модели аналогичен node.js — есть один основной поток выполнения (поток), который прерывается асинхронными вызовами.

При каждом следующем прерывании (а UI делает их постоянно) обрабатываются результаты завершенных асинхронных операций.

В то же время можно выполнять сложные вычисления в фоновых потоках (с помощью функции вычисления).

Итак, запись и чтение в SharedPreferences происходит асинхронно (хотя библиотека допускает синхронное чтение из кеша).

Во-первых, давайте посмотрим на чтение.

Стандартный подход к асинхронному получению данных выглядит так: запускаем асинхронный процесс, а по его завершению выполняем SetState, записывая полученные значения.

В результате пользовательский интерфейс будет обновлен с использованием полученных данных.

Однако в данном случае мы работаем не с данными, а с позицией прокрутки.

Нам не нужно обновлять интерфейс, нам просто нужно вызвать метод jumpTo в ScrollController. Проблема в том, что результат обработки асинхронного запроса может быть возвращен в любой момент и не обязательно будет что и где прокручивать.

Чтобы быть уверенным, что операция будет выполнена на полностью инициализированном интерфейсе, нам нужно… по-прежнему прокручивать внутри setState. Мы получаем что-то вроде этого кода: Установка состояния

@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); }); }

С записью все становится интереснее.

Дело в том, что в процессе прокрутки постоянно приходят события, сообщающие об этом.

Запуск асинхронной записи при изменении значения может привести к ошибкам приложения.

Нам нужно обработать только последнее событие в цепочке.

В терминах реактивного программирования это называется устранением дребезга, и мы будем его использовать.

Dart поддерживает основные возможности реактивного программирования через потоки, поэтому нам нужно будет создать поток обновлений положения прокрутки и подписаться на него, преобразовав его с помощью Debounce. Для конвертации нам понадобится библиотекаstream_transform .

В качестве альтернативного подхода вы можете использовать RxDart и работать с ReactiveX. В результате получается следующий код: Статус записи

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); }

Полный пример Теги: #iOS #Android #разработка iOS #разработка Android #разработка мобильных приложений #mobile #разработка мобильных устройств #flutter

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.