Работа с Data Assets | Flutter

Привет, если вы на пути изучения Flutter/Dart или вам просто интересно почитать про путь изучения, подписывайтесь на мой канал в telegram, буду рад вас видеть! А сегодня поговорим про работу с Data Assets во Flutter!

В этой статье вы узнаете, как Flutter и Dart объединяются для управления ресурсами данных. В ходе этой статье вы увидите, как расположение данных влияет на подход к доступу к необходимой информации. На протяжении всей этой главы следуйте пунктам, чтобы изучить различные методы embedded, asset-based и remote данных в ваше приложение.

Вы узнаете, как:
• Реорганизовать данные для приложения
• Использовать информацию из локального файла JSON
• Работать с данными, расположенными в папке assets
• Обрабатывать remote данных с помощью Future
• Автоматизировать JSON в классе Dart

В этой статье мы будем использовать два примера JSON-файлов, чтобы продемонстрировать, как загружать внешние данные. Формат каждого файла показан в следующих примерах:

Пример 1: Однослойный JSON

{  "1": "January",  "2": "February",  "3": "March",  "4": "April" }

Пример 2: Многопользовательский JSON

{  "data": [  {  "title": "January"  },  {  "title": "February"  },  {  "title": "March"  }  ] }

Стратегический доступ к данным

Проблема

У вас есть доступные данные, но вы не уверены, где они должны быть расположены.

Решение

Подумайте, как данные будут использоваться в приложении, и примените соответствующую стратегию для эффективного доступа к информации. При выборе правильного местоположения для ваших данных учитывайте: • Объем данных, которые будут храниться • Частота обновлений

Обсуждение

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

На рисунке 13-1 показаны три распространенных схемы доступа к данным, основанные на том, где будут располагаться данные приложения. В процессе разработки обработка ресурсов данных будет иметь важное значение для создания более продвинутых приложений.

Работа с Data Assets | Flutter

Рисунок 13-1. Конвейер данных

Встроенные/локальные данные представляют данные, размещенные совместно с приложением. Помните, что если вы объединяете данные с приложением, вам необходимо включить отдельный метод обновления приложения для замены требуемого набора данных. Используйте встроенные данные там, где у вас есть большой набор данных или данные, которые не нуждаются в частом обновлении.

Данные папки Assets часто используются для небольших наборов данных, которые обеспечивают начальную функциональность, не требуя от пользователя выполнения начальной загрузки. Загрузка данных из папки assets – хороший подход, когда данные не требуют частых обновлений и, следовательно, могут быть включены в приложение. Хранение данных локально не всегда выгодно. Ключевой проблемой является обеспечение того, чтобы хранящиеся локальные данные не устаревали. Один из способов минимизировать устаревание данных – это переопределить данные локальной папки assets, например, применить дату или цикл обновления к данным, хранящимся в папке assets. Если вы этого не сделаете, не забудьте предусмотреть способ включения новых данных при обновлении вашего приложения. Суть в том, что локальное хранение данных в папке assets может быть быстрым способом организовать данные таким образом, чтобы они были доступны. Типичным вариантом использования этой функции является хранение данных, которые не зависят от времени.

Удаленный доступ к данным – это стратегия, применяемая многими приложениями. Удаленные данные обеспечивают большую гибкость, поскольку они изолированы от приложения, поэтому их можно изменять независимо. Однако избегайте связывания загрузки значительных наборов данных с удаленными данными, поскольку это может привести к ошибкам, в результате чего приложение окажется в нерабочем состоянии. Кроме того, для доступа к объектам данных будет использоваться асинхронный код, используемый для обработки неопределенной продолжительности вызова функции. При загрузке наборов данных всегда рассматривайте возможность использования асинхронного, а не синхронного кода, чтобы обеспечить лучший пользовательский опыт.

Как только у вас появятся доступные данные, еще одним соображением будет частота, с которой их необходимо будет обновлять. Опять же, учитывайте размер набора данных. Большой набор данных не поддается частым обновлениям по воздуху. Если в вашей ситуации требуется большой набор данных, вам нужно будет разработать соответствующий механизм для обработки обновлений данных в соответствии с вашими требованиями.

Рефакторинг данных

Проблема

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

Решение

Чтобы повысить читаемость кода, создайте независимый класс данных, ответственный за хранение ваших данных. Выделение данных в отдельный класс упрощает обработку данных. Мы используем отдельный класс MyData, чтобы обеспечить два конкретных преимущества изоляции и абстракции. Два варианта использования более подробно обсуждаются ниже.

Вот пример, в котором обрабатываемые данные находятся в классе с именем MyData:

import 'package:flutter/material.dart';  void main() {   runApp(MyApp()); }  class MyData {   final List<String> items = [     'January',     'February',     'March',     'April',     'May',     'June',     'July',     'August',     'September',     'October',     'November',     'December'   ];    MyData(); }  class MyApp extends StatelessWidget {   MyApp({Key? key}) : super(key: key);   final MyData data = MyData();    @override   Widget build(BuildContext context) {     const title = 'MyAwesomeApp';     List items = data.items;     return MaterialApp(       title: title,       home: Scaffold(         appBar: AppBar(           title: const Text(title),         ),         body: ListView.builder(           itemCount: items.length,           itemBuilder: (context, index) {             return ListTile(               title: Text(items[index]),             );           },         ),       ),     );   } } 

Обсуждение

Определение класса MyData отвечает за объявление данных, к которым будет осуществляться доступ в приложении. Переменная data основана на классе MyData. Доступ к базовым данным осуществляется путем объявления переменной data типа MyData, которая отвечает за доступ к информации, связанной с классом MyData.

В примере мы создаем новый класс с именем MyData. Как указывалось ранее, мы делаем это, чтобы воспользоваться преимуществами изоляции и абстракции данных.

Изоляция данных улучшает общую читаемость вашего кода. Создав класс, предназначенный для хранения нашей информации, мы можем легче понять код. Класс MyData используется для хранения списка строк. Поскольку определение данных и их использование разделены, нашу программу легче читать, поскольку мы можем четко видеть ответственность каждого раздела кода.

Абстракция данных предоставляет возможность упростить соглашение между использованием и реализацией. В примере, чтобы использовать класс data, мы просто объявляем ссылку на него. Когда мы создаем переменную экземпляра, которая представляет объект, содержащий структуру данных, определенную в классе MyData. На данный момент мы мало что знаем о реализации, кроме того, что она предоставляет необходимые данные. По мере усложнения приложения полезно иметь возможность абстрагировать определения. В предыдущем примере мы переместили наши данные в определенный класс и можем расширить этот класс данных для выполнения дополнительных действий по мере необходимости.

Генерация Dart классов из JSON

Проблема

Вы хотите создавать пользовательские классы Dart без необходимости писать аннотации или изучать сериализацию JSON.

Решение

Используйте одну из многочисленных утилит с открытым исходным кодом, таких как онлайн-конвертер JSON в Dart или JSON to Dart. Утилита определит классы, необходимые для переноса ваших данных в формате JSON.

Вот несколько примеров сгенерированных классов, демонстрирующих выходные данные при использовании набора данных Sample 2 в формате JSON:

class Month {   List<Data>? data;   Month({this.data});   Month.fromJson(Map<String, dynamic> json) {     if (json['data'] != null) {       data = <Data>[];       json['data'].forEach((v) {         data!.add(new Data.fromJson(v));       });     }   }   Map<String, dynamic> toJson() {     final Map<String, dynamic> data = new Map<String, dynamic>();     if (this.data != null) {       data['data'] = this.data!.map((v) => v.toJson()).toList();     }     return data;   } }  class Data {   String? title;   Data({this.title});   Data.fromJson(Map<String, dynamic> json) {     title = json['title'];   }   Map<String, dynamic> toJson() {     final Map<String, dynamic> data = new Map<String, dynamic>();     data['title'] = this.title;     return data;   } }

Обсуждение

Как показано на рисунке 13-2, мы используем внешний инструмент для создания серии классов Dart для использования файла JSON. Примеры классов были сгенерированы сайтом JSON to Dart и основаны на наборе данных JSON образца 2. Добавьте название класса Month, чтобы сгенерировать пример, аналогичный предыдущему.

Работа с Data Assets | Flutter

Рисунок 13-2. Пример преобразования JSON в класс

На первый взгляд сгенерированный код может показаться довольно пугающим; однако на самом деле нам просто нужно обновить три настройки, чтобы они соответствовали требованиям нашего приложения и последним рекомендациям Dart:

  • Dart, скорее всего, укажет вам на наличие в вашем коде “ненужного нового ключевого слова”. Dart сделал использование ключевого слова new необязательным, поэтому вы можете безопасно удалить его, если компилятор жалуется на его присутствие в вашем коде:

      Month.fromJson(Map<String, dynamic> json) {     if (json['data'] != null) {       data = <Data>[];       json['data'].forEach((v) {         data!.add(new Data.fromJson(v));       });     }   }
  • Вы можете наткнуться на восхитительное сообщение “Используйте литералы коллекции, когда это возможно”. В вашем коде вам нужно обновить Map<String, dynamic>() до <String, dynamic>{}, чтобы удалить это сообщение и еще раз порадовать компилятор:

    Map<String, dynamic> toJson() {     final Map<String, dynamic> data = Map<String, dynamic>();     if (this.data != null) {       data['data'] = this.data!.map((v) => v.toJson()).toList();     }     return data;   }
  • Последнее сообщение относится к использованию ключевого слова this. Если вы видите “Не обращаться к элементам с помощью ”this”, если не избегать shadowing”, то это говорит вам удалить эту ссылку из переменной, поскольку она не требуется:

    Map<String, dynamic> toJson() {    final Map<String, dynamic> data = <String, dynamic>{};    data['title'] = this.title;    return data; }

Как только вы внесете эти изменения, ваш класс Dart будет готов к использованию в вашем приложении Flutter. Хотя переход из JSON можно выполнить вручную, более эффективно и менее подвержено ошибкам использовать утилиту для выполнения этой задачи. Типичный вариант использования такого подхода – это когда в наборе данных не потребуется много изменений.

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

Асинхронное использование локальных данных JSON

Проблема

Вам нужен способ использовать строку, содержащую информацию в формате JSON.

Решение

Используйте встроенные возможности обработки JSON в Dart для анализа информации, отформатированной в формате JSON. Без использования пакета обработка Dart может быть сложной.

Вот пример, в котором используется набор встроенных данных JSON из примера 2. Данные JSON загружаются асинхронно, присваиваются переменной и преобразуются в строку:

import 'package:flutter/material.dart'; import 'dart:convert';  void main() {   runApp(MyApp()); }  // Example 2: JSON Dataset class MyData {   final String items = '{"data": [ 	 { "title": "January" }, 	 { "title": "February" }, 	 { "title": "March" },  ] }'; } class DataSeries {   final List<DataItem> dataModel;   DataSeries({required this.dataModel});   factory DataSeries.fromJson(Map<String, dynamic> json) {     var list = json['data'] as List;     List<DataItem> dataList =         list.map((dataModel) => DataItem.fromJson(dataModel)).toList();     return DataSeries(dataModel: dataList);   } }  class DataItem {   final String title;   DataItem({required this.title});   factory DataItem.fromJson(Map<String, dynamic> json) {     return DataItem(title: json['title']);   } }  class MyApp extends StatelessWidget {   // This widget is the root of your application.   @override   Widget build(BuildContext context) {     return MaterialApp(       title: 'Local JSON Future Demo',       theme: ThemeData(         primarySwatch: Colors.blue,       ),       home: const MyHomePage(         title: 'Local JSON Future Demo',         key: null,       ),     );   } }  class MyHomePage extends StatefulWidget {   const MyHomePage({Key? key, required this.title}) : super(key: key);   final String title;   @override   State<MyHomePage> createState() => _MyHomePageState(); }  Future<String> _loadLocalData() async {   final MyData data = MyData();    return data.items; }  class _MyHomePageState extends State<MyHomePage> {   Future<DataSeries> fetchData() async {     String jsonString = await _loadLocalData();     final jsonResponse = json.decode(jsonString);     DataSeries dataSeries = DataSeries.fromJson(jsonResponse);     print(dataSeries.dataModel[0].title);     return dataSeries;   }    late Future<DataSeries> dataSeries;   @override   void initState() {     super.initState();     dataSeries = fetchData();   }    @override   Widget build(BuildContext context) {     return Scaffold(       appBar: AppBar(         title: Text(widget.title),       ),       body: FutureBuilder<DataSeries>(           future: dataSeries,           builder: (context, snapshot) {             if (snapshot.hasData) {               return ListView.builder(                 itemCount: snapshot.data!.dataModel.length,                 itemBuilder: (BuildContext context, int index) {                   return ListTile(                     title: Text(snapshot.data!.dataModel[index].title),                   );                 },               );             } else if (snapshot.hasError) {               return Text("Error: ${snapshot.error}");             }             return const CircularProgressIndicator();           }),     );   } } 

Обсуждение

В примере код вводит использование initState и FutureBuilder. initState – это метод, используемый для выполнения задач на этапе инициализации класса. Используйте этот метод для загрузки ресурсов перед их использованием в приложении, как показано на рисунке 13-3. По завершении этого этапа структура данных должна быть загружена локальными встроенными данными.

Работа с Data Assets | Flutter

Рисунок 13-3. Пример загрузки встроенных данных

Вызов initState является одноразовым вызовом, связанным с инициализацией _MyHome PageState. Во время этого метода выполните вызов fetchData, который является универсальным асинхронным методом, используемым для загрузки и обработки данных, готовых к использованию в приложении. Мы используем этот метод для ссылки на загрузку данных, а также на обработку информации, возвращаемой в данном случае, превращая объект JSON в строку, которая будет отображаться в ListView.

Чтобы перенести наши входные данные из набора данных JSON, мы используем импортированный пакет dart:convert. Пакет включает в себя json.decode, который принимает строковый ввод и возвращает JSON.

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

Чтобы загрузить наши данные, мы используем закрытый метод с именем _loadLocalData.

Этот метод также асинхронен и помечен как private и просто возвращает значение нашей структуры данных, содержащей наш встроенный JSON. Метод помечен как private, чтобы указать, что он должен использоваться внутри класса и не предназначен для публичного доступа. Именно этот метод используется для определения загружаемых данных.

FutureBuilder – это еще один метод построения, который поможет вам эффективно обрабатывать данные. Если асинхронный процесс не завершен, FutureBuilder достаточно умен, чтобы подождать. FutureBuilder будет ждать доступности наших данных; однако в течение этого периода он будет показывать индикатор выполнения, указывающий на выполнение фонового действия. Как только данные станут доступны, FutureBuilder отобразит данные, полученные с помощью метода fetchData. На этом этапе процесс загрузки данных завершится, и FutureBuilder отобразит информацию на основе связанного дерева виджетов, как показано на рисунке 13-4.

В нашем приложении FutureBuilder использует набор данных, помеченный как Future. Будущее – это асинхронный вызов, который улучшает работу пользователя при доступе к данным за счет разгрузки основного потока. Рекомендации Dart указывают, что мы всегда должны использовать асинхронный вызов для длительных действий, таких как чтение файлов, запросы к базе данных и получение результатов веб-страницы.

Работа с Data Assets | Flutter

Рисунок 13-4. Пример рендеринга встроенных данных

Иллюстрации на рисунке 13-4 представляют очень распространенный шаблон загрузки данных, который вы можете использовать с большими наборами данных. Запомните последовательность построения диаграммы и какие аспекты необходимо определить как асинхронные.

Использование набора данных JSON из папки Assets

Проблема

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

Решение

Используйте папку assets для размещения информации, которая будет загружена в ваше приложение. Папка assets представляет собой хранилище общего назначения для вашего приложения.

Обновите pubspec.yaml, чтобы он ссылался на созданный каталог assets/example2.json:

flutter:  uses-material-design: true  assets:  - assets/example2.json

Вот пример того, как получить доступ к набору данных JSON, расположенному в папке assets вашего приложения:

import 'package:flutter/material.dart'; import 'dart:convert';  void main() {   runApp(MyApp()); }  class DataSeries {   final List<DataItem> dataModel;   DataSeries({required this.dataModel});   factory DataSeries.fromJson(Map<String, dynamic> json) {     var list = json['data'] as List;     List<DataItem> dataList =         list.map((dataModel) => DataItem.fromJson(dataModel)).toList();     return DataSeries(dataModel: dataList);   } }  class DataItem {   final String title;   DataItem({required this.title});   factory DataItem.fromJson(Map<String, dynamic> json) {     return DataItem(title: json['title']);   } }  class MyApp extends StatelessWidget {   // This widget is the root of your application.   @override   Widget build(BuildContext context) {     return MaterialApp(       title: 'JSON Future Demo',       theme: ThemeData(         primarySwatch: Colors.blue,       ),       home: const MyHomePage(         title: 'JSON Future Demo',         key: null,       ),     );   } }  class MyHomePage extends StatefulWidget {   const MyHomePage({Key? key, required this.title}) : super(key: key);   final String title;   @override   State<MyHomePage> createState() => _MyHomePageState(); }  Future<String> _loadAssetData() async {   final AssetBundle rootBundle = _initRootBundle();   return await rootBundle.loadString('assets/example2.json'); }  class _MyHomePageState extends State<MyHomePage> {   Future<DataSeries> fetchData() async {     String jsonString = await _loadAssetData();     final jsonResponse = json.decode(jsonString);     DataSeries dataSeries = DataSeries.fromJson(jsonResponse);     print(dataSeries.dataModel[0].title);     return dataSeries;   }    late Future<DataSeries> dataSeries;   @override   void initState() {     super.initState();     dataSeries = fetchData();   }    @override   Widget build(BuildContext context) {     return Scaffold(       appBar: AppBar(         title: Text(widget.title),       ),       body: FutureBuilder<DataSeries>(           future: dataSeries,           builder: (context, snapshot) {             if (snapshot.hasData) {               return ListView.builder(                 itemCount: snapshot.data!.dataModel.length,                 itemBuilder: (BuildContext context, int index) {                   return ListTile(                     title: Text(snapshot.data!.dataModel[index].title),                   );                 },               );             } else if (snapshot.hasError) {               return Text("Error: ${snapshot.error}");             }             return const CircularProgressIndicator();           }),     );   } } 

Обсуждение

В примере код следует шаблону, описанному в пункте 13.3. Заметное отличие заключается в том, что вводится метод _loadAssetData, который используется для доступа к данным, размещенным в папке application assets.

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

На рисунке 13-5 показано, как работает процесс загрузки ресурсов в Flutter. Чтобы загрузить данные из папки assets, вам необходимо обратиться к корневому пакету. rootBundle содержит ресурсы, которые были включены в приложение при сборке. Любой актив, добавленный в подраздел assets pubspec.yaml, будет доступен через это свойство.

Работа с Data Assets | Flutter

Рисунок 13-5. Пример загрузки данных Assets

Ресурсы для приложения Flutter хранятся в AssetBundle. Ресурсы, добавленные в папку assets в Flutter pubspec.yaml, доступны через это свойство. Чтобы получить доступ к информации в вашем приложении, создайте переменную rootBundle типа AssetBundle. Из этой переменной rootBundle у вас будет доступ к активам, объявленным в вашем приложении:

final AssetBundle rootBundle = _initRootBundle(); return await rootBundle.loadString('assets/example2.json');

Если вы решите использовать альтернативную структуру в своей папке assets, обязательно укажите полный путь к объекту, который вы хотите загрузить.

Как только папка и данные станут доступны, обновите pubspec.yaml. Добавление информации в папку (локальных) ресурсов – это хороший способ сохранить данные приложения без необходимости обращаться к внешнему решению, такому как база данных или API.

Доступ к удаленным данным JSON

Проблема

Вы хотите получать информацию из внешнего удаленного API.

Решение

Используйте HTTP-пакет Dart для доступа к удаленным источникам данных. Пакет позволяет вам получать доступ к информации, размещенной на внешнем сервере, и извлекать ее, а также использовать ее через ваше приложение.

Вот пример, который добавляет пакеты JSON и async, используемые для доступа к удаленным данным:

import 'package:http/http.dart' as http; import 'dart:async' show Future; import 'dart:convert';  Future<String> _loadRemoteData() async {   final response = await (http.get(Uri.parse('https://oreil.ly/ndCPN')));   if (response.statusCode == 200) {     print('response statusCode is 200');     return response.body;   } else {     print('Http Error: ${response.statusCode}!');     throw Exception('Invalid data source.');   } }

Обсуждение

В примере код следует шаблону, описанному в пункте 13.3. Заметное отличие заключается в том, что для доступа к данным из Интернета используется асинхронный метод _loadRemoteData, показанный на рисунке 13-6.

Работа с Data Assets | Flutter

Рисунок 13-6. Пример удаленной загрузки данных

Пакет dart:async используется для выполнения асинхронной операции для сетевых вызовов. Кроме того, требуется пакет dart:convert, поскольку используемый API возвращает JSON. Убедитесь, что эти пакеты были добавлены в ваше приложение, прежде чем пытаться их использовать.

Использование HTTP-пакета в Dart значительно упрощает процесс извлечения внешних данных. Аналогично примерам для локальных и встроенных данных, параметр remote объявляет параметр _loadRemoteData. В этом методе мы выполняем удаленный вызов Uniform Resource Identifier (URI), содержащего наш загружаемый файл. Вызов вернет ответ, содержащий код состояния, который необходимо проверить, чтобы убедиться, успешно ли мы выполнили наш запрос.

При работе с удаленными данными обращайте внимание на возвращаемые коды ответа. В общем случае HTTP 200 означает успешный ответ, HTTP 400 указывает на ошибку в запросе, а 500 указывает на ошибку сервера. Наличие удаленной конечной точки, соответствующей общепринятым кодам возврата HTTP, может значительно сэкономить усилия по отладке.

Если ответ действителен (т.е. HTTP 200), мы можем вернуть тело ответа, содержащее запрошенные данные JSON. Запрос использует будущее, чтобы сообщить нашему приложению, что мы используем асинхронные вызовы. Используйте Future при загрузке внешних данных, особенно если вы не знаете, насколько велики данные, которые необходимо извлечь. Перевод удаленного доступа к данным в асинхронный вызов повысит общую производительность вашего приложения, не блокируя уровень представления.

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