Comments 52
Другой пример — androidx.lifecycle.ViewModel. Этот класс бесполезен чуть менее, чем полностью, так как не переживает destroy активности
Согласно официальной документации:
The system never kills an activity directly to free up memory. Instead, it kills the process in which the activity runs, destroying not only the activity but everything else running in the process, as well.
Получается, что не нужно рассматривать вариант со смертью активити и тогда ViewModel становится удобным инструментом для хранения данных/соединений не зависящим от смены конфигурации.
"The system never kills an activity" — означает, что активность не может просто так без предупреждения исчезнуть, у активности обязательно будут вызваны коллбэки onStop()
, onDestroy()
(хотя на практике в некоторых случаях onDestroy()
не вызывается).
Активность, если не находится внутри [onStart(), onStop()]
, может быть в любой момент уничтожена системой. В этом случае, если это не configuration change, данные внутри androidx.lifecycle.ViewModel
будет потеряны.
Идея с MVVM хорошая, но для гугловой реализации ViewModel
сложно найти применение. Поэтому меня удивляет поднятый вокруг этого класса хайп, куча примеров в сети. Вспоминается песня Тараканов "Кто-то из нас двоих" )
Активность, если не находится внутри [onStart(), onStop()], может быть в любой момент уничтожена системой. В этом случае, если это не configuration change, данные внутри androidx.lifecycle.ViewModel будет потеряны.
Утверждаете ли вы, что процесс при этом (при смерти активити) останется жить?
Да, конечно.
Чтобы это сэмулировать, в Developer Options есть опция "Don't keep activities".
Есть ли у вас прямые доказательства обратного?
Начиная с 4.0 (по-моему), Android никогда не убивает Activity по одиночку, только вместе с процессом целиком.
Последний раз обратное видел на Android 2.3.
Можете провести хоть миллион экспериментов, вы никогда не добъетесь такой ситуации даже на дохлых телефонах/эмуляторах.
Edit: в качестве теста можете сделать Activity, которая открывает такую же, и так стотыщ раз. На 2.3 старые Activity начнут умирать начиная с какого-то момента, а на 4.0+ вы просто поймаете OOM.
Похоже, что всё так и есть, большое спасибо за комментарий! Добавил обновление в статью.
Начиная с 4.0 (по-моему), Android никогда не убивает Activity по одиночку, только вместе с процессом целиком.
Ну так сделайте простой тест
1. Включите флаг,
2. запустите приложение на 8ке, например,
4. поставьте break на onDestroy, и
5. попереключайтесь между другими запущенными приложениями.
Выпадете в onDestroy, а приложение, вот сюрприз-то, останется в памяти
Да, всё верно. Но как выяснилось, без включённой опции "Don't keep activities" воспроизвести аналогичный сценарий (чтобы вызвался onDestroy()
) не удастся.
Так что вполне воспроизводится, по крайней мере клиентами. Но у нас и приложение тяжелое в плане использования памяти, так что может поэтому.
Ну и к вопросу о статье — я, имхо, не очень понимаю, зачем хранить состояние приложения, если прибился сам процесс. Разве что какие-то уже введенные и сохраненные данные (сохраненные по ходу, или по кнопке save, грубо говоря)
В этом случае ни о каких parcelable и речи не идет, это в базу/облако/етц
Смерть процесса и потом восстановление на том же скрине — это самый обыденный сценарий, который происходит много раз на день. И это нужно поддерживать обязательно, чтобы обеспечить хороший UX. Это так же источник кучи крашей и багов, если это не тестировать.
Что вероятнее всего у вас и происходит, а не смерть отдельной Activity.
У меня есть баг в трекере. Запускаем отладку, с включенным флагом. Делаем, как я описал выше. Активити уходит в onDestroy, процесс остается в памяти, в отладчике это прекрасно видно. Какие логи я неправильно интерпретирую?
Кроме того, если процесс прибивается, где хранится savedInstance?
Saved instance хранится в системном сервисе Activity Manager, который отвечает за создание и восстановление Activity.
То что вы можете воспроизвести это с флагом, не значит, что без флага в реальной ситуации убивается только активити. И я могу поспорить, что убивается весь процесс. И вы можете это воспроизвести кучей других способов, вроде kill через adb, лимита фоновых процессов в дев сеттингах и банальным запуском других тяжелых приложений
Я утверждаю простую вещь — система может прибить активити без убиения процесса. Собственно, об этом в activity lifecycle написано, секция onDestroy:
developer.android.com/guide/components/activities/activity-lifecycle#ondestroy
И для этих случаях ноленс воленс нужно сохранять состояние.
Я не выдумываю эти проблемы, я, повторюсь, смотрю на баги, которые идут от клиентов. При помощи флага они отлично отлаживаются. Т.е., у клиентов возникают ситуации, когда активити уходит в destroy, и потом восстанавливается в том же процессе из saved instance state.
Подозреваю, подобное можно получить, например, создав активити с 2-мя лейаутами, для портрета и ландскейпа, и потом просто поворачивать телефон. Сам не пробовал, да:)
UPD. Ага, почитал. Действительно, система не убивает отдельные активитиз из-за недостатка памяти. Спасибо за наводку
Тем не менее, активити таки может быть уничтожена, и потом восстановлена, так что, вуходит, от savedInstance все равно никуда не деться
вполне воспроизводится
Если найдёте способ воспроизвести, это может оказаться очень интересным, иметь мировую значимость.
ответил не в тред
Рестар интерфейс при перезапуске процесса считаю халтурой и неуважением к пользователям.
Долго убивался почему-же андроид такой. Потом понял, что задача решаемая разработчиками — сохранить приложение в том виде, в каком оно было даже после убиения от нехватки памяти.
И если попробовать написать это самому с нуля, то получатся activity и fragment. И в последних обновлениях с Jetpack их как раз довели до ума.
Особенно если пользователь ввел какие-то данные и они вдруг пропали.
Согласен, если речь идёт о небольшом личном проекте ради развлечения, самообразования, портфолио.
Было бы интересно обсудить выбор наёмного архитектора/девлида, когда перед ним стоит задача разработать основу крупного коммерческого приложения. И выбор небольшой команды ИПшников, начинающей разработку очередной игры с целью заработать денег. В этих случаях рассуждают другими категориями: выгоды, целесообразности.
В большинстве приложений имеет смысл принять явное осознанное архитектурное решение, что в случае смерти процесса приложение просто перезапускается с нуля, не пытаясь восстанавливать состояниеЕсть причины отказа от сохранения состояния, которые не зависят от самого разработчика. Они могут быть разными- использование сторонних кроссплатформенных движков, криво выстроенная логика сервиса\бэкенда\авторизации, прямое указание начальства\заказчика, куча легаси и прочее. Таких приложений совсем не большинство.
В ином случае, если решение было именно архитектурное (то есть архитектура является ограничителем для сохранения состояния)- то использованная Вами архитектура (или ее реализация) не удовлетворяет требованиям современных приложений. А это- Ваша личная проблема как разработчика.
Меняйте архитектуру или ее реализацию если Вам за это готовы платить. А лучше сразу писать с сохранением состояния- в большинстве случаев нет серьезных причин, которые этому мешают, да и современные инструменты позволяют этот момент сильно упростить.
Другой момент- насколько подробно сохранять состояние. Сегодня, как минимум, открыть тот же экран с теми же входными данными после перезапуска не составляет проблем.
Раньше можно было в любом месте воткнуть статическое поле и хранить в нём состояниеЭто когда? Признаюсь честно- я мало писал под версии старше 2. Но уже 2.1 выкидывал процессы из памяти по причине нехватки, что, в последствии, приводило к перезапуску с, возможно, не пустым SavedInstanceState.
Такой подход имеет большой потенциал. Можно красиво обернуть и использовать во многих местах. Например, для передачи произвольных аргументов во фрагменты.Простите, нет. Раз Вы в примерах используете Navigation component (или что-то очень на него похожее)- специально для этого придумали Safe Args. Это дополнение генерирует классы для аргументов фрагмента, которые, в свою очередь, сохраняются при смене конфигурации и перезапуске. Не факт, что Вам придется делать для этого много кастомных Parcelable\Serializable (в этом, кстати, тоже нет проблемы- студия для большинства типов генерирует реализацию Parcelable нажатием одной кнопки).
Такой набор поможет при перезапуске сохранить стек фрагментов и показать тот же экран с теми же вводными. В большинстве случаев этого достаточно. При этом использовать его не менее комфортно.
Если Вам нужно сохранять более подробное состояние и Вы эти данные обрабатываете во ViewModel- для Вас есть SavedStateHandle и AbstractSavedStateViewModelFactory.
использованная Вами архитектура (или ее реализация) не удовлетворяет требованиям современных приложений
Спасибо за идею. Добавил в статью:
Для интереса проверил несколько приложений. На 02.03.2020 прозрачно обрабатывают смерть процесса: Facebook, Twitter, WhatsApp, Chrome, Gmail, Yandex.Maps. Перезапускают приложение: Yandex.Navigator, YouTube, мобильный банкинг от Сбербанка и от Альфа-Банка, Skype.
но в большинстве приложений это нецелесообразноЭто всегда и везде целесообразно. Если, конечно, Ваше приложение не состоит из одного экрана или вообще не имеет основного UI. Ну или если есть жесткие требования к безопасности.
Тот же навигатор- будет неприятно если ОС, по каким-либо веским причинам, решит выкинуть его из памяти. Например, во время достаточно долгого разговора.
YouTube проверил- перезапускает с сохранением, показал то же видео. Возможно, есть разница в версии ОС и приложений.
На моем наборе приложений те, что перезапускаются- в меньшинстве. И это всегда неприятно- на превью мультитаска видишь один контент, а после выбора- всё сначала.
Да, таких приложений просто много. Но это точно не норма и практиковать такой подход без веских причин не стоит.
YouTube проверил- перезапускает с сохранением
Откройте, например, экран "History" (история просмотров), сверните и убейте процесс:
adb shell am kill com.google.android.youtube
adb shell "ps -A | grep youtube"
Открывается главный экран, а не "History".
Это когда?
Возможно, не очень удачно выразился, поправил в статье. "Раньше" — это в случае, если не восстанавливаем состояние после рестарта процесса.
ViewModel или аргументы\SavedInstanceState эти проблемы решают.
специально для этого придумали Safe Args
Если гуглокодеры предлагают инструмент для решения некой задачи, то это не значит, что необходимо использовать именно этот инструмент. Особенно учитывая низкое качество создаваемых инструментов и вообще принимаемых решений (что касается Андроида).
У меня не было возможности плотно поработать с Jetpack Navigation, поэтому не претендую на авторитетное мнение. Идея соединять фрагменты стрелочками интересная, но боюсь, что в реальных проектах от <action>
-ов мало проку из-за, как часто бывает в Андроиде, недоделанности и недопродуманности, в частности из-за ограниченности androidx.navigation.NavGraph
. В итоге всё приходит к несвязанным <fragment>
.
В Safe Args смысла не вижу. Возьмём пример из документации (https://developer.android.com/guide/navigation/navigation-pass-data).
<fragment
android:id="@+id/bbbFragment"
. . . >
<argument
android:name="myArg"
app:argType="integer"/>
</fragment>
findNavController(R.id.nav_host_fragment).navigate(
R.id.bbbFragment, BbbFragmentArgs(17).toBundle()
)
Вместо возни с XML и получения нетипизированного кода (ничто не мешает вместо BbbFragmentArgs
передать СссFragmentArgs
) лучше в BbbFragment
добавить
fun NavController.navigateToBbb(myArg: Int) {
this.navigate(R.id.bbbFragment, myArg.bundle)
}
И вызывать
findNavController(R.id.nav_host_fragment).navigate(17)
Аналогично с <action>
. Вместо
<fragment ...>
<action android:id="@+id/startBbbFragment"
app:destination="@+id/bbbFragment"
. . . >
<argument
android:name="myArg"
app:argType="integer"/>
</action>
</fragment>
findNavController(R.id.nav_host_fragment).navigate(
AaaFragmentDirections.startBbbFragment(17)
)
делаем
fun NavController.navigateToBbb(myArg: Int) {
this.navigate(AaaFragmentDirections.startBbbFragment().actionId, myArg.bundle)
}
findNavController(R.id.nav_host_fragment).navigate(17)
студия для большинства типов генерирует реализацию Parcelable
По поводу потенциала ) Обнаружил в закромах интерфейс, который делает любой класс Parcelable
-ом.
/**
* Add this marker interface to make any class parcelable.
*/
interface ReferenceParcelable : Parcelable {
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeStrongBinder(BinderReference(this))
}
override fun describeContents(): Int = 0
companion object {
@Suppress("unused")
@JvmField
val CREATOR = object : Parcelable.Creator<Any> {
override fun createFromParcel(parcel: Parcel): Any? {
return (parcel.readStrongBinder() as BinderReference<*>).value
}
override fun newArray(size: Int): Array<Any?> {
return arrayOfNulls(size)
}
}
}
}
Если гуглокодеры предлагают инструмент для решения некой задачи, то это не значит, что необходимо использовать именно этот инструмент. Особенно учитывая низкое качество создаваемых инструментов и вообще принимаемых решений (что касается Андроида).Верно, не обязательно. Но есть рекомендуемые подходы. Это означает, что предлагаемый инструмент прошел UX-тестирование и имеет высокую степень совместимости с самим Android framework и сопутствующими инструментами.
Пример- Coroutines.
По поводу качества не согласен. Ранее была такая проблема, но новые инструменты радуют (если, конечно, Вы ознакомились с практиками их использования). Да, баги есть. Но их в среднем не больше, чем у других известных инструментов. Просто внимания больше, от того находят быстрее.
В итоге всё приходит к несвязанным fragmentЭто говорит о непродуманности UX Вашего приложения. И об отношении к пользователям…
По опыту скажу, что при рефакторинге даже весьма хаотичных b2b-проектов, они, по итогу, прекрасно укладывались в навигационную диаграмму.
В Safe Args смысла не вижуА он есть. Вы показали сторону вызова, а посмотрите на принимающую сторону:
class AaaFragment : Fragment() {
private val args: AaaFragmentArgs by navArgs()
}
AaaFragmentArgs- это сгенерированный класс. В нем свойства, которые Вы укажете в navigation.xml (или в граф.редакторе). Не надо возиться с ключами и есть гарантия того, что Вы передадите в фрагмент то, что он ожидает. AaaFragmentArgs будет восстанавливаться при смене конфигурации и перезапуске. Минимальный набор для восстановления экрана с вводными.В отдельных случаях Вам даже экшены использовать не обязательно.
как часто бывает в Андроиде, недоделанности и недопродуманностиПредполагаю, что Вы просто мало работали с другими ОС и фреймворками. Там разных недопониманий тоже хватает. Но, что в Android, что в других ОС, такие проблемы чаще возникают от незнания good practices.
Вы показали сторону вызова, а посмотрите на принимающую сторону
На принимающей стороне тоже выглядит красивее, если не используем Safe Args: вместо ненужной обёртки сразу инжектим переданный аргумент. Пример с defaultArg()
есть в статье.
class AaaFragment : Fragment() {
private val myArg: Int by defaultArg()
}
С точки зрения проектирования- сгенерированный args-класс инкапсулирует в себе набор параметров различных типов. Если (а точнее- когда) этот набор будет меняться со временем- содержать его внутри одного класса-обертки будет выгоднее.
Зачастую отдельная обёртка не нужна, имеются естественные, готовые классы для передачи. Например, из AccountsFragment
в AccountFragment
можно передать класс Account
, который у нас уже живёт во ViewModel.
Но если понадобилась обёртка, то всё равно вместо
<fragment . . . >
<argument
android:name="fieldA"
app:argType="integer" />
<argument
android:name="fieldB"
app:argType="string" />
<argument
android:name="fieldC"
app:argType="boolean" />
</fragment>
лучше писать
class BbbFragmentArgs(val fieldA: Int, val fieldB: String, val fieldC: Boolean)
Kotlin DSL лаконичнее, и такой подход естественнее, проще, более type safe.
В принципе я не против кодогенерации, скорее за. Но конкретно в Navigation Safe Args она не имеет смысла. Возможно, когда-нибудь с развитием функционала Navigation смысл появится.
В случае Safe Args наличие обертки за один аргумент- небольшая плата за расширяемость и вариативность. И с type safe там тоже всё в порядке. Даже поддержка Null Safety есть.
новые инструменты радуют
Недавний пример из реальной жизни. Есть экран OpportunitiesFragment
, представляющий собой список. Кликаем по элементу — проваливаемся в экран конкретного "opportunity": AddMobilePhoneFragment
, SetupMedicineCabinetFragment
, PrescriptionSavingsFragment
и т. п.
Выносим это в отдельный граф, всё чудесно:
<navigation
app:startDestination="@id/opportunities_fragment"
. . . >
<fragment
android:id="@+id/opportunities_fragment"
. . . />
<fragment
android:id="@+id/add_mobile_phone_fragment"
. . . />
<fragment
android:id="@+id/setup_medicine_cabinet_fragment"
. . . />
<fragment
android:id="@+id/prescription_savings_fragment"
. . . />
. . .
</navigation>
Затем возникает бизнес-требование попадать на некоторые opportunity сразу из главного экрана, минуя opportunities_fragment
. Подскажите способ, как это можно красиво сделать, и я перестану ругаться на Андроид )
Делать наследника от NavDestination
, который, в отличие от androidx.navigation.NavGraph
, умеет роутить не только на фиксированный (app:startDestination
) фрагмент, а более гибко, или вводить для app:startDestination
искусственный "routing" фрагмент — не предлагать: это работает, но не характеризует Андроид с хорошей стороны.
OpportunitiesFragment
не должен быть в стеке, при нажатии на Back должны возвращаться обратно на главный экран, откуда перешли на opportunity. Но это не принципиально, можно удалять OpportunitiesFragment
из стека вручную.
Но это вариация на тему искусственного routing фрагмента, который не предлагать ) OpportunitiesFragment
не должен заниматься маршрутизацией, это вообще задача не View, а ViewModel.
В первой приведённой вами ссылке (на Stack Overflow) предлагается недоделанная реализация варианта с наследованием от NavDestination
.
Вторая ссылка не имеет отношения к обсуждению. Вопрос в том, как сгруппировать фрагменты, относящиеся к opportunity, но при этом иметь возможность открывать не только startDestination. Конечно, можно убрать группировку, вынести все фрагменты в корень, получится куча разрозненных фрагментов, о чём я писал выше:
В итоге всё приходит к несвязанным <fragment>
OpportunitiesFragment не должен быть в стеке, при нажатии на Back должны возвращаться обратно на главный экранТогда сделайте главный экран частью графа и переходите оттуда- для Вас есть Nested navigation graphs.
OpportunitiesFragment не должен заниматься маршрутизацией, это вообще задача не View, а ViewModelЯ это и мел в виду- решение о переходе принимает ViewModel, но саму команду отдает View-слой. Navigation component предполагает использование view-driven архитектуры. Собственно, как и большинство рекомендуемых реализаций.
Как видите, решения есть. Я не вижу исходников Вашего проекта (да и не хочу тратить на это время), чтобы порекомендовать наиболее подходящее. Нужно уметь находить и правильно применять решения, а не сводить к несвязанному хаосу.
Тогда сделайте главный экран частью графа и переходите оттуда- для Вас есть Nested navigation graphs.
Конечно, мы и обсуждаем nested navigation graphs. Главный экран никак не может быть частью opportunity графа.
Как видите, решения есть.
Почему-то вспомнился Винни-Пух: "У меня правильнописание хромает. Оно хорошее, но почему-то хромает." )
не хочу тратить на это время
Спасибо вам большое за обсуждение! Надеюсь, оно окажется полезным техническому сообществу.
Оно хорошее, но почему-то хромаетНу это, на мой взгляд, точно лучше хаоса в навигации.
Спасибо вам большое за обсуждение!Имелось в виду, что я не хочу тратить время на просмотр исходников Ваших проектов. А вводных данных, что Вы описали, возможно, недостаточно для подбора наиболее подходящего решения.
Как-то не конструктивно с Вашей стороны.
Ой, наверное, меня можно было неправильно понять.
Ранее была такая проблема, но новые инструменты радуют
Под "хромающим правильнописанием" имею ввиду, что, наверное, Андроид — и Jetpack в частности — хорошие. Но я навскидку привёл реальную проблему, типичный юзкейс с навигацией opportunity, для которой хорошего решения не существует. Как оно семь лет назад хромало, так и продолжает хромать. Конечно, наука развивается, работать становится интереснее и приятнее. Но количество дурацких решений в архитектуре Андроида по-прежнему велико.
Моё мнение, что-то у них там в Андроидном подразделении Гугла фундаментально сломано. Как в старом анекдоте: "Всю систему надо менять!" Пока команду не сменят, так и будут лажать. Видимо, не справляются текущие архитекторы.
Но я навскидку привёл реальную проблему, типичный юзкейс с навигацией opportunity, для которой хорошего решения не существуетПоказывать что-либо на старте экрана — нормальная практика. В том числе и другой экран через directions. Можно использовать одни и те же фрагменты в разных графах, что увеличивает систематизацию. Почему данные подходы Вам не нравятся — для меня загадка.
Но количество дурацких решений в архитектуре Андроида по-прежнему велико.Любому решению есть причина. Например, многие ругают Android за пересоздание активити при повороте и прочих сменах конфигурации. В то же время это — хорошая возможность смены лейаута и его поведения для конкретных условий. Вспомните тот же master-detail.
Моё мнение, что-то у них там в Андроидном подразделении Гугла фундаментально сломано.Я часто присутствовал на презентациях о Jetpack и других решениях. Так же, на них присутствовали iOS-разработчики с большим опытом. В последний год от них всё чаще слышу возгласы вроде «почему у нас так нельзя» или «почему у нас такого нет». На мой взгляд, это о многом говорит.
Да, Android не идеален. Но не более, чем что-либо еще.
В то же время Вы ругаете Android и его практики и предлагаете не целевое использование Binder — технически опасное решение, которое снижает качество приложения на выходе.
Мне кажется, что проблема тут совсем не в Android…
По поводу потенциала ) Обнаружил в закромах интерфейс, который делает любой класс Parcelable-ом.Вы же понимаете, что такая реализация не переживет перезапуск?.. А, при неумелом использовании, еще и спровоцирует утечки.
такая реализация не переживет перезапуск
Да, не переживёт. Речь и идёт о возможностях, открывающихся, если отказываемся от восстановления состояния после рестарта процесса.
Про утечки не очень понял, вроде бы ничего не утекает.
о возможностях, открывающихся, если отказываемся от восстановления состояния после рестарта процессаUX важнее проблем разработки. За некоторыми исключениями. Нужно решать проблемы пользователей, а не собственные.
вроде бы ничего не утекаетВы так можете случайно передать в долгоживущую часть приложения (или даже часть системы) сильную ссылку на объект с коротким жизненным циклом. Может даже и не один и не маленький в плане потребления памяти. Вот Вам и утечка.
передать в долгоживущую часть приложения (или даже часть системы) сильную ссылку на объект
Это да, поначалу реально возникает такая проблема, особенно если пытаться передавать всякие Runnable
, коллбэки (неявно держащие, например, this
на активность или фрагмент), таски, запущенные потоки. With great power comes great responsibility )
Но со временем народ осваивается, вырабатываются паттерны, и проблема теряет актуальность.
При этом Вы лишаете пользователя довольно ценного функционала- восстановления состояния.
Это механизм для удаленного вызова процедур, о чем написано в документации. Он не предназначен для хранения и передачи данных. Там же есть напоминание о смерти процесса.
Я согласен, что использование Binder выглядит кривовато, искусственно. Но в принципе Binder используется вполне по назначению. Из API к IBinder:
These semantics allow IBinder/Binder objects to be used as a unique identity (to serve as a token or for other purposes) that can be managed across processes.
То, что не передаю Binder в другие процессы — моё дело: хочу — передаю, не хочу — непередаю ) Если вас смущает наследование от Binder, то в этом не вижу ничего плохого, но можно и не наследоваться, а использовать java.util.WeakHashMap
.
А она заключается в том, что Binder и IBinder — части одного и того же RPC-механизма. А значит порождают строгую ссылочную зависимость.
Не надо путать RPC с remote storage.
Позволю себе немного критики по структуре статьи. Ожидаемая структура:
- Описание задачи/проблемы
- Варианты решения: 1 и 2.
- Особенности, плюсы и минусы варианта 1
- Особенности, плюсы и минусы варианта 2
- Выводы, выбор варианта.
У вас же пункты 4 и 5 переставлены местами. Из-за этого нарушается логика выводов и не совсем понятно, почему выбор сделан в пользу варианта 2. Тем более, что далее выясняется, что с ним есть проблема и "красивого и универсального решения проблемы я не знаю".
В большинстве случаев сериализация в Андроиде не нужна