Flutter и натив. Пример с Яндекс OAuth
Краткое содержание этого чуда
Разберем как Flutter взаимодействует c нативными хост приложениями и как можно использовать это в наших проектах. Для примера возьмем реализацию работы библиотеки Яндекс OAuth. Почему именно ее? Потому что существующее решение не захотело работать в моем проекте, и я написал свое с блэк-джеком и MethodChannel. Также доступен пример реализации в репозитории github
Немного теории для простых смертных
Документация по работе с нативом
Приложение Flutter встраивается (можно сказать воспроизводится) в нативное приложение. Для обращения к нативному приложению хосту есть класс MethodChannel
import 'package:flutter/services.dart';
static const platform = MethodChannel('example.kotelnikoff.expert/example');
Проще говоря вы можете создать канал для общения с нативной платформой, добавить слушатели событий на каждой платформе и обмениваться сообщениями. Для передачи вы можете использовать следующие типы:
Dart | Java | Kotlin | Swift |
null | null | null | nil |
bool | java.lang.Boolean | Boolean | NSNumber (value: Bool) |
int | java.lang.Integer | Int | NSNumber (value: Int32) |
int, if 32 bits not enough | java.lang.Long | Long | NSNumber (value: Int) |
double | java.lang.Double | Double | NSNumber (value: Double) |
String | java.lang.String | String | String |
Uint8List | byte[] | ByteArray | FlutterStandardTypedData (bytes: Data) |
Int32List | int[] | IntArray | FlutterStandardTypedData (int32: Data) |
Int64List | long[] | LongArray | FlutterStandardTypedData (int64: Data) |
Float32List | float[] | FloatArray | FlutterStandardTypedData (float32: Data) |
Float64List | double[] | DoubleArray | FlutterStandardTypedData (float64: Data) |
List | java.util.ArrayList | List | Array |
Map | java.util.HashMap | HashMap | Dictionary |
Примеры использования нативного кода
На стороне Flutter
Для общения с нативом вам требуется создать канал MethodChannel
. И все! вы можете вызывать созданные вами методы и передавать аргументы на нативную платформу при помощи invokeMethod
// Создаем канал. 'kotelnikoff_dev' - его название.
// Документация рекомендует использовать имя пакета и функцию
// (например "samples.flutter.dev/battery"). Но это не обязательно.
// Важно использовать название канала символ в символ на нативной платформе
final _methodChannel = const MethodChannel('kotelnikoff_dev');
// Вызов метода на нативе. Метод принимает 2 аргумента String method,
// [dynamic arguments].
// method - должен совпадать символ в символ с нативом
// arguments - необязательно. Можно передать простые типы. (список ниже)
final message=await _methodChannel.invokeMethod('ping',
'Say hi to my little friends');
На стороне Android
Для работы с нативом я выбрал kotlin, потому что он выбран по-умолчанию. Для работы нужно лишь добавитьMethodChannel.MethodCallHandler
и пару строк душистого кода в app/src/main/kotlin/имя/вашего/пакета/MainActivity.kt
class MainActivity: FlutterActivity(), MethodChannel.MethodCallHandler {
// Имя канала символ в символ
private final var CHANEL_NAME="kotelnikoff_dev"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// При создании Activity создаем канал
// и добавляем слушатель вызовов из Flutter
val channel = MethodChannel(
flutterEngine!!.dartExecutor.binaryMessenger, CHANEL_NAME)
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
// Наш метод из кода выше
if (call.method == "ping") {
//Смотрим на arguments и если они совпадают, то вернем успешное
//выполнение запроса
if(call.arguments.toString() == "Say hi to my little friends"){
result.success("Hello")
}else{
// А тут вернем ошибку
result.error("PingError", "What", "I don't know this phrase")
}
} else {
// Ошибка на случай если метод не найден
result.notImplemented()
}
}
}
На стороне IOS
Для работы на IOS нам так же потребуется создать канал и слушателя в файле ios/Runner/AppDelegate.swift
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("Invalid root view controller")
}
//Создаем нанал
let channel = FlutterMethodChannel(name: "kotelnikoff_dev", binaryMessenger: controller.binaryMessenger)
// Слушаем сообщения
channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
if call.method == "ping" {
//Смотрим на arguments и если они совпадают, то вернем успешное выполнение запроса
if call.arguments == "Say hi to my little friends" {
result("Hello")
}else {
// А тут вернем ошибку
result(FlutterError(code: "PingError",
message: "What",
details: "I don't know this phrase"))
}
}
else {
// Ошибка на случай если метод не найден
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
И на этом все. Наше приложение может общаться с хост платформами. Правда, сейчас от этого смысла как от строчки в резюме о знании нативных платформ. Но мы сейчас мы это исправим.
Пример приложения для работы Yandex ID
Как я ранее говорил на просторах интернета есть готовый пакет для Flutter и яндекс авторизации, но он давно не обновлялся и для создания велосипеда меня уговаривать не Придется, где тут либа какая-нибудь?
Начало работы
Все как всегда начинается с документации, анализа и подключения. Документация тут. Требования тут:
Для android: имя пакета и отпечаток приложения (результат метода getCertificateFingerprint)
Для IOS: имя пакета и Team ID из консоли разработчика.
Подготовка приложения для Android
Ссылка на документацию
Добавляем в файл
android/app/build.gradle
ключ приложения из консоли и зависимостьcom.yandex.android:authsdk
android {
.....
defaultConfig {
....
///Можно и повыше
minSdkVersion 23
//Добавим клюя авторизации для работы либы
manifestPlaceholders += [YANDEX_CLIENT_ID:"Ключ приложения"]
...
}
}
dependencies {
//Добавим библиотеку
implementation "com.yandex.android:authsdk:3.1.0"
}
Для работы
com.yandex.android:authsdk
требуется изменить версию kotlin в файлеandroid/settings.gradle
. Это нужно именно для authsdk. Если вы хотите использовать, что то другое, то можно не менять эту зависимость.
plugins {
/// Обновить версию 1.8.0 или выше. Это нужно для библиотеки яндекса. Так можно не трогать
id "org.jetbrains.kotlin.android" version "1.8.0" apply false
}
Готово
Подготовка на IOS
Ссылка на документацию
Модифицировать
ios/Podfile
и добавить в него зависимостьYandexLoginSDK
# Дефолтный файл Podfile
# Uncomment this line to define a global platform for your project
platform :ios, '14.0'
# Добавляем библиотеку авторизации яндекса
pod 'YandexLoginSDK'
# ....Продолжение файла...
Добавить информацию в
ios/Runner/Info.plist
из документации
LSApplicationQueriesSchemes
primaryyandexloginsdk
secondaryyandexloginsdk
CFBundleURLTypes
CFBundleURLName
YandexLoginSDK
CFBundleURLSchemes
yx{ВАШ_КЛЮЧ}
Блок
CFBundleURLTypes
должен быть только один на весь файл. Для нескольких сервисов просто создайте новыевнутри
Добавить домен
applinks:yx{Client_ID}.oauth.yandex.ru
в список ассоциированных доменовCapability: Associated Domains
Готово!
Переходим к разработке
Вам доступен исходный код в репозитории github в нем есть комментарии во всех важных местах. И в не важных тоже. Тут только самые интересные моменты. В репозитории все методы (все 3) находятся в классе OAuthYandex. Для общения используются bool и JSON string. Почему не использую тип Map, если он доступен во всех языках? Потому что я художник и я так вижу.
Мини лайфхак.
Если вам нужно конвертировать JSON строку в объект, то можно закинуть ее quicktype.io и в ответе получить готовый файлик. Без регистрации и смс.
Метод получения отпечатка
Метод только для android. Нужен для получения SHA или SHA256 Fingerprint. Эти отпечатки необходимы для создания приложения в консоли разработчика яндекса. Да и в других местах они потребуются (VK. google и прочие штуки на андроиде). Главное не забыть добавить в консоль отпечаток релизной версии и версии из маркета.
Как это работает на стороне Flutter. Подробнее можно посмотреть репозитории,
Future getCertificateFingerprint({FingerprintType type=FingerprintType.SHA1})async{
if(!Platform.isAndroid){
throw UnsupportedError('Метод доступен только на android');
}
try{
final message=await _methodChannel.invokeMethod('getCertificateFingerprint',type.label);
//парсим ответ
return fingerprintModelFromJson(message);
}catch(e,s){
rethrow;
}
}
Код на kotlin находится внутри onMethodCall
// этот метод нужен для получения отпечатка. можно использовать консольную команду,
// но она не всегда работает.
else if(call.method=="getCertificateFingerprint"){
try {
val info = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
for (signature in info.signatures) {
val md: MessageDigest = MessageDigest.getInstance(call.arguments.toString())
md.update(signature.toByteArray())
val digest = md.digest()
val hexString = digest.joinToString(":") { "%02x".format(it) }
val res=mapOf(
"fingerprint" to hexString,
"packageName" to context.packageName
)
result.success(JSONObject(res.toMap()).toString())
}
} catch (e: Exception) {
result.error("Error cert","${e.message}","${e.stackTrace}")
}
}
Запуск библиотеки
Для запуска Яндекс OAuth я создал метод start
, который нужен для запуска библиотеки или получения ошибки с объяснением о том, почему ей не нравится наше великолепное приложение
На стороне flutter вызов не сильно отличается (поменяй getCertificateFingerprint
на start
). Поэтому тут будет только код на kotlin. В нем есть пример работы с ошибками. Сам код взят из документации.
if(call.method=="start"){
try {
yandexSdk = YandexAuthSdk.create(YandexAuthOptions(applicationContext))
result.success(true)
}catch (e: Exception){
result.error("InitError","${e.message}","${e.stackTrace}")
}
}
Запрос доступа
Теперь рассмотрим как получить информацию о пользователе. Для этого нам нужно создать Intent на приложения яндекса (Если его нет, то должен открыться браузер). Ответ мы получим в методе onActivityResult
.
try{
val loginOptions = YandexAuthLoginOptions()
val intent: Intent = yandexSdk.contract.createIntent(
applicationContext,loginOptions)
// Запускаем активити авторизации и ждем результат
startActivityForResult(intent, REQUEST_LOGIN_YANDEX);
}catch (e: Exception){
result.error("InitError","${e.message}","${e.stackTrace}")
}
/// много строк спустя
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode==REQUEST_LOGIN_YANDEX){
val sdk = YandexAuthSdk.create(YandexAuthOptions(applicationContext))
// метод из документации. Подробнее в репозитории
handleResult(sdk.contract.parseResult(resultCode,data))
}
super.onActivityResult(requestCode, resultCode, data)
}
На android это все. Полный код класса доступен в спойлере и в репозитории.
Полный код для android
package expert.kotelnikoff.oauthdemo
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import com.yandex.authsdk.YandexAuthException
import com.yandex.authsdk.YandexAuthLoginOptions
import com.yandex.authsdk.YandexAuthOptions
import com.yandex.authsdk.YandexAuthResult
import com.yandex.authsdk.YandexAuthSdk
import com.yandex.authsdk.YandexAuthToken
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.json.JSONObject
import java.security.MessageDigest
class MainActivity: FlutterActivity(), MethodChannel.MethodCallHandler {
/// Это наш канал для работы с флаттером. Важно указать его символ в символ
private final var CHANEL_NAME="kotelnikoff_dev"
private lateinit var yandexSdk: YandexAuthSdk
private var result: MethodChannel.Result? = null
private val REQUEST_LOGIN_YANDEX = 100
private lateinit var channel : MethodChannel
private lateinit var context: Context;
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if(call.method=="start"){
try {
yandexSdk = YandexAuthSdk.create(YandexAuthOptions(applicationContext))
result.success(true)
}catch (e: Exception){
result.error("InitError","${e.message}","${e.stackTrace}")
}
}else if(call.method=="yandexAuth"){
// создаем интент для запуска активити яндекса и авторизации в ней. Сохраняем result для последующего использования.
if(this.result!=null){
result.error("InitError","Прошлый запрос еще не выполнен, ждите","Подожди")
return;
}
this.result=result
// из документации яндекса
try{
val loginOptions = YandexAuthLoginOptions()
val intent: Intent = yandexSdk.contract.createIntent(applicationContext,loginOptions)
startActivityForResult(intent, REQUEST_LOGIN_YANDEX);
}catch (e: Exception){
result.error("InitError","${e.message}","${e.stackTrace}")
}
}
// этот метод нужнен для получения отпечатка. можно использовать консольную команду, но у меня она не всегда работает.
else if(call.method=="getCertificateFingerprint"){
try {
val info = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
for (signature in info.signatures) {
val md: MessageDigest = MessageDigest.getInstance(call.arguments.toString())
md.update(signature.toByteArray())
val digest = md.digest()
val hexString = digest.joinToString(":") { "%02x".format(it) }
val res=mapOf(
"fingerprint" to hexString,
"packageName" to context.packageName
)
result.success(JSONObject(res.toMap()).toString())
}
} catch (e: Exception) {
result.error("Error cert","${e.message}","${e.stackTrace}")
}
} else {
result.notImplemented()
}
}
// При создании нативной активити добавляем слушатель событий Flutter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context = applicationContext
channel = MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANEL_NAME)
channel.setMethodCallHandler(this)
}
// метод из документации яндекса. смотрим на сколько успешным был запрос
private fun handleResult(result: YandexAuthResult) {
when (result) {
is YandexAuthResult.Success -> onSuccessAuth(result.token)
is YandexAuthResult.Failure -> onProccessError(result.exception)
YandexAuthResult.Cancelled -> onCancelled()
}
}
private fun onSuccessAuth(token: YandexAuthToken){
val res=mapOf(
"susses" to true,
"token" to token.value,
"expiresIn" to token.expiresIn,
)
if(result!==null){
result!!.success(JSONObject(res.toMap()).toString())
result=null
}
}
/// слушаем результат выполнения запроса на авторизацию
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode==REQUEST_LOGIN_YANDEX){
val sdk = YandexAuthSdk.create(YandexAuthOptions(applicationContext))
handleResult(sdk.contract.parseResult(resultCode,data))
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun onProccessError(exception: YandexAuthException){
val arg = HashMap()
arg.put("susses",false)
arg.put("provider","yandex")
arg.put("error",exception.toString())
if(result!==null){
result!!.success(JSONObject(arg.toMap()).toString())
result=null
}
}
private fun onCancelled(){
val arg = HashMap()
arg.put("susses",false)
arg.put("provider","yandex")
arg.put("cancelled",true)
if(result!==null){
result!!.success(JSONObject(arg.toMap()).toString())
result=null
}
}
}
Работаем с IOS
Нам необходимо реализовать методы start и yandexAuth на нативе IOS. Логика кода будет точно такой-же как и на android. Но сначала лайфхак
Если при запуске проекта вы увидите напротив import Flutter надпись «No such module 'Flutter' » , то вам нужно перейти в Runner и включить поддержку плагинов (скрины ниже)
Вот так выглядит ошибка
Решение проблемы. Возможно прийдется перебилдить приложение в xcode (очистить билд и сбилдить)
Для работы с IOS яндекс подготовил YandexLoginSDKObserver
. Создаем класс с NSObject
и YandexLoginSDKObserver
и добавляем необходимы для работы метод didFinishLogin
class MyYandexLoginSDKObserver: NSObject, YandexLoginSDKObserver {
// тут отслеживаем результат авторизации
func didFinishLogin(with result: Result) {
do {
let res = try result.get()
let response = SussesResponse(susses: true, token: res.token, expiresIn: -1)
let encoder = JSONEncoder()
let responseJson = try encoder.encode(response)
let responseJsonString = String(data: responseJson, encoding: .utf8)
// Если все хорошо, то отправляем строку в формате json во flutter приложение.
self.resultFlutter(responseJsonString)
}catch{
let response = ErrorResponse(susses: false, errorMessage: "\(error)")
let encoder = JSONEncoder()
do {
let responseJson = try encoder.encode(response)
let responseJsonString = String(data: responseJson, encoding: .utf8)
// Если все плохо, то передаем сведения об ошибке
self.resultFlutter(FlutterError.init(code: "errorSetDebug",message: responseJsonString,details:nil))
}catch{
}
}
}
}
Теперь мы можем работать с нативом IOS. Подключаемся к каналу и создаем код для методов start
и yandexAuth
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("Invalid root view controller")
}
//Подключаемся к нашему каналу
let channel = FlutterMethodChannel(name: "kotelnikoff_dev", binaryMessenger: controller.binaryMessenger)
// добавляем слушаем
channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
if call.method == "start" {
do {
let clientID = "{ВАШ_КЛЮЧ}" //<= ваш id приложения из консоли. В теории можно взять из Info.plist
try YandexLoginSDK.shared.activate(with: clientID)
self.myYandex = MyYandexLoginSDKObserver()
YandexLoginSDK.shared.add(observer: self.myYandex)
result(true)
} catch {
result(FlutterError(code: "InitError",
message: "Error YandexLoginSDK \(error)",
details: "\(error)"))
}
} else if call.method == "yandexAuth" {
if let viewController = UIApplication.shared.keyWindow?.rootViewController {
do{
self.myYandex.setResult(result: result);
try YandexLoginSDK.shared.authorize(with: viewController)
}catch{
result(FlutterError(code: "ERROR YandexLoginSDK",
message: "Error YandexLoginSDK \(error)",
details: "\(error)"))
}
}
}
else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
Готово, теперь наше приложение работает и на IOS.
Полный код для IOS
import UIKit
import Flutter
import YandexLoginSDK
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var myYandex:MyYandexLoginSDKObserver=MyYandexLoginSDKObserver()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("Invalid root view controller")
}
//Подключаемся к нашему каналу
let channel = FlutterMethodChannel(name: "kotelnikoff_dev", binaryMessenger: controller.binaryMessenger)
// добавляем слушаем
channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
if call.method == "start" {
do {
let clientID = "{ВАШ_КЛЮЧ}" //<= ваш id приложения из консоли. В теории можно взять из Info.plist
try YandexLoginSDK.shared.activate(with: clientID)
self.myYandex = MyYandexLoginSDKObserver()
YandexLoginSDK.shared.add(observer: self.myYandex)
result(true)
} catch {
result(FlutterError(code: "InitError",
message: "Error YandexLoginSDK \(error)",
details: "\(error)"))
}
} else if call.method == "yandexAuth" {
if let viewController = UIApplication.shared.keyWindow?.rootViewController {
do{
self.myYandex.setResult(result: result);
try YandexLoginSDK.shared.authorize(with: viewController)
}catch{
result(FlutterError(code: "ERROR YandexLoginSDK",
message: "Error YandexLoginSDK \(error)",
details: "\(error)"))
}
}
}
else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
class MyYandexLoginSDKObserver: NSObject, YandexLoginSDKObserver {
var resultFlutter: FlutterResult!
func setResult(result: @escaping FlutterResult){
self.resultFlutter = result
}
func didFinishLogin(with result: Result) {
do {
let res = try result.get()
let response = SussesResponse(susses: true, token: res.token, expiresIn: -1)
let encoder = JSONEncoder()
let responseJson = try encoder.encode(response)
let responseJsonString = String(data: responseJson, encoding: .utf8)
self.resultFlutter(responseJsonString)
}catch{
let response = ErrorResponse(susses: false, errorMessage: "\(error)")
let encoder = JSONEncoder()
do {
let responseJson = try encoder.encode(response)
let responseJsonString = String(data: responseJson, encoding: .utf8)
self.resultFlutter(FlutterError.init(code: "errorSetDebug",message: responseJsonString,details:nil))
}catch{
}
}
}
}
//Это нужно для типизации успешного результата с токеном. Я не нашел как это реализовать иначе. Если есть идеи, то напишите в комменты
struct SussesResponse: Encodable {
var susses: Bool
var token: String
var expiresIn: Int
}
struct ErrorResponse: Encodable {
var susses: Bool
var errorMessage: String
}
Готово
Наше приложение может использовать авторизацию yandex на уровне нативных хост приложений.
Осталось только обменять полученный токен на данные пользователя. Для этого необходимо выполнить запрос (лучше на сервере)
curl --location 'https://login.yandex.ru/info' \
--header 'Authorization: OAuth {Полученный_токен_в_приожении}'
И все готово.
P.S. Если есть идеи как можно украсить код на нативныйх платформах, то жду предложений в комментариях. Особенно для ios.
Посетить телеграмм канал автора можно посетить тут
Подкинте программисту на кофе, ему еще песика кормить,