4

I have a pretty simple app with a dummy Activity and dummy Android Lifecycle ViewModel ViewModel.

FragmentActivity

class FragmentActivity: AppCompatActivity() {
    companion object {
        private const val TAG = "FragmentActivity"
        private const val KEY = "key_key"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fragment)
        Log.d(TAG, "Activity ${hashCode()}, onCreate: orientation ${resources.configuration.orientation}")

        if (savedInstanceState != null) {
            Log.d(TAG, "Activity ${hashCode()}, onCreate: saved string from savedInstanceState ${savedInstanceState.getString(KEY)}")
        } else {
            Log.d(TAG, "Activity ${hashCode()}, onCreate: no savedInstanceState")
        }

        val myViewModel: MyViewModel = ViewModelProviders
                .of(this, VmFactory())
                .get(MyViewModel::class.java)

    }

    override fun onResume() {
        super.onResume()

        Log.d(TAG, "Activity ${hashCode()}, onResume: orientation ${resources.configuration.orientation}")
    }

    override fun onStop() {
        super.onStop()

        Log.d(TAG, "Activity ${hashCode()}, onStop: orientation ${resources.configuration.orientation}")
    }

    override fun onDestroy() {
        super.onDestroy()

        Log.d(TAG, "Activity ${hashCode()}, onDestroy: orientation ${resources.configuration.orientation}")
    }

    override fun onSaveInstanceState(outState: Bundle?) {
        val savedString = "SAVED_STATE_" + hashCode()
        outState?.putString(KEY, savedString)
        Log.d(TAG, "Activity ${hashCode()}, onSaveInstanceState: $savedString")

        super.onSaveInstanceState(outState)
    }
}

ViewModel

class MyViewModel: ViewModel() {
    companion object {
        private const val TAG = "MyViewModel"
    }

    init {
        Log.d(TAG, "MyViewModel ${hashCode()}: created")
    }

    override fun onCleared() {
        Log.d(TAG, "MyViewModel ${hashCode()}: onCleared")

        super.onCleared()
    }
}

ViewModelFactory

class VmFactory: ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass == MyViewModel::class.java) {
            return MyViewModel() as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dkarmazi.unknownmemorysampleapp">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".WebViewActivity">
        </activity>

        <activity android:name=".FragmentActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Steps to kill the ViewModel

  1. Put the app to landscape mode
  2. Lock the screen
  3. Unlock the screen and observe that ViewModel is gone, Activity destroyed and created two times.

Log output

02-20 16:30:14.159 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onCreate: orientation 2
02-20 16:30:14.159 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onCreate: no savedInstanceState
02-20 16:30:14.169 8296-8296/com.dkarmazi.viewmodelscoping D/MyViewModel: MyViewModel 55090662: created
02-20 16:30:14.183 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onResume: orientation 2
### LOCKED IN LANDSCAPE MODE
02-20 16:30:22.978 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onSaveInstanceState: SAVED_STATE_244798673
02-20 16:30:22.996 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onStop: orientation 2
### UNLOCKED IN LANDSCAPE MODE
02-20 16:30:33.177 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onStop: orientation 2
02-20 16:30:33.178 8296-8296/com.dkarmazi.viewmodelscoping D/MyViewModel: MyViewModel 55090662: onCleared
02-20 16:30:33.179 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onDestroy: orientation 2
02-20 16:30:33.241 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 218111434, onCreate: orientation 1
02-20 16:30:33.241 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 218111434, onCreate: saved string from savedInstanceState SAVED_STATE_244798673
02-20 16:30:33.242 8296-8296/com.dkarmazi.viewmodelscoping D/MyViewModel: MyViewModel 113479034: created
02-20 16:30:33.248 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 218111434, onResume: orientation 1
02-20 16:30:33.705 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 218111434, onSaveInstanceState: SAVED_STATE_218111434
02-20 16:30:33.710 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 218111434, onStop: orientation 1
02-20 16:30:33.712 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 218111434, onDestroy: orientation 1
02-20 16:30:33.815 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 158140230, onCreate: orientation 2
02-20 16:30:33.815 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 158140230, onCreate: saved string from savedInstanceState SAVED_STATE_218111434
02-20 16:30:33.822 8296-8296/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 158140230, onResume: orientation 2

This behavior is consistent with findings here, however, I'd expect the arch library to handle this case as it is pretty standard and locking and unlocking in portrait mode works as expected.

Any good ideas on preventing that ViewModel from being destroyed in this particular scenario?

Tested on Nexus 5X, API 27

EDIT 1: after adding a string to be saved in onSaveInstanceState and checking if that string persists through all activity destroys and creates, I'm pretty sure that this is a bug with the library.

EDIT 2: Why is this a problem?

Problem 1: In case of landscape locking, the bundle gets somehow properly routed from Activity 244798673 to Activity 218111434 at 02-20 16:30:33.241, however the ViewModel is not able to persist through this sequence of actions. This is inconsistent with bundles behavior as we're technically still within the same Activity scope.

Problem 2: Log output for locking and unlocking in portrait mode:

02-20 16:38:10.283 8567-8567/? D/FragmentActivity: Activity 244798673, onCreate: orientation 1
02-20 16:38:10.283 8567-8567/? D/FragmentActivity: Activity 244798673, onCreate: no savedInstanceState
02-20 16:38:10.293 8567-8567/? D/MyViewModel: MyViewModel 55090662: created
02-20 16:38:10.301 8567-8567/? D/FragmentActivity: Activity 244798673, onResume: orientation 1
02-20 16:38:13.459 8567-8567/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onSaveInstanceState: SAVED_STATE_244798673
02-20 16:38:13.480 8567-8567/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onStop: orientation 1
02-20 16:38:17.704 8567-8567/com.dkarmazi.viewmodelscoping D/FragmentActivity: Activity 244798673, onResume: orientation 1

ViewModel is persisted in portrait locking and unlocking which is inconsistent with landscape scenario.

dkarmazi
  • 3,199
  • 1
  • 13
  • 25

2 Answers2

1

This is actually a bug that originates from Android framework:

https://issuetracker.google.com/issues/73644080

Android Arch Library under the hood uses retained fragments in order to persist ViewModels. Experimentally, in the same scenario of locking and unlocking the device in landscape mode when PIN / PATTERN / SWIPE / PASSWORD is turned on, retained fragments won't be able to survive it either. Therefore, each time we unlock the device, we will be getting a new instance of a ViewModel.

Some use cases:

  1. Device: NEXUS 5X, API level: 24, lock method: PIN / PATTERN / SWIPE / PASSWORD. Can reproduce by unlocking with any of the unlocking methods. Can also reproduce by unlocking with fingerprint.

  2. Device: NEXUS 5X, API level: 27, lock method: PIN / PATTERN / PASSWORD . Can reproduce only when unlocking the device with a fingerprint. Unlocking with PIN / PATTERN / PASSWORD works fine.

  3. Device: Pixel, API level: 27, lock method: PIN (haven't tested other) + fingerprint. Can reproduce only when unlocking the device with a fingerprint. Unlocking with PIN / PATTERN / PASSWORD works fine.

dkarmazi
  • 3,199
  • 1
  • 13
  • 25
0

The ViewModelFactory you provide here is not the Singleton. That should be the issue.

val myViewModel: MyViewModel = ViewModelProviders
                .of(this, VmFactory())
                .get(MyViewModel::class.java)

Make the factory to singleton, it should work.

Ponsuyambu
  • 7,876
  • 5
  • 27
  • 41
  • Nope, it will not help, if you look at the source code for `android.arch.lifecycle.ViewModelProvider::get()` will call `Factory::create` only if it could not retrieve the existing view model internally. Scope of the factory doesn't really matter in this case, `Factory::create` will be called regardless if it's a singleton or not. – dkarmazi Mar 11 '18 at 19:25
  • See the source below. If you provide your own factory, it uses the provided one else it uses a static instance of default factory(sDefaultFactory variable). https://android.googlesource.com/platform/frameworks/support/+/master/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java. – Ponsuyambu Mar 11 '18 at 21:35