ble에서 custom_id를 advertising 하는 기능이 없는 이슈로..네이티브 통신을 통해 해결하기로 했다.

우선 나는 Method channel에 대해 1도 모르기 때문에 공부를 해보고자 한다.
그럼 시작~!

Platform channels

메세지는 클라이언트인 ui에서 host인 플랫폼으로 전달된다.
이는 플러터에서 native code(and, ios)를 동작하고 싶을 때 사용하는 인터페이스이다.

클라이언트 측에서, MethodChannel은 메세지를 보낼 수 있다.

  • Android Method Channel : MethodChannelAndroid
  • IOS Method Channel : MethodChanneliOS
    method channel은 message를 받을 수 있고, responding을 보낼 수도 있다.

Create the Flutter platform client

공식문서의 예시에서는 배터리를 보여주는 플랫폼 채널로 예시를 들고 있다. 이를 통해 어떤 과정을 거쳐 method channel을 열게 되는지를 살펴보자.

1. 채널 생성

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  // Get battery level.

클라이언트와 호스트는 채널 생성자로 보내진 채널 이름을 통해 연결된다. 모든 채널 이름은 유니크 해야 한다고 한다. 예시에서는 다음과 같은 채널 이름을 제시하고 있다. : samples.flutter.dev/battery.

2. method channel에 method를 불러온다.

다음으로 메서드를 메서드 채널에 불러온다. 플랫폼이 플랫폼 api를 제공하지 않는다면 실패할 수도 있다고 한다.(예를 들면 시뮬레이터..)
사실 이 부분이 좀 무시무시해서 무슨 뜻인지 찾아봤는데, 애뮬레이터에서 호환이 안 된다고 해도 어플에선 잘 돌아가는 경우도 있다고 한다. 솔직히 좀 공포스럽긴 하지만 일단은 넘어가자.

// 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() : 이를 통해 디바이스의 배터리 레벨을 가져온다. 이 함수는 비동기로 되어 있어서 await를 통해 플랫폼에서의 작업이 완료 될 때까지 대기한다.
  • try-catch : 예외 처리를 위해 사용됩니다. invokeMethod를 호출할 때, 만약 플랫폼에서 해당 메서드를 지원하지 않거나 다른 이유로 호출에 실패하면 PlatformException이 발생할 수 있다. 이 예외를 처리하기 위해 catch 블록에서 실패 이유를 가져와서 _ batteryLevel 변수에 적절한 메시지를 할당해서 확인할 수 있도록 한다.
  • setState() : 이 함수를 사용하여 _ batteryLevel 변수의 값을 업데이트하고, Flutter 프레임워크에게 위젯 트리를 다시 그리도록 알려준다. 이를 통해 사용자 인터페이스 업데이트가 완료된다.
  • Future : 비동기 함수이다. 결과를 기다렸다가 나중에 가져오는 역할을 한다.
    • 여기서 platform.invokeMethod('getBatteryLevel');를 통해 플랫폼 결과를 가져오고, 해당 결과를 기다린 다음 _ batteryLevel 변수를 업데이트 한다. 이때, await 키워드가 사용되어 호출한 메서드가 완료 될때까지 기다린다.이를 위해 사용하는 것이 바로 future이다.

3. build 재정의(ui)

마지막으로, build 메서드를 재정의해서 battery state를 나타내 줄 수 있는 작은 버튼을 예시로 생성해본다.

@override
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),
        ],
      ),
    ),
  );
}

Add an Android platform-specific implementation

난 자바가 좋으므로(사실 코틀린 모른다...) 이 부분은 자바로 살펴보도록 하겠다.

intro

flutter 앱의 android host 부분을 android studio에서 연다. 이 부분에 대한 과정은 공식 사이트에서 다음과 같이 정의하고 있다.
1. Start Android Studio
2. Select the menu item File > Open…
3. Navigate to the directory holding your Flutter app, and select the android folder inside it. Click OK.
4. Open the MainActivity.java file located in the java folder in the Project view.

MethodChannel 만들기

앞에서, 통신을 위헤 methodChannel을 만들어야 한다고 말한 바 있다. 그러므로 andriod단에 통신을 위한 채널을 만들어야 한다.
그러므로 MethodChnnel을 생성하고 MethodCallHandler 내부에 configureFlutterEngine() method를 set한다.
중요한 것은, flutter client에서 사용했던 채널 이름과 같은 이름을 사용해야 한다는 것이다.

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

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "samples.flutter.dev/battery";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
  super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // This method is invoked on the main thread.
            // TODO
          }
        );
  }
}

궁금해서 configureFlutterEngine의 형태를 살펴봤는데, 대강 이렇게 된다.

@Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    if (delegate.isFlutterEngineFromHost()) {
      // If the FlutterEngine was explicitly built and injected into this FlutterActivity, the
      // builder should explicitly decide whether to automatically register plugins via the
      // FlutterEngine's construction parameter or via the AndroidManifest metadata.
      return;
    }

    GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine);
  }
  • String CHANNEL = "samples.flutter.dev/battery"; : 채널명이다. 같은 이름으로 구성한다.
  • configureFlutterEngine() : 이 메서드는 Flutter 엔진을 구성하고 플러그인과 네이티브 코드 간의 통신을 처리하기 위한 MethodChnnel을 설정한다.
    • super.configureFlutterEngine(flutterEngine) : 이 코드는 부모 클래스인 FlutterActivity의 configureFlutterEngine() 메서드를 호출한다. 이를 통해 기본적은 flutter 엔진 구성이 초기화 된다.
    • new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) : 이 부분은 MethodChannel을 생성하는 코드이다. MethodChannel은 Flutter와 네이티브 플랫폼 간의 통신을 수행하는데 사용된다. 여기서 flutterEngine.getDartExecutor().getBinaryMessenger()는 flutter 엔진에서 사용되는 dart 코드와 네이티브 플랫폼 사이의 통신을 담당하는 BinaryMessenger를 가져온다. 이 메신저를 사용해서 flutter와 네이티브 사이에 메세지를 주고 받는 것이 가능해진다. 또한 channel은 이 메서드 채널의 이름을 나타낸다.(flutter의 client와 동일하게 잡아줘야 한다.)
    • .setMethodCallHandler((call, result) -> { / TODO / }); : 이 부분은 MethodChannel이 Flutter 측에서 호출된 메서드 호출을 처리하는 방법을 정의한다. 람다 표현식을 사용해서 메서드 호출 핸들러를 정의하고 있다. 이 핸드러는 메서드 호출을 수신하면 실행된다. 이 부분에선 특정 메서드 호출을 수신햇을 때 수행할 동작이 구현되어야 한다.
  • call, result
    • call은 method와 argument라는 두가지 프로퍼티를 가지고 있다.
      • method : Flutter에서 호출한 함수의 이름정보
      • argument : Flutter에서 네이티브 함수를 호출

Battery 예시

공식 문서에 있는 단계별 예시를 살펴보자!

1. import 하기

이 과정에선 필요한 import들을 전부 적어준다.

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 android.os.Bundle;

2. activoty class에 method 넣기

configureFlutterEngine()에 다음의 method를 넣어 준다고 한다.

private int getBatteryLevel() {
  int batteryLevel = -1;
  if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
    BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
    batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
  } else {
    Intent intent = new ContextWrapper(getApplicationContext()).
        registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
        intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
  }

  return batteryLevel;
}

안드로이드 기기의 베터리 레벨을 가져오는 함수이다. 동작 방식은 다음과 같다.

  • getBatteryLevel() 함수는 정수형 값을 반환합니다.
  • batteryLevel 변수를 -1로 초기화 한다.
  • 안드로이드 기기의 sdk 버전이 LOLLIPOP(5.0) 이상인지 확인하고 시행한다.
  • LOLLIPOP 이상인 경우, BatteryManager 클래스를 사용하여 배터리 관리자를 가져온다.
    • getSystemService(BATTERY_SERVICE)를 호출하여 배터리 서비스를 가져온다.
    • batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)를 호출하여 배터리 레벨을 가져온다.
  • LOLLIPOP 미만인 경우, ContextWrapper를 사용하여 getApplicationContext()를 통해 컨텍스트를 가져온 후 registerReceiver()를 호출하여 배터리 관련 정보를 받아온다.
    • Intent.ACTION_BATTERY_CHANGED 액션으로 등록된 브로드캐스트 수신기를 통해 배터리 정보를 가져온다.
    • intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)를 호출하여 배터리 레벨을 가져오고, intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)을 호출하여 배터리 스케일을 가져온다.
    • 배터리 레벨을 스케일로 나누어 백분율로 변환한다.
  • 블루투스도 버전에 따라 가져오는 방식이 다를지..이건 해 봐야 알 것 같다.

3. setMethodCallHandler()를 완성한다.

앞에 있었던 to-do 부분을 완성하면 되는 것이다!

(call, result) -> {
  // This method is invoked on the main thread.
  // TODO
} 
(call, result) -> {
  // This method is invoked on the main thread.
  if (call.method.equals("getBatteryLevel")) {
    int batteryLevel = getBatteryLevel();

    if (batteryLevel != -1) {
      result.success(batteryLevel);
    } else {
      result.error("UNAVAILABLE", "Battery level not available.", null);
    }
  } else {
    result.notImplemented();
  }
}

앞에서 정의한 activy 함수인 getBatteryLevel()을 처리하기 위한 것이다.
메서드 호출 핸들러를 구현하여 특정 메서드 호출을 처리하고, 성공 및 오류 경우에 대한 응답을 반환해야 한다. 알려지지 않은 메서드가 호출되면 해당 사실을 보고해서 에러를 핸들링 하는 것이 중요하다.

마찬가지로 하나하나 뜯어보자.

  • setMethodCallHandler() : 메서드 안에 있는 람다 함수를 완성한다. 이 함수는 호출된 메서드를 수신하고, 이를 처리하는 코드를 포함한다.
  • getBatteryLevel() : 메서드가 호출 되었을 때, 이에 대한 처리를 구현한다. 이는 이전단계에서 작성한 안드로이드 코드를 호출하여 배터리 레벨을 가져오고, 결과를 반환해야 한다.
  • 알려지지 않은 메서드가 호출 되었을 때, 이를 보고하고 적절한 응답을 .error등으로 반환한다.
  • result.notImplemented() : MethodChannel의 핸들러 함수 내에서 호출할 수 있는 메서드 중 하나이다.
    • 이 메서드는 flutter 측에서 특정 플랫폼 메서드에 대한 구현이 아직 제공되지 않았음을 나타낸다. 따라서 이 메서드는 구현되지 않음을 의미하는 플러터 측에 적절한 응답을 반환한다. 일반적으로 이 메서드는 플러터 측 코드가 아직 플랫폼 메서드를 처리할 준비가 되지 않았거나 해당 기능이 구현되지 않았을 때 사용된다.

✅ 다 정리하고 나중에 안 사실인데, method channel 말고도 event channel이라는게 하나 더 있다고 한다. 뭐 스캔 결과를 계속 받아오는 거면 event channel 써야하지만 나는 advertising만 명령하고 나머지는 flutter_ble_plus 사용할꺼라..우선 method 사용해보면 될 것 같다.
✅ 위 코드들은 전부 main thread에서 실행되는 내용이라, 성능 향상을 위해서는 백그라운드에서 실행되도록 하는 것이 좋다고 한다. 나의 경우는 초보기도 하고 지금 시간이 많지 않아서, 우선 main thread에서 진행하고 성공했는데 시간이 남으면 번외로 분리해보고자 한다. 그러고 보니 깃 레포 관리도 공부해야 하는데 언제하지..ㅠ

참고

profile
코린이

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN