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

Самые такие главные три «НЕ», которые важно запомнить:

  1. Ваш проект НЕ должен содержать в себе плагины. Никакие вообще. Их не должно быть в принципе. 

  2. В проекте НЕ должно быть проверки на текущую систему. Например:

      if(isHarmony){}

  3. Все проверки (и все реализации) должны быть внутри реализации.

  4. Самое важное, НЕ должно быть никаких реализаций. Вы должны использовать только интерфейсы. Например, вот у нас есть интерфейс iSecureStorage, и мы его только используем, больше ничего.

У нас есть Flutter Friflex Starter. Здесь мы с ребятами реализовали данную схему. Вы можете посмотреть, как это реализовано в целом. Этот подход нам сейчас очень помогает портировать приложения на различные системы. Я очень рекомендую хотя бы посмотреть. Если есть пожелания, можно делать pull-request, мы их с радостью рассмотрим.

Комментарии (0)