[Flutter] Flutter Method Channel

Raon·2023년 5월 21일
2

Flutter

목록 보기
18/24

플러터를 사용해 앱을 개발하다보면 네이티브 기능을 호출할 수 없는 경우가 찾아오게 된다.
이 때, 두가지 선택지를 고를 수 있는데 하나는 pub.dev에서 원하는 기능에 해당하는 서드파티 패키지를 찾아 활용하는 방법이고, 다른 하나는 오늘 소개할 MethodChannel을 이용하는 방법이다.

Method Channel

메소드 채널은 플러터에서 네이티브에 작성된 함수를 호출하고 그 결과를 플러터 코드로 받을 수 있는 기능인데, 이를 통해 플러터만으로는 구현할 수 없는 다양한 기능을 구현할 수 있다.

실제로 서드파티 패키지들 중 네이티브 기능을 호출해야하는 패키지의 경우(특히 하드웨어 접근과 관련된 기능이 대표적이다)메소드 채널을 활용해 네이티브로 구현되어 있는 함수를 플러터에서 호출하고 그 결과를 활용한다.

아래의 코드는 메소드 채널을 구현한 예제이다.

Flutter 코드

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

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

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
    	home: HomePage(),
    );
  }
}

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

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final MethodChannel _channel =
      const MethodChannel("com.example.native_connection_study");

  String _resultData = "";

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Method Channel')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_resultData),
            const SizedBox(height: 32.0),
            ElevatedButton(
              // 메소드 채널의 결과는 Future타입으로 반환되므로 async 키워드를 이용한다.
              onPressed: () async {
              	// 메소드 채널을 호출할 때는 invokeMethod를 통해 호출한다.
                final result = await _channel
                    .invokeMethod("getNativeData", ["Raon", "29"]);

                setState(() {
                  _resultData = result.toString();
                });
              },
              child: const Text("Call Native Method"),
            ),
          ],
        ),
      ),
    );
  }
}

MethodChannel객체는 네이티브와 통신할 수 있는 채널을 연결한다. 이 때 "com.example.native_connection_study"와 같이 패키지 이름으로 채널을 연결하는데, 굳이 패키지 이름으로 할 필요는 없지만 구분이 확실한 이름으로 채널명을 지정하는 것이 좋다.

channel.invokeMethod()를 보면 두 개의 파라미터를 전달하는데, 이는 각각 네이티브 함수 호출 이름, 네이티브로 전달하는 값을 의미한다.

여기서는 getNativeData라는 이름으로 네이티브 함수를 호출하는데, 이 때, 전달되는 값은 "Raon""29"라는 값이다.

Native 코드 (Swift)

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      GeneratedPluginRegistrant.register(with: self)
          
      // 작성된 코드 영역 시작
      
      // MethodChannel 연결
      let controller = window?.rootViewController as! FlutterViewController
      let channel: FlutterMethodChannel = FlutterMethodChannel(
        name: "com.example.native_connection_study",
        binaryMessenger: controller.binaryMessenger)
      
      // 핸들러를 이용한 네이티브 코드 실행
      channel.setMethodCallHandler({
          [weak self] (methodCall: FlutterMethodCall, result: FlutterResult) -> Void in
          // Flutter코드에서 명시된 호출명 구분
          if(methodCall.method == "getNativeData") {
              result(self?.getNativeData(arguments: methodCall.arguments))
              return
          }
          result(FlutterMethodNotImplemented)
      })
      // 작성된 코드 영역 끝
      
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
    // 새로 작성한 함수
    private func getNativeData(arguments:Any?) -> Any {
    	// Flutter에서 arguments가 전달될 때는 어차피 배열로 전달되므로 Array<Any>?로 캐스팅한다.
        let args = arguments as? Array<Any>
        
        // Flutter로 전달해줄 예제 데이터 생성
        let jsonData : [String: Any] = [
            "name": args?[0] ?? "None",
            "age": args?[1] ?? "None"
        ] as Dictionary
        
        // Json으로 변환
        var jsonObj : String = ""
        do {
            let jsonCreate = try JSONSerialization.data(withJSONObject: jsonData, options: .prettyPrinted)
            jsonObj = String(data: jsonCreate, encoding: .utf8) ?? ""
        }catch{
            jsonObj = ""
        }
        return jsonObj
    }
}

이 코드는 flutter 프로젝트를 처음 생성하면 자동으로 만들어지는 AppDelegate.swift파일에 추가로 작성된 코드이다.

Swift를 경험해보지 못한 사람이 보면 장황하게 길어 보여 다소 복잡하게 느낄수 있는데, 크게 3부분으로 나눌 수 있다.

1. MethodChannel 연결

let controller = window?.rootViewController as! FlutterViewController
let channel: FlutterMethodChannel = FlutterMethodChannel(
        name: "com.example.native_connection_study",
        binaryMessenger: controller.binaryMessenger)

이 부분이 MethodChannel을 연결하는 부분인데, 플러터 코드의

final channel = MethodChannel("com.example.native_connection_study");

에 대응하는 코드로 볼 수 있다.

2. Handler 설정

channel.setMethodCallHandler({
          [weak self] (methodCall: FlutterMethodCall, result: FlutterResult) -> Void in
          // Flutter코드에서 명시된 호출명 구분
          if(methodCall.method == "getNativeData") {
              result(self?.getNativeData(arguments: methodCall.arguments))
              return
          }
          result(FlutterMethodNotImplemented)
      })

핸들러는 FlutterMethodChannel.setMethodCallHandler()로 작성할 수 있다. 핸들러는 기본적으로 FlutterMethodCall, FlutterResult를 전달받는데, 각각에 대한 설명은 아래와 같다.

FlutterMethodCall : Flutter에서 invokeMethod가 수행되면서 전달된 객체로, method, arguments 변수를 가지고 있다.

  • method : invokeMethod에서 정의된 호출 명
  • arguments : invokeMehtod의 arguments

FlutterResult : 네이티브코드에서 수행된 로직의 결과를 Flutter코드로 전달하기 위한 객체.

3. 필요한 로직 작성

private func getNativeData(arguments:Any?) -> Any {
	// Flutter에서 arguments가 전달될 때는 어차피 배열로 전달되므로 Array<Any>?로 캐스팅한다.
    let args = arguments as? Array<Any>
        
	// Flutter로 전달해줄 예제 데이터 생성
	let jsonData : [String: Any] = [
		"name": args?[0] ?? "None",
        "age": args?[1] ?? "None"
    ] as Dictionary
        
    // Json으로 변환
	var jsonObj : String = ""
	do {
		let jsonCreate = try JSONSerialization.data(withJSONObject: jsonData, options: .prettyPrinted)
		jsonObj = String(data: jsonCreate, encoding: .utf8) ?? ""
	}catch{
		jsonObj = ""
	}
    
	return jsonObj
}

이 예제에서는 Json으로 결과를 반환하는 함수를 작성했다. 굳이 Json이 아니더라도 필요에 따라 원하는 결과를 반환하면 된다.

Native 코드 (Kotlin)

package com.example.native_connection_study

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import org.json.JSONObject

class MainActivity: FlutterActivity() {
	
    // 작성된 코드 영역 시작
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        GeneratedPluginRegistrant.registerWith(flutterEngine)

        val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.native_connection_study")
        channel.setMethodCallHandler { methodCall, result ->
            if(methodCall.method == "getNativeData"){
                result.success(getNativeData(methodCall.arguments))
            }else{
                result.notImplemented()
            }
        }
    }

    private fun getNativeData(arguments: Any?): Any {
        val args: List<Any> = (arguments as List<Any>?) ?: mutableListOf()
        val json = JSONObject(mapOf("name" to args[0], "age" to args[1]).toString())
        return json.toString()
    }
    // 작성된 코드 영역 끝
}

Swift와 거의 동일한 구조로 작성되었다. 다른 점이 있다면 binaryMessenger를 호출하기 위해 참조해야할 객체나, kotlin과 swift라는 언어적 차이에서 발생하는 문법 정도가 다를 수 있다.

이렇게 Flutter에서 네이티브 코드를 호출하는 방법에 대해 알아보았다.

profile
Flutter 개발자

0개의 댓글