Создаем federated plugin для Flutter-проекта

60080dfe970d38e3b6ae90bd354f1a2e.jpg

Всем привет! Это Мурат Насиров, Flutter-разработчик в Friflex. Мы разрабатываем высоконагруженные мобильные приложения для бизнеса и специализируемся на Flutter. В этой статье я рассказываю о том, как создать federated plugin для Flutter-проектов.

В мае 2022 года на Google I/O был представлен урок по созданию federated plugin в Flutter. Federated plugin — это способ разделения функционала в рамках одного плагина на разные платформы. Он позволяет сегрегировать функционал на зоны ответственности для каждой из платформ. 

К примеру, если мы создаем плагин для работы с bluetooth, тогда нужно будет создавать пакеты отдельно для каждой платформы, то есть: flutter_bluetooth (как пакет flutter), flutter_bluetooth_android, flutter_bluetooth_ios и flutter_bluetooth_platform_interface (интерфейс для работы с платформами). 

Создавая federated plugins для всех платформ, разработчики могут использовать только те из них, которые необходимы. 

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

Структура взаимодействия пакетов внутри federated plugin:

Структура взаимодействия пакетов внутри federated plugin

Структура взаимодействия пакетов внутри federated plugin

  • app-facing package — пакет, в котором описывается API на языке Dart для взаимодействия сplatform interface package. То есть это тот самый flutter_bluetooth, который мы прописываем в pubspec.yaml, когда хотим получить функционал библиотеки со всеми платформенными плагинами вместе. Таким образом, этот app-facing package зависит от platform interface package и platform packages;

  • platform interface package — пакет-связка между app-facing package и platform packages. В пакете описывается интерфейс плагина. Как и при создании обычного плагина, объявляются функции, которые будут использованы для вызова платформенного кода. В этом пакете применяется зависимость plugin_platform_interface, помогающая описать интерфейс, который будет использоваться в platform packages;

  • platform packages — пакеты, представляющие платформенную реализацию методов из абстракции platform interface package

Независимость каждого компонента упрощает настройку и отладку кода. Разработчикам, работающим с Android и iOS, не нужно знать о прогрессе или особенностях реализации платформы друг друга. Нужно лишь применить методы из интерфейса, которые описаны в platform interface package.

Создание federated plugin

Идея создавать независимые плагины для каждой из платформ появилась еще в 2019 году Однако по сей день можно лишь использовать команду для создания стандартного плагина:

 flutter create --org plugin --template=plugin --platforms=android,linux platform_info

Со временем сообщество предложило свои реализации этого подхода, из которых рабочая, как мне известно, только Very Good Flutter Plugin. Это решение со своими нюансами, нужно убирать немало лишнего и подпиливать до нужной кондиции. Поэтому, мне кажется, будет проще создать federated plugin самим.

Используем команду выше в удобной папке, где будет создан плагин. Так как в команде применяются платформы android и linux, всего понадобится создать четыре папки: platform_info, platform_info_android, platform_info_linux, platform_info_platform_interface

В папку platform_info_android перенесем папку android, в папку platform_info_linux — папку linux, а в папку platform_info — папку example. Во все четыре папки нужно скопировать папку lib из корня (и test, если нужны тесты), а также pubspec.yaml, analysis_options.yaml, .gitignore и README.md (по желанию). Из корня удаляем все, кроме четырех папок, папки .idea (если вы пользуетесь IntelliJ IDEA/Android Studio), .gitignore и README.md (по желанию). Должна получиться примерно такая структура:

Структура проектов после размещения файлов

Структура проектов после размещения файлов

Настройка platform interface

Теперь настроим структуру и код в каждой папке. Начнём с platform_info_platform_interface. В pubspec.yaml вставляем с заменой код:

name: platform_info_platform_interface
description: A common platform interface for the platform_info plugin.
version: 0.1.0
publish_to: none

environment:
 sdk: ">=3.0.0 <4.0.0"

dependencies:
 plugin_platform_interface: ^2.1.6

Из папки lib удаляем все, кроме platform_info_platform_interface.dart, в котором чуть меняем содержимое:

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

abstract class PlatformInfoPlatform extends PlatformInterface {
 PlatformInfoPlatform() : super(token: _token);

 static final Object _token = Object();

 static PlatformInfoPlatform _instance = _PlaceholderImplementation();

 /// Стандартный [instance] текущего класса.
 ///
 /// По умолчанию [_PlaceholderImplementation].
 static PlatformInfoPlatform get instance => _instance;

 /// Имплементация текущего [instance] на определенной платформе.
 ///
 /// В коде платформы должна быть реализована функция, определяющая [instance].
 static set instance(PlatformInfoPlatform instance) {
   PlatformInterface.verifyToken(instance, _token);
   _instance = instance;
 }

 Future getPlatformVersion() {
   throw UnimplementedError('platformVersion() has not been implemented.');
 }
}

class _PlaceholderImplementation extends PlatformInfoPlatform {}

Настройка Android-платформы

Переходим к настройке platform_info_android. В pubspec.yaml меняем все на:

name: platform_info_android
description: Android implementation of the platform_info plugin.
version: 0.1.0
publish_to: none

environment:
 sdk: ">=3.0.0 <4.0.0"

flutter:
 plugin:
   implements: platform_info
   platforms:
     android:
       package: plugin.platform_info
       pluginClass: PlatformInfoPlugin #Главный класс Android кода
       dartPluginClass: PlatformInfoAndroid #Главный класс Dart кода

dependencies:
 flutter:
   sdk: flutter
 platform_info_platform_interface:
   path: ../platform_info_platform_interface

dev_dependencies:
 flutter_lints: ^2.0.0

Удаляем все из platform_info_android/lib, создаем файл platform_info_android.dart и добавляем:

import 'package:flutter/services.dart';
import 'package:platform_info_platform_interface/platform_info_platform_interface.dart';

class PlatformInfoAndroid extends PlatformInfoPlatform {
 static const MethodChannel _channel = MethodChannel('platform_info_android');

 static void registerWith() {
   PlatformInfoPlatform.instance = PlatformInfoAndroid();
 }

 @override
 Future getPlatformVersion() {
   return _channel.invokeMethod('getPlatformVersion');
 }
}

На стороне Android нужно просто поменять название платформенного канала. В platform_info_android/android/src/main/kotlin/plugin/platform_info/PlatformInfoPlugin.kt указываем с заменой:

channel = MethodChannel(flutterPluginBinding.binaryMessenger, "platform_info_android")

Настройка Linux-платформы

Порядок, теперь переходим к настройке platform_info_linux. В pubspec.yaml указываем:

name: platform_info_linux
description: Linux implementation of the platform_info plugin.
version: 0.1.0
publish_to: none

environment:
 sdk: ">=3.0.0 <4.0.0"

flutter:
 plugin:
   implements: platform_info
   platforms:
     linux:
       pluginClass: PlatformInfoPlugin #Главный класс Linux кода
       dartPluginClass: PlatformInfoLinux #Главный класс Dart кода

dependencies:
 flutter:
   sdk: flutter
 platform_info_platform_interface:
   path: ../platform_info_platform_interface

dev_dependencies:
 flutter_lints: ^2.0.0

Также опустошаем все из platform_info_linux/lib, создаем файл platform_info_linux.dart и добавляем:

import 'package:flutter/services.dart';
import 'package:platform_info_platform_interface/platform_info_platform_interface.dart';

class PlatformInfoLinux extends PlatformInfoPlatform {
 static const MethodChannel _channel = MethodChannel('platform_info_linux');

 static void registerWith() {
   PlatformInfoPlatform.instance = PlatformInfoLinux();
 }

 @override
 Future getPlatformVersion() {
   return _channel.invokeMethod('getPlatformVersion');
 }
}

В platform_info/platform_info_linux/linux/platform_info_plugin.cc в методе platform_info_plugin_register_with_registrar также заменяем channel:

g_autoptr(FlMethodChannel) channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), "platform_info_linux", FL_METHOD_CODEC(codec));

Также нужно в platform_info/platform_info_linux/linux/include/platform_info_linux/ переместить файл platform_info_plugin.h, затем в platform_info/platform_info_linux/linux/platform_info_plugin.cc и platform_info/platform_info_linux/linux/platform_info_plugin_private.h заменить

#include "include/platform_info/platform_info_plugin.h"

на

#include "include/platform_info_linux/platform_info_plugin.h"

А в platform_info/platform_info_linux/linux/CMakeLists.txt заменить

# Project-level configuration.
set(PROJECT_NAME "platform_info")
project(${PROJECT_NAME} LANGUAGES CXX)

# This value is used when generating builds using this plugin, so it must
# not be changed.
set(PLUGIN_NAME "platform_info_plugin")

на

# Project-level configuration.
set(PROJECT_NAME "platform_info_linux")
project(${PROJECT_NAME} LANGUAGES CXX)

# This value is used when generating builds using this plugin, so it must
# not be changed.
set(PLUGIN_NAME "${PROJECT_NAME}_plugin")

Настройка Flutter-пакета для работы с платформами

Наконец, настроим пакет platform_info. Он будет подтягивать все платформенные реализации. В pubspec.yaml указываем:

name: platform_info
description: Flutter package of the platform_info plugin.
version: 0.1.0
publish_to: none

environment:
 sdk: ">=3.0.0 <4.0.0"

flutter:
 plugin:
   platforms:
     android:
       default_package: platform_info_android
     linux:
       default_package: platform_info_linux

dependencies:
 flutter:
   sdk: flutter
 platform_info_android:
   path: ../platform_info_android
 platform_info_linux:
   path: ../platform_info_linux
 platform_info_platform_interface:
   path: ../platform_info_platform_interface

dev_dependencies:
 flutter_lints: ^2.0.0

Из platform_info/lib также все удаляем и создаем файл platform_info.dart. А затем пишем:

import 'package:platform_info_platform_interface/platform_info_platform_interface.dart';

class PlatformInfo {
 static PlatformInfoPlatform get _platform => PlatformInfoPlatform.instance;

 static Future getPlatformVersion() async {
   return _platform.getPlatformVersion();
 }
}

Настройка example

Немного меняем platform_info/platform_info/example/lib/main.dart:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:platform_info/platform_info.dart';

void main() {
 runApp(const MyApp());
}

class MyApp extends StatefulWidget {
 const MyApp({super.key});

 @override
 State createState() => _MyAppState();
}

class _MyAppState extends State {
 String _platformVersion = 'Неизвестно';

 @override
 void initState() {
   super.initState();
   initPlatformState();
 }

 /// Так как платформенные вызовы асинхронны, мы ожидаем получения информации.
 Future initPlatformState() async {
   String platformVersion;
   // Платформенный вызов может завершиться с ошибкой, поэтому здесь используется
   // блок try/on PlatformException.
   try {
     platformVersion =
         await PlatformInfo.getPlatformVersion() ?? 'Неизвестная версия платформы';
   } on PlatformException {
     platformVersion = 'Не удалось получить версию платформы.';
   }
  
   // Если виджет был удален из дерева во время выполнения асинхронной функции
   // - выходим из нее.
   if (!mounted) return;

   setState(() {
     _platformVersion = platformVersion;
   });
 }

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     home: Scaffold(
       appBar: AppBar(
         title: const Text('Plugin example app'),
       ),
       body: Center(
         child: Text('Запущено на: $_platformVersion\n'),
       ),
     ),
   );
 }
}

А из platform_info/platform_info/example/android/settings.gradle, platform_info/platform_info/example/android/app/build.gradle и platform_info/platform_info/example/android/build.gradle удаляем, если есть:

package platform_info.example.android.app

Теперь запускаем Android:

bf7295b925813bd2cb7efbca962fbabd.jpg

И попробуем запустить Linux:

f01542d8a40f58e17e9ec35cd372f6b4.jpg

Вот и все:) Это решение отлично подходит для работы с любой платформой. Разница лишь в специфике, как, например, с linux-частью. Проект шаблона доступен на GitHub. Делитесь впечатлениями об этом решении, что показалось сложным, где нужны уточнения. Спасибо, что дочитали!

© Habrahabr.ru