Dolgo.polo Dev | Денис Долгополов – Telegram
Dolgo.polo Dev | Денис Долгополов
2.27K subscribers
88 photos
2 videos
120 links
Разбираемся в мобильной разработке (Android/iOS) и пытаемся выяснить, зачем оно так устроено

Статистика/цены: @dolgo_polo_dev_stats

По вопросам/рекламе: @dolgopolovdenis
Download Telegram
​​MVI — архитектура за 100 слов

Этот подход набирает все большую популярность, вытесняя MVVM

Яркий признак MVI — наличие State
State
— неизменяемое состояние (объект, который называет состояние и передает все связанные с ним данные)

Например, ErrorState(val errorMessage: String)

Слои:

• Model — источник State для View
• View — принимает State и отображает
• Intent — события

Не буду вдаваться в философию, приведу пример:

когда пользователь открывает экран, Model создает State.Defult
загружает в этот State всю информацию, которую нужно отобразить, и отдает View

через некоторые время пользователь нажимает кнопку
View сообщает об новом Intent в Model
Model
формирует новый State и отдает View

В целом — это все. Но мир не идеален, поэтому:

Intent можно разделить на 2 группы:

• Действия юзера
• События от бекенда (например, пришло новое сообщение)

State можно поделить на 2 группы:

• Стабильные — должны отображаться даже после переворота экрана
• Эффекты — должны отобразиться один раз (например, Toast\Snackbar)

Плюсы MVI:
• наглядность — прямой "поток" данных и событий, без макарон
• модульность — тестируемость
• заранее прописываешь стейты, так что не забываешь учесть разные состояния (обычно State.Default, State.Failed, State.Loading, State.Success)

Минусы MVI:
• часто приходится делать тяжелый моральный выбор - если на экране появляются новые вьюшка, то нужно создавать новый State или модифицировать старый?
👍28🔥82
Прошло несколько месяцев — мы праздновали первую сотню человек в нашей тусовочке — теперь масштабы серьезнее

Теперь мы поднимаем кружки кофе и пива за первую 1000 человек — торжественное ура

.....

Желаю всем классного ворк-лайф баланса, чтобы разработка приносила интересные события в жизнь и новые технологии перестали появляться каждую неделю)

А мне желаю наконец-то найти классных рекламодателей — это будет следующий большой рывок в развитии этой тусовки

🍻🤝🍻

.....

в ближайших планах:

• продолжаю равно то же самое, пытаюсь удержаться от ввода новых рубрик. еще не время)

• появится форма обратной связи, через которую можно будет влиять на темы следующих постов
👍15🎉7🔥51🤩1🤮1
​​Порядок инициализации полей, конструкторов и блоков

Рассмотрим самый сложный пример — представим, что у нас есть наследование, статика, конструкторы и поля (+ companion object, + init)

Порядок инициализации в Java и Kotlin немного различны, хотя и схожи по общей идее

Java:

Статические и нестатические поля и блоки инициализируются в порядке объявления (чем выше строчка, тем первее она будет инициализирована)

1. Статические поля и блоки родителя
2. Статические поля и блоки ребенка
3. Нестатические поля и блоки родителя
4. Конструктор родителя
5. Нестатические поля и блоки ребенка
6. Конструктор ребенка

Kotlin

В котлине нет статики, нельзя написать код блока вне функции, но появляются companion object и init-функция

1. Поля внутри companion object родителя
2. Поля внутри companion object ребенка
3. Поля и блок init родителя (в порядке объявления)
4. Конструктор родителя
5. Поля и блок init ребенка (в порядке объявления)
6. Конструктор ребенка

....

когда приходится использовать статические или нестатические блоки кода в боевых проектах?

в какой момент инициализируется статическое поле, если к нему обратиться, не создавая объект класса? как это повлияет на порядок инициализации в момент создания объекта класса?
👍293🤩1
​​Task или виды Activity launch mode

У каждого приложения может быть несколько стеков из Activity. Под каждый стек создается новая Task

Activity launch mode
— описывает сценарий изменения стека при запуске новой Activity

Например, можно открывать каждую Activity в отдельном стеке. Тогда в списке запущенных приложений (Recent screen) вы увидите, что ваше приложение открыто в нескольких окнах, каждое из которых можно закрыть отдельно


Выбрать, что произойдет после запуска новой Activity, можно с помощью тега launchMode у <activity> в Manifest. И при помощи установка флага в Intent. А еще при помощи параметра taskAffinity, allowTaskReparenting, clearTaskOnLaunch, alwaysRetainTaskState, finishOnTaskLaunch....

Все гениальное просто, да?
Но разберем основные варианты, которые покрывают 99,99% случаев:

• standart

создастся новая Activity в текущей таске. даже если ее экземпляры есть в текущей таске или в другой

• singleTop

если в текущей таске Activity находится на вершине, то новый ее инстанс создан не будет. текущий инстанс получит Intent через onNewIntent()

в противном случае создастся новый экземпляр в текущей таске

• singleTask

если уже есть таска, в которой вызываемая Activity находится в корне стека, то откроется экземпляр этой Activity, ей будет передан Intent через onNewIntent(), а все остальные Activity в этой таске будут задестроены

в противном случае создается новая таска с новым Activity

• singleInstance

аналогично singleTask. отличие - в таске, в которой находится Activity с таким флагом, может находиться только эта Activity. Под остальные Activity, запущенные из этой Activity, будут создаваться новые таски

• singleInstancePerTask

аналогично singleTask. отличие - если добавить флаг FLAG ACTIVITY MULTIPLE TASK, запуск новой Activity может вызвать создание новой таски, даже если уже есть таска с инстансом этой Activity в корне стека

• standart + FLAG ACTIVITY CLEAR TOP

если в текущей таске есть запускаемая Activity, то она получит Intent через onNewIntent(), а все Activity, запущенные после нее, будут задестроены

в противном случае будет создан новый экземпляр Activity в текущей таске

....

1. есть Activity, есть Task. а зачем тогда вводится и за что отвечает абстракция Window?

2. все вышеописанное справедливо для приложения с множеством Activity. а возможно ли добиться создания нескольких Task в single activity приложении на Fragments/Compose?
👍142😁1🤩1
​​Как придумать свои единицы измерения на примере Compose

Вероятно, это начало серии постов о Compose. Начнем с малого

В Compose можно использовать привычные единицы измерения:
• 18.dp
• 18.sp
• 18.em

Но почему не приходится писать Dp(18) или Dp.Builder().get(18)?

Фокус в extensions-функциях, созданных для всех численных примитивов (Int, Float, Double)

Например:

inline val Int.dp: Dp get() = Dp(value = this.toFloat())

(если вы практикуете только Java, вам может стать плохо, это нормально)

Разберем каждую часть строчки:

• inline — слово, заставляющее компилятор на этапе компиляции упростить код за счет замены лямбда-выражения на последовательный код

если слово inline стоит перед переменной (как в текущем примере), то оно относится к функциям get() и set() переменной

• .dp: Dp — создание переменное типа Dp в классе Int (расширили класс Int новой переменной)

• get() — переопределили зарезервированную функцию get() для переменной dp

• this — ссылка на класс Int, то есть в случае 18.dp ссылка на класс Int(18)

• value — название параметра в конструкторе класса Dp (просто для наглядности)

вот так, использовав всего 5 трюков котлина в одной строчке, можно придумать собственную единицу измерения

....

в боевом проекте такую фишку можно применить для управления, например, балансом юзера — 18.rub + 20.dol = 30.eur. еще идеи?
🔥145👍3😁2
​​Thread Safe — когда пора начать об этом думать

Понятие Thread Safe имеет множество определений, но суть у них одна:

если переменную изменяют несколько потоков, то результат изменения должен быть предсказуемый

(если b = 1, и мы дважды вызываем b++, то ожидаем увидеть b = 3 — это и есть предсказуемость)


Что может пойти не так?

Во-первых, по умолчанию потоки работают с переменной не в общей памяти, а создают свою локальную копию. А после всех манипуляций с объектом внутри потока возвращают объект в общую память

То есть другие потоки смогут увидеть изменения в объекте только после завершения этапа работы первого потока


Во-вторых, один поток может взять переменную и начать изменять ее. Например, была строка var s = "Big", а на выходе должна получится строка s = s + "Hot"

Но пока первый поток не закончил изменение s, второй поток тоже считал s. И решил сделать из нее s = s + "Dog"

Получается, что мы хотели прибавить к "Big" строки "Hot" и "Dog", а получим на выходе не "BigHotDog", а "BigHot" или "BigDog" или "BigDogHot" — результат непредсказуем


Ответ на заголовок поста:

пора начинать разбираться в многопоточности, как только в проекте появляется два и более потоков, работающих с общими переменными

....

с чего стоит начать, если хочешь понять эту вашу многопоточность вдоль и поперек?
👍244
​​Как перехватывать Exception в Coroutine. Часть 1

Поймать Exception в Java и Kotlin просто — ошибка, выброшенная через throw, поднимается вверх по стеку функций, пока не попадет в try-catch блок или в Thread.UncaughtExceptionHandler


Но как только появляются корутины...

Это одна из самых сложных тем по двум причинам:

• зависимость пути проброса Exception от множества условий

• минимальное количество примеров (в документации и интернете в целом), покрывающих неочевидные кейсы


На логику обработки ошибок в корутинах влияет:

• создаем корутину верхнего уровня или вложенную

• используем ли мы Job или SupervisorJob

• используем ли мы билдер launch или async

• устанавливаем ли мы CoroutineExceptionHandler (и куда устанавливаем)

• еще и особая CancellationException обрабатывается по другим правилам


причем здесь не бинарная логика, а комбинация этих условий определяет, как и где мы сможем перехватить Exception

....

в следующих частях разберемся, зачем столько сложностей, и создадим единую логику отслеживания ошибок
👍22🔥21😱1🎉1
​​Как перехватывать Exception в Coroutine. Часть 2

Разберемся с тем, как формируется контекст корутины

CoroutineContext — параметры корутины или CoroutineScope:

Job или SupervisorJob
Dispatcher (IO, Main, Default...)
CoroutineExceptionHandler (или его отсутствие)
CoroutineName (параметр для дебага)


Алгоритм формирования для CoroutineScope:

• стандарные параметры + унаследованные + указанные в билдере

Алгоритм формирования для корутины:

• стандарные параметры + унаследованные + указанные в билдере + Job

при этом слагаемые справа переопредяют предыдущие


Пример:

CoroutineScope() {
launch(Dispatcher.Main
+ SupervisorJob()) {
launch(Dispatcher.Default)
{}
}
}

Контекст для CoroutineScope:

Dispatcher.Default, CoroutineName("coroutine"), Job1

для внешнего launch:

Dispatcher.Main, CoroutineName("coroutine"), Job2

для внутреннего launch:

Dispatcher.Default, CoroutineName("coroutine"), Job3


На что обратить внимание:

• у внешнего launch в контексте исчезает SuperviserJob, потому что при формирование прибавляется Job справа

то есть установит SuperviserJob можно только в CoroutineScope


• между всеми Job установлены parent-child отношения

если вызвать Job1.cancel(), то Job1 и Job2 тоже будут остановлены

а если в launch явно передать Job(), то взаимоотношения разрушаться. родитель не будет ожидать завершения работы такого ребенка, и останавливать его не будет
👍71
​​А теперь к новостям канала

1. Появился бот приема обратной связи для выбора темы следующих постов — t.me/dolgopolobot

Если есть идея, о чем было бы интересно почитать в следующем посте, то можно оставить сообщение этому боту с тегом #idea_dpd

Принимаются темы любой сложности и актуальности — от "вот бы был пост про синхронизацию потоков и поддержку OS Android 2.1 в 2022 году" до "а какой жизненный цикл у Fragment?!"


2. Выходит в свет мой второй канал — там картинки со смешными местами из путешествий и рассказы об интересных штуках, о которых невозможно молчать

Отдыхать от программирования сюда — t.me/dolgo_polo_ahaha


3. Оказалась, есть часть людей, которая пользуется Яндекс.Дзеном

Игнорировать это больше нельзя, поэтому постепенно там (тут — zen.yandex.ru/android_dpd) будут появляться старые посты

Хотя есть подозрение, что эти посты многие из вас не видели. Кто вообще листает канал выше пяти последних сообщений?
6👍4
​​Как перехватывать Exception в Coroutine. Часть 3

Факты, которые нам понадобятся:

• корутина верхнего уровня — корутина, чьим родителем является Scope, а не другая корутина

scope.launch { — верхнего уровня
launch { } — не верхнего
}


CoroutineExceptionHandler — получатель необработанных ошибок

Его можнно назначить в конструкторе Scope или в билдере launch() верхнего уровня, если его родитель Scope(SupervisorJob)


• два понятия, которые нельзя путать:

throws (выбрасывать)

если ошибка выброшена, то она поднимается по стеку вызова функций, пока не попадется в try-catch или Thread.UncaughtExceptionHandler


propagate (распространять)

если ошибка распространяется, то она игнорирует блоки try-catch и передается от дочерний корутины к родителю

если ошибка добралась до коренного Scope, а у него не установлен CoroutineExceptionHandler, то ошибка передается в Thread.UncaughtExceptionHandler


• при создании Scope можно назначить два вида Coroutine Context:

Job — если внутри Scope появляется ошибка, не перехваченная try-catch, то Scope завершает работу всех дочерних корутин, завершается сам и распространяет ошибку

SupervisorJob — если внутри Scope появляется ошибка, не перехваченная try-catch, то Scope распространяет ошибку, не завершая дочерние корутины и себя


CancellationException — особый вид ошибки. Выбрасывается, когда корутина была остановлена с помощью cancel()

Она, в отличие от остальных ошибок, не останавливает родительский Scope и не влияет на работу остальных корутин в Scope

....

если какой-то из пунктов вызывает вопросы — готов расписать подробнее в комментариях)

и нам наконец хватает знаний, чтобы перейти к практике в следующем посте
5👍5🎉1
​​Как перехватывать Exception в Coroutine. Часть 4

Разберемся с билдером launch

Рабочие способы перехватить ошибки:

• внутри launch

launch {
try {
throw Exception()
}
catch(e: Exception) {}
}


• с помощью перехватчика необработанных ошибок, установленного в Scope или launch верхнего уровня

scope.launch(handler) {
throw
Exception()
}

СoroutineScope(handler) {
launch {
throw
Exception()
}

}

в этом способе важно понимать — если родитель Scope(Job), дети этого Scope будут остановлены

если родитель Scope(SupervisorJob), дети этого Scope продолжат работу

если родитель корутина, то дети этой корутины будут остановлены в любом случае



Что будет, если не обработать:

ошибка будет распространена родительским корутинам и Scope, пока не попадет в CoroutineExceptionHandler или Thread.UncaughtExceptionHandler



Что не получится сделать:

• обернуть launch в try-catch

такое не сработает:

try {
launch
{ throw Exception() }
} catch(e: Exception) {}

потому что launch распространяет ошибку родителю, а не выбрасывает


....

если вы встретили примеры, в которых не очевидно поведение корутин при возникновении ошибки — скидывайте их в комментарии, разберем)
7👍4🎉1
​​Как перехватывать Exception в Coroutine. Часть 5 (финал)

Разберемся с билдером async

Рабочие способы обработки ошибки:

• внутри async

async
{
try
{
throw Exception()
}
catch(e: Exception) {}
}

• с помощью try-catch, если async верхнего уровня

Ошибка выбрасывается во время вызова await()

runBlocking
{
val job = scope.async {
throwError()
}

tryCatch {
job.await()
}
}


Что будет, если не обработать:

ошибка будет распространяться родительским корутинам и Scope, пока не попадет в CoroutineExceptionHandler или Thread.UncaughtExceptionHandler

....

в комментариях оставлю ссылку на Kotlin Playground с тестами, на которых можно проверить свое чутье — где и кем будет перехвачена ошибка
🔥51
​​GET vs POST vs PUT в Retrofit 2 — уровень "Мобильный разработчик"

Вероятно, если вы человек-бекендер, то нужно углубляться сильнее. А для нас достаточно следующих фактов:

Указывая аннотации @GET, @POST или @PUT, вы определяете, в каком виде будут переданы параметры запроса на сервер


GET

Параметры запроса вставляются в URL запроса с помощью аннотации Query(parameterName) в формате key=value

Параметры добавляются после знака "?"

Длина URL ограничена 2048 символами

Например, на выходе можете получить такой запрос: https://www.google.com/search?q=tg+dolgo.polo+dev
key = q
value = tg+dolgo.polo+dev

POST

Параметры запроса вставляются не в URL, а в поле requestBody с помощью аннотации @Body в формате key=value

POST-запросом можно отправить файл, так как он поддерживает отправку бинарных данных

Например, на выходе можете получить такой запрос: https://www.google.com/search

Параметры при этом передаются отдельно в поле requestBody


PUT

То же самое, что и POST. Нужен для логического разделения ролей запросов в API (просто другое название)

POST — обновляет данные по какому-то правилу
например, увеличивает количество яблок в коробке: apples++

PUT — устанавливает данные
например, устанавливает количество яблок в коробке: apples = 1

Поэтому говорят, PUT — идемпотентный — можно отправлять один и тот же запрос много раз, данные изменятся только один раз

Еще есть мнение, что POST для списков, а PUT для обращения по конкретному id

....

а менее известные DELETE, PATCH, OPTIONS, CONNECT и TRACE зачем и когда используются?
👍204🐳2👎1🙏1
​​Чек-лист профессионального программиста

Это свод правил, выработанный за годы фриланса

Когда соблюдаю их, приложение получается удобным для юзера

Да, этот список не сделает из вас сеньора, как и не делает им меня. Но это как с мужем-на-час, который не только починил кран, но и убрал за собой рабочее место — сразу видно, крутой


• сортировать списки

если список выводится пользователю, нужно не забыть понятную сортировку - по дате создания или алфавиту, если нет других заданных параметров


• проверять нули, пустые строки и нуллы, количество элементов в списке

продумайте, что увидит юзер. например, если список пуст, не забыть вывести "оëой, данных пока нет"

и не выводить "ваша скидка 0% !!!"


• поднимать ошибку по стеку в обертке

если вы запрашиваете Long из базы данных, то возвращать -1 в случае ошибки не лучшая идея. Лучше вернуть Success(value) или Error(errorCode)


• использовать варианты toDoubleOrNull() вместо toDouble()

никогда не надейтесь, что преобразование строки в число пройдёт успешно
а вдруг кто-то подсунет строку с запятой вместо точки?

приложение крашнется с ошибкой NotNumber, а юзер умрёт от инфаркта

лучше добавить лишнюю проверку на null (или обернуть в try-catch)


• сообщать о загрузке

если контент загружается больше 500мс, отобразите индикатор загрузки или скелет будущего контента


• помнить, что юзер может закрыть приложение в любой момент, но операция должна завершиться

юзер не обязан ждать, пока данные сохранятся - может свернуть приложение, заблокировать телефон, уронить его в лужу - операция должна выполниться в фоне
👍277🏆4🤔2
Стартовал мой второй серьезный проект — место, где можно выкладывать свои идеи и очень обоснованно критиковать чужие

tg: mvpшная

• зачем это нужно?

появляется идея. она кажется тебе неплохой (принесет успех, деньги и внутренний покой), но ведь оцениваешь ее только через призму своего опыта

а можешь сразу обсудить ее с другими крутыми людьми, которые увидят слабые места


например, можете залететь с идеями своих приложений или сервисов. узнаете, на чем можете споткнуться. или найдете инвесторов и собутыльников единомышленников

"а вдруг украдут!" - если у вашу идею можно легко украсть, то либо это уже делают тысячи людей, либо вы что-то упускаете в расчетах. и как раз обсуждение с незнакомыми людьми поможет узнать, что забываешь учесть
👍31😁1🤔1
​​Kotlin Flow: StateFlow vs SharedFlow
Часть №1

Сначала общие факты для понимания задумки авторов библиотеки Kotlin Flow:


• Flow (потоки) — никак не связаны с Android
это библиотека котлина, которая завязано только на корутины (которые тоже являются библиотекой, не связанной с Android SDK)


• основные методы:
collect() - подписаться на данные из потока
emit() - положить значение в поток


• холодный поток — запускает создание элементов каждый раз при вызове collect()

при объявление потока описывается функция-билдер новых элементов, внутри которого вызывается emit()

то есть для каждого подписчика свой поток данных


• горячий поток — не зависит от вызова collect()

то есть для каждого подписчика один и тот же поток данных


• если корутина, запустившая холодный поток, остановлена — поток тоже остановится

но только если внутри есть проверка флага isActive


• collect() и emit() — suspend-функции, выполнение корутины не продолжится, пока они не завершатся

collect() приостанавливает корутину до завершения потока данных
emit() приостанавливает корутину до момента, пока поток данных не будет готов принять элемент (освободится буфер)


• CoroutineContext определяется родительской корутиной, вызвавшей collect(). но его можно сменить с помощью оператора flowOn(CouroutineContext)

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


• зачем нужен буфер?

если речь идет о холодном потоке, то буфер помогает выполнить emit(), не дожидаясь завершения collect()

если речь идет о горячем потоке, то буфер определяет, сколько последних элементов получит новый подписчик, вызвавший collect()
19👍13❤‍🔥1🥰1
Kotlin Flow: StateFlow vs SharedFlow
Часть №2


Общее для StateFlow и SharedFlow:

• это горячие потоки

то есть вызов collect() лишь позволяет собрать данные, которые уже в них есть или поступят в них, но не запускает выполнение какого-либо билдера (как это происходит с холодными потоками)


• они не отслеживает жизненный цикл активити\фрагмента\вьюшки

но их можно запустить в корутинах, которые автоматически остановятся во время вызова onStop(). остановка \корутины вызывает остановку выполнения collect()



Отличия StateFlow и SharedFlow:

• при создании StateFlow нужно указать начальное значение, а при создании SharedFlow — нет

• у SharedFlow можно настроить вместимость буфера, у StateFlow хранится только 1 последний элемент



У SharedFlow два буфера — replayBuffer и extraBuffer:

replay — сколько новый подписчик, вызвавший collect(), получит последних выпущенных элементов

extraBuffer — сколько дополнительно элементов сохранится для "медленного" подписчика, который не успевает выполнять collect()

этот буфер работает в следующей ситуации:
допустим, emit() вызывается каждые 100мс, а collect() выполняется за 150мс

onBufferOverflow — определяет, что делать, когда вызывается emit() при полностью заполненном extraBuffer

сценарии:
SUSPEND (остановить корутину на строчке emit, пока не освободится буфер)
DROPLATEST (удалить последний элемент в буфере)
DROP
OLDEST (удалить самый старый элемент в буфере)

эти сценарии включаются в работу только если есть хотя бы один подписчик, который не успевает выполнять collect(). они не влияют на работу replayBuffer
👍18🔥51
​​Рассказ о стартапе

У меня появилась боль — когда я хотел потратить свободное время, у меня из головы вылетали все идеи

Начал вести список развлечений — оказалось, что без фильтров это очень неудобно

Решил делать приложения (не зря же умею). А чтобы оно не умерло до релиза, решил обернуть это все в стартап и завести отдельный канал, где рассказываю о трудностях в разработке


Если вам интересно, с какими факапами можно столкнуться при запуске своего проекта (от поиска иконок до оформления документов) и пройти путь от зарождения идеи до релиза (или удаления папки с проектом) со мной, то раз в пару дней посты об этом будут появляться там — t.me/find_fun_app

....

в этом канале скоро будут посты о болях с точки зрения кода — почему мне пришлось писать свой LazyList, как сделать кастомную анимацию на Compose и как вкатиться в многомодульную архитектуру, если ты скромный проект на пару тысяч строк (и нужно ли это)
🔥11👌52👍2
​​Kotlin — вложенные и внутренние классы

Кроме наследования, есть два типа отношений между классами:

• вложенные (nested)

class A {
class B
}

создать экземпляр класса B: A.B()

зачем: выделить часть функционала класса A, используемый только с классом A, в отдельный класс B

• внутренние (inner)

class A {
inner class B
}

создать экземпляр класса B: A().B()

зачем: аналогично вложенному, но при этом функционал, выделенный в класс B, зависит от полей класса A


Отличия:

вложенный класс не связан с экземпляром внешнего — они создаются отдельно

внутренние классы содержат ссылку на внешний класс, то есть во внутренний класс неявно передается ссылка на внешний класс

у внутреннего класса есть доступ к public, private и protected полям внешнего класса


Возможная опасность:

передавая куда-то внутренний класс, нужно помнить, что он содержит ссылку на внешний. это может привести к утечки памяти

....

есть примеры из Android SDK, библиотек или личного опыта, когда использование вложенных или внутренних классов было действительно оправдано?
👍21🔥31
​​Почему нельзя писать liveData.observe(this...) во Fragment?


Lifecycle — объект, который отслеживает жизненный цикл (Fragment, Activity....) и передает их в качестве состояния тем, кто на этот жизненный цикл подписался (ViewModel, LiveData...)

LifecycleOwner — интерфейс, который содержит единственный метод — getLifecycle() : Lifecycle

LiveData.observe(...) — может принимать ссылку и на Lifecycle, и на LifecycleOwner


С помощью объекта Lifecycle мы сообщаем LiveData, когда данные должны приходить подписчику, а когда — уже нет


Activity и Fragment реализует интерфейс LifecycleOwner

Получается, что в Activity и Fragment мы спокойно можем просто передавать this в LiveData.observe()?


В Activity можем, а во Fragment — можем, но так делать не стоит


Дело в том, что у Fragment жизненный цикл View и самого фрагмента не жестко связаны:

если фрагмент был убран с экрана (onDetach), но добавлен в backstack, то View может быть уничтожена (onDestroyView), а сам фрагмент — нет

получается, если при подписки на LiveData мы передадим ссылку на фрагмент, а не на viewLifecycleOwner, то возможна ситуация, когда данные придут, а View уже уничтожено — приложение упадет
👍333
​​Многопоточность за 100 слов

Проблема:
есть объект
запускаем два потока
у каждого потока свой кэш, то есть каждый поток создает копию этого объекта, а не работает с ним напрямую

дальше по шагам:
1) первый поток скопировал объект к себе в кэш
2) первый поток изменил поле объекта
3) второй поток скопировал объект к себе в кэш
4) второй поток изменяет это же поле (и не знает об изменениях, сделанных первым потоком!)
5) второй поток записывает изменения в общую память
6) первый поток записывает изменения в общую память

бац — мы потеряли изменения, сделанные во втором потоке. первый поток их перезаписал + второй поток даже не знал, что объект был изменен в первом потоке!


Как этого избежать:

• synchronized — запрещает нескольким потокам работать с объектом (классом) одновременно

перед тем как выполнить участок кода, поток захватывает объект (класс), а другие потоки ждут освобождения объекта (класса)

- synchronized(Object) — захватывает объект
- synchronized fun — можно читать как synchronized(this), захватывает объект
- static synchronized fun — захватывает класс (а не объект)


• volatile — синхронизирует чтение и запись

сработает, если один поток только читает, а другой только пишет

не сработает, если оба потока изменяют объект — потому что операции между чтением и записью не синхронизированы


• atomic

атомарная операция = неделимая операция изменения объекта

с объектами, имеющими атомарные операции изменения (например, увеличение на единицу для AtomicInteger), можно работать в разных потоках без ручной синхронизации

в Java есть атомарные примитивы, коллекции и класс AtomicReference для работы с произвольными классами

....

а нужно ли думать о синхронизации переменной в корутинах?
сработают ли там эти подходы?
🔥26👍174🐳1