Skip to content

Flutter PlatformChannel #

Find similar titles

2회 업데이트 됨.

Edit
  • 최초 작성자
  • 최근 업데이트

Structured data

Category
Programming

Flutter/PlatformChannel #

목차 #

  1. 서론
  2. 개요
  3. 예시
  4. 효과
  5. 결론

서론 #

Flutter는 모바일, 웹, 데스크톱 등 다양한 플랫폼에서 동작하는 효율적이고 강력한 UI 프레임워크로서, 다양한 기능을 제공하고 있다. 그러나 때로는 특정 플랫폼의 네이티브 기능이나 라이브러리를 활용해야 할 필요가 있다. 이러한 요구사항을 충족하기 위해 Flutter는 Platform Channel을 제공하고 있다.

개요 #

Platform Channel은 Flutter와 네이티브 코드 간에 통신을 담당하는 메커니즘이다. 이를 통해 Flutter 애플리케이션은 Dart 코드와 네이티브 코드(Kotlin, Swift 등) 간에 데이터를 주고받을 수 있다. 주로 네이티브 코드에서 제공되는 특정 기능을 활용하거나, 반대로 Flutter 앱에서 네이티브로부터 데이터를 수신하는 데 사용된다. 필자는 flutter에 구글 헬스 커넥트 라이브러리가 배포되기 전, 구글의 공식 문서를 참고하여 Flutter - Kotlin 통신을 구현했다.

예시 #

Flutter에서 네이티브(Kotlin)로 데이터 보내기(Dart File) #

  class HealthConnectMethodChannel with ChangeNotifier {
      MethodChannel channel = const MethodChannel('google-health-connect');

      String hasGoogleHealthConnectPermission = 'hasGoogleHealthConnectPermission';
      String getGoogleHealthConnectData = 'getGoogleHealthConnectData';

      static int responseDateCount = 0;

      List<BloodGlucoseRecordModel> bloodGlucoseRecordModelList = [];
      List<OxygenSaturationRecordModel> oxygenSaturationRecordModelList = [];

      // Function to check if Google Health Connect has allowed permission for this application by using method channel
      Future<bool> checkGoogleHealthConnectPermission() async {
        return await channel.invokeMethod(hasGoogleHealthConnectPermission);
      }

      Future<bool> tryToGetHealthConnectData(
          GoogleHealthConnectRequestModel googleHealthConnectRequestModel) async {
        final res = await channel.invokeMethod(
            getGoogleHealthConnectData, googleHealthConnectRequestModel.toMap());

        // Before getting Google Health data, Success comes first.
        if (res["success"] == "success") {
          return true;
        }
        return false;
        }
        (중략)

네이티브(Kotlin)에서 데이터 받은 후 Flutter로 다시 보내기(Kotlin File) #

(중략)

class MainActivity : FlutterFragmentActivity() {
private val CHANNEL = "google-health-connect"
private var _callBackChannel: MethodChannel? = null

var providerPackageName = applicationContext.packageName;



@RequiresApi(Build.VERSION_CODES.O)
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)

    _callBackChannel =
        MethodChannel(flutterEngine.dartExecutor, "google-health-connect")
    MethodChannel(
        flutterEngine.dartExecutor.binaryMessenger,
        CHANNEL
    ).setMethodCallHandler { call, result ->

        if (call.method == "getGoogleHealthConnectData") {

            val googleHealthConnectRequestModel =
                GoogleHealthConnectRequestModel(call.arguments as Map<String, Any>)

            Log.i("Status", "Beginning")

            if (HealthConnectClient.isProviderAvailable(applicationContext)) {


                // Health Connect is available and installed.
                val map = HashMap<String, Any>()

                val healthConnectClient = HealthConnectClient.getOrCreate(applicationContext)

                map["success"] = "success"


                CoroutineScope(Dispatchers.Main).launch {
                    readOxygenSaturationByTimeRange(
                        healthConnectClient,
                        Instant.parse(googleHealthConnectRequestModel.startTime),
                        Instant.parse(googleHealthConnectRequestModel.endTime)
                    )
                    readBloodGlucoseByTimeRange(
                        healthConnectClient,
                        Instant.parse(googleHealthConnectRequestModel.startTime),
                        Instant.parse(googleHealthConnectRequestModel.endTime)
                    )
                }
                result.success(map)

            } else {
                val map = HashMap<String, Any>()
                map["success"] = "fail"
                result.success(map)
            }
        } else if (call.method == "hasGoogleHealthConnectPermission") {

            val availabilityStatus = HealthConnectClient.sdkStatus(applicationContext)
            if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
                return@setMethodCallHandler // early return as there is no viable integration
            }
            if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
                // Optionally redirect to package installer to find a provider, for example:
                val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
                applicationContext.startActivity(
                    Intent(Intent.ACTION_VIEW).apply {
                        setPackage("com.android.vending")
                        data = Uri.parse(uriString)
                        putExtra("overlay", true)
                        putExtra("callerId", applicationContext.packageName)
                    }
                )
                return@setMethodCallHandler
            }
            val healthConnectClient = HealthConnectClient.getOrCreate(applicationContext)


        val PERMISSIONS =
            setOf(
                HealthPermission.getReadPermission(BloodGlucoseRecord::class),
                HealthPermission.getWritePermission(BloodGlucoseRecord::class),
                HealthPermission.getReadPermission(OxygenSaturationRecord::class),
                HealthPermission.getWritePermission(OxygenSaturationRecord::class)
            )

        // Create the permissions launcher.
        val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()

        suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient): Boolean {
            // For alpha09 and earlier versions, use getGrantedPermissions(PERMISSIONS) instead
            val granted = healthConnectClient.permissionController.getGrantedPermissions()
            if (granted.containsAll(PERMISSIONS)) {
                // Permissions already granted; proceed with inserting or reading data.
                return true
            } else {
                val requestPermissions =
                    registerForActivityResult(ActivityResultContracts.GetContent()) { granted ->
                        if (granted.containsAll(PERMISSIONS)) {
                            // Permissions successfully granted
                        } else {
                            // Lack of required permissions
                        }
            }
                requestPermissions.launch(PERMISSIONS)
                return true
            }
        }

            CoroutineScope(Dispatchers.Main).launch {
                if (checkPermissionsAndRun(healthConnectClient = healthConnectClient)) {
                    // Method Channel to Flutter application(com.example.flutter_with_hc)
                    result.success(true)
                } else {
                    PermissionController.createRequestPermissionResultContract()
                    result.success(false)
                }
            }

                   (중략)

suspend fun readOxygenSaturationByTimeRange(
    healthConnectClient: HealthConnectClient,
    startTime: Instant,
    endTime: Instant
) {

    val response =
        healthConnectClient.readRecords(
            ReadRecordsRequest(
                OxygenSaturationRecord::class,
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
            )
        )

    val responseList = mutableListOf<HashMap<String, Any>>()

    for (oxygenSaturationRecord in response.records) {
        val map = HashMap<String, Any>()
        map.put("percentage", oxygenSaturationRecord.percentage.value.toString())
        map.put("type", "BLOOD_OXYGEN")
        map.put("time", oxygenSaturationRecord.time.toString())
        map.put("zoneOffset", oxygenSaturationRecord.zoneOffset.toString())
        responseList.add(map)
    }
    _callBackChannel?.invokeMethod("oxygenSaturationRecord", responseList)
}

네이티브(Kotlin)에서 보낸 데이터를 Flutter에서 처리(Dart File) #

(중략)
// Register a handler to receive data asynchronously coming from the Kotlin method channel
  void registerMethodCallbackHandler() {
    channel.setMethodCallHandler(handler);
  }

  // Handler to receive data asynchronously coming from Kotlin method channel
  Future<dynamic> handler(MethodCall call) async {
    switch (call.method) {
      case "bloodGlucoseRecord":
        var records = call.arguments;
        bloodGlucoseRecordModelList = [];
        for (var record in records) {
          // print(BloodGlucoseRecordModel.fromJson( record));

          bloodGlucoseRecordModelList.add(BloodGlucoseRecordModel.fromJson(
              Map<String, dynamic>.from(record)));
        }
        log(bloodGlucoseRecordModelList.toString());
        notifyListeners();
        responseDateCount += 1;

        break;
      case "oxygenSaturationRecord":
        var records = call.arguments;
        oxygenSaturationRecordModelList = [];
        for (var record in records) {
          // print(OxygenSaturationRecordModel.fromJson(record));
          oxygenSaturationRecordModelList.add(
              OxygenSaturationRecordModel.fromJson(
                  Map<String, dynamic>.from(record)));
        }
        log(oxygenSaturationRecordModelList.toString());
        notifyListeners();

        responseDateCount += 1;
        break;

      default:
        throw ("method not defined");
    }
  }
  (중략)

효과 #

건강 데이터에 필요한 보안 평가 심사 비용(연 최대 75,000달러) 절감 #

구글 피트니스에서 Oauth2 인증 후 건강 데이터를 연동하는 것이 가능했지만, 걸음수와 심박수를 제외한 데이터들이 제한된 컨텐츠로 변경되면서 이들을 사용하기 위해서는 매년 최대 75,000달러의 심사 비용을 지불해야 하도록 정책이 바뀌었다. 우리는 이 비용을 절감하기 위한 방법으로 Google Health Connect를 연동하기로 했다. Google Health Connect는 Flutter를 지원하지 않기 때문에, 필자가 Platform Channel을 통해 네이티브 플랫폼과 통신하는 구조를 만들었다. 그 결과 우리는 Google Health Connect를 통해 걸음수와 심박수를 제외한 민감한 데이터들을 무료로 가져와 과일 궁합 애플리케이션(AOS)에서 보여줄 수 있게 하였다.

Image

IT 기술력이 있는 생물정보 전문기업으로서의 인정 #

Google Health Connect는 Flutter Framework를 지원할 계획이 없는 플랫폼이었다. 하지만 Platform Channel을 사용한다면, Flutter에서도 얼마든지 네이티브 플랫폼을 사용할 수 있다. 단, 이를 위해서는 네이티브 코드에 대한 이해가 전제된다. 필자는 딥 링크 구현을 위해 Google Health Connect 팀에 기술 지원을 요청한 적이 있었는데, 오히려 연동 자체를 어떻게 구현했는지 궁금해했다. 필자는 자사 코드의 보안을 위해 Health Connect Team을 위한 다른 새로운 프로젝트를 생성하였고 코드 스니펫을 전달하였다. 그 결과, 우리는 Google Health Connect의 Flutter Integration에 크게 기여를 하였고, 그들은 우리 회사와 파트너쉽을 맺고 싶은 의사를 표현할 정도로 고마움을 표했다.

Image ImageImage

결론 #

Flutter의 Platform Channel은 다양한 플랫폼과의 통합을 용이하게 해주는 강력한 도구이다. 네이티브 기능의 효과적인 활용과 Flutter의 강점을 결합하여 다양한 애플리케이션을 개발할 수 있다. 적절한 사용법과 주의를 기울이면 무엇이든 만들 수 있다. 물론 그것이 가능한 개발자는 극히 드물겠지만 말이다.

Suggested Pages #

0.0.1_20240214_1_v81