From 9b815fe412857dfe5a301cfc3b8522c5588200e8 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 15:20:34 -0400 Subject: [PATCH 01/10] Autoconversion by Android Studio --- .../LegacySharedPreferencesPlugin.java | 227 ------------------ .../SharedPreferencesListEncoder.java | 22 -- .../LegacySharedPreferencesPlugin.kt | 219 +++++++++++++++++ .../SharedPreferencesListEncoder.kt | 16 ++ 4 files changed, 235 insertions(+), 249 deletions(-) delete mode 100644 packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java delete mode 100644 packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java create mode 100644 packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt create mode 100644 packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java deleted file mode 100644 index 5a99d5f319b4..000000000000 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2013 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.sharedpreferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Base64; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -/** LegacySharedPreferencesPlugin */ -public class LegacySharedPreferencesPlugin implements FlutterPlugin, SharedPreferencesApi { - private static final String TAG = "SharedPreferencesPlugin"; - private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"; - // All identifiers must match the SharedPreferencesPlugin.kt file, as well as the strings.dart - // file. - private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; - // The symbol `!` was chosen as it cannot be created by the base 64 encoding used with - // LIST_IDENTIFIER. - private static final String JSON_LIST_IDENTIFIER = LIST_IDENTIFIER + "!"; - private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy"; - private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu"; - - private SharedPreferences preferences; - private final SharedPreferencesListEncoder listEncoder; - - public LegacySharedPreferencesPlugin() { - this(new ListEncoder()); - } - - @VisibleForTesting - LegacySharedPreferencesPlugin(@NonNull SharedPreferencesListEncoder listEncoder) { - this.listEncoder = listEncoder; - } - - private void setUp(@NonNull BinaryMessenger messenger, @NonNull Context context) { - preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); - try { - SharedPreferencesApi.Companion.setUp(messenger, this); - } catch (Exception ex) { - Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex); - } - } - - @Override - public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { - setUp(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { - SharedPreferencesApi.Companion.setUp(binding.getBinaryMessenger(), null); - } - - @Override - public boolean setBool(@NonNull String key, boolean value) { - return preferences.edit().putBoolean(key, value).commit(); - } - - @Override - public boolean setString(@NonNull String key, @NonNull String value) { - // TODO (tarrinneal): Move this string prefix checking logic to dart code and make it an - // Argument Error. - if (value.startsWith(LIST_IDENTIFIER) - || value.startsWith(BIG_INTEGER_PREFIX) - || value.startsWith(DOUBLE_PREFIX)) { - throw new RuntimeException( - "StorageError: This string cannot be stored as it clashes with special identifier" - + " prefixes"); - } - return preferences.edit().putString(key, value).commit(); - } - - @Override - public boolean setInt(@NonNull String key, long value) { - return preferences.edit().putLong(key, value).commit(); - } - - @Override - public boolean setDouble(@NonNull String key, double value) { - String doubleValueStr = Double.toString(value); - return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit(); - } - - @Override - public boolean remove(@NonNull String key) { - return preferences.edit().remove(key).commit(); - } - - @Override - public boolean setEncodedStringList(@NonNull String key, @NonNull String value) - throws RuntimeException { - return preferences.edit().putString(key, value).commit(); - } - - // Deprecated, for testing purposes only. - @Deprecated - @Override - public boolean setDeprecatedStringList(@NonNull String key, @NonNull List value) - throws RuntimeException { - return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)).commit(); - } - - @Override - public @NonNull Map getAll( - @NonNull String prefix, @Nullable List allowList) throws RuntimeException { - final Set allowSet = allowList == null ? null : new HashSet<>(allowList); - return getAllPrefs(prefix, allowSet); - } - - @Override - public boolean clear(@NonNull String prefix, @Nullable List allowList) - throws RuntimeException { - SharedPreferences.Editor clearEditor = preferences.edit(); - Map allPrefs = preferences.getAll(); - ArrayList filteredPrefs = new ArrayList<>(); - for (String key : allPrefs.keySet()) { - if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - filteredPrefs.add(key); - } - } - for (String key : filteredPrefs) { - clearEditor.remove(key); - } - return clearEditor.commit(); - } - - // Gets all shared preferences, filtered to only those set with the given prefix. - // Optionally filtered also to only those items in the optional [allowList]. - private @NonNull Map getAllPrefs( - @NonNull String prefix, @Nullable Set allowList) throws RuntimeException { - Map allPrefs = preferences.getAll(); - Map filteredPrefs = new HashMap<>(); - for (String key : allPrefs.keySet()) { - if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - filteredPrefs.put(key, transformPref(key, Objects.requireNonNull(allPrefs.get(key)))); - } - } - - return filteredPrefs; - } - - private Object transformPref(@NonNull String key, @NonNull Object value) { - if (value instanceof String) { - String stringValue = (String) value; - if (stringValue.startsWith(LIST_IDENTIFIER)) { - // The JSON-encoded lists use an extended prefix to distinguish them from - // lists that are encoded on the platform. - if (stringValue.startsWith(JSON_LIST_IDENTIFIER)) { - return value; - } else { - return listEncoder.decode(stringValue.substring(LIST_IDENTIFIER.length())); - } - } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { - // TODO (tarrinneal): Remove all BigInt code. - // https://github.com/flutter/flutter/issues/124420 - String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length()); - return new BigInteger(encoded, Character.MAX_RADIX); - } else if (stringValue.startsWith(DOUBLE_PREFIX)) { - String doubleStr = stringValue.substring(DOUBLE_PREFIX.length()); - return Double.valueOf(doubleStr); - } - } else if (value instanceof Set) { - // TODO (tarrinneal): Remove Set code. - // https://github.com/flutter/flutter/issues/124420 - - // This only happens for previous usage of setStringSet. The app expects a list. - @SuppressWarnings("unchecked") - List listValue = new ArrayList<>((Set) value); - // Let's migrate the value too while we are at it. - preferences - .edit() - .remove(key) - .putString(key, LIST_IDENTIFIER + listEncoder.encode(listValue)) - .apply(); - - return listValue; - } - return value; - } - - static class ListEncoder implements SharedPreferencesListEncoder { - @Override - public @NonNull String encode(@NonNull List list) throws RuntimeException { - try { - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - ObjectOutputStream stream = new ObjectOutputStream(byteStream); - stream.writeObject(list); - stream.flush(); - return Base64.encodeToString(byteStream.toByteArray(), 0); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("unchecked") - @Override - public @NonNull List decode(@NonNull String listString) throws RuntimeException { - try { - ObjectInputStream stream = - new StringListObjectInputStream(new ByteArrayInputStream(Base64.decode(listString, 0))); - return (List) stream.readObject(); - } catch (IOException | ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java deleted file mode 100644 index f49cab3670b0..000000000000 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2013 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.sharedpreferences; - -import androidx.annotation.NonNull; -import java.util.List; - -/** - * An interface used to provide conversion logic between List and String for - * SharedPreferencesPlugin. - */ -public interface SharedPreferencesListEncoder { - /** Converts list to String for storing in shared preferences. */ - @NonNull - String encode(@NonNull List list); - - /** Converts stored String representing List to List. */ - @NonNull - List decode(@NonNull String listString); -} diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt new file mode 100644 index 000000000000..3c57a279146e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -0,0 +1,219 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package io.flutter.plugins.sharedpreferences + +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import android.util.Log +import androidx.annotation.VisibleForTesting +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugins.sharedpreferences.SharedPreferencesApi.Companion.setUp +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.math.BigInteger +import java.util.Objects + +/** LegacySharedPreferencesPlugin */ +class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( + private val listEncoder: SharedPreferencesListEncoder +) : FlutterPlugin, SharedPreferencesApi { + private var preferences: SharedPreferences? = null + + constructor() : this(ListEncoder()) + + private fun setUp(messenger: BinaryMessenger, context: Context) { + preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + try { + setUp(messenger, this) + } catch (ex: Exception) { + Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex) + } + } + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + setUp(binding.getBinaryMessenger(), binding.getApplicationContext()) + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + setUp(binding.getBinaryMessenger(), null) + } + + override fun setBool(key: String, value: Boolean): Boolean { + return preferences!!.edit().putBoolean(key, value).commit() + } + + override fun setString(key: String, value: String): Boolean { + // TODO (tarrinneal): Move this string prefix checking logic to dart code and make it an + // Argument Error. + if (value.startsWith(LIST_IDENTIFIER) + || value.startsWith(BIG_INTEGER_PREFIX) + || value.startsWith(DOUBLE_PREFIX) + ) { + throw RuntimeException( + "StorageError: This string cannot be stored as it clashes with special identifier" + + " prefixes" + ) + } + return preferences!!.edit().putString(key, value).commit() + } + + override fun setInt(key: String, value: Long): Boolean { + return preferences!!.edit().putLong(key, value).commit() + } + + override fun setDouble(key: String, value: Double): Boolean { + val doubleValueStr = value.toString() + return preferences!!.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit() + } + + override fun remove(key: String): Boolean { + return preferences!!.edit().remove(key).commit() + } + + @Throws(RuntimeException::class) + override fun setEncodedStringList(key: String, value: String): Boolean { + return preferences!!.edit().putString(key, value).commit() + } + + // Deprecated, for testing purposes only. + @Deprecated("") + @Throws(RuntimeException::class) + override fun setDeprecatedStringList(key: String, value: MutableList): Boolean { + return preferences!!.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)) + .commit() + } + + @Throws(RuntimeException::class) + override fun getAll( + prefix: String, allowList: MutableList? + ): MutableMap { + val allowSet: MutableSet? = + if (allowList == null) null else HashSet(allowList) + return getAllPrefs(prefix, allowSet) + } + + @Throws(RuntimeException::class) + override fun clear(prefix: String, allowList: MutableList?): Boolean { + val clearEditor = preferences!!.edit() + val allPrefs = preferences!!.getAll() + val filteredPrefs = ArrayList() + for (key in allPrefs.keys) { + if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { + filteredPrefs.add(key) + } + } + for (key in filteredPrefs) { + clearEditor.remove(key) + } + return clearEditor.commit() + } + + // Gets all shared preferences, filtered to only those set with the given prefix. + // Optionally filtered also to only those items in the optional [allowList]. + @Throws(RuntimeException::class) + private fun getAllPrefs( + prefix: String, allowList: MutableSet? + ): MutableMap { + val allPrefs = preferences!!.getAll() + val filteredPrefs: MutableMap = HashMap() + for (key in allPrefs.keys) { + if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { + filteredPrefs.put( + key, + transformPref(key, Objects.requireNonNull(allPrefs.get(key))) + ) + } + } + + return filteredPrefs + } + + private fun transformPref(key: String, value: Any): Any { + if (value is String) { + val stringValue = value + if (stringValue.startsWith(LIST_IDENTIFIER)) { + // The JSON-encoded lists use an extended prefix to distinguish them from + // lists that are encoded on the platform. + if (stringValue.startsWith(JSON_LIST_IDENTIFIER)) { + return value + } else { + return listEncoder.decode(stringValue.substring(LIST_IDENTIFIER.length)) + } + } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { + // TODO (tarrinneal): Remove all BigInt code. + // https://github.com/flutter/flutter/issues/124420 + val encoded: String = stringValue.substring(BIG_INTEGER_PREFIX.length) + return BigInteger(encoded, Character.MAX_RADIX) + } else if (stringValue.startsWith(DOUBLE_PREFIX)) { + val doubleStr: String = stringValue.substring(DOUBLE_PREFIX.length) + return doubleStr.toDouble() + } + } else if (value is MutableSet<*>) { + // TODO (tarrinneal): Remove Set code. + // https://github.com/flutter/flutter/issues/124420 + + // This only happens for previous usage of setStringSet. The app expects a list. + + val listValue: MutableList = ArrayList(value as MutableSet) + // Let's migrate the value too while we are at it. + preferences!! + .edit() + .remove(key) + .putString(key, LIST_IDENTIFIER + listEncoder.encode(listValue)) + .apply() + + return listValue + } + return value + } + + internal class ListEncoder : SharedPreferencesListEncoder { + @Throws(RuntimeException::class) + override fun encode(list: MutableList): String { + try { + val byteStream = ByteArrayOutputStream() + val stream = ObjectOutputStream(byteStream) + stream.writeObject(list) + stream.flush() + return Base64.encodeToString(byteStream.toByteArray(), 0) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + @Throws(RuntimeException::class) + override fun decode(listString: String): MutableList { + try { + val stream: ObjectInputStream = + StringListObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) + return stream.readObject() as MutableList + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: ClassNotFoundException) { + throw RuntimeException(e) + } + } + } + + companion object { + private const val TAG = "SharedPreferencesPlugin" + private const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences" + + // All identifiers must match the SharedPreferencesPlugin.kt file, as well as the strings.dart + // file. + private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" + + // The symbol `!` was chosen as it cannot be created by the base 64 encoding used with + // LIST_IDENTIFIER. + private val JSON_LIST_IDENTIFIER: String = LIST_IDENTIFIER + "!" + private const val BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy" + private const val DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu" + } +} diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt new file mode 100644 index 000000000000..fd31e33d3c6a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package io.flutter.plugins.sharedpreferences + +/** + * An interface used to provide conversion logic between List and String for + * SharedPreferencesPlugin. + */ +interface SharedPreferencesListEncoder { + /** Converts list to String for storing in shared preferences. */ + fun encode(list: MutableList): String + + /** Converts stored String representing List to List. */ + fun decode(listString: String): MutableList +} From d60d56b89b1ce1adca462fa535a26d89101ea156 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 15:32:12 -0400 Subject: [PATCH 02/10] Fix type translations --- .../LegacySharedPreferencesPlugin.kt | 29 +++++++++---------- .../SharedPreferencesListEncoder.kt | 4 +-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index 3c57a279146e..4be6478ffc6d 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -85,22 +85,22 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( // Deprecated, for testing purposes only. @Deprecated("") @Throws(RuntimeException::class) - override fun setDeprecatedStringList(key: String, value: MutableList): Boolean { + override fun setDeprecatedStringList(key: String, value: List): Boolean { return preferences!!.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)) .commit() } @Throws(RuntimeException::class) override fun getAll( - prefix: String, allowList: MutableList? - ): MutableMap { - val allowSet: MutableSet? = - if (allowList == null) null else HashSet(allowList) + prefix: String, allowList: List? + ): Map { + val allowSet: Set? = + if (allowList == null) null else HashSet(allowList) return getAllPrefs(prefix, allowSet) } @Throws(RuntimeException::class) - override fun clear(prefix: String, allowList: MutableList?): Boolean { + override fun clear(prefix: String, allowList: List?): Boolean { val clearEditor = preferences!!.edit() val allPrefs = preferences!!.getAll() val filteredPrefs = ArrayList() @@ -119,10 +119,10 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( // Optionally filtered also to only those items in the optional [allowList]. @Throws(RuntimeException::class) private fun getAllPrefs( - prefix: String, allowList: MutableSet? - ): MutableMap { + prefix: String, allowList: Set? + ): Map { val allPrefs = preferences!!.getAll() - val filteredPrefs: MutableMap = HashMap() + val filteredPrefs: Map = HashMap() for (key in allPrefs.keys) { if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { filteredPrefs.put( @@ -155,13 +155,12 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( val doubleStr: String = stringValue.substring(DOUBLE_PREFIX.length) return doubleStr.toDouble() } - } else if (value is MutableSet<*>) { + } else if (value is Set<*>) { // TODO (tarrinneal): Remove Set code. // https://github.com/flutter/flutter/issues/124420 // This only happens for previous usage of setStringSet. The app expects a list. - - val listValue: MutableList = ArrayList(value as MutableSet) + val listValue: List = ArrayList(value as Set) // Let's migrate the value too while we are at it. preferences!! .edit() @@ -176,7 +175,7 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( internal class ListEncoder : SharedPreferencesListEncoder { @Throws(RuntimeException::class) - override fun encode(list: MutableList): String { + override fun encode(list: List): String { try { val byteStream = ByteArrayOutputStream() val stream = ObjectOutputStream(byteStream) @@ -189,11 +188,11 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( } @Throws(RuntimeException::class) - override fun decode(listString: String): MutableList { + override fun decode(listString: String): List { try { val stream: ObjectInputStream = StringListObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) - return stream.readObject() as MutableList + return stream.readObject() as List } catch (e: IOException) { throw RuntimeException(e) } catch (e: ClassNotFoundException) { diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt index fd31e33d3c6a..be9d2e2127d2 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt @@ -9,8 +9,8 @@ package io.flutter.plugins.sharedpreferences */ interface SharedPreferencesListEncoder { /** Converts list to String for storing in shared preferences. */ - fun encode(list: MutableList): String + fun encode(list: List): String /** Converts stored String representing List to List. */ - fun decode(listString: String): MutableList + fun decode(listString: String): List } From 6511298515c01faf78a9b35db0071a6330c69d84 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 15:49:09 -0400 Subject: [PATCH 03/10] Initial fixes --- .../LegacySharedPreferencesPlugin.kt | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index 4be6478ffc6d..c9b7f439894b 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -18,13 +18,12 @@ import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.math.BigInteger -import java.util.Objects /** LegacySharedPreferencesPlugin */ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( private val listEncoder: SharedPreferencesListEncoder ) : FlutterPlugin, SharedPreferencesApi { - private var preferences: SharedPreferences? = null + private lateinit var preferences: SharedPreferences constructor() : this(ListEncoder()) @@ -38,19 +37,19 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( } override fun onAttachedToEngine(binding: FlutterPluginBinding) { - setUp(binding.getBinaryMessenger(), binding.getApplicationContext()) + setUp(binding.binaryMessenger, binding.applicationContext) } override fun onDetachedFromEngine(binding: FlutterPluginBinding) { - setUp(binding.getBinaryMessenger(), null) + setUp(binding.binaryMessenger, null) } override fun setBool(key: String, value: Boolean): Boolean { - return preferences!!.edit().putBoolean(key, value).commit() + return preferences.edit().putBoolean(key, value).commit() } override fun setString(key: String, value: String): Boolean { - // TODO (tarrinneal): Move this string prefix checking logic to dart code and make it an + // TODO (tarrinneal): Move this string prefix checking logic to Dart code and make it an // Argument Error. if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX) @@ -61,32 +60,32 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( + " prefixes" ) } - return preferences!!.edit().putString(key, value).commit() + return preferences.edit().putString(key, value).commit() } override fun setInt(key: String, value: Long): Boolean { - return preferences!!.edit().putLong(key, value).commit() + return preferences.edit().putLong(key, value).commit() } override fun setDouble(key: String, value: Double): Boolean { val doubleValueStr = value.toString() - return preferences!!.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit() + return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit() } override fun remove(key: String): Boolean { - return preferences!!.edit().remove(key).commit() + return preferences.edit().remove(key).commit() } @Throws(RuntimeException::class) override fun setEncodedStringList(key: String, value: String): Boolean { - return preferences!!.edit().putString(key, value).commit() + return preferences.edit().putString(key, value).commit() } // Deprecated, for testing purposes only. @Deprecated("") @Throws(RuntimeException::class) override fun setDeprecatedStringList(key: String, value: List): Boolean { - return preferences!!.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)) + return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)) .commit() } @@ -101,8 +100,8 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( @Throws(RuntimeException::class) override fun clear(prefix: String, allowList: List?): Boolean { - val clearEditor = preferences!!.edit() - val allPrefs = preferences!!.getAll() + val clearEditor = preferences.edit() + val allPrefs = preferences.all val filteredPrefs = ArrayList() for (key in allPrefs.keys) { if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { @@ -121,14 +120,11 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( private fun getAllPrefs( prefix: String, allowList: Set? ): Map { - val allPrefs = preferences!!.getAll() - val filteredPrefs: Map = HashMap() - for (key in allPrefs.keys) { - if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - filteredPrefs.put( - key, - transformPref(key, Objects.requireNonNull(allPrefs.get(key))) - ) + val allPrefs = preferences.all + val filteredPrefs: MutableMap = HashMap() + for ((key, value) in allPrefs) { + if (key.startsWith(prefix) && value != null && (allowList == null || allowList.contains(key))) { + filteredPrefs[key] = transformPref(key, value) } } @@ -137,22 +133,21 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( private fun transformPref(key: String, value: Any): Any { if (value is String) { - val stringValue = value - if (stringValue.startsWith(LIST_IDENTIFIER)) { + if (value.startsWith(LIST_IDENTIFIER)) { // The JSON-encoded lists use an extended prefix to distinguish them from // lists that are encoded on the platform. - if (stringValue.startsWith(JSON_LIST_IDENTIFIER)) { - return value + return if (value.startsWith(JSON_LIST_IDENTIFIER)) { + value } else { - return listEncoder.decode(stringValue.substring(LIST_IDENTIFIER.length)) + listEncoder.decode(value.substring(LIST_IDENTIFIER.length)) } - } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { + } else if (value.startsWith(BIG_INTEGER_PREFIX)) { // TODO (tarrinneal): Remove all BigInt code. // https://github.com/flutter/flutter/issues/124420 - val encoded: String = stringValue.substring(BIG_INTEGER_PREFIX.length) + val encoded: String = value.substring(BIG_INTEGER_PREFIX.length) return BigInteger(encoded, Character.MAX_RADIX) - } else if (stringValue.startsWith(DOUBLE_PREFIX)) { - val doubleStr: String = stringValue.substring(DOUBLE_PREFIX.length) + } else if (value.startsWith(DOUBLE_PREFIX)) { + val doubleStr: String = value.substring(DOUBLE_PREFIX.length) return doubleStr.toDouble() } } else if (value is Set<*>) { @@ -160,9 +155,10 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( // https://github.com/flutter/flutter/issues/124420 // This only happens for previous usage of setStringSet. The app expects a list. + @Suppress("UNCHECKED_CAST") val listValue: List = ArrayList(value as Set) // Let's migrate the value too while we are at it. - preferences!! + preferences .edit() .remove(key) .putString(key, LIST_IDENTIFIER + listEncoder.encode(listValue)) @@ -192,6 +188,7 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( try { val stream: ObjectInputStream = StringListObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) + @Suppress("UNCHECKED_CAST") return stream.readObject() as List } catch (e: IOException) { throw RuntimeException(e) @@ -211,7 +208,7 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( // The symbol `!` was chosen as it cannot be created by the base 64 encoding used with // LIST_IDENTIFIER. - private val JSON_LIST_IDENTIFIER: String = LIST_IDENTIFIER + "!" + private const val JSON_LIST_IDENTIFIER = "$LIST_IDENTIFIER!" private const val BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy" private const val DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu" } From ce137487adfbeae480423307eaafe34a17aeeae9 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 15:56:04 -0400 Subject: [PATCH 04/10] Suppress lint for a replacement that doesn't wonk --- .../plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index c9b7f439894b..b39019608254 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -3,6 +3,7 @@ // found in the LICENSE file. package io.flutter.plugins.sharedpreferences +import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.util.Base64 @@ -20,6 +21,7 @@ import java.io.ObjectOutputStream import java.math.BigInteger /** LegacySharedPreferencesPlugin */ +@SuppressLint("UseKtx") class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( private val listEncoder: SharedPreferencesListEncoder ) : FlutterPlugin, SharedPreferencesApi { From 6d733ebc0dde3afe9760aeac470d9505469b4d8f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 16:35:50 -0400 Subject: [PATCH 05/10] Share constants --- .../LegacySharedPreferencesPlugin.kt | 23 +++++-------------- .../SharedPreferencesPlugin.kt | 5 ++-- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index b39019608254..3423837725ac 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -53,7 +53,7 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( override fun setString(key: String, value: String): Boolean { // TODO (tarrinneal): Move this string prefix checking logic to Dart code and make it an // Argument Error. - if (value.startsWith(LIST_IDENTIFIER) + if (value.startsWith(LIST_PREFIX) || value.startsWith(BIG_INTEGER_PREFIX) || value.startsWith(DOUBLE_PREFIX) ) { @@ -87,7 +87,7 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( @Deprecated("") @Throws(RuntimeException::class) override fun setDeprecatedStringList(key: String, value: List): Boolean { - return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)) + return preferences.edit().putString(key, LIST_PREFIX + listEncoder.encode(value)) .commit() } @@ -135,13 +135,13 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( private fun transformPref(key: String, value: Any): Any { if (value is String) { - if (value.startsWith(LIST_IDENTIFIER)) { + if (value.startsWith(LIST_PREFIX)) { // The JSON-encoded lists use an extended prefix to distinguish them from // lists that are encoded on the platform. - return if (value.startsWith(JSON_LIST_IDENTIFIER)) { + return if (value.startsWith(JSON_LIST_PREFIX)) { value } else { - listEncoder.decode(value.substring(LIST_IDENTIFIER.length)) + listEncoder.decode(value.substring(LIST_PREFIX.length)) } } else if (value.startsWith(BIG_INTEGER_PREFIX)) { // TODO (tarrinneal): Remove all BigInt code. @@ -163,7 +163,7 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( preferences .edit() .remove(key) - .putString(key, LIST_IDENTIFIER + listEncoder.encode(listValue)) + .putString(key, LIST_PREFIX + listEncoder.encode(listValue)) .apply() return listValue @@ -201,17 +201,6 @@ class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( } companion object { - private const val TAG = "SharedPreferencesPlugin" - private const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences" - - // All identifiers must match the SharedPreferencesPlugin.kt file, as well as the strings.dart - // file. - private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" - - // The symbol `!` was chosen as it cannot be created by the base 64 encoding used with - // LIST_IDENTIFIER. - private const val JSON_LIST_IDENTIFIER = "$LIST_IDENTIFIER!" private const val BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy" - private const val DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu" } } diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt index dc54cb39c067..31e9f236f4ac 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt @@ -31,11 +31,10 @@ import kotlinx.coroutines.runBlocking const val TAG = "SharedPreferencesPlugin" const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences" -// All identifiers must match the LegacySharedPreferencesPlugin.java file, as well as the -// strings.dart file. +// All identifiers must match the strings.dart file. const val LIST_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" // The symbol `!` was chosen as it cannot be created by the base 64 encoding used with LIST_PREFIX. -const val JSON_LIST_PREFIX = LIST_PREFIX + "!" +const val JSON_LIST_PREFIX = "$LIST_PREFIX!" const val DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu" /*package private*/ val Context.sharedPreferencesDataStore: DataStore by From a7f6ebf5f411e4f0bb83e976c44d43f427c5a617 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 16:37:20 -0400 Subject: [PATCH 06/10] Autoformat --- .../LegacySharedPreferencesPlugin.kt | 347 +++++++++--------- .../SharedPreferencesListEncoder.kt | 10 +- 2 files changed, 175 insertions(+), 182 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index 3423837725ac..d00b27cb0c6e 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -20,187 +20,180 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.math.BigInteger -/** LegacySharedPreferencesPlugin */ +/** LegacySharedPreferencesPlugin */ @SuppressLint("UseKtx") -class LegacySharedPreferencesPlugin @VisibleForTesting internal constructor( - private val listEncoder: SharedPreferencesListEncoder -) : FlutterPlugin, SharedPreferencesApi { - private lateinit var preferences: SharedPreferences - - constructor() : this(ListEncoder()) - - private fun setUp(messenger: BinaryMessenger, context: Context) { - preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) - try { - setUp(messenger, this) - } catch (ex: Exception) { - Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex) +class LegacySharedPreferencesPlugin +@VisibleForTesting +internal constructor(private val listEncoder: SharedPreferencesListEncoder) : + FlutterPlugin, SharedPreferencesApi { + private lateinit var preferences: SharedPreferences + + constructor() : this(ListEncoder()) + + private fun setUp(messenger: BinaryMessenger, context: Context) { + preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + try { + setUp(messenger, this) + } catch (ex: Exception) { + Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex) + } + } + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + setUp(binding.binaryMessenger, binding.applicationContext) + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + setUp(binding.binaryMessenger, null) + } + + override fun setBool(key: String, value: Boolean): Boolean { + return preferences.edit().putBoolean(key, value).commit() + } + + override fun setString(key: String, value: String): Boolean { + // TODO (tarrinneal): Move this string prefix checking logic to Dart code and make it an + // Argument Error. + if (value.startsWith(LIST_PREFIX) || + value.startsWith(BIG_INTEGER_PREFIX) || + value.startsWith(DOUBLE_PREFIX)) { + throw RuntimeException( + "StorageError: This string cannot be stored as it clashes with special identifier" + + " prefixes") + } + return preferences.edit().putString(key, value).commit() + } + + override fun setInt(key: String, value: Long): Boolean { + return preferences.edit().putLong(key, value).commit() + } + + override fun setDouble(key: String, value: Double): Boolean { + val doubleValueStr = value.toString() + return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit() + } + + override fun remove(key: String): Boolean { + return preferences.edit().remove(key).commit() + } + + @Throws(RuntimeException::class) + override fun setEncodedStringList(key: String, value: String): Boolean { + return preferences.edit().putString(key, value).commit() + } + + // Deprecated, for testing purposes only. + @Deprecated("") + @Throws(RuntimeException::class) + override fun setDeprecatedStringList(key: String, value: List): Boolean { + return preferences.edit().putString(key, LIST_PREFIX + listEncoder.encode(value)).commit() + } + + @Throws(RuntimeException::class) + override fun getAll(prefix: String, allowList: List?): Map { + val allowSet: Set? = if (allowList == null) null else HashSet(allowList) + return getAllPrefs(prefix, allowSet) + } + + @Throws(RuntimeException::class) + override fun clear(prefix: String, allowList: List?): Boolean { + val clearEditor = preferences.edit() + val allPrefs = preferences.all + val filteredPrefs = ArrayList() + for (key in allPrefs.keys) { + if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { + filteredPrefs.add(key) + } + } + for (key in filteredPrefs) { + clearEditor.remove(key) + } + return clearEditor.commit() + } + + // Gets all shared preferences, filtered to only those set with the given prefix. + // Optionally filtered also to only those items in the optional [allowList]. + @Throws(RuntimeException::class) + private fun getAllPrefs(prefix: String, allowList: Set?): Map { + val allPrefs = preferences.all + val filteredPrefs: MutableMap = HashMap() + for ((key, value) in allPrefs) { + if (key.startsWith(prefix) && + value != null && + (allowList == null || allowList.contains(key))) { + filteredPrefs[key] = transformPref(key, value) + } + } + + return filteredPrefs + } + + private fun transformPref(key: String, value: Any): Any { + if (value is String) { + if (value.startsWith(LIST_PREFIX)) { + // The JSON-encoded lists use an extended prefix to distinguish them from + // lists that are encoded on the platform. + return if (value.startsWith(JSON_LIST_PREFIX)) { + value + } else { + listEncoder.decode(value.substring(LIST_PREFIX.length)) } - } - - override fun onAttachedToEngine(binding: FlutterPluginBinding) { - setUp(binding.binaryMessenger, binding.applicationContext) - } - - override fun onDetachedFromEngine(binding: FlutterPluginBinding) { - setUp(binding.binaryMessenger, null) - } - - override fun setBool(key: String, value: Boolean): Boolean { - return preferences.edit().putBoolean(key, value).commit() - } - - override fun setString(key: String, value: String): Boolean { - // TODO (tarrinneal): Move this string prefix checking logic to Dart code and make it an - // Argument Error. - if (value.startsWith(LIST_PREFIX) - || value.startsWith(BIG_INTEGER_PREFIX) - || value.startsWith(DOUBLE_PREFIX) - ) { - throw RuntimeException( - "StorageError: This string cannot be stored as it clashes with special identifier" - + " prefixes" - ) - } - return preferences.edit().putString(key, value).commit() - } - - override fun setInt(key: String, value: Long): Boolean { - return preferences.edit().putLong(key, value).commit() - } - - override fun setDouble(key: String, value: Double): Boolean { - val doubleValueStr = value.toString() - return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit() - } - - override fun remove(key: String): Boolean { - return preferences.edit().remove(key).commit() - } - + } else if (value.startsWith(BIG_INTEGER_PREFIX)) { + // TODO (tarrinneal): Remove all BigInt code. + // https://github.com/flutter/flutter/issues/124420 + val encoded: String = value.substring(BIG_INTEGER_PREFIX.length) + return BigInteger(encoded, Character.MAX_RADIX) + } else if (value.startsWith(DOUBLE_PREFIX)) { + val doubleStr: String = value.substring(DOUBLE_PREFIX.length) + return doubleStr.toDouble() + } + } else if (value is Set<*>) { + // TODO (tarrinneal): Remove Set code. + // https://github.com/flutter/flutter/issues/124420 + + // This only happens for previous usage of setStringSet. The app expects a list. + @Suppress("UNCHECKED_CAST") val listValue: List = ArrayList(value as Set) + // Let's migrate the value too while we are at it. + preferences + .edit() + .remove(key) + .putString(key, LIST_PREFIX + listEncoder.encode(listValue)) + .apply() + + return listValue + } + return value + } + + internal class ListEncoder : SharedPreferencesListEncoder { @Throws(RuntimeException::class) - override fun setEncodedStringList(key: String, value: String): Boolean { - return preferences.edit().putString(key, value).commit() + override fun encode(list: List): String { + try { + val byteStream = ByteArrayOutputStream() + val stream = ObjectOutputStream(byteStream) + stream.writeObject(list) + stream.flush() + return Base64.encodeToString(byteStream.toByteArray(), 0) + } catch (e: IOException) { + throw RuntimeException(e) + } } - // Deprecated, for testing purposes only. - @Deprecated("") @Throws(RuntimeException::class) - override fun setDeprecatedStringList(key: String, value: List): Boolean { - return preferences.edit().putString(key, LIST_PREFIX + listEncoder.encode(value)) - .commit() - } - - @Throws(RuntimeException::class) - override fun getAll( - prefix: String, allowList: List? - ): Map { - val allowSet: Set? = - if (allowList == null) null else HashSet(allowList) - return getAllPrefs(prefix, allowSet) - } - - @Throws(RuntimeException::class) - override fun clear(prefix: String, allowList: List?): Boolean { - val clearEditor = preferences.edit() - val allPrefs = preferences.all - val filteredPrefs = ArrayList() - for (key in allPrefs.keys) { - if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - filteredPrefs.add(key) - } - } - for (key in filteredPrefs) { - clearEditor.remove(key) - } - return clearEditor.commit() - } - - // Gets all shared preferences, filtered to only those set with the given prefix. - // Optionally filtered also to only those items in the optional [allowList]. - @Throws(RuntimeException::class) - private fun getAllPrefs( - prefix: String, allowList: Set? - ): Map { - val allPrefs = preferences.all - val filteredPrefs: MutableMap = HashMap() - for ((key, value) in allPrefs) { - if (key.startsWith(prefix) && value != null && (allowList == null || allowList.contains(key))) { - filteredPrefs[key] = transformPref(key, value) - } - } - - return filteredPrefs - } - - private fun transformPref(key: String, value: Any): Any { - if (value is String) { - if (value.startsWith(LIST_PREFIX)) { - // The JSON-encoded lists use an extended prefix to distinguish them from - // lists that are encoded on the platform. - return if (value.startsWith(JSON_LIST_PREFIX)) { - value - } else { - listEncoder.decode(value.substring(LIST_PREFIX.length)) - } - } else if (value.startsWith(BIG_INTEGER_PREFIX)) { - // TODO (tarrinneal): Remove all BigInt code. - // https://github.com/flutter/flutter/issues/124420 - val encoded: String = value.substring(BIG_INTEGER_PREFIX.length) - return BigInteger(encoded, Character.MAX_RADIX) - } else if (value.startsWith(DOUBLE_PREFIX)) { - val doubleStr: String = value.substring(DOUBLE_PREFIX.length) - return doubleStr.toDouble() - } - } else if (value is Set<*>) { - // TODO (tarrinneal): Remove Set code. - // https://github.com/flutter/flutter/issues/124420 - - // This only happens for previous usage of setStringSet. The app expects a list. - @Suppress("UNCHECKED_CAST") - val listValue: List = ArrayList(value as Set) - // Let's migrate the value too while we are at it. - preferences - .edit() - .remove(key) - .putString(key, LIST_PREFIX + listEncoder.encode(listValue)) - .apply() - - return listValue - } - return value - } - - internal class ListEncoder : SharedPreferencesListEncoder { - @Throws(RuntimeException::class) - override fun encode(list: List): String { - try { - val byteStream = ByteArrayOutputStream() - val stream = ObjectOutputStream(byteStream) - stream.writeObject(list) - stream.flush() - return Base64.encodeToString(byteStream.toByteArray(), 0) - } catch (e: IOException) { - throw RuntimeException(e) - } - } - - @Throws(RuntimeException::class) - override fun decode(listString: String): List { - try { - val stream: ObjectInputStream = - StringListObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) - @Suppress("UNCHECKED_CAST") - return stream.readObject() as List - } catch (e: IOException) { - throw RuntimeException(e) - } catch (e: ClassNotFoundException) { - throw RuntimeException(e) - } - } - } - - companion object { - private const val BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy" - } + override fun decode(listString: String): List { + try { + val stream: ObjectInputStream = + StringListObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) + @Suppress("UNCHECKED_CAST") return stream.readObject() as List + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: ClassNotFoundException) { + throw RuntimeException(e) + } + } + } + + companion object { + private const val BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy" + } } diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt index be9d2e2127d2..8b768737ddde 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt @@ -6,11 +6,11 @@ package io.flutter.plugins.sharedpreferences /** * An interface used to provide conversion logic between List and String for * SharedPreferencesPlugin. - */ + */ interface SharedPreferencesListEncoder { - /** Converts list to String for storing in shared preferences. */ - fun encode(list: List): String + /** Converts list to String for storing in shared preferences. */ + fun encode(list: List): String - /** Converts stored String representing List to List. */ - fun decode(listString: String): List + /** Converts stored String representing List to List. */ + fun decode(listString: String): List } From d2e1ea26cec4915f13ccfe26a31188fc21993192 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 7 Jul 2026 16:41:48 -0400 Subject: [PATCH 07/10] Use a non-null mock binary messenger --- .../plugins/sharedpreferences/LegacySharedPreferencesTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java index 5e95d637ae2e..c9e1c8344367 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java @@ -39,6 +39,7 @@ public void before() { Context context = Mockito.mock(Context.class); SharedPreferences sharedPrefs = new FakeSharedPreferences(); + mockMessenger = Mockito.mock(BinaryMessenger.class); flutterPluginBinding = Mockito.mock(FlutterPlugin.FlutterPluginBinding.class); Mockito.when(flutterPluginBinding.getBinaryMessenger()).thenReturn(mockMessenger); From 36be4096eac9e448daea3ac4758f503f77970905 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 8 Jul 2026 10:40:12 -0400 Subject: [PATCH 08/10] Idiomatic Kotlin --- .../LegacySharedPreferencesPlugin.kt | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index d00b27cb0c6e..3c5588666e90 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -77,55 +77,38 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : return preferences.edit().remove(key).commit() } - @Throws(RuntimeException::class) override fun setEncodedStringList(key: String, value: String): Boolean { return preferences.edit().putString(key, value).commit() } // Deprecated, for testing purposes only. @Deprecated("") - @Throws(RuntimeException::class) override fun setDeprecatedStringList(key: String, value: List): Boolean { return preferences.edit().putString(key, LIST_PREFIX + listEncoder.encode(value)).commit() } - @Throws(RuntimeException::class) override fun getAll(prefix: String, allowList: List?): Map { - val allowSet: Set? = if (allowList == null) null else HashSet(allowList) - return getAllPrefs(prefix, allowSet) + return getAllPrefs(prefix, allowList?.toSet()) } - @Throws(RuntimeException::class) override fun clear(prefix: String, allowList: List?): Boolean { val clearEditor = preferences.edit() - val allPrefs = preferences.all - val filteredPrefs = ArrayList() - for (key in allPrefs.keys) { - if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - filteredPrefs.add(key) - } - } - for (key in filteredPrefs) { - clearEditor.remove(key) - } + val allowSet = allowList?.toSet() + preferences.all.keys + .filter { key -> key.startsWith(prefix) && (allowSet == null || allowSet.contains(key)) } + .forEach { key -> clearEditor.remove(key) } return clearEditor.commit() } // Gets all shared preferences, filtered to only those set with the given prefix. - // Optionally filtered also to only those items in the optional [allowList]. - @Throws(RuntimeException::class) + // Optionally filtered also to only those items in private fun getAllPrefs(prefix: String, allowList: Set?): Map { - val allPrefs = preferences.all - val filteredPrefs: MutableMap = HashMap() - for ((key, value) in allPrefs) { - if (key.startsWith(prefix) && - value != null && - (allowList == null || allowList.contains(key))) { - filteredPrefs[key] = transformPref(key, value) - } - } - - return filteredPrefs + return preferences.all + .filter { (key, value) -> + key.startsWith(prefix) && value != null && (allowList == null || allowList.contains(key)) + } + // Force-unwrapping is safe here due to the `value != null` in the filter above. + .mapValues { (key, value) -> transformPref(key, value!!) } } private fun transformPref(key: String, value: Any): Any { @@ -152,7 +135,7 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : // https://github.com/flutter/flutter/issues/124420 // This only happens for previous usage of setStringSet. The app expects a list. - @Suppress("UNCHECKED_CAST") val listValue: List = ArrayList(value as Set) + @Suppress("UNCHECKED_CAST") val listValue = (value as Set).toList() // Let's migrate the value too while we are at it. preferences .edit() @@ -166,7 +149,6 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : } internal class ListEncoder : SharedPreferencesListEncoder { - @Throws(RuntimeException::class) override fun encode(list: List): String { try { val byteStream = ByteArrayOutputStream() @@ -179,7 +161,6 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : } } - @Throws(RuntimeException::class) override fun decode(listString: String): List { try { val stream: ObjectInputStream = From aaafd157ee116f6df261a877b554648fa4e86da1 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 8 Jul 2026 11:15:59 -0400 Subject: [PATCH 09/10] Version bump --- .../shared_preferences_android/CHANGELOG.md | 4 ++++ .../shared_preferences_android/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index b4ef17c76bf0..702f103174ba 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.27 + +* Converts legacy backend to Kotlin. + ## 2.4.26 * Updates internal implementation to use Kotlin Pigeon. diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 9dabd4fa232b..8f152209487a 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_android description: Android implementation of the shared_preferences plugin repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.4.26 +version: 2.4.27 environment: sdk: ^3.12.0 From 588a848fdc1f710d7d0a0f30dc301e8d35eef27e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 8 Jul 2026 11:16:04 -0400 Subject: [PATCH 10/10] Gemini review --- .../LegacySharedPreferencesPlugin.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt index 3c5588666e90..a5f2757bf969 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -12,7 +12,6 @@ import androidx.annotation.VisibleForTesting import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugins.sharedpreferences.SharedPreferencesApi.Companion.setUp import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.IOException @@ -33,7 +32,7 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : private fun setUp(messenger: BinaryMessenger, context: Context) { preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) try { - setUp(messenger, this) + SharedPreferencesApi.setUp(messenger, this) } catch (ex: Exception) { Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex) } @@ -44,7 +43,7 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : } override fun onDetachedFromEngine(binding: FlutterPluginBinding) { - setUp(binding.binaryMessenger, null) + SharedPreferencesApi.setUp(binding.binaryMessenger, null) } override fun setBool(key: String, value: Boolean): Boolean { @@ -103,12 +102,15 @@ internal constructor(private val listEncoder: SharedPreferencesListEncoder) : // Gets all shared preferences, filtered to only those set with the given prefix. // Optionally filtered also to only those items in private fun getAllPrefs(prefix: String, allowList: Set?): Map { - return preferences.all - .filter { (key, value) -> - key.startsWith(prefix) && value != null && (allowList == null || allowList.contains(key)) + return buildMap { + preferences.all.forEach { (key, value) -> + if (key.startsWith(prefix) && + value != null && + (allowList == null || allowList.contains(key))) { + put(key, transformPref(key, value)) } - // Force-unwrapping is safe here due to the `value != null` in the filter above. - .mapValues { (key, value) -> transformPref(key, value!!) } + } + } } private fun transformPref(key: String, value: Any): Any {