Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

【Flutter】shared_preferencesパッケージではネイティブアプリで保存した値を取得できない

こんにちは。
BPSの福岡拠点として一緒にお仕事させてもらっています、ウイングドアのウメバヤシです。

Flutterでローカルに値を保存したい時はFlutter公式のshared_preferencesパッケージを使うと思いますが、実はこのパッケージではネイティブアプリをリプレイスする際に、以前のネイティブアプリバージョンで保存していたローカルデータまでは取得することができません。

※ここでいうローカルデータとはiOSでいうとUserDefaults、AndroidでいうとSharedPreferencesのことです。

前提

対象のshared_preferencesパッケージは現時点で最新Verの0.5.12+4です。
shared_preferences | Flutter Package - Pub.Dev

なぜネイティブアプリで保存されたデータが取得できないのか?

結論からいうと、shared_preferencesパッケージでは読み書きの際に「flutter.」というプレフィックスをキーに付与してデータを扱っているからです。

「hoge」というキーで取得しようとしても、実際には「flutter.hoge」というキーで値を取得しにいきます。また、「hoge」というキーで書き込みしても実際には「flutter.hoge」というキーで端末に保存されます。

ネイティブアプリバージョンで保存された値のキーは、もちろんプレフィックスなしで保存されていますので、純粋な「hoge」というキーで保存されている値はshared_preferencesパッケージでは取得できないのです。

プレフィックスが定義されている場所

GitHubでパッケージのソースコードを読んでみました。
以下のようにプレフィックスがプライベートでconst定義されています。

static const String _prefix = 'flutter.';

plugins/shared_preferences.dart at e014c208909772cee2328a91b7225e667a2681a9 · flutter/pluginsより

setter

以下のコードの_storeがネイティブ側のローカルデータを操作するオブジェクトで、_preferenceCacheはFlutter側で扱う値を保持しているオブジェクトです。
setterではネイティブ側にプレフィックス付きのキーで値を保存していることがわかります。

  Future<bool> _setValue(String valueType, String key, Object value) {
    final String prefixedKey = '$_prefix$key';
    if (value == null) {
      _preferenceCache.remove(key);
      return _store.remove(prefixedKey);
    } else {
      if (value is List<String>) {
        // Make a copy of the list so that later mutations won't propagate
        _preferenceCache[key] = value.toList();
      } else {
        _preferenceCache[key] = value;
      }
      return _store.setValue(valueType, prefixedKey, value);
    }
  }

plugins/shared_preferences.dart at e014c208909772cee2328a91b7225e667a2681a9 · flutter/pluginsより

getter

以下の_getSharedPreferencesMap()メソッドで返されているpreferencesMapは、SharedPreferencesgetInstance()呼び出し時に、Flutterでのデータの出し入れに使う_preferenceCacheに格納されることになるのですが、その際に全てのキーからプレフィックスが削除されているのがわかります。

また、ネイティブ側と直接通信している_store.getAll()メソッドでは、それぞれのOSのMethod Channelで、キーにプレフィックスが付与されてる値のみが返ってくるようになっています。

  static Future<Map<String, Object>> _getSharedPreferencesMap() async {
    final Map<String, Object> fromSystem = await _store.getAll();
    assert(fromSystem != null);
    // Strip the flutter. prefix from the returned preferences.
    final Map<String, Object> preferencesMap = <String, Object>{};
    for (String key in fromSystem.keys) {
      assert(key.startsWith(_prefix));
      preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
    }
    return preferencesMap;
  }

plugins/shared_preferences.dart at e014c208909772cee2328a91b7225e667a2681a9 · flutter/pluginsより

どうしたらいいのか?

このプレフィックスについてはプライベートで定数定義されているので、削除したり変更したりできません。
プレフィックスを自由に設定できるように改修したプルリクエストもいくつかあるようですが、まだ採用はされていないようです。

解決法としてはnative_shared_preferencesというパッケージを使って、ネイティブからプレフィックスなしで値を取って来るのが今のところ一番手っ取り早そうです。

参考: native_shared_preferences | Flutter Package

このパッケージの説明にもありますが、version_migrationというパッケージを併用して、以前のネイティブアプリバージョンからの取得のみに使用してくださいとのことなので、ネイティブアプリからFlutterアプリへのリプレイス時に一度だけマイグレーションさせるのが良さそうです。

参考: version_migration | Flutter Package

注意点

native_shared_preferencesですが、iOSでDate型で保存していた値をミリ秒のUNIX時間としてDouble型で取得してきたり、Double型のデータに計算誤差のようなものがついてきたり(例:199.9→199.89999389648438)するみたいなので、少し癖があるようです。

実際にマイグレーションする際には正確に値が移行できているかテストする必要がありそうです。
おそらく上記のように正確に値が取得できない部分があるので、常用するのはやめましょうということなんだと思います。

まとめ

リプレイス自体そこまでケースとして多くはないと思いますが、すんなりshared_preferencesパッケージでそのまま利用できると思っていると、意外と面倒なことになっているので、一つ知識として持っておくと良いかもしれません。



株式会社ウイングドアでは、Ruby on RailsやPHPを活用したwebサービス、webサイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。