ble에서 custom_id를 advertising 하는 기능이 없는 이슈로..네이티브 통신을 통해 해결하기로 했다.
우선 나는 Method channel에 대해 1도 모르기 때문에 공부를 해보고자 한다.
그럼 시작~!
메세지는 클라이언트인 ui에서 host인 플랫폼으로 전달된다.
이는 플러터에서 native code(and, ios)를 동작하고 싶을 때 사용하는 인터페이스이다.
클라이언트 측에서, MethodChannel은 메세지를 보낼 수 있다.
공식문서의 예시에서는 배터리를 보여주는 플랫폼 채널로 예시를 들고 있다. 이를 통해 어떤 과정을 거쳐 method channel을 열게 되는지를 살펴보자.
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.
다음으로 메서드를 메서드 채널에 불러온다. 플랫폼이 플랫폼 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;
});
}
이 부분 코드는 이해가 잘 안가서 하나하나가 어떤 의미인지 좀 찾아봤다
마지막으로, 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),
],
),
),
);
}
난 자바가 좋으므로(사실 코틀린 모른다...) 이 부분은 자바로 살펴보도록 하겠다.
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을 만들어야 한다고 말한 바 있다. 그러므로 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);
}
공식 문서에 있는 단계별 예시를 살펴보자!
이 과정에선 필요한 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;
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;
}
안드로이드 기기의 베터리 레벨을 가져오는 함수이다. 동작 방식은 다음과 같다.
앞에 있었던 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()을 처리하기 위한 것이다.
메서드 호출 핸들러를 구현하여 특정 메서드 호출을 처리하고, 성공 및 오류 경우에 대한 응답을 반환해야 한다. 알려지지 않은 메서드가 호출되면 해당 사실을 보고해서 에러를 핸들링 하는 것이 중요하다.
마찬가지로 하나하나 뜯어보자.
✅ 다 정리하고 나중에 안 사실인데, method channel 말고도 event channel이라는게 하나 더 있다고 한다. 뭐 스캔 결과를 계속 받아오는 거면 event channel 써야하지만 나는 advertising만 명령하고 나머지는 flutter_ble_plus 사용할꺼라..우선 method 사용해보면 될 것 같다.
✅ 위 코드들은 전부 main thread에서 실행되는 내용이라, 성능 향상을 위해서는 백그라운드에서 실행되도록 하는 것이 좋다고 한다. 나의 경우는 초보기도 하고 지금 시간이 많지 않아서, 우선 main thread에서 진행하고 성공했는데 시간이 남으면 번외로 분리해보고자 한다. 그러고 보니 깃 레포 관리도 공부해야 하는데 언제하지..ㅠ