Android sharedPreferences not saving with commit/apply/clear - java

I am trying to save the text contents of a TextView when my app closes, so I am trying to save that info in the onDestroy method, and then set them in the onCreate method.
I have written the following 2 functions to get rid of the boilerplate of getting and putting a value in the shared prefs:
fun MainActivity.putStringInPrefs(prefsFile: String, key: String, value: Any) =
getSharedPreferences(prefsFile, Context.MODE_PRIVATE)
.edit()
.putString(key, value.toString())
.apply()
fun MainActivity.getStringFromPrefs(prefsFile: String, key: String, default: Any = "") : String =
getSharedPreferences(prefsFile, Context.MODE_PRIVATE).getString(key, default.toString())
When the app closes this is what gets called:
override fun onDestroy() {
super.onDestroy()
log("OnDestroy!")
putStringInPrefs(mainPrefsFile, "lastSelectedItemDescription", textViewItemDetailsTextView.text) }
log is just a wrapper around Log.d("", "string")
And in the onCreate this gets called:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) ; log("OnCreate!")
setContentView(R.layout.activity_main)
// Set up the item details text view
textViewItemDetailsTextView.text = getStringFromPrefs(mainPrefsFile, "lastSelectedItemDescription", detailsText)
Problem is that whatever I do the preferences are not saved and the default value is always returned in onCreate. I have tried both apply and commit, with and without clear. No results. What am I doing wrong.

super.onDestroy() should be the last line in your overridden method onDestroy(). You might experience unexpected behaviour on whatever code you execute after "destructive" code has been executed

Save Activity state in onDestroy() is not good idea, because it is possible for the system to kill the process hosting your activity without calling the activity's final onDestroy() callback. (see docs).
To save state you can use onSaveInstanceState / onRestoreInstanceState instead. See: Saving and restoring activity state
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putString(YOUR_TEXT, textView.text)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
if (savedInstanceState != null) {
textView.text = savedInstanceState.getString(YOUR_TEXT)
}
}
companion object {
val YOUR_TEXT = "your_text"
}

Related

Android pass Edit Text data onChange from Activity to Fragment

I have a main activity with a heading and a search field (edit text), I want to be able to search and the results are immediately shown in the fragment, like an onChange instead of waiting for the user to click a button to filter results. (which is in the activity).
I can get it working if I include the Edit Text in my fragment too, but I don't want it that way for design purposes, I'd like to retrieve the user values as they are typed from the activity, and get them in my fragment to filter results
I've tried Bundles but could not get it working, and also not sure If i could use Bundles to get the results as they are being input.
Here's a screenshot to help understand better
You can make it happen using ViewModel + MVVM architecture.
MainActivity:
binding.editText.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
viewModel.updateSearchText(s)
}
})
ViewModel:
private val _searchText = MutableLiveData<Editable?>()
val searchText: LiveData<Editable?> get() = _searchText
fun updateSearchText(text: Editable?) {
_searchText.value = s
}
Fragment:
searchText.observe(viewLifecycleOwner) {
// TODO: handle the searched query using [it] keyword.
}
If you don't know what View Model is or how to implement it, use the official Google tutorial: https://developer.android.com/codelabs/basic-android-kotlin-training-viewmodel
Another way to achieve this (besides using an Android ViewModel) is use the Fragment Result API.
For instance, if you place the EditText into a fragment (let's call it QueryFragment), you can get the result of the QueryFragment in your SearchResults fragment like so:
// In QueryFragment
editText.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(s: Editable?) {
setFragmentResult("searchQueryRequestKey", bundleOf("searchQuery" to s.toString()))
}
})
// In SearchResultsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Retrieve the searched string from QueryFragment
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("searchQueryRequestKey") { requestKey, bundle ->
val searchQuery = bundle.getString("searchQuery")
// Perform the search using the searchQuery and display the search results
}
}

Rotating the screen on Android spams saved instance state and ActivityResultRegistry

My app was saving a bigger bundle in activity onSaveInstanceState() each time the screen was rotated, and I came across something really confusing. Here is a minimal example:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val rcs = outState.getIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
val keys = outState.getStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS")
Log.d("MainActivity", "${rcs?.size}: $rcs, $keys")
}
}
I get the following output each time the screen is rotated:
3: [1332505437, 1835553837, 670316111], [FragmentManager:StartIntentSenderForResult, ...
6: [1332505437, 91080073, 1835553837, 381123153, 1187376284, 670316111], ...
...
See the full log after 6 rotations: https://pastebin.com/yfE04Fmc
Each time the screen is rotated, 3 elements are added to these two entries. After rotating the screen for a while, the instance state becomes significantly bigger for no apparent reason.
Target API: 30
Tested on: API 28 and 30
Does anyone know why this is happening?
UPDATE: I want to clarify that I do not save any state into the Bundle. The values I show are created by Android itself: in ActivityResultRegistry. I've created an empty project for this example, and the code I provided is the only code I have - there is nothing else that interacts with the state Bundle.
UPDATE 2: Bug report submitted: https://issuetracker.google.com/issues/191893160
UPDATE 3: Bug fixed, will be a part of Activity 1.3.0-rc02 and 1.2.4 releases
Does anyone know why this is happening?
AppCompatActivity is extended from ComponentActivity (AppCompatActivity -> FragmentActivity -> ComponentActivity)
ComponentActivity is responsible for keeping the reference of ViewModelFactory, so that when the Activity recreates if can instantiate the same ViewModel before screen rotation.
Now inside ComponentActivity you will notice this:
#CallSuper
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
Lifecycle lifecycle = getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
}
super.onSaveInstanceState(outState);
mSavedStateRegistryController.performSave(outState);
mActivityResultRegistry.onSaveInstanceState(outState);
}
Here the important part is mActivityResultRegistry.onSaveInstanceState(outState)
Inside this method you will see
public final void onSaveInstanceState(#NonNull Bundle outState) {
outState.putIntegerArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_RCS,
new ArrayList<>(mRcToKey.keySet()));
outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS,
new ArrayList<>(mRcToKey.values()));
outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_LAUNCHED_KEYS,
new ArrayList<>(mLaunchedKeys));
outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS,
(Bundle) mPendingResults.clone());
outState.putSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT, mRandom);
}
So you can see from there that the Android platform itself is saving this key in the bundle. Hence you can not do much about it.
In other word, One of the parents of AppCompatActivity is saving these keys inside the Bundle. If you are not using ViewModel and do not want these keys to be saved; you can remove super.onSaveInstanceState(outState) from MainActivity (NOT RECOMMENDED). You will still get your saved keys but there could be some side effect, I am not aware of.
Good thing that you have reported an issue regarding this, let's see how the Google Team respond to it.
It all depends on how you store the data in Bundle, according to standard Android developer document https://developer.android.com/guide/components/activities/activity-lifecycle#restore-activity-ui-state-using-saved-instance-state
Saving the state:
override fun onSaveInstanceState(outState: Bundle?) {
// Save the user's current game state
outState?.run {
putInt(STATE_SCORE, currentScore)
putInt(STATE_LEVEL, currentLevel)
}
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(outState)
}
companion object {
val STATE_SCORE = "playerScore"
val STATE_LEVEL = "playerLevel"
}
Restoring the state:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // Always call the superclass first
// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
with(savedInstanceState) {
// Restore value of members from saved state
currentScore = getInt(STATE_SCORE)
currentLevel = getInt(STATE_LEVEL)
}
} else {
// Probably initialize members with default values for a new instance
}
// ...
}
Am not sure why you are getting the data from the bundle in onSaveInstanceState() method.
Also since you are saving list, want to highlight this point.
Saved instance state bundles persist through both configuration
changes and process death but are limited by storage and speed,
because onSavedInstanceState() serializes data to disk. Serialization
can consume a lot of memory if the objects being serialized are
complicated. Because this process happens on the main thread during a
configuration change, long-running serialization can cause dropped
frames and visual stutter.
Do not use store onSavedInstanceState() to store large amounts of
data, such as bitmaps, nor complex data structures that require
lengthy serialization or deserialization. Instead, store only
primitive types and simple, small objects such as String. As such, use
onSaveInstanceState() to store a minimal amount of data necessary,
such as an ID, to re-create the data necessary to restore the UI back
to its previous state should the other persistence mechanisms fail.
Most apps should implement onSaveInstanceState() to handle
system-initiated process death
First of all, why u don't use ViewModel instead?
This may not solve your problem but one thing that you could try is removing the duplicate values. Keep the latest ones and remove the olders. Something like this:
Kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
removeDuplicates(outState)
}
private fun removeDuplicates(bundle: Bundle) {
val prevRCS = bundle.getIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
val newRCS = bundle.getIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
val newKeys = bundle.getStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS")
if (newRCS == null || newKeys == null) return
if (prevRCS == null) {
bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS)
//bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys)
} else if(prevRCS.size() != newRCS.size()) {
for (rcs in prevRCS) {
val index = newRCS.indexOf(rcs)
newRCS.remove(index)
newKeys.remove(index)
}
bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS")
bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS")
bundle.putIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS)
bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS)
bundle.putStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys)
//bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys)
}
}
Java
#Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
removeDuplicates(outState);
}
private static final void removeDuplicates(final Bundle bundle)
{
if(bundle == null) return;
final ArrayList<Integer> prevRCS = bundle.getIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS");
final ArrayList<Integer> newRCS = bundle.getIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS");
final ArrayList<String> newKeys = bundle.getStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS");
if(newRCS == null || newKeys == null) return;
if(prevRCS == null) {
bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS);
//bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys);
} else if(prevRCS.size() != newRCS.size()) {
for(Integer rcs : prevRCS) {
final int index = newRCS.indexOf(rcs);
newRCS.remove(index);
newKeys.remove(index);
}
bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS");
bundle.remove("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS");
bundle.putIntegerArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS);
bundle.putIntegerArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_RCS", newRCS);
bundle.putStringArrayList("KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys);
//bundle.putStringArrayList("PREVIOUS_KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS", newKeys);
}
}

Android registerOnSharedPreferenceChangelistener only triggered a few times after that not working

Hello so I have the problem that I registered a onSharedPreferenceChangeListener in my MainActivity. The only preference I have is a ListPreference with 3 different options. So at the start of the program it still gets triggered the first - 3 times mostly, sometimes it doesn't even trigger at the beginning. I don't think that's how it is supposed to work so my code is down below if more is needed just write a comment of a specific part.
// (from MainActivity)
this.sharedPreferences.registerOnSharedPreferenceChangeListener { sharedPreferences: SharedPreferences, s: String ->
var value = sharedPreferences.getString("location", "")
controller.setLocation(value, this)
}
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
}
}
}
Found the solution by myself, for everyone who is having an issue like that. It's probably the scope. The listener gets lost when you are switching between activities.
How to fix: put the listener block as a global variable and just register it in your "onCreate" method.

How can I randomly reward points for app usage

I am working on an android app where user get points for using the app which can be used to unlock in-app features.
I have a function called rewardPoints() which generates random integer and I want it to get called randomly while the user is using the app. The points then gets added up in database.
fun rewardPoints() {
var points = Random().nextInt((5-1) + 1)
}
How do I call the function rewardPoints() randomly while the user is using/interacting with the app?
I'd use a Handler to post a Runnable that re-posts itself. Like so,
val handler = Handler()
handler.post({
rewardPoints()
handler.postDelayed(this, DELAY_TIME_MS)
})
You could kick this off in your Activity's onResume and stop it onPause to make sure it's only running when the app is active.
You could add an observer on your activities, check whether you have active activities and when that's the case start a periodic task to award points.
Sample:
class MyApp : Application(), Application.ActivityLifecycleCallbacks {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(this)
}
var count: Int by Delegates.observable(0) { _, old, newValue ->
when (newValue) {
0 -> onBackground()
1 -> if (old == 0) onForeground()
}
}
override fun onActivityResumed(activity: Activity?) {
count++
}
override fun onActivityPaused(activity: Activity?) {
count--
}
fun onForeground() {
Log.d("TAG", "start.")
events.start()
}
fun onBackground() {
Log.d("TAG", "stop.")
events.cancel()
}
val events = object: CountDownTimer(Long.MAX_VALUE, 1000) {
// is called once per second as long as your app is in foreground
override fun onTick(millisUntilFinished: Long) {
if (ThreadLocalRandom.current().nextInt(100) < 5) {
Toast.makeText(this#MyApp, "You earned a point.", Toast.LENGTH_SHORT).show()
}
}
override fun onFinish() { /* will never happen */}
}
/* not needed */
override fun onActivityStarted(activity: Activity?) {}
override fun onActivityDestroyed(activity: Activity?) {}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
override fun onActivityStopped(activity: Activity?) {}
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {}
}
If you use architecture components Lifecycle, implementing above is even simpler with https://developer.android.com/reference/android/arch/lifecycle/ProcessLifecycleOwner and listening to the desired Lifecycle.Event

How to set error label with TextInputLayout in the viewmodel with kotlin

I need to do some input validation for my TextInputEditText that is wrapped with TextInputLayout.
I'd like errors to appear below the line if the input is done in wrong format.
All the logic is done in the viewmodel instead of the view(fragment or activity). But I can't seem to access the view through viewmodel, for instance:
textinputlayout.setError("error") doesn't work in the viewmodel
and layout.findViewbyId(layoutId) doesn't work in the viewmodel either.
Any idea?
used below code to set error in TextInputLayout..
class SpinerActivity :AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.spiner)
setEdittext()
}
private fun setEdittext() {
var textError:TextInputLayout=findViewById(R.id.amEt1)
textError.error="Please Enter Name"
}
}

Categories