前提(バンドルIDについて)
組織名(Organization): com.example
プロジェクト名(Project name): myapp
の場合、
バンドルID: com.example.myapp
MethodChannel(双方向通信)
Flutter側からNative側へメソッドの呼び出しを行い、Native側はそのメソッドを実行後、結果やデータをFlutter側へ返却する
"バンドルID/~"のMethodChannel文字列がFlutter側とNative側で一致している必要がある。
■用途
任意のタイミングでネイティブの機能を利用
このチャンネルを通じて、Flutterアプリケーションはネイティブプラットフォームの機能(カメラアクセス、位置情報サービスなど)を利用できる
■iOS側の操作
iOS/Runner/AppDelegate.swiftを開く
「Open iOS/macOS module in Xcode」というリンクがあるのでクリックするとXcodeで開ける
開いたXcodeでAppDlegateとinfo.plistを編集
■Android側の操作
以下のMainActivity/AndroidManifest.xmlを編集
■位置情報を取得する実装例
<Flutter>
gps_loation_repository.dart
import 'dart:async';
import 'package:flutter/services.dart';
import './location_repository.dart';
/// GPSを使った位置情報を取得するための仕組み。
class GpsLocationRepository extends LocationRepository {
static const platform = MethodChannel('com.example.multios/location');
// 2) ここに後ほどEventChannelの処置を追加
/// 1) MethodChannelを使って位置情報を取得します。
@override
Future<Location> get() async {
final String result = await platform.invokeMethod('getLocation');
final splitted = result.split(',');
return Location(double.parse(splitted[0]), double.parse(splitted[1]));
}
/// 2) EventChannelを使って位置情報を監視します。(後ほど追加)
}
<iOS>
AppDelegate.swift
import UIKit
import Flutter
import CoreLocation
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
// @objc class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate, FlutterStreamHandler {
private var locationChannel: FlutterMethodChannel?
private var locationManager: CLLocationManager!
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// ここから追加
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
locationChannel = FlutterMethodChannel(name: "com.example.multios/location",
binaryMessenger: controller.binaryMessenger)
locationChannel?.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "getLocation" {
self.getLocation(result: result)
}
else {
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// 位置情報取得ロジック
private func getLocation(result: @escaping FlutterResult) {
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse || CLLocationManager.authorizationStatus() == .authorizedAlways {
if let location = locationManager.location {
result("\(location.coordinate.latitude),\(location.coordinate.longitude)")
} else {
result(FlutterError(code: "UNAVAILABLE", message: "Location not available.", details: nil))
}
} else {
result(FlutterError(code: "PERMISSION_DENIED", message: "Location permission denied.", details: nil))
}
}
}
<key>NSLocationWhenInUseUsageDescription</key>
<string>位置情報を使用します</string>
<!-- ↑ファイル末尾</dict>タグの上に-->
<Android>
MainActivity.kt
package com.example.multios
import android.Manifest
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
//import com.example.multios.map.GoogleMapViewFactory
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
/**
* Android Studio内のOpen for Editing in Android Studioをクリックすることで、
* もう一つAndroid Studio開きましょう。そうすることで補完が効くようになります。
*/
class MainActivity : FlutterActivity() {
// 追加
private val CHANNEL = "com.example.multios/location"
// 2) 追加
// var locationListener: LocationListener? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 1) 追加
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
if (call.method == "getLocation") {
val location = getLocation() // 実際の位置情報取得ロジックを実装
if (location.isEmpty()) {
result.error("UNAVAILABLE", "Location not available.", null)
} else {
result.success(location)
}
}
// 2) 追加
else {
result.notImplemented()
}
}
// 2) 追加
}
// 1) 位置情報取得ロジック
private fun getLocation(): String {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
1
)
return ""
}
val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
val location: Location? = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
return if (location == null) {
""
} else {
"${location.latitude},${location.longitude}"
}
}
// 2) 位置情報の更新を開始するロジックを実装
}
<!-- <manifest xmlns:android="~">タグの↓に -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
EventChannel(一方向通信:Native -> Flutter)
Native側で発生したイベント(センサーデータの更新、位置情報の変更など)をFlutter側へ非同期で送信。
"バンドルID/~"のEventChannel文字列がFlutter側とNative側で一致している必要がある。
■用途
ネイティブの機能を常に監視する
このチャンネルは、特定のイベントやデータが更新された時に、それをFlutterアプリケーションでリッスンし、ハンドリングする
■位置情報を監視する実装例
<Flutter>
gps_loation_repository.dart
import 'dart:async';
import 'package:flutter/services.dart';
import './location_repository.dart';
/// GPSを使った位置情報を取得するための仕組み。
class GpsLocationRepository extends LocationRepository {
static const platform = MethodChannel('com.example.multios/location');
// 2) EventChannelの追加
static const eventChannel =
EventChannel('com.example.multios/locationUpdates');
StreamSubscription<dynamic>? _locationSubscription;
StreamController<Location>? _locationStreamController;
/// 1) MethodChannelを使って位置情報を取得します。
@override
Future<Location> get() async {
final String result = await platform.invokeMethod('getLocation');
final splitted = result.split(',');
return Location(double.parse(splitted[0]), double.parse(splitted[1]));
}
/// 2) EventChannelを使って位置情報を監視します。
@override
Stream<Location> watch() {
if (_locationSubscription != null) {
return _locationStreamController!.stream;
}
_locationSubscription = eventChannel.receiveBroadcastStream().listen(
(event) {
final splitted = (event as String).split(',');
final location =
Location(double.parse(splitted[0]), double.parse(splitted[1]));
print("更新された位置情報: 緯度 ${location.latitude}, 経度 ${location.longitude}");
_locationStreamController!.add(location);
},
onError: (dynamic error) {
print('Received error: ${error.message}');
},
);
platform.invokeMethod('watchLocation');
_locationStreamController = StreamController<Location>();
return _locationStreamController!.stream;
}
}
<iOS>
AppDelegate.swift
import UIKit
import Flutter
import CoreLocation
// Android Studio内のOpen iOS/macOS module in Xcodeをクリックすることで、
// もう一つXcode開きましょう。そうすることで補完が効くようになります。
// , CLLocationManagerDelegate を追加することを忘れずに。
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate, FlutterStreamHandler {
// 追加
private var locationChannel: FlutterMethodChannel?
private var locationManager: CLLocationManager!
// 2) 追加
private var eventChannel: FlutterEventChannel?
private var eventSink: FlutterEventSink?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// ここから追加
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
locationChannel = FlutterMethodChannel(name: "com.example.multios/location",
binaryMessenger: controller.binaryMessenger)
locationChannel?.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "getLocation" {
self.getLocation(result: result)
} else if call.method == "watchLocation" {// 2) 追加
print("watchLocation")
self.watchLocation()
} else {
result(FlutterMethodNotImplemented)
}
})
// ここまで追加
// 2) 追加
eventChannel = FlutterEventChannel(name: "com.example.multios/locationUpdates", binaryMessenger: controller.binaryMessenger)
eventChannel?.setStreamHandler(self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// 位置情報取得ロジック
private func getLocation(result: @escaping FlutterResult) {
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse || CLLocationManager.authorizationStatus() == .authorizedAlways {
if let location = locationManager.location {
result("\(location.coordinate.latitude),\(location.coordinate.longitude)")
} else {
result(FlutterError(code: "UNAVAILABLE", message: "Location not available.", details: nil))
}
} else {
result(FlutterError(code: "PERMISSION_DENIED", message: "Location permission denied.", details: nil))
}
}
}
// 2) 追加:位置情報監視処理
extension AppDelegate {
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
locationManager.stopUpdatingLocation()
return nil
}
private func watchLocation() {
// 位置情報の更新を開始するロジックを実装
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
eventSink?("\(location.coordinate.latitude),\(location.coordinate.longitude)")
}
}
}
MethodChannelの実装例と同様の設定
<Android>
MainActivity.kt
package com.example.multios
import android.Manifest
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
//import com.example.multios.map.GoogleMapViewFactory
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
/**
* Android Studio内のOpen for Editing in Android Studioをクリックすることで、
* もう一つAndroid Studio開きましょう。そうすることで補完が効くようになります。
*/
class MainActivity : FlutterActivity() {
// 追加
private val CHANNEL = "com.example.multios/location"
// 2) 追加
var locationListener: LocationListener? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 1) 追加
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
if (call.method == "getLocation") {
val location = getLocation() // 実際の位置情報取得ロジックを実装
if (location.isEmpty()) {
result.error("UNAVAILABLE", "Location not available.", null)
} else {
result.success(location)
}
// 2) 追加
} else if (call.method == "watchLocation") {
if (watchLocation()) {
result.success(true)
} else {
result.error("UNAVAILABLE", "Location not available.", null)
}
} else {
result.notImplemented()
}
}
// 2) 追加
EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.multios/locationUpdates"
).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
locationListener = LocationListener { location ->
events?.success("${location.latitude},${location.longitude}")
}
}
override fun onCancel(arguments: Any?) {
// 位置情報の更新を停止するロジックを実装
}
}
)
// 3) 追加
// flutterEngine
// .platformViewsController
// .registry
// .registerViewFactory("map", GoogleMapViewFactory(flutterEngine.dartExecutor.binaryMessenger))
}
// 1) 位置情報取得ロジック
private fun getLocation(): String {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
1
)
return ""
}
val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
val location: Location? = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
return if (location == null) {
""
} else {
"${location.latitude},${location.longitude}"
}
}
// 2) 位置情報の更新を開始するロジックを実装
private fun watchLocation(): Boolean {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
1
)
return false
}
val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
10000,
0f,
locationListener!!
)
return true
}
}
MethodChannelの実装例と同様の設定
PlatformView
ネイティブの画面をFlutterアプリに投影する
■用途
地図(AndroidはGoogle Map、 iOSはApple標準地図)表示分けなど
<Flutter>
map_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/locations/location_repository_provider.dart';
import '../../domain/locations/location.dart';
import '../../use_cases/locations/get_location_use_case.dart';
import '../../use_cases/locations/watch_location_use_case.dart';
import 'map_view.dart';
class MapPage extends StatelessWidget {
const MapPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("真逆地図アプリ"),
),
// 1) 位置情報を一度だけ取得します。
// body: const _Body1(),
// 2) 位置情報を常に更新できるようにします。
body: const _Body2(),
);
}
}
class _Body1 extends ConsumerWidget {
const _Body1({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
/// 1) 位置情報を一度だけ取得します。
final location = ref.watch(getLocationUseCaseProvider);
return switch (location) {
AsyncData(:final value) => _Main(
location: value,
),
AsyncError(:final error, :final stackTrace) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("読み込みエラー: $error"),
ElevatedButton(//ユーザーが位置情報を許容した後のリトライボタン
onPressed: () {
ref.refresh(getLocationUseCaseProvider);
},
child: Text('再試行'),
),
],
),
),
_ => const Center(
child: CircularProgressIndicator(),
),
};
}
}
class _Main extends StatefulWidget {
final Location location;
const _Main({Key? key, required this.location}) : super(key: key);
@override
_MainState createState() => _MainState();
}
class _MainState extends State<_Main> {
// 現在表示している位置(デフォルトは現在位置)
late Location displayedLocation;
bool isOpposite = false; // 裏側の位置を表示しているかどうか
@override
void initState() {
super.initState();
displayedLocation = widget.location; // 初期状態では現在位置を表示
}
void switchLocation(bool toOpposite) {
setState(() {
isOpposite = toOpposite;
if (toOpposite) {
// 裏側位置に切り替え
// 緯度は-90度から+90度までの値を取ります。赤道が0度、北極が+90度、南極が-90度
// 緯度は符号を反転させるのみで逆側になります。
double oppositeLatitude = -widget.location.latitude;
// 経度は-180度から+180度までの値を取ります。本初子午線 を0度として、東方向に180度、西方向に-180度
// 経度を反転させる には、 180を加算します。ただし、その値が 180度 を超えた場合 360度を引きます。
double oppositeLongitude = widget.location.longitude + 180;
if (oppositeLongitude > 180) {
oppositeLongitude -= 360;
}
displayedLocation = Location(oppositeLatitude, oppositeLongitude);
} else {
// 現在位置に戻す
displayedLocation = widget.location;
}
});
}
@override
Widget build(BuildContext context) {
// 現在位置と裏側位置の表示切り替え
String locationText = isOpposite ?
"裏側緯度:${displayedLocation.latitude.toStringAsFixed(2)} 裏側経度:${displayedLocation.longitude.toStringAsFixed(2)}" :
"現在緯度:${widget.location.latitude.toStringAsFixed(2)} 現在経度:${widget.location.longitude.toStringAsFixed(2)}";
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(locationText),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => switchLocation(!isOpposite),
child: Text(isOpposite ? '現在の位置' : '裏側の位置'),
),
),
],
),
Expanded(
child: MapView(
key: ValueKey(displayedLocation), // 表示位置が変わるたびにウィジェットを再構築
latitude: displayedLocation.latitude,
longitude: displayedLocation.longitude,
),
),
],
);
}
}
class _Body2 extends ConsumerWidget {
const _Body2({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
/// 2) 位置情報を常に更新できるようにします。
final location = ref.watch(watchLocationUseCaseProvider);
return switch (location) {
AsyncData(:final value) => _Main(
location: value,
),
AsyncError(:final error, :final stackTrace) => Center(
// child: Text("読み込みエラー: $error"),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("読み込みエラー: $error"),
ElevatedButton(//ユーザーが位置情報を許容した後のリトライボタン
onPressed: () {
ref.refresh(getLocationUseCaseProvider);
},
child: Text('再試行'),
),
],
),
),
_ => const Center(
child: CircularProgressIndicator(),
),
};
}
}
<iOS>
AppDelegate.swift
import UIKit
import Flutter
import CoreLocation
// Android Studio内のOpen iOS/macOS module in Xcodeをクリックすることで、
// もう一つXcode開きましょう。そうすることで補完が効くようになります。
// , CLLocationManagerDelegate を追加することを忘れずに。
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate, FlutterStreamHandler {
// 1) 追加
private var locationChannel: FlutterMethodChannel?
private var locationManager: CLLocationManager!
// 2) 追加
private var eventChannel: FlutterEventChannel?
private var eventSink: FlutterEventSink?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// ここから追加
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
locationChannel = FlutterMethodChannel(name: "com.example.multios/location",
binaryMessenger: controller.binaryMessenger)
locationChannel?.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "getLocation" {
self.getLocation(result: result)
} else if call.method == "watchLocation" {
print("watchLocation")
self.watchLocation()
} else {
result(FlutterMethodNotImplemented)
}
})
// ここまで追加
// 2) 追加
eventChannel = FlutterEventChannel(name: "com.example.multios/locationUpdates", binaryMessenger: controller.binaryMessenger)
eventChannel?.setStreamHandler(self)
// 3) 追加
let registrar = self.registrar(forPlugin: "multios")
let factory = FlutterMapKitFactory(messenger: (registrar!.messenger()))
self.registrar(forPlugin: "<multios>")!.register(
factory,
withId: "map")
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// 位置情報取得ロジック
private func getLocation(result: @escaping FlutterResult) {
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse || CLLocationManager.authorizationStatus() == .authorizedAlways {
if let location = locationManager.location {
result("\(location.coordinate.latitude),\(location.coordinate.longitude)")
} else {
result(FlutterError(code: "UNAVAILABLE", message: "Location not available.", details: nil))
}
} else {
result(FlutterError(code: "PERMISSION_DENIED", message: "Location permission denied.", details: nil))
}
}
}
// 2) 追加:位置情報監視処理
extension AppDelegate {
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
locationManager.stopUpdatingLocation()
return nil
}
private func watchLocation() {
// 位置情報の更新を開始するロジックを実装
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
eventSink?("\(location.coordinate.latitude),\(location.coordinate.longitude)")
}
}
}
MethodChannelの実装例と同様の設定
<Android>
MainActivity.kt
map/GoogleMapView.kt
map/GoogleMapViewFactory.kt
// 末尾に↓追加
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.android.gms:play-services-maps:18.2.0'
}
MethodChannelの実装例と同様の設定に加えて↓
<!-- <application タグの↓に -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="Google MapのAPIキーを入れる"/>
Google MapのAPIキー取得は以下参照
https://developers.google.com/maps/documentation/android-sdk/get-api-key?hl=ja