이 글은 누구를 위한 것인가
- Flutter 앱에서 네이티브 iOS/Android 기능을 호출하려는 팀
- Bluetooth, NFC, 센서 등 Flutter에서 지원하지 않는 기능을 구현하는 개발자
- Pigeon으로 타입 안전한 Platform Channel을 만들려는 팀
들어가며
Flutter는 대부분의 기능을 지원하지만, 플랫폼 특화 기능(NFC, 고급 카메라 설정, 기기별 BLE 스택)은 네이티브 코드가 필요하다. Platform Channel은 Dart↔네이티브 양방향 통신 브릿지다.
이 글은 bluefoxdev.kr의 Flutter Platform Channel 네이티브 브릿지 가이드 를 참고하여 작성했습니다.
1. Platform Channel 유형
[3가지 채널]
MethodChannel:
Dart → 네이티브: 메서드 호출 (1회성)
네이티브 → Dart: 결과 반환
예: 파일 저장, 권한 요청, 디바이스 정보
EventChannel:
네이티브 → Dart: 연속 스트림
예: 센서 데이터, 위치 업데이트, BLE 스캔
BasicMessageChannel:
양방향 메시지 전달
예: 커스텀 직렬화 형식
[Pigeon (권장)]
타입 안전한 Platform Channel 코드 자동 생성
보일러플레이트 감소
컴파일 타임 타입 체크
2. Platform Channel 구현
// Dart 측 (lib/platform/battery_channel.dart)
import 'package:flutter/services.dart';
class BatteryService {
static const MethodChannel _channel = MethodChannel('com.example.app/battery');
static const EventChannel _batteryLevelChannel = EventChannel('com.example.app/battery_stream');
// 1회성 메서드 호출
static Future<int> getBatteryLevel() async {
try {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level ?? -1;
} on PlatformException catch (e) {
print('배터리 정보 오류: ${e.message}');
return -1;
}
}
// 네이티브에 데이터 전달
static Future<void> setScreenBrightness(double brightness) async {
await _channel.invokeMethod('setScreenBrightness', {'brightness': brightness});
}
// 스트림으로 실시간 배터리 레벨 수신
static Stream<int> get batteryLevelStream {
return _batteryLevelChannel.receiveBroadcastStream()
.map((level) => level as int);
}
}
// iOS 측 (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: "com.example.app/battery", binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler { call, result in
switch call.method {
case "getBatteryLevel":
UIDevice.current.isBatteryMonitoringEnabled = true
let level = Int(UIDevice.current.batteryLevel * 100)
result(level >= 0 ? level : FlutterError(code: "UNAVAILABLE", message: "배터리 정보 불가", details: nil))
case "setScreenBrightness":
if let args = call.arguments as? [String: Any],
let brightness = args["brightness"] as? Double {
UIScreen.main.brightness = CGFloat(brightness)
result(nil)
}
default:
result(FlutterMethodNotImplemented)
}
}
// EventChannel: 배터리 레벨 스트림
let batteryStreamChannel = FlutterEventChannel(name: "com.example.app/battery_stream", binaryMessenger: controller.binaryMessenger)
batteryStreamChannel.setStreamHandler(BatteryStreamHandler())
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
class BatteryStreamHandler: NSObject, FlutterStreamHandler {
private var timer: Timer?
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
UIDevice.current.isBatteryMonitoringEnabled = true
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
let level = Int(UIDevice.current.batteryLevel * 100)
events(level)
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
timer?.invalidate()
timer = nil
return nil
}
}
// Android 측 (MainActivity.kt)
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel
import android.content.Context
import android.os.BatteryManager
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/battery"
private val EVENT_CHANNEL = "com.example.app/battery_stream"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
result.success(level)
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
.setStreamHandler(object : EventChannel.StreamHandler {
private var thread: Thread? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
thread = Thread {
while (!Thread.interrupted()) {
val bm = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
runOnUiThread { events.success(level) }
Thread.sleep(5000)
}
}
thread?.start()
}
override fun onCancel(arguments: Any?) { thread?.interrupt() }
})
}
}
마무리
Platform Channel의 핵심은 채널 이름 일치다. Dart, iOS Swift, Android Kotlin 모두 동일한 채널 이름을 사용해야 통신이 된다. MethodChannel은 1회성 호출, EventChannel은 연속 스트림에 사용한다. 프로젝트 규모가 커지면 Pigeon으로 타입 안전한 코드를 자동 생성해 보일러플레이트와 런타임 오류를 줄인다.