Android Lifecycle ViewModel не сохраняется после блокировки и разблокировки в ландшафтном режиме

У меня есть довольно простое приложение с фиктивным Activity и фиктивным Android Lifecycle ViewModel ViewModel.

Фрагментальная активность

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)
    }
}

Модель представления

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()
    }
}

Фабрика моделей модели

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()
        }
    }
}

Манифест

<?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>

Шаги по уничтожению ViewModel

  1. Переведите приложение в ландшафтный режим
  2. Заблокировать экран
  3. Разблокируйте экран и обратите внимание, что ViewModel исчезла, Activity уничтожена и создана два раза.

Вывод журнала

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

Такое поведение согласуется с выводами здесь, однако я ожидаю библиотека Arch для обработки этого случая, поскольку она довольно стандартна, а блокировка и разблокировка в портретном режиме работает, как и ожидалось.

Есть хорошие идеи по предотвращению уничтожения этого ViewModel в этом конкретном сценарии?

Протестировано на Nexus 5X, API 27.

РЕДАКТИРОВАТЬ 1: после добавления строки для сохранения в onSaveInstanceState и проверки, сохраняется ли эта строка во всех действиях, уничтожающих и создающих, я почти уверен, что это ошибка библиотеки.

EDIT 2: Почему это проблема?

Проблема 1: в случае блокировки ландшафта пакет каким-то образом правильно перенаправляется из действия 244798673 в действие 218111434 по адресу 02-20 16:30:33.241, однако ViewModel не может сохраняться в этой последовательности действий. Это несовместимо с поведением пакетов, поскольку технически мы все еще находимся в одной и той же области действия.

Проблема 2: Вывод лога блокировки и разблокировки в портретном режиме:

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 сохраняется при блокировке и разблокировке портретной ориентации, что несовместимо с ландшафтным сценарием.




Ответы (2)


На самом деле это ошибка, возникающая в рамках Android:

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

Библиотека Android Arch использует сохраненные фрагменты для сохранения ViewModels. Экспериментально, в одном и том же сценарии блокировки и разблокировки устройства в ландшафтном режиме при включенном PIN/PATTERN/SWIPE/PASSWORD сохранившиеся фрагменты тоже не смогут его пережить. Поэтому каждый раз, когда мы разблокируем устройство, мы будем получать новый экземпляр файла ViewModel.

Некоторые варианты использования:

  1. Устройство: NEXUS 5X, уровень API: 24, метод блокировки: PIN-код/ШАБЛОН/ПРОВЕДЕНИЕ/ПАРОЛЬ. Воспроизвести можно, разблокировав любым из способов разблокировки. Также можно воспроизвести, разблокировав отпечатком пальца.

  2. Устройство: NEXUS 5X, уровень API: 27, метод блокировки: PIN-код/шаблон/пароль. Может воспроизвести только при разблокировке устройства отпечатком пальца. Разблокировка с помощью PIN-кода/ШАБЛОНА/ПАРОЛЯ работает нормально.

  3. Устройство: Pixel, уровень API: 27, метод блокировки: PIN-код (другой не тестировал) + отпечаток пальца. Может воспроизвести только при разблокировке устройства отпечатком пальца. Разблокировка с помощью PIN-кода/ШАБЛОНА/ПАРОЛЯ работает нормально.

person dkarmazi    schedule 14.03.2018

Предоставленная вами ViewModelFactory не является синглтоном. Это должно быть проблемой.

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

Сделайте фабрику синглтоном, должно работать.

person Ponsuyambu    schedule 11.03.2018
comment
Нет, это не поможет, если вы посмотрите на исходный код для android.arch.lifecycle.ViewModelProvider::get(), он вызовет Factory::create только в том случае, если он не может получить существующую модель представления внутри. Объем фабрики в этом случае не имеет большого значения, Factory::create будет вызываться независимо от того, синглтон это или нет. - person dkarmazi; 11.03.2018
comment
См. источник ниже. Если вы предоставляете свою собственную фабрику, она использует предоставленную, в противном случае она использует статический экземпляр фабрики по умолчанию (переменная sDefaultFactory). android.googlesource.com/platform/frameworks/support/+/master/. - person Ponsuyambu; 12.03.2018