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

Конструктор приложений

Свой сервис отложенного постинга и почти без кода

18.12.2020 10:08:54 | Автор: admin

Если вы владеете Telegram-каналом (или несколькими), раскрученным аккаунтом в Instagram или любой другой социальной сети, то уже наверняка задавались вопросом: А как мне планировать посты заранее? Существует очень много разных сервисов, которые решают эту задачу. Но по тем или иным причинам они могут не подходить: где-то цена большая, где-то функционал беден, а где-то вообще страшно оставлять логин-пароль от своего раскрученного аккаунта. Сегодня я расскажу и покажу как на основе нашей платформы для разработки бизнес приложений с открытым кодом Orienteer сделать свой собственный сервис буквально за 60 минут! Заинтересовал? Проваливаемся под кат.



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

Шаг 1: Запускаем Orienteer



Нам понадобится docker, а еще лучше вместе с docker-compose. Можно его использовать как на локальном компьютере, так и на любом доступном хостинге, включая AWS или DigitalOcean. Если не знакомы с docker, то крайне рекомендую потратить 3-5 часов своей жизни и изучить основы. Хабр, medium или даже первоисточник вам в помощь.

Запускаем сам Orienteer, например вот так:
docker run -p 8080:8080 orienteer/orienteer

Но чтобы не потерять базу данных при обновлении контейнера и обезопасить от проникновения под стандартными паролями можно усложнить запуск следующим образом:
docker run -p 8080:8080 -v <runtime>:/app/runtime -e ORIENTDB_ADMIN_PASSWORD=<password> -e ORIENTDB_GUEST_PASSWORD=<password> orienteer/orienteer

Если же вы любите docker-compose, то вот вам шаблон для старта:

version: '2.1'services:   orienteer:      image: orienteer/orienteer:latest      container_name: my_posting_service      network_mode: 'bridge'      ports:          - "8080:8080"      volumes:          - ./runtime:/app/runtime          - ./maven-repository:/app/repository      environment:          - ORIENTDB_ADMIN_PASSWORD=<Admin Password>          - ORIENTDB_GUEST_PASSWORD=<Guest Password>          - JAVA_TOOL_OPTIONS= -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/runtime/heapdump.bin

Если вы всё сделали правильно, то при открытии localhost:8080 вы должны увидеть что-то вроде снимка ниже. Да, по умолчанию вы попадаете в Orienteer как пользователь `reader`, который умеет всё читать, но не изменять. Права у reader'а можно позже отнять, чтобы backend могли видеть только пользователи, вошедшие в систему.



Кликаем на правый верхний угол и входим в систему как `admin` (по умолчанию пароль тоже `admin`).



Шаг 2: Подключаем необходимые модули и зависимости



Для того чтобы превратить Orienteer в сервис по отложенному постингу нам понадобится:

  • java-telegram-bot-api простая java библиотека для работы с Telegram
  • orienteer-architect модуль для проектирования предметной области
  • orienteer-devutils модуль для удобного отлаживания скриптов

После разработки, последние 2 модуля, кстати, можно будет отключить.
Идем на страницу Schema, затем на вкладку Artifacts. Нажимаем Add. Для добавления библиотеки java-telegram-bot-api вводим все так же, как если бы подключали обычную Java библиотеку:

<dependency>   <groupId>com.github.pengrad</groupId>   <artifactId>java-telegram-bot-api</artifactId>   <version>5.0.0</version></dependency>


Должно получиться вот так:



А чтобы добавить 2 последних модуля, нажимаем вновь Add, а затем на кнопку Available Orienteer Modules. После загрузки списка выбираем нужный модуль и нажимаем Install as Trusted. После добавления всех нужных зависимостей жмем оранжевую кнопку Reload Orienteer. После перезагрузки можем двигаться дальше.



Шаг 3: Моделируем нашу предметную область


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

  • Контент (или пост) то что мы собираемся послать в нужный канал в определенное время
  • Канал то куда мы собираемся послать наш контент
  • Бот сущность, осуществляющая посылку контента


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

Для моделирования предметной области рекомендую использовать модуль orienteer-architect. Идем в ODataModel по кнопке Browse, нажимаем Create, вписываем `Scheduler Data Model` в поле Name и нажимаем Save. А дальше чистое творчество! Но думаю, в конце у вас должно получиться что-то вроде данной картинки:



Рекомендую после сохранения и применения вашей предметной области (кнопки Save Data Model и Apply Changes) пройтись по созданным классам и их полям и доконфигурировать для большего комфорта такие вещи, как:

  • Поле определяющее имя документа (Document Name Property)
  • Поле определяющее ссылку на документ более высокого уровня (Parent Document Property) влияет на строчку навигации
  • Тип визуализации (Visualization) как именно отображать значения


Шаг 4: Оживляем наш сервис скриптами



Нам понадобится всего 2 скрипта.

  1. Scheduler скрипт, который по запуску каждую минуту будет искать следующую порцию сообщений на отправку, находить по ссылке конкретный скрипт по отправке сообщения и вызывать его, передавая сообщение, которое надо отправить, как аргумент
  2. SendToTelegram скрипт, который получает сообщение как аргумент и, используя Telegram API, отправляет его в нужный канал или чат


Скрипты в Orienteer (и OrientDb) это объекты класса OFunction. Соответственно, вам надо будет создать объект данного класса с нужным именем, кодом и языком, на котором последний написан. Я рекомендую указывать nashorn это Java реализация языка JavaScript.
Не буду сильно томить объяснениями: Лучше один раз увидеть код, чем 100 раз его обсудить.

Scheduler


var db = orient.getDatabase(); //Получаем базу данных OrientDBvar resultSet = db.query("select from SContent where published!=true and when < sysdate()"); //Запрашиваем все сообщения, которые еще не опубликованы, но ожидаемая дата публикации уже в прошломfor(var i in resultSet) { //Итерируемся по результатам   var content = resultSet[i]; //Очередное сообщение на отправку   var sendFunction = content.field("channel.bot.sendFunction.name"); //Через цепочку ссылок получаем имя скрипта, который ответстеннен за отправку   if(sendFunction) {      db.getMetadata().getFunctionLibrary().getFunction(sendFunction).execute(content); //Вызываем функцию по имени и передаем наше сообщение на отправку как аргумент      content.field("published", true); //Помечаем наше сообщение как отправленное      content.save(); //Сохраняем   }}

Как видно, скрипт очень простой, и в нем нет никакой специфики Telegram: все скрыто в скрипте, который мы указываем при конфигурации бота.

SendToTelegram



Помните, мы раньше подключили библиотеку java-telegram-bot-api? Теперь самое время её задействовать для посылки сообщения в Telegram!

var content = orient.getDatabase().load(content); //На случай, если в качестве аргумента пришла ссылка, а не сам объект, подгружаем объект из базыvar channel = content.field("channel"); //Получаем наш канал (или чат), куда будем отправлять сообщениеvar botDoc = channel.field("bot");//Получаем документ с конфигурацией бота. Именно с этого документа ранее мы считали "sendFunction" - скрипт по отсылке сообщенияvar botKey = "Bot"+botDoc.getIdentity(); //Генерируем уникальный ключ для сохранения в окружении TelegramBotvar app = org.orienteer.core.OrienteerWebApplication.lookupApplication(); //Находим Orienteer Web Application как Java объектvar bot = app.getMetaData(botKey); //Пытаемся получить бота по ключуif(!bot) { //Если бот не найдет - инициализируем егоbot = new com.pengrad.telegrambot.TelegramBot(botDoc.field("token")); //Создаем объект TelegramBot и передаем ему token нужный для работыapp.setMetaData(botKey, bot); //Сохраняем бота под Orienteer Web Application, чтобы следующий раз не создавать его заново}bot.execute(new com.pengrad.telegrambot.request.SendMessage(channel.field("chatId"), content.field("content"))); //Отсылаем сообщение, передавая в API само сообщение и chatId


Но как же нам вызывать скрипт Scheduler каждую минуту? Для этого в Orienteer (и OrientDB) используются объекты класса OSchedule. Создаем объект данного класса, указываем произвольное имя, выбираем наш скрипт Scheduler в поле Function, а затем указываем следующее значние в поле Rule: `0 * * * * ?`. Данная запись означает вызов скрипта в нулевую секунду каждой минуты, каждого часа и т.д.

Шаг 5: Тестируем наше творение



А теперь самое интересное! Приступаем к тестам.

Создаем бота



Нам надо создать самого бота в Telegram, а затем сконфигурировать объект бота с его конфигурацией в Orienteer.
Как создать бота в Telegram и получить token для него можно почитать здесь. Если коротко:

  1. Связываемся с BotFather
  2. Посылаем команду /newbot
  3. Указываем отображаемое имя для бота
  4. Указываем имя пользователя для бота

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

Далее идем в наш Orienteer и создаем документ класса SBot, который мы смоделировали прежде в шаге 3. Указываем имя, ссылку на наш скрипт SendToTelegram и непосредственно token, который мы получили из общения с BotFather прежде.



Создаем канал



Я предполагаю, что в Telegram у вас уже создан канал или чат. Если нет, то уверен, что вы с легкостью справитесь с этим. Для того чтобы правильно сконфигурировать канал в Orienteer, нам понадобится так называемый chatId. Проще всего это сделать с помощью бота GetIDs Bot. Если вы настраиваете своего бота для канала, то просто перешлите ему любое сообщение из канала. А если для группы, то добавьте данного бота в группу и первым сообщением он вам выдаст ChatId, затем его можно из группы удалить.




Если вы всё сделали правильно, то у вас есть ChatId, и всё, что осталось сделать, это пойти в Orienteer, создать объект из нашей предметной области SChannel, указать имя и данный ChatId. Ну и не забываем добавить Telegram бота в канал или чат, а так же указать ссылку на бота в Orienteer, который мы уже сконфигурировали.

Создаем сообщения


А теперь момент истины: создаём сообщения и тестируем их появление в канале или группе. Для сообщений мы уже создали класс SContent. В объекте этого класса указываем название (не публикуется в Telegram), ссылку на желаемый канал или чат, время публикации, ну и, собстенно, само сообщение.



Если все сделали правильно, то в обозначенное время сообщение будет послано.



Ура! Нашим сервисом можно пользоваться!

На этом на сегодня всё!
Если вам нравятся наши статьи, то дайте, пожалуйста, нам знать об этом плюсом или комментарием.

P.S. Если что-то не получилось по данной инструкции или же хотите воспользоваться уже сконфигуренным Orienteer'ом, то пишите нам: либо здесь, либо в Telegram.
P.P.S. Нам в проект Orienteer нужны люди! Даже если вы только учитесь и еще толком не погрузились в Java или JavaScript, но хотели бы поучавствовать то ничего страшного пишите! А мы научим!
Подробнее..

Системный гайд по созданию White Label android-приложений

07.02.2021 14:05:43 | Автор: admin

Как написать код один раз, а продать 20 мобильных приложений? Мы нашли ответ путём проб и факапов и разложили опыт по пунктам: из статьи вы узнаете, как безболезненно реализовать White Label android-проект.

Greetings and salutations! По работе я однажды получил крутую задачу по разработке White Label android-приложения. Изучил достижения коллег в этой области и нашёл только:

  • входные гайды (раз, два, три, etc) о механизмах, но без промышленного дизайна;

  • статьи, в которых освещены узкие аспекты задачи (раз, два, etc).

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

1 Ставим задачу

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

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

Бюджет ограничен... фичи типовые... да здравствует конструктор приложений! Или White Label продукт? Пока отложим термины и опишем задачу: генерировать приложения из единой кодовой базы, каждое с дизайном под бренд клиента и только нужными ему фичами.

Задача: создавать приложения для разных клиентов из единой кодовой базы

1.1 Визуализируем решение

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

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

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

Как реализовать такой проект без боли? Прочитайте статью и найдёте ответ.

1.2 Детализируем требования

Разложим видение по полочкам: как в ТЗ, но проще.

Функциональные требования

  1. Реализовать общие модули фичей:

    • новости клиент узнаёт об акциях и жизни сети магазинов;

    • лояльность получает дисконтную карту, узнаёт баланс, пробивает на кассе;

    • ...

  2. Задавать отдельно для каждого приложения:

    • наборы фичей, чтобы выбирать сами модули и настраивать их параметры;

    • бренд, чтобы настраивать цвета и менять ресурсы: шрифты, картинки, зашитый контент.

Нефункциональные

  • у приложений должен быть общий код;

  • настройка нового приложения меньше четырёх часов разработчика;

  • архитектура должна упрощать расширение модулей и поддержку от 10 до 100 приложений.

1.3 Что пилим то? Конструктор? White Label?

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

  1. Что даёт конструктор/платформа:

    • универсальный сервис сборки приложений из готовых компонентов;

    • например, AppGyver dragndrop вёрстка, программирование на низкоуровневых фичах (открыть экран, сделать фото);

    • творим что угодно от приложений по покупке золота до приёмки грузов.

  2. Что даёт White Label:

    • конструктор для конкретного типа приложений, например для такси;

    • ребрендинг под клиента и настройка высокоуровневых фич (новости, профиль)

Наш фокус на системах лояльности. Значит, делаем White Label. Гуглим white label android development и находим то, что нужно.

2 Проектируем и воплощаем

Строим системную схему White Label приложения

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

и получим четыре жирные проблемы:

  1. Как шарить кодовую базу между приложениями?

  2. Как сделать ребрендинг?

  3. Как задавать конфиги?

  4. Как отключать ненужные модули и настраивать необходимые?

В очередь, проблемы, в очередь!

2.1 Шарим код

Задача одна кодовая база, до 100 приложений. Решение Gradle Product Flavors.

Если вы ещё не знакомы с Gradle Product Flavors, советую почитать документацию или общие статьи. А можно и сразу в контексте White Label: кратко или в формате инструкции

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

Главное преимущество. Относительная простота переиспользования кода и ресурсов, удобство сборки.

Главный недостаток. Если вариантов больше 100, то в проекте и конфигах будет тяжело ориентироваться. Но у нас меньше, поэтому ок.

Альтернативы, на мой взгляд, рассматривать нет смысла: решение надёжное, из коробки.

Пример flavors. Допустим, на старте делаем два приложения:

  1. Лояка абстрактная компания;

  2. Ювелирия сеть ювелирных магазинов.

Назовём flavors соответственно loyaka и jewelry. Сразу реализуем best practice конфиг каждого flavor вынесем в отдельный файлик. Зачем? Станет ясно чуть позже.

Пока создадим:

  1. папку project_flavors;

  2. в ней gradle-скрипты flavor_loyaka.gradle, flavor_jewelry.gradle и flavors_common.gradle;

  3. задействуем скрипты в build.gradle уровня app.

Здесь и далее привожу сокращённые примеры из тестового проекта к статье.

flavor_loyaka.gradle

apply from: "$rootDir/project_flavors/flavors_common.gradle"  android {    productFlavors {        loyaka {            dimension APP_DIMENSION            resValue "string", APP_NAME_VAR, 'Лояка'            applicationId BASE_PACKAGE + 'loyaka'        }    }}

flavor_jewelry.gradle

apply from: "$rootDir/project_flavors/flavors_common.gradle"  android {    productFlavors {        jewerly {            dimension APP_DIMENSION            resValue "string", APP_NAME_VAR, 'Ювелирия'            applicationId BASE_PACKAGE + 'jewelry'        }    }}

flavors_common.gradle

android {    ext.DIMENSION_APP = "app"    ext.APP_NAME_VAR = "app_name"    ext.BASE_PACKAGE = "com.livetyping."}

Наконец, задействуем flavors в build.gradle уровня app:

...apply from: "$rootDir/project_flavors/flavor_loyaka.gradle"apply from: "$rootDir/project_flavors/flavor_jewelry.gradle"apply from: "$rootDir/project_flavors/flavors_common.gradle"android {    ...    flavorDimensions APP_DIMENSION}...

2.2 Перекрашиваем

2.2.1 Концепт

У каждого приложения свой бренд, который складывается из:

  • цветовой схемы;

  • шрифтов, картинок, строк;

  • зашитого контента (соглашений, ссылок в соц. сети).

Благодаря flavors тоже решим задачу просто. Загрузим в голову 3 факта:

  1. общие код и ресурсы проекта лежат в папке main;

  2. для gradle main это как дефолтный flavor;

  3. у каждого flavor свои исходники. Например, общие ресурсы лежат в main/res, а специфичные для флэйвора loyaka в loyaka/res;

Что произойдёт, если в main/res и loyaka/res будут картинки с одинаковым именем animal.webp? Возникнет конфликт, и чтобы решить его, Gradle переопределит базовые ресурсы кастомными. Если непонятно, поможет диаграмма:

Слева ресурсы по flavor; справа итоговый APK.Слева ресурсы по flavor; справа итоговый APK.

Задача решена! Уберём дефолтные ресурсы в main, а в конкретных flavor будем переопределять по необходимости.

2.2.2 Best practices

Крайне важно заранее договориться с дизайнерами:

  • ресурсы в приложениях называем одинаково вставляем в проект прямиком из дизайна;

  • тему задаём чётким набором цветов для перекрашивания копируем colors.xml в новый flavor и просто меняем значения.

И, конечно, соблюдаем договорённости, ведь впереди ждут испытания. Например, мы сразу решили, что задаём строгий набор цветов. Однако в очередном приложении цвета не сошлись часть элементов цвета primary в новом дизайне стали accent. Сразу обсудили и изменили дизайн, а ведь могли бы и вставить костыль.

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

Создать и поддерживать чёткую схему для всех приложений на порядок дешевле, чем расширять код, состоящий из уникальных косяков.

2.2.3 Пример схемы цветов

Задаём цвета бренда в файле project_styleguide.xml:

<?xml version="1.0" encoding="utf-8"?><resources>    <color name="active">#68b881</color>    <color name="background">#36363f</color>    <color name="disabled">#daede0</color>    <color name="field_dark">#f5f5f5</color>    ...</resources
<?xml version="1.0" encoding="utf-8"?><resources>    <color name="active">#a160d5</color>    <color name="background">#f6ebff</color>    <color name="disabled">#e2c8f6</color>    <color name="field_dark">#f5f5f5</color>    ...</resources>

2.3 Задаём конфиг

2.3.1 Концепт

Фичи настраиваем на двух уровнях:

  1. отключаем ненужные модули;

  2. меняем параметры внутри самих модулей.

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

Упрощённый пример дока в формат модуль-фича-параметры:

  1. Подключаемые модули:

    • лояльность;

    • новости;

  2. Аутентификация:

    • логин пользователя: телефон или email;

    • маска логина.

  3. Карта лояльности:

    • тип штрих-кода: EAN-8, EAN-13, CODE-128.

2.3.2 Пути решения

Как сделать качественный конфиг? Само качество определим так:

  1. удобство работы простота чтения, простота заполнения (в идеале, хотим DSL);

  2. Скорость обработки важно, чтобы чтение конфига не тормозило приложение.

Выделим основные пути:

  1. Gradle buildConfigField

    • задаём переменные в gradle скрипте;

    • во время компиляции генерится java класс BuildConfig, переменные доступны как его поля.

  2. JSON

    • json объект в файле;

    • зашит локально, либо получаем с сервера.

Кратко оценим пути по критериям.

2.3.3 Путь 1. Gradle buildConfigField

Плюсы:

  • удобство создания делаем DSL на минималках: выносим типы и возможные значения параметров в переменные; выявляем синтаксические ошибки на компиляции;

  • простота большинству уже знаком;

  • скорость обращаемся к классу BuildConfig в памяти.

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

Пример переменной на условном DSL:

buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS

2.3.4 Путь 2. JSON

Плюсы:

  • удобство чтения особенно в формате HOCON;

  • удобство создания делаем DSL через JSON Schema, проверяем на ошибки по мере написания;

  • переиспользование шарим между iOS и Android.

Минусы:

  • скорость придётся перед запуском считать из файла или получать с сервера;

  • время на освоение по сравнению с первым вариантом JSON Schema наверняка менее популярна.

2.3.5 Так что же лучше?

Когда делали проект, даже не изучали альтернативы. Сразу сделали через Gradle. На мой взгляд, JSON + Schema его побеждает. Удобство чтения приоритет, при этом удобство создания остаётся на том же уровне, если не лучше. Дополнительная секунда для загрузки файла на общем фоне незначительна.

Сделали конфиг через Gradle, не изучая альтернатив. Но оказалось, что JSON Schema удобнее для чтения и это её главное преимущество.

2.3.6 Best practices для buildConfigField

Если выбрали buildConfigField, то в сыром виде с ним будут проблемы:

  1. чтобы использоватьEnum, придётся указать полный путь к пакету как в типе, так и в значениях;

  2. при изменение имени или типа переменной придётся делать Find & Replace по всем конфигам.

Решение: DSL на минималках. Заводим переменные для названий параметров, а также кастомных типов и вариантов значений. Создаём отдельный gradle-скрипт на каждый модуль. Параметры описываем в формате экран-параметр-переменные. Скрипты кладём в папку business_rules.

Пример: модуль лояльности loyalty_business_rules.gradle:

/*_______________ENTER USER ID________________*//*________User ID________*//*__Variable__*/ext.USER_ID_VAR = "USER_ID"ext.USER_ID_TYPE = "com.example.whitelabelexample.domain.models.UserIdType"/*__Values__*/ext.UI_PHONE = USER_ID_TYPE + ".PHONE"ext.UI_EMAIL = USER_ID_TYPE + ".EMAIL"/*_______________NO CARD________________*//*________Obtain card methods________*//*__Variable__*/ext.OBTAIN_METHODS_VAR = "OBTAIN_CARD_METHODS"ext.OBTAIN_METHODS_ENUM = "com.example.whitelabelexample.domain.models.ObtainCardMethod"ext.OBTAIN_METHODS_TYPE = "java.util.List<" + OBTAIN_METHODS_ENUM + ">"/*__Optional values__*/ext.OM_GENERATE = OBTAIN_METHODS_ENUM + ".GENERATE_VIRTUAL"ext.OM_BIND = OBTAIN_METHODS_ENUM + " .BIND_PHYSICAL"...

UI_PHONE что за UI_? Это сокращение переменной UserId: добавляем префиксы, чтобы избежать коллизий.

Дальше настраиваем приложения в скриптах flavor, которые на первом шаге заботливо вытащили по файлам.

Пример: flavor_loyaka.gradle:

...loyaka {    ...    /* MAIN SCREEN */    buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_CARD    /* MODULES */    buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOWCASE)    /* REGISTRATION */    buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_EMAIL    ...}

flavor_jewelry.gradle:

...jewelry {    ...    /* MAIN SCREEN */    buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS    /* MODULES */    buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOPS)    /* REGISTRATION */    buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_PHONE    ...}

2.3.7 Получаем доступ к конфигу

Спроектируем решение в контексте Clean Architecture.

Классы конфигов приравниваю к источникам в слое data, ибо они только предоставляют данные. Тогда ui получает параметры посредством domain.

Как сгруппировать параметры по классам? Мы на проекте за группу взяли экран, а зря. С одной стороны, компактные классы, а с другой возникли коллизии, например на экранах ввода номера телефона и подтверждения кода нужна маска номера. Пришлось реализовать считывание из конфига 2 раза.

С BuildConfig это легко, но с JSON будет грязно. Считаю, что оптимально группировать по процессу (флоу). Под процессом здесь понимаю целевой use case и вторичные по отношению к нему. Обычно это группа экранов, например в модуле лояльности два целевых процесса:

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

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

Пример реализации конфига для второго процесса: фрагмент BuildCardConfig.kt:

class BuildCardConfig : CardConfig {    override fun numberMask(): String = BuildConfig.CARD_NUMBER_MASK    override fun barcodeType(): BarcodeType = BuildConfig.BARCODE_TYPE    override fun obtainmentMethods(): List<ObtainCardMethod> = BuildConfig.OBTAIN_CARD_METHODS    ...}

В итоге получим архитектуру работы с конфигом (диаграмма классов UML; в ui MVVM):

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

2.3.8 Валидируем конфиг

Зачем нужна прослойка в domain? Она же будет пустая! Необязательно. В идеале, хотим защиту от дурака проверку параметров фичей на непротиворечивость. Допустим, дано 2 параметра:

  1. включённые модули;

  2. главный экран.

Если модуль новости и акции выключён, то логично, что главным экраном новости быть не может. Но на уровне Gradle или JSON Schema подобное ограничение сделать нетривиально таким правилам и место в domain.

Например, реализуем описанное условие в GetMainTabUseCase.kt:

class GetMainTabUseCase(    private val mainConfig: MainConfig) {    operator fun invoke(): NavigationTab {        val mainTab = mainConfig.mainTab()        val mainModule = tabsByModules.entries.find { it.value == mainTab }!!.key        val isModuleEnabled = BuildConfig.APP_MODULES.contains(mainModule)        if (isModuleEnabled.not()) {            throw IllegalStateException("Can't use a tab ($mainTab) as main, it's module is disabled   fix config!")        }        return mainTab    }}

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

Альтернатива создавать UseCase только по надобности, но тогда возникает неоднородность: в ui используются одновременно и Config и UseCase. Рискуем использовать параметры, которые требуют валидации, в её обход, и следом за этим растёт вероятность багов.

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

2.4 Настраиваем фичи

2.4.1 Выбираем модули

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

Модуль здесь крупный связный и независимый кусок функциональности.

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

Рассмотрим главные точки связи модуля с приложением:

  1. Переходы изui боттом навигация, рандомная кнопка, etc;

  2. Реакция на события кастомные (выбран город), платформы (найдена сеть), etc.

По опыту, обычно хватит убрать пункт модуля из меню, например, для лояльности это таб из боттом навигации. Я реализовал пример в MainViewModel и MainActivity тестового проекта.

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

На события в Лояке реагирует только модуль пушей. Когда юзер выбирает свой город подписываемся на соответствующий новостной канал. Опять же обрабатываем каждый кейс.

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

2.4.2 Настраиваем экраны и бизнес-правила

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

На уровне UseCase берём параметр из нужного класса Config.

GetCardUseCase.kt:

class GetCardUseCase(    private val netRep: CardNetRepository,    private val storageRep: CardStorageRepository,    private val config: CardConfig) {    operator fun invoke(): Card? {        return if (config.isCacheCard()) {            try {                val card = netRep.getCard()                storageRep.save(card)                card            } catch (exception: Exception) {                return storageRep.get()            }        } else {            netRep.getCard()        }    }}

В ui же обращаемся к UseCase на уровне ViewModel или Presenter.

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

Реализация: NoCardViewModel.kt:

class NoCardViewModel(    private val getObtainMethodsUseCase: GetObtainMethodsUseCase,    ...){    private val cardObtainMethods by lazy { getObtainMethodsUseCase() }    val isShowGetVirtualButton by lazy {        cardObtainMethods.contains(ObtainCardMethod.GENERATE_VIRTUAL)    }    val isShowBindPlasticButton by lazy {        cardObtainMethods.contains(ObtainCardMethod.BIND_PHYSICAL)    }    ...}

fragment_nocard.xml:

...<com.google.android.material.button.MaterialButton    android:id="@+id/no_card_bind_plastic_button"    ...    app:isVisible="@{viewmodel.isShowBindPlasticButton}" /><com.google.android.material.button.MaterialButton    android:id="@+id/no_card_get_virtual_button"    ...    app:isVisible="@{viewmodel.isShowGetVirtualButton}" />...

2.4.3 Ещё один трюк

Иногда вариативную вёрстку целесообразнее сделать без конфига.

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

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

3 Подведём итог

Мы успешно спроектировали архитектуру White Label android-проекта, которая соответствует поставленным требованиям, а именно позволяет:

развивать общую кодовую базу расширять модули фичей и собирать из одного кода разные приложения, от 10 до 100;

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

Горькими уроками поделились, best practices передали. Надеюсь, наш опыт создал цельное представление о создании White Label android-приложений и комфортную отправную точку для вашего проекта.

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

Если возникли вопросы пишите в комменты, буду рад ответить!

4 Куда развить решение?

  • Мы продаём целую систему, а что если продавать модули в другие приложения? Тот же White Label, но на системный уровень ниже. Мы такую задачу решали, если интересно напишите в комментах, расскажем.

  • Когда количество приложений растёт, хочется CI и CD. В этом репозитории есть подробный гайд по настройке Azure Devops.

  • Если не нужна детальная настройка фичей, а писать flavors руками надоело сделайте автогенерацию flavors по json конфигу.

  • Бизнес бьёт ключом, клиентов больше сотни? Пора автоматизировать создание приложений.

Если знаете кейсы, на которые нет ссылок в нашей статье, то обязательно скиньте их в комменты вместе мы точно соберём крутую библиографию!

P.S. Shout-out дорогим коллегам за работу над проектом и помощь в написании статьи - без вас это было бы невозможно :)

Подробнее..

Категории

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

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