- Введение
- Взаимодействие с нативной платформой
- Каналы платформы
- Pigeon
- Создание плагина
- Шаблон плагина
- Использование Pigeon
- Установка пакета pigeon
- Определение API использования приложений
- Предостережения и ограничения
- Фьючерсы
- Импорт запрещен
- Поддерживаемые типы данных
- Перечисления пока не поддерживаются для примитивных типов возврата
- Генерации поддерживаются, но могут использоваться только с nullable типами
- Генерация кода
- Запуск генератора
- Dart
- Java
- Swift
- Objective-C
- Понимание сгенерированного кода
- Реализация нативной платформы
- Android
- iOS
- Использование плагина
- Сравнение каналов Pigeon и методов
- Подведение итогов
Введение
В этом руководстве мы создадим плагин для Flutter, нацеленный на платформы Android и iOS, используя пакет Pigeon.
Поскольку Flutter — это UI-фреймворк, взаимодействие с родной платформой не всегда нужно и не всегда используется. И обычно, когда нам это нужно, существует масса пакетов, которые уже делают это для конкретных случаев использования.
Но бывают случаи, когда функциональность, которую может предложить нативная платформа, еще не доступна в публичном пакете на pub.dev. В этих случаях все зависит от нас! Нам нужно написать немного кода Dart на стороне Flutter для вызова платформы, а также код нативной платформы для вызова нужной нам функциональности (и это один раз для каждой платформы, которую поддерживает ваше приложение!)
Взаимодействие с нативной платформой
Каналы платформы
Одним из способов взаимодействия с нативной платформой во Flutter является использование MethodChannel
. В предыдущем посте мы рассказали о том, как создать плагин или вызвать код нативной платформы во Flutter с помощью каналов методов.
Хотя использование MethodChannel
относительно простое, на самом деле оно может отнимать много времени; при вызове методов через MethodChannel
вы можете передавать только аргументы простых типов, таких как int
, double
, bool
или String
, а также карты списков с такими типами в качестве значений. С полной поддержкой типов данных вы можете ознакомиться здесь. Если вам нужно передать сложный объект в качестве аргумента, вам потребуется логика для его разбора, скажем, в карту строковых ключей к их значениям. Кроме того, то же самое нужно сделать для любых данных, которые возвращаются из родной платформы.
Мы можем обойти необходимость внедрения логики разбора, используя пакет, такой как json_serializable
для разбора данных в JSON и обратно, чтобы сэкономить время. Однако вам нужно будет убедиться, что родные платформы возвращают данные именно в том формате, который вы ожидаете, и наоборот. В противном случае парсинг будет неудачным.
Pigeon
Pigeon — это пакет генератора кода, который генерирует весь код, необходимый для взаимодействия между Flutter и любой хост-платформой. Все, что вам нужно сделать, это определить API. Это удобно, потому что вам не нужно беспокоиться о логике парсинга, а связь гарантированно безопасна для типов.
По состоянию на 10 июля 2022 года Pigeon поддерживает только Android и iOS и генерирует код на Java и Objective-C (Swift является экспериментальным) соответственно. Сгенерированный код по-прежнему доступен на Kotlin или Swift. Существует также экспериментальная поддержка Windows с C++.
Создание плагина
В этом посте мы создадим простой плагин с помощью Pigeon. То, что мы создадим, будет идентично (фальшивому) плагину использования приложений, который мы создали ранее, чтобы мы могли сравнить результаты.
Плагин прост; он должен возвращать список всех приложений и их использование, а также поддерживать возможность устанавливать временные ограничения на определенные приложения. Мы не будем реализовывать эту функциональность на нативной стороне, поскольку это выходит за рамки данного руководства, а вместо этого будем возвращать фиктивные данные с нативной стороны.
Шаблон плагина
Для начала мы создадим плагин Flutter, используя flutter create
с шаблоном плагина.
flutter create --org dev.dartling --template=plugin --platforms=android,ios app_usage_pigeon
В результате будет сгенерирован код плагина, а также пример проекта, использующего этот плагин. По умолчанию сгенерированный код для Android будет на Kotlin, а для iOS — на Swift, но вы можете указать Java или Objective-C с помощью флагов -a и -i соответственно. (-a java
и/или -i objc
).
В шаблон плагина включено довольно много кода. Если вам интересно, мы более подробно рассмотрим код на Dart, Kotlin и Swift в этом посте. Для контекста этого руководства достаточно знать следующее:
На стороне Dart есть три класса:
Сгенерированный код на языке Kotlin — AppUsagePlugin.kt
, который использует каналы методов. Он определен в нашем pubspec.yaml
как класс плагина для платформы Android, поэтому он нам еще понадобится, хотя позже мы внесем в него некоторые изменения. То же самое относится и к коду Swift, который включает SwiftAppUsagePlugin.swift
, а также AppUsagePlugin.h
и AppUsagePlugin.m
.
В этом руководстве мы напишем реализацию AppUsagePlatform
, которая использует Pigeon, а не каналы методов. Мы можем удалить MethodChannelAppUsage
, так как он нам не понадобится.
Примечание: новый шаблон плагина, использующий PlatformInterface
, вводит довольно много кода, который вам может и не понадобиться, если вы просто хотите вызвать некоторый нативный код в вашем приложении. Если вы хотите, вы можете использовать Pigeon без создания плагина или отдельного пакета, но именно это мы и будем делать в этом руководстве.
Использование Pigeon
Установка пакета pigeon
Давайте установим пакет:
flutter pub add --dev pigeon
Или же добавьте это в ваш pubspec.yaml
:
dev_dependencies:
pigeon: ^3.2.3
Определение API использования приложений
Принцип работы Pigeon довольно прост: мы определяем наш API в классе Dart вне папки lib
(поскольку Pigeon является зависимостью dev). Класс API должен быть абстрактным классом с декоратором @HostApi()
, а его методы должны иметь декоратор @async
.
Давайте определим наш App Usage API в новом каталоге с именем pigeons
:
// pigeons/app_usage_api.dart
import 'package:pigeon/pigeon.dart';
enum State { success, error }
class StateResult {
final State state;
final String message;
StateResult(this.state, this.message);
}
class UsedApp {
final String id;
final String name;
final int minutesUsed;
UsedApp(this.id, this.name, this.minutesUsed);
}
@HostApi()
abstract class AppUsageApi {
@async
String? getPlatformVersion();
@async
List<UsedApp> getApps();
@async
StateResult setAppTimeLimit(String appId, int minutesUsed);
}
Предостережения и ограничения
Определение API было относительно простым, но есть несколько моментов, о которых следует упомянуть:
Фьючерсы
Нам не нужно указывать возвращаемые значения как Future
s, но в сгенерированном коде они будут таковыми. Так getPlatformVersion
на самом деле вернет Future<String?>
в сгенерированном коде Dart.
Импорт запрещен
Никакой импорт, кроме package:pigeon/pigeon.dart
, не разрешен. Это означает, что КАЖДЫЙ класс модели должен быть определен в этом файле Pigeon API.
Поддерживаемые типы данных
Как уже упоминалось, поддерживаются только простые JSON-подобные значения. Это означает, что мы не можем использовать полезные типы Dart, такие как DateTime
или Duration
. Это означает, что нам может понадобиться дополнительная логика отображения для преобразования модели Pigeon в модель, которую мы хотим использовать в приложении. Для minutesUsed
в UsedApp
нам нужно будет вручную создать Duration
из минут, хотя было бы неплохо иметь это в виде Duration
изначально.
Перечисления пока не поддерживаются для примитивных типов возврата
Мы не можем вернуть перечисление из метода, но можем иметь перечисление в качестве параметра метода. Мы все еще можем возвращать перечисления, но только если мы обернем их в отдельный класс, как показано ниже.
// Not valid, enums cannot be returned.
enum ResultState { success, error }
@HostApi()
abstract class AppUsageApi {
@async
ResultState getState();
}
// Valid, enums can be method parameters and fields of returned objects.
enum ResultState { success, error }
class ApiResult {
final ResultState state;
final String message;
ApiResult(this.state, this.message);
}
@HostApi()
abstract class AppUsageApi {
@async
ApiResult getResult();
@async
void setState(ResultState state);
}
Генерации поддерживаются, но могут использоваться только с nullable типами
Мы все еще можем определить их как ненулевые в нашем определении HostApi
, например, List<Something>
, но сгенерированный класс Dart будет иметь List<Something?>
вместо этого.
Генерация кода
Запуск генератора
После определения API мы можем сгенерировать код с помощью команды flutter pub run pigeon
. Эта команда требует довольно много аргументов:
flutter pub run pigeon
--input pigeons/app_usage_api.dart
--dart_out lib/app_usage_api.dart
--java_package "dev.dartling.app_usage"
--java_out android/src/main/java/dev/dartling/app_usage/AppUsage.java
--experimental_swift_out ios/Classes/AppUsage.swift
Мы сохраним эту команду в файле pigeon.sh
, чтобы ее было легко найти и запустить в будущем.
Мы будем использовать Swift, который пока поддерживает экспериментальный режим, а не Objective-C, но для Objective-C мы можем просто отказаться от аргумента experimental_swift_out
в пользу этих трех:
--objc_header_out ios/Classes/AppUsageApi.h
--objc_source_out ios/Classes/AppUsageApi.m
--objc_prefix FLT
Dart
Аргумент input
должен быть файлом, в котором мы определили API, а dart_out
должен находиться в папке lib
, поскольку именно этот код мы будем использовать в нашем приложении.
Java
java_package
— это полное имя пакета, в данном случае dev.dartling.app_usage
, а java_out
— это путь к Java-файлу, который будет сгенерирован.
Примечание: Убедитесь, что имя сгенерированного Java-класса НЕ совпадает с именем Pigeon HostApi
. В нашем случае сгенерированный Java-класс будет AppUsage
и будет включать вложенный интерфейс public AppUsageApi
, взятый из имени класса HostApi
, определенного в Dart. Если мы используем одинаковые имена (что я и сделал изначально!), компиляция завершится неудачей из-за дублирования имен.
Примечание: если ваш шаблон плагина использует Kotlin, как мы сделали в данном случае, вам нужно будет создать каталог java/dev/dartling/app_usage
вручную под src/main
, так как только kotlin/dev/dartling/app_usage
был сгенерирован как часть шаблона плагина.
Swift
experimental_swift_out
— это путь к Swift-файлу, который будет сгенерирован.
Objective-C
Аргументы objc_header_out
и objc_source_out
определяют сгенерированные файлы на стороне Objective-C, а objc_prefix
является необязательным и определяет префикс сгенерированных имен классов.
Понимание сгенерированного кода
Код, генерируемый Pigeon после выполнения команды flutter pub run pigeon
, не является тем, на что мы должны часто смотреть. Все, что нам нужно знать, это то, что класс Java будет иметь интерфейс AppUsageApi
, который должен реализовать наш класс реализации; это может быть как в Java, так и в Kotlin. В Objective-C будет эквивалент протокола FLTAppUsageApi
(обратите внимание на префикс FLT
, который является аргументом при запуске генератора), а в Swift — протокол AppUsageApi
.
Реализация нативной платформы
Android
У нас есть наш интерфейс, теперь все, что нам нужно сделать, это реализовать его. Чтобы не усложнять ситуацию, мы просто используем существующий класс AppUsagePlugin
Kotlin для реализации AppUsageApi
, в дополнение к существующему интерфейсу FlutterPlugin
.
Вот полный текст класса:
// AppUsagePlugin.kt
class AppUsagePlugin : FlutterPlugin, AppUsageApi {
val usedApps: MutableList<UsedApp> = mutableListOf(
usedApp("com.reddit.app", "Reddit", 75),
usedApp("dev.hashnode.app", "Hashnode", 37),
usedApp("link.timelog.app", "Timelog", 25),
)
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
AppUsageApi.setup(flutterPluginBinding.binaryMessenger, this)
}
override fun getPlatformVersion(result: Result<String>?) {
result?.success("Android ${android.os.Build.VERSION.RELEASE}")
}
override fun getApps(result: Result<MutableList<UsedApp>>?) {
result?.success(usedApps);
}
override fun setAppTimeLimit(
appId: String,
durationInMinutes: Long,
result: Result<TimeLimitResult>?
) {
val stateResult = TimeLimitResult.Builder()
.setState(ResultState.success)
.setMessage("Timer of $durationInMinutes minutes set for app ID $appId")
.build()
result?.success(stateResult)
}
private fun usedApp(id: String, name: String, minutesUsed: Long): UsedApp {
return UsedApp.Builder()
.setId(id)
.setName(name)
.setMinutesUsed(minutesUsed)
.build();
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
AppUsageApi.setup(binding.binaryMessenger, null)
}
}
onAttachedToEngine
и onDetachedFromEngine
взяты из FlutterPlugin
. Ранее они настраивали работу для реализации канала метода. Теперь мы вызываем метод AppUsageApi#setup
, чтобы заставить его работать со сгенерированным Pigeon кодом.
Остальные три функции, которые мы переопределяем, относятся к интерфейсу AppUsageApi
. На самом деле это функции void
, и мы «возвращаем» результаты, используя result
, которая на самом деле была сгенерирована как nullable. Чтобы вернуть результат, мы просто используем result?.success(...)
, а если мы хотим бросить ошибку, мы можем использовать result?.error(...)
и передать Throwable
; это будет обернуто в PlatformException
на стороне Dart.
iOS
Очень похоже на Android, мы заставим существующий SwiftAppUsagePlugin
реализовать протокол AppUsageApi
в дополнение к FlutterPlugin
. Вместо result
у нас есть completion
, который мы вызываем, передавая результат в качестве аргумента. Мы также вносим некоторые изменения в register
, чтобы использовать статическую функцию AppUsageApiSetup#setUp
для настройки работы со сгенерированным файлом.
public class SwiftAppUsagePlugin: NSObject, FlutterPlugin, AppUsageApi {
var usedApps = [
UsedApp(id: "com.reddit.app", name: "Reddit", minutesUsed: 75),
UsedApp(id: "dev.hashnode.app", name: "Hashnode", minutesUsed:37),
UsedApp(id: "link.timelog.app", name: "Timelog", minutesUsed: 25)
]
public static func register(with registrar: FlutterPluginRegistrar) {
let messenger : FlutterBinaryMessenger = registrar.messenger()
let api : AppUsageApi & NSObjectProtocol = SwiftAppUsagePlugin.init()
AppUsageApiSetup.setUp(binaryMessenger: messenger, api: api)
}
func getPlatformVersion(completion: @escaping (String?) -> Void) {
completion("iOS " + UIDevice.current.systemVersion)
}
func getApps(completion: @escaping ([UsedApp]) -> Void) {
completion(usedApps)
}
func setAppTimeLimit(appId: String, durationInMinutes: Int32, completion: @escaping (TimeLimitResult) -> Void) {
completion(TimeLimitResult(state: ResultState.success, message: "Timer of (durationInMinutes) minutes set for app ID (appId)"))
}
}
Примечание: Я столкнулся с некоторыми странными проблемами, когда сборка iOS иногда не удавалась из-за того, что AppUsageApi
не был найден в области видимости. Если вы столкнулись с такой же проблемой, быстрый хакерский способ — скопировать все в сгенерированном AppUsage.swift
в существующий файл SwiftAppUsagePlugin.swift
. После этого все должно работать! Если вы выясните, как/почему это происходит и как это исправить, пожалуйста, дайте мне знать в комментариях!
Использование плагина
Теперь у нас все готово. Реализация родной платформы завершена, и класс AppUsageApi
Dart можно использовать для взаимодействия с родными платформами.
Все, что нам нужно сделать, это создать экземпляр AppUsageApi
и вызвать его метод… но подождите, мы же создаем плагин! Мы не должны использовать AppUsageApi
напрямую (хотя могли бы!). Помните класс MethodChannelAppUsage
Dart, который мы удалили некоторое время назад? Нам нужно представить альтернативу, которая будет использовать Pigeon вместо каналов методов.
Во-первых, давайте убедимся, что все методы, которые являются частью нашего Pigeon HostApi
, также определены в нашем AppUsagePlatform
.
abstract class AppUsagePlatform extends PlatformInterface {
...
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
Future<List<UsedApp>> get apps async {
throw UnimplementedError('apps has not been implemented.');
}
Future<TimeLimitResult> setAppTimeLimit(String appId, Duration duration) async {
throw UnimplementedError('setAppTimeLimit() has not been implemented.');
}
}
Теперь наша реализация AppUsagePlatform
с помощью Pigeon очень проста. Мы просто будем вызывать методы AppUsageApi
, которые были сгенерированы Pigeon.
// app_usage_pigeon.dart
/// An implementation of [AppUsagePlatform] that uses Pigeon.
class PigeonAppUsage extends AppUsagePlatform {
final AppUsageApi _api = AppUsageApi();
@override
Future<String?> getPlatformVersion() {
return _api.getPlatformVersion();
}
@override
Future<List<UsedApp>> get apps {
return _api
.getApps()
.then((apps) => apps.where((e) => e != null).map((e) => e!).toList());
}
@override
Future<TimeLimitResult> setAppTimeLimit(String appId, Duration duration) async {
return _api.setAppTimeLimit(appId, duration.inMinutes);
}
}
Обратите внимание, что в apps
мы фильтруем нулевые значения и используем оператор !
, так как AppUsageApi#getApps()
возвращает List<UsedApp?>
, из-за текущих ограничений Pigeon.
Наконец, AppUsage
, наш основной класс плагина, также должен быть обновлен. Все, что он делает, это делегирует вызовы методов на AppUsagePlatform.instance
.
class AppUsage {
Future<String?> getPlatformVersion() {
return AppUsagePlatform.instance.getPlatformVersion();
}
Future<List<UsedApp>> get apps {
return AppUsagePlatform.instance.apps;
}
Future<TimeLimitResult> setAppTimeLimit(String appId, Duration duration) {
return AppUsagePlatform.instance.setAppTimeLimit(appId, duration);
}
}
И давайте не будем забывать, что AppUsagePlatform.instance
теперь должен возвращать экземпляр PigeonAppUsage
, а не MethodChannelAppUsage
:
abstract class AppUsagePlatform extends PlatformInterface {
...
static AppUsagePlatform _instance = PigeonAppUsage();
/// The default instance of [AppUsagePlatform] to use.
///
/// Defaults to [PigeonAppUsage].
static AppUsagePlatform get instance => _instance;
...
}
Я не буду делиться фрагментами кода пользовательского интерфейса и виджетов, но вы можете взглянуть на них здесь. Использовать плагин в любом приложении очень просто: мы инициализируем экземпляр AppUsage
и вызываем его методы, которые нам нужны.
Вот конечный результат:
Сравнение каналов Pigeon и методов
Мы создали почти идентичный плагин и пример приложения в предыдущей статье, поэтому мы можем сравнить Pigeon с использованием каналов методов.
В целом, Pigeon — это определенно улучшение. Нам нужно только один раз определить наш API и модели; сгенерированный Android/iOS будет включать эти модели за нас.
Нам также не придется беспокоиться о сериализации данных, которые мы хотим передать на сторону платформы, или десериализации данных, поступающих со стороны платформы, и наоборот для стороны платформы; нам не придется беспокоиться о десериализации данных, поступающих со стороны Dart, и сериализации данных, которые мы возвращаем на сторону Dart.
Благодаря этим двум пунктам нам потребовалось значительно меньше строк кода, чтобы написать плагин, используя Pigeon, а не каналы методов.
Подведение итогов
В этом руководстве мы представили Pigeon как способ упрощения взаимодействия с родными платформами и создали пользовательский плагин Flutter с реализациями для Android и iOS для вызова (ненастоящей) нативной функциональности, используя Pigeon вместо каналов методов.
Вы можете найти полный исходный код здесь.
Если вы нашли это полезным и хотите получать уведомления о будущих уроках, пожалуйста, подпишитесь, указав свой email здесь.