Android under the hood


Гео и язык канала: Россия, Русский
Категория: Технологии


Пишу об Android разработке, программировании и о всяких интересных штуках.
while (isAlive) { beHappy(); }
лс: @dmitry_tsyvtsyn

Связанные каналы

Гео и язык канала
Россия, Русский
Категория
Технологии
Статистика
Фильтр публикаций


Полезные штуки.

1) Сделал рефакторинг своего Open Source проекта, очень круто получилось, мне даже в какой-то момент показалось что рефачить код это моё призвание (однозначно нет), большую часть времени ушло на продумывание новой логики и проектирование кода, а это для меня самая любимая часть в разработке. В итоге получилось небольшое многомодульное приложение (3-4 экрана) с интересной логикой + архитектура и всякая такая ерундистика в стиле detekt'а, в общем делайте fork и рекомендуйте друзьям, буду признателен.

2) Переписал статью из своего прошлогоднего Github репозитория и опубликовал на Хабре, статья про интересные технические решения или "фишки", минус только в том, что я писал материал еще в ноябре 2023, а с того времени в моей нейронной жвачке накопилось на порядок больше технической фигни, к сожалению до этого пока не доходят руки, благо на реставрацию их хватает
ссылка на статью

Пишите в комментах ваше мнение и всем хорошего кода.


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

Начну с фотографии. Я довольно давно, ещё в 2020 году, начинал снимать природу на свой старенький телефон Honor 30i, сейчас он уже в негодности к сожалению. Фотки, которые я тогда делал, по правде говоря были неплохие, разве что приходилось удалять лишние гигабайты с неудачными ракурсами. Со временем я начал использовать такие простые механики как приближение, яркость, разные углы при съёмке и тд, а буквально недели две назад решил попробовать редактировать фотографии с изменением таких параметров как экспозиция, контраст, тени, насыщенность, баланс света и тд, чтобы создать более яркий или наоборот тусклый образ, немного отдаленный от реальности. Все свои фотографии я снимаю на обычный Android и ни в коем случае не считаю это профессиональной съёмкой, некоторые из них прикреплены.

Что касается ремесла писателя, то я не совсем имел в виду технические статьи, а скорее тексты более приближенные к литературным произведениям, таким как книги Николая Гоголя и Джека Лондона, по крайней мере для меня эти два человека являются примером безупречного слога. Мой интерес этой областью по большей части был мечтательным, я больше думал что напишу что-то невероятнее, когда читал такие легендарные произведения как Мартин Иден и Мертвые Души, но мои мысли к сожалению не материализовались. Тем не менее я достиг хорошего результата в технических статьях, например считаю своих личным достижением статью по корутинам, а на этой неделе мои руки наконец-то дотянулись до литературной области. Я решился написать небольшое произведение, получилось что-то необычное, но мне лично понравилось, хотя я часто недоволен тем что делаю: ссылка на Telegraph.

Пишите в комментах ваше мнение и всем хороших выходных!


В продолжении поста про архитектуру.

При написании Android прилок я всегда стараюсь придерживаться простому правилу: проще - лучше и опираясь на свой опыт, могу сказать, что в большинстве случаев приходиться пилить экраны, которые делаются буквально на одной ViewModel без всяких самописных архитектур и сложных MVI реализаций (MVIKotlin).

Сейчас кто-то возразит: "чувак, у нас проект на 1000 модулей и много логики, чтобы поделить её мы придумали X паттерн, так что х*йню не неси, знаем на своей шкуре". В качестве аргумента приведу следующее: я работал в достаточно большом банке и как-то залез в один модуль, где был список с вложенными списками списков, на моё удивление я быстро во всём разобрался несмотря на то, что там был простой MVVM. Почему там не возникла каша? А всё потому что, под каждый элемент списка была своя ViewModel, в результате каждый элемент был своего рода отдельным функциональным блоком, который можно было переиспользовать.

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

Пишите в комментах ваше мнение и всем хорошего кода!


Какой у вас уровень?
Опрос
  •   Пока учусь (еще не нашел работу)
  •   Junior
  •   Middle
  •   Senior
  •   Team Lead / Staff
  •   У меня своя компания
  •   Никуда не подхожу
526 голосов


Важность архитектуры.

Можно придумать 5-слойную архитектуру, где всё будет поделено на триллион сущностей, но при этом страдать из-за неочевидных связей в коде, а можно написать приличных размеров приложение с небольшим количеством слоёв и удивляться насколько всё просто и логично. Это всё к тому, что нет никакого магического архитектурного паттерна, следуя которому ваш проект будет настолько суперкрутым, что новые разработчики буквально с 1-го рабочего дня начнут пилить новые фичи, поражаясь насколько всё аху*нно сделано.

Чтобы стало ещё понятнее, приведу такую аналогию: если человек любит захламлять комнаты в своём доме, то сколько новых комнат не выделить ему, везде будет беспорядок. Так же и с кодом, если он пишется как попало, то чем больше сущностей будет в проекте, тем больше вероятность, что этот человек испортит каждую такую сущность и в результате начальная цель архитектурного паттерна "поделить всё на отдельные функциональные блоки" обратится вспять и получится что-то вроде "куски отдельные есть, но что-то с ними явно не так, может ещё поделить?"

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

Пишите в комментах что думаете и всем хорошего кода!


Как можно использовать Dispatchers.Unconfined?

Начну с проблематики, как мы знаем в Android приложениях чаще всего используются архитектурные паттерны, основанные на такой штуке как ViewModel, внутри которой происходит получение данных из различных источников, таких как: Repository, Interactor, UseCase и тд, получение данных должно происходить на background потоках, чтобы случайно не заблочить UI, для этой задачи чаще всего используются диспатчеры из Kotlin корутин, вроде бы всё круто, но есть один нюанс: где должны переключаться потоки, в источниках данных или всё таки во ViewModel...
продолжение в Telegraph'е

Пишите в комментах ваше мнение и всем хорошего кода!

1.3k 0 33 17 11

Кодинг и творчество.

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

Но с другой стороны почему бы не копаться неделями в багах, получая условно свои 200-300к, возможно кому-то и по кайфу такое, но я не могу чувствовать себя полноценно счастливым если постоянно занимаюсь подобными задачами, мне обязательно нужна творческая составляющая, а для этого приходиться смешивать рутину с действительно интересными штуками: например перевести Gradle скрипты на Kotlin DSL при этом добавив удобные Convention плагины с красиво настроенными flavor'ами, упростить архитектуру, в которой полно оверхеда, исправить баг, попутно сделав рефакторинг проблемного места для уменьшения вероятности возникновения новых багов в этом же месте, ну и конечно же при написании новой фичи хорошенько всё продумать, чтобы конечное решение получилось изящным, простым и понятным, и чтобы новый разраб, увидев ваш код, сказал: "Вау, где подвох? Почему всё так просто?!"

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

P.S. Недавно запилил небольшой многомодульный проект по Manual DI, ссылка на Github

Пишите в комментах как вы получаете удовольствие от работы и всем хорошего кода!


Автоформатирование кода или как испортить свой код.

Чаще всего разработчику приходиться читать код своих коллег и зачастую этот код сильно отличается от собственных представлений "правильного" кода, по крайней мере у меня часто возникают вопросы в стиле "зачем здесь var?", "почему нельзя было сделать конкретный тип, а не Any?", а ещё чаще я задаю себе вопрос "что вообще за х*йня тут написана?", как говорится все люди разные и все видят по разному решение одной и той же проблемы, но что ещё важнее написать решение таким образом, чтобы остальные разработчики смогли понять что за черную магию вы натворили.

Для этого как раз и существуют всякие кодстайлы, например официальный от Kotlin, статические анализаторы или подмножество линтеров, например detekt и другие похожие штуки, которые нужны чтобы упорядочить написание кода разными разработчиками / командами / компаниями и даже отдельными OpenSource перцами, в итоге формируется некоторый общий стиль написания кода для целого сообщества языка программирования, это очень круто, я всеми нейронами за такие формирования, но есть такой сомнительный инструмент из разряда линтеров и кодстайлов как автоформатирование кода, ярким примером такой штуки является Gradle таска ktlintFormat из Gradle плагина ktlint'а.

В последующих абзацах я ни в коем случае не хочу сказать что автоформатирование кода это полностью ненужная вещь и что я не вижу в этом инструменте ничего хорошего, это не совсем так, я считаю что автоформатирование кода это хороший инструмент, но только для небольших и рутинных задач - удалить ненужные отступы, отсортировать список зависимостей по алфавиту и всякое такое, но когда дело доходит до форматирования исходников, я выхожу из игры и делаю это не только потому что сильно люблю свой код (и это тоже конечно), самое главное что можно потерять при автоформатировании кода это возможность прокачать умение писать понятный и логичный код, приведу два примера, в первом будет использован статический анализатор detekt, который не пропустит ваш код если с ним что-то не так, во втором будет использоваться автоформатирование кода через специальную Gradle таску, в ktlint'е как я уже упоминал она называется ktlintFormat:

1) Пишем какой-то код, пропускаем через статический анализатор detekt, видим что проект не собирается, исправляем, снова пропускаем через detekt, опять ничего не собирается, повторяем, и после десятка / сотни таких итераций разработчик начинает привыкать к правильному кодстайлу и вырабатывает автоматизм написания такого кода

2) Пишем какой-то код, запускаем автоформатирование через Gradle таску ktintFormat, всё ок, пишем другой код, также запускаем автоформатирование, опять всё ок, при таком решении проблемы нам со временем становится всё равно на то, что мы пишем, ведь Gradle таска сама отформатирует код, зачем напрягать жвачку, когда есть волшебная кнопка?

Вдобавок, автоформатирование кода - это нисколько не магическая утилита, способная превратить запутанный и нелогичный код во что-то более менее понятное, что ещё хуже эта штука формирует иллюзорное восприятие: можно написать что угодно, все равно отформатируется, более логичным решением будет способ заставить разработчика задуматься когда он пишет код, например используя какой-нибудь статический анализатор, кричащий "чувак, здесь ерунда какая-то, исправляй", в итоге, когда начинаешь задумываться о стиле написания своего кода выстраиваются правильные ориентиры и появляется стремление к более осознанному проектированию классов / функций и т.д.

Пишите в комментах ваше мнение и всем хорошего кода!


Пару слов о value классах.

В объектно-ориентированных языках нам приходиться оперировать классами, это очень удобно в прикладных задачах, например: нужно реализовать логику корзины с продуктами, создаём новый класс для корзины и продукта, кладём в корзину нужные товары и отправляем на оплату, всё очень просто и логично, но предположим, что нам нужно создать класс для прогресса, где корректными значениями считаются числа от 0 до 100, у нас появляется компромисс использовать класс, в котором инкапсулированы проверки на корректность, но при этом есть дополнительный оверхед вследствие работы сборщика мусора или использовать...
продолжение в Telegraph'е

Пишите в комментах ваше мнение и всем хорошего кода!


Kotlin Coroutines internals.

Закончил наконец-то статью на Хабре по корутинам, получилась просто бомба! Никогда ещё не писал настолько объёмный и сложный материал, ушло около 2-х недель из которых половину времени я прокрастинировал и в какой-то момент даже думал что брошу это дело, но благо всё обошлось и статья, отредактированная практически до мелочей уже опубликована!

Теперь по материалу статьи, я долго пытался разобраться в исходниках корутин, на это ушло буквально десятки вечеров занимательного чтива под названием "исходный код", мне ни раз рекомендовали книжку Kotlin Coroutines: Deep Dive, но удивительно устроен мозг человека, вопреки лени порой выбор падает на более сложный путь и вместо книжки я выбрал дебаг с покраснением глаз, ладно это всё лирика, в статье описан принцип работы корутин под капотом и базовые кирпичики такие как диспатчеры, контекст, EventLoop, Continuation и другие важные штуки, в общем не пожалеете если потратите часик на статью)

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

Kotlin Coroutines под капотом на Хабре

Пишите в комментах ваше мнение и всем хорошего кода!

2.6k 1 82 15 62

Пару слов о Kotlin Compiler'е.

Сейчас уже не новость что Kotlin может компилироваться не только в Java байт-код, но и в JavaScript, Objective-C и даже в нативный код под разные архитектуры, всё это возможно благодаря архитектуре компилятора, построенной на простой идеи:

1) есть Frontend часть, которая компилирует исходной код программы на Kotlin в некоторое промежуточное представление или IR (intermediate representation), удобное для дальнейшей обработки

2) есть Backend часть, которая берёт промежуточное представление или IR и превращает его в код конкретной платформы: для JVM - это байт-код, для iOS - Objective-C, для какой-нибудь нативной утилиты под Linux - ассемблер или машинный код

В такой схеме IR независим от конкретной платформы, а следовательно есть пространство для создания всяких штук, которые могут его менять при компиляции кода, например компиляторные плагины, такие как Compose Compiler.

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

1) LLVM - набор компиляторов для генерации нативного кода под определённую архитектуру, имеет своё промежуточное представление LLVM IR, используется Kotlin'ом для Native таргета.

2) Rust - один из самых высоко оптимизированных компиляторов и языков соответственно, имеет несколько уровней промежуточного представления, компилится в нативный код и WebAssembly, язык низкого уровня того же назначения что и JavaScript

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

Пишите в комментах ваше мнение и всем хорошего кода!


Пару слов о Backend Driven UI.

Backend Driven UI или как часто сокращают BDUI - это библиотека, фреймворк или проще говоря штука для создания динамического интерфейса на основе ответа бэкенда без изменения кода Android / iOS приложения.

Небольшой пример: в приложении есть экран с детальной информацией об игре, вам сказали что нужно добавить карусельку "похожие игры", вы идёте на экран об игре, добавляете карусельку, пересобираете apk / aab, публикуете в магазине приложений и так каждый раз, когда появляются новые изменения в дизайне деталки, в целом это нормально, обычно такие изменения заранее планируются и заливаются в пределах релиза.

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

Пишите в комментах ваше мнение и всем хорошего кода!


Dispatchers.Main под капотом.

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

Любая корутина создаёт Continuation объект и передаёт его suspend функциям, те в свою очередь вызывают метод continuation.resumeWith() когда нужно вернуть результат или ошибку, это можно сравнить с обычными callback'ами, только с тем отличием, что в Kotlin Coroutines всё реализовано под капотом + есть дополнительные механизмы в стиле Structured Concurrency.

Но чтобы переключать потоки знания общего принципа достаточно, давайте задумаемся на минутку, что надо сделать, чтобы результат callback'а или Continuation'а в нашем случае был на другом потоке?

Да конечно вызвать Continuation.resumeWith() на другом потоке:

// IO thread
val ctx = coroutineContext
// метод isDispatchNeeded проверяет есть ли смысл переключать корутину на главный поток, чтобы не делать лишних переключений, если корутина и так находится на главном потоке
if (mainDispatcher.isDispatchNeeded(ctx)) {
// метод dispatch выполняет блок кода на главном потоке
mainDispatcher.dispatch(ctx) {
// Main thread
continuation.resumeWith(...)
}
} else {
// уже находимся на главном потоке, продолжаем выполнение
continuation.resumeWith(...)
}

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

Вернёмся к главному потоку, в Android такой Dispatcher или как он назван в исходниках HandlerContext реализован с помощью Handler'а:

// реализация для Dispatchers.Main
override fun isDispatchNeeded(...) = true

// реализация для Dispatchers.Main.immediate
override fun isDispatchNeeded(...): Boolean {
// сравнивает Looper текущего потока с главным
// handler.looper это Looper.getMainLooper()
return Looper.myLooper() != handler.looper
}

override fun dispatch(
context: CoroutineContext,
block: Runnable
) {
// handler.post() выполняет код на главном потоке
if (!handler.post(block)) { ... }
}

Dispatchers.Main не делает проверок на главный поток, поэтому если написать несколько вложенных viewModelScope.launch() вызовов всегда будет происходить переключение текущего потока на главный через Handler.post(), даже если код уже выполняется на главном потоке.

Чтобы избежать ненужных переключений используйте Dispatchers.Main.immediate, в таком случае если код уже выполняется на главном потоке, метод isDispatchNeeded() вернёт false и dispatch() не будет вызван.

P.S. Хочу порекомендовать крутой канал с авторским контентом и полезными материалами в области Android разработки @dolgo_polo_dev

Пишите в комментах ваше мнение и всем хорошего кода!

Telegraph версия

1.6k 0 26 12 23

Пару фактов о StateFlow и SharedFlow.

1) В отличии от простой реализации паттерна Observer, как это было в LiveData например, в StateFlow / SharedFlow механика основана на корутинах.

Всё начинается с подписки данных через collect, где запускается бесконечный цикл, который при отсутствии новых значений переводит корутину в SUSPEND состояние и что очень важно сохраняет ссылку на Continuation.

Думаю не секрет, что Continuation это обычный callback, который неявно передаётся в каждую suspend функцию и нужен для выхода из SUSPEND состояния.

При добавлении новых значений в StateFlow или SharedFlow извлекается сохранённый Continuation объект, у которого вызывается метод resume(), корутина выходит из SUSPEND состояния и подписчик получает новое значение или значения если это SharedFlow.

2) Состояния подписчиков, такие как ссылки на Continuation объекты, хранятся в массиве слотов AbstractSharedFlowSlot, по большей части это сделано для переиспользования общей логики, так как у StateFlow и SharedFlow общий родитель - AbstractSharedFlow.

3) Все возможные операции изменения состояния сделаны через атомарные конструкции (StateFlow) или synchronized блоки (SharedFlow), поэтому это потокобезопасные штуки.

4) StateFlow при изменении своего единственного значения сравнивает его с новым equals() методом, если значения равны, ничего не делает.

5) SharedFlow хранит значения в буфере, размер которого настраивается двумя параметрами: replay и extraBufferCapacity, первый отвечает за основную часть буфера, значения из этой части будут прилетать новым подписчикам, например если replay = 3, то при добавлении нового подписчика, в него придут 3 значения из буфера, второй параметр только добавляет дополнительное место для буферизации значений.

6) SharedFlow нельзя превратить в StateFlow если указать размер буфера нулевым, так как эта штука работает немного по другому, предположим у нас есть два подписчика и какой-нибудь источник в котором происходит генерация новых значений, все хорошо до тех пор пока один из подписчиков вдруг не окажется очень медленным и в случае нулевого размера буфера новое значение будет добавлено только когда все подписчики получат предыдущее, вызов SharedFlow.emit() в таком случае переходит в SUSPEND состояние.

Если буфер ненулевой то значения будут добавляться до тех пор пока он не заполнится.

7) Помимо перехода источников SharedFlow в SUSPEND состояние при переполнении буфера, есть ещё две интересные стратегии:

BufferOverflow.DROP_OLDEST - удаление из буфера самых старых значений

BufferOverflow.DROP_LATEST - пропуск новых значений, важно что здесь не происходит никаких операций с буфером

В обеих случаях SharedFlow.emit() успешно завершается и не переходит в SUSPEND состояние.

Пишите в комментах ваше мнение и всем хорошего кода!

1.4k 0 24 14 24

Многомодульность в Android приложениях.

Недавно я привёл в порядок мой небольшой Pet проект и поделил его на несколько модулей, получилось очень даже неплохо, что мне захотелось черкануть парочку мыслей на этот счёт:

1) В проекте должен быть как минимум один модуль, являющийся точкой входа в приложение, если вы не пишите библиотеку конечно, чаще всего его называют app модулем, в таком модуле обычно происходит настройка параметров для сборки aab / apk артефактов, подключение всех остальных модулей, построение DI графа и графа навигации, а также тут живёт наследник Application класса с главной Activity.

2) Построение DI графа или графа навигации происходит в app модуле, который собирает всё воедино, например если для DI используется Dagger, то в дочерних модулях будут свои Dagger компоненты, app модуль в данном случае будет иметь общий Dagger компонент, включающий в себя остальные, таким образом получится целостный граф зависимостей, где без проблем можно пробросить Application Context или другие корневые штуки в дочерние модули.

3) Для уменьшения времени сборки проекта и повышения инкапсуляции модуля можно поделить классы на реализацию и интерфейс: реализацию ограничить internal модификатором, а интерфейс сделать публичным, в таком случае при изменении кода реализации будет пересобран только один модуль, содержащий эту реализацию, если конечно не был изменён интерфейс.

4) Gradle позволяет переиспользовать настройки скриптов с помощью Convention Plugins, например можно создать шаблон с уже прописанными настройками для Android: compileSdk, minSdk, targetSdk, compileOptions и тд, а потом просто подключать его в модулях, как обычный Gradle плагин, это позволит уменьшить количество шаблонного кода.

Всё что я описал здесь можете глянуть на Github'е, если знаете как сделать правильнее, жду PR в репозиторий)

P.S. На прошлой неделе появилось сообщество Mobile Broadcast в Барнауле, пока не было встреч, но если в группе есть люди из моего города, вступайте, если вы не знаете прикола сообщества, то это лайтовые встречи с обсуждением тем из Android / iOS разработки, а также смежных областях, таких как дизайн например.

Пишите в комментах ваше мнение и всех с первыми деньками лета!

1.3k 0 18 26 16

Итоги недели.

Настал переломный момент в моей жизни, когда хочется просто взять уже готовые библиотеки и начать писать проект, а не думать о самописных решениях, хотя раньше я прилично заморачивался в этом плане: писал свою навигацию на вьюшках, делал MVI на битовых масках и тд, в общем современный стэк добро пожаловать: Room KSP / Jetpack Compose + MVI / Jetpack Compose Navigation.

Парочка проблем с которыми я столкнулся на новом пути:

1) Плагин KSP должен быть совместим с Kotlin версией:

// версия Kotlin 1.9.0
id("com.google.devtools.ksp") version "1.9.0-1.0.13"

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

2) MVI в Jetpack Compose достаточно легко реализуется из коробки за исключением некоторых нюансов, связанных с жизненным циклом Composable функций, которые могут вызываться по множеству раз, поэтому для одноразовых действий в стиле "перейти на следующий экран" или "показать диалог" не совсем подходит MutableSharedFlow, так как он сохраняет последнее действие в replayCache, что может случайно привезти к его повторному выполнению, поэтому я решил использовать Channel'ы: небольшой пример на Github.

UPD. На самом деле можно сделать через MutableSharedFlow, пример

3) Jetpack Compose Navigation позволяет передавать только примитивные типы и строки в качестве аргументов: как я не старался передать Parcelable / Serializable объект, ничего не получилось, можно конечно придумать своё собственное строковое представление или использовать JSON сериализацию / десериализацию, но это уже дополнительные манипуляции, если знаете как сделать проще напишите в комментах.

P.S. Недавно вышло легендарное продолжение отечественного комикса Майор Гром: Игра, которого я ждал почти целый год, однозначно рекомендую посмотреть! Это если что не реклама, а реал experience хорошего кино.

Пишите в комментах ваше мнение и всем хорошего кода!


В память об RxJava.

Kotlin Coroutines уже стали де-факто стандартом во многих современных проектах, но тем не менее RxJava продолжает свою жизнь и даже если все проекты будут полностью переписаны на корутины, этой библиотеке явно поставят памятник как "одной из самых мощных реализаций реактивного программирования".

Рассмотрим внутрянку RxJava, которая полностью построена на паттерне Декоратор (чек и чек):

class JustObservable(
    private val value: Int
) : Observable() {

    override fun subscribeActual(observer: Observer) {
        observer.onNext(value)
        observer.onComplete()
    }

}

class MapObservable(
    private val source: Observable,
    private val transform: (Int) -> Int
) : Observable() {

    override fun subscribeActual(observer: Observer) {
        source.subscribe {
            observer.onNext(transform(it))
        }
    }

}

MapObservable(
    source = JustObservable(7),
    transform = { it * 2 }
).subscribe { result ->
    println(result)
}

1) Каждый оператор в RxJava реализован как отдельный класс, наследующий некоторый общий интерфейс, в данном примере Observable.

2) Оператор может принимать данные от предыдущих операторов или производить их сам.

3) Observable.subscribe под капотом вызывает Observable.subscribeActual, который по цепочке подписывается на все предыдущие операторы вплоть до JustObservable, где начинают производиться данные.

4) Как только JustObservable начинает производить данные, в обратном порядке начинают срабатывать Observer'ы: первым получит данные MapObservable, пропустит их через лямбду и передаст следующему по цепочке Observer'у, в этом примере он является конечным и просто выводит результат в консоль.

К сожалению такая организация операторов приводит к очень большому количеству классов, что увеличивает размер библиотеки в отличии от Coroutines Flow, где операторы сделаны небольшими inline функциями.

Пишите в комментах ваше мнение и всем хорошего кода!


Как работает remember из Jetpack Compose.

Composable функции работают не совсем так, как обычные вызовы методов: они могут перезапускаться по множеству раз в зависимости от входных данных или при изменении состояния, поэтому нельзя хранить критичные данные в локальных переменных, а следует оборачивать их в вызов remember:

@Composable
fun CoffeeListScreen() {
val repository = remember {
CoffeeRepository()
}
// ...
}

Функция remember сохраняет вычисляемое лямбдой значение в слот-таблицу для дальнейшего к нему доступа при последующих вызовах Composable функции.

Слот-таблица - некоторый контейнер, используемый для хранения данных Composable дерева: вызов Composable функции создаёт группу в таблице, в пределах таких групп создаются слоты, те самые данные, которые сохраняются при повторном вызове функции в дереве.

В качестве сохраняемого значения remember может быть любой подтип Any? или наследники класса RememberObserver:

remember {
// callback'и для отслеживания жизненного цикла значений в слот-таблице, чтобы реализовать некоторые side effects
object : RememberObserver {

// значение было записано в слот-таблицу, обычно вызывается только 1 раз при первом вызове Composable функции, используется при вызове корутины в LaunchedEffect
override fun onRemembered() {}

// не получилось записать значение в слот-таблицу, возможно какая-то ошибка, используется для отмены корутины в LaunchedEffect
override fun onAbandoned() {}

// значение было удалено из слот-таблицы, так как Composable функции уже нет в дереве, используется для выполнение onDispose действия в DisposableEffect или для отмены корутины в LaunchedEffect
override fun onForgotten() {}

}
}

Существует вариант remember(key1, ..., calculation), который повторно вычисляет значение если был изменён один из ключей, как и вычисляемое значение ключи также хранятся в слот-таблице.

Пишите в комментах ваше мнение и всем хорошего кода!


Пару слов о библиотеке Jetpack Compose Navigation.

Jetpack Compose Navigation - библиотека, написанная джедаями из Google для навигации между Composable функциями, изначательно эта штука использовалась для навигации между фрагментами через построение графа в xml редакторе, может кто помнит этот ад)

Я собрал несколько полезных фактов об этой библиотеке:

1) NavHost - это всего лишь контейнер, отображающий нужную Composable функцию в виде экрана или диалога по подписке на StateFlow у одного из навигаторов.

2) Навигатор - штука, отвечающая за навигацию определённого типа NavDestination, это может быть Activity, Composable функция и тд, имеет свой стэк навигации

В случае с Compose навигатора три: ComposeNavGraphNavigator, ComposeNavigator и DialogNavigator: первый отвечает за навигацию между отдельными графами, второй за навигацию между Composable функциями, а третий - за диалоги.

3) Навигаторы не используются напрямую, а содержатся в NavHostController'е, который имеет свой стэк навигации из NavBackStackEntry сущностей для отслеживания всех навигационных переходов.

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

4) Дерево навигации или NavGraph - это специальный контейнер, в котором определяются NavDestination сущности: Composable функции, дочерние графы и тд.

Создание дочерних графов возможно благодаря тому, что NavGraph наследует NavDestination, для понимания почему так: чек и чек.

5) Поиск по дереву навигации всегда происходит по строке, которая приводится к единому формату - диплинку, даже если вы прописали только название для экрана:

"android-app://androidx.navigation/$route"

6) Чтобы сохранить ViewModel'и любой экран создаёт свой собственный ViewModelStore, который кладётся в родительский из Activity или Fragment, в качестве ключа используется NavBackStackEntry.id, рандомное UUID значение.

Пишите в комментах ваше мнение и всем хорошего кода!


Пару фактов о JVM стэке.

Любая последовательность Kotlin / Java команд выполняется на некотором потоке и ни для кого не секрет что каждый поток имеет свой персональный стэк, некоторую область памяти которую выделяет виртуальная машина Java.

Ниже я привёл несколько полезных фактов об этой магической штуке:

1) JVM стэк, как я уже упомянул, выделяется под каждый поток и предназначен для хранения некоторых кусков памяти называемых фреймами.

Фрейм является отдельным элементом стэка и используется для хранения локальных переменных, стэка операндов и cсылки на специальную структуру данных, содержащую информацию о текущем методе.

Возьмём для примера следующую программу:

fun main() {
println("Hello, Universe!")
}

При вызове метода main будет создан фрейм, метод в таком случае называется текущим, так как на данный момент выполняется его код, затем вызывается метод println для которого будет создан новый фрейм, теперь он станет текущим и так далее, после завершения выполнения метода println его фрейм будет удалён.

2) Фреймы могут содержать стэк операндов для вычисления команд байт-кода, это как регистры процессора, чтобы вычислить что-то нужно сначала положить операнды в регистр или в случае JVM в стэк операндов.

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

3) Размер стэка зависит от реализации виртуальной машины Java, для современных версий Android это около 8000-8200KB или ~8MB.

4) Если стэк переполнится JVM упадёт с исключением StackOverflowError.

5) При создании нового потока Thread можно указать в конструкторе размер стэка в битах, новый размер будет зависеть от внутренних политик JVM, например на моём телефоне удалось получить стэк размером от 2MB до 134MB.

6) Для уменьшения загрузки стэка стоит поменьше использовать рекурсию + в Kotlin были придуманы inline functions для встраивания кусков куда без вызова метода.

P.S. Хочу немного похвастаться тем, что написал первую полноценную статью!

Пишите в комментах ваше мнение и всем хорошего кода!

Показано 20 последних публикаций.