Привет, Хабр! Меня зовут Юра Петров, я руководитель отдела разработки компании 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, мы их с радостью рассмотрим.