2.12. Работа с сетью (http, socket), сериализация, хранение данных

Введение

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

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

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

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

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

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

Пример 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

Структура HTTP запроса:

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

Пример 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>

Структура HTTP ответа:

  1. Код состояния (Status code) — коды состояния HTTP используются, чтобы сообщить клиенту статус его запроса. HTTP-сервер может вернуть код, принадлежащий одной из пяти категорий кодов состояния (1xx, 2xx, 3xx, 4xx, 5xx).
    Например, самые распространенные коды ответов:
    200 OK — запрос обработан успешно;
    400 Bad Request — в запросе ошибка;
    500 Internal Error — сервер не может обработать запрос.

  2. Заголовки (Headers) — используются, чтобы уточнить ответ, и никак не влияют на содержимое тела ответа. Например, заголовок WWW-Authenticate уведомляет клиента о типе аутентификации, который необходим для доступа к запрашиваемому ресурсу.

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

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

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

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

Пример АPI-сервера:

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

HttpClient dart:io

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

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

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

    1Uri.parse('https://mysite.ru/user');
    
  2. Отправка запроса и получение ответа (метод 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}
    
  3. Отправка запроса и получение ответа (метод POST)
    Мы используем метод POST для отправки данных на сервер. Для POST-запроса требуется тело — в нём мы определяем данные сущности, которую хотим отправить.
    В примере ниже успешный POST возвращает ответ, содержащий созданную или обновлённую нами информацию о пользователе. Однако тело возвращаемого ответа может быть и пустым — это зависит от конфигурации API-сервера.

    1Future<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.

package:http

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

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

    1dependencies:
    2  http: <latest_version>
    

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

  2. Добавление импорта
    Добавьте импорт пакета http в необходимый файл.

    1import 'package:http/http.dart' as http;
    

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

  3. Отправка запроса и получение ответа (метод GET)
    Перепишем метод для получения данных пользователя.

    1Future<String?> getUserData(String userID) async {
    2  final userDataUrl = Uri.parse('https://mysite.ru/user/data/$userID');
    3  final userDataResponse = await http.get(userDataUrl);
    4  if (userDataResponse.statusCode != 200) {
    5    print('Failed to retrieve user data!');
    6    return null7  }
    8  return userDataResponse.body;
    9}
    
  4. Отправка запроса и получение ответа (метод POST)
    Перепишем метод для обновления данных пользователя.

    1Future<String?> setUserData(String userId, String name, String email) async {
    2  final userUpdateUrl = Uri.parse('https://mysite.ru/user/update');
    3  final userUpdate = {'id': userId, 'name': name, 'email': email};
    4  final userUpdateResponse = await http.post(userUpdateUrl, body: userUpdate);
    5  if (userUpdateResponse.statusCode != 200) {
    6    print('Failed to update user data!');
    7    return null;
    8  }
    9  return userUpdateResponse.body;
    10}
    

Как видите, нам удалось значительно сократить объём кода и упростить работу с 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

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

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

  1. Создание класса
    Создаем класс User, который хранит информацию о пользователе.

    1class User {
    2  final String name;
    3  final String email;
    4  const User(this.name, this.email);
    5}
    
  2. Ручное кодирование JSON
    Чтобы отправлять данные пользователя на сервер, нам необходимо кодировать класс User в формат данных JSON. Для этого добавим в него метод toJson(), который преобразует экземпляр класса User в Map<String, dynamic>.

    1Map<String, dynamic> toJson() => {
    2  'name': name,
    3   'email': email,
    4};
    

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

    1Map<String, dynamic> userMap = user.toJson();
    2String jsonString = jsonEncode(user);
    3print(jsonString);
    

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

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

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

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

    1Map<String, dynamic> userMap = jsonDecode(jsonString);
    2User user = User.fromJson(userMap);
    3print('Hello, ${user.name}!');
    
  4. Получившийся класс
    Благодаря этому подходу вы можете легко кодировать и декодировать свои классы.

    1class User {
    2  final String name;
    3  final String email;
    4  const User(this.name, this.email);
    5
    6  User.fromJson(Map<String, dynamic> json)
    7  : name = json['name'],
    8     email = json['email'];
    9
    10  Map<String, dynamic> toJson() => {
    11  'name': name,
    12   'email': email,
    13  };
    14}
    

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

package:json_serializable

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

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

    1dependencies:
    2  json_annotation: <latest_version>
    3
    4dev_dependencies:
    5  build_runner: <latest_version>
    6  json_serializable: <latest_version>
    
  2. Преобразование класса
    Преобразуем наш класс User в класс JsonSerializable. Для этого добавьте аннотацию @JsonSerializable(), функцию toJson(), конструктор fromJson() и файл user.g.dart.

    1import 'package:json_annotation/json_annotation.dart';
    2part 'user.g.dart'3
    4@JsonSerializable()
    5class User {
    6  final String name;
    7  final String email;
    8  const User(this.name, this.email);
    9
    10  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    11  Map<String, dynamic> toJson() => _$UserToJson(this);
    12}
    
  3. Настройка полей класса
    Вы также можете использовать дополнительные аннотации для настройки полей класса.

    1// Обязательность поля
    2@JsonKey(required: true)
    3final String id;
    4
    5// Значение поля по умолчанию
    6@JsonKey(defaultValue: '')
    7final String name;
    8
    9// Название поля
    10@JsonKey(name: 'email')
    11final String email;
    

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

user

По умолчанию json_serializable поддерживает следующие типы данных: bool, DateTime, double, Duration, Enum, int, Iterable, List, Map, num, Object, Set, String, Uri. Если вы хотите использовать типы, которые не поддерживаются из коробки, то у вас есть несколько вариантов решения этой проблемы, о которых вы можете прочитать в документации к пакету.

package:build_runner

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

user

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

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

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

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

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

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

Такой подход имеет два главных недостатка:

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

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

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

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

WebSocket dart:io

Для работы с веб-сокетами библиотека dart:io предоставляет нам класс WebSocket.

⚠️ Внимание!
WebSocket из dart:io работает только для мобильных и десктопных платформ. Чтобы использовать веб-сокеты для веб-приложений, используйте библиотеку dart:html.

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

    1final socket = await WebSocket.connect('ws://mysite.ru/user/ws');
    
  2. Получение сообщения или ошибки
    Слушайте входящие сообщения и ошибки с помощью onMessage и onError.

    1socket.listen(
    2  (message) => print('Received message: $message'),
    3  onError: (error) => print('Received error: $error'),
    4);
    
  3. Отправка сообщения на сервер
    Используйте метод add для отправки сообщений на сервер.

    1socket.add(message);
    
  4. Закрытие соединения
    Используйте метод close для закрытия соединения.

    1await socket.close();
    

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

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

package:web_socket_channel

Пакет web_socket_channel предоставляет кроссплатформенную реализацию, обёртывающую класс WebSocket из dart:io и dart:html.

  1. Добавление зависимости
    Добавьте плагин web_socket_channel в файл pubspec.yaml.

    1dependencies:
    2  web_socket_channel: <latest_version>
    
  2. Подключение к веб-сокету
    Создайте нового клиента с помощью WebSocketChannel и подключитесь к каналу с помощью метода connect.

    1final url = Uri.parse('ws://mysite.ru/user/ws');
    2final channel = WebSocketChannel.connect(url);
    
  3. Получение сообщения или ошибки
    Слушайте входящие сообщения и ошибки с помощью stream.

    1channel.stream.listen(
    2  (message) => print('Received message: $message'),
    3  onError: (error) => print('Received error: $error'),
    4);
    
  4. Отправка сообщения на сервер
    Используйте метод add для отправки сообщений на сервер.

    1channel.sink.add(message);
    
  5. Закрытие соединения
    Используйте метод close для закрытия соединения.

    1channel.sink.close();
    

Persistence — хранение данных простыми инструментами

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

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

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

⚠️ Внимание!
Данный способ не подходит для разработки веб-приложений. При разработке на веб используйте Window.localStorage или Window.sessionStorage из библиотеки dart:html.

  1. Добавление зависимости
    Добавьте плагин path_provider в файл pubspec.yaml.

    1dependencies:
    2  path_provider: <latest_version>
    
  2. Поиск правильного локального пути
    Пакет path_provider предоставляет независимый от платформы доступ к часто используемым местам в файловой системе устройства. Плагин поддерживает доступ к нескольким местоположениям файловой системы, но мы рассмотрим наиболее подходящий вариант для хранения данных — документы приложения (Application Documents).
    Документы приложения — это каталог, в котором приложение может хранить файлы, доступ к которым есть только у него. Система очищает этот каталог только при удалении самого приложения. В iOS это соответствует NSDocumentDirector, в Android это каталог AppData.
    Вы можете найти путь к документам приложения следующим образом:

    1Future<String> get _localPath async {
    2  final directory = await getApplicationDocumentsDirectory();
    3  return directory.path;
    4}
    
  3. Создание ссылки на расположение файла
    Как только вы узнаете расположение документов приложения, создайте ссылку на местоположение файла. Для этого вы можете использовать класс File из библиотеки dart:io.

    1Future<File> get _localFile async {
    2  final path = await _localPath;
    3  return File('$path/user_data.txt');
    4}
    
  4. Запись данных в файл
    Теперь, когда у нас есть файл для работы, используйте его для чтения и записи данных. Сначала запишите данные в файл.

    1Future<File> writeUserData(User user) async {
    2  final file = await _localFile;
    3  return file.writeAsString(jsonEncode(user));
    4}
    
  5. Чтение данных из файла
    Теперь, когда у вас есть данные на диске, вы можете их прочитать.

    1Future<User> readUserData() async {
    2    final file = await _localFile;
    3    final userData = await file.readAsString();
    4    return User.fromJson(jsonDecode(userData));
    5}
    

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

package:shared_preferences

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

  1. Добавление зависимости
    Добавьте плагин shared_preferences в файл pubspec.yaml.

    1dependencies:
    2  shared_preferences: <latest_version>
    
  2. Инициализация SharedPreferences
    Чтобы начать работу с SharedPreferences, необходимо получить его инстанс.

    1final prefs = await SharedPreferences.getInstance();
    
  3. Запись данных
    Для сохранения данных используйте set-методы.
    Эти методы делают две вещи: синхронно обновляют пару «ключ-значение» в памяти приложения, затем сохраняют данные на диск.

    1await prefs.setInt('userId', userId);
    2await prefs.setBool('userValid', userValid);
    3await prefs.setString('userName', userName);
    4await prefs.setDouble('userRating', userRating);
    5await prefs.setStringList('userRoles', userRoles);
    
  4. Чтение данных
    Для чтения данных используйте get-методы.

    1final int? userId = prefs.getInt('userId');
    2final bool? userValid = prefs.getBool('userValid');
    3final String? userName = prefs.getString('userName');
    4final double? userRating = prefs.getDouble('userRating');
    5final List<String>? userRoles = prefs.getStringList('userRoles');
    
  5. Удаление данных
    Для удаления данных используйте метод remove.

    1await prefs.remove('counter');
    

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

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

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

Заключение

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

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

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

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