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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Всем привет! Это Мурат Насиров, 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<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. Делитесь впечатлениями об этом решении, что показалось сложным, где нужны уточнения. Спасибо, что дочитали!

Источник: https://habr.com/ru/companies/friflex/articles/780956/


Интересные статьи

Интересные статьи

Весной этого года мы выпустили Squadus — цифровое рабочее пространство для компаний любого масштаба. Решение позволяет общаться в чатах, проводить конференции, совместно работать над документами и авт...
Привет! Продолжаю выкладывать перевод статьи, которую я использовал как основу для реализации социального функционала в нашем проекте Dom24x7, где люди могут общаться друг с другом, решать возникающие...
React — это библиотека JavaScript для создания пользовательского интерфейса. Это официальное определение React. Но что если вы не знаете, что такое JavaScript? Что если вы не разработч...
Часть 1. Всем привет! Продолжаем серию туториалов по разработке веб-приложения на Reflex. В этой части мы добавим возможность выполнять различные манипуляции со списком задач. ...
Иногда простые вещи очень утомляют, особенно когда их необходимо делать постоянно. Одна из таких вещей при работе с фреймворком Moxy — это добавление стратегий к функциям. Для ускорения этого пр...