Привет, Хабр! Меня зовут Юра Петров, я руководитель отдела разработки компании Friflex и автор канала «Мобильный разработчик». Это вторая статья в серии о платформах, которые поддерживает Flutter, и в ней на самом деле ничего не будет про чайник. Про чайник было в первой.
А эта статья о том, как все-таки начать Flutter-проект так, чтобы можно было бы его легко портировать на другие платформы и сохранить себе кучу нервов.
Шаг 1. Анализируем подключаемые пакеты
Я написал в чате GPT: «Как начать проект на Flutter?» И gpt выдал вот такой мемик :

Ну, на самом деле это так и есть. Но именно с пакетами проблем нет, так ��ак они не взаимодействуют с платформой, они работают везде, где работает Dart-код.
А вот с плагинами уже проблематичнее, потому что у них есть ограниченная поддержка систем. Например, плагин flutter_secure_storage.

Мы видим, что у него есть поддержка Android, iOS, Linux, MacOS, Web и Windows. А что, если нам нужно, например, это плагин использовать на Авроре или на Huawei? Что нам делать?
Давайте попробуем начать проект и представить, что нам этот проект нужно портировать, допустим, на Аврору. То есть мы все сделали базовую часть. Нам нужно сделать так, чтобы проект запускался на Авроре, на Андроиде и на iOS с минимальными изменениями. Даже не с минимальными, а вообще чтобы не менялось ничего в основном коде нашего проекта. Это очень важно. Что нам нужно для этого сделать?
Шаг 2. Создаем папку app_service
Ну, во-первых, нам нужно создать глобальную папку app_service в корне проекта. В этой папке мы создаем папку для хранения интерфейсов interfaces.

Вы можете в принципе назвать ее как угодно. И вот в этой папке мы создаем простой проект со своим pubspec.yaml. Объявляем здесь export и сам интерфейс iSecureStorage.
app_services\interfaces\lib\src\i_secure_storage.dart
abstract interface class ISecureStorage { const ISecureStorage._({required this.secretKey}); String get name; final String? secretKey; Future<String?> read(String key); Future<void> write(String key, String value); }
Обратите внимание, что здесь мы передаем ключ. Все, кто использовал когда-то Flutter SecureStorage, знают, что ключ нам по факту не нужен, потому что сам пакет использует встроенное защищенное хранилище iOS и Android. Но если брать, например, Аврору, у нее такого хранилища нет. Из-за этого плагин сам шифрует эти значения. Но ему нужен ключ. Вот мы и делаем это опциональным параметром. А дальше уже объявляем два метода: Read, Write. В настоящем проекте методов, конечно, намного больше. Но это просто для чистоты эксперимента.
Мы объявили интерфейс, что дальше?
Экспортируем интерфейс i_secure_storage.dart.
app_services\interfaces\lib\interfaces.dart
library; export 'src/i_secure_storage.dart';
Шаг 3. Создаем реализации для Base
Дальше делаем базовую реализацию. Что такое базовая реализация? Это реализация как раз тех плагинов, которые мы берем именно с pub.dev.

Создаем в папке app_service папку base. В этой папке мы создаем простой проект со своим pubspec.yaml. Дальше создаем там имплементацию AppSecureStorage. Секретный ключ не используем: все как обычно, ничего нового.
app_services\base\app_services\lib\src\app_secure_storage.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:interfaces/interfaces.dart'; final class AppSecureStorage implements ISecureStorage { AppSecureStorage({required this.secretKey}) { storage = FlutterSecureStorage(); } late final FlutterSecureStorage storage; @override final String? secretKey; @override String get name => 'BaseAppSecureStorage'; @override Future<String?> read(String key) => storage.read(key: key); @override Future<void> write(String key, String value) => storage.write(key: key, value: value); }
Делаем экспорт
app_services\base\app_services\lib\app_services.dart
library; export 'src/app_secure_storage.dart';=
И последнее, объявляем flutter_secure_storage как зависимость в pubspec.yaml.
app_services\base\app_services\pubspec.yaml
name: app_services description: "Базовые сервисы для приложения" version: 0.0.1 publish_to: none environment: sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter # Зависимости для сервиса защищенного хранилища flutter_secure_storage: 9.2.4 # Пути к интерфейсам interfaces: path: ../../interfaces dev_dependencies: flutter_lints: 6.0.0
Шаг 4. Создаем реализацию для ОС Аврора
Дальше мы уже добавляем, например, реализацию для Авроры. Здесь в принципе то же самое, просто название будет уже не «base», а «aurora». Создаем два файла: имплементацию под Аврору и файл экспорта от сервисов.

Мы видим, что есть имплементация. И вот здесь уже используется секретный ключ, который мы передаем опционально. Обратите внимание, что мы здесь используем импорт flutter_secure_storage_aurora. В базовой реализации у нас импорта такого не было.
app_services\aurora\app_services\lib\src\app_secure_storage.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage_aurora/flutter_secure_storage_aurora.dart'; import 'package:interfaces/interfaces.dart'; final class AppSecureStorage implements ISecureStorage { AppSecureStorage({required this.secretKey}) { FlutterSecureStorageAurora.setSecret(secretKey ?? ''); // Устанавливаем ключ storage = FlutterSecureStorage(); } late final FlutterSecureStorage storage; @override final String? secretKey; @override String get name => 'AuroraAppSecureStorage'; @override Future<String?> read(String key) => storage.read(key: key); @override Future<void> write(String key, String value) => storage.write(key: key, value: value); }
У нас есть простая реализация. Мы используем ключ, задаем его во FlutterSecureStorageAurora.
Дальше добавляем экспорт. Мы никаким образом не связываем эти две библиотеки.
app_services\aurora\app_services\lib\app_services.dart
library; export 'src/app_secure_storage.dart';
И объявляем flutter_secure_storage_aurora как зависимость в pubspec.yaml.
app_services\aurora\app_services\pubspec.yaml
name: app_services description: "Аврора сервисы для приложения" version: 0.0.1 publish_to: none environment: sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter # Зависимости для сервиса защищенного хранилища flutter_secure_storage: ^8.0.0 flutter_secure_storage_aurora: git: url: https://gitlab.com/omprussia/flutter/flutter-community-plugins/flutter_secure_storage_aurora.git ref: aurora-0.5.3 # Пути к интерфейсам interfaces: path: ../../interfaces dev_dependencies: flutter_lints: 6.0.0
Таким образом у нас получается полностью изолированная реализация для Авроры, которая никаким образом не влияет на какую-то базовую версию
Шаг 5. Внедряем в основное приложение
Дальше мы в основном pubspec.yaml приложения объявляем интерфейсы, чтобы можно было их использовать.
pubspec.yaml
name: flutter_example_services description: "A new Flutter project." publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: sdk: ^3.9.0 dependencies: flutter: sdk: flutter ### основной сервис с интерфейсами interfaces: path: ./app_services/interfaces ### реализация сервисов ### ### В зависимости от платформы ### app_services: path: app_services/base/app_services ### Базовая реализация ### # path: app_services/aurora/app_services ### Аврора реализация ### dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0
Затем берем путь к AppServices и видим, что пу��и два. Первый — к базовым сервисам, и второй — к Аврора-сервисам. Один закомментирован, один нет. К сожалению, pubspec не дает создавать conditional imports.
Далее создаем псевдо-DI, где мы объявляем интерфейс ISecureStorage , реализуем этот интерфейс AppSecureStorage и передаем опционально секретный ключ. И мы помним, что если мы используем базовые версии, то этот секретный ключ мы не используем. Если это версия для Авроры или, например, для Huawei, то секретный ключ нужен.
lib\di.dart
import 'package:app_services/app_services.dart'; import 'package:interfaces/interfaces.dart'; final class Di { late final ISecureStorage secureStorage; void init() { secureStorage = AppSecureStorage(secretKey: 'secretKey'); } }
Важный момент: у нас нигде нет импорта flutter_secure_storage. Даже на какую-то определенную версию app_service. У нас есть некий интерфейс iSecureStorage и его некая реализация.
Основное приложение абсолютно не знает, чем оно пользуется, каким сервисом. Объявляем этот DI в main.dartи выводим на экране имя, базовое или «Аврора». Запускаем приложение и видим, что используем BaseAppSecureStorage, то есть базовую версию защищенного хранилища.
lib\main.dart
import 'package:flutter/material.dart'; import 'package:flutter_example_services/di.dart'; void main() { final di = Di()..init(); runApp(MyApp(di: di)); } class MyApp extends StatelessWidget { final Di di; const MyApp({super.key, required this.di}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold(body: Center(child: Text('Используем сервисы: ${di.secureStorage.name}'))), ); } }
Запускаем и видим, что приложение использует базовые сервисы.

Перейдем в pubspec, закомментируем базовую версию и раскомментируем Аврора-версию.
name: flutter_example_services description: "A new Flutter project." publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: sdk: ^3.9.0 dependencies: flutter: sdk: flutter ### основной сервис с интерфейсами interfaces: path: ./app_services/interfaces ### реализация сервисов ### ### В зависимости от платформы ### app_services: # path: app_services/base/app_services ### Базовая реализация ### path: app_services/aurora/app_services ### Аврора реализация ### dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0
Запускаем, и видим, что у нас появляется АuroraAppSecureStorage.

Шаг 6. Автоматизация
Мы как разработчики, конечно, скажем, что постоянно что-то там переключать — так себе. Где-то что-то комментировать, раскомментировать. Это все можно обойти легко.
Для упрощения у нас есть простой скрип на YQ. Он просто берет параметр TYPE, который вы задаете изначально при старте, и меняет строчку в pubspec.yaml.
#!/bin/bash TYPE=$1 if [ -z "$TYPE" ]; then echo "Error: TYPE is not set. Please provide a value." exit 1 fi yq -i '.dependencies.app_services.path = "app_services/'"$TYPE"'/app_services"' pubspec.yaml
Также вы можете легко внедрить его в CI, и у вас будет автоматически собираться ваше приложение с Аврора-сервисами, с Huawei-сервисами, с Telegram Mini App или что появится еще в будущем.
Шаг 7. Самопроверка
Самые такие главные три «НЕ», которые важно запомнить:
Ваш проект НЕ должен содержать в себе плагины. Никакие вообще. Их не должно быть в принципе.
В проекте НЕ должно быть проверки на текущую систему. Наприме��:
if(isHarmony){}Все проверки (и все реализации) должны быть внутри реализации.
Самое важное, НЕ должно быть никаких реализаций. Вы должны использовать только интерфейсы. Например, вот у нас есть интерфейс
iSecureStorage, и мы его только используем, больше ничего.
У нас есть Flutter Friflex Starter. Здесь мы с ребятами реализовали данную схему. Вы можете посмотреть, как это реализовано в целом. Этот подход нам сейчас очень помогает портировать приложения на различные системы. Я очень рекомендую хотя бы посмотреть. Если есть пожелания, можно делать pull-request, мы их с радостью рассмотрим.
