2.16. Основы работы с сетью и данными

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

А конкретнее:

  • Что такое HTTP и из чего состоит HTTP-запрос.
  • Как работать с форматом JSON в Dart.
  • Обмен данными в реальном времени с помощью веб-сокетов.
  • Простое хранение данных.

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

Обмен данными с помощью HTTP

Большинство знакомых нам мобильных приложений постоянно взаимодействует с глобальной сетью с помощью протокола HTTP.

HTTP (англ. HyperText Transfer Protocol, протокол передачи гипертекста) — протокол обмена данными между клиентом и сервером, благодаря которому разработчики разных сервисов взаимодействуют между собой, отправляют информацию в едином виде.

Изначально задуманный для передачи HTML-документов, протокол HTTP получил широкое применение для обмена произвольными данными, такими как текст, картинки, аудио, видео и прочее.

Протокол работает по принципу «запрос — ответ». Клиент отправляет HTTP-запрос (HTTP Request) серверу, тот его обрабатывает и отправляет клиенту HTTP-ответ (HTTP Response). Давайте рассмотрим структуру запросов и ответов подробнее.

HTTP-запросы

Ниже — пример HTTP-запроса:

1GET /handbook/ HTTP/1.1
2Host: academy.yandex.ru
3User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 
4Accept: text/html,application/xhtml+xml,application/xml
5Accept-Language: en-US,en;q=0.9,ru;q=0.8
6Accept-Encoding: gzip, deflate, br
7Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

Давайте подробно разберём его структуру.

  1. Адрес (URL) — получение доступа к ресурсам осуществляется с помощью указателя URL (Uniform Resource Locator). URL представляет собой строку, которая позволяет указать запрашиваемый ресурс и ещё ряд параметров. Пример URL-адреса: https://education.yandex.ru/handbook
  2. Метод (Method) — позволяет указать конкретное действие, которое нужно выполнить серверу. Например, метод GET указывает, что нужно получить некоторые данные с сервера, а метод POST используется для отправки данных на сервер.
  3. Заголовки (Headers) — характеризуют тело запроса, параметры передачи и прочие сведения. Имеют стандартную структуру для HTTP-заголовка «Название:Значение», с двоеточием в качестве разделителя. Например, заголовок Authorization, который в своём значении хранит токен авторизации, используется в качестве метода идентификации клиента на сервере.
  4. Тело запроса (Body) — используется для передачи объекта, связанного с запросом. Не каждому HTTP-методу необходимо тело. Например, для метода GET оно обычно не требуется.

HTTP-ответы

А вот пример HTTP-ответа:

1HTTP/1.1 200 OK
2Date: Tue, 02 May 2023 22:42:26 GMT
3Content-Type: text/html; charset=utf-8
4Content-Encoding: gzip
5WWW-Authenticate: Basic
6
7<!DOCTYPE html><html>...</html>

Он обладает чуть иной структурой:

  1. Код состояния (Status code) — коды состояния HTTP используются, чтобы сообщить клиенту статус его запроса. HTTP-сервер может вернуть код, принадлежащий одной из пяти категорий кодов состояния (1xx, 2xx, 3xx, 4xx, 5xx).

Например, самые распространенные коды ответов:

  • 200 OK — запрос обработан успешно;
  • 400 Bad Request — в запросе ошибка;
  • 500 Internal Error — сервер не может обработать запрос.
  1. Заголовки (Headers) — используются, чтобы уточнить ответ, и никак не влияют на содержимое тела ответа. Например, заголовок WWW-Authenticate уведомляет клиента о типе аутентификации, который необходим для доступа к запрашиваемому ресурсу.

  2. Тело ответа (Body) — используется для передачи объекта, связанного с ответом. Несмотря на то, что у большинства ответов тело присутствует, оно также не является обязательным. Например, у кодов 201 Created или 204 No Content тело отсутствует, так как достаточную информацию для ответа на запрос они передают в заголовке.

Благодаря протоколу HTTP все клиенты и серверы в интернете могут расшифровать присланные данные и отправлять их в понятном другим клиентам и серверам виде. Однако каждый сервер имеет свой API, действующий над протоколом HTTP.

API (англ. Application Programming Interface, программный интерфейс приложений) — это набор инструкций, который позволяет разным приложениям общаться между собой. Правила API описывают возможные запросы к серверу и ответы сервера на эти запросы.

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

1// Метод GET для получения данных о пользователе
2https://mysite.ru/user/data
3
4// Метод POST для отправки данных о пользователе
5https://mysite.ru/user/update

Библиотеки для работы с HTTP

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

  • dart:io;
  • http.

dart:io (HttpClient)

Библиотека dart:io предоставляет нам классы для работы с файлами, директориями, ссылками, сокетами и другим функционалом. Например, для работы с HTTP библиотека dart:io предоставляет класс HttpClient.

👉 Важно: класс HttpClient не подходит для разработки веб-приложений. При разработке под веб используйте класс HttpRequest из библиотеки dart:html.

Далее мы:

  1. Создадим URL-адрес;
  2. Отправим GET-запрос и получим ответ.
  3. Отправим POST-запрос и получим ответ.

Давайте начнём.

Создадим URL-адрес

Чтобы сделать HTTP-запрос, вам нужен URL-адрес, который идентифицирует запрашиваемый ресурс или конечную точку, к которой осуществляется доступ. В Dart URL-адреса представлены через объекты Uri.

Существует множество способов создания Uri, но самым распространённым является Uri.parse (используйте Uri.tryParse, чтобы не получить FormatException, если указали невалидный адрес).

1Uri.parse('https://mysite.ru/user');

Отправим GET-запрос и получим ответ

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

Пример (здесь и далее мы скрыли под катом часть объёмных примеров для удобства чтения)
1Future<String?> getUserData(String userID) async {
2  // Устанавливаем URL-адрес 
3  final userDataUrl = Uri.parse('http://mysite.ru/user/data/$userID');
4  // Создаем инстанс HTTP-клиента
5  final httpClient = HttpClient();
6  // Открываем HTTP-соединение, используя метод GET 
7  final userDataRequest = await httpClient.getUrl(userDataUrl);
8  // Закрываем HTTP-соединение
9  final userDataResponse = await userDataRequest.close();
10  // Декодируем тело HTTP-ответа из UTF8
11  final userDataResponseBody =
12      await userDataResponse.transform(const Utf8Decoder()).join();
13  // Завершаем работу HTTP-клиента
14  httpClient.close();
15  // Если сервер обработал запрос с ошибкой, то statusCode != 200
16  if (userDataResponse.statusCode != 200) {
17    // Выводим ошибку в консоль
18    print('Failed to retrieve user data!');
19    // Возвращаем null
20    return null;
21  }
22  // Возвращаем данные с сервера
23  return userDataResponseBody;
24}

Отправим POST-запрос и получим ответ

Мы используем метод POST для отправки данных на сервер. Для POST-запроса требуется тело — в нём мы определяем данные сущности, которую хотим отправить.

В примере ниже успешный POST возвращает ответ, содержащий созданную или обновлённую нами информацию о пользователе. Однако тело возвращаемого ответа может быть и пустым — это зависит от конфигурации API-сервера.

Пример
1    Future<String?> setUserData(String userId, String name, String email) async {
2      // Устанавливаем URL-адрес 
3      final userUpdateUrl = Uri.parse('https://mysite.ru/user/update');
4      // Устанавливаем обновлённые данные пользователя
5      final userUpdate = {'id': userId, 'name': name, 'email': email};
6      // Создаем инстанс HTTP-клиента
7      final httpClient = HttpClient();
8      // Открываем HTTP-соединение, используя метод POST
9      final userUpdateRequest = await httpClient.postUrl(userUpdateUrl);
10      // Устанавливаем тип передаваемых данных application/json
11      userUpdateRequest.headers
12          .set(HttpHeaders.contentTypeHeader, 'application/json; charset=UTF-8');
13      // Устанавливаем передаваемые данные  
14      userUpdateRequest.write(userUpdate);
15      // Закрываем HTTP-соединение
16      final userUpdateResponse = await userUpdateRequest.close();
17      // Декодируем тело HTTP-ответа из UTF8
18      final userUpdateResponseBody =
19          await userUpdateResponse.transform(const Utf8Decoder()).join();
20      // Завершаем работу HTTP-клиента
21      httpClient.close();
22      // Если сервер обработал запрос с ошибкой, то statusCode != 200
23      if (userUpdateResponse.statusCode != 200) {
24        // Выводим ошибку в консоль
25        print('Failed to update user data!');
26        // Возвращаем null
27        return null;
28      }
29      // Возвращаем данные с сервера
30      return userUpdateResponseBody;
31    }

Вы можете использовать HttpClient из dart:io или HttpRequest из dart:html для выполнения HTTP-запросов, однако эти библиотеки предоставляют низкоуровневую функциональность HTTP и зависят от платформы. Как вы могли заметить, выполнять запросы и получать ответы, используя эти классы напрямую, достаточно сложно.

Поэтому можно воспользоваться пакетом http, который предоставляет универсальную кроссплатформенную библиотеку для HTTP-запросов, упрощая взаимодействие с HttpClient и HttpRequest.

http

Этот мультиплатформенный пакет содержит набор высокоуровневых функций и классов, упрощающих использование ресурсов HTTP.

Далее мы:

  1. Добавим зависимость.
  2. Добавим импорт.
  3. Отправим GET-запрос и получим ответ.
  4. Отправим POST-запрос и получим ответ.

Поехали.

Добавим зависимость

Для начала установим пакет http. Добавьте его в раздел зависимостей файла pubspec.yaml.

Пример
1    dependencies:
2      http: <latest_version>

👉 Важно: если у вас не включена автоматическая установка зависимостей при сохранении изменений в файле pubspec.yaml, то вам необходимо выполнить flutter pub get (для Flutter-проекта) или dart pub get (для Dart-проекта).

Добавим импорт

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

Пример
```dart
import 'package:http/http.dart' as http;
```

👉 Важно: пакет http предоставляет глобальные функции, для удобства обращения к которым мы импортируем пакет с помощью префикса as http . Добавление префикса необязательно, вы можете использовать пакет без него.

Отправим GET-запрос и получим ответ

Перепишем метод для получения данных пользователя.

Пример
```dart
Future<String?> getUserData(String userID) async {
  final userDataUrl = Uri.parse('https://mysite.ru/user/data/$userID');
  final userDataResponse = await http.get(userDataUrl);
  if (userDataResponse.statusCode != 200) {
    print('Failed to retrieve user data!');
    return null;  
  }
  return userDataResponse.body;
}
```

Отправим POST-запрос и получим ответ

Перепишем метод для обновления данных пользователя.

Пример
```dart
Future<String?> setUserData(String userId, String name, String email) async {
  final userUpdateUrl = Uri.parse('https://mysite.ru/user/update');
  final userUpdate = {'id': userId, 'name': name, 'email': email};
  final userUpdateResponse = await http.post(userUpdateUrl, body: userUpdate);
  if (userUpdateResponse.statusCode != 200) {
    print('Failed to update user data!');
    return null;
  }
  return userUpdateResponse.body;
}
```

Как видите, нам удалось значительно сократить объём кода и упростить работу с HTTP.

Помимо этого пакет предоставляет и другие возможности. Например, класс RetryClient отправляет упавшие запросы повторно. Такое может пригодиться, например, когда ваш сервер не готов обработать запрос и возвращает ошибку 503 Service Unavailable.

Пример
```dart
import 'package:http/http.dart' as http;
import 'package:http/retry.dart';

Future<String?> setUserData(String userId, String name, String email) async {
  final userUpdateUrl = Uri.parse('https://mysite.ru/user/update');
  final userUpdate = {'id': userId, 'name': name, 'email': email};
  final client = RetryClient(http.Client());
  final userUpdateResponse = await client.post(userUpdateUrl, body: userUpdate);
  if (userUpdateResponse.statusCode != 200) {
    print('Failed to update user data!');
    return null;
  }
  return userUpdateResponse.body;
}
```

Методы GET и POST возвращают Response , который содержит данные, полученные от успешного HTTP-запроса. Например, в поле statusCode возвращается статус запроса, а в поле body — тело ответа.

Наш API отправляет данные пользователя, которые содержатся в поле body в виде строки.

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

Давайте рассмотрим его подробнее.

Формат передачи данных JSON

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

JSON (англ. JavaScript Object Notation) — это формат обмена данными, который стал повсеместным при разработке приложений и взаимодействии между клиентом и сервером. С помощью JSON различные типы данных, такие как данные пользователя, могут быть сериализованы и представлены строками.

Пример JSON:

1{'name': 'Alex', 'email': 'alex@ya.ru'}

Библиотеки для преобразования данных в JSON

Процесс преобразования данных в JSON называется кодированием, обратный процесс — декодированием. Существует два подхода к преобразованию данных: ручной и автоматический.

  • Для ручного преобразования в библиотеке dart:convert есть функции-преобразователи JSON.
  • Для автоматического — библиотека json_serializable и вспомогательная библиотека build_runner

Рассмотрим оба подхода.

dart:convert

Для ручного кодирования и декодирования в библиотеке dart:convert есть функции-преобразователи JSON. Если в вашем проекте не так много моделей JSON или вы хотите быстро протестировать какую-то гипотезу, лучше выбрать именно ручную сериализацию.

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

Вот что нам нужно сделать дальше:

  1. Создать класс User для удобной работы с этими данными.
  2. Добавить метод для кодирования класса User в JSON.
  3. Добавить метод для декодирования JSON в класс User

Приступим.

Создадим класс User

Он будет хранить информацию о пользователе.

Пример
```dart
class User {
  final String name;
  final String email;
  const User(this.name, this.email);
}
```

Добавим метод для кодирования класса в JSON

Чтобы отправлять данные пользователя на сервер, нам необходимо кодировать класс User в формат данных JSON. Для этого добавим в него метод toJson(), который преобразует экземпляр класса User в Map<String, dynamic>.

```dart
Map<String, dynamic> toJson() => {
  'name': name,
   'email': email,
};
```

Если вы хотите преобразовать Map<String, dynamic> в строку, то можете воспользоваться функцией jsonEncode. Это может понадобиться, например, при сохранении данных на диск или при записи в базу данных.

Пример
```dart
Map<String, dynamic> userMap = user.toJson();
String jsonString = jsonEncode(user);
print(jsonString);
```

👉 Важно: можно не вызывать метод toJson() у экземпляра класса User, функция jsonEncode() может сделать это за вас.

Добавим метод для декодирования JSON в класс

Чтобы получать данные пользователя в виде класса User, нам необходимо декодировать их из формата данных JSON. Для этого добавим в него конструктор fromJson(), который преобразует Map<String, dynamic> в экземпляр класса User.

Пример
```dart
User.fromJson(Map<String, dynamic> json)
  : name = json['name'],
     email = json['email'];
```

Теперь, чтобы декодировать строку JSON в класс User, сначала передайте строку в функцию jsonDecode(), а затем вызовите конструктор класса fromJson(), передав получившейся Map<String, dynamic>.

Пример
```dart
Map<String, dynamic> userMap = jsonDecode(jsonString);
User user = User.fromJson(userMap);
print('Hello, ${user.name}!');
```

Благодаря этому подходу вы можете легко кодировать и декодировать свои классы.

Вот что у нас получилось в итоге
```dart
class User {
  final String name;
  final String email;
  const User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
  : name = json['name'],
     email = json['email'];

  Map<String, dynamic> toJson() => {
  'name': name,
   'email': email,
  };
}
```

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

json_serializable и build_runner

Сериализация JSON с генерацией кода означает, что внешняя библиотека генерирует шаблон кодирования за вас.

Для этого необходимо:

  1. Добавить зависимости.
  2. Преобразовать класс (для примера возьмём уже знакомый вам User).
  3. Настроить поля класса.
  4. Сгенерировать код.

Разберём каждый шаг подробнее.

Добавим зависимости

Чтобы включить json_serializable и build_runner в свой проект, нам понадобится обновить pubspec.yaml.

Пример
```yaml
dependencies:
  json_annotation: <latest_version>

dev_dependencies:
  build_runner: <latest_version>
  json_serializable: <latest_version>
```

Преобразуем класс

Преобразуем наш класс User в класс JsonSerializable. Для этого добавьте аннотацию @JsonSerializable(), функцию toJson(), конструктор fromJson() и файл user.g.dart.

Пример
```dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; 

@JsonSerializable()
class User {
  final String name;
  final String email;
  const User(this.name, this.email);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
```

Настроим поля класса

Вы также можете использовать дополнительные аннотации для настройки полей класса.

Пример
```dart
// Обязательность поля
@JsonKey(required: true)
final String id;

// Значение поля по умолчанию
@JsonKey(defaultValue: '')
final String name;

// Название поля
@JsonKey(name: 'email')
final String email;
```

С json_serializable вы можете забыть о ручной сериализации и десериализации JSON. Генератор кода создает файл с именем user.g.dart, содержащий всю необходимую логику сериализации.

user

По умолчанию json_serializable поддерживает все основные типы данных. Но если вы хотите использовать типы, которые не поддерживаются из коробки, то советуем изучить документацию к библиотеке.

Сгенерируем код

При первом создании классов с json_serializable вы получите следующие ошибки:

user

И эти ошибки совершенно нормальны просто потому, что сгенерированный код для класса модели ещё не существует.

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

1flutter pub run build_runner build --delete-conflicting-outputs

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

Обмен данными с помощью веб-сокетов

Как мы помним, протокол HTTP работает по принципу «запрос — ответ». Чтобы получить новую информацию от сервера, клиент должен направить ему запрос, каждый раз устанавливая и закрывая соединение с сервером.

У такого подхода два главных недостатка:

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

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

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

Главное отличие веб-сокетов заключается в том, что они позволяют получать данные без необходимости отправлять отдельный запрос, как это происходит в HTTP. После установки соединения данные будут приходить сами, не требуя отправки запроса.

Библиотеки для работы с веб-сокетами

Мы рассмотрим две — dart:io (класс WebSocket) и web_socket_channel.

Первая работает только на мобильных и десктопных платформах, поэтому если нужно поддерживать веб, то придётся использовать библиотеку dart:html.

Вторая — кросс-платформенная, но «под капотом» всё равно использует dart:io и dart:html.

dart:io (WebSocket)

Далее мы опустим шаги по инициализации библиотеки — к этому моменту вы наверняка уже мастерски с этим справляетесь.

Лучше сфокусируемся на основной механике. Что сделаем дальше:

  1. Подключимся к веб-сокету.
  2. Получим сообщение или ошибку.
  3. Отправим сообщение на сервер.
  4. Закроем соединение.

Стартуем!

Подключаемся к веб-сокету

Создадим нового клиента и подключимся к каналу с помощью WebSocket.connect().

Пример
```dart
final socket = await WebSocket.connect('ws://mysite.ru/user/ws');
```

Получаем сообщение (или ошибку)

Чтобы слушать входящие сообщения и ошибки можно воспользоваться коллбэками onMessage и onError.

Пример
```dart
socket.listen(
  (message) => print('Received message: $message'),
  onError: (error) => print('Received error: $error'),
);
```

Отправляем сообщение на сервер

Для этого воспользуемся методом add.

Пример
```dart
socket.add(message);
```

Закроем соединение

Тут нам поможет метод close.

Пример
```dart
await socket.close();
```

Если вы разрабатываете приложение только для одной платформы (мобильной, десктопной или веб), то можете безопасно использовать WebSocket из dart:io или dart:html. Но если нужно скомпилировать приложение сразу под несколько платформ, вы столкнётесь с проблемой.

Чтобы её избежать, команда Dart создала библиотеку web_socket_channel, которая абстрагирует и упрощает логику dart:io и dart:html, позволяя использовать один класс для создания мультиплатформенного приложения.

web_socket_channel

Шаги будут те же, что и выше, только немного будет отличаться API.

Подключаемся к веб-сокету

Создадим нового клиента с помощью WebSocketChannel и подключимся к каналу с помощью метода connect.

Пример
```dart
final url = Uri.parse('ws://mysite.ru/user/ws');
final channel = WebSocketChannel.connect(url);
```

Получаем сообщение (или ошибку)

Для этого воспользуемся методо  stream, который также позволяет прокидывать коллбэки onError и onMessage.

Пример
```dart
channel.stream.listen(
  (message) => print('Received message: $message'),
  onError: (error) => print('Received error: $error'),
);
```

Отправим сообщение на сервер

Для этого воспользуемся методом add.

Пример
```dart
channel.sink.add(message);
```

Закроем соединение

С помощью метода clos.

Пример
```dart
channel.sink.close();
```

Хранение данных

В некоторых случаях необходимо сохранять данные с сервера. Например, чтобы использовать их в автономном режиме, когда отсутствует доступ к интернету (эта механика называется persistence, в переводе с английского — «постоянство», «живучесть»).

Для этого мы рассмотрим библиотеки, с помощью которых можно быстро организовать:

  • чтение и запись файлов (библиотеки path_provider и dart:io).
  • хранение коллекций «ключ-значение» (библиотека shared_preferences).

👉 Важно: это только основы persistance. Более подробно мы рассмотрим эту тему в следующих главах.

Чтение и запись файлов

Чтобы сохранить данные на диск, нам потребуется плагин path_provider и библиотека dart:io (класс File).

👉 Важно: этот способ не подходит для разработки веб-приложений. В этом случае используйте Window.localStorage или Window.sessionStorage из библиотеки dart:html.

Вот что мы будем делать дальше:

  1. Найдём правильный локальный путь.
  2. Создадим ссылку на расположение файла.
  3. Запишем данные в файл.
  4. Прочитаем данные из файла.

Итак, вперёд.

Найдём правильный локальный путь

Пакет path_provider предоставляет независимый от платформы доступ к часто используемым местам в файловой системе устройства. Плагин поддерживает доступ к нескольким местоположениям файловой системы, но мы рассмотрим наиболее подходящий вариант для хранения данных — документы приложения (Application Documents).

Документы приложения — это каталог, в котором приложение может хранить файлы, доступ к которым есть только у него. Система очищает этот каталог только при удалении самого приложения. В iOS это соответствует NSDocumentDirector, в Android это каталог AppData.

Вот как найти путь к документам приложения
```dart
Future<String> get _localPath async {
  final directory = await getApplicationDocumentsDirectory();
  return directory.path;
}
```

Создадим ссылки на расположение файла

Как только мы узнали расположение документов приложения, мы можем создать ссылку на местоположение файла. Для этого можно использовать класс File из библиотеки dart:io.

Пример
```dart
Future<File> get _localFile async {
  final path = await _localPath;
  return File('$path/user_data.txt');
}
```

Запись данных в файл

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

```dart
Future<File> writeUserData(User user) async {
  final file = await _localFile;
  return file.writeAsString(jsonEncode(user));
}
```

Чтение данных из файла

Теперь, когда данные на диски, мы можем их прочитать.

```dart
Future<User> readUserData() async {
    final file = await _localFile;
    final userData = await file.readAsString();
    return User.fromJson(jsonDecode(userData));
}
```

Хранить данные в файлах и работать с ними не всегда удобно.

Так что можно рассмотреть альтернативный вариант — хранить данные в нативном хранилище «ключ-значение» с помощью пакета shared_preferences.

Хранение коллекций «ключ-значение»

Если у вас есть относительно небольшая коллекция ключей и значений для сохранения, вы можете использовать плагин shared_preferences. Этот плагин оборачивает нативное хранилище данных NSUserDefaults (на iOS и macOS) и SharedPreferences (на Android), обеспечивая хранение для простых данных.

Далее мы:

  1. Инициализируем хранилище SharedPreferences.
  2. Запишем данные.
  3. Прочитаем данные.
  4. Удалим данные.

Инициализируем хранилище SharedPreferences

Чтобы начать работу с SharedPreferences, необходимо получить его инстанс.

```dart
final prefs = await SharedPreferences.getInstance();
```

Запишем данные

Для сохранения данных используем set-методы. Эти методы делают две вещи: синхронно обновляют пару «ключ-значение» в памяти приложения, затем сохраняют данные на диск.

Пример
```dart
await prefs.setInt('userId', userId);
await prefs.setBool('userValid', userValid);
await prefs.setString('userName', userName);
await prefs.setDouble('userRating', userRating);
await prefs.setStringList('userRoles', userRoles);
```

Прочитаем данные

Для чтения данных используем get-методы.

Пример
```dart
final int? userId = prefs.getInt('userId');
final bool? userValid = prefs.getBool('userValid');
final String? userName = prefs.getString('userName');
final double? userRating = prefs.getDouble('userRating');
final List<String>? userRoles = prefs.getStringList('userRoles');
```

Удалим данные

Для удаления данных воспользуемся методом remove.

Пример
1  await prefs.remove('counter');

Хотя хранилище «ключ-значение» простое и удобное в использовании, а также поддерживает все доступные платформы, у него есть ряд ограничений:

  1. можно использовать только следующие типы данных: intdoubleboolString и List<String>;
  2. оно не предназначено для хранения большого количества данных;
  3. оно больше подходит для данных, которые часто читаются, но редко обновляются;
  4. хранить конфиденциальные данные небезопасно, для этого стоит использовать плагин flutter_secure_storage или шифровать данные перед сохранением с помощью encrypt/flutter_string_encrypt.

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


Вот мы и рассмотрели основы работы с данными. Как обмениваться ими с сервером в режиме «запрос — ответ» (HTTP) или в реальном времени (WebSocket). Какой формат использовать (JSON), как преобразовывать данные в этот формат и как хранить их на устройстве пользователя.

Это только начало, но это уже неплохой фундамент! А чтобы закрепить знания, советуем прорешать квиз.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.15. Виджеты: формы и кнопки
Следующий параграф2.17. Project: логирование