9. 플러터 : 플랫폼 채널 기본 (2)

플러터는 안드로이드 스튜디오에서 코딩한다.
다른 IDE도 가능하겠지만
안드로이드 프로젝트를 주로 안드로이드 스튜디오에서 했기 때문에
그나마 이게 편하다.

특이하게도 플러터 프로젝트에 포함된 아이폰 프로젝트는 xcode에서 코딩할 수도 있게
안드로이드 프로젝트는 별도의 안드로이드 스튜디오에서 코딩할 수도 있게 만들었다.
자신 있는 코드라면 별도의 IDE 없이 코딩하면 되겠지만
자동완성을 비롯해 불편한 점이 많을 수 있다.
여기서는 별도의 IDE를 열고 설명하는 것이 불편하기 때문에
플러터 프로젝트 안에서 모두 처리하도록 한다.
말했다시피 에러가 발생하면 굉장히 곤란할 수 있음을 명심한다.

프로젝트를 완성한 다음에 성공적으로 구동하면
아래와 같은 화면을 볼 수 있다.
왼쪽은 처음 구동했을 때,
오른쪽은 아이폰/안드로이드에 데이터를 수신한 후의 모습이다.


아이폰 먼저 한다.
아이폰 소스코드는 ios 폴더의 Runner 폴더의 AppDelegate.swift에 있다.
플러터 프로젝트를 생성할 때
스위프트 옵션을 설정했기 때문에 확장자로 swift가 붙었다.

스위프트의 코드는 참 간결하다.
그런데 옵셔널 등의 다른 언어와는 다른, 반드시 이해해야 하는 문법들이 존재한다.
이것저것 참고해서 코드를 구성하기는 했는데
훨씬 효율적이고 정리가 잘 된 코드가 존재할 거라 확신한다.

xcode에서 코드를 입력하는 것이 자동완성으로 인해 너무 편했다.
xcode에서 수정하면 안드로이드 스튜디오에서 바로 반영되니까 xcode를 사용하지 않기는 어렵겠다.
윈도우 운영체제라면.. 글쎼.. 어떻게 해야 하나?
아이폰 시뮬레이터 실행은 안드로이드 상단에 위치한 도구막대에서 선택하면 된다.
자꾸 안 뜬다고 화내지 말기로 하자.

소스코드에 대한 설명은 아래 있는 안드로이드 코드를 보면 될 것 같다.
스위프트를 다시 보기 싫어서 그런다.
보고 나면 또 잊어먹을 것이고 다시 볼 일이 없을 거라고 믿는다.
이 부분은 다른 개발자에게 맡기는 걸로.
가령, 큰 아들 우재?

import UIKit

import Flutter


@UIApplicationMain

@objc class AppDelegate: FlutterAppDelegate {

    override func application(

        _ application: UIApplication,

        didFinishLaunchingWithOptions

        options: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        let flutterView = window?.rootViewController as! FlutterViewController;

        let channel = FlutterMethodChannel(name: "mobile/parameters",

                                           binaryMessenger: flutterView)

        

        channel.setMethodCallHandler {

            (call: FlutterMethodCall, result: FlutterResult) in

            switch (call.method) {

            case "getMobileString":

                result("IOS")

            case "getMobileNumber":

                guard let args = call.arguments else {

                    return

                }

                let myArgs = args as! [String: Int32]

                let left = myArgs["left"]

                let rite = myArgs["rite"]

                result(left! * rite!)

            case "getMobileArray":

                let words = ["first", "second", "third"]

                result(words)

            default:

                result(FlutterMethodNotImplemented)

            }

        }

        

        // 빌트인 코드

        GeneratedPluginRegistrant.register(with: self)

        return super.application(application,

                                 didFinishLaunchingWithOptions: options)

    }

}


스위프트 버전을 만들 때 참고한 사이트를 소개한다.
특히 두 번째 사이트가 아이폰과 안드로이드 양쪽에 대해 체계적으로 설명하고 있다.
난 다 읽지는 않았고, 필요한 부분만 발췌해서 사용했다.

Flutter plugin: invoking iOS and Android method including parameters not working
Flutter Platform Channels


처음에 생각없이 오브젝티브-C로 만든 코딩을 했다.
어렵게 구한 코드가 스위프트가 아니었다는 이유로.
따로 저장하는 것도 이상하고 해서 오브젝티브-C 코드도 추가한다.
스위프트 옵션을 설정하지 않았을 때 사용한다.
AppDelegate.m 파일이다.

#include "AppDelegate.h"

#include "GeneratedPluginRegistrant.h"


@implementation AppDelegate


- (BOOL)application:(UIApplication *)application

        didFinishLaunchingWithOptions:(NSDictionary *)options {

    FlutterViewController* controller =

        (FlutterViewController*) self.window.rootViewController;


    // 채널 이름은 플러터와 정확히 일치해야 한다.

    FlutterMethodChannel* channel = [FlutterMethodChannel

        methodChannelWithName: @"mobile/parameters" binaryMessenger: controller];

    [channel setMethodCallHandler: ^(FlutterMethodCall* call, FlutterResult result) {

        if([@"getMobileString" isEqualToString: call.method]) {

            result(@"IOS");

        }

        else if([@"getMobileNumber" isEqualToString: call.method]) {

            // 전달 받은 key를 사용해서 데이터 추출

            NSNumber* left = call.arguments[@"left"];

            NSNumber* rite = call.arguments[@"rite"];


            // NSNumber 객체로부터 int 추출

            result(@(left.intValue * rite.intValue));

        }

        else if([@"getMobileArray" isEqualToString: call.method]) {

            NSArray* words = @[@"first", @"second", @"third"];

            result(words);

        }

        else {

            result(FlutterMethodNotImplemented);

        }

    }];


    // 빌트인 코드

    [GeneratedPluginRegistrant registerWithRegistry:self];

    return [super application:application didFinishLaunchingWithOptions:options];

}

@end


안드로이드는 아이폰과 비슷하다.
정확하게는 두 가지가 서로 비슷하다.
프로젝트를 구성하는 방법에 있어서 다르기 때문에 살짝 달라보이는 것일 뿐이다.

안드로이드 소스코드는
android, app, src, main, java 폴더를 따라 들어가면 마지막에 있는 MainActivity.java 파일에 있다.
플러터 프로젝트를 생성할 때
코틀린 옵션을 설정하지 않았기 때문에 확장자로 java가 붙었다.

플러터와 통신하기 위해서는
메소드 채널(MethodChannel) 객체를 만들고
메소드 채널 객체의 핸들러(setMethodCallHandler)를 등록하고
핸들러 안에서 처리할 이벤트를 구분하는 onMethodCall 함수를 구현하면 된다.

아래 코드에서 주의깊게 볼 부분은
플러터쪽으로 데이터를 전달하는 Result 객체에 반환값을 넣는 부분이다.
모든 데이터를 반환할 수 있어야 하기 때문에
Result 객체에는 무엇이건 넣을 수 있지만 1개만 넣을 수 있고, 플러터쪽에서 정확하게 자료형을 일치시켜야 한다.

package tf.com.gluesoft.tflite_flutter_1;

import android.os.Bundle;
import java.util.ArrayList; // 배열 사용 필수

// 기본적으로 포함되어 있는 모듈
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;

// MethodChannel 관련 모듈 (추가해야 함)
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.MethodCall;

public class MainActivity extends FlutterActivity {
// 채널 문자열. 플러터에 정의한 것과 동일해야 한다.
private static final String mChannel = "mobile/parameters";

@Override
protected void onCreate(Bundle savedInstanceState) {
// 빌트인 코드
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);

// 추가한 함수.
new MethodChannel(getFlutterView(), mChannel).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// 각각의 메소드를 switch로 분류. 아이폰에서는 else if 사용.
switch (call.method) {
case "getMobileString":
result.success("ANDROID");
break;
case "getMobileNumber":
// 매개 변수 추출
final int left = call.argument("left");
final int rite = call.argument("rite");
result.success(left * rite);
break;
case "getMobileArray":
// 배열 생성 후 전달. success 함수에 넣으면 json으로 인코딩된다. 플러터에서는 json 문자열 디코딩.
ArrayList<String> words = new ArrayList<>();
words.add("one");
words.add("two");
words.add("three");
result.success(words);
break;
default:
result.notImplemented();
}
}
}
);
}
}


안드로이드 코드 구성 중에 문제가 있는 것처럼 보일 수도 있다.
가령, "Cannot resolve symbol android" 에러가 뜨지만 잘못된 것은 아니다.
플러터 프로젝트에서 안드로이드 프로젝트 인식을 말끔하게 하지 못하는 것처럼 보인다.

이 부분은 안드로이드 프로젝트만 떼어서
다른 안드로이드 스튜디오로 열어서 gradle sync를 해도 달라지지 않는다.

오른쪽 상단에 Setup SDK 명령이 뜨면,
gradle 파일에 있는 버전 선택하면 해결된다. 꼭 해당 버전을 선택해야 한다.

여러 가지가 섞이니까
뭘 만들던지 쉽게 되지 않는다.
잘 따라왔다.