
Привет! Меня зовут Даниил, я работаю в команде SDK в VK. Одно из направлений, которым занимается наша команда, — разработка SDK для авторизации через сервисы экосистемы VK. Он состоит из нескольких компонентов: авторизации, логина в один клик, шторки для входа с более удобным интерфейсом и поддержкой Mail и OK как провайдеров авторизации.
Мы давно задумывались о поддержке Flutter, поскольку это быстроразвивающаяся платформа, которой уже пользуется много клиентов. Было видно, что поддержка Flutter нужна клиентам, даже на фрилансовых биржах публиковали заказы на поддержку VK ID. Логичным поступком стало сделать официальное решение от VK, чтобы клиентам не приходилось делать одинаковую работу. В этой статье поделюсь опытом поддержки Flutter в нашем SDK. Статья будет полезна любому разработчику, который хочет добавить поддержку Flutter в свою библиотеку. Материал рассчитан на разработчиков, которые ничего не знают о Flutter и будут разбираться с ним с нуля. Приведены примеры кода только под Android, поскольку под iOS всё делается аналогично.
Предыстория
К моменту принятия решения о том, что нужна поддержка Flutter, у нас уже были готовые SDK под iOS и Android. Они были реализованы, протестированы и проверены в эксплуатации. Хотелось переиспользовать код, чтобы существенно сократить затраты на разработку. Кроме того, у команды не было знаний ни про Flutter, ни даже про Dart. Если бы мы стали писать SDK с нуля, то разработчикам пришлось бы потратить много времени на ознакомление и привыкание к тонкостям платформы, а это лишнее время и ресурсы, которые не хочется тратить, когда мы точно не знаем, сколько клиентов у нас будет. К счастью, Flutter позволяет обернуть нативный код, и в итоге пришлось написать только бриджи и публичный интерфейс, что значительно проще полноценной разработки. Для большинства мобильных библиотек это реальная возможность, поскольку пишется всё достаточно просто. Далее расскажу о тонкостях того, как это делается.
Планирование
Перед началом работы над новым SDK я составил план работ, по которым будет идти разработка. Хотелось понять, что именно мы будем делать, и оценить сроки. Получился такой список:

На схеме показаны этапы разработки и возможность их распараллеливания. Поскольку на SDK работало два разработчика — iOS и Android, — часть работ получилось распараллелить. Далее расскажу последовательно про каждый из этапов.
Разработка архитектуры
Наш новый SDK состоит их трёх частей:
Кода на Kotin, оборачивающего VK ID Android SDK и обрабатывающего взаимодействие между Flutter и Android SDK.
Кода на Swift, оборачивающего VK ID iOS SDK и реализующего взаимодействие между Flutter и iOS.
Кода на Dart, оборачивающего нативные SDK и предоставляющего публичный интерфейс.
В итоге архитектура выглядит так:

Чтобы это реализовать, SDK предоставляется как Plugin Package — это решение для пакетов, использующих платформозависимый код. Для создания Plugin Package под Android и iOS нужно выполнить команду:
flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin -i swift project_name
В результате будут созданы четыре модуля:
example с примером использования на Dart;
lib с кодом библиотеки на Dart;
android с кодом библиотеки на Kotlin;
ios с кодом библиотеки на Swift.
Настройка Code Quality
В сообществе Flutter не так много популярных инструментов для контроля качества кода. Поскольку мы делали достаточно простой проект, то решили использовать только dart format. Это инструмент, который следит за стилем кода. Если запускать его в git-хуке перед коммитом, то весь код будет отформатирован согласно официальному dart code style.
Согласование публичного интерфейса
Перед началом работ я описал публичный интерфейс, который хотелось сделать для SDK. Мы обсудили его, и в итоге сделали много доработок. Это полезный этап, особенно если над проектом работает несколько человек: позволяет «на берегу» обсудить вещи, которые будут затрагивать всех разработчиков. Также важно помнить, что проектам, которыми будут пользоваться другие люди, важно думать про обратную совместимость, и если выпустить в эксплуатацию, то удалить что-то из публичного интерфейса уже будет нельзя до следующей мажорной версии проекта.
Реализация фич
При разработке фич у нас уже был готовый публичный интерфейс, оставалось написать бридж на Dart и платформенном языке. Код достаточно простой, поэтому даже не зная языка другой платформы, можно было эффективно проверять написанное. Для того, чтобы погрузиться в Dart на уровне написания обёртки для SDK, нам было достаточно официальной документации Dart, Flutter и поиска ответов на StackOverflow. Прочитав парочку вступительных статей про синтаксис, мы начали писать код. Разбирались по ходу. Суммарно за всё время работы над проектом у меня ушло около недели на изучении документации, поскольку система виджетов Flutter относительно похожа на Compose, с которым я уже работал. Далее последовательно расскажу про создание бриджей во Flutter.
UI-фреймворк Flutter
UI-фреймворк Flutter напоминает Compose и Swift UI. Весь интерфейс представлен деревом виджетов, которые описываются в декларативном стиле. Можно написать свой виджет, что мы и сделали для компонентов интерфейса нашего SDK. Каждый виджет представляет из себя композицию базовых виджетов, которые используются для его создания в методе build
. Если виджету нужны состояния, то используется StatefulWidget
с состоянием, вынесенным в отдельный класс, а если состояние не нужно — то StatelessWidget
.
Состояние виджета
Поскольку в нашем SDK не самые простые UI-компоненты, им было необходимо состояние. Посмотрите на пример реализации виджета с состоянием:
class MyCounter extends StatefulWidget {
const MyCounter({super.key});
@override
State<MyCounter> createState() => _MyCounterState();
}
class _MyCounterState extends State<MyCounter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
TextButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text('Increment'),
)
],
);
}
}
Обратите внимание вот на что:
Виджет наследуется от класса
StatefulWidget
.В функции
build
задаётся внешний вид виджета с помощью стандартных компонентов.Состояние обновляется в лямбда-параметре метода
setState()
.Класс состояния наследуется от стандартного класса
State<Название виджета>
.Состояние виджета создаётся в методе
createState
.У конструктора виджета есть параметр
key
, в котором задается ключ, по которому хранится состояние виджета. Код, создающий виджет, должен хранить этот ключ, например, в случае перемещения виджета в иерархии виджетов.
Использование платформенных View
Чтобы переиспользовать платформенный UI, Flutter предоставляет классы UiKitView
и AndroidView
. Они представляют из себя обёртку нативных View
. В базовом варианте код обёртки будет выглядеть так:
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
return UiKitView(
viewType: "OneTapBottomSheet",
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (id) => _setCallback(id),
);
case TargetPlatform.android:
return AndroidView(
viewType: "OneTapBottomSheet",
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (id) => _setCallback(id),
);
default:
throw UnsupportedError('Unsupported platform');
}
void _setCallback(int id) async {
_methodChannel = MethodChannel("com.vk.id/sheet-$id");
_methodChannel!.setMethodCallHandler((call) => _handleMethod(
widget._onAuth, widget._onAuthCode, widget._onError, call));
}
Обратите внимание вот на что:
Свойство
defaultTargetPlatform
хранит платформу, с которой работает код.Происходит переключение между платформами, а если подходящей платформы нет, то бросается исключение.
В зависимости от платформы выбирается
UiKitView
илиAndroidView
.В свойстве
viewType
задается названиеView
, которому будет соответствовать платформенный бридж.В
creationParamsCodec
задаётся кодек для кодирования параметров.StandardMessageCodec
поддерживает примитивы, списки и мапы примитивов.В
creationParams
передаётся список параметров, которые будет принимать бридж при созданииView
.onPlatformViewCreated
— обратный вызов, который вызывается при созданииView
с уникальным идентификатором конкретно этого экземпляраView
._setCallback
инициализируетMethodChannel
для двустороннего взаимодействия с платформенной частью посредством вызова функций.
FlutterPlugin
Чтобы настроить бридж между платформенным SDK и Flutter, создают FlutterPlugin
. В нём инициализируется платформенная часть кода. В методе onAttachedToEngine
можно получить Context
и, при необходимости, инициализировать библиотеку. Также, если есть платформенные View
, то здесь регистрируют их фабрики:
internal class VkidFlutterSdkPlugin : FlutterPlugin {
override fun onAttachedToEngine(
flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
) {
flutterPluginBinding.platformViewRegistry.registerViewFactory(
"OneTapBottomSheet",
OneTapBottomSheetViewFactory(flutterPluginBinding.binaryMessenger)
)
}
}
Подробнее о фабриках платформенных View
расскажу далее.
MethodChannel
Для передачи данных между платформой и Flutter используется MethodChannel
. Это интерфейс, позволяющий передавать данные в обе стороны между платформами в понятном Flutter виде. Каждый MethodChannel
определяется именем, по которому в него можно отправлять данные. На стороне Flutter его создают так:
class VKID {
static const _platform = MethodChannel('com.vk.id/vkid');
…
}
На стороне Android в конструктор помимо имени передают BinaryMessenger
, который можно получить в onAttachedToEngine
:
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.vk.id/vkid")
}
Чтобы обращаться к платформенным методам, используют invokeMethod
:
setLogsEnabled(bool value) async {
_platform.invokeMethod("setLogsEnabled", value);
}
Для обработки на стороне Android регистрируют обратный вызов:
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
…
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"setLogsEnabled" -> {
If (canEnableLogs() {
VKID.logsEnabled = call.arguments as Boolean
result.success(null)
} else {
result.error(“cantenable”, “Can’t enable logs because of some reason”, null)
}
}
else -> result.notImplemented()
}
}
У класса MethodCall
есть свойство method
, по которому можно понять, что за метод был вызван.
Платформенные View
Для работы с платформенными View
, на каждой платформе создаются обёртки. На Android это выглядит так:
internal class OneTapBottomSheetViewFactory(
private val binaryMessenger: BinaryMessenger,
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
val methodChannel = MethodChannel(binaryMessenger, "com.vk.id/sheet-$viewId")
return OneTapBottomSheetPlatformView(context, methodChannel, args as List<Any?>)
}
}
internal class OneTapBottomSheetPlatformView(
private val context: Context,
private val methodChannel: MethodChannel,
private val arguments: List<Any?>,
) : PlatformView {
private val composeView = ComposeView(context)
private var hasParams = mutableStateOf(false)
private var autoHideOnSuccess = true
private lateinit var sheetState: OneTapBottomSheetState
init {
autoHideOnSuccess = arguments[0] as Boolean
hasParams.value = true
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"isVisible" -> result.success(sheetState.isVisible)
else -> result.error("unsupported method", null, null)
}
}
}
@Composable
private fun Content() {
if (!hasParams.value) return
OneTapBottomSheet(
autoHideOnSuccess = autoHideOnSuccess,
state = rememberOneTapBottomSheetState().also { sheetState = it },
)
}
override fun getView(): View {
composeView.setContent { Content() }
return composeView
}
override fun dispose() {}
}
Как видите, мы используем Compose, который создаёт View
в методе getView()
. Метод dispose()
нужен для очистки используемых ресурсов, у нас он не используется. Параметр arguments
прокидывается во View
из PlatformViewFactory
и содержит аргументы, переданные в creationParams
на стороне Dart. MethodChanel
необходим для двустороннего взаимодействия с кодом на Dart и имеет тот же идентификатор, что и MethodChannel
, созданный в Dart Widget-е в методе _setCallback
.
Собираем всё вместе

Как видно на схеме, на стороне iOS, Android и Dart создают компоненты, которые общаются через MethodChannel
. У каждого компонента свой объект MethodChannel
, при этом им задают одинаковые id
, чтобы Flutter мог понимать, что с чем общается.
Тестирование
Flutter можно тестировать на эмуляторе из консоли:
Создаем эмулятор
flutter emulators --create --name example_emulator
Запускаем эмулятор
flutter emulators --launch example_emulator
Запускаем код на эмуляторе
flutter run
Тестировать код необходимо как на iOS, так и на Android, поскольку иногда возникают платформоспецифичные баги.
Проверка ИБ
У VK есть инструмент для анализа кода на безопасность — Security Gate. Он позволяет статически анализировать кода и в удобном виде формировать отчёт. После этого с ним удобно взаимодействовать командой, оставляя комментарии и размечая проблемы. Мы просканировали инструментом проект и убедились, что уязвимостей нет.
Документация
У нас уже была документация по Android и iOS SDK, поэтому работы было мало. Мы адаптировали нашу документацию под Flutter, используя ту же структуру, достаточно было переписать примеры кода на Dart. Сейчас документация доступна на нашей платформе.
Публикация
Чтобы проверить, что библиотека готова к публикации, необходимо выполнить команду:
flutter pub publish --dry-run
После этого необходимо исправить проблемы, а когда команда будет завершаться успешно, выполняем ту же команду без –dry-run
:
flutter pub publish
Заключение
За два месяца работы усилиями двух разработчиков мы добавили поддержку Flutter в нашем SDK под каждую из платформ. Несмотря на то, что Flutter был новой для нас технологией, мы не совершили многочисленных ошибок, а те проблемы, что были, оказались реальными недостатками совместимости Flutter c платформенным кодом под Android и iOS. В целом, опыт работы с Flutter получился положительным, писать на нём код после Android достаточно просто, хоть инструментарий пока что не на том же уровне, что для нативного Android. Если вы хотите интегрировать наш SDK во Flutter, то начните с документации VK ID SDK. С вопросами вы всегда может обратиться в нашу поддержку по адресу devsupport@corp.vk.com. Спасибо за внимание, если у вас остались вопросы — предлагаю пообщаться в комментариях к статье.