Реалізація AI-персоналізації головного екрана мобільного додатка
Головний екран — це перше, що бачить користувач при відкритті додатка. Для e-commerce це вибір товарів, для медіаплатформи — добірки, для супер-апп — набір віджетів. Статичний головний екран відображає усередненого користувача, якого не існує. Персоналізація перетворює його на індивідуальний інтерфейс.
Технічне завдання складніше за ленту контенту: тут персоналізується не порядок однотипних елементів, а сама структура екрана — які секції показувати, у якому порядку, з яким контентом всередину кожної.
Рівні персоналізації головного екрана
Секції та їх порядок
Перший рівень — які блоки взагалі показувати. Користувач, який ніколи не відкривав «Акції», не повинен бачити промо-банер на першому екрані. Той, хто регулярно дивиться історії, отримує їх у топі.
Персоналізація порядку секцій — це задача контекстного бандита (contextual bandit). Кожна секція — це «рука бандита». Награда — клік або час взаємодії. Алгоритм UCB або Thompson Sampling балансує探索 (показуємо секції, про які мало даних) та експлуатацію (показуємо секції з високим історичним CTR).
from vowpalwabbit import pyvw
# Contextual Bandit через VowpalWabbit
vw = pyvw.vw("--cb_explore_adf --epsilon 0.1 --quiet")
def get_section_order(user_features: dict, sections: list[str]) -> list[str]:
# формуємо VW-формат запиту
context = f"|user age_group:{user_features['age_group']} time_of_day:{user_features['hour']}"
actions = "\n".join(
f"|section name:{s} historical_ctr:{user_features.get(f'ctr_{s}', 0.1):.2f}"
for s in sections
)
example = f"{context}\n{actions}"
scores = vw.predict(example)
return [s for _, s in sorted(zip(scores, sections))]
Контент усередину секцій
Другий рівень — що показувати всередину кожної секції. «Рекомендовані товари», «Для вас», «Продовжити перегляд» — кожний блок заповнюється через відповідний recommendation API (CF, CB або гібрид з попередніх послуг).
Персоналізовані банери та CTA
Промо-банери з різним текстом та зображеннями для різних сегментів користувачів. Сегментація через кластеризацію (KMeans на поведінкових ознаках) або правилову логіку: частих покупців бачать «Новинки», давно не заходивших — «Ми чекали, ось знижка».
Конфігурація екрана з сервера
Жорстко закодити структуру головного екрана в мобільному клієнті — погана практика. Персоналізована конфігурація приходить з сервера при кожному відкритті додатка:
// Android: HomeScreen конфігурація з сервера
data class HomeScreenConfig(
val sections: List<SectionConfig>
)
data class SectionConfig(
val type: SectionType, // BANNER, PRODUCTS, STORIES, CATEGORIES, CONTINUE_WATCHING
val title: String?,
val items: List<HomeItem>,
val layout: LayoutType // HORIZONTAL_SCROLL, GRID, CAROUSEL
)
// ViewModel завантажує конфіг при старті
class HomeViewModel(private val api: HomeApi) : ViewModel() {
private val _config = MutableStateFlow<HomeScreenConfig?>(null)
val config = _config.asStateFlow()
init {
viewModelScope.launch {
_config.value = api.getPersonalizedHome(userId = currentUser.id)
}
}
}
// Composable рендерить динамічну структуру
@Composable
fun HomeScreen(config: HomeScreenConfig) {
LazyColumn {
items(config.sections) { section ->
when (section.type) {
SectionType.BANNER -> BannerSection(section)
SectionType.PRODUCTS -> ProductsSection(section)
SectionType.STORIES -> StoriesSection(section)
SectionType.CONTINUE_WATCHING -> ContinueWatchingSection(section)
else -> {}
}
}
}
}
Jetpack Compose + LazyColumn із динамічним рендерингом секцій — чисте рішення. Додавання нового типу секції — лише новий when-гілка без зміни layout-логіки.
Аналогічно на iOS через SwiftUI ForEach по конфігурації секцій та @ViewBuilder фабрику.
Кеш та миттєвий старт
Конфігурація кешується локально. При наступному відкритті — миттєво показуємо попередню конфігурацію, паралельно завантажуємо нову. Коли нова приходить, оновлюємо екран. Це паттерн stale-while-revalidate: користувач ніколи не бачить порожній екран.
// iOS: stale-while-revalidate для конфігурації головного екрана
func loadHomeConfig() {
// одразу показуємо кеш
if let cached = configCache.load() {
homeConfig = cached
}
// оновлюємо у фоні
Task {
let fresh = try await api.getPersonalizedHome()
configCache.save(fresh)
homeConfig = fresh
}
}
Процес роботи
Аудит поточної структури головного екрана та доступних сигналів персоналізації.
Проектування server-driven UI протоколу — формат конфігурації, типи секцій.
Реалізація алгоритму рейтингу секцій (від простих правил до contextual bandit).
Розробка клієнтського рендерера динамічних конфігурацій.
Метрики: клік за секціями, глибина скролінгу першого екрана, повернення на наступний день.
Орієнтири за часом
Server-driven UI з простою персоналізацією на правилах — 1–2 тижні. Contextual bandit для рейтингу секцій + повний динамічний рендерер — 3–5 тижнів.







