ํ๋ซํผ ์ฑ๋ - Flutter์์ Kotlin/Swift ํธ์ถํ๊ธฐ
Flutter๋ก ์ฑ์ ๋ง๋ค๋ค ๋ณด๋ฉด Dart์์๋ ํ ์ ์๋ ์ผ์ ๋ง๋๋ ๋๊ฐ ์จ๋ค. ๊ธฐ๊ธฐ ๋ฐฐํฐ๋ฆฌ ์๋ ์ฝ๊ธฐ, ํน์ ๊ธฐ๊ธฐ์ ์ผ์, ์ด๋ฏธ Kotlin์ด๋ Swift๋ก ๋ง๋ค์ด ๋ ์ฝ๋ ์ฌ์ฌ์ฉ ๊ฐ์ ๊ฒ๋ค์ด๋ค. ์ด๋ฐ ๊ฑด Android์ iOS ์ด์์ฒด์ ๋ง ์๊ณ ์๋ ์ผ์ด๋ค. ๋ง์นจ ์ด ์๋ฆฌ์ฆ๋ฅผ ์ฝ๋ ์ฌ๋์ Kotlin๊ณผ Swift๋ฅผ ์ด๋ฏธ ์๋ค. ์ด ๊ธ์ ๊ทธ ๊ฒฝํ์ ์ด๋ฆด ์ฐจ๋ก๋ค.
Dart ์ชฝ์์ ๋ฐฐํฐ๋ฆฌ ์๋์ ์๊ณ ์ถ๋ค๊ณ ํ์. ์ผ๋จ "๊ทธ๋ฐ ํจ์๊ฐ ์๊ฒ ์ง" ํ๊ณ ๋ถ๋ฅด๋ฉด ์ด๋ ๊ฒ ๋๋ค.
final level = await MethodChannel('samples.flutter.dev/battery')
.invokeMethod<int>('getBatteryLevel');์ฑ์์ ์ด ์ค์ด ์คํ๋๋ฉด ์ด๋ฐ ์๋ฌ๊ฐ ๋๋ค.
MissingPluginException(No implementation found for method getBatteryLevel on channel samples.flutter.dev/battery)Dart ์ฝ๋๋ ๋ฌธ์ ๊ฐ ์๋ค. ๋ฐ์์ค ๋ค์ดํฐ๋ธ ์ชฝ์ด ์์ง ์์ด์ ๋ ์๋ฌ๋ค.
์์ธ: Dart๋ ์ง๋ฌธ๋ง ๋์ง ๋ฟ์ด๊ณ , ๋ต์ ๋ค์ดํฐ๋ธ๊ฐ ํ๋ค
Flutter ์ฑ์ Dart ์ฝ๋์ Android, iOS ์ฝ๋๊ฐ ํ ์ฑ ์์ ๊ฐ์ด ๋ค์ด ์๋ค. ์ด ๋์ด ์๋ก ๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ๋ ํต๋ก๊ฐ ํ๋ซํผ ์ฑ๋์ด๋ค.
- Dart(ํด๋ผ์ด์ธํธ):
MethodChannel๋ก "์ด ์ด๋ฆ์ ๋ฉ์๋๋ฅผ ํธ์ถํด ๋ฌ๋ผ"๋ ๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค - Android(ํธ์คํธ):
MethodChannel๋ก ๋ฐ์์ ์ฒ๋ฆฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋๋ ค์ค๋ค - iOS(ํธ์คํธ):
FlutterMethodChannel๋ก ๋ฐ์์ ์ฒ๋ฆฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋๋ ค์ค๋ค
๋์ ์ฑ๋ ์ด๋ฆ(๋ฌธ์์ด)์ผ๋ก ๋ง๋๋ค. ์ ์๋ฌ๋ ๊ฐ์ ์ด๋ฆ์ ์ฑ๋์ ๋ฃ๊ณ ์๋ ์ชฝ์ด ์๋ค๋ ๋ป์ด๋ค. ์ด์ ๋ค์ดํฐ๋ธ ์ชฝ์ ์ฑ์ฐ์.
1๋จ๊ณ: Dart ์ชฝ์์ ์ฑ๋์ ๋ง๋ ๋ค
import 'package:flutter/services.dart';
class BatteryService {
static const _channel = MethodChannel('samples.flutter.dev/battery');
Future<int?> getBatteryLevel() async {
try {
return await _channel.invokeMethod<int>('getBatteryLevel');
} on PlatformException catch (e) {
print('๋ฐฐํฐ๋ฆฌ๋ฅผ ์ฝ์ง ๋ชปํ์ด์: ${e.message}');
return null;
}
}
}MethodChannel('์ฑ๋ ์ด๋ฆ'): ์ฑ๋ ์ด๋ฆ์ ์ฑ๋ง๋ค ๊ฒน์น์ง ์๊ฒ๋๋ฉ์ธ/๊ธฐ๋ฅ๋ชจ์์ผ๋ก ์ง๋๋ค. ๋ค์ดํฐ๋ธ ์ชฝ๊ณผ ๊ธ์๊น์ง ๋๊ฐ์์ผ ํ๋คinvokeMethod<int>('๋ฉ์๋ ์ด๋ฆ'): ๋ค์ดํฐ๋ธ์ ๋ฉ์๋๋ฅผ ๋ถ๋ฅธ๋ค. ๋ค์ดํฐ๋ธ๊ฐ ์ฌ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋await๊ฐ ํ์ํ๋คPlatformException: ๋ค์ดํฐ๋ธ๊ฐ "์คํจํ๋ค"๊ณ ๋๋ ค์ฃผ๋ฉด ์ด ์์ธ๋ก ์จ๋ค. ๋น๋๊ธฐ ๊ธ์try/catch, HTTP ๊ธ์DioException๊ณผ ๊ฐ์ ์๋ฆฌ๋ค
์ด ํด๋์ค๋ ์์ ๋ฐฐ์ด Service ๊ณ์ธต์ ๋๋ ๊ฒ ๋ง๋ค. ํ๋ฉด์ด ์ฑ๋์ ์ง์ ์๋ฉด ๊ณ์ธต์ด ๋ฌด๋์ง๋ค. ํ๋ฉด์ BatteryService.getBatteryLevel()๋ง ๋ถ๋ฅด๊ณ , ๊ทธ๊ฒ ์ฑ๋๋ก ๊ฐ๋์ง ํ๋ฌ๊ทธ์ธ์ผ๋ก ๊ฐ๋์ง ๋ชจ๋ฅด๊ฒ ํ๋ค.
2๋จ๊ณ: Android(Kotlin) ์ชฝ์์ ๋ฐ๋๋ค
android/app/src/main/kotlin/.../MainActivity.kt๋ฅผ ์ฐ๋ค.
import android.os.BatteryManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "samples.flutter.dev/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val level = getBatteryLevel()
if (level != -1) {
result.success(level)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val manager = getSystemService(BATTERY_SERVICE) as BatteryManager
return manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}configureFlutterEngine: Flutter ์์ง์ด ์ค๋น๋ ๋ ํธ์ถ๋๋ค. ์ฌ๊ธฐ์ ์ฑ๋์ ์ฐ๋คMethodChannel(binaryMessenger, CHANNEL): Dart์ ๊ฐ์ ์ด๋ฆ์ ์ฑ๋์ ์ฐ๋คsetMethodCallHandler { call, result -> ... }: Dart๊ฐinvokeMethod๋ฅผ ๋ถ๋ฅด๋ฉด ์ด ๋๋ค๊ฐ ์คํ๋๋ค. ํธ๋ ์ผ๋ง ๋๋ค์it์์ด ์ด๋ฆ์ ์ง์ ๋ถ์ธcall,result๋คcall.method: Dart๊ฐ ๋ถ๋ฅธ ๋ฉ์๋ ์ด๋ฆ์ด๋ค. ์ด๋ฆ์ ๋ฐ๋ผ ๋ถ๊ธฐํ๋คresult.success(๊ฐ): ์ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ๋๋ ค์ค๋ค. Dart์์๋invokeMethod์ ๋ฐํ๊ฐ์ด ๋๋คresult.error(์ฝ๋, ๋ฉ์์ง, ์์ธ): ์คํจ๋ฅผ ๋๋ ค์ค๋ค. Dart์์๋PlatformException์ด ๋๋คresult.notImplemented(): ๋ชจ๋ฅด๋ ๋ฉ์๋๋ผ๊ณ ๋๋ ค์ค๋ค. Dart์์๋MissingPluginException์ด ๋๋ค
Kotlin์ ์๋ ์ฌ๋์๊ฒ๋ ์ต์ํ ์ฝ๋๋ค. ์๋ก ๋ฐฐ์ฐ๋ ๊ฑด result ์ธ ๊ฐ์ง ์๋ต๋ฟ์ด๋ค.
3๋จ๊ณ: iOS(Swift) ์ชฝ์์ ๋ฐ๋๋ค
ios/Runner/AppDelegate.swift๋ฅผ ์ฐ๋ค.
import UIKit
import Flutter
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(
name: "samples.flutter.dev/battery",
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { [weak self] (call, result) in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self?.receiveBatteryLevel(result: result)
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == .unknown {
result(FlutterError(code: "UNAVAILABLE", message: "Battery level not available.", details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}FlutterMethodChannel(name:binaryMessenger:): Dart์ ๊ฐ์ ์ด๋ฆ์ ์ฑ๋์ ์ฐ๋คsetMethodCallHandler: Dart์ ํธ์ถ์ ๋ฐ๋ ํด๋ก์ ๋คresult(๊ฐ): ์ฑ๊ณต,result(FlutterError(...)): ์คํจ,result(FlutterMethodNotImplemented): ๋ชจ๋ฅด๋ ๋ฉ์๋๋ค. Kotlin์success,error,notImplemented์ ์ง์ด๋ค
Flutter ๋ฒ์ ์ด ์ฌ๋ผ๊ฐ๋ฉด์ iOS ํ๋ก์ ํธ ํ
ํ๋ฆฟ์ AppDelegate ๋ชจ์์ด ๋ฐ๋๋ ์ค์ด๋ผ, ์ ์ฝ๋๊ฐ ๋ด ํ๋ก์ ํธ์ AppDelegate์ ๋ค๋ฅผ ์ ์๋ค. ์ฑ๋์ ๋ง๋ค๊ณ ํธ๋ค๋ฌ๋ฅผ ๊ฑฐ๋ ํต์ฌ ๋ ์ค(FlutterMethodChannel๊ณผ setMethodCallHandler)์ ๊ฐ์ผ๋, ๊ณต์ ๋ฌธ์์ ํ์ฌ ์ฝ๋๋ฅผ ํ์ธํ๊ณ ๋ด ํ๋ก์ ํธ์ ๋ง์ถฐ ๋ฃ๋๋ค.
์ฝ์ง ํฌ์ธํธ 1: ์ฝ๋๋ฅผ ๊ณ ์น๊ณ hot reload๋ฅผ ๋๋ ๋๋ฐ ๊ทธ๋๋ก๋ค
Dart ์ฝ๋์ ๋ฌ๋ฆฌ ๋ค์ดํฐ๋ธ ์ฝ๋๋ hot reload๋ก ๋ฐ์๋์ง ์๋๋ค. Kotlin์ด๋ Swift๋ฅผ ๊ณ ์ณค๋ค๋ฉด ์ฑ์ ์์ ํ ๋๊ณ ๋ค์ ๋น๋ํด์ ์คํํด์ผ ํ๋ค. ์ฒ์ ์ฑ๋์ ๋ง๋ค๊ณ MissingPluginException์ด ๊ณ์ ๋๋ค๋ฉด ์ฑ์ ์๋ก ๋น๋ํ๋์ง๋ถํฐ ํ์ธํ๋ค.
์ฝ์ง ํฌ์ธํธ 2: ์ฑ๋ ์ด๋ฆ์ด ํ ๊ธ์๋ง ๋ฌ๋ผ๋ ์ ๋ง๋๋ค
Dart์ 'samples.flutter.dev/battery'์ Kotlin์ CHANNEL, Swift์ name์ ๋ฌธ์์ด ๋น๊ต๋ผ์ ๋์๋ฌธ์์ ์คํ๊น์ง ์ผ์นํด์ผ ํ๋ค. ๊ฐ์ ์ด๋ฆ์ ์์๋ก ํ ๊ณณ์ ๋๊ณ ๋ณต์ฌํ๋ ์ต๊ด์ด ์ข๋ค. ๋ฉ์๋ ์ด๋ฆ(getBatteryLevel)๋ ๋ง์ฐฌ๊ฐ์ง๋ค.
์ฝ์ง ํฌ์ธํธ 3: ๋ฌด๊ฑฐ์ด ์ผ์ ์ฑ๋ ํธ๋ค๋ฌ ์์์ ํ๋ค
์ฑ๋ ํธ๋ค๋ฌ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ซํผ์ ๋ฉ์ธ(UI) ์ค๋ ๋์์ ์คํ๋๋ค. ํธ๋ค๋ฌ ์์์ ์ค๋ ๊ฑธ๋ฆฌ๋ ์ผ์ ํ๋ฉด ์ฑ์ด ๋ฉ์ถ๋ค. ํ์ผ ์ฝ๊ธฐ๋ ๊ณ์ฐ์ฒ๋ผ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋ ๊ฑด ๋ณ๋ ์ค๋ ๋๋ ์ฝ๋ฃจํด์์ ํ๊ณ , ๋๋๋ฉด result.success๋ฅผ ๋ถ๋ฅธ๋ค.
๊ฐ์ ์ฃผ๊ณ ๋ฐ๊ธฐ: ์ธ์์ ํ์
Dart์์ ๊ฐ์ ๊ฐ์ด ๋ณด๋ผ ๋๋ Map์ ๋ด๋๋ค.
final result = await _channel.invokeMethod<String>('greet', {'name': 'ํ๊ธธ๋'});// Kotlin
val name = call.argument<String>("name")
result.success("์๋
, $name")// Swift
let args = call.arguments as? [String: Any]
let name = args?["name"] as? String ?? ""
result("์๋
, \(name)")์ฑ๋๋ก ์ฃผ๊ณ ๋ฐ์ ์ ์๋ ํ์
์ ํ์ค ์ฝ๋ฑ์ด ์ง์ํ๋ ๊ฒ์ผ๋ก ํ์ ๋๋ค. ์ซ์, ๋ถ๋ฆฌ์ธ, ๋ฌธ์์ด, List, Map, null ๊ฐ์ ๊ธฐ๋ณธ ํ์
์ด๋ค. User ๊ฐ์ ๋ด๊ฐ ๋ง๋ ํด๋์ค๋ ๊ทธ๋๋ก ๋ชป ๋ณด๋ด์ Map์ผ๋ก ๋ฐ๊ฟ์ ๋ณด๋ด๊ณ ๋ฐ๋ ์ชฝ์์ ๋ค์ ๋ง๋ ๋ค. ํ์
์ด ๋ง์์ง๋ฉด Map์ ํค ์ด๋ฆ์ ์์ชฝ์์ ๋ง์ถ๋ค ์คํ๊ฐ ๋๊ธฐ ์ฌ์ด๋ฐ, ์ด๋๋ Pigeon์ด๋ผ๋ ๋๊ตฌ๊ฐ Dart, Kotlin, Swift์ ํ์
์ด ์๋ ์ฝ๋๋ฅผ ์์ฑํด ์ค๋ค.
๋จผ์ ํ๋ฌ๊ทธ์ธ์ ์ฐพ์๋ณธ๋ค
๋ฐฐํฐ๋ฆฌ, ์นด๋ฉ๋ผ, ์์น ๊ฐ์ ํํ ๊ธฐ๋ฅ์ ์ด๋ฏธ ๋๊ตฐ๊ฐ ํ๋ซํผ ์ฑ๋๋ก ๋ง๋ค์ด์ ํ๋ฌ๊ทธ์ธ์ผ๋ก ๊ณต๊ฐํด ๋์๋ค. ์ง์ ๋ง๋ค๊ธฐ ์ ์ pub.dev์์ ๋จผ์ ์ฐพ์๋ณธ๋ค. ์๋ฅผ ๋ค์ด ๋ฐฐํฐ๋ฆฌ๋ battery_plus ๊ฐ์ ํจํค์ง๊ฐ ์๋ค. ํ๋ฌ๊ทธ์ธ์ด ์๊ฑฐ๋, ํ์ฌ ๋ด๋ถ SDK, ๊ธฐ๊ธฐ ์ ์ฉ ๊ธฐ๋ฅ์ฒ๋ผ ๋ด๊ฐ ์ง์ ์ฐ๊ฒฐํด์ผ ํ๋ ๊ฒฝ์ฐ์ ์ด ๊ธ์ ๋ฐฉ๋ฒ์ ์ด๋ค. ํ๋ฌ๊ทธ์ธ๋ ์์ ์ด์ด๋ณด๋ฉด ๊ฐ์ MethodChannel๋ก ๋ง๋ค์ด์ ธ ์๋ค.
Android, iOS ์ชฝ ๊ด์ ์ผ๋ก ์ ๋ฆฌํ๋ฉด
- Dart๊ฐ ๋ถ๋ฅด๋ ๋ฐฉ๋ฒ:
MethodChannel('์ด๋ฆ').invokeMethod('๋ฉ์๋', ์ธ์) - Android๊ฐ ๋ฐ๋ ๋ฐฉ๋ฒ:
MethodChannel(binaryMessenger, '์ด๋ฆ').setMethodCallHandler { call, result -> } - iOS๊ฐ ๋ฐ๋ ๋ฐฉ๋ฒ:
FlutterMethodChannel(name:binaryMessenger:).setMethodCallHandler { call, result in } - ์ฑ๊ณต ์๋ต: Kotlin
result.success(๊ฐ)/ Swiftresult(๊ฐ) - ์คํจ ์๋ต: Kotlin
result.error(...)/ Swiftresult(FlutterError(...))/ Dart์์๋PlatformException - ๋ชจ๋ฅด๋ ๋ฉ์๋: Kotlin
result.notImplemented()/ Swiftresult(FlutterMethodNotImplemented)/ Dart์์๋MissingPluginException
๋ค์ดํฐ๋ธ ์ชฝ์ ํ์์ ์ฐ๋ Kotlin๊ณผ Swift ๊ทธ๋๋ก๋ค. Flutter ๋๋ฌธ์ ์๋ก ๋ฐฐ์ธ ๊ฑด ์ฑ๋ ์ด๋ฆ์ผ๋ก ๋ง๋๊ณ , call.method๋ก ๋ถ๊ธฐํ๊ณ , result๋ก ๋๋ ค์ค๋ค๋ ์ธ ๊ฐ์ง ๊ท์น์ด ์ ๋ถ๋ค. ๋ค์ดํฐ๋ธ ๊ฒฝํ์ด ์์ผ๋ฉด Flutter๊ฐ ๋ชป ํ๋ ์ผ์ ์ง์ ๋ฉ๊ฟ ์ ์๋ค๋ ์ ์ด ์ด ์๋ฆฌ์ฆ์ ๊ฐ์ ์ด๊ธฐ๋ ํ๋ค.
์ ๋ฆฌ
- Dart์์ ํ ์ ์๋ ๊ธฐ๊ธฐ ๊ธฐ๋ฅ์
MethodChannel๋ก Android์ iOS์ ๋ค์ดํฐ๋ธ ์ฝ๋๋ฅผ ๋ถ๋ฌ์ ํด๊ฒฐํ๋ค - Dart์ ๋ค์ดํฐ๋ธ๋ ์ฑ๋ ์ด๋ฆ(๋ฌธ์์ด)์ผ๋ก ๋ง๋๊ณ , ๋ฐ์์ค ์ชฝ์ด ์์ผ๋ฉด
MissingPluginException์ด ๋๋ค - Dart๋
invokeMethod๋ก ๋ถ๋ฅด๊ณ , Android๋MethodChannel, iOS๋FlutterMethodChannel์setMethodCallHandler๋ก ๋ฐ์์call.method๋ก ๋ถ๊ธฐํ๋ค - ์ฑ๊ณต์
result.success, ์คํจ๋result.error(Dart์์๋PlatformException), ๋ชจ๋ฅด๋ ๋ฉ์๋๋notImplemented๋ก ๋๋ ค์ค๋ค - ๋ค์ดํฐ๋ธ ์ฝ๋๋ฅผ ๊ณ ์น๋ฉด hot reload๊ฐ ์๋๋ผ ์ฑ์ ์๋ก ๋น๋ํด์ผ ํ๊ณ , ํธ๋ค๋ฌ ์์์ ๋ฌด๊ฑฐ์ด ์ผ์ ํ๋ฉด ์ฑ์ด ๋ฉ์ถ๋ค
- ์ฃผ๊ณ ๋ฐ์ ์ ์๋ ํ์
์ ๊ธฐ๋ณธ ํ์
์ผ๋ก ํ์ ๋๋ฏ๋ก ๋ด ํด๋์ค๋
Map์ผ๋ก ๋ฐ๊พธ๊ณ , ํ์ ์ด ๋ง์์ง๋ฉด Pigeon์ ์ด๋ค - ์ฑ๋ ํธ์ถ์ Service ๊ณ์ธต์ ๋๊ณ , ์ง์ ๋ง๋ค๊ธฐ ์ ์ ํ๋ฌ๊ทธ์ธ์ด ์๋์ง ๋จผ์ ํ์ธํ๋ค
๋ค์ ๊ธ์์๋ Firebase๋ฅผ ์ฑ์ ์ฐ๋ํ๋ ๋ฐฉ๋ฒ(Auth, Firestore, FCM)์ ๋ค๋ค๋ณด๊ฒ ๋ค.
