【Flutter】KotlinでNative側を実装しFlutterと連携

flutter

1. 概要

今回は下記記事を参考にNativeメソッドを呼び出し、連携してみます。

Androidデバイスのバッテリー残量を取得し、画面に表示します。

  • Nativeコードは、上記参考ページのKotlinをそのまま流用します。
  • Flutterコードは、上記参考ページを少し改良してます。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. FlutterとNativeの連携方法

FlutterでNativeと連携する方法は、下記3つがあるようです。

今回は「MethodChannel」を使い、やってみます。

※下記図は公式より引用

Architectural overview: platform channels

3. プロジェクトの準備

3-1. プロジェクトを作成

3-2. Dependencyのインストール

flutter pub add flutter_riverpod
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  flutter_riverpod: ^1.0.4

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

4. ソースコード(Kotlin)

4-1. android/app/src/main/kotlin/com/example/batterylevel/MainActivity.kt

  • このファイルはFlutterプロジェクトを作成すると、自動生成されるので内容を変更
package com.example.batterylevel

import android.content.Context
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 androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      // This method is invoked on the main thread.
      call, result ->
      if (call.method == "getBatteryLevel") {
        val batteryLevel = getBatteryLevel()

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

  private fun getBatteryLevel(): Int {
    val batteryLevel: Int
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    } else {
      val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    }
    return batteryLevel
  }
}

Androidデバイスのバッテリー残量を取得し、返す。

  • 「CHANNEL」について
    • samples.flutter.dev/battery
    • Flutter側との通路

The client and host sides of a channel are connected through a channel name passed in the channel constructor. All channel names used in a single app must be unique; prefix the channel name with a unique ‘domain prefix’, for example: samples.flutter.dev/battery.

Writing custom platform-specific code | Flutter
  • 「getBatteryLevel」について
    • Flutter側で呼び出すメソッド名

5. ソースコード(Dart)

5-1. lib/main.dart

import 'package:batterylevel/models/battery_data.dart';
import 'package:batterylevel/views/home.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'notifiers/battery_notifier.dart';

final batteryProvider =
    StateNotifierProvider<BatteryNotifier, BatteryData>((ref) {
  return BatteryNotifier();
});

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

5-2. lib/views/home.dart

import 'package:batterylevel/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'components/battery_icon.dart';

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key, required String title}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(batteryProvider);
    final batteryNotifier = ref.read(batteryProvider.notifier);
    BatteryIcon batteryIcon = BatteryIcon(amount: data.amount);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Battery Level'),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
                onPressed: () => batteryNotifier.getBatteryLevel(),
                child: const Text('Get Battery Level')),
            batteryIcon.batteryIcon(),
            Text(_getBatteryLevel(data.amount))
          ],
        ),
      ),
    );
  }

  String _getBatteryLevel(int amount) {
    return 'Battery level at $amount % .';
  }
}

5-3. lib/views/components/battery_icon.dart

import 'package:flutter/material.dart';

class BatteryIcon {
  final int amount;

  const BatteryIcon({required this.amount});

  Widget batteryIcon() {
    Color batteryColor = Colors.grey;
    IconData batteryIcon = Icons.battery_alert;
    if (amount >= 20 && amount < 80) {
      batteryColor = Colors.blue;
      batteryIcon = Icons.battery_charging_full;
    } else if (amount >= 80) {
      batteryColor = Colors.green;
      batteryIcon = Icons.battery_full;
    }
    return SizedBox(
      width: 200,
      height: 200,
      child: Icon(
        batteryIcon,
        size: 200,
        color: batteryColor,
      ),
    );
  }
}

5-4. lib/models/battery_data.dart

import 'package:flutter/material.dart';

@immutable
class BatteryData {
  final int amount;
  const BatteryData({required this.amount});
}

5-5. lib/repository/battery_repository.dart

import 'package:flutter/services.dart';

class BatteryRepository {
  static const platform = MethodChannel('samples.flutter.dev/battery');

  Future<int> getBatteryLevel() async {
    int amount = 0;
    try {
      amount = await platform.invokeMethod('getBatteryLevel');
      // ignore: empty_catches
    } on PlatformException {}
    return amount;
  }
}

Native側のメソッドを呼び出す。

  • 「MethodChannel」
    • samples.flutter.dev/battery
    • Native側との通路
  • 「getBatteryLevel」
    • Native側に用意されているメソッド名

5-6. lib/notifiers/battery_notifier.dart

import 'package:batterylevel/models/battery_data.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../repository/battery_repository.dart';

class BatteryNotifier extends StateNotifier<BatteryData> {
  BatteryNotifier() : super(const BatteryData(amount: 0));
  void getBatteryLevel() async {
    BatteryRepository repository = BatteryRepository();
    int amount = await repository.getBatteryLevel();
    state = BatteryData(amount: amount);
  }
}

6. 結果

6-1. 10% / 50% / 90%

  • バッテリー残量に合わせてバッテリーアイコンと数値が変化

下記ページにて、Android側のNativeに引数を渡して処理する内容を書いたので合わせて読んで頂けますと幸いです。

下記ページにて、EventChannelで連携する内容を書いたので合わせて読んで頂けますと幸いです。

7. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. flutter

    【Flutter】GraphQLClientを使ってGraphQL・A…

  2. flutter

    【Flutter】画面が開く前にローディングを表示

  3. flutter

    【Flutter】状態管理(setState,Provider,Riv…

  4. flutter

    【Flutter】コード自動生成とGraphQLClientを使ってG…

  5. 【Widget of the Week】#6 FutureBuilde…

  6. 【Widget of the Week】#4 AnimatedCont…

最近の記事

  1. AWS
  2. AWS
  3. flutter

制作実績一覧

  1. Checkeys