13

I have the following problem: right now there is an app in the PlayStore that is written in native code (both iOS and Android) which I'm planning on migrating to flutter. My aim is that the users don't notice there were changes under the hood but can continue using the app like before. For that I need to migrate the shared preferences as well. This is, however, quite difficult. In the native Android application I stored the relevant shared preference like this:

SharedPreferences sharedPrefs = context.getSharedPreferences(
    "storage",
    Context.MODE_PRIVATE
);

sharedPrefs.putString('guuid', 'guuid_value');
editor.apply();

which results in a file being created at this path:

/data/data/patavinus.patavinus/shared_prefs/storage.xml

with this content:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <int name="guuid" value="guuid_value" />
</map>

If I use shared_prefences in Flutter to obtain this value by doing this:

final sharedPreferences = await SharedPreferences.getInstance();
sharedPreferences.getString('guuid');

it returns null because it looks for

/data/data/patavinus.patavinus/shared_prefs/FlutterSharedPreferences.xml which is the file that is written to when using shared_preferences in Flutter to store shared preferences. Because the shared prefs were written in native app context, the file is obviously not there.

Is there any way to tell Flutter to look for /data/data/patavinus.patavinus/shared_prefs/storage.xml without having to use platform channel?

I know how this works the other way around like it's mentioned here: How to access flutter Shared preferences on the android end (using java). This way is easy because in Android you can choose to prepend Flutter's prefix. However, in Flutter you can't.

I am also aware of this plugin: https://pub.dev/packages/native_shared_preferences however, I can't believe that a third party plugin is the recommended way. Also, I have spread the relevant shared preferences across multiple resource files. In this plugin, you can only set one (by specifying the string resource flutter_shared_pref_name).

Schnodderbalken
  • 3,257
  • 4
  • 34
  • 60
  • You can use `MethodChannel`s. See [this article](https://flutter.dev/docs/development/platform-integration/platform-channels). – creativecreatorormaybenot Jun 04 '20 at 16:25
  • 1
    He says he doens't want to use a `PlatformChannel` – Michel Feinstein Jun 05 '20 at 06:19
  • Flutter uses it's own name of the sharedprefs https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java check here. If your using same name that stored in Android preference then I belice flutter shared_prefs can read. not tested though – Hemanth S Nov 04 '21 at 08:26

5 Answers5

6

As suggested here you can get the native file's content and copy it to a new file. You can copy the content to flutter's storage file when the user upgrades to your flutter app for the first time.

Sami Haddad
  • 1,356
  • 10
  • 15
  • This is indeed a nice suggestion for the Android platform. The major drawback ist however that it does not work for iOS this way :(. – Schnodderbalken Jun 11 '20 at 10:25
  • On iOS it seems they only prefix the keys with `flutter.`. So creating a platform channel would be easier than you think, you can create a similar function to [this](https://github.com/flutter/plugins/blob/2fb8bf023a43ef93b2f372f277fa16399d79b744/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m#L69) without the flutter prefix. – Sami Haddad Jun 11 '20 at 11:00
3

Best option is to store shared preferences in native part in same file as SharedPreferences plugin does it. So it means to do like that:

  1. In Java code replace your SharedPreferences code, with same key, as it is in plugin: SharedPreferences sharedPref = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
  2. In native part save all preferences with prefix "flutter.". So in native part you will get needed preferences like this: String test = sharedPref.getString("flutter.test", "");
Andris
  • 3,895
  • 2
  • 24
  • 27
  • This is not an option as I don't have access to the native apps and can not migrate the keys. I only have the existing native apps and the desire to migrate to Flutter. – Schnodderbalken Nov 01 '21 at 07:11
  • @Schnodderbalken You can migrate to flutter only by creating apps on flutter. And if you are making apps on flutter, you can still write native code. – Andris Nov 01 '21 at 11:06
  • You suggest "store shared preferences in native part in same file as SharedPreferences plugin does it". But the preferences are ALREADY stored. There are native apps out there that have NOT stored it with flutter prefix. So how do I access them? That's what my question is about. – Schnodderbalken Nov 01 '21 at 13:57
  • @Schnodderbalken You can read them using specific preferences key and store them in new file, which one is using same key as flutter is using. – Andris Nov 05 '21 at 09:09
3

Since there was no satisfying answer to my question that works for both of the major operating systems and taking into account the distribution over multiple resource files, I have written a platform channel solution myself.

On Android (Kotlin) I let the caller provide a "file" argument because it's possible to have the data spread over multiple resource files (like I described in my question).

package my.test

import android.content.*
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 = "testChannel"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            when (call.method) {
                "getStringValue" -> {
                    val key: String? = call.argument<String>("key");
                    val file: String? = call.argument<String>("file");

                    when {
                        key == null -> {
                            result.error("KEY_MISSING", "Argument 'key' is not provided.", null)
                        }
                        file == null -> {
                            result.error("FILE_MISSING", "Argument 'file' is not provided.", null)
                        }
                        else -> {
                            val value: String? = getStringValue(file, key)
                            result.success(value)
                        }
                    }
                }
                else -> {
                    result.notImplemented()
                }
            }
        }
    }

    private fun getStringValue(file: String, key: String): String? {
        return context.getSharedPreferences(
                file,
                Context.MODE_PRIVATE
        ).getString(key, null);
    }
}

On iOS (Swift) this is not necessary as I'm working with UserDefaults

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let platformChannel = FlutterMethodChannel(
      name: "testChannel",
      binaryMessenger: controller.binaryMessenger
    )

    platformChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      guard call.method == "getStringValue" else {
        result(FlutterMethodNotImplemented)
        return
      }

      if let args: Dictionary<String, Any> = call.arguments as? Dictionary<String, Any>,
        let number: String = args["key"] as? String, {
        self?.getStringValue(key: key, result: result)
        return
      } else {
        result(
          FlutterError.init(
            code: "KEY_MISSING",
            message: "Argument 'key' is not provided.", 
            details: nil
          )
        )
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
    
private func getStringValue(key: String, result: FlutterResult) -> String? {
  let fetchedValue: String? = UserDefaults.object(forKey: key) as? String
  result(fetchedValue)
}

I'd wish for the SharedPreferences package to add the possibility of omitting the flutter prefix, enabling the developer to migrate content seamlessly from native apps.

I also wrote a blog post that explains the problem and the solution in a little bit more details: https://www.flutterclutter.dev/flutter/tutorials/read-shared-preferences-from-native-apps/2021/9753/

Schnodderbalken
  • 3,257
  • 4
  • 34
  • 60
  • 1
    This is indeed annoying and I cannot believe you and I are the only two people on earth migrating from native Android to Flutter and want to keep preferences? This is a major flaw on the developers especially considering how widely SharedPreferences is used. Thanks for asking this Q and providing your answer. – johnw182 Feb 20 '22 at 21:19
0

I am also aware of this plugin: https://pub.dev/packages/native_shared_preferences however, I can't believe that a third party plugin is the recommended way.

I think the third party plugin is just fine. The best thing about flutter is its amazing community that keeps contributing thoroughly to the ecosystem. That said, I would not recommend keeping using the non-official library. So you could test migrating your shared preferences behind the scenes.

  • In your initState query for flutter shared preferences.
  • If they exist, skip.
  • If they don't exist, query for native shared preferences, copy if exist

And done. This will help with the migration IMHO.

Raveesh Agarwal
  • 168
  • 1
  • 8
0

My choice is to treat it just as a file.
In flutter read the android shared preference file

/data/data/patavinus.patavinus/shared_prefs/storage.xml

take the values in the way you handle other xml file.

If you want to make some changes, don't use sharedPrefs.put, Instead write into the file (/data/data/patavinus.patavinus/shared_prefs/storage.xml ) using file's write method in flutter

prabhu r
  • 233
  • 2
  • 10
  • 16
  • @Schnodderbalken I don't know about iOS since I've not worked on it? – prabhu r Jun 22 '21 at 11:47
  • I understand. But that's what my question is about. I am seeking for a solution that works for both OS. – Schnodderbalken Jun 22 '21 at 11:50
  • If you know how to read write in a file in android & iOS that's enough. put a if condition to check if the app runs on android, and write it in a file using native method(java) & method channel , in else block do it iOS native way & method channel – prabhu r Jun 22 '21 at 11:59