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

Введение

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

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

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

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

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

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

Пример HTTP-запроса:

GET /handbook/ HTTP/1.1
Host: academy.yandex.ru
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 
Accept: text/html,application/xhtml+xml,application/xml
Accept-Language: en-US,en;q=0.9,ru;q=0.8
Accept-Encoding: gzip, deflate, br
Authorization: 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-ответа:

HTTP/1.1 200 OK
Date: Tue, 02 May 2023 22:42:26 GMT
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
WWW-Authenticate: Basic

<!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-сервера:

// Метод GET для получения данных о пользователе
https://mysite.ru/user/data

// Метод POST для отправки данных о пользователе
https://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, если указали невалидный адрес).

    Uri.parse('https://mysite.ru/user');
    
  2. Отправка запроса и получение ответа (метод GET)
    Давайте рассмотрим, как сделать GET-запрос, который мы используем для получения данных от сервера. В данном примере успешный GET возвращает запрошенную нами информацию о пользователе по его userID.

    Future<String?> getUserData(String userID) async {
      // Устанавливаем URL-адрес 
      final userDataUrl = Uri.parse('http://mysite.ru/user/data/$userID');
      // Создаем инстанс HTTP-клиента
      final httpClient = HttpClient();
      // Открываем HTTP-соединение, используя метод GET 
      final userDataRequest = await httpClient.getUrl(userDataUrl);
      // Закрываем HTTP-соединение
      final userDataResponse = await userDataRequest.close();
      // Декодируем тело HTTP-ответа из UTF8
      final userDataResponseBody =
          await userDataResponse.transform(const Utf8Decoder()).join();
      // Завершаем работу HTTP-клиента
      httpClient.close();
      // Если сервер обработал запрос с ошибкой, то statusCode != 200
      if (userDataResponse.statusCode != 200) {
        // Выводим ошибку в консоль
        print('Failed to retrieve user data!');
        // Возвращаем null
        return null;
      }
      // Возвращаем данные с сервера
      return userDataResponseBody;
    }
    
  3. Отправка запроса и получение ответа (метод POST)
    Мы используем метод POST для отправки данных на сервер. Для POST-запроса требуется тело — в нём мы определяем данные сущности, которую хотим отправить.
    В примере ниже успешный POST возвращает ответ, содержащий созданную или обновлённую нами информацию о пользователе. Однако тело возвращаемого ответа может быть и пустым — это зависит от конфигурации API-сервера.

    Future<String?> setUserData(String userId, String name, String email) async {
      // Устанавливаем URL-адрес 
      final userUpdateUrl = Uri.parse('https://mysite.ru/user/update');
      // Устанавливаем обновлённые данные пользователя
      final userUpdate = {'id': userId, 'name': name, 'email': email};
      // Создаем инстанс HTTP-клиента
      final httpClient = HttpClient();
      // Открываем HTTP-соединение, используя метод POST
      final userUpdateRequest = await httpClient.postUrl(userUpdateUrl);
      // Устанавливаем тип передаваемых данных application/json
      userUpdateRequest.headers
          .set(HttpHeaders.contentTypeHeader, 'application/json; charset=UTF-8');
      // Устанавливаем передаваемые данные  
      userUpdateRequest.write(userUpdate);
      // Закрываем HTTP-соединение
      final userUpdateResponse = await userUpdateRequest.close();
      // Декодируем тело HTTP-ответа из UTF8
      final userUpdateResponseBody =
          await userUpdateResponse.transform(const Utf8Decoder()).join();
      // Завершаем работу HTTP-клиента
      httpClient.close();
      // Если сервер обработал запрос с ошибкой, то statusCode != 200
      if (userUpdateResponse.statusCode != 200) {
        // Выводим ошибку в консоль
        print('Failed to update user data!');
        // Возвращаем null
        return null;
      }
      // Возвращаем данные с сервера
      return userUpdateResponseBody;
    }
    

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

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

package:http

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

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

    dependencies:
      http: <latest_version>
    

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

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

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

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

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

    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;
    }
    
  4. Отправка запроса и получение ответа (метод POST)
    Перепишем метод для обновления данных пользователя.

    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:

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

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

JSON dart:convert

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

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

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

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

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

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

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

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

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

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

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

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

    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.

package:json_serializable

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

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

    dependencies:
      json_annotation: <latest_version>
    
    dev_dependencies:
      build_runner: <latest_version>
      json_serializable: <latest_version>
    
  2. Преобразование класса
    Преобразуем наш класс User в класс JsonSerializable. Для этого добавьте аннотацию @JsonSerializable(), функцию toJson(), конструктор fromJson() и файл user.g.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);
    }
    
  3. Настройка полей класса
    Вы также можете использовать дополнительные аннотации для настройки полей класса.

    // Обязательность поля
    @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 поддерживает следующие типы данных: bool, DateTime, double, Duration, Enum, int, Iterable, List, Map, num, Object, Set, String, Uri. Если вы хотите использовать типы, которые не поддерживаются из коробки, то у вас есть несколько вариантов решения этой проблемы, о которых вы можете прочитать в документации к пакету.

package:build_runner

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

user

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

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

flutter 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().

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

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

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

    await 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.

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

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

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

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

    channel.sink.close();
    

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

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

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

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

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

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

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

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

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

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

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

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

package:shared_preferences

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

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

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

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

    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);
    
  4. Чтение данных
    Для чтения данных используйте get-методы.

    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');
    
  5. Удаление данных
    Для удаления данных используйте метод remove.

    await 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: логи, обработка ошибок