Всем привет! Это Мурат Насиров, 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:
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<String?> 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<String?> 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<String?> 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<String?> 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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Неизвестно';
@override
void initState() {
super.initState();
initPlatformState();
}
/// Так как платформенные вызовы асинхронны, мы ожидаем получения информации.
Future<void> 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:
И попробуем запустить Linux:
Вот и все:) Это решение отлично подходит для работы с любой платформой. Разница лишь в специфике, как, например, с linux-частью. Проект шаблона доступен на GitHub. Делитесь впечатлениями об этом решении, что показалось сложным, где нужны уточнения. Спасибо, что дочитали!