[Flutter]Platform Channel API

한상욱·2025년 9월 30일
0

Flutter

목록 보기
35/35
post-thumbnail

들어가며

Flutter는 선언형으로 UI를 제작하여 빠른 생산성을 보장합니다. 하지만, 각 플랫폼 OS에 특화된 Native 코드를 호출해야되는 경우가 발생할 수 있습니다.

이러한 경우 Platform Channel API 혹은 pigeon 패키지를 공식문서에서 추천하고 있습니다. 오늘은 Platform Channel API만 알아보겠습니다.

왜 Native코드를 호출할까?

Flutter만으로도 앱, 나아가서 웹, 데스크톱 등 여러가지 플랫폼에 특화된 프론트엔드를 제작할 수 있습니다. 다만, 우리는 이러한 상황에서 Native 코드가 필요할지 모릅니다.

  • 플랫폼 하드웨어에 특화된 블루투스, GPS, NFC 등의 기능 또는 갤러리 정보
  • 이미 기존에 만들어진 조직 내 Native 모듈
  • 아직 Flutter로 제공되지 않는 Native 기능

위 예가 아니더라도 여러 상황이 존재할 수 있습니다. 또한, Native 코드는 각 OS에 제일 적합한 개발 언어로 특정 기능에서는 더 뛰어난 성능을 보여줄 수 있습니다.

Platform Channel API


Platform Channel API를 통해서 메세지 형식으로 Native 코드를 호출할 수 있습니다.

Flutter App에서는 메세지와 응답을 Method Channel을 통해 비동기적으로 전달하여 UI를 반응형으로 유지합니다. 반면 플랫폼, Android에서는MethodChannel, iOS에서는 FlutterMethodChannel을 이용하여 함수의 호출을 수신하여 응답을 반환할 수 있습니다.

MethodChannel은 비동기적으로 동작하지만 반드시 플랫폼의 Main Thread에서 해당 메소드를 호출해야합니다.

실제 구현

공식문서에서 제공하는 배터리 정보를 가져오는 플랫폼 코드를 호출하는 예제를 보며 구현방식을 이해해봅시다. Windows, MacOS, Linux도 가능하지만 모바일 플랫폼만 알아보도록 하겠습니다.

Flutter 클라이언트

  static const platform = MethodChannel('samples.flutter.dev/battery');

통신을 위한 MethodChannel을 정의합니다. 내부에 정의된 문자열은 채널의 이름으로 Native에서도 동일한 채널을 사용해야 통신이 가능합니다.

  // Get battery level.
  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final result = await platform.invokeMethod<int>('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

배터리 정보를 가져오는 _getBatteryLevel()메소드입니다. 실행하면 배터리 정보를 Native로부터 가져와서 변수에 할당하게 됩니다.


Widget build(BuildContext context) {
  return Material(
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton(
            onPressed: _getBatteryLevel,
            child: const Text('Get Battery Level'),
          ),
          Text(_batteryLevel),
        ],
      ),
    ),
  );
}

UI는 단순하게 배터리 정보를 화면에 랜더링하게 됩니다.

Android

Android의 kotlin 하위에는 MainActivity.kt 파일이 존재합니다. 내부에는 Android의 Native 코드를 정의할 수 있습니다. 예제에서는 배터리 정보를 BetteryManager를 통해서 가져오게 됩니다.

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

상단에는 import 정보가 필요합니다.

  private val CHANNEL = "samples.flutter.dev/battery"

채널은 Flutter 클라이언트와 동일하게 정의해야 식별이 가능합니다.

  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
  }

위 메소드를 통해서 우리는 Android 디바이스의 배터리 정보를 가져올 수 있습니다. 참고로 에뮬레이터 환경에서도 배터리 정보를 가져올 수 있습니다.

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
        // This method is invoked on the main thread.
        call, result ->
        if (call.method == "getBatteryLevel") {
          val batteryLevel = getBatteryLevel()
  
          if (batteryLevel != -1) {
            result.success(batteryLevel)
          } else {
            result.error("UNAVAILABLE", "Battery level not available.", null)
          }
        } else {
          result.notImplemented()
        }
      }
  }

실질적으로 위 코드에서 MethodChannel호출을 전달받아 배터리 정보를 다시 클라이언트로 반환하게 됩니다.

iOS

ios/Runner 하위에는 AppDelegate.swift가 존재합니다. 내부에는 iOS와 관련된 Native 코드를 정의할 수 있습니다. 예제에서는 device.batteryLevel API로 배터리 정보를 가져옵니다.

let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)

iOS에서는 FlutterMethodChannel을 통해서 Flutter 클라이언트의 채널을 식별하게 됩니다. 마찬가지로 클라이언트와 동일한 식별자를 사용해야 호출이 가능합니다.

  private func receiveBatteryLevel(result: FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    if device.batteryState == UIDevice.BatteryState.unknown {
      result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery level not available.",
                        details: nil))
    } else {
      result(Int(device.batteryLevel * 100))
    }
  }

위 메소드는 device.batteryLevel API를 통해 iOS 디바이스의 배터리 정보를 반환하게 됩니다. 다만, 시뮬레이터 환경에서는 배터리 정보가 반환되지 않습니다.

    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      // This method is invoked on the UI thread.
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      self?.receiveBatteryLevel(result: result)
    })

실질적으로 위 코드에서 채널을 식별하고 배터리 정보를 클라이언트로 반환하는 역할을 수행하게 됩니다.

마무리

이번 글이 Flutter ↔ Native 연동을 고민하는 분들께 도움이 되었길 바랍니다. 지적은 언제나 환영입니다!

profile
자기주도적, 지속 성장하는 모바일앱 개발자의 기록

0개의 댓글