Многопоточность за 100 слов Проблема: есть объект запускаем два потока у каждого потока свой кэш, то есть каждый поток создает копию этого объекта, а не работает с ним напрямую дальше по шагам: 1) первый поток скопировал объект к себе в кэш 2) первый поток изменил поле объекта 3) второй поток скопировал объект к себе в кэш 4) второй поток изменяет это же поле (и не знает об изменениях, сделанных первым потоком!) 5) второй поток записывает изменения в общую память 6) первый поток записывает изменения в общую память бац — мы потеряли изменения, сделанные во втором потоке. первый поток их перезаписал + второй поток даже не знал, что объект был изменен в первом потоке! Как этого избежать: • synchronized — запрещает нескольким потокам работать с объектом (классом) одновременно перед тем как выполнить участок кода, поток захватывает объект (класс), а другие потоки ждут освобождения объекта (класса) - synchronized(Object) — захватывает объект - synchronized fun — можно читать как synchronized(this), захватывает объект - static synchronized fun — захватывает класс (а не объект) • volatile — синхронизирует чтение и запись сработает, если один поток только читает, а другой только пишет не сработает, если оба потока изменяют объект — потому что операции между чтением и записью не синхронизированы • atomic атомарная операция = неделимая операция изменения объекта с объектами, имеющими атомарные операции изменения (например, увеличение на единицу для AtomicInteger), можно работать в разных потоках без ручной синхронизации в Java есть атомарные примитивы, коллекции и класс AtomicReference для работы с произвольными классами .... а нужно ли думать о синхронизации переменной в корутинах? сработают ли там эти подходы?
Dolgo.Polo Dev Android
4
подписчика
Интересные ситуации в android-разработке из личного опыта и их обсуждение…
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, библиотек или личного опыта, когда использование вложенных или внутренних классов было действительно оправдано?
Почему нельзя писать 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 уже уничтожено — приложение упадет
Kotlin Flow: StateFlow vs SharedFlow Часть №2 Общее для StateFlow и SharedFlow: • это горячие потоки то есть вызов collect() лишь позволяет собрать данные, которые уже в них есть или поступят в них, но не запускает выполнение какого-либо билдера (как это происходит с холодными потоками) • они не отслеживает жизненный цикл активити\фрагмента\вьюшки но их можно запустить в корутинах, которые автоматически остановятся во время вызова onStop(). остановка \корутины вызывает остановку выполнения collect() • при создании нужно указать начальное значение Отличия StateFlow и SharedFlow: • у SharedFlow можно настроить вместимость буфера, у StateFlow хранится только 1 последний элемент У SharedFlow два буфера — replayBuffer и extraBuffer: • replay — сколько новый подписчик, вызвавший collect(), получит последних выпущенных элементов • extraBuffer — сколько дополнительно элементов сохранится для "медленного" подписчика, который не успевает выполнять collect() этот буфер работает в следующей ситуации: допустим, emit() вызывается каждые 100мс, а collect() выполняется за 150мс • onBufferOverflow — определяет, что делать, когда вызывается emit() при полностью заполненном extraBuffer сценарии: SUSPEND (остановить корутину на строчке emit, пока не освободится буфер) DROPLATEST (удалить последний элемент в буфере) DROPOLDEST (удалить самый старый элемент в буфере) эти сценарии включаются в работу только если есть хотя бы один подписчик, который не успевает выполнять collect(). они не влияют на работу replayBuffer
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()
Чек-лист профессионального программиста Это свод правил, выработанный за годы фриланса Когда соблюдаю их, приложение получается удобным для юзера Да, этот список не сделает из вас сеньора, как и не делает им меня. Но это как с мужем-на-час, который не только починил кран, но и убрал за собой рабочее место — сразу видно, крутой • сортировать списки если список выводится пользователю, нужно не забыть понятную сортировку - по дате создания или алфавиту, если нет других заданных параметров • проверять нули, пустые строки и нуллы, количество элементов в списке продумайте, что увидит юзер. например, если список пуст, не забыть вывести "оëой, данных пока нет" и не выводить "ваша скидка 0% !!!" • поднимать ошибку по стеку в обертке если вы запрашиваете Long из базы данных, то возвращать -1 в случае ошибки не лучшая идея. Лучше вернуть Success(value) или Error(errorCode) • использовать варианты toDoubleOrNull() вместо toDouble() никогда не надейтесь, что преобразование строки в число пройдёт успешно а вдруг кто-то подсунет строку с запятой вместо точки? приложение крашнется с ошибкой NotNumber, а юзер умрёт от инфаркта лучше добавить лишнюю проверку на null (или обернуть в try-catch) • сообщать о загрузке если контент загружается больше 500мс, отобразите индикатор загрузки или скелет будущего контента • помнить, что юзер может закрыть приложение в любой момент, но операция должна завершиться юзер не обязан ждать, пока данные сохранятся - может свернуть приложение, заблокировать телефон, уронить его в лужу - операция должна выполниться в фоне
GET vs POST vs PUT в Retrofit 2 — уровень "Мобильный разработчик"
GET vs POST vs PUT в Retrofit 2 — уровень "Мобильный разработчик" Вероятно, если вы человек-бекендер, то нужно углубляться сильнее. А для нас достаточно следующих фактов: Указывая аннотации @GET, @POST или @PUT, вы определяете, в каком виде будут переданы параметры запроса на сервер • GET Параметры запроса вставляются в URL запроса с помощью аннотации Query(parameterName) в формате key=value Параметры добавляются после знака "?" Длина URL ограничена 2048 символами Например, на выходе можете получить такой запрос: https://www...
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" — результат непредсказуем Ответ на заголовок поста: пора начинать разбираться в многопоточности, как только в проекте появляется два и более потоков, работающих с общими переменными .... с чего стоит начать, если хочешь понять эту вашу многопоточность вдоль и поперек?
Android Разработка - Общая ViewModel
Не самая очевидная вещь: один экземпляр ViewModel может быть доступен из нескольких фрагментов или активитей Например, вам нужна аватарка пользователя в двух фрагментах Вы можете: — для каждого фрагмента создать свою ViewModel и прописать в них обоих одинаковую логику получения аватарки. При этом в каждой из этих вьюМоделей будет своя MutableLiveData<Bitmap>, хранящая картинку — создать одну вьюМодел и получить к ней доступ из обоих фрагментов Фокус в том, что вьюМодел хранится у кого-то. И этот кто-то - ViewModelStoreOwner...
Каналы уведомлений в Android (Development, Java, Kotlin)
Начиная с версии Android 8 (API 26), перед тем как вывести любое уведомление в шторку, нужно создать Notification Channel (далее - канал), а после этого указать channelId при создании объекта Notification (собстна, самого уведомления) Каналы нужны для того, чтобы пользователь мог в настройках (Приложения -> О приложении -> Уведомления) отключить только ненужные сообщения от приложения И это чертовски удобно...
Главная цель архитектуры или зачем столько страданий (Мобильная разработка)
Можно выкупить принципы всех архитектур, так и не поняв, зачем столько геморроя © Аристотель, 300кк век до н. э. Маленькие приложения могут прожить без архитектуры. Первые 5-7 приложений я написал, совершенно не задумываясь о потребности в строгом паттерне. Каждая статья про архитектуру мобильного приложения рождала мысль: "И зачем это усложнение - будем писать в три раза больше кода, чтобы код стал понятнее, вы серьезно?" А потом начались проекты c десятками тысяч строк. И мне показалось, что понял, зачем нужны MVC, MVVM, MVI, MVP...
Волшебство LiveData - Android Разработка (Development)
LiveData - это класс, работающий по принципу Observable. Поэтому сначала вспомним, какими функциями занимается Observable-класс: 1. Хранит объект какого-то типа. Например, Observable<Integer> - хранит объект типа Integer 2. Может получать новый объект. Например, вот так: Observable .post(123) //готово, мы отдали на хранение новый объект типа Integer 3. Может отдавать всем подписчикам хранимый объект. Подписаться на такой Observable можно в любом месте, вызвав Observable.observe() Обычно Observable нужен для такого сценария: 1...