In my application, I am using SharedPreferences to store some user preferences. The application was not obfuscated (-dontobfuscate in the proguard file).
Now in the next version of the application, I want to enable obfuscation. When I try this, the application returns NullPointerException while reading the SharedPreferences data from the previous version of the application. The error log is not helpful because the code is already obfuscated and it does not provide meaningful information. However, while trying in the debug mode I found the crash may be due to null context which is a static variable in the code! That should not be the case because the application works file if SharedPreferences were not there already.
Is there any way the app can still read the SharedPreferences data from unobfuscated version?
Writing / reading the SharedPreferences is pretty standard:
Writing:
SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor prefsEditor = mPrefs.edit();
prefsEditor.putString("userUnitsOption", "C");
//apply the storage
prefsEditor.apply();
Reading:
final SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
return mPrefs.getString("userUnitsOption", "A");
If you want to use shared preferences in Android then use it as given below :
First, you have to store your preferences like this :
SharedPreferences.Editor editor = getSharedPreferences("mySharedPref", MODE_PRIVATE).edit();
editor.putString("myName", "abc");
editor.apply();
Now, To read or get those stored shared preferences write code as given below :
SharedPreferences prefs = MainActivity.this.getSharedPreferences("mySharedPref", MODE_PRIVATE); // Here MainActivity.this represent the context. So you can use your context in place of MainActivity.this
String strName = prefs.getString("myName","defaultName");
In Kotlin,
Usage:
private val sharedPref = defaultPrefs(this)
To Save Data--> sharedPref[KEY] = *String data to save*
To Get Data --> val userDetails = sharedPref[KEY, ""]
Create a shared preference helper class like below.
object PreferenceHelper {
fun defaultPrefs(context: Context): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
fun customPrefs(context: Context, name: String): SharedPreferences =
context.getSharedPreferences(name, Context.MODE_PRIVATE)
inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = this.edit()
operation(editor)
editor.apply()
}
/**
* puts a key value pair in shared prefs if doesn't exists, otherwise updates value on given [key]
*/
operator fun SharedPreferences.set(key: String, value: Any?) {
when (value) {
is String? -> edit { it.putString(key, value) }
is Int -> edit { it.putInt(key, value) }
is Boolean -> edit { it.putBoolean(key, value) }
is Float -> edit { it.putFloat(key, value) }
is Long -> edit { it.putLong(key, value) }
else -> throw UnsupportedOperationException("Not yet implemented")
}
}
/**
* finds value on given key.
* [T] is the type of value
* #param defaultValue optional default value - will take null for strings, false for bool and -1 for numeric values if [defaultValue] is not specified
*/
inline operator fun <reified T : Any> SharedPreferences.get(
key: String,
defaultValue: T? = null
): T? {
return when (T::class) {
String::class -> getString(key, defaultValue as? String) as T?
Int::class -> getInt(key, defaultValue as? Int ?: -1) as T?
Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T?
Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T?
Long::class -> getLong(key, defaultValue as? Long ?: -1) as T?
else -> throw UnsupportedOperationException("Not yet implemented")
}
}
}
Related
I am using Androids shared preferences as a simple Storage like this:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
private val myKey = "my_string_key"
fun getMyString(): String {
return storage.getString(myKey, "default String") ?: "default String"
}
fun setMyString(str: String) {
storage.edit().apply {
putString(myKey, str)
apply()
}
}
}
However, I don't like how this introduces boilerplate code each time I add another stored value.
This is my current workaround:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
private inline fun put(block: (SharedPreferences.Editor) -> Unit) {
storage().edit().apply {
block(this)
apply()
}
}
var myString: String = "default String"
set(str) = put { it.putString(::myString.name, str) }
get() = storage.getString(::myString.name, field) ?: field
}
Now my Questions are:
Is this even a good idea? (Im new to programming on the Android platform and I haven't seen anyone do this, so there might be a good reason to stick to the function approach)
Can this be optimised further? Ideally I would like to only declare the variable and have the getter and setter generated somehow.
Thank you in advance.
I would choose different approach. You just need keys for this. For example something like this:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
public fun getKey(key: String): String = storage.getString(key, "defaultString") ?: "defaultString"
public fun set(key: String, value: String) = storage.edit().putString(key, value)
object Keys {
const val key1 = "Key1"
const val key2 = "Key2"
}
}
Or, if you want more safety for keys to be used as constants, not strings
enum class Keys {
key1, key2
}
class Storage(context: Context) {
private fun getDefaultValue(key:Keys): String = when(key) {
Keys.key1 -> "string"
Keys.key2 -> "String2"
}
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
public fun getKey(key: Keys): String =
storage.getString(key.name, null) ?: getDefaultValue(key)
public fun set(key: Keys, value: String) = storage.edit().putString(key.name, value)
}
I have a code where I called sharedPref.edit() and sharedPref.apply() multiple times. How to make convert it to call only once.
if (success) {
val data = response.getJSONObject("data")
sharedPreferences.edit().putBoolean("isLoggedIn", true).apply()
sharedPreferences.edit()
.putString("user_id", data.getString("user_id")).apply()
sharedPreferences.edit().putString("name", data.getString("name"))
.apply()
sharedPreferences.edit().putString("email", data.getString("email"))
.apply()
sharedPreferences.edit()
.putString("mobile_number", data.getString("mobile_number"))
.apply()
sharedPreferences.edit()
.putString("address", data.getString("address")).apply()
StyleableToast.Builder(this)
.text("Welcome " + data.getString("name"))
.backgroundColor(Color.RED)
.textColor(Color.WHITE).show()
userSuccessfullyLoggedIn()
}
I want to use the method call only once.
This can be called once, the returned editor instance can be stored in
a variable and re-used.
How to do this ??
These little steps will organize your code.
You can put it like this:
val editor = sharedPreferences.edit()
Then use it :
editor.putBoolean("isLoggedIn", true)
And Add others values without ".apply()"
Then Put at the End:
editor.apply()
you can create your custom Shared Preferences
class CustomSharedPreferences {
companion object {
private val PREFERENCES_USER_NAME = "preferences_user_name"
private var sharedPreferences: SharedPreferences? = null
#Volatile private var instance: CustomSharedPreferences? = null
private val lock = Any()
operator fun invoke(context: Context) : CustomSharedPreferences = instance ?: synchronized(lock){
instance ?: makeCustomSharedPreferences(context).also {
instance = it
}
}
private fun makeCustomSharedPreferences(context: Context) : CustomSharedPreferences{
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
return CustomSharedPreferences()
}
}
fun saveUser(name: String, email: String){
sharedPreferences?.edit(commit = true){
putString(PREFERENCES_USER_NAME, name)
}
}
fun getUser() = sharedPreferences?.getString(PREFERENCES_USER_NAME, "")
}
You can save all information to SP in saveUser().
I have this preference manager
class JournalManager {
lateinit var pref: SharedPreferences
lateinit var editor: SharedPreferences.Editor
lateinit var con: Context
var PRIVATE_MODE: Int = 0
constructor(con: Context?) {
if (con != null) {
this.con = con
}
if (con != null) {
pref = con.getSharedPreferences(PREF_NAME,PRIVATE_MODE)
}
editor = pref.edit()
}
companion object {
val PREF_NAME: String = "Journal"
val KEY_TEXT: String = "text"
}
fun createJournalSession(
text: EditText,
) {
editor.putString(KEY_TEXT, text.toString())
editor.commit()
}
fun getJournalDetails(): Map<String, String>
{
var journal: Map<String, String> = HashMap<String, String>()
pref.getString(KEY_TEXT,null)?.let { (journal as HashMap).put(KEY_TEXT, it) }
return journal
}
fun DeleteJournal() {
editor.clear()
editor.commit()
var i: Intent = Intent(con, JournalActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
con.startActivity(i)
}
}
And I'm looking for solution to add objects in it but my app is crashing, here is a sample of how I try to add object
lateinit var journalSession: JournalManager
override fun onCreate(savedInstanceState: Bundle?) {...}
fun openDialog() {
val dialog = MaterialDialog(this)
.noAutoDismiss()
.customView(R.layout.layout_new_journal)
//set initial preferences
dialog.findViewById<Button>(R.id.save_btn).setOnClickListener{
val note = dialog.findViewById<EditText>(R.id.new_journal_input)
//add to preference
journalSession.createJournalSession(
note
)
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.cancel_btn).setOnClickListener {
dialog.dismiss()
}
dialog.show()
}
Any suggestion?
Update
Let me make it clear what I'm looking for:
At the beginning I do not have any data, nor shared preference in device (shared preference will create when user saves it's first journal).
2.When user adds new journal it suppose to be stored (as of sample) like this
journal [{
note="this was user first note"
}]
Then when next time user adds new journal, it suppose to be stored (as of sample) like this
journal [{
note="this was user first note"
},
{note="this was user second note"
}]
and so on...
PS So far all videos, articles anything else I've found on web was with same logic: (they've had a List and then store that list into preferences!), my case is not like that, I do not have any list to store my list will be created one by one, during the time just like any real world apps.
Now, any idea how to make it happen, and what should I change in my code?
SharedPreferences saves primitive type data and Srting. For saving object like List you can use GSON library (by Google) to convert the object into JSON String.
1. Import Dependency:
implementation 'com.google.code.gson:gson:2.8.6'
2. Basic Usage:
val studentJsonString = Gson().toJson(student) //object -> String
val student = Gson().fromJson(studentJsonString, Student.class) //String -> object
3. For List:
val typeToken = TypeToken<List<Student>>(){}
val students = Gson().fromJson(studentsJsonString, typeToken.getType())
SharedPreferenceChangeListener can (and does!) skip notifications when db is updated with the same value e.g. boolean value from true to true. In practice you get notified when value is changed to a different value.
How can I get notification on every SharedPreference key/value update?
Simple, naive solution is to remove a key/value pair when it exists in SharedPreferences. Remember to ignore empty notification in SharedPreferenceChangeListener. You'll be notified when value is removed and when new value is inserted.
Check out preferenceManager.remove(key) in:
open class MyBasePreferences : KoinComponent {
val context: Context by inject()
val preferenceManager: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
/**
* Saves primitive value to shared preferences
*/
fun SharedPreferences.save(key: String, value: Any?) {
if (value == null)
throw RuntimeException("Trying to save null value!")
if (preferenceManager.contains(key))
preferenceManager.remove(key)
preferenceManager.edit {
when (value) {
is String -> putString(key, value)
is Long -> putLong(key, value)
is Boolean -> putBoolean(key, value)
is Int -> putInt(key, value)
is Float -> putFloat(key, value)
else -> throw RuntimeException("Type not supported in Preferences!")
}
}
}
}
custom SharedPrefChangeListener:
class MySharedPrefChangeListener : SharedPreferences.OnSharedPreferenceChangeListener, KoinComponent {
/**
* Called when a shared preference is changed, added, or removed.
*/
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (sharedPreferences == null)
return
if (sharedPreferences.contains(key)) {
when (key) {
"YOUR_KEY" -> {
// action to perform
}
}
}
}
}
inside App class:
/**
* Main application class
*/
class MyApp : Application(), KoinComponent {
var preferenceManager: SharedPreferences? = null
var prefChangeListener: MySharedPrefChangeListener? = null
override fun onCreate() {
super.onCreate()
preferenceManager = PreferenceManager.getDefaultSharedPreferences(this)
prefChangeListener = MySharedPrefChangeListener()
preferenceManager?.registerOnSharedPreferenceChangeListener(prefChangeListener)
}
}
The Code B is from webpage, the Code A will cause the error "java.lang.String cannot be cast to java.lang.Long".
I try to analyze reason:
First the key="hello" in SharedPreferences store the value of String, then I hope to get a value of Long from the same key, the app crash when Code B try to convert.
I think the Code B is not good, could you fix it?
I guess the code res as T in Code B is bad.
Code A
val key="hello"
try {
var my1: String by PreferenceTool(this, key, "-1")
my1 = "2"
val my2: Long by PreferenceTool(this, key, -1L)
var ss=my2
}catch (e:Exception){
logError(e.message?:"None")
}
Code B
class PreferenceTool<T>(private val context: Context, private val name: String, private val default: T) {
private val prefs: SharedPreferences by lazy {
context.defaultSharedPreferences
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
res as T
}
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
SharedPreferences save types as well as key/values. For example:
<string name="last-version">8.clan.0h</string>
<int name="last-city" value="3" />
<boolean name="record-cached" value="true" />
So if you call PreferenceTool(this, key, "-1"), you're looking for key hello, which is saved as String, and it will work as expected.
But if we look into the implementation for getLong in SharedPreferences class:
/**
* Retrieve a long value from the preferences.
*
* #param key The name of the preference to retrieve.
* #param defValue Value to return if this preference does not exist.
*
* #return Returns the preference value if it exists, or defValue. Throws
* ClassCastException if there is a preference with this name that is not
* a long.
*
* #throws ClassCastException
*/
long getLong(String key, long defValue);
Type for key hello is saved as String, so when PreferenceTool calls getLong you will get a ClassCastException as documented.
You can check the documentation here:
https://developer.android.com/reference/android/content/SharedPreferences
String in Kotlin already has an extension function you can call
toLong()
use this in your code.