Flutter 플랫폼 채널 소개: Method Channel과 Event Channel
Flutter는 Dart 코드와 플랫폼별 네이티브 코드(Android의 경우 Kotlin/Java, iOS의 경우 Swift/Objective-C) 간의 통신을 가능하게 하는 강력한 메커니즘을 제공합니다. 그중에서도 MethodChannel
과 EventChannel
은 플러그인을 만들거나 Flutter가 직접 제공하지 않는 네이티브 기능을 사용할 때 특히 중요합니다.
Method Channel: 요청-응답 방식 통신
MethodChannel
은 Dart와 네이티브 코드 간의 비동기 메서드 호출을 용이하게 합니다. Dart에서 네이티브 함수를 호출하고 (선택적으로) 단일 결과를 받아야 하는 시나리오에 이상적입니다. 이는 Dart가 요청을 보내고 네이티브 측에서 응답(또는 오류)을 보내는 방식으로, 양방향 통신의 한 형태로 볼 수 있습니다.
주요 사용 사례:
- 단일 데이터 가져오기 (예: 배터리 잔량, 기기 이름)
- 네이티브 액션 실행 (예: 특정 네이티브 UI 열기, 소리 재생)
- 네이티브 측에서 일회성 연산 수행
Dart 측 예제 (Flutter 위젯 내):
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // MethodChannel 및 PlatformException에 필요
class BatteryInfoWidget extends StatefulWidget {
const BatteryInfoWidget({super.key});
@override
State createState() => _BatteryInfoWidgetState();
}
class _BatteryInfoWidgetState extends State {
// 1. 채널을 정의합니다. 이름은 네이티브 측과 일치해야 합니다.
static const platform = MethodChannel('samples.flutter.dev/battery');
String _batteryLevel = '배터리 잔량 알 수 없음.';
@override
void initState() {
super.initState();
_getBatteryLevel(); // 위젯 초기화 시 배터리 잔량 가져오기
}
// 2. 네이티브 함수를 호출하는 비동기 메서드를 정의합니다.
Future _getBatteryLevel() async {
String batteryLevel;
try {
// 3. 메서드를 호출합니다. 'getBatteryLevel'은 네이티브 측의 메서드 이름입니다.
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = '배터리 잔량: $result%.';
} on PlatformException catch (e) {
batteryLevel = "배터리 잔량 가져오기 실패: '${e.message}'.";
} catch (e) {
batteryLevel = "예상치 못한 오류 발생: '${e.toString()}'.";
}
if (mounted) { // 위젯이 아직 위젯 트리에 있는지 확인
setState(() {
_batteryLevel = batteryLevel;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('배터리 정보')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_batteryLevel),
ElevatedButton(
onPressed: _getBatteryLevel,
child: const Text('배터리 잔량 새로고침'),
),
],
),
),
);
}
}
Event Channel: 네이티브에서 Dart로 데이터 스트리밍
EventChannel
은 네이티브 코드에서 Dart로 데이터를 스트리밍하기 위해 설계되었습니다. Dart는 스트림을 구독하고, 네이티브 측에서는 시간이 지남에 따라 여러 이벤트(데이터 패킷 또는 오류 알림)를 보낼 수 있습니다. Dart가 리스닝을 시작하지만, 이벤트의 지속적인 흐름은 일반적으로 네이티브에서 Dart로의 단방향입니다.
주요 사용 사례:
- 지속적인 센서 업데이트 수신 (예: 가속도계, GPS 위치)
- 네이티브 이벤트 모니터링 (예: 네트워크 연결 변경, 배터리 상태 변경)
- 장시간 실행되는 네이티브 작업의 진행 상황 업데이트 수신
Dart 측 예제 (Flutter 위젯 내):
import 'dart:async'; // StreamSubscription에 필요
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // EventChannel에 필요
class ConnectivityMonitorWidget extends StatefulWidget {
const ConnectivityMonitorWidget({super.key});
@override
State createState() => _ConnectivityMonitorWidgetState();
}
class _ConnectivityMonitorWidgetState extends State {
// 1. 채널을 정의합니다. 이름은 네이티브 측과 일치해야 합니다.
static const eventChannel = EventChannel('samples.flutter.dev/connectivity');
String _connectionStatus = '알 수 없음';
StreamSubscription? _connectivitySubscription;
@override
void initState() {
super.initState();
_enableEventReceiver();
}
void _enableEventReceiver() {
// 2. EventChannel로부터 브로드캐스트 스트림을 수신합니다.
_connectivitySubscription = eventChannel.receiveBroadcastStream().listen(
_onEvent,
onError: _onError,
cancelOnError: true, // 오류 발생 시 구독 자동 취소
);
}
void _onEvent(Object? event) { // 이벤트는 코덱이 지원하는 모든 타입이 될 수 있습니다.
if (mounted) {
setState(() {
_connectionStatus = event?.toString() ?? 'null 이벤트 수신';
});
}
}
void _onError(Object error) {
if (mounted) {
setState(() {
_connectionStatus = '연결 상태 가져오기 실패: ${error.toString()}';
});
}
}
@override
void dispose() {
// 3. 위젯이 파괴될 때 구독을 취소합니다.
_connectivitySubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('연결 상태 모니터')),
body: Center(
child: Text('연결 상태: $_connectionStatus'),
),
);
}
}
Android(Kotlin)에서 Method Channel과 Event Channel 사용하기
Android에서 플랫폼 채널을 사용하려면 일반적으로 MainActivity.kt
또는 사용자 정의 Flutter 플러그인 내에 핸들러를 등록합니다.
Android (Kotlin) - Method Channel 예제 (MainActivity.kt
내):
package com.example.my_flutter_app // 앱의 패키지 이름으로 변경하세요.
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
class MainActivity: FlutterActivity() {
private val BATTERY_CHANNEL_NAME = "samples.flutter.dev/battery" // Dart 측과 일치해야 함
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// MethodChannel 설정
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL_NAME).setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "배터리 잔량을 사용할 수 없습니다.", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
}
위 코드는 MainActivity
에서 MethodChannel
을 설정합니다. Flutter가 'getBatteryLevel' 메서드를 호출하면, 네이티브 Kotlin 코드가 현재 배터리 잔량을 가져와 성공 결과로 반환하거나, 사용할 수 없는 경우 오류를 반환합니다.
Android (Kotlin) - Event Channel 예제 (MainActivity.kt
내):
package com.example.my_flutter_app // 앱의 패키지 이름으로 변경하세요.
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
// import android.os.Handler // 필요에 따라
// import android.os.Looper // 필요에 따라
class MainActivity: FlutterActivity() {
private val CONNECTIVITY_CHANNEL_NAME = "samples.flutter.dev/connectivity" // Dart 측과 일치해야 함
private var connectivityReceiver: BroadcastReceiver? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// EventChannel 설정
EventChannel(flutterEngine.dartExecutor.binaryMessenger, CONNECTIVITY_CHANNEL_NAME).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
// 초기 연결 상태 전송
events?.success(checkConnectivity())
// 연결 변경을 수신하기 위한 BroadcastReceiver 설정
connectivityReceiver = createConnectivityReceiver(events)
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(connectivityReceiver, filter)
}
override fun onCancel(arguments: Any?) {
unregisterReceiver(connectivityReceiver)
connectivityReceiver = null
}
}
)
}
private fun createConnectivityReceiver(events: EventChannel.EventSink?): BroadcastReceiver {
return object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
events?.success(checkConnectivity())
}
}
}
private fun checkConnectivity(): String {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
return if (capabilities != null &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))) {
"연결됨"
} else {
"연결 끊김"
}
} else {
// API 29 이상에서는 사용되지 않음
@Suppress("DEPRECATION")
val activeNetworkInfo = connectivityManager.activeNetworkInfo
@Suppress("DEPRECATION")
return if (activeNetworkInfo != null && activeNetworkInfo.isConnected) {
"연결됨"
} else {
"연결 끊김"
}
}
}
override fun onDestroy() {
super.onDestroy()
// 스트림이 활성 상태일 때 액티비티가 파괴되면 수신기가 등록 해제되도록 보장
if (connectivityReceiver != null) {
unregisterReceiver(connectivityReceiver)
connectivityReceiver = null
}
}
}
이 Android 예제는 EventChannel
을 설정합니다. Dart가 수신을 시작하면, 네이티브 코드는 연결 변경에 대한 BroadcastReceiver
를 등록합니다. 연결이 변경될 때마다 이벤트("연결됨" 또는 "연결 끊김")가 EventSink
를 통해 Flutter로 전송됩니다. Dart가 스트림을 취소하면 수신기가 등록 해제됩니다.
iOS(Swift)에서 Method Channel과 Event Channel 사용하기
iOS의 경우, 일반적으로 AppDelegate.swift
파일 또는 사용자 정의 Flutter 플러그인 내에 채널 핸들러를 등록합니다.
iOS (Swift) - Method Channel 예제 (AppDelegate.swift
내):
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private let BATTERY_CHANNEL_NAME = "samples.flutter.dev/battery" // Dart 측과 일치해야 함
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController가 FlutterViewController 타입이 아닙니다.")
}
// MethodChannel 설정
let batteryChannel = FlutterMethodChannel(name: BATTERY_CHANNEL_NAME,
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
// 참고: 이 메서드는 UI 스레드에서 호출됩니다.
if call.method == "getBatteryLevel" {
self.receiveBatteryLevel(result: result)
} else {
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
UIDevice.current.isBatteryMonitoringEnabled = true // 중요!
if UIDevice.current.batteryState == .unknown {
result(FlutterError(code: "UNAVAILABLE",
message: "배터리 잔량을 사용할 수 없습니다.",
details: nil))
} else {
result(Int(UIDevice.current.batteryLevel * 100)) // batteryLevel은 0.0에서 1.0 사이
}
}
}
이 iOS Swift 예제에서는 AppDelegate
에서 FlutterMethodChannel
을 설정합니다. Flutter가 'getBatteryLevel'을 호출하면, Swift 코드는 배터리 모니터링을 활성화하고 배터리 잔량을 가져와 반환합니다. 배터리 상태를 알 수 없는 경우 오류를 반환합니다.
iOS (Swift) - Event Channel 예제 (AppDelegate.swift
내):
Event Channel의 경우, AppDelegate
(또는 전용 클래스)가 FlutterStreamHandler
를 준수해야 합니다.
import UIKit
import Flutter
// 연결성의 경우 Reachability.swift와 같은 라이브러리나 Network.framework를 사용할 수 있습니다.
// 간단하게 하기 위해 이 예제에서는 이벤트를 시뮬레이션합니다.
// 실제 연결 모니터의 경우 NWPathMonitor (iOS 12+) 또는 SCNetworkReachability를 사용합니다.
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler { // FlutterStreamHandler 준수
private let CONNECTIVITY_CHANNEL_NAME = "samples.flutter.dev/connectivity" // Dart 측과 일치해야 함
private var eventSink: FlutterEventSink?
// 실제 앱에서는 실제 연결성을 위해 NWPathMonitor 등을 사용합니다.
// 이 타이머는 데모용입니다.
private var timer: Timer?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController가 FlutterViewController 타입이 아닙니다.")
}
// EventChannel 설정
let connectivityChannel = FlutterEventChannel(name: CONNECTIVITY_CHANNEL_NAME,
binaryMessenger: controller.binaryMessenger)
connectivityChannel.setStreamHandler(self) // 'self'가 onListen과 onCancel을 처리합니다.
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - FlutterStreamHandler 메서드
// Flutter가 스트림 수신을 시작할 때 호출됩니다.
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 이 예제에서는 주기적으로 연결 상태를 보내는 것을 시뮬레이션합니다.
// 실제 앱에서는 실제 시스템 알림(예: NWPathMonitor)에 등록합니다.
self.eventSink?("연결됨 (초기)") // 초기 이벤트 전송
// 예: 타이머로 네트워크 변경 시뮬레이션
self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
let isConnected = arc4random_uniform(2) == 0 // 무작위로 연결 또는 연결 끊김
self?.eventSink?(isConnected ? "연결됨 (시뮬레이션)" : "연결 끊김 (시뮬레이션)")
}
return nil // 오류 없음
}
// Flutter가 스트림 수신을 중지할 때 호출됩니다.
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
self.timer?.invalidate()
self.timer = nil
// 실제 앱에서는 여기서 시스템 알림 등록을 해제합니다.
return nil // 오류 없음
}
}
이 iOS Swift 예제는 FlutterEventChannel
설정을 보여줍니다. AppDelegate
는 FlutterStreamHandler
를 준수합니다.
Dart가 수신을 시작하면(onListen
), FlutterEventSink
를 저장하고 연결 이벤트를 보내는 타이머를 시작합니다(시뮬레이션). 실제 애플리케이션에서는 NWPathMonitor
(iOS 12+의 경우) 또는 다른 메커니즘을 사용하여 실제 네트워크 변경을 감지하고 eventSink
로 상태를 보냅니다.
Dart가 스트림을 취소하면(onCancel
), 싱크가 지워지고 타이머가 중지됩니다(또는 네이티브 리스너가 제거됩니다).
주요 고려 사항 및 모범 사례
- 채널 이름: 애플리케이션 전체에서 고유해야 하며 Dart 측과 네이티브 측에서 동일해야 합니다. 일반적인 규칙은
your.domain/featureName
입니다. - 데이터 타입: 플랫폼 채널은 기본 타입(null, Boolean, 숫자, 문자열), 바이트 배열, 이러한 타입의 리스트 및 맵을 지원하는 표준 메시지 코덱을 사용합니다. 복잡한 사용자 정의 객체의 경우, 지원되는 타입 중 하나(예: 맵 또는 JSON 문자열)로 직렬화합니다.
- 비동기 작업: 모든 채널 통신은 비동기적입니다. Dart에서는
async/await
를 사용하고 네이티브 측에서는 적절한 스레딩/콜백 메커니즘을 사용합니다. - 오류 처리: Dart 측에서는 항상 잠재적인
PlatformException
을 처리합니다. 네이티브 측에서는 MethodChannel의 경우result.error()
, EventChannel의 경우eventSink.error()
를 사용하여 오류를 Dart로 전파합니다. - 생명주기 관리:
EventChannel
의 경우, 네이티브 측StreamHandler
의onCancel
메서드에서 네이티브 리소스(리스너 또는 옵저버 등)를 정리하고 Dart의dispose
메서드에서StreamSubscription
을 취소해야 합니다.MethodChannel
의 경우, 특정 위젯의 생명주기에 연결되어 있다면 해당 범위를 고려해야 합니다. 애플리케이션 수준(예:MainActivity
또는AppDelegate
)에서 등록된 채널은 앱의 생명주기 동안 유지됩니다.
- 스레드 안전성:
- 네이티브 메서드 호출 핸들러(
MethodChannel
용) 및 스트림 핸들러(EventChannel
용)는 일반적으로 플랫폼의 기본 UI 스레드에서 호출됩니다. - 네이티브 측에서 장시간 실행되는 작업을 수행하는 경우, UI 스레드를 차단하지 않도록 백그라운드 스레드로 디스패치합니다. 그런 다음, 해당 결과/이벤트가 네이티브 UI 구성 요소와 상호 작용해야 하는 경우 Flutter로 결과/이벤트를 다시 보내기 전에 기본 스레드로 다시 전환해야 합니다(채널 통신 자체의 경우
result.success/error
및eventSink.success/error
는 일반적으로 스레드로부터 안전합니다).
- 네이티브 메서드 호출 핸들러(
- 플러그인: 재사용 가능한 플랫폼별 기능의 경우, 채널 구현을 Flutter 플러그인으로 패키지화합니다. 이는 모듈성과 공유성을 향상시킵니다.
Method Channel과 Event Channel을 이해하고 올바르게 구현함으로써, 기본 네이티브 플랫폼의 모든 기능을 활용하여 Flutter 애플리케이션의 기능을 크게 확장할 수 있습니다.