cookie

Ми використовуємо файли cookie для покращення вашого досвіду перегляду. Натиснувши «Прийняти все», ви погоджуєтеся на використання файлів cookie.

avatar

Android under the hood

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

Більше
Рекламні дописи
385
Підписники
+524 години
+177 днів
+3430 днів

Триває завантаження даних...

Приріст підписників

Триває завантаження даних...

Фото недоступнеДивитись в Telegram
Kotlin Coroutines internals. Закончил наконец-то статью на Хабре по корутинам, получилась просто бомба! Никогда ещё не писал настолько объёмный и сложный материал, ушло около 2-х недель из которых половину времени я прокрастинировал и в какой-то момент даже думал что брошу это дело, но благо всё обошлось и статья, отредактированная практически до мелочей уже опубликована! Теперь по материалу статьи, я долго пытался разобраться в исходниках корутин, на это ушло буквально десятки вечеров занимательного чтива под названием "исходный код", мне ни раз рекомендовали книжку Kotlin Coroutines: Deep Dive, но удивительно устроен мозг человека, вопреки лени порой выбор падает на более сложный путь и вместо книжки я выбрал дебаг с покраснением глаз, ладно это всё лирика, в статье описан принцип работы корутин под капотом и базовые кирпичики такие как диспатчеры, контекст, EventLoop, Continuation и другие важные штуки, в общем не пожалеете если потратите часик на статью) Ну и конечно же убедительная просьба, делитесь статьёй со своими коллегами, друзьями, знакомыми, ссылка на статью: Kotlin Coroutines под капотом на Хабре Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
🔥 33👍 3 3👏 1
Фото недоступнеДивитись в Telegram
Пару слов о 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 - если вы не задумывались, то байт-код это тоже своего рода промежуточное представление, которое исполняется на разных виртуальных машинах, заточенных под свои платформы. Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
👍 10
Фото недоступнеДивитись в Telegram
Пару слов о Backend Driven UI. Backend Driven UI или как часто сокращают BDUI - это библиотека, фреймворк или проще говоря штука для создания динамического интерфейса на основе ответа бэкенда без изменения кода Android / iOS приложения. Небольшой пример: в приложении есть экран с детальной информацией об игре, вам сказали что нужно добавить карусельку "похожие игры", вы идёте на экран об игре, добавляете карусельку, пересобираете apk / aab, публикуете в магазине приложений и так каждый раз, когда появляются новые изменения в дизайне деталки, в целом это нормально, обычно такие изменения заранее планируются и заливаются в пределах релиза. Проблема тут только в частоте этих самых изменений, если... продолжение в Telegraph'е Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
👍 7👎 4🔥 4😐 3
Фото недоступнеДивитись в Telegram
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 Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
🔥 15👍 4🥰 1
Где проведем первую встречу?Anonymous voting
  • FreeMan's
  • Вилка Ложка
  • Шашлыкoff
  • Другой вариант (напишите ниже)
0 votes
Фото недоступнеДивитись в Telegram
Пару фактов о 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 состояние. Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
👍 19🔥 3
Фото недоступнеДивитись в Telegram
Многомодульность в 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 разработки, а также смежных областях, таких как дизайн например. Пишите в комментах ваше мнение и всех с первыми деньками лета!
Показати все...
👍 15
Итоги недели. Настал переломный момент в моей жизни, когда хочется просто взять уже готовые библиотеки и начать писать проект, а не думать о самописных решениях, хотя раньше я прилично заморачивался в этом плане: писал свою навигацию на вьюшках, делал 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 хорошего кино. Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
Фото недоступнеДивитись в Telegram
В память об RxJava. Kotlin Coroutines уже стали де-факто стандартом во многих современных проектах, но тем не менее RxJava продолжает свою жизнь и даже если все проекты будут полностью переписаны на корутины, этой библиотеке явно поставят памятник как "одной из самых мощных реализаций реактивного программирования". Рассмотрим внутрянку RxJava, которая полностью построена на паттерне Декоратор (чек и чек):
class JustObservable(
    private val value: Int
) : Observable<Int>() {

    override fun subscribeActual(observer: Observer<in Int>) {
        observer.onNext(value)
        observer.onComplete()
    }

}

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

    override fun subscribeActual(observer: Observer<in Int>) {
        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 функциями. Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
🔥 8
Фото недоступнеДивитись в Telegram
Как работает 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), который повторно вычисляет значение если был изменён один из ключей, как и вычисляемое значение ключи также хранятся в слот-таблице. Пишите в комментах ваше мнение и всем хорошего кода!
Показати все...
16👍 6🔥 5
Оберіть інший тариф

На вашому тарифі доступна аналітика тільки для 5 каналів. Щоб отримати більше — оберіть інший тариф.