В комментариях в прошлом посту меня попросили прикладывать участки кода в виде картинок, так как на телефоне они читаются максимально всрато. К сожалению телеграм в канале не позволяет вставлять картинки в середину поста, и в целом это правильно потому что канал в телеграмме это не медиум и не про это.
Поэтому я решил попробовать небольшой эксперимент. Я попробую делать посты с большими участками кода в телеграфе, посты без кода буду выпускать как обычно.
Поэтому ловите, мини статейку про то, как правильно передавать данные во фрагмент.
Поэтому я решил попробовать небольшой эксперимент. Я попробую делать посты с большими участками кода в телеграфе, посты без кода буду выпускать как обычно.
Поэтому ловите, мини статейку про то, как правильно передавать данные во фрагмент.
Telegraph
Как правильно передать данные в фрагмент?
Самый популярный вопрос на собеседовании про фрагменты причем спрашивают как у джунов, так и у сеньоров. Давайте разбираться как? Допустим нам нужно передать id во фрагмент. Нужно понимать принципиальную разницу м/у Activity и фрагментом. Activity мы не создаем…
👍30
Вспомним что такое вообще транзакция. Само по себе понятие транзакции это некоторый набор действий который либо выполняется полностью, либо не выполняется. Это свойство назвается атомарность.
Фрагменты мы добавляем/удаляем/меняем через транзакции. В одной транзакции можем заменить текущий фрагмент и одновременно показать поверх него новый. Все эти действия произойдут одновременно. Как и полагается транзакции эта цепочка действий либо выполняется полностью, либо мы упадем. Это отличается от работы с Activity, где это было бы три отдельных действия.
Далее вы могли заметить что есть два метода чтобы закоммитить транзакцию:
Чтобы это понять есть простая аналогия. Когда вызываем просто
На практике крайне редко возникает потребность показать фрагмент без очереди через
Также есть такой метод как
Фрагменты мы добавляем/удаляем/меняем через транзакции. В одной транзакции можем заменить текущий фрагмент и одновременно показать поверх него новый. Все эти действия произойдут одновременно. Как и полагается транзакции эта цепочка действий либо выполняется полностью, либо мы упадем. Это отличается от работы с Activity, где это было бы три отдельных действия.
Далее вы могли заметить что есть два метода чтобы закоммитить транзакцию:
commit
и commitNow
. В чем разница? В том, что когда делаем commit, мы эту транзакцию отправляем в Looper главного потока, а когда делаем commitNow транзакция происходит вот прямо сейчас синхронно. Чтобы это понять есть простая аналогия. Когда вызываем просто
commit
, транзакция встает в очередь. Когда вызываем commitNow
эта транзакция превращается в чела который: "Мне просто спросить".На практике крайне редко возникает потребность показать фрагмент без очереди через
commitNow
. В большинстве случаев лучше просто не парится и показывать фрагменты через commit
. С commitNow
есть еще одно ограничение о котором поговорим в следующих постах.Также есть такой метод как
executePendingTransaction
, который просто выполняет все транзакции посланные в Looper
. Метод блокирующий, это значит, что при его вызове, все транзакции которые были посланы в Looper
и находятся в очереди, забьют на эту очередь и выполнятся сейчас. Отсюда выходит, что если вызвать сначала commit
, а потом следующей строкой executePendingTransaction
, то это аналогично простому вызову commitNow
.👍28❤5
Для начала вернемся к транзакциям. Помимо атомарности у транзакций есть еще одно свойство. Грубо говоря это свойство можно назвать откатываемостью. Другими словами, допустим совершается транзакция, в которой есть набор действий меняющие состояние. Откатываемость позволяет полностью откатить эту транзакцию, что значит вернуть состояние системы в изначальное. Зная это свойство идем дальше.
Что такое Back Stack? Это просто стэк в котором лежат сохранённые транзакции, стэк позволяет их откатывать. Чтобы понять, как это работает, рассмотрим как закрываются Activity. Когда нажимаем на кнопку назад то верхняя Activity убивается и мы возвращаемся к предыдущей. Сохранение транзакций в Back Stack позволяет по нажатию на эту кнопку откатывать транзакции.
Работает это так, при нажатии на кнопку назад Activity сначала у Fragment Manager узнает если что-то в Back Stack. Если есть тогда откатывается последняя транзакция добавленная в Back Stack. Это происходит до тех пор, пока в Back Stack не будет пуст. После этого уже закрывается сама Activity.
Давайте на примере. Вы добавили первый фрагмент FirstFragment() в Activity. Затем в эту же Activity мы добавляем еще один фрагмент SecondFragment(). Однако транзакцию по запуску SecondFragment добавляем в Back Stack. Теперь когда нажимаем на кнопку назад, у нас сначала откатывается транзакция SecondFragment() т.е этот фрагмент удаляется, а после второго нажатия уже закроется вся Activity, так как первый фрагмент мы не добавляли в Back Stack.
Чтобы добавить транзакцию в Back Stack нужно использовать метод
Как уже упоминал выше, Activity сама откатывает транзакции по нажатию на кнопку назад. Помимо это можно откатывать транзакции через специальный метод
Важный момент. Нельзя вызывать
Что такое Back Stack? Это просто стэк в котором лежат сохранённые транзакции, стэк позволяет их откатывать. Чтобы понять, как это работает, рассмотрим как закрываются Activity. Когда нажимаем на кнопку назад то верхняя Activity убивается и мы возвращаемся к предыдущей. Сохранение транзакций в Back Stack позволяет по нажатию на эту кнопку откатывать транзакции.
Работает это так, при нажатии на кнопку назад Activity сначала у Fragment Manager узнает если что-то в Back Stack. Если есть тогда откатывается последняя транзакция добавленная в Back Stack. Это происходит до тех пор, пока в Back Stack не будет пуст. После этого уже закрывается сама Activity.
Давайте на примере. Вы добавили первый фрагмент FirstFragment() в Activity. Затем в эту же Activity мы добавляем еще один фрагмент SecondFragment(). Однако транзакцию по запуску SecondFragment добавляем в Back Stack. Теперь когда нажимаем на кнопку назад, у нас сначала откатывается транзакция SecondFragment() т.е этот фрагмент удаляется, а после второго нажатия уже закроется вся Activity, так как первый фрагмент мы не добавляли в Back Stack.
Чтобы добавить транзакцию в Back Stack нужно использовать метод
addToBackStack()
. У функции есть один параметр типа String, это tag транзакции. В большинстве случаев этот tag не используется и просто передается null. Однако можно туда передать строку, чтобы потом можно было откатить все транзакции до транзакции с этим tag. Причем нужно держать в голове, что отказывается вся транзакция. Это значит что если вы в одной транзакции меняете какой-то фрагмент и одновременно показываете что-то поверх, при откате этой транзакции верхний фрагмент удалится, а замена фрагментов произойдет в обратном порядке.Как уже упоминал выше, Activity сама откатывает транзакции по нажатию на кнопку назад. Помимо это можно откатывать транзакции через специальный метод
popBackStack()
. Есть два варианта этого метода обычный без параметров и второй с параметром типа String это тот tag транзакции, чтобы откатить все транзакции до этой.Важный момент. Нельзя вызывать
commitNow()
с addToBackStack()
, потому как транзакция сразу же упадет. Сделано это из-за очевидной проблемы. Когда вызываем commitNow()
, эта транзакция минует очередь, а в очереди могут быть транзакции которые меняют Back Stack. Разруливать такое довольно сложно, поэтому очевидное решение просто падать в таком случае.👍32❤3🔥1
В предыдущих постах я упоминал что ЖЦ фрагмента полностью завязан на ЖЦ Activity. Однако интересные вещи происходят на стыке этих двух сущностей. Один из интересных вопросов который может возникнуть – что произойдет если закоммитить транзакцию фрагмента в тот момент когда у Activity уже был вызван onStop?
Fragment Manager умеет сохранять состояние фрагментов и делает он это в тот момент, когда Activity сохраняет свое состояние. Но каким образом сохранять состояние когда методы сохранения состояния Activity уже вызваны?
Для этого существует такая ошибка как State Loss Exception которая позволяет понять, что мы закомитили транзакцию после вызова
Однако это поведение можно обойти, и для этого созданы методы
Когда пользователь сворачивает приложение и возвращается назад, он ожидает увидеть экран на котором был. При использовании
Поэтому лучше всего никогда не использовать
Fragment Manager умеет сохранять состояние фрагментов и делает он это в тот момент, когда Activity сохраняет свое состояние. Но каким образом сохранять состояние когда методы сохранения состояния Activity уже вызваны?
Для этого существует такая ошибка как State Loss Exception которая позволяет понять, что мы закомитили транзакцию после вызова
onSaveInstanceState
у Activity. Такая ситуация может возникнуть, когда вы показываете результирующий экран после какого-нибудь запроса к серверу. Однако это поведение можно обойти, и для этого созданы методы
commitAllowingStateLoss()
и commitNowAllowingStateLoss()
. Они позволяют закоммитить транзакцию фрагмента в тот момент, когда Activity уже сохранила свое состояние. Однако в этом случае нет гарантии того, что эта транзакция вообще выполнится. Когда пользователь сворачивает приложение и возвращается назад, он ожидает увидеть экран на котором был. При использовании
commitAllowingStateLoss()
мы пытаемся показать новый экран когда пользователь уже не в приложении. Во-первых, это плохой UX, во-вторых, это лотерея, потому как я уже сказал выше гарантий никаких нет. Поэтому лучше всего никогда не использовать
commitAllowingStateLoss()
, есть куча инструментов тот же Cicerone который позволяет навигироваться между экранами, даже если пользователь свернул приложение. commitAllowingStateLoss()
лучше использовать только для экранов, который пользователю не критично пропустить вроде какого-нибудь банера и только в том случае когда вы уверены, что другого выбора нет.👍32❤2
Вот это по мне так самый интересный пост за всю эту серию.❗Есть одна ошибка которую совершают все при работе с фрагментами. Причем, под все я не сильно утрирую, за всю мою карьеру был только один проект, в котором про эту ошибку не забыли.
Начнем с простого, когда создаем фрагмент мы переопределяем метод
Раньше в стародавние времена, не было такой штуки как ViewModel, которая умела переживать смерть Activity. Большие объекты сохраняли либо через статику, которая несет свои сложности, либо через retain фрагмент.
У фрагментов есть такая настройка
У View всегда есть ссылка на Activity (сейчас не рассматриваем кейс с виджетами), и соответственно у Activity всегда есть ссылка на View которую мы видим на экране. Когда умирает Activity, умирает и View которая к ней привязана. Фрагмент как мы помним это в некотором смысле сложная View. Если в retain фрагменте сохранить ссылку на View в поле этого фрагмента, то при перевороте у нас будет жить ссылка на старую Activity которая умерла:
Из-за такой возможности легко сделать утечку памяти, а также из-за смешивания View и презентационной логики были сделаны ViewModel. Сейчас настройка retainInstance deprecated и очень не рекомендуется к использованию. retainInstance ушел в прошлое, правда иногда спрашивают на собесах.
Однако это еще не все интересные кейсы где может быть фрагмент без View.
Неочевидно поведение проявляется при транзакции с заменой фрагмента. Начнем с примера, у вас есть FirstFragment в Activity, затем мы делаем транзакцию с заменой фрагмента FirstFragment на SecondFragment. Что при этом происходит с FirstFragment?
Вот тут интересная часть, у него удаляется View, однако сам фрагмент какое-то время продолжает жить. Это сделано для оптимизации. Если нажмем на кнопку назад, то нам нужно будет создать заново фрагмент FirstFragment, а это накладные расходы т.к рефлексия и вот это все. Теперь вдумайтесь, обычный фрагмент, может в некоторых кейсах вести себя очень похоже на retain фрагмент. Да разумеется, он уничтожится при перевороте и тогда уже нет проблем. Однако не факт что этот переворот случится. Это именно тот момент когда можно подорваться и вот каким образом.
Самая распространённая ошибка это сохранение Adapter во фрагменте. Когда вы используете RecycleView вы обязаны создать Adapter чтобы с ним работать. Часто этот Adapter сохраняют в поле Fragment, потому как пересоздавать его каждый раз когда обновляются данные такая себе затея и это правильно. Однако в таком случае важно в
Интересный факт о котором не пишет Гугл – Adapter держит ссылку на RecyclerView. И по факту мы можем оказаться в ситуации когда у нас фрагмент, который ведет себя как retain фрагмент, да еще и с ссылкой на View. Он конечно удалится со временем, однако не понятно когда, да и удалится ли вообще, до того момента, как умрет Activity.
Поэтому просто возьмите за правило – не сохраняйте ссылки на View в поля фрагментов, или очищайте их в
Начнем с простого, когда создаем фрагмент мы переопределяем метод
onCreateView
. В этом методе можно просто вернуть null. Это самый простой кейс как может быть Fragment без View. В этом случае немного изменяется ЖЦ фрагмента. В частности не вызывается метод onViewCreated
, потому как View не создалась. Может возникнуть вопрос, а зачем это нужно? Для этого окунемся в историю.Раньше в стародавние времена, не было такой штуки как ViewModel, которая умела переживать смерть Activity. Большие объекты сохраняли либо через статику, которая несет свои сложности, либо через retain фрагмент.
У фрагментов есть такая настройка
setRetainInstance
, которая позволяет указать нужно ли пересоздавать фрагмент при смерти Activity. Если выставляем retainInstance = true,
то при пересоздании Activity фрагмент умирать не будет. Это позволяло использовать такие фрагменты, как сейчас, используются ViewModel. Однако есть важное ограничение. Нельзя делать такие фрагменты с View это может привести к утечкам памяти. У View всегда есть ссылка на Activity (сейчас не рассматриваем кейс с виджетами), и соответственно у Activity всегда есть ссылка на View которую мы видим на экране. Когда умирает Activity, умирает и View которая к ней привязана. Фрагмент как мы помним это в некотором смысле сложная View. Если в retain фрагменте сохранить ссылку на View в поле этого фрагмента, то при перевороте у нас будет жить ссылка на старую Activity которая умерла:
Fragment -> View -> Activity
.Из-за такой возможности легко сделать утечку памяти, а также из-за смешивания View и презентационной логики были сделаны ViewModel. Сейчас настройка retainInstance deprecated и очень не рекомендуется к использованию. retainInstance ушел в прошлое, правда иногда спрашивают на собесах.
Однако это еще не все интересные кейсы где может быть фрагмент без View.
Неочевидно поведение проявляется при транзакции с заменой фрагмента. Начнем с примера, у вас есть FirstFragment в Activity, затем мы делаем транзакцию с заменой фрагмента FirstFragment на SecondFragment. Что при этом происходит с FirstFragment?
Вот тут интересная часть, у него удаляется View, однако сам фрагмент какое-то время продолжает жить. Это сделано для оптимизации. Если нажмем на кнопку назад, то нам нужно будет создать заново фрагмент FirstFragment, а это накладные расходы т.к рефлексия и вот это все. Теперь вдумайтесь, обычный фрагмент, может в некоторых кейсах вести себя очень похоже на retain фрагмент. Да разумеется, он уничтожится при перевороте и тогда уже нет проблем. Однако не факт что этот переворот случится. Это именно тот момент когда можно подорваться и вот каким образом.
Самая распространённая ошибка это сохранение Adapter во фрагменте. Когда вы используете RecycleView вы обязаны создать Adapter чтобы с ним работать. Часто этот Adapter сохраняют в поле Fragment, потому как пересоздавать его каждый раз когда обновляются данные такая себе затея и это правильно. Однако в таком случае важно в
onDestroyView
зачищать ссылку на этот Adapter у RecyclerView:
recycler.adapter = null
Интересный факт о котором не пишет Гугл – Adapter держит ссылку на RecyclerView. И по факту мы можем оказаться в ситуации когда у нас фрагмент, который ведет себя как retain фрагмент, да еще и с ссылкой на View. Он конечно удалится со временем, однако не понятно когда, да и удалится ли вообще, до того момента, как умрет Activity.
Поэтому просто возьмите за правило – не сохраняйте ссылки на View в поля фрагментов, или очищайте их в
onDestroyView
, что делает ViewBindingPropertyDelegate. Помимо это затирайте ссылку на Adapter у RecyclerView.🔥39👍18🤔3❤2
Что не так с Fragment Factory?
В комментах меня попросили рассказать почему я считаю FragmentFactory не очень хорошим решением для фрагментов. Я не буду тут приводить описание того, что это и как работает. Есть вот такая статья которая достаточно просто и понятно описывает эту технологии и основные кейсы.
Начну с мысли о том, что не бывает плохой технологии. Оценивать технологии стоит исходя из их списка плюсов и минусов, а также проблем которые она решает. Выбор технологии это всегда trade off, мы всегда чем-то жертвуем.
Какую проблему решает FragmentFactory? Единственная проблема которую она решает это внедрение зависимостей во фрагмент через конструктор. Для зависимостей все проекты используют какую-либо DI библиотеку. Вне зависимости от того, существующее это решение для DI или самописное у нас во фрагменте будет завязка на этот DI. Если это Dagger, то будет аннотация inject, если это Koin, то специальные extension функции и т.д. FragmentFactory позволяет внедрять зависимости через конструктор, что означает что сам фрагмент ничего не будет знать про DI.
Это безусловно плюс этого решения. Один из кейсов когда это может понадобиться это когда вы свой фрагмент предоставляете как либу. И чтобы пользователи вашей либы не тянули зависимости на ваш DI можно использовать FragmentFactory. Это обяжет пользователей вашей либы создавать FragmentFactory, но зато предоставит возможность им выбирать с помощью чего внедрять зависимости. Вот один из вариантов через Dagger.
Из минусов этой библиотеки, проблема с передачей аргументов. Посмотрев внимательнее вы заметите, что динамическая информация вроде id по-прежнему передается через bundle. В этом и заключается неудобство. С обычным созданием фрагмента мы делаем метод newInstance который четко говорит какие данные нужны для показа фрагмента. Это четкий контракт который не позволить создать фрагмент без нужных данных.
С использованием FragmentFactory мы уже не можем создать такой четкий контракт. Потому как мы не создаем фрагмент, мы лишь передаем класс фрагмента в транзакцию, а создается фрагмент уже в FragmentFactory. Это приводит к тому, что у нас размазывается логика передачи аргументов и мы не можем корректно проконтролировать что эти данные вообще передаются.
Помимо этого возникает трудность интеграции с инструментами навигации. Например, тот же Cicerone, обязывает нас передавать именно объект фрагмента, уже с аргументами чтобы его запустить. Еще из минусов вытекает, что если у вас много фрагментов, то FragmentFactory становится единой точкой у которой есть риск разрастись. FragmentFactory также обязана знать про все зависимости, что может добавить геморроя.
Конечно проблемы которые я перечислил решаемы, за исключением аргументов. Однако нужно ли вам такое решение, которое помогает решить одну проблему, но создает целый ворох других. Причем решает она проблему, которую уже давно решили. FragmentFactory прикольное решение, однако для очень узкого круга задач.
В комментах меня попросили рассказать почему я считаю FragmentFactory не очень хорошим решением для фрагментов. Я не буду тут приводить описание того, что это и как работает. Есть вот такая статья которая достаточно просто и понятно описывает эту технологии и основные кейсы.
Начну с мысли о том, что не бывает плохой технологии. Оценивать технологии стоит исходя из их списка плюсов и минусов, а также проблем которые она решает. Выбор технологии это всегда trade off, мы всегда чем-то жертвуем.
Какую проблему решает FragmentFactory? Единственная проблема которую она решает это внедрение зависимостей во фрагмент через конструктор. Для зависимостей все проекты используют какую-либо DI библиотеку. Вне зависимости от того, существующее это решение для DI или самописное у нас во фрагменте будет завязка на этот DI. Если это Dagger, то будет аннотация inject, если это Koin, то специальные extension функции и т.д. FragmentFactory позволяет внедрять зависимости через конструктор, что означает что сам фрагмент ничего не будет знать про DI.
Это безусловно плюс этого решения. Один из кейсов когда это может понадобиться это когда вы свой фрагмент предоставляете как либу. И чтобы пользователи вашей либы не тянули зависимости на ваш DI можно использовать FragmentFactory. Это обяжет пользователей вашей либы создавать FragmentFactory, но зато предоставит возможность им выбирать с помощью чего внедрять зависимости. Вот один из вариантов через Dagger.
Из минусов этой библиотеки, проблема с передачей аргументов. Посмотрев внимательнее вы заметите, что динамическая информация вроде id по-прежнему передается через bundle. В этом и заключается неудобство. С обычным созданием фрагмента мы делаем метод newInstance который четко говорит какие данные нужны для показа фрагмента. Это четкий контракт который не позволить создать фрагмент без нужных данных.
С использованием FragmentFactory мы уже не можем создать такой четкий контракт. Потому как мы не создаем фрагмент, мы лишь передаем класс фрагмента в транзакцию, а создается фрагмент уже в FragmentFactory. Это приводит к тому, что у нас размазывается логика передачи аргументов и мы не можем корректно проконтролировать что эти данные вообще передаются.
Помимо этого возникает трудность интеграции с инструментами навигации. Например, тот же Cicerone, обязывает нас передавать именно объект фрагмента, уже с аргументами чтобы его запустить. Еще из минусов вытекает, что если у вас много фрагментов, то FragmentFactory становится единой точкой у которой есть риск разрастись. FragmentFactory также обязана знать про все зависимости, что может добавить геморроя.
Конечно проблемы которые я перечислил решаемы, за исключением аргументов. Однако нужно ли вам такое решение, которое помогает решить одну проблему, но создает целый ворох других. Причем решает она проблему, которую уже давно решили. FragmentFactory прикольное решение, однако для очень узкого круга задач.
👍25
На данный момент в индустрии есть два варианта как мы работаем с асинхронными задачами. Мы либо используем Rx, либо Coroutines. Разумеется, существуют проекты, где эти технологии не используются, но это либо мелкие либы, либо просто мелкие проекты. На большом проекте без использования библиотеки просто невозможно будет выжить, сложность будет расти экспоненциально.
Что в Rx что в Coroutines есть абстракция, которая позволяет указать на каких потоках нужно выполнять операции. У Rx эта абстракция называется Sсheduler, у Coroutines это Dispatcher. Обе эти абстракции работают практически одинаково, поэтому просто держите в голове что все, что сказано про Sсheduler применимо и к Dispatcher.
Обычно у нас несколько вариантов Sсheduler для разных типов задач. Для похода в сеть или базу данных мы используем
Чтобы понять в чем разница, нужно разобраться как работают Sсheduler. Sсheduler это лишь обертка над Executor. Executor сделали для решения одной интересной задачи. Поток очень тяжелая штука по памяти, примерно 1mb, если каждый раз создавать поток, когда нам нужно сделать асинхронную задачу это может привести к OutOfMemory.
В основе Executor лежит простая идея. Если подумать, то очевидно что нам не нужно каждый раз создавать новый поток на каждую задачу. Достаточно переиспользовать имеющиеся, и просто раздавать им задачи. Чем-то такая система напоминает Looper и MessageQueue. Идем дальше, действительно ли много потоков позволят быстрее выполнять программу? На самом деле нет, после некоторого количества потоков, программа наоборот будет работать медленнее.
У нас есть процессор, в котором несколько ядер. Операционная система имитирует параллельные выполнение программ путем шедулинга. Другими словами, она дает одной программе немного поработать на процессоре, затем все приостанавливает и дает поработать другой программе. Поэтому даже если у процессора одно ядро, можно запускать несколько программ, которые будут выполнятся параллельно, ну точнее сказать "как бы параллельно".
Когда речь идет о потоках работает тот же принцип, система может сделать так, чтобы два потока по очереди работали на одном процессоре, а может запустить их параллельно на разных. Система также переключается между потоками. Операция переключения довольно дорогая, потому как нужно приостановить поток, сохранить его состояние, запустить другой поток, накатить его сохраненное состояние. Это операция называется переключение контекста. Если потоков слишком много, система будет часто переключатся м/у этими потоками и программа будет продвигаться очень медленно.
Вернемся к нашим задачам. Когда мы идем в базу или сеть мы точно будем ждать пока придут данные, как вы понимаете сеть в рамках выполнения программы довольно медленная штука. Поток в этом случае блокируется и просто ждет данные с сокета, ну или с файла есть речь о базе данных. В таком случае если у нас мало потоков, и много запросов в сеть, мы тупо будем долго ждать, т.к запросы будут идти практически последовательно. Поэтому в
Касательно задачи расчета, мы никого не ждем, ни сети ни файловой системы, нам просто нужно быстро что-то посчитать. В этому случае нужно минимизировать переключение контекста между потоками. Именно поэтому в
Можно также вспомнить что есть
В этом и заключается разница между Sсheduler. Никакой магии и особых потоков. Все максимально просто, разница лишь в количестве потоков.
Что в Rx что в Coroutines есть абстракция, которая позволяет указать на каких потоках нужно выполнять операции. У Rx эта абстракция называется Sсheduler, у Coroutines это Dispatcher. Обе эти абстракции работают практически одинаково, поэтому просто держите в голове что все, что сказано про Sсheduler применимо и к Dispatcher.
Обычно у нас несколько вариантов Sсheduler для разных типов задач. Для похода в сеть или базу данных мы используем
IO
, а для расчетов используем Computation
. Возникает вопрос, а в чем разница? Почему один Sсheduler хорош для одних задач, а другой для других? Чтобы понять в чем разница, нужно разобраться как работают Sсheduler. Sсheduler это лишь обертка над Executor. Executor сделали для решения одной интересной задачи. Поток очень тяжелая штука по памяти, примерно 1mb, если каждый раз создавать поток, когда нам нужно сделать асинхронную задачу это может привести к OutOfMemory.
В основе Executor лежит простая идея. Если подумать, то очевидно что нам не нужно каждый раз создавать новый поток на каждую задачу. Достаточно переиспользовать имеющиеся, и просто раздавать им задачи. Чем-то такая система напоминает Looper и MessageQueue. Идем дальше, действительно ли много потоков позволят быстрее выполнять программу? На самом деле нет, после некоторого количества потоков, программа наоборот будет работать медленнее.
У нас есть процессор, в котором несколько ядер. Операционная система имитирует параллельные выполнение программ путем шедулинга. Другими словами, она дает одной программе немного поработать на процессоре, затем все приостанавливает и дает поработать другой программе. Поэтому даже если у процессора одно ядро, можно запускать несколько программ, которые будут выполнятся параллельно, ну точнее сказать "как бы параллельно".
Когда речь идет о потоках работает тот же принцип, система может сделать так, чтобы два потока по очереди работали на одном процессоре, а может запустить их параллельно на разных. Система также переключается между потоками. Операция переключения довольно дорогая, потому как нужно приостановить поток, сохранить его состояние, запустить другой поток, накатить его сохраненное состояние. Это операция называется переключение контекста. Если потоков слишком много, система будет часто переключатся м/у этими потоками и программа будет продвигаться очень медленно.
Вернемся к нашим задачам. Когда мы идем в базу или сеть мы точно будем ждать пока придут данные, как вы понимаете сеть в рамках выполнения программы довольно медленная штука. Поток в этом случае блокируется и просто ждет данные с сокета, ну или с файла есть речь о базе данных. В таком случае если у нас мало потоков, и много запросов в сеть, мы тупо будем долго ждать, т.к запросы будут идти практически последовательно. Поэтому в
IO
Sсheduler, а точнее сказать в Executor этого Sсheduler много потоков, из-за блокирующих задач. Касательно задачи расчета, мы никого не ждем, ни сети ни файловой системы, нам просто нужно быстро что-то посчитать. В этому случае нужно минимизировать переключение контекста между потоками. Именно поэтому в
Сomputation
Sсheduler наоборот мало потоков. Если точнее количество потоков в Сomputation
равно количеству ядер процессора. Это позволяет минимизировать переключение контекста и тем самым ускорить выполнение расчета. Можно также вспомнить что есть
MainThread
Sсheduler, который позволяет выполнять задачи на потоке UI. Все, что делает этот Sсheduler, тупо отправляет все задачи в Looper главного потока.В этом и заключается разница между Sсheduler. Никакой магии и особых потоков. Все максимально просто, разница лишь в количестве потоков.
🔥41👍11
Меня всегда удивляло как легаси превратили в маркетинговый инструмент для вакансии. Очень часто можно увидеть фразу у нас нет легаси, весь код написан на Kotlin. Возникает вопрос, а точно ли они понимают что такое легаси? Почему если фича написана на Java то это сразу легаси, а может ли быть легаси на Kotlin? Складывается впечатление, что если мы пишем на Kotlin то это по определению код который легаси быть не может.
Представьте ситуацию, есть крутая команда разработчиков. Они придерживаются лучших практик, уделяют внимание архитектуре, чистоте кода, тестам вся фигня, но при этом пишут все на Java. Затем в индустрии все начинают переходить на Kotlin. Переходят на него по понятным причинам, он позволяет избежать многих ошибок, писать на нем приятнее и при этом все плюсы JVM сохраняются.
В этот момент весь код на Java нарекают легаси. Возникает вопрос, схерали? Да в этом коде нет фишек которые есть у Kotlin, но код по-прежнему чистый, поддерживаемый и покрыт тестами в разумных пределах. Его нарекли легаси только потому, что он не написан на другом более новом языке. Однажды наступит момент, когда на смену Kotlin придет еще более совершенный язык. Тогда весь на код на Kotlin станет легаси?
Если задуматься то какой код можно назвать легаси? Мне нравится определение Майкла Физерса: “легаси код – код который не проходит тесты“. В продукте всегда происходит куча изменений, изменения в архитектуре, подходах, требованиях. Это нормально так и должно быть. Бизнес может менять стратегию, а следовательно и требования. Если бизнес не меняется и не растет, то конкуренты просто загребут весь рынок. Кодовая база при этом должна поддерживать эти изменения, архитектура должна помогать в этом.
Код становится легаси в том случае, когда внесения изменений в него становится страшно дорогим. Если у кода, нет никаких инструментов тестирования и внятной архитектуры ты не можешь безопасно вносить изменения. Ты не уверен в том, не сломал ли ты чего. Приложение при этом может быть громадным, протестировать все будет очень дорого для команды. Появляется страх изменений и появляются выражения: “работает не трогает”.
Легаси не зависит от языка на котором писали. Если есть хорошее тестирование, код на старом языке можно понемногу переписывать на новый, и даже без переписывания безопасно вносить изменения. Отсюда также выходит другая интересная вещь. Даже новый код, написанный на новом языке, может стать легаси, если у него нет никаких тестов и выбрана архитектура которая не дает легко изменять код.
Поэтому забавно когда пытаются продать проект, написанный полностью на Kotlin с утверждением, что “У нас вообще нет легаси”. Камон ребята, пара итераций и это легаси появится. Поэтому не ведитесь на этот маркетинговый ход, задавайте кучу вопросов на собесе.
Представьте ситуацию, есть крутая команда разработчиков. Они придерживаются лучших практик, уделяют внимание архитектуре, чистоте кода, тестам вся фигня, но при этом пишут все на Java. Затем в индустрии все начинают переходить на Kotlin. Переходят на него по понятным причинам, он позволяет избежать многих ошибок, писать на нем приятнее и при этом все плюсы JVM сохраняются.
В этот момент весь код на Java нарекают легаси. Возникает вопрос, схерали? Да в этом коде нет фишек которые есть у Kotlin, но код по-прежнему чистый, поддерживаемый и покрыт тестами в разумных пределах. Его нарекли легаси только потому, что он не написан на другом более новом языке. Однажды наступит момент, когда на смену Kotlin придет еще более совершенный язык. Тогда весь на код на Kotlin станет легаси?
Если задуматься то какой код можно назвать легаси? Мне нравится определение Майкла Физерса: “легаси код – код который не проходит тесты“. В продукте всегда происходит куча изменений, изменения в архитектуре, подходах, требованиях. Это нормально так и должно быть. Бизнес может менять стратегию, а следовательно и требования. Если бизнес не меняется и не растет, то конкуренты просто загребут весь рынок. Кодовая база при этом должна поддерживать эти изменения, архитектура должна помогать в этом.
Код становится легаси в том случае, когда внесения изменений в него становится страшно дорогим. Если у кода, нет никаких инструментов тестирования и внятной архитектуры ты не можешь безопасно вносить изменения. Ты не уверен в том, не сломал ли ты чего. Приложение при этом может быть громадным, протестировать все будет очень дорого для команды. Появляется страх изменений и появляются выражения: “работает не трогает”.
Легаси не зависит от языка на котором писали. Если есть хорошее тестирование, код на старом языке можно понемногу переписывать на новый, и даже без переписывания безопасно вносить изменения. Отсюда также выходит другая интересная вещь. Даже новый код, написанный на новом языке, может стать легаси, если у него нет никаких тестов и выбрана архитектура которая не дает легко изменять код.
Поэтому забавно когда пытаются продать проект, написанный полностью на Kotlin с утверждением, что “У нас вообще нет легаси”. Камон ребята, пара итераций и это легаси появится. Поэтому не ведитесь на этот маркетинговый ход, задавайте кучу вопросов на собесе.
🔥34👍9❤2
Утечка памяти наверное одна и самых неприятных ошибок которую можно совершить при разработке. Она может привести к лагам на устройстве и порой даже крэшу. В предыдущих постах я упоминал, что сделать утечку памяти можно даже не подозревая об этом. Насколько бы вы внимательно не относились к коду из-за человеческого фактора такая ошибка рано или поздно всплывет. Причем утечка памяти может быть даже в библиотеках от гугла.
Естественно эту проблему нужно было как-то решать. Поэтому был создан инструмент, который позволяет находить утечки памяти во время тестирования работы приложения. Возможно вы уже натыкались на этот инструмент – LeakCanary. Библиотека максимальна проста в использовании при этом позволяет быстро найти корень проблемы. LeakCanary следит за приложением, проводит анализ в фоне, сохраняет результаты, даже строит путь до ссылки из-за которой произошла утечка.
При всем при этом, со стороны клиента ничего не нужно делать. Просто указал либу в зависимостях gradle, и она сама начинает работать. Естественно инженерное любопытство заставляет задаться вопрос, а как работает эта технология?
Я планировал сделать один пост по тому, как работает leak canary, но в ней перемешано столько интересных технологий, что я решил разбить на несколько постов. Каждый их которых будет разбирать отдельную часть библиотеки и отвечать на конкретный вопрос:
👉 Как она запускается, хотя мы в коде ничего не прописывали?
👉 Откуда берется отдельный ярлык, который позволяет посмотреть историю всех утечек конкретного приложения?
👉 Как leak canary вообще находит утечки?
👉 Как находится ссылка из-за которой произошла утечка?
Естественно эту проблему нужно было как-то решать. Поэтому был создан инструмент, который позволяет находить утечки памяти во время тестирования работы приложения. Возможно вы уже натыкались на этот инструмент – LeakCanary. Библиотека максимальна проста в использовании при этом позволяет быстро найти корень проблемы. LeakCanary следит за приложением, проводит анализ в фоне, сохраняет результаты, даже строит путь до ссылки из-за которой произошла утечка.
При всем при этом, со стороны клиента ничего не нужно делать. Просто указал либу в зависимостях gradle, и она сама начинает работать. Естественно инженерное любопытство заставляет задаться вопрос, а как работает эта технология?
Я планировал сделать один пост по тому, как работает leak canary, но в ней перемешано столько интересных технологий, что я решил разбить на несколько постов. Каждый их которых будет разбирать отдельную часть библиотеки и отвечать на конкретный вопрос:
👉 Как она запускается, хотя мы в коде ничего не прописывали?
👉 Откуда берется отдельный ярлык, который позволяет посмотреть историю всех утечек конкретного приложения?
👉 Как leak canary вообще находит утечки?
👉 Как находится ссылка из-за которой произошла утечка?
🔥36👍10❤1
В каждом Android приложении есть такой файл AndroidManifest. В манифесте мы прописываем основные компоненты нашего приложения. Делаем мы это для того, чтобы показать системе какие компоненты у нас есть, какие события мы хотим отлавливать, какие разрешения нам нужны и еще дофига всего. Нужен он для того, чтобы система понимала, что наше приложение умеет и какие данные может предоставить.
Приложение может состоять из многих модулей. Соответственно в каждом модуле будет определен свой AndroidManifest, в нем будут описываться компоненты, которые используются в данном модуле. Помимо этого вы можете подключать некоторые библиотеки, в которых могут быть свои Activity, Service и т.д. У этой библиотеки также будет свой AndroidManifest.
К чем я все это веду? Когда вы собираете ваше приложение, компилятор мержит все эти манифесты в один большой манифест. Потому как в конечном архиве(apk) система ожидает увидеть только один манифест. Нужно понимать этот механизм.
Теперь поговорим об основных компонентах приложения. Среди них есть один, используется он реже всего, но позволяет делать интересные штуки. Есть такой компонент Content Provider, предназначается он для данных между приложениями. Передача данных нас сейчас не интересует, а интересует его фишки.
Во-первых,
Другими словами, если вы в своей либе создадите манифест, в котором укажете свой Content Provider, он будет запущен до старта приложения. Это дает возможность словить момент запуска приложения и даже получить контекст. При этом не нужно ничего нигде прописывать, система сама создаст Content Provider и дернет метод onCreate. Из этого получаем два вывода.
Вывод номер рас. Нужно проверять код незнакомых библиотек. В одной из них может оказаться вот такой Content Provider который безнаказанно стырит данные пользователя и отправит их на левый сервер.
Вывод номер два. Можно прикрутить функциональность ничего не прописывая в коде. Именно этот механизм и использует LeakCanary. Библиотека просто подсовывает свой Content Provider, тем самым отлавливает момент запуска приложения. Ну а получив доступ к Context, LeakCanary получает доступ практически ко всему приложению. Она навешивает кучу листнеров которые позволяют отлеживать все Acivity, Fragment, Service и т.д.
P.S По этой же схеме работают некоторые библиотеки гугла, вроде Firebase.
Приложение может состоять из многих модулей. Соответственно в каждом модуле будет определен свой AndroidManifest, в нем будут описываться компоненты, которые используются в данном модуле. Помимо этого вы можете подключать некоторые библиотеки, в которых могут быть свои Activity, Service и т.д. У этой библиотеки также будет свой AndroidManifest.
К чем я все это веду? Когда вы собираете ваше приложение, компилятор мержит все эти манифесты в один большой манифест. Потому как в конечном архиве(apk) система ожидает увидеть только один манифест. Нужно понимать этот механизм.
Теперь поговорим об основных компонентах приложения. Среди них есть один, используется он реже всего, но позволяет делать интересные штуки. Есть такой компонент Content Provider, предназначается он для данных между приложениями. Передача данных нас сейчас не интересует, а интересует его фишки.
Во-первых,
onCreate
у Content Provider вызывается перед onCreate
у Application, из-за этого Content Provider часто используют для какой-нибудь аналитики, которую нужно настроить еще до запуска самого приложения. Во вторых это единственный компонент приложения который создается в момент старта приложение, даже чуть раньше. Другими словами, если вы в своей либе создадите манифест, в котором укажете свой Content Provider, он будет запущен до старта приложения. Это дает возможность словить момент запуска приложения и даже получить контекст. При этом не нужно ничего нигде прописывать, система сама создаст Content Provider и дернет метод onCreate. Из этого получаем два вывода.
Вывод номер рас. Нужно проверять код незнакомых библиотек. В одной из них может оказаться вот такой Content Provider который безнаказанно стырит данные пользователя и отправит их на левый сервер.
Вывод номер два. Можно прикрутить функциональность ничего не прописывая в коде. Именно этот механизм и использует LeakCanary. Библиотека просто подсовывает свой Content Provider, тем самым отлавливает момент запуска приложения. Ну а получив доступ к Context, LeakCanary получает доступ практически ко всему приложению. Она навешивает кучу листнеров которые позволяют отлеживать все Acivity, Fragment, Service и т.д.
P.S По этой же схеме работают некоторые библиотеки гугла, вроде Firebase.
❤36👍20🔥2
С этим пунктом в LeakCanary все еще проще. Как вообще мы указываем системе какую Activity нужно запустить первой? Опять-таки через AndroidManifest и специальные intent-filter которые указываем у Activity. В intent-filter мы прописываем Action показывающий на какие действия система должна предлагать эту Activity и Category показывающая системе дополнительную инфу где располагать эту Activity.
Для главной Activity Action = android.intent.action.MAIN, Category = android.intent.category.LAUNCHER. Система читает этот Manifest и исходя из этих action и category понимает, что эту Activity нужно отобразить в лаунчере. Интересный момент заключается в том, что таких Activity может быть много. У вас есть возможность сделать хоть 3 разных точек входа в приложения причем с разными иконками и разными подписями.
LeakCanary в своем манифесте подсовывает такую Activity. При нажатии на эту Activity просто открывается не главная Activity вашего приложения, а вот эта Activity библиотеки которая позволяет получить данные об утечках этого приложения. Другими словами, помимо специального Content Provider, библиотека подсовывает вам еще и свои Activity (их там несколько).
Application при этом только один, а значит каждая такая Activity будет привязана именно к конкретному приложению, это позволяет избежать путаницы когда у вас два разных приложения в которых используется LeakCanary.
Очевидно что остается не очень удобное поведение когда мы сначала запустили Activity LeakCanary, а затем запустили Activity уже нашего приложения. Неудобство тут в том, что не понятно что делать с навигацией, т.к это вроде две отдельные части приложения которые не должны быть вместе.
Чтобы убрать это неудобство, используется taskAffinity. Если не знаете или забыли давайте вспомним. Activity у нас запускаются в стэке, который чем-то напоминает стэк фрагментов. Этих стэков у приложения может быть несколько. По дефолту все Activity запускаются в одном стэке. Однако у Activity есть специальный атрибут который позволяет указать в каком стеке должна запускаться Activity.
Этот атрибут taskAffinity. Прописываем какую-то уникальную строку в этом атрибуте желательно чтобы в этой строке был ваш applicationId чтобы не было путаницы в другими приложениями. После этого Activity будет запускаться не в стандартном стэке, а в другом. В лаунчере со списком запущенных приложений эти стэки будут разными, т.е у вас будет как будто бы два отдельных приложения, хотя на самом деле одно.
LeakCanary используют taskAffinity для своих Activity чтобы не влезать в навигацию вашего приложения. Это позволяет сделать полную видимость того, что у вас в одном приложении два. Первое основное и второе которое связано исключительно с информацией про утечки.
Для главной Activity Action = android.intent.action.MAIN, Category = android.intent.category.LAUNCHER. Система читает этот Manifest и исходя из этих action и category понимает, что эту Activity нужно отобразить в лаунчере. Интересный момент заключается в том, что таких Activity может быть много. У вас есть возможность сделать хоть 3 разных точек входа в приложения причем с разными иконками и разными подписями.
LeakCanary в своем манифесте подсовывает такую Activity. При нажатии на эту Activity просто открывается не главная Activity вашего приложения, а вот эта Activity библиотеки которая позволяет получить данные об утечках этого приложения. Другими словами, помимо специального Content Provider, библиотека подсовывает вам еще и свои Activity (их там несколько).
Application при этом только один, а значит каждая такая Activity будет привязана именно к конкретному приложению, это позволяет избежать путаницы когда у вас два разных приложения в которых используется LeakCanary.
Очевидно что остается не очень удобное поведение когда мы сначала запустили Activity LeakCanary, а затем запустили Activity уже нашего приложения. Неудобство тут в том, что не понятно что делать с навигацией, т.к это вроде две отдельные части приложения которые не должны быть вместе.
Чтобы убрать это неудобство, используется taskAffinity. Если не знаете или забыли давайте вспомним. Activity у нас запускаются в стэке, который чем-то напоминает стэк фрагментов. Этих стэков у приложения может быть несколько. По дефолту все Activity запускаются в одном стэке. Однако у Activity есть специальный атрибут который позволяет указать в каком стеке должна запускаться Activity.
Этот атрибут taskAffinity. Прописываем какую-то уникальную строку в этом атрибуте желательно чтобы в этой строке был ваш applicationId чтобы не было путаницы в другими приложениями. После этого Activity будет запускаться не в стандартном стэке, а в другом. В лаунчере со списком запущенных приложений эти стэки будут разными, т.е у вас будет как будто бы два отдельных приложения, хотя на самом деле одно.
LeakCanary используют taskAffinity для своих Activity чтобы не влезать в навигацию вашего приложения. Это позволяет сделать полную видимость того, что у вас в одном приложении два. Первое основное и второе которое связано исключительно с информацией про утечки.
👍29❤1🔥1