Русский
Русский
English
Статистика
Реклама

Архитектура android-приложений

История про боль и как мы ее исправляем

17.08.2020 08:11:16 | Автор: admin

Представлюсь, Малюгин Платон Android Lead в Dejavoo Systems. Эта история про нашу "боль" с которой мы боремся уже год и эволюцию нашей архитектуры. Основной профиль кассовые терминалы для ритейлеров и ресторанов, поэтому многое завязано на особенности индустрии.


В любом случаи изменения архитектуры для приложения, не только усложняет его, но увеличивает количество документации и требует поддержки. Поэтому стоит подумать, а нужно ли на это делать сейчас? Достаточно накопленного опыта и количество блокирующих задач?


Почему сейчас? Можно же дальше страдать и как можно меньше трогать этот экран, раз работает, зачем ломать то, что работает сейчас? Все просто, накопилось большое количество блокирующих задач, для которых требуется переделать текущую структуру заказа, на более сложную, но гибкую. Без переделок не обойтись.


Описание проблемы


Используем одну Activity для заказа, которая поддерживает версию для планшета и для телефона. Нам проще работать с диалогами (у нас диалоги это Activity), а их очень много на этом экране и дублировать не хотелось бы.


Вот так выглядит версия для планшета:


Версия для планшета


Версию для телефонов собираем из тех же элементов, но с другим навигатором.


Версия для телефона


Опишу из чего состоит состоит экран:


  • Items фрагмент
  • Departments фрагмент
  • Line Items и Amount фрагмент
  • Order Parameters это отдельная вьюшка

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


Текущая архитектура


Текущая архитектура


Изначально допустил ошибку и поместил заказ в презентер, только сохранение или изменение вызывает Use case, который обновляет кэш и БД. Нам важно чтобы операции выполнялись последовательно. Например, у заказа есть депозит, при превышении его нужно показать пользователю, что сумма превышена и предложить увеличить депозит.


Выполнение в главном потоке гарантируют, что операции выполняется последовательно, конечно все выполняет довольно быстро, больше всего времени занимает уведомление всех остальных фрагментов, так как главный поток, проседает общая производительность. Пришлось добавлять костыли, например сумма заказа пересчитывается в отдельном потоке (через Use case).


Подытожим:


  • В презентере много логики, которой не должно быть
  • Много выполняется в главном потоке, хотя стоит вынести в отдельный поток
  • Уведомляем только об изменении заказа и каждом объекту, нужно самому обработать что изменилось, так что логика может дублироваться
  • В презентере, не получится просто добавить дополнительный запроса к кэше и к БД
  • Презентер отвечающий на заказ, на экране заказа, становится "массивным" презентром
  • Работаем напрямую с явными структурами, а не через абстракции

Обновление архитектуры


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


  • Не делаем новые бизнес задачи
  • Можно "перегореть" (чистая психология)
  • Сделаем такое, что потребуется переделать
  • Поддерживать обе версии, пока окончательно не перейдем

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


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


  1. Спроектировать архитектуру и добавить абстракции для полноценной работы
  2. Адаптировать новую архитектуру под структуру старого заказа
  3. Добавить поддержку структуру нового заказа

После обсуждений появились общие требования


  • Все можно поделить на небольшие куски и поделить в команде
  • Каждый новый релиз (у нас цикл релиза 2 недели) получал новые изменения, которые используются
  • Должны быть написаны тесты
  • Все вычисления должны производится отдельном потоке
  • Получение уведомлений в главном потоке
  • Желательно не менять UI
  • Убрать зависимость от Rx и скрыть под абстракциями
  • Упростить работу с зависимостями

Что придумали


Новая архитектура


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


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


Краткая UML схема репозитория:


Краткая UML схема репозитория


Итог


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

Подробнее..

За двумя мобильными сервисами HMS и GMS в одном приложении

05.10.2020 10:14:40 | Автор: admin


Привет, Хабр! Меня зовут Андрей, я делаю приложение Кошелёк для Android. Уже больше полугода мы помогаем пользователям смартфонов Huawei оплачивать покупки банковскими картами бесконтактно через NFC. Для этого нам потребовалось добавить поддержку HMS: Push Kit, Map Kit и Safety Detect. Под катом я расскажу, какие проблемы нам пришлось решать при разработке, почему именно так и что из этого вышло, а также поделюсь тестовым проектом для более быстрого погружения в тему.

Для того, чтобы предоставить всем пользователям новых смартфонов Huawei возможность бесконтактной оплаты из коробки и обеспечить лучший пользовательский опыт в остальных сценариях, в январе 2020 года мы начали работы по поддержке новых пушей, карт и проверок на безопасность. Результатом должно было стать появление в AppGallery версии Кошелька с родными для телефонов Huawei мобильными сервисами.

Вот что удалось выяснить на этапе первоначальной проработки


  • Huawei распространяет AppGallery и HMS без ограничений можно скачать и установить их на устройства других производителей;
  • После того, как мы установили AppGallery на Xiaomi Mi A1, все обновления начали подтягиваться в первую очередь с новой площадки. Сложилось впечатление, что AppGallery успевает обновлять приложения быстрее конкурентов;
  • Сейчас Huawei стремится как можно быстрее наполнить AppGallery приложениями. Чтобы ускорить миграцию на HMS, они решили предоставить разработчикам уже знакомый (похожий на GMS) API;
  • На первых порах, пока экосистема Huawei для разработчиков не заработает на полную мощность, отсутствие Google-сервисов скорее всего будет являться главной проблемой для пользователей новых смартфонов Huawei, и они будут всеми способами пытаться их установить.

Мы решили делать одну общую версию приложения для всех площадок распространения. Она должна уметь определять и использовать подходящий тип мобильных сервисов в рантайме. Этот вариант казался медленнее в реализации, чем отдельная версия под каждый тип сервисов, но мы рассчитывали выиграть в другом:

  • Исключается риск попадания версии, предназначенной для Google Play, на девайсы Huawei и наоборот;
  • Можно внедрить любой алгоритм выбора мобильных сервисов, в том числе с использованием feature toggle;
  • Тестировать одно приложение проще, чем два;
  • Каждый релиз можно выкладывать на все площадки распространения;
  • Не приходится переключаться с написания кода на управление сборкой проекта при разработке/модификации.

Для работы с разными реализациями мобильных сервисов в одной версии приложения необходимо:

  1. Спрятать все обращения за абстракцию, сохранив работу с GMS;
  2. Добавить реализацию для HMS;
  3. Разработать механизм выбора реализации сервисов в рантайме.

Методика внедрения поддержки Push Kit и Safety Detect значительно отличается от Map Kit, поэтому рассмотрим их отдельно.

Поддержка Push Kit и Safety Detect


Как и положено в таких случаях, процесс интеграции начался с изучения документации. В разделе предостережений обнаружились вот такие пункты:
  • If the EMUI version is 10.0 or later on a Huawei device, a token will be returned through the getToken method. If the getToken method fails to be called, HUAWEI Push Kit automatically caches the token request and calls the method again. A token will then be returned through the onNewToken method.
  • If the EMUI version on a Huawei device is earlier than 10.0 and no token is returned using the getToken method, a token will be returned using the onNewToken method.
  • For an app with the automatic initialization capability, the getToken method does not need to be called explicitly to apply for a token. The HMS Core Push SDK will automatically apply for a token and call the onNewToken method to return the token.

Главное, что нужно вынести из этих предостережений существует разница в получении пуш-токена на разных версиях EMUI. После вызова метода getToken(), реальный токен может быть возвращен через вызов метода onNewToken() сервиса. Наши испытания на реальных устройствах показали, что телефоны с EMUI < 10.0 на вызов метода getToken возвращают null или пустую строку, после чего происходит вызов метода onNewToken() сервиса. Телефоны с EMUI >= 10.0 всегда возвращали пуш-токен из метода getToken().

Можно реализовать вот такой источник данных, чтобы привести логику работы к единому виду:
class HmsDataSource(   private val hmsInstanceId: HmsInstanceId,   private val agConnectServicesConfig: AGConnectServicesConfig) {   private val currentPushToken = BehaviorSubject.create<String>()   fun getHmsPushToken(): Single<String> = Maybe       .merge(           getHmsPushTokenFromSingleton(),           currentPushToken.firstElement()       )       .firstOrError()   fun onPushTokenUpdated(token: String): Completable = Completable       .fromCallable { currentPushToken.onNext(token) }   private fun getHmsPushTokenFromSingleton(): Maybe<String> = Maybe       .fromCallable<String> {           val appId = agConnectServicesConfig.getString("client/app_id")           hmsInstanceId.getToken(appId, "HCM").takeIf { it.isNotEmpty() }       }       .onErrorComplete()}

class AppHmsMessagingService : HmsMessageService() {   val onPushTokenUpdated: OnPushTokenUpdated = Di.onPushTokenUpdated   override fun onMessageReceived(remoteMessage: RemoteMessage?) {       super.onMessageReceived(remoteMessage)       Log.d(LOG_TAG, "onMessageReceived remoteMessage=$remoteMessage")   }   override fun onNewToken(token: String?) {       super.onNewToken(token)       Log.d(LOG_TAG, "onNewToken: token=$token")       if (token?.isNotEmpty() == true) {           onPushTokenUpdated(token, MobileServiceType.Huawei)               .subscribe({},{                   Log.e(LOG_TAG, "Error deliver updated token", it)               })       }   }}

Важные замечания:

  • Предложенное решение работает не во всех случаях. При тестировании на физических устройствах проблем выявлено не было, но на пуле устройств, предоставляемых AppGallery для онлайн-дебаггинга, подход не срабатывает. Причём не срабатывает из за того, что вызова метода HmsMessageService.onNewToken() не происходит, что, кажется, не соответствует документации. Причина такого поведения по сей день остаётся для нас невыясненной;
  • Оказалось, что на некоторых устройствах метод HmsMessageService.onMessageReceived() может вызываться на main потоке, поэтому будьте аккуратнее с походами в БД и сеть из него;
  • Как только вы добавите зависимость от библиотеки com.huawei.hms:push, в манифесте проекта после сборки будет объявлен сервис com.huawei.hms.support.api.push.service.HmsMsgService, сконфигурированный для работы в отдельном процессе :pushservice. С этого момента, при порождении каждого процесса, в нём будет создаваться свой экземпляр класса Application. Это принципиально важно осознавать, если вы обращаетесь к файлам или БД или, например, собираете данные о скорости инициализации приложения через Firebase Performance. Мы встретились с порождением второго процесса только на не-Huawei устройствах, куда были установлены AppGallery и HMS.

Для случаев поддержки работы приложения с пуш-токеном и проверки устройства на безопасность общий алгоритм будет одинаковым:


  • Создаём по отдельному источнику данных для каждого типа сервисов;
  • Добавляем по репозиторию для пушей и безопасности, принимающих на вход тип мобильных сервисов и выбирающих конкретный источник данных;
  • Некая сущность бизнес-логики определяет, какой тип мобильных сервисов (из доступных) уместно использовать в конкретном случае.

Разработка механизма выбора реализации сервисов в рантайме


Как действовать, если на устройстве установлен всего один тип сервисов или их нет вовсе, понятно, а вот что делать, если одновременно установлены и Google-, и Huawei-сервисы?

Вот что мы обнаружили и из чего исходили:

  • При внедрении любой новой технологии её нужно использовать в приоритете, если устройство пользователя полностью соответствует всем требованиям;
  • На устройствах с EMUI >= 10.0 алгоритм получения пуш-токена отличается от предыдущих версий;
  • Подавляющее большинство устройств Huawei без Google-сервисов будут иметь версию EMUI 10.0 и выше;
  • На новые устройства Huawei пользователи будут пытаться установить Google-сервисы, чтобы пользоваться всеми привычными приложениями. Надёжного способа сделать это нет, поэтому мы не должны рассчитывать на стабильную и корректную работу Google-сервисов на таких устройствах;
  • Технически пользователи смартфонов других вендоров могут установить себе AppGallery и Huawei-сервисы, но мы предполагаем, что на текущий момент таких пользователей очень мало.

Разработка алгоритма оказалась, наверное, самым выматывающим делом. Здесь в одну точку сошлось множество технических и бизнесовых факторов, но в конечном итоге нам удалось прийти к наилучшему для нашего продукта решению. Сейчас даже немного странно, что описание самой обсуждаемой части алгоритма помещается в одно предложение, но я рад, что в конечном итоге получилось просто:
В случае, если на устройстве установлены оба типа сервисов и удалось определить, что версия EMUI < 10 используем Google, иначе используем Huawei.

Для реализации итогового алгоритма требуется найти способ определить версию EMUI на устройстве пользователя.
Один из способов сделать это прочитать системные свойства:
class EmuiDataSource {    @SuppressLint("PrivateApi")    fun getEmuiApiLevel(): Maybe<Int> = Maybe        .fromCallable<Int> {            val clazz = Class.forName("android.os.SystemProperties")            val get = clazz.getMethod("getInt", String::class.java, Int::class.java)            val currentApiLevel = get.invoke(                    clazz,                    "ro.build.hw_emui_api_level",                    UNKNOWN_API_LEVEL            ) as Int            currentApiLevel.takeIf { it != UNKNOWN_API_LEVEL }        }        .onErrorComplete()    private companion object {        const val UNKNOWN_API_LEVEL = -1    }}

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

Итоговая реализация алгоритма, учитывающая тип операции, для которой выбирается сервис, и определение версии EMUI устройства, может выглядеть так:
sealed class MobileServiceEnvironment(   val mobileServiceType: MobileServiceType) {   abstract val isUpdateRequired: Boolean   data class GoogleMobileServices(       override val isUpdateRequired: Boolean   ) : MobileServiceEnvironment(MobileServiceType.Google)   data class HuaweiMobileServices(       override val isUpdateRequired: Boolean,       val emuiApiLevel: Int?   ) : MobileServiceEnvironment(MobileServiceType.Huawei)}

class SelectMobileServiceType(        private val mobileServicesRepository: MobileServicesRepository) {    operator fun invoke(            case: Case    ): Maybe<MobileServiceType> = mobileServicesRepository            .getAvailableServices()            .map { excludeEnvironmentsByCase(case, it) }            .flatMapMaybe { selectEnvironment(it) }            .map { it.mobileServiceType }    private fun excludeEnvironmentsByCase(            case: Case,            envs: Set<MobileServiceEnvironment>    ): Iterable<MobileServiceEnvironment> = when (case) {        Case.Push, Case.Map -> envs        Case.Security       -> envs.filter { !it.isUpdateRequired }    }    private fun selectEnvironment(            envs: Iterable<MobileServiceEnvironment>    ): Maybe<MobileServiceEnvironment> = Maybe            .fromCallable {                envs.firstOrNull {                    it is HuaweiMobileServices                            && (it.emuiApiLevel == null || it.emuiApiLevel >= 21)                }                        ?: envs.firstOrNull { it is GoogleMobileServices }                        ?: envs.firstOrNull { it is HuaweiMobileServices }            }    enum class Case {        Push, Map, Security    }}

Поддержка Map Kit


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

  1. Определить тип сервисов для отображения карт;
  2. Заинфлейтить соответствующий layout и работать с конкретной реализацией карт.

Однако здесь есть одна особенность, о которой хочется рассказать. Rx головного мозга позволяет практически куда угодно добавить любую асинхронную операцию без риска переписать всё приложение, но накладывает и свои ограничения. Например, в данном случае для определения соответствующего лэйаута, скорее всего, потребуется вызвать .blockingGet() где-нибудь на Main потоке, что совсем нехорошо. Решить эту проблему можно, например, с помощью дочерних фрагментов:

class MapFragment : Fragment(),   OnGeoMapReadyCallback {   override fun onActivityCreated(savedInstanceState: Bundle?) {       super.onActivityCreated(savedInstanceState)       ViewModelProvider(this)[MapViewModel::class.java].apply {           mobileServiceType.observe(viewLifecycleOwner, Observer { result ->               val fragment = when (result.getOrNull()) {                   Google -> GoogleMapFragment.newInstance()                   Huawei -> HuaweiMapFragment.newInstance()                   else -> NoServicesMapFragment.newInstance()               }               replaceFragment(fragment)           })       }   }   override fun onMapReady(geoMap: GeoMap) {       geoMap.uiSettings.isZoomControlsEnabled = true   }}

class GoogleMapFragment : Fragment(),   OnMapReadyCallback {   private var callback: OnGeoMapReadyCallback? = null   override fun onAttach(context: Context) {       super.onAttach(context)       callback = parentFragment as? OnGeoMapReadyCallback   }   override fun onDetach() {       super.onDetach()       callback = null   }   override fun onMapReady(googleMap: GoogleMap?) {       if (googleMap != null) {           val geoMap = geoMapFactory.create(googleMap)           callback?.onMapReady(geoMap)       }   }}

class HuaweiMapFragment : Fragment(),   OnMapReadyCallback {   private var callback: OnGeoMapReadyCallback? = null   override fun onAttach(context: Context) {       super.onAttach(context)       callback = parentFragment as? OnGeoMapReadyCallback   }   override fun onDetach() {       super.onDetach()       callback = null   }   override fun onMapReady(huaweiMap: HuaweiMap?) {       if (huaweiMap != null) {           val geoMap = geoMapFactory.create(huaweiMap)           callback?.onMapReady(geoMap)       }   }}

Теперь можно написать отдельную реализацию для работы с картой для каждого отдельного фрагмента. Если потребуется реализовать одинаковую логику, то можно поступить по знакомому алгоритму подогнать работу с каждым типом карт под один интерфейс и передать одну из реализаций этого интерфейса в родительский фрагмент, как это сделано в MapFragment.onMapReady()

Что из этого вышло


В первые дни после релиза обновленной версии приложения число установок достигло 1 млн. Мы связываем это отчасти с фичерингом со стороны AppGallery, а отчасти с тем, что наш релиз подсветило несколько СМИ и блогеров. А ещё со скоростью обновления приложений ведь в AppGallery на протяжении двух недель лежала версия с самым высоким versionCode.

Мы получаем полезные отзывы о работе приложения в общем и о токенизации банковских карт в частности от пользователей в нашей ветке на 4pda. После релиза Pay-функциональности для Huawei посетителей на форуме прибавилось, и проблем, с которыми они сталкиваются, тоже. Мы продолжаем работать над всеми обращениями, но массовых проблем при этом не наблюдаем.

В целом, релиз приложения в AppGallery прошёл успешно и можно сделать вывод, что наш подход к решению задачи оказался рабочим. Благодаря выбранному методу реализации у нас сохранилась возможность выкладывать все релизы приложения как в Google Play, так и в AppGallery.

Пользуясь этим методом, мы уже добавили в приложение Analytics Kit, APM, работаем над поддержкой Account Kit и не планируем на этом останавливаться, тем более, что с каждой новой версией HMS становится доступно всё больше возможностей.

Послесловие


Регистрация аккаунта разработчика в AppGallery представляет собой гораздо более сложную процедуру, чем в случае с Google. У меня, например, этап проверки подтверждения личности занял 9 дней. Не думаю что так происходит со всеми, но любая задержка способна поубавить оптимизма. Поэтому вместе с полным кодом всего демо-решения, описанного в статье, я закоммитил в репозиторий и все ключи приложения, чтобы у вас была возможность не только оценить решение целиком, но и прямо сейчас испытать и усовершенствовать предложенный подход.

Пользуясь выходом в публичное пространство, хочу поблагодарить всю команду Кошелька и особенно umpteenthdev, Артёма Кулакова и Егора Аганина за неоценимый вклад в интеграцию HMS в Кошелёк!

Полезные ссылки


  • Полный код демонстрационного проекта на GitHub;
  • Скачать AppGallery на телефон любого производителя. Актуальную версию приложения HMS-Core можно загрузить из AppGallery;
  • Push Kit codelab;
  • Map Kit codelab;
  • Safety Detect codelab;
  • Инструкция к сервису онлайн-дебаггинга своих приложений на устройствах Huawei. Возможность использования появляется после регистрации в AppGallery Connect;
  • Ветка приложения Кошелёк на 4PDA.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru