flutterのpluginに渡すcallback内でfirebase cloud_firestoreを使う

5 min read読了の目安(約4800字

flutterでバックグラウンドでGPSを記録するようなアプリを作れるようになりました。

https://pub.dev/packages/background_locator

他にもあるかもしれませんが、このpluginはandroid, iosでもちゃんと動作しました。

詳しい使い方は省きますが、以下のような感じで位置情報が変化したときのcallbackを指定することができます。

Future<void> callback(LocationDto locationDto) async {
  // 位置情報が変化したときの処理
}

void _startLocator() {
    Map<String, dynamic> data = {'countInit': 1};
    BackgroundLocator.registerLocationUpdate(callback,
        initCallback: initCallback,
        initDataCallback: data,
/*...省略...*/
  }

さて、バックグラウンドでGPSを記録する際にfirebase cloudstoreに保存したくなったら以下のようにすればいいように思えます。

Future<void> callback(LocationDto locationDto) async {
  final _service = FirebaseFirestore.instance;
  CollectionReference tracks = _service.collection('tracks');
  final ref = tracks.add({
    'created_at': DateTime.now().millisecondsSinceEpoch,
    'track': locationDto.toString(),
  }).then((value) {
    print("tracks Added in callback");
  }).catchError((error) => print("Failed to add track: $error"));
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  
  runApp(MyApp());
}

これは動きません。

Androidだとこんな感じのエラーになるとおもいます

[ERROR:flutter/lib/ui/ui_dart_state.cc(171)] Unhandled Exception: [core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()

これはfirebaseを初期化していないときのエラーなのですが、mainで初期化しているのになぜか出てきます。

ではこうするとどうでしょうか

Future<void> callback(LocationDto locationDto) async {
  await Firebase.initializeApp();
  final _service = FirebaseFirestore.instance;
  CollectionReference tracks = _service.collection('tracks');
  final ref = tracks.add({
    'created_at': DateTime.now().millisecondsSinceEpoch,
    'track': locationDto.toString(),
  }).then((value) {
    print("tracks Added in callback");
  }).catchError((error) => print("Failed to add track: $error"));
}

callbackの度に初期化するのは微妙ですがとりあえず動いてくれればいいので。
ですがこれもエラーになります。

[ERROR:flutter/lib/ui/ui_dart_state.cc(171)] Unhandled Exception: MissingPluginException(No implementation found for method Firebase#initializeCore on channel plugins.flutter.io/firebase_core)

これはpluginとしてfirebase_coreが登録されていない時に出るエラーです。
ちなみに、mainでの初期化ではこのエラーは起こりません。

どうすれば動くかというと、

https://github.com/rekab-app/background_locator/wiki/Use-other-plugins-in-callback

実は background_locatorのドキュメントに書いてあり、

androidの場合は独自にApplicationファイルを作成し、明示的にpluginをregisterしてあげます。

import io.flutter.app.FlutterApplication
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback
import io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin
import io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin
import io.flutter.plugins.pathprovider.PathProviderPlugin
import io.flutter.view.FlutterMain
import rekab.app.background_locator.LocatorService

class Application : FlutterApplication(), PluginRegistrantCallback {
    override fun onCreate() {
        super.onCreate()
        LocatorService.setPluginRegistrant(this)
        FlutterMain.startInitialization(this)
    }
    
    override fun registerWith(registry: PluginRegistry?) {
        if (!registry!!.hasPlugin("io.flutter.plugins.pathprovider")) {
            PathProviderPlugin.registerWith(registry!!.registrarFor("io.flutter.plugins.pathprovider"))
        }
	if (!registry!!.hasPlugin("plugins.flutter.io/firebase_core")) {
            FlutterFirebaseCorePlugin.registerWith(registry!!.registrarFor("plugins.flutter.io/firebase_core"))
        }
        if (!registry!!.hasPlugin("plugins.flutter.io/firebase_firestore")) {
            FlutterFirebaseFirestorePlugin.registerWith(registry!!.registrarFor("plugins.flutter.io/firebase_firestore"))
        }
    }
}

AndroidManifest.xmlにファイル名を指定します

<application
        android:name=".Application"

これで無事callback内でcloud firestoreを使うことができるようになります。

本記事では例としてbackground_locatorをあげましたが他のnative pluginでも同じように使えるハズです。

ドキュメントはちゃんと読もうという話でした。


余談ですが、pluginに渡すcallbackはstaticな関数か、トップレベルの名前付き関数である必要があります。なので以下のようなことはできません。

void _startLocator() {
    Map<String, dynamic> data = {'countInit': 1};
    BackgroundLocator.registerLocationUpdate((LocationDto locationDto) async {
    /* callback処理 */
    },
        initCallback: initCallback,
        initDataCallback: data,
/*...省略...*/
  }

これもドキュメントに書かれています。

https://api.flutter.dev/flutter/dart-ui/PluginUtilities-class.html

Get a tear-off of a named top-level or static callback represented by a handle.
Get a handle to a named top-level or static callback function which can be easily passed between isolates.

参考