Доброго времени суток! Flutter, имеет большое коммьюнити, огромное множество пакетов и плагинов, но всего не предусмотришь и, рано или поздно, разработчик сталкивается с задачей написания своего плагина, и сегодня рассмотрим как это сделать. Для примера возьмем несложный, но включающий многие аспекты разработки, плагин шаринга из приложения.
Создается новый плагин интуитивно понятно и никаких затруднений не вызывает, нужно в меню IDE выбрать создание нового flutter-проекта, из шаблонов взять “Flutter plugin” и действовать по инструкции (ввести название, описание, имя пакета и т.п.). Ошибиться здесь вряд ли получится — все поля валидируются и если что-то не так, то будет выведена поясняющая ошибка.
Вариант без использования шаблонов в IDE — консольной командой вида:
flutter create --org com.example --template=plugin -a kotlin hello_world
Где com.example
— организация, после -a
— язык под android (java или kotlin) и имя плагина.
Итак, создаем и открываем класс плагина plugin_name.dart
(ошибиться будет сложно он в директории lib один):
1 2 3 4 5 6 7 8 9 |
class FlutterShareExample { static const MethodChannel _channel = const MethodChannel('flutter_share_example'); static Future<String> get platformVersion async { final String version = await _channel.invokeMethod('getPlatformVersion'); return version; } } |
Для примера нам любезно предоставили код простого плагина, который получает версию платформы. Как мы можем видеть общение между dart и нативной частью осуществляется при помощи MethodChannel
, в конструктор класс принимает имя канала в который будет отправлять сообщения или прослушивать, при необходимости. То есть канал работает в обе стороны, забегая вперед скажу, что на стороне нативной части, отправкой/прослушиванием занимается класс с тем же именем и похожим интерфейсом.
И как показано в примере, для отправки сообщения (вызова определенного метода с необходимыми аргументами) мы выполняем invokeMethod()
и ждем результат, (также можно использовать invokeListMethod()
или invokeMapMethod()
, если заранее известно, что возвращаемый результат List
или Map
соответственно).
Перепишем этот код, чтобы он делал то что нам нужно:
1 2 3 4 5 6 7 8 |
class FlutterShareExample { static const MethodChannel _channel = const MethodChannel('flutter_share_example'); static Future share(String title, String text) async { await _channel.invokeMethod('share', {'title': title, 'text': text}); } } |
Ничего сложного, мы просто передаем в канал, что мы хотим вызвать метод share
и передаем аргументы title
и text
.
Теперь нам осталось реализовать функционал на стороне платформы. Для того, чтобы IDE правильно работала с проектом необходимо открыть Android-модуль. Это можно сделать кликнув правой кнопкой мыши по директории android и выбрав пункт Flutter -> Open android module
. Либо просто открыть через меню File -> Open
. Обратите внимание, что необходимо открывать модуль не самого плагина, а билдящегося проекта, в нашем случае example ({plugin_directory}/example/android
), в противном случае классы не будут видеть зависимостей от Flutter SDK, что весьма неудобно.
На всякий случай прикладываю скрин из IDE с отображением пути до класса плагина. В случае если разработка ведется на Java путь будет тем же, кроме директории kotlin/java соответственно.
Итак, открываем класс плагина, если убрать все комментарии, то получаем следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class FlutterShareExamplePlugin: FlutterPlugin, MethodCallHandler { private lateinit var channel : MethodChannel companion object { @JvmStatic fun registerWith(registrar: Registrar) { val channel = MethodChannel(registrar.messenger(), "flutter_share_example") channel.setMethodCallHandler(FlutterShareExamplePlugin()) } } override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_share_example") channel.setMethodCallHandler(this) } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "getPlatformVersion") { result.success("Android ${android.os.Build.VERSION.RELEASE}") } else { result.notImplemented() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } } |
Методы onAttachedToEngine()
и статический registerWith()
делают одно и тоже. Статический по сути устаревший и оставлен для совместимости с версией Flutter ниже 1.12, если поддерживать его не планируется, то можно смело убирать, что я и сделаю.
После создания экземпляра MethodChannel
ему устанавливается обработчик, реализующий интерфейс MethodCallHandler
в котором по сути один метод void onMethodCall(MethodCall call, Result result)
, в данном случае его реализует сам класс плагина, но в крупных проектах, возможно удобнее будет его вынести в отдельный класс.
По вызову этого метода мы должны выполнить необходимую работу в зависимости от переданного метода и вернуть результат вызвав success()
(или error()
если что-то не так) у экземпляра Result
. Сейчас выполняется код из примера, давайте заменим, на то что нужно нам.
В документации Android есть пример, как выполнить шаринг.
1 2 3 4 5 6 7 8 |
val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, "This is my text to send.") type = "text/plain" } val shareIntent = Intent.createChooser(sendIntent, null) startActivity(shareIntent) |
Мы можем взять этот код почти без изменений и добавить в обработчик, но вызываемый startActivity()
это метод Activity
, которой у нас пока нет. Чтобы ее получить плагин должен реализовывать ActivityAware
, добавим эту реализацию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class FlutterShareExamplePlugin : FlutterPlugin, MethodCallHandler, ActivityAware { ... private var _activity : Activity? = null ... override fun onAttachedToActivity(binding: ActivityPluginBinding) { _activity = binding.activity } override fun onDetachedFromActivity() { _activity = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { _activity = binding.activity } override fun onDetachedFromActivityForConfigChanges() { _activity = null } } |
То есть по сути мы реализуем методы добавления, удаления плагина к activity и передобавления при изменении конфигурации. Добавим теперь обработку вызова метода share
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "share" -> { if (_activity != null) { val title = call.argument<String>("title") val text = call.argument<String>("text") if (title != null && text != null) { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, text) type = "text/plain" } val shareIntent = Intent.createChooser(sendIntent, title) _activity!!.startActivity(shareIntent) } else { result.error("INVALID_ARGS", "Invalid arguments", null) } } else { result.error("OPERATION_ERROR", "Activity is not defined", null) } } else -> result.notImplemented() } } |
Все просто, код из документации перенесся практически без изменений, получение переданных аргументов также не составляет сложностей, все есть в переданном нам экземпляре MethodCall
.
Плагин готов, все что нам остается сделать, это добавить в проект example вызов метода share()
и проверить работоспособность. В нашем случае будет достаточно текстового поля и кнопки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class _MyAppState extends State<MyApp> { TextEditingController _controller; @override void initState() { super.initState(); _controller = TextEditingController(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Share plugin example app'), ), body: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextField(controller: _controller), OutlineButton( child: Text('Share'), onPressed: () { FlutterShareExample.share( 'Example share', _controller.text); }, ) ]), ), ), ), ); } } |
Как будет выглядеть непосредственно шаринг зависит от версии Android и ее оболочки. В моем случае (в эмуляторе) это выглядит так:
Итак, не смотря на то, что статья получилась довольно объемной, с казалось бы большим количеством кода, на деле написание такого плагина занимает совершенно не много времени, но может помочь реализовать задуманное или позволит отказаться от какого-то стороннего большого и тяжелого плагина в пользу своего маленького, который будет реализовывать только необходимый функционал.
Всем удачи в разработке, спасибо, что дочитали до конца :)