[Flutter/Kotlin/Swift] 예제로 알아보는 네이티브 통신 (Platform Channel)하는 방법_Method Channel / Event Channel

Tyger·2023년 3월 31일
5

Flutter

목록 보기
38/56

예제로 알아보는 네이티브 통신 (Platform Channel)하는 방법

flutter_bloc
equatable

플랫폼 통신(IOS) - Method Channel
플랫폼 통신(IOS) - Event Channel
플랫폼 통신(Android) - Method Channel
플랫폼 통신(Android) - Event Channel

이번 글에서는 Platform Channel에 대해서 살펴보도록 하겠다.

Platform 채널과 관련된 여러 글들을 올렸었는데, 아직 어려워하시는 분들이 많은 것 같아서 Flutter에서 네이티브와 통신하는 채널인 Platform Channel을 간단한 예제를 통해서 알아보겠다.

먼저 Platform Channel에 대해서 모르시는 분들을 위해 간단하게 살펴보자면, Flutter는 앱, 웹, 데스크탑, 리눅스 등의 환경을 개발할 수 있도록 하는 프레임워크라는 사실은 이미 알고 계실거다. 앱에서는 양대 플랫폼인 Android / IOS를 모두 개발하는 크로스플랫폼인데, 네이티브 고유의 기능에 접근해야 하는 등의 개발이 필요한 경우 네이티브 언어를 통해 Flutter와 통신을 할 수 있다. 물론 통신을 하지 않고도 Flutter의 Dart Package에 등록된 PlugIn을 사용하여 개발이 가능하긴 하다.

여기서 말한 PlugIn이 바로 네이티브 코드로 만들어진 것이다. Library라고 불리는거는 Flutter 프레임워크에서 만들어진거고, PlugIn은 네이티브로 만들어진 것이다.

네이티브 언어로는 안드로이드의 Kotlin, IOS의 Swift를 사용하여 개발을 하여야 하는데, 네이티브 언어를 사용하지 못하거나 플랫폼 개발이 어려운 분들에게는 어려울 수도 있다.

Flutter만으로 앱을 개발하는 경우가 가장 베스트이기는 하지만 저도 많은 부분에서 Platform 채널을 통한 네이티브 코드를 사용하는 경우가 상당히 많은 편이다.

Flutter <> Native 간의 Platform Channle에는 MethodChannel, EventChannel, BasicMessageChannel 이렇게 세 가지 방법으로 통신할 수 있는데, BasicMessageChannel은 가벼운 통신 방식이며, MethodChannel이 활발하게 사용되다 보니 잘 사용하지 않는 기능이다. 저도 간단한 로그 출력하는 용도로만 사용하고, 잘 사용하지는 않는다.

MethodChannel은 단기적 이벤트를 발생시키는 채널로, 쉽게 생각하면 Flutter의 Future 비동기라고 생각하면 된다. EventChannel은 Stream 비동기 방식으로, 변경이 발생할 때마다 Flutter에서 Native로 또는 Native에서 Flutter로 이벤트를 발생하게 해준다.
당연히 MethodChannel도 Flutter -> Native, Natieve -> Flutter로 양방향 통신이 가능하다.

세 가지 채널의 사용법을 간단한 예제를 통해서 살펴볼 예정이니, 천천히 따라해보면 된다.

Method Channel

먼저 가장 흔하게 사용되는 Platform Channel인 MethodChannel에 대해서 살펴보도록 하겠다.

Method Channel에 사용되는 예제는 Flutter 프로젝트 생성시 기본 기능으로 제공되는 카운트 앱을 Native에서 상태를 가져와 사용할 예정이고, 또 다른 예제는 배터리 레벨을 가져오는 예제를 통해서 이해해 보도록 하자.

dependencies

사용되는 예제를 Cubit을 통해서 만들었기에 필요하시면 flutter_bloc, equatable 라이브러리를 추가하여야 한다.

dependencies:
	flutter_bloc: ^8.1.1
	equatable: ^2.0.5

Count App

카운트 예제 앱에는 Cubit 상태 관리를 사용하여 개발하였으며, UI 코드는 설명하지 않고 공유한 Git에서 클론하여 사용해 볼 수 있도록 하겠다.

카운트 앱에는 숫자 카운트를 증가시키는 기능과 감소시키는 기능, 그리고 초기화하는 기능을 사용할 것인데, 기본 예제랑은 다르게 얼마만큼의 숫자를 증감할지에 대한 상태 하나를 더 추가한 예제이다.

아래 결과물이 있으니, 결과물을 먼저 보고 작업을 진행해보자.

여기서 카운트 값은 네이티브에서 받아올 것이고, 얼마만큼 증감할지에 대한 값은 Flutter에서 관리를 하다가 카운트를 증가시킬 때에 MethodChannel로 Native에 전달해 줄 것이다.

Native에서는 전달해 준 값을 받아서 해당하는 숫자만큼 카운트를 증가시킨 뒤 다시 Flutter로 카운트 값을 전달해주면서, 스낵바를 띄우는 이벤트를 같이 전달하는 예제이다.

Flutter

Flutter에 카운트 이벤트 전달에 사용되는 MehtodChannel과 스낵바 노출을 전달 받는 MethodChannel을 선언해주자.

final MethodChannel _countChannel = const MethodChannel("tyger/count/app");
final MethodChannel _toastChannel = const MethodChannel("tyger/count/toast");

얼마만큼 증감을 할지에 대한 숫자를 선택하는 로직이다. 이 부분은 그냥 Flutter에서 관리하고 있는 값이다.

Future<void> changedSelectCount(int count) async {
    emit(state.copyWith(currentCount: count));
  }
Increment

여기서 부터가 중요하다. 등록한 countChannel을 호출할 때에 invokeMethod를 사용할 수있다. invokeMethod 호출시 콜 사인과 arguments를 전달할 수 있는데, Object, Map 둘 다 가능하다.

콜 사인은 "increment"라고 해주고, count 값으로 얼마만큼 값을 증가시킬지에 대한 값인 state.currentCount를 전달해 주었다.

MethodChannel의 전달되는 count 값으로 state의 count 값을 변경해 주고 있다.

Future<void> increment() async {
    int? _count = await _countChannel.invokeMethod("increment", {
      "count": state.currentCount,
    });
    emit(state.copyWith(count: _count));
  }
Decrement

감소도 증가와 동일하다. Flutter에서는 Native로 부터 받아온 카운트 값을 그냥 보여주기만 하면 된다.

Future<void> decrement() async {
    int? _count = await _countChannel.invokeMethod("decrement", {
      "count": state.currentCount,
    });
    emit(state.copyWith(count: _count));
  }
Reset

카운트를 다시 0으로 변경하는 초기화 기능이다. 증가와 감소와 동일하게 Flutter에서는 Native에서 관리되는 카운트 값만 가지고 올 것이기 때문에 count 값만 변경해주면 된다.

Future<void> countReset() async {
    int? _count = await _countChannel.invokeMethod("reset");
    emit(state.copyWith(count: _count));
  }
Listener

여기 부분은 toastChannel로 전달되는 값을 받는 부분이다. 여태까지 사용한 MethodChannel과는 다르다.

이유는 위에서 살펴본 증감/리셋 기능은 Flutter가 Native에 이벤트를 호출하는 거라면, 여기서는 Native가 호출하는 이벤트를 Flutter에서 수신을 받는 것이다 (Event Channel 과는 다른 개념이기 때문에 혼동하면 안됨).

setMethodCallHandler에서는 Object, Map 형태로 콜백이 오는데, 우리는 네이티브로부터 받은 콜백을 스낵바로 노출만 해주도록 하자.

void listener(BuildContext context) {
    _toastChannel.setMethodCallHandler((call) async {
      String _content = call.method;
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text(_content)));
    });
  }

Kotlin

Android 플랫폼을 작성하도록 하자. Platform Channel 사용이 익숙치 않으신 분들이 많기 때문에 최대한 이해할 수 있는 코드를 제공하도록 하겠다.

Kotlin 파일을 열어보자. 안드로이드 플랫폼 개발은 Android Studio에서 작업하면 편하다.

Flutter 프로젝트를 생성하면 아래와 같이 MainActivity를 FlutterActivity로 사용하는데, 아래에 configureFlutterEngine 코드를 추가해주자.

class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
     }
 }    

Kotlin에서 카운트의 상태를 가져야 하기에 count를 정수형 타입으로 선언해주고, countToastChannel을 등록해주자. MethodChannel 등록시 binaryMessenger와 채널명이 필요하다.

참고로 채널명은 패키지네임 + 이벤트 경로로 작성한다. 패키지 네임을 넣어서 만드는 이유는 타 PlugIn과의 충돌을 방지하기 위함이다.

class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        
        var count : Int = 0

        val countToastChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/toast")
     }
 }    

자 여기서는 MethodChannel을 선언하지 않고 바로 사용을 하였다. 여기는 이벤트 값을 수신받으면 되서 별도로 등록을 하지 않았다.

이제 MethodChannel의 setMethodCallHandler 부분을 보자.

익숙한 코드이다. 바로 Flutter에서도 스낵바 노출을 수신받는 부분에 똑같은 함수가 사용되었었다. 기능적으로 동일하다.

Kotlin 코드를 모르더라도 따라서 작성해보도록 하자.

when 절은 Flutter의 switch-case문이고 생각하면 된다. 자 call.method가 우리가 Flutter에서 호출한 콜사인이 된다.

해당 콜 사인이 reset인 경우 count를 0으로 변경해주고 result.success()를 호출해주고 있다.

위에 코드를 보면 result를 리턴하게 되어있는데, FlutterResult 객체를 리턴하여야 한다. 실패시 플랫폼 에러를 발생시키고 싶다면 result.notImplemented()를 리턴해주면 된다.

"increment", "decrement" 콜 사인이 호출될 때에는 arguments를 받아서 얼마만큼을 증감할지를 전달하고 있어서 Map의 count를 숫자형으로 파싱해서 count 변수를 변경시켜 주고 result를 호출한다.

자 여기서 보면 countToastChannel이 사용되는데, 카운트 값과 전달받은 값을 Flutter로 보내기 위해 invokeMethod에 값을 전달하고 있다. 콜 사인이 따로 사용되지 않아도 되서 간편하게 콜 사인에 호출하였다.

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/app").setMethodCallHandler {
                call, result ->

            when(call.method){
                "reset" -> {
                    count = 0
                    result.success(count)
                }
                "increment" ->{
                    val args : Int? = call.argument<Int>("count")
                    count += args!!
                    result.success(count)
                    countToastChannel.invokeMethod("Count : $count     Argument : $args", null)
                }
                "decrement" -> {
                    val args : Int? = call.argument<Int>("count")
                    count -= args!!
                    result.success(count)
                    countToastChannel.invokeMethod("Count : $count     Argument : $args", null)
                }
                else -> {
                    result.success(null)
                }
            }

        }

지금까지 개발한 Kotlin 코드의 전체 코드이다.

class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        
        var count : Int = 0

        val countToastChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/toast")
     
     MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/count/app").setMethodCallHandler {
                call, result ->

            when(call.method){
                "reset" -> {
                    count = 0
                    result.success(count)
                }
                "increment" ->{
                    val args : Int? = call.argument<Int>("count")
                    count += args!!
                    result.success(count)
                    countToastChannel.invokeMethod("Count : $count     Argument : $args", null)
                }
                "decrement" -> {
                    val args : Int? = call.argument<Int>("count")
                    count -= args!!
                    result.success(count)
                    countToastChannel.invokeMethod("Count : $count     Argument : $args", null)
                }
                else -> {
                    result.success(null)
                }
            }

        }
     
     }
 }    

Swift

이번에는 IOS 플랫폼 코드 작성을 위해 Xcode를 열어 Swift로 개발을 해보자. 로직은 Kotlin과 동일하다고 보면된다.

Flutter 프로젝트 생성시 Swift 코드가 아래와 같이 생성되어있다. Kotlin 구조와 도일하게 Delegate에 FlutterAppDelegate가 사용되고 있는 것을 알 수 있다.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

우선 count 변수를 정수형으로 Kotlin과 동일하게 선언을 해주자. Swift에서는 Kotlin에서 작업한 방식과 조금다르게 selectCount라는 Flutter에서 전달해 줄 값을 미리 선언해 주었다.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

     
    var count : Int = 0
    var selectCount : Int = 1

 	let countToastChannel = FlutterMethodChannel(name: "tyger/count/toast", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
 

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

Kotlin과 동일하게 Swift에서도 MethodChannel을 따로 선언하지 않았다. 여기서도 switch-case 문을 통해서 call.mehtod인 콜 사인을 통해 분기 처리를 해주었고, 전달 받은 arguments 값을 selectCount 값으로 전달하고 있다.

Kotlin에서 설명한 것과 로직은 동일하며, 작업이 끝나고 나면 countToastChannel을 통해 값을 리턴하여 Flutter로 전달하고 있다.

다른 점은 Swift는 result 함수안에 바로 전달 값을 보내주면 된다. 물론 Platform 에러를 전달하고 싶다면 전달할 수 있다. result(FlutterError(...)) 이렇게 FlutterError를 발생시킬 수 있다.

FlutterMethodChannel(name: "tyger/count/app",binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger).setMethodCallHandler({
          [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
          if let args = call.arguments as? Dictionary<String, Any>,
             let param = args["count"] as? Int {
              selectCount = param
          }
          switch call.method {
          case "reset":
              count = 0
              result(count)
              break
          case "increment":
              count += selectCount
              result(count)
              countToastChannel.invokeMethod("Count : \(count)     Argument : \(selectCount)",arguments: nil)
              break
          case "decrement":
              count -= selectCount
              result(count)
              countToastChannel.invokeMethod("Count : \(count)     Argument : \(selectCount)",arguments: nil)
              break
          default:
              break
          }
    })

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/platform_channel/count

Battery Level

지금까지 PlatformChannel의 작동 방식을 이해하기 쉽게 최대한 작성해 봤는데, 이해가 잘 되었나 ? 아직 잘 이해가 되지 않았을 거다. 저도 처음에 Platform 채널에 대해서 알아볼 때는 너무 어려웠다.
이것저것 예제 따라해보고, 네이티브 플랫폼에 대해 조금씩 공부해 나아가다 보니 이제는 제법 익숙해 졌다.

이번에는 좀 더 실용적인 예제를 만들어보자. 위에서 사용한 카운트 앱은 PlatformChannel의 작동 방식과 MethodChannel의 양방향 통신에 대해서 살펴보기 위한 예제이기 때문에 조금 복잡하게 구성을 한듯하다.

이번에 만들어 볼 기능은 배터리 레벨을 가져와서 Flutter에서 띄어주는 방법이다. 참고로 Flutter는 Platform 채널 없이는 디바이스의 배터리 정보를 가져오지 못한다.

이번 예제도 Cubit을 사용했기에 어려우신 분들은 Git에 공유한 파일을 복사해서 사용해 보시기 바란다.

Flutter

먼저 MethodChannel을 등록해주자.

final MethodChannel _levelChannel =
      const MethodChannel("tyger/battery/level");

배터리 레벨을 Native에 요청해서 가져올 것이기에 invokeMethod를 호출하였다. 콜 사인은 "level"로 하였다.

Future<void> getBatteryLevel() async {
    int? _level = await _levelChannel.invokeMethod("level");
  }

Kotlin

MethodChannel의 setMethodCallHandler를 바로 사용하도록 하자.

여기서 보면 getBatteryLevel 함수를 만들어 주었다. getBatteryLevel 함수는 integer 타입을 리턴하여 result.success(level)을 호출하여 배터리 레벨을 Flutter에 콜백으로 넘겨주면 된다.

class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
       
       MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/battery/level").setMethodCallHandler{
            call, result ->
            if(call.method == "level"){
                val level = getBatteryLevel()
                result.success(level)
            }
        }
       
     }
 }    

Kotlin에서 Android 플랫폼 배터리 레벨을 가져오는 방법이다.

 private fun getBatteryLevel(): Int {
    val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
    val batteryLevel : Int = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    return batteryLevel
  }

배터리 레벨을 가져오는 MethodChannel 개발의 전체 Kotlin 코드이다.

class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
       
       MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/battery/level").setMethodCallHandler{
            call, result ->
            if(call.method == "level"){
                val level = getBatteryLevel()
                result.success(level)
            }
        }
       
     }
     
      private fun getBatteryLevel(): Int {
    val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
    val batteryLevel : Int = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    return batteryLevel
  }
 }    

Swift

이어서 Swift 코드도 작성해 보자.

MethodChannel을 사용해서 콜 사인이 level인 경우에 receveBatteryLevel을 리턴하고 있다. 해당 함수의 리턴 타입이 FlutterResult 객체이기 때문에 따로 리턴 키워드를 넣지 않아도 된다.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

	FlutterMethodChannel(name: "tyger/battery/level", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger).setMethodCallHandler({
          [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
          if(call.method == "level"){
              self?.receiveBatteryLevel(result: result)
          }
      })

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

Swift에서 배터리 레벨을 가져오는 코드이다.

private func receiveBatteryLevel(result: FlutterResult) {
     let device = UIDevice.current
     device.isBatteryMonitoringEnabled = true
     if device.batteryState == UIDevice.BatteryState.unknown {
         result(nil)
     } else {
         result(Int(device.batteryLevel * 100))
     }
   }

Swift 코드 전체 소스 코드이다.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

	FlutterMethodChannel(name: "tyger/battery/level", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger).setMethodCallHandler({
          [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
          if(call.method == "level"){
              self?.receiveBatteryLevel(result: result)
          }
      })

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  private func receiveBatteryLevel(result: FlutterResult) {
     let device = UIDevice.current
     device.isBatteryMonitoringEnabled = true
     if device.batteryState == UIDevice.BatteryState.unknown {
         result(nil)
     } else {
         result(Int(device.batteryLevel * 100))
     }
   }
   
 }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/platform_channel/battery_level

Event Channel

MethodChannel에 이어서 EventChannel에 대해서 알아보자. EventChannel은 조금 어려울 수도 있다.

EventChannel은 Stream 구독이라는 것을 잊지 말자. 변경이 있을 때에만 자동으로 데이터를 전달해주는 기능이기 때문에 예제로 디바이스의 충전기가 연결된 상태와 연결이 끊긴 상태를 EventChannel을 통해서 받아오는 예제를 만들어보도록 하겠다.

Flutter <> Native를 EventChannel로 연결하여 디바이스의 충전기가 연결되거나 연결이 끊겼을 때에 EvnetChannel을 작동시켜 결곽 값을 Flutter에서 받아오는 기능이다.

Flutter

Flutter에 EventChannel을 등록하자.

final EventChannel _stateChannel = const EventChannel("tyger/battery/state");

등록한 EventChannel을 구독하면 된다. 이제 네이티브 코드에서 변경사항이 있을 때마다 이벤트가 전달될 것이다.

 _stateChannel.receiveBroadcastStream().listen((event) {
      // event 
    });

Kotlin

조금 복잡하고 어려울 수도있다. Kotlin 파일에 새로운 class를 만들어주어야 하는데, EventChannel.StreamHandler를 받는 객체를 생성해주자.

onListen, onCancel을 재정의 해주어야 한다.

Flutter Stream과 구조가 비슷하다. 구독하는 listen과 해당 구독을 취소하는 cancel 기능을 필수로 생성하여야 한다.

class BatteryStateEventChannel(context: Context) : EventChannel.StreamHandler {

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
    }

    override fun onCancel(arguments: Any?) {
    }

}

먼저 BroadcastReceiver를 만들어주어야 하는데, Receiver 작동에는 연결됨과 연결끊김 이렇게 두 가지의 Receiver가 필요하다.

class BatteryStateEventChannel(context: Context) : EventChannel.StreamHandler {

	private var connectReceiver: BroadcastReceiver? = null
    private var disConnectReceiver: BroadcastReceiver? = null
    private var context: Context = context

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
    }

    override fun onCancel(arguments: Any?) {
    }

}

먼저 onCancel 부분의 구독 취소시 코드를 만들어주자. Receiver 구독을 취소해주고 BroadcastReceiver을 Null로 변경해주면 된다.

class BatteryStateEventChannel(context: Context) : EventChannel.StreamHandler {

	private var connectReceiver: BroadcastReceiver? = null
    private var disConnectReceiver: BroadcastReceiver? = null
    private var context: Context = context

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
    }

    override fun onCancel(arguments: Any?) {
    	context.unregisterReceiver(connectReceiver)
        context.unregisterReceiver(disConnectReceiver)
        connectReceiver = null
        disConnectReceiver = null
    }

}

이번에는 onListen 부분의 코드를 작성해보자. Receiver 객체를 만들어야 하는데, 그 부분은 아래에서 살펴보고 더 중요한 Receiver가 실행되는 시점에 대해서 살펴보도록 하자.

코드를 살펴보면 context로 구독을 연결시켜 주는데, 여기서 구독의 조건은 IntentFilter 부분이다.

연결이 되었을 때와 연결이 끊겼을 때에 맞는 각각의 Receiver를 작동시키는 것이다.

class BatteryStateEventChannel(context: Context) : EventChannel.StreamHandler {

	private var connectReceiver: BroadcastReceiver? = null
    private var disConnectReceiver: BroadcastReceiver? = null
    private var context: Context = context

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
    	connectReceiver = connected(events!!)
        disConnectReceiver = disConnected(events!!)
        context.registerReceiver(connectReceiver, IntentFilter(Intent.ACTION_POWER_CONNECTED))
        context.registerReceiver(disConnectReceiver, IntentFilter(Intent.ACTION_POWER_DISCONNECTED))
    	
    }

    override fun onCancel(arguments: Any?) {
    	context.unregisterReceiver(connectReceiver)
        context.unregisterReceiver(disConnectReceiver)
        connectReceiver = null
        disConnectReceiver = null
    }

}

연결되었을 때의 BroadcastReceiver 부분이다. events.cusscess를 true로 해주었다. 이제 충전기가 연결이 되면 Flutter에 true를 리턴할 것이다.

private  fun connected(events: EventChannel.EventSink) : BroadcastReceiver? {
        return  object : BroadcastReceiver(){
            override fun onReceive(context: Context?, intent: Intent) {
                events.success(true)
            }
        }
    }

연결이 끊겼을 때의 Receiver이며, false를 리턴해 주었다.

 private  fun disConnected(events: EventChannel.EventSink) : BroadcastReceiver? {
        return  object : BroadcastReceiver(){
            override fun onReceive(context: Context?, intent: Intent) {
                events.success(false)
            }
        }
    }

생성한 StreamHandler를 등록해주자.

class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
      
      EventChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/battery/state").setStreamHandler(BatteryStateEventChannel(context))
      
     }
 } 

StreamHandler 전체 코드이다.

class BatteryStateEventChannel(context: Context) : EventChannel.StreamHandler {

	private var connectReceiver: BroadcastReceiver? = null
    private var disConnectReceiver: BroadcastReceiver? = null
    private var context: Context = context

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
    	connectReceiver = connected(events!!)
        disConnectReceiver = disConnected(events!!)
        context.registerReceiver(connectReceiver, IntentFilter(Intent.ACTION_POWER_CONNECTED))
        context.registerReceiver(disConnectReceiver, IntentFilter(Intent.ACTION_POWER_DISCONNECTED))
    }
    
    private  fun connected(events: EventChannel.EventSink) : BroadcastReceiver? {
        return  object : BroadcastReceiver(){
            override fun onReceive(context: Context?, intent: Intent) {
                events.success(true)
            }
        }
    }

    private  fun disConnected(events: EventChannel.EventSink) : BroadcastReceiver? {
        return  object : BroadcastReceiver(){
            override fun onReceive(context: Context?, intent: Intent) {
                events.success(false)
            }
        }
    }

    override fun onCancel(arguments: Any?) {
    	context.unregisterReceiver(connectReceiver)
        context.unregisterReceiver(disConnectReceiver)
        connectReceiver = null
        disConnectReceiver = null
    }

}

Swift

이이서 Swift에도 EventChannel을 연결해 보자.

StreamHandler를 새로운 class로 만들어주자. FlutterEventSink를 Nullable 타입으로 선언해주자.

class BatteryStateStreamHandler: NSObject, FlutterStreamHandler {
    
    var events : FlutterEventSink?
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.events = events    
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        return nil
    }
}

StreamHandler 구독이 취소되면 onCancel에서 events를 다시 Null로 변경할 수 있도록 해주자.

class BatteryStateStreamHandler: NSObject, FlutterStreamHandler {
    
    var events : FlutterEventSink?
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.events = events    
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
    	self.events = nil
        return nil
    }
}

onListen 부분에서 해당 Stream이 수신되는 조건을 추가하여 넣어주자.

class BatteryStateStreamHandler: NSObject, FlutterStreamHandler {
    
    var events : FlutterEventSink?
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.events = events    
        
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        batteryStateDidChange()
        
        NotificationCenter.default.addObserver(self, selector: #selector(self.batteryStateDidChange), name: UIDevice.batteryStateDidChangeNotification, object: nil)
        
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
    	self.events = nil
        return nil
    }
}

스트림이 호출될 때 events에 값을 전달하여 배터리 상태가 unknown, unplugged 일 때에 false를 리턴하고 아닐 때에 true를 리턴해주자.

 @objc private func batteryStateDidChange(){
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        switch device.batteryState {
        case UIDevice.BatteryState.unplugged, UIDevice.BatteryState.unknown:
            self.events!(false)
            break
        default:
            self.events!(true)
            break
        }
    }

AppDelegate에 EvnetChannel을 등록하도록 하자.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    
    FlutterEventChannel(name: "tyger/battery/state", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger).setStreamHandler(BatteryStateStreamHandler())

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

BatteryStateStreamHandler 전체 소스코드이다.

class BatteryStateStreamHandler: NSObject, FlutterStreamHandler {
    
    var events : FlutterEventSink?
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.events = events
        
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        batteryStateDidChange()
        
        NotificationCenter.default.addObserver(self, selector: #selector(self.batteryStateDidChange), name: UIDevice.batteryStateDidChangeNotification, object: nil)
        
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        self.events = nil
        return nil
    }
    
    @objc private func batteryStateDidChange(){
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        switch device.batteryState {
        case UIDevice.BatteryState.unplugged, UIDevice.BatteryState.unknown:
            self.events!(false)
            break
        default:
            self.events!(true)
            break
        }
    }
}

Result

이제 충전기를 연결했다가 충전기 연결을 해제하면서 테스트를 해보자. 정상적으로 작동을 하는 것을 확인할 수 있다.

EventChannel을 사용해서 위에서 MethodChannel의 예제로 사용된 배터리 레벨 기능도 넣을 수 있다. 네이티브 코드에서 StreamHandler의 onListen을 작동만 시켜주면 된다.


Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/platform_channel/battery_level

Basic Message Channel

마지막으로 BasicMessageChannel에 대해서 살펴보도록 하자. BasicMessageChannel은 가장 단순한 MessageCodec을 사용하는 Platform Channel로 단순하지만 거의 사용하지 않는다. MethodChannel 처럼 데이터를 전달하기에 한계가 있다.

저도 BasicMessageChannel은 Platform Channel 개발시 단순한 로그를 출력하거나 디버깅하는 용도로만 사용하고 있어서 단순히 디바이스 네임을 받아오는 정도로만 살펴보도록 하겠다.

Flutter

BasicMessageChannel을 선언하자. StringCodec은 필수 파라미터기 때문에 아래와 같이 등록하면 된다.

final BasicMessageChannel<String> _deviceNameChannel = const BasicMessageChannel<String>("tyger/device/name", StringCodec());

setMessageHandler를 등록하고, 네이티브로부터 호출되는 콜백을 받도록 하자. setMessageHandler에의 리턴 값에 우리가 선언한 String을 리턴해줘야 한다.

_deviceNameChannel.setMessageHandler((String? message) async {
      if (message != null) {
       	//
      }
      return message!;
    });

Kotlin

Kotlin에도 BasicMessageChannel을 선언해주자.

class MainActivity: FlutterActivity() {

    private lateinit var deviceNameChannel : BasicMessageChannel<String>

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)

	}
}    

단순한 String을 전달하는 PlatformChannel로 디바이스의 이름을 리턴하도록 하자.

deviceNameChannel.send(Build.MODEL)

Swift

동일하게 FlutterBasicMessageChannel을 선언하자.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    var deviceNameBasicChannel : FlutterBasicMessageChannel!

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

	})
}
     

사용법은 Kotlin과 동일하다.

self?.deviceNameBasicChannel.sendMessage(UIDevice.current.name)

마무리

지금까지 Flutter에서 네이티브(Kotlin/Swift)와 Platform Channel을 사용해서 통신하는 방법에 대해서 살펴봤다. 자주 사용하지 않으신 분들은 다소 어렵다고 생각할 수 있지만 막상 사용해보면 생각만큼 어렵지는 않다.

물론 네이티브 코드를 사용할 줄 알아야 한다는 점에서 여전히 어렵긴 하다. Flutter에서는 상당히 많은 기능을 이미 PlugIn으로 지원하고 있어서 네이티브 코드를 통한 Platform Channel을 사용하지는 않지만, 실제 프로덕션으로 서비스되고 있는 프로젝트에서는 Flutter로만 처리하는데 한계가 있는 부분이 있어 Platform Channel을 학습해두면 좋은 점은 많다.

네이티브 플랫폼의 대표적인 고유 기능이 디바이스의 정보를 가져오는 기능인데, 이미 Dart Package에서 device_info / device_info_plus 플러그인이 배포되어 있다.

다음번에는 디바이스 고유 정보를 가져와 활용할 수 있는 앱을 EventChannel을 통해서 다뤄보도록 하겠다.

이해가 안되시거나 궁금하신 부분은 언제든 댓글 남겨주시면 답변하도록 하겠다.

profile
Flutter Developer

0개의 댓글