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

Deliveryclub

Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS

15.03.2021 12:08:42 | Автор: admin


Привет, Хабр! Меня зовут Георгий Гигаури, я разрабатываю Android-приложение Delivery Club. Эта статья появилась после доклада на конференции Mobius 2020, где мы выступали вместе с Павлом Борзиковым. Для тех, кто любит видео, ищите его в конце статьи.

Почему мы вообще обратили внимание на Huawei-устройства? Всё началось с того, что Huawei теперь не может распространять свои устройства с сервисами Google Play. Да, они могут использовать ОС Android, так как это открытая операционная система, но чтобы распространять устройства с сервисами Google Play, необходимо иметь лицензию. К сожалению, Huawei не может получить её из-за разногласий между Китаем и США. Поэтому Huawei приходится разрабатывать свои собственные Mobile Services. Справедливости ради, они этим занимались уже давно, но теперь им приходится расширять кодовую базу, активно увеличивать количество сервисов.

Почему стоит обратить внимание на экосистему Huawei


Смартфоны Huawei очень популярны: в 2020 году в России они занимали почти 18% рынка (Рис.1), а в мире 11% (Рис.2), (источник). Huawei заявила, что более 490 млн человек в более чем в 170 странах мира пользуются AppGallery (источник). Поскольку аудитория у Huawei-устройств огромная, мы не можем это игнорировать и решили поддержать пользователей нашего приложения. Далее поэтапно рассмотрим, что же нужно сделать.


Рис.1


Рис.2

Этап 1: проверка наличия Services


Если у вас в приложении при входе есть проверка наличия Google Services, то придётся от этого отказаться, и проверять наличие соответствующих сервисов только по мере необходимости.

fun Context.getMobileServiceSource(): MobileServicesSource {    val googleApi = GoogleApiAvailability.getInstance()    if (googleApi.isGooglePlayServicesAvailable(this) == com.google.android.gms.common.ConnectionResult.SUCCESS) {        return MobileServicesSource.GOOGLE    }    val huaweiApi = HuaweiApiAvailability.getInstance()    if (huaweiApi.isHuaweiMobileServicesAvailable(this) == com.huawei.hms.api.ConnectionResult.SUCCESS) {        return MobileServicesSource.HMS    }    return MobileServicesSource.NONE}enum class MobileServicesSource {    GOOGLE,    HMS,    NONE}

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

Этап 2: карты


В приложении Delivery Club три основные страницы:

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

На устройствах Huawei все эти карты не работают. Чтобы это исправить, можно просто заменить зависимости: вместо пакета com.google.android.gms использовать com.huawei.hms:





Конечно, есть нюансы, но мы уже сделали большую часть работы. Huawei сделала Maps SDK с контрактами, по большей части соответствующий Google Maps SDK. Однако у Google есть deprecated-методы, если вы их используете, то аналогов у Huawei может и не найтись. Например, для получения местоположения пользователя мы используем:

LocationServices.FusedLocationApi.getLastLocation(googleApiClien)

Такой подход считается deprecated, и если мы просто скопируем код для Huawei Maps и заменим зависимости, то работать не будет. Нужно поменять так:

LocationServices.getFusedLocationProviderClient().getLastLocation().addOnSuccessListener()

PolyUtil. Расшифровка с помощью Polyline


Теперь нам нужно отображать перемещение курьера. Для этого нам нужно расшифровывать строку, которая приходит с сервера. Для этого воспользуемся тем же алгоритмом от Google, с помощью которого строка кодировалась.



После расшифровки мы получили список координат курьера.

Реализация поддержки двух карт


Для поддержки нескольких карт необходимо создать обёртку для самих карт и для объекта.

Добавляем общий интерфейс, например, IMapWidget. Не забываем сделать общий класс для LatLng список координат курьера. У Google он лежит в пакете com.google.android.gms.maps.model.LatLng, а у Huawei в com.huawei.hms.maps.model.LatLng. Кладём список в PolyLineOptions и задаём ширину и цвет линии маршрута.

interface IMapWidget {    void animateCamera(...);    void setListener(OnMapEventListener listener);    void setMapPadding(...);    MapMarker addMarker(...);    ...}

Добавляем Custom Map View реализующего интерфейс IMapWidget:



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

class MapWrapper : FrameLayout() {    fun setupMap(widget: IMapWidget) {        removeAllViews()        addView(widget as View)    }}

И в нужном месте вызываем метод добавления карты.

override fun onCreateView(...) {    ...    val map: IMapWidget = MapFactory.createMap()    viewMapWrapper.setupMap(map)    ...}

Такие обёртки класса нужно создать для всего: объектов, маркеров, PolyUtil, PolyLine и т.д.

Проблема: Карта не работает




Однажды нам сообщили о баге. Пользователь с устройством Huawei, находившийся в центре Москвы (Рис.3), открыл приложение, нажал на кнопку Переместиться на своё местоположение, и его перенесло в пустоту (Рис.4). Пользователь не видит, ни улиц, ни зданий, и он решил, что карта не работает.

Мы попробовали воспроизвести у себя эту проблему. И действительно попадали в неопределённое пространство. Когда попробовали чуть-чуть уменьшить масштаб карты, то оказалось, что мы попали в пригород Мариуполя (Рис.5). То есть из московских координат (55.819207, 37.493424) перенеслись в мариупольские (47.187447, 37.593137). Мы были в полном недоумении. Может быть, где-то у нас с числами что-то не то происходит. Возможно, происходят некие вычитания наших координат. Очень долго искали решение этой проблемы или хотя бы причину. Оказалось, что мы заменили импорты из Google-карт, и поэтому всё перестало работать. В конце концов мы добрались до paddingа.



Давайте быстро вспомним, что такое padding у карты. На (Рис.6) показан экран авторизации, карта занимает всю область экрана, даже под плашкой ручного ввода адреса. В таком случае, если мы не добавим padding карте, её центр будет находиться на месте зелёного треугольника, но мы хотим, чтобы он был в центре рабочей области карты. Padding сужает рабочую область (Рис.7). Не видимую, а именно рабочую. Карта будет по-прежнему занимать весь экран, но размер её рабочей области изменится. И когда вы будете переходить в новую координату, она будет принимать положение новой рабочей карты. Как оказалось, баг был именно из-за этого.

Первое решение: убрать padding. Как вы понимаете, такой вариант нам не подошёл. Мы хотели, чтобы всё отображалось красиво.

Второе решение проблемы: использовать анимированное перемещение, но с масштабированием.

val zoom = map.cameraPosition.zoommap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom))

При переходе с изменением масштаба карты всё работало правильно. Здорово! Мы подумали, что это нам подходит. На самом деле нет. У нас ещё есть третий экран, на котором нужно увеличивать карту относительно двух маркеров, чтобы zoom сам рассчитывался, поэтому мы не можем задать какое-то константное масштабирование. То есть такой вариант нам тоже не подошёл. Начали думать дальше и нашли новое решение.

Третье решение проблемы: вообще отказаться от анимации. Как оказалось, если вместо animateCamera сделать просто move, то перемещение будет происходить правильно. Так мы и сделали. Надеемся, в скором времени Huawei устранит эту проблему.

Этап 3: push-сервис


Идём дальше. На Huawei-устройства не приходят уведомления нашего приложения. Дело в том, что мы не можем получить токен. Давайте его получим. В Google мы получаем задачу и извлекаем токены так:

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->    if (task.isSuccessful) {        val token = task.result    }}

Наше решение:

class ImplementationHuaweiMessagingService : HmsMessageService() {    override fun onNewToken(token: String?) {        val commonApi = getComponentFactory().get(CommonApi::class.java)        commonApi.settingsManager().setPushToken(token)    }    override fun onMessageReceived(message: RemoteMessage?) {        message?.let {            val appManagersComponent = getComponentFactory().get(AppManagersApi::class.java)            appManagersComponent.pushManager().handle(it.dataOfMap)        }    }

Выглядит всё так же, как и с реализацией FirebaseMessagingService(), даже есть callbackи onNewToken и onMessageReceived. Однако без нюансов не обойтись. Случается, что на некоторых редких устройствах onMessageReceived вызывается в главном потоке, поэтому лучше не использовать здесь долго выполняющиеся задачи.

Получаем токены на Huawei:

val token = HmsInstanceId.getInstance(context)    .getToken(appId, com.huawei.hms.push.HmsMessaging.DEFAULT_TOKEN_SCOPE)public static final String DEFAULT_TOKEN_SCOPE = "HCM";

Обратите внимание, что метод выполняется в главном потоке. И для получения токена нужно отдельно реализовать поток. У Google такой подход уже считается устаревшим, возможно, Huawei придёт к тому же.

Мы можем вообще не использовать getToken, а прописать в манифесте автоматическую инициализацию или в коде методом setAutoInitEnabled() и всегда получать token в onNewToken (подробнее). Это решит ещё одну проблему: getToken в версиях EMUI ниже 10 вообще возвращает null.

<meta-data    android:name="push_kit_auto_init_enabled"    android:value="true"/>

Этап 4: Chrome Custom Tabs


Наше приложение при запуске регулярно вылетает с ошибкой ActivityNotFoundException. Чтобы от этого избавиться, нужно обработать отсутствие Chrome Tabs.

fun Context.openLink(url: String, customTabsSession: CustomTabsSession? = null): Boolean {    try {        openLinkInCustomTab(url, customTabsSession)        return true    } catch (throwable: Throwable) {        Timber.tag("Context::openLink").e(throwable, "CustomTabsIntent error on url: $url")    }    return openLinkInBrowser(url)}@Throws(Throwable::class)fun Context.openLinkInCustomTab(url: String, customTabsSession: CustomTabsSession? = null) {    CustomTabsIntent.Builder(customTabsSession)        .build()        .launchUrl(this, Uri.parse(url))}private fun Context.openLinkInBrowser(url: String): Boolean {    val intent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {        addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT)    }    if (intent.resolveActivity(packageManager) != null) {        startActivity(intent)        return true    }    return false}

Мы просто обернули openLinkInCustomTab() в try catch и в случае ошибки пытаемся открыть в браузере. Но бывает такого, чтобы на устройстве не было подходящего браузера, способного обработать наш неявный intent. Поэтому если метод openLinkInBrowser() возвращает false, мы открываем страницу в webview.

Этап 5: аналитика


Аналитика у Huawei похожа на Google Analytics. Покажу замену на примере Firebase. Сначала инициализируем: HiAnalytics.getInstance(context). Затем с помощью HAEventType.STARTCHECKOUT копируем все наши события из Firebase в отдельный файл huaweiAnalytics:

huaweiAnalytics.onEvent(name, bundle)

Системные параметры: HAParamType.PRICE, HAParamType.CURRNAME

Даже если у вас нет Firebase, добавить аналитику в Huawei очень просто. У них отличная документация, контракт соблюдается. Также у Huawei есть отличные инструменты для исследования аудитории.

Этап 6: crashlytics


Следующий инструмент, который нам тоже стало интересно попробовать, это Crashlytics от Huawei, которая называется AGConnectCrash. Она позволяет с минимальными усилиями собирать и анализировать информацию о падении приложения.

Инициализируем crashlytics:

AGConnectCrash.getInstance().enableCrashCollection(true)

Добавляем свои ключи и журналируем нужные события:

AGConnectCrash.getInstance().setUserId("testuser")AGConnectCrash.getInstance().log(Log.DEBUG, "set debug log.")AGConnectCrash.getInstance().log(Log.INFO, "set info log.")AGConnectCrash.getInstance().log(Log.WARN, "set warning log.")AGConnectCrash.getInstance().log(Log.ERROR, "set error log.")AGConnectCrash.getInstance().setCustomKey("stringKey", "Hello world")AGConnectCrash.getInstance().setCustomKey("booleanKey", false)AGConnectCrash.getInstance().setCustomKey("doubleKey", 1.1)AGConnectCrash.getInstance().setCustomKey("floatKey", 1.1f)AGConnectCrash.getInstance().setCustomKey("intKey", 0)AGConnectCrash.getInstance().setCustomKey("longKey", 11L)

Этап 7: покупки в приложении


Если в вашем приложении есть встроенные покупки, вы должны подписать согласие на передачу персональных данных, и отослать письмом в конверте или через курьера в московский офис Huawei. И только через пару дней они вам дадут доступ для реализации этой функциональности.

Всё очень похоже на реализацию Google. При запуске приложения запрашиваем все прошлые покупки пользователя:

fun getOwnedPurchases(    activity: Activity,    ownedPurchasesResultOnSuccessListener: OnSuccessListener<OwnedPurchasesResult>,    failureListener: OnFailureListener) {    val ownedPurchasesReq = OwnedPurchasesReq()    // priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription    ownedPurchasesReq.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION    // To get the Activity instance that calls this API.    val task: Task<OwnedPurchasesResult> = Iap.getIapClient(activity)        .obtainOwnedPurchases(ownedPurchasesReq)    task.addOnSuccessListener(ownedPurchasesResultOnSuccessListener)        .addOnFailureListener(failureListener)}

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

fun loadProduct(    context: Context,    productInfoResultOnSuccessListener: OnSuccessListener<ProductInfoResult>,    onFailureListener: OnFailureListener) {    // obtain in-app product details configured in AppGallery Connect, and then show the products    val iapClient: IapClient = Iap.getIapClient(context)    val task: Task<ProductInfoResult> = iapClient.obtainProductInfo(createProductInfoReq())    task.addOnSuccessListener(productInfoResultOnSuccessListener)        .addOnFailureListener(onFailureListener)}private fun createProductInfoReq(): ProductInfoReq {    val req = ProductInfoReq()    // 0: consumable ; 1: non-consumable ; 2: auto-renewable subscription    req.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION    val productIds = ArrayList<String>()    productIds.add("PRODUCT_ID")    req.productIds = productIds    return req}

Когда пользователь кликает на товар, мы открываем страницу с оплатой. Она не такая красивая, как у Google, и не выезжает снизу.

fun gotoPay(activity: Activity, productId: String, type: Int) {    val client: IapClient = Iap.getIapClient(activity)    val task: Task<PurchaseIntentResult> = client.createPurchaseIntent(createPurchaseIntentReq(type, productId))    task.addOnSuccessListener { result ->        result?.let {            val status: Status = result.status            if (status.hasResolution()) {                try {                    status.startResolutionForResult(activity, PAY_RESULT_ARG)                } catch (exception: SendIntentException) {                    Timber.e(exception)                }            } else {                Timber.d("intent is null")            }        }    }.addOnFailureListener { exception ->        Timber.e(exception)    }}

Так как это Activity, мы передаём ему аргумент, по которому можно отловить OnActivityResult и понять, успешно ли прошла оплата и как закончилась транзакция:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {    super.onActivityResult(requestCode, resultCode, data)    if (resultCode == PAY_RESULT_ARG) {        val purchaseResultInfo: PurchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data)        when (purchaseResultInfo.returnCode) {            OrderStatusCode.ORDER_STATE_SUCCESS -> {                successResult(purchaseResultInfo)            }            OrderStatusCode.ORDER_STATE_CANCEL -> {            }            OrderStatusCode.ORDER_PRODUCT_OWNED -> {            }        }    }}

У нас есть специальные статусы: ORDER_SUCCESS, CANCEL, OWNED. Первый означает успешную оплату. Второй пользователь просто закрыл страницу без покупки, тогда мы обрабатываем этот callback и предлагаем скидку, чтобы уговорить на покупку. А третий статус означает, что товар уже куплен пользователем. Если товар разовый или подписочный, то на этом моменте нужно остановиться, в противном случае виртуально доставить покупку.

В случае успешной оплаты доставляем пользователю купленный товар:

private fun successResult(purchaseResultInfo: PurchaseResultInfo) {    val inAppPurchaseData = InAppPurchaseData(purchaseResultInfo.inAppPurchaseData)    val req = ConsumeOwnedPurchaseReq()    req.purchaseToken = inAppPurchaseData.purchaseToken    val client: IapClient = Iap.getIapClient(this)    val task: Task<ConsumeOwnedPurchaseResult> =        client.consumeOwnedPurchase(req)    task.addOnSuccessListener {        // Consume success    }.addOnFailureListener { exception ->        Timber.e(exception)    }}

Если не сделать доставку, то функциональность товара будет у пользователя заблокирована, а деньги возвращены. В Google Play Billing Library до третьей версии этого делать не нужно было, но потом Google тоже это добавил, и если мы не доставим товар, через 48 часов покупка отменится, а деньги вернутся пользователю. То есть в Huawei покупки реализованы как в третьей версии Google Play Billing.

Выводы


На реализацию поддержки Huawei-устройств не уйдёт много времени. Даже без реальных устройств вы сможете проверить работоспособность вашего приложение: у Huawei есть своя тестовая лаборатория с виртуальными устройствами наподобие Samsung Remote test lab. Количество пользователей быстро растёт, и бизнесу может оказаться выгодным вложиться в доработку продуктов, а отличная документация поможет разработчикам всё сделать быстро. Поддержка HMS активно отвечает на любые вопросы, если вы не сможете в документации что-то найти.

Видеозапись доклада с конференции Mobius 2020.

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


Подробнее..

We need to go deeper как пасхалка в приложении Delivery Club сократила субъективное время ожидания еды

11.06.2021 16:11:37 | Автор: admin


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

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

Сашин скриншот того, как всё начиналось.

Саша:
У меня есть свой небольшой проект словесно-карточная игра Кто из нас? На одном из очередных разборов игры родилась мысль, что можно внутри игры спрятать ещё одну игру. Люди найдут её и скажут: Ого, я играл в одну, а сейчас играю в совершенно другую. Но игра, которую я делаю, не настолько популярна, поэтому если туда прятать ещё одну игру, то её просто никто не увидит. Возникла вторая идея. Я работаю в Delivery Club, и у нас миллионы пользователей, у которых есть общая проблема время ожидания заказа. Нужно либо сокращать время доставки, либо как-то развлечь пользователя, пока он ожидает. И я пошёл по второму пути: придумал решение в виде небольшой игры внутри основного приложения.
Дизайн для первого прототипа змейки сделала девушка Саши, а звуки Саша создал сам в интернет-синтезаторе.


Прототипы первых экранов.

Уже в январе ребята буквально за полдня в коворкинге написали MVP на Swift и SpriteKit. Там были крупные кнопки и небольшой игровой экран, как в тетрисе. В финальной версии от кнопок на экране отказались: из-за того, что они плоские, а не объёмные, как на настоящей консоли, змейкой было неудобно управлять.
Саша:
Мы собрались с Сахеем и начали писать приложение в духе лучших стартапов. Заказали пиццу. Пива там не было, поэтому мы пили кофе. За один день у нас был готов прототип, который уже выполнял свою задачу. Игра запускалась, был первый квадратик, который начинал ездить. Он поедал эмодзи, змейка росла; врезавшись в стенку, она умирала. Осталось её только докрутить: добавить включение и выключение звука, доработать экраны начала и конца игры. Этим мы занялись уже в свободное время дома.
Сахей:
Очень понравился этот режим стартапа. Обычно на работе есть все спецификации и документация протоптанная тропинка, по которой ты идёшь. А тут был полный полёт мысли. Мы ещё хотели изначально поменять рендеринг на SwiftUI или на UIKit, но не стали так извращаться, SpriteKit отлично подходил для нашей задачи. Он оптимизирован под отрисовку спрайтов. Если в UIKit 100-200 вьюшек, то FPS очень сильно проседает, а в SpriteKit нет. Но мы фана ради хотели попробовать.


Разработка MVP.

В феврале ребята пришли с готовым прототипом к руководителю направления и показали, как змейка могла бы выглядеть внутри приложения Delivery Club. Мы собрали небольшую рабочую группу, в которую, помимо ребят, вошла дизайнер продукта Лера Зуйкова. И решили доводить проект до совершенства.

Финальную версию змейки пришлось достаточно сильно упростить ребята увлеклись и придумали много дополнительных возможностей. Решили оставить простой и красивый вариант, чтобы не отвлекать пользователя от основной функции приложения.
Саша:
Когда Лера показала свой дизайн, у нас с Сахеем загорелись глаза. Мы такие: Вау! Можно же настолько красиво и современно всё сделать! У нас появилась куча новых идей. Можно нарабатывать опыт у змейки с каждой игрой и набирать очки, за них покупать змейке какой-то апгрейд, типа шапочки или щита, чтобы она врезалась в стенку, а у неё на одну жизнь больше было. Но всё-таки история со змейкой должна была где-то кончиться, чтобы дойти до релиза. И мы решили упростить игру, чтобы не смещать фокус с основного функционала Delivery Club.
Лера:
На самом деле, когда в работе есть много рутинных задач, и внезапно кто-то приходит и предлагает сделать игру, это очень воодушевляет. Поэтому мы сразу активно включились в историю со змейкой и стали делать супер-красивые дизайны, продумывать механики. Все механики не были добавлены в финальный релиз ещё и потому, что вначале мы хотели всё проверить. Просто потратить кучу времени на разработку и сделать фичу, которую потом нашли бы три человека, было бы странно. Поэтому мы сошлись на простом решении, которое можно было быстро сделать, запустить и посмотреть реакцию пользователей. Если это окажется интересным, мы будем развивать игру: делать рейтинги, баллы и так далее.
Георгий:
После того, как ребята реализовали версию под iOS, мы принялись адаптировать приложение под Android. Разработка велась на Kotlin, всё сделали нативно, без использования сторонних библиотек. В конечном итоге версию под Android получилось написать всего за четыре дня. Идея была творческой и разнообразила череду рутинных задач.
Реализацию Android-версии Змейки можно посмотреть на GitHub.



Промежуточные варианты дизайна экранов.

18 мая змейку добавили в приложение Delivery Club. Чтобы в неё поиграть, нужно потрясти телефон на экране с заказом. Мы нигде не писали про пасхалку, но уже сейчас в неё играют 5-7 тысяч человек в день со средним временем игры 3 минуты 10 секунд. Нескольким тысячам пользователей игра скрасила минуты ожидания заказа. Надеемся, теперь игроков станет больше.

А если после прочтения у вас появились собственные идеи для пасхалок в Delivery Club, принимайте участие в нашем конкурсе. Лучшие идеи будут опубликованы на N + 1 и, возможно, реализованы в нашем приложении.
Подробнее..

Зачем нам 170 разработчиков

21.07.2020 14:09:04 | Автор: admin
image

Привет, Хабр! Меня зовут Андрей Евсюков, я заместитель CTO в Delivery Club. Наша компания устроена сложнее, чем может показаться, когда представляешь себе сервис по доставке еды. Даже когда примерно знаешь, что там может быть под капотом.

В этой серии статей я расскажу о том, как у нас всё работает, начиная с того, как устроена IT-система Delivery Club в целом и как создаются новые фичи, и заканчивая построением команд, процессом найма сотрудников и собеседованиями. А самое главное, почему именно такую архитектуру мы выбрали, почему именно так выстроены процессы и каких именно специалистов мы ищем на рынке.

В каждой из статей я подробно опишу, как мы работаем в DC Tech. При этом каждая из этих статей будет логичным следствием того, как устроен рынок FoodTech в России и какие ограничения он накладывает. В первую очередь, рынок влияет на культуру компании, а следовательно, и на инженерную культуру. Особенности культуры диктуют нам те решения, к которым мы пришли. Вот про эти особенности и поговорим. Поехали!

Начну со статьи про особенности индустрии foodtech, которые напрямую влияют на то, как всё организовано внутри Delivery Club. И в процессе постараюсь объяснить, для чего нам 170 разработчиков и почему это не может быть просто outsource.

Особенности FoodTech в России и отличия от классического e-commerce


Под классическим e-com я подразумеваю коммерцию на стыке онлайна и оффлайна. Всегда есть сайт или мобильное приложение, где совершается заказ, системы автоматизации, например, склада, а есть оффлайн-часть операционная деятельность, в подавляющем большинстве случаев это доставка last mile. Помимо этого существуют системы мониторинга для контроля за эффективностью всех онлайн- и оффлайн-процессов.

Доставка еды сильно отличается от большинства других доставок


Давайте рассмотрим доставку канцелярии, книг или одежды: заказ сформирован, собран в коробку, лежит на складе. Автоматически формируется маршрутный лист, даже если это same day delivery это не происходит мгновенно. Курьер забирает заказы и развозит их по списку: одному клиенту, другому всё по порядку. Всё заранее известно. Если происходит задержка в небольшом диапазоне времени, это не так критично все товары останутся целыми, клиент, как правило, готов немного подождать. А если диапазон хотя бы часовой, то точное время доставки доподлинно неизвестно.

С едой всё не так.

  • Мы должны контролировать время! Когда пользователь делает заказ, он голоден. Он не может ждать. Еда должна быть горячей, каждая минута на счету.
  • Невозможно составить маршрутный лист. В Delivery Club только 2% предзаказов. А в остальном никто не заказывает еду заранее это всегда происходит on demand.

  • Процесс работы курьеров динамичный. Ситуация меняется каждые 5-15 минут. Когда начинается дождь или снег, всегда повышается спрос. А когда на улице солнечно и не хочется сидеть дома, спрос снижается. В праздничные и выходные дни профиль спроса отличается от будней. Дорожная ситуация и пробки также вносят свои коррективы, особенно в тех зонах, где превалируют авто/мото-курьеры.

Давайте ещё раз посмотрим на ситуацию на рынке:

  1. Рынок быстро меняется. Появляются новые вертикали и направления. Представьте, Delivery Club уже 10 лет. С 2009 по 2016 мы были маркетплейсом. В 2016-м начали развивать логистику. За последние полгода мы запустили доставку продуктов, доставку из аптек, Takeaway и начали предоставлять сервис экспресс-логистики сторонним компаниям (Связной). Мы видим ещё много ниш, в которых мы можем упростить жизнь пользователей.
  2. Ожидания от уровня сервиса меняются быстро. Ещё пару лет назад мы заказывали суши по телефону и были готовы ждать пиццу три часа. Сегодня пользователь привык получать бургер и любимый боул с авокадо через 40 минут, сделав пару кликов в смартфоне. FoodTech это одна из тех индустрий, где сегодня создаются новые потребительские привычки, и происходит это прямо сейчас!
  3. Количество заказов продолжает расти с колоссальной скоростью. Представьте: за весь 2018 год мы выполнили 4 миллиона заказов, в то время как только за один сентябрь 2019-го 3 миллиона, а уже в марте 2020-го вышли на показатель в 1 миллион заказов в неделю!
  4. С другой стороны, нам нужно быстро реагировать на новые возникающие потребности пользователей. В период пандемии нужно было быстро катить обновления. Организовать быструю доставку продуктов в период самоизоляции. Вводить новые меры для профилактики коронавирусной инфекции, обеспечить безопасность курьеров, клиентов и партнеров. Отменить расчёт наличными.

Почему в России всё по-другому


Конечно же, мы смотрим на опыт компаний на тех рынках, где foodtech более развит в Европе, Юго-Восточной Азии, Индии. Но этот опыт невозможно использовать as is, так как у них другая география и топология, условия, покупательская способность. У нас крупнейшая страна мира по площади, организовать здесь логистику уникальная задача. Инфраструктура наших городов тоже отличается: другое разделение на авто/мото/пешую доставку, другая плотность расположения ресторанов (много ТЦ и отдельных маленьких кафешек).

Работа курьеров на велосипедах в Европе распространенная практика, а у нас для повсеместного внедрения велосипедов нет инфраструктуры, велодорожек. Поэтому большинство курьеров в России пешеходы. А еще есть разница в погодных условиях. Такой зимы, как у нас, нет, например, в Юго-Восточной Азии. Все эти факторы накладывают свои ограничения.

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

Как мы с этим справляемся

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

Для контроля ситуации мы построили системы, которые отслеживают изменения в реальном времени. Например, когда погода меняется пошел дождь и увеличился спрос мы сразу это увидим на мониторингах, изменятся вводные, а наша система может внести коррективы в параметры алгоритмов. А вот более прогнозируемые изменения уже зашиты в работу таких систем. Например, утренние, дневные и вечерние часы имеют разный профиль спроса. Всё это ещё и адаптировано к разным часовым поясам. Подробно об этом рассказали Денис Горев в докладе Автоназначение курьеров в Delivery Club и Николай Архипов в докладе Как мы побеждаем неопределенность в Delivery Club.

При разработке новой функциональности мы используем гипотезы. Мы оцениваем, как изменения в продукте будут влиять на пользователя, проводим исследования, подкрепляем эти результаты теми аналитическими данными, которые у нас уже есть. Делим разработку на этапы, чтобы понять, где можно сделать проще, и быстрее выпустить MVP. Это особенно актуально при выходе на новые вертикали рынка. Чтобы собрать всё это вместе, мы внедрили отдельный процесс построения и проверки гипотез. Про это я подробно расскажу в отдельной статье GIST фреймворк верификации гипотез в Delivery Club.

Структуры команд мы поделили по продуктовым направлениям и провели продуктовую трансформацию. Все команды у нас отвечают за свой продукт или value stream внутри отдельно взятого продукта. Стратегию продуктовых изменений определяет Product Manager. Это позволяет, во-первых, командам более четко фокусироваться на поставленной задаче и добиваться результата в ожидаемые сроки. Во-вторых, позволяет разработчикам лучше понимать конечных пользователей и быть вовлеченными.

Сама трансформация у нас началась в конце 2018 года, а новый процесс разработки закрепился в начале 2019 года. С середины 2019-го у нас идет активный найм. За это время мы выросли в 4 раза, а это +120 человек. Поэтому я бы сказал, что процесс трансформации продолжается до сих пор. О нём я расскажу в отдельной статье.

За десять лет Delivery Club стал лидером доставки еды в России с присутствием более чем в 150 городах, 22 тысячами ресторанов-партнеров и более 5,5 млн заказов ежемесячно. Чтобы быстро реагировать на все изменения, скорость роста количества заказов и новые вызовы, и при этом оставаться лидерами, мы должны понимать свою аудиторию, быть гибкими и адаптивными, ориентироваться на результат и строить такие процессы внутри, которые помогли бы достигнуть этих целей. Всё это находит отражение в нашей культуре.

Особенности культуры Delivery Club Tech


Давайте подытожим, какие есть особенности у современного рынка FoodTech в России:

  • Опыт не всегда можно скопировать.
  • Появляются новые вертикали, рынок быстро меняется.
  • Один из самых быстрорастущих сегментов e-commerce.
  • Формирование новых потребительских привычек.
  • Нужно быстро реагировать на возникающие пользовательские потребности.

Из этих особенностей и строятся основные принципы нашей культуры:



Инженерная культура, в свою очередь, является субкультурой естественным продолжением культуры компании. В ней отражены не только принципы работы, но и то, как мы выбираем технологии, строим архитектуру, нанимаем людей и строим команды. Сейчас я остановлюсь только на одном аспекте: in-house разработке!

Нам важна скорость выпуска фич и качество. Я верю, что этого сочетания можно добиться только при полной вовлеченности сотрудников в процесс, предметную область и платформу. Когда команды плотно общаются между собой и с бизнесом, понимают, как они влияют на развитие продукта, и как выпуск новой фичи влияет на конечного пользователя. При этом вникают в тонкости технической реализации, чтобы при подготовке того же MVP, когда мы делаем довольно быстрые решения, они были ещё и надежными.

Я убежден, что это достижимо только, когда люди мотивированы, правильно погружены в контекст и заряжены на результат. В этом и проявляется в полной мере сила и важность in-house разработки.

Но приложение же работает нормально, зачем вам 170 человек?


Как я писал выше, e-com похож на айсберг, со стороны всегда видна лишь малая верхняя часть. Вся операционная деятельность, которая в основном сосредоточена в оффлайне, скрыта от глаз обычного пользователя.

В основе бизнеса Delivery Club лежат четыре самых важных вектора:

  1. Клиент, который покупает еду.
  2. Доставщик.
  3. Партнёр (ресторан/магазин).
  4. Техподдержка: call-центр и диспетчеры, которые контролируют процесс.

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

В прошлом году мы сформировали ещё два направления: R&D и Platform. Направление R&D решает наукоёмкие задачи, работает с зоной низкой определенности, которая сейчас в основном сосредоточена вокруг логистических задач. Ребята вместе с отделом Операций оптимизируют бизнес-процессы и автоматизируют ручные и рутинные действия.

Команда Platform занимается технологическими улучшениями платформы, меняет архитектуру, чтобы мы могли справляться с возрастающим количеством заказов и нагрузками. Рост заказов настолько стремительный, что мы выросли в целых три раза по сравнению с предыдущим годом. И теперь представьте, как важно поэтапно пересматривать архитектуру, чтобы справляться с постоянно возрастающими нагрузками.

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

Выводы


Рынок быстро меняется, поэтому важно так же быстро адаптировать процессы и технологии под него. Вот, что делаем мы:

  • сохраняем гибкость и меняемся вместе с рынком;
  • генерируем много гипотез и умеем их измерять;
  • делаем процессы прозрачными, чтобы вся команда понимала, что мы делаем и зачем, и как именно фичи влияют на конечных пользователей;
  • непрерывно оптимизируем процессы разработки и релизного цикла, чтобы снизить Time to Market.

Для этого мы выбрали путь in-house разработки. А все особенности рынка FoodTech отразили в своих принципах инженерной культуры. Кстати, вот они, взгляните: tech.delivery-club.ru/culture.

Инженерная культура, в свою очередь, нам подсказывает, какие Soft Skills важны для сотрудников IT-отдела Delivery Club. Эти качества стали основой нашего фреймворка найма.

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

Технические аспекты мы также не пропустим. Отдельную статью посвящу Платформе и Архитектуре. А также отдельно поговорим про Go-Swagger и Kafka Connect.

Надеюсь, мне удалось погрузить вас в контекст foodtech-рынка и объяснить, зачем Delivery Club 170 разработчиков.

Спасибо, что дочитали!
Подробнее..

Категории

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

  • Имя: Макс
    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