(Flutter-기초 강의) 17. Channel을 이용하여 Native Language 사용하기

소개

Flutter는 주로 Dart 언어를 사용하지만, 때때로 특정 기능을 위해 운영체제 별 네이티브 API를 사용해야 할 필요가 있습니다.

안드로이드는 JAVA 혹은 Kotlin, iOS는 Object-C나 Swift, Windows는 C++, macOS는 Objective-C, Linux는 C
이처럼 운영체제에 최적화된 언어로 작성된 소스를 네이티브 언어(Native Language)라고 합니다.

Flutter는 이러한 네이티브 언어로 작성된 코드와 통신할 수 있는 메커니즘으로 Platform Channel을 통하여 Message를 전달하는 방식을 제공합니다.

Platform Channel

개요

Platform Channel은 Dart(플러터) 코드와 네이티브 코드(예: Java, Kotlin, Swift) 간의 통신을 가능하게 하는 메커니즘입니다.

이를 통해 Flutter 앱에서 안드로이드나 iOS의 네이티브 기능을 사용할 수 있습니다.

예를 들어, 배터리 상태 확인, 카메라 기능 접근, 센서 데이터 사용 등이 이에 해당합니다.

구조

  • Flutter App (Client): Flutter로 개발된 애플리케이션을 나타냅니다.
  • MethodChannel: Flutter 앱과 네이티브 플랫폼 간의 통신을 위해 사용되는 채널입니다. MethodChannel을 통해 Flutter 앱은 메시지를 보내고, 네이티브 코드는 이 메시지를 수신하여 처리합니다.
  • FlutterMethodChannel: 이는 구체적으로 Flutter에서 MethodChannel을 사용할 때의 인스턴스를 나타냅니다. Flutter 코드에서 MethodChannel을 선언하고, 네이티브 코드와 통신할 때 사용합니다.
  • iOS Host: iOS에서 Flutter 앱을 호스팅하는 부분입니다. AppDelegateFlutterViewController는 Flutter 앱을 iOS 애플리케이션 내에서 실행하는 역할을 합니다. 여기서 iOS 플랫폼 API나 제3자 API를 호출할 수 있습니다.
  • Android Host: 안드로이드에서 Flutter 앱을 호스팅하는 부분입니다. Activity는 안드로이드 애플리케이션의 진입점으로, FlutterView를 통해 Flutter 앱이 표시됩니다. 안드로이드 플랫폼 API나 제3자 API를 호출할 수 있습니다.
Platform Channel 구조

작동 방식

  1. Flutter 앱에서 MethodChannel을 통해 메시지를 보냅니다.
  2. 해당 메시지는 iOS나 안드로이드 호스트로 전달됩니다.
  3. 호스트는 해당 메시지를 받아 네이티브 코드를 통해 처리합니다. 예를 들어, 배터리 상태를 확인하거나 카메라를 열 수 있습니다.
  4. 네이티브 처리 결과는 다시 MethodChannel을 통해 Flutter 앱으로 전달됩니다.
  5. Flutter 앱은 이 결과를 받아 사용자에게 보여주거나 다른 처리를 할 수 있습니다.

데이터 타입

개요

Flutter와 네이티브 사이에서 데이터를 주고받을 때는 서로 다른 언어 간의 데이터 타입 호환성을 고려해야 합니다.

예를 들어, Dart의 String은 Java의 String, iOS의 NSString과 호환됩니다.

이러한 변환은 Flutter 시스템이 자동으로 처리해 줍니다.

언어(플랫폼) 별 데이터 타입

DartJava (Android)Kotlin (Android)Obj-C (iOS/macOS)Swift (iOS)C++ (Windows)C (Linux)
nullnullnullNSNullnilstd::nullptr_tNULL
boolbooleanBooleanBOOLBoolboolbool
intintIntNSIntegerIntint32_tint32_t
doubledoubleDoubledoubleDoubledoubledouble
StringStringStringNSStringStringstd::stringconst char*
Uint8Listbyte[]ByteArrayFlutterStandardTypedData (binary)FlutterStandardTypedData (binary)std::vector<uint8_t>uint8_t[]
Int32Listint[]IntArrayFlutterStandardTypedData (int32)FlutterStandardTypedData (int32)std::vector<int32_t>int32_t[]
Int64Listlong[]LongArrayFlutterStandardTypedData (int64)FlutterStandardTypedData (int64)std::vector<int64_t>int64_t[]
Float64Listdouble[]DoubleArrayFlutterStandardTypedData (float64)FlutterStandardTypedData (float64)std::vector<double>double[]
ListListListNSArrayArraystd::vector<EncodableValue>Array
MapMapMapNSDictionaryDictionarystd::map<EncodableValue, EncodableValue>Map

예제

Platform Channel을 사용하여 핸드폰 정보를 받아오는 예제 구현해보겠습니다.

1. 플랫폼 별 Class 생성

iOS에서 사용될 CupertinoNativeApp Class (CupertinoNativeApp)을 정의하고
Android에서 사용될 AndroidNativeApp Class (NativeApp)을 표시하도록 합니다.

/lib/main/android_native_app.dart (AndroidNativeApp)

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class AndroidNativeApp extends StatefulWidget {
  const AndroidNativeApp({super.key});

  @override
  State<StatefulWidget> createState() => _AndroidNativeApp();
}

class _AndroidNativeApp extends State<AndroidNativeApp> {
  @override
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

/lib/main/cupertino_native_app.dart

class CupertinoNativeApp extends StatefulWidget {
  const CupertinoNativeApp({super.key});

  @override
  State<StatefulWidget> createState() {
    return _CupertinoNative();
  }
}

class _CupertinoNative extends State<CupertinoNativeApp> {
  @override
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

2. 플랫폼 확인

dart:io를 활용하여 Platform을 확인하고, iOS일 때는 CupertinoNativeApp을 표시하고
Android일 때는 NativeApp을 표시하도록 합니다.

/lib/main.dart

import './main/android_native_app.dart';
import './main/cupertino_native_app.dart';

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    if (Platform.isIOS) {
      return const CupertinoApp(
        home: CupertinoNativeApp(),
      );
    } else {
      return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const AndroidNativeApp(),
      );
    }
  }
}

3. Android에 message 전달

Message를 전달하기 위한 MethodChannel을 이용하려면 serivces 패키지를 포함해야합니다.

import 'package:flutter/services.dart';

MethodChannel() 을 이용하기 위해 변수로 정의하고 (MethodChannel 내부 parameter는 어떤 통신을 할지 구분하는 키값)
휴대폰 정보를 받아오기 위한 변수도 정의합니다.

class _AndroidNativeApp extends State<AndroidNativeApp> {
  String _deviceInfo = '';
  static const platformMsg = MethodChannel('com.flutter.dev/info');

이 변수를 이용하여, 휴대폰 정보를 가져오는 내부함수를 정의합니다.

  Future<void> _getDeviceInfo() async {
    String batteryLevel;
    try {
      final String result = await platformMsg.invokeMethod('getDeviceInfo');
      batteryLevel = 'Device info : $result';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get Device info: '${e.message}'.";
    }
    setState(() {
      _deviceInfo = batteryLevel;
    });
  }

4. Android Native API 호출

이제 메시지를 받아서 Android에서 처리하는 방법을 알아봅니다.

본인 프로젝트 이름은 android_api 입니다.

아래 파일에서 메시지를 처리할 수 있는 함수를 override 해줍니다.

android.os.Buildandroidx.annotaion.NonNull 은 안드로이드에서 제공하는 기능을 사용하기 위함이고 나머지는 flutter 의 기능을 쓰기 위함입니다.

앞서 사용한 MethodChannel의 Key 값(com.flutter.dev/info)을 초기화하, configureFlutterEngine() 을 override하여 MethodChannel로 들어오는 요청의 응답코드를 작성합니다.

이제 ‘getDeviceInfo’ 메시지를 처리할 함수를 정의한다.

​앞서 안드로이드에서 추가한 Build를 사용하여 DEVICE, BRAND, MODEL을 이용하여 디바이스 정보를 string으로 변환하여 반환합니다.

app/src/main/kotlin/com.example.android_api_1/MainActivity.kt

package com.example.android_api

import android.os.Build
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.flutter.dev/info"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
                if(call.method == "getDeviceInfo") {
                    val deviceInfo = getDeviceInfo()
                    result.success(deviceInfo)
                }
            }
    }

    private fun getDeviceInfo() : String {
        val sb = StringBuffer()
        sb.append(Build.DEVICE+"\n")
        sb.append(Build.BRAND+"\n")
        sb.append(Build.MODEL+"\n")
        return sb.toString()
    }
}

5. 반환 결과 표시

Android에서 표시할 AndroidNativeApp에 getDeviceInfo를 통해 받아온 String을 표시합니다.

/lib/main/android_native_app.dart

class _AndroidNativeApp extends State<AndroidNativeApp> {
  String _deviceInfo = 'Unknown Info';
  static const platformMsg = MethodChannel('com.flutter.dev/info');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Natvie API Example'),
      ),
      body: Container(
        child: Center(
          child: Text(
            _deviceInfo,
            style: const TextStyle(fontSize: 30),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _getDeviceInfo();
        },
        child: const Icon(Icons.get_app),
      ),
    );
  }

  Future<void> _getDeviceInfo() async {
    String batteryLevel;
    try {
      final String result = await platformMsg.invokeMethod('getDeviceInfo');
      batteryLevel = 'Device info : $result';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get Device info: '${e.message}'.";
    }
    setState(() {
      _deviceInfo = batteryLevel;
    });
  }
}

결과

Platform Channel

참고 링크

  1. 내 Github 저장소-devitworld-flutter-basic (_6_android_api)
  2. Flutter Docs – Platform Channels
  3. Flutter Docs – Platform Integration
  4. DEV.to – Platform Channels Guide
  5. DhiWise – Ultimate Guide to Flutter Platform Channels

Leave a Comment