Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Read shared preferences in Flutter app that were previously stored in a native app

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).

like image 488
Schnodderbalken Avatar asked Jun 02 '20 08:06

Schnodderbalken


People also ask

How to use sharedpreferences in flutter?

With SharedPreferences, you can configure your Flutter app to remember the data even after the user terminates their activity. SharedPreferences can be used to store critical data such as passwords, tokens, and complex relational data. In this tutorial, we’ll demonstrate how to persist and modify data using SharedPreferences in a Flutter app.

How to store data locally in a flutter application?

In this article, we will use the shared preferences plugin to store data locally in a Flutter application. First we need to create a Flutter project, after following the documentation and installing the Flutter SDK. You can then open vscode or android studio and execute in the terminal the following command:

Can I migrate from native apps to Flutter apps?

Sometimes it is necessary to migrate from native apps (e. g. iOS or Android) to a Flutter app. If there is already a user base, you do not want all users to lose their settings stored in the shared preferences once they update their app.

How to use flutter SDK with Android Studio?

To use Android Studio to develop Flutter projects, you’ll need to install the following plugins: This command creates a folder, shared_pref, and places a Flutter project inside it. You can open the project using VS Code or Android Studio. Now that we have the Flutter SDK ready and set, it’s time to install the shared_preferences plugin.


2 Answers

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/

like image 158
Schnodderbalken Avatar answered Sep 22 '22 21:09

Schnodderbalken


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.

like image 24
Sami Haddad Avatar answered Sep 24 '22 21:09

Sami Haddad