12

For my Android project, I need global singleton Cache object to access data about a user through the app.

A problem occurs when an app goes into the background and after some time of using other apps I try to open app variables in Cache objects are null. Everything is okay when I kill the app and open it again. I'm using dependency injection to access Cache object. Why doesn't app start again if that happened? Is there some annotation to keep cache variable even in low memory conditions?

This is my Cache class

class Cache {
    var categories : Array<BaseResponse.Category>? = null
    var user : BaseResponse.User? = null
    var options : BaseResponse.OptionsMap? = null
    var order: MenuOrderDataModel? = null
}

This is Storage module for DI

@Module class StorageModule {

    @Singleton @Provides fun getSharedPrefs(context: Context): SharedPreferences {
        return PreferenceManager.getDefaultSharedPreferences(context)
    }


    @Singleton @Provides fun getCache() : Cache = Cache()
}

I inject object @Inject lateinit var cache: Cache and then populate with user data in splash screen.

Edit - added code snippets from Application and launch activity

class MyApp : Application() {
    val component: ApplicationComponent by lazy {
        DaggerApplicationComponent
                .builder()
                .appModule(AppModule(this))
                .build()
    }

    companion object {
        @JvmStatic lateinit var myapp: MyApp 
    }

    override fun onCreate() {
        super.onCreate()
        myapp= this
        Fabric.with(this, Crashlytics())
    }
}

Splash activity:

class SplashActivity : AppCompatActivity(), View.OnClickListener {

    @Inject lateinit var viewModel : ISplashViewModel
    private lateinit var disposable : Disposable

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)

        MyApp.myapp.component.inject(this)
}
  • Probably you run depednency injection from `Application.onCreate` instead of `MainActivity.onCreate` – Marko Topolnik Mar 01 '18 at 09:50
  • Yes. Can you explain a bit more why in MainActivity? Because in SplashActivity I need to inject viewmodel. I've edited answer so you can see my Application class and how I use DI – Nikola Samardzija Mar 01 '18 at 11:34
  • You probably set up the variables in `Cache` only in ONE activity, and your app restarts from some other Activity after process death. – EpicPandaForce Mar 04 '18 at 21:19
  • Yes, you are right, but should I initialize user data in every activity to keep app consistent? Is there a way to annotate cache in dagger so cache variables keep they state after cleaning of ram? It will be great if dagger can automatically serialize and then deserialize, or something like that. – Nikola Samardzija Mar 04 '18 at 21:32
  • Now if only the code for initializing some variable in `Cache` was part of the question... :) – EpicPandaForce Mar 04 '18 at 22:01

2 Answers2

38

You're getting crashes because you initialize those variables in one Activity, and expect it to be set always, in the other Activity.

But Android doesn't work like that, you can easily end up crashing because after low memory condition happens, only the current Activity gets recreated at first, previous Activity is recreated on back navigation, and all static variables are nulled out (because the process is technically restarted).

Try it out:

  • put your application in background with HOME button

  • click the TERMINATE button on Logcat tab

terminate button

  • then re-launch the app from the launcher.

You'll experience this phenomenon.

In Android Studio 4.0, after you use Run from Android Studio instead of starting the app from launcher, the Terminate button SOMETIMES works a bit differently and MIGHT force-stop your app making it forget your task state on your attempt. In that case, just try again, launching the app from launcher. It will work on your second try.

You can also trigger the "Terminate Application" behavior from the terminal, as per https://codelabs.developers.google.com/codelabs/android-lifecycles/#6, it looks like this:

$ adb shell am kill your.app.package.name

In the new Logcat (Android Studio Electric Eel and above), the "terminate process" button is removed, and was moved into the Device Monitor tab ("Kill Process").

enter image description here


Solution: check for nulls and re-initialize things in a base activity (or in LiveData.onActive), or use onSaveInstanceState.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • It would be great if we can keep those variables even in low memory conditions. So if that is not possible, then this solution is correct. Thanks – Nikola Samardzija Mar 07 '18 at 09:30
  • It is not possible to keep them, that is why they are nulled out. – EpicPandaForce Mar 07 '18 at 09:48
  • use persistent storage with either SQL or Room or Shared Preferences, if you need to, or save it in a file with some encryption. – SowingFiber Dec 16 '19 at 05:24
  • @EpicPandaForce Could we recreate the same behavior by using the "Don't keep Activities" setting in developer options? – 11m0 Jan 09 '20 at 21:30
  • 1
    Nope, that's a completely different behavior as `static` variables don't get `null`ed out. Also Parcelables are cached, so it can mask certain state restoration class loader bugs, like the one you get when you use `BaseSavedState` in a class that `extends RecyclerView`. – EpicPandaForce Jan 09 '20 at 21:34
0

I would like to expand on @EpicPandaForce answer with implementation details using savedInstanceState and SavedStateHandle.


Official documentation: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate

Note that I’m using Koin for dependency injection.

Inject your viewModel inside your Activity as stateViewModel:

val viewModel: MyActivityViewModel by stateViewModel(state = { Bundle() })

That way you can inject SavedStateHandle inside ViewModel and handle storing and restoring data inside it.

Override onPause method in your Activity that will be opened after the process is killed so you can storeState at that moment.

override fun onPause() {
    super.onPause()
    viewModel.storeState()
}

Store all properties you don’t want to become null after the process is killed. An object has to be parcelable.

savedStateHandle.set(STATE_KEY, yourProperty)

In the onCreate method, you will check if savedInstanceState is not null so you can restore the state.

override fun onCreate(savedInstanceState: Bundle?) {
    setTheme(R.style.AppTheme)
    super.onCreate(savedInstanceState)

    if (savedInstanceState != null) {
        viewModel.restoreState()
    }
}

Reinit your property and remove that value from savedStateHandle

fun restoreState() {
    yourProperty = savedStateHandle.get<Any>(STATE_KEY)
    savedStateHandle.remove<Any>(STATE_KEY)
}