Ефективне відлагодження застосунків. Як заощадити час і підвищити якість продукту
Всім привіт! Мене звати Артур Мицко, я Lead Software Engineer у компанії GlobalLogic. Уже понад 12 років я займаюсь тим, що драйвить, — розробкою застосунків для мобільних платформ.
Я розпочав свій шлях в IT, будучи студентом
Згодом я почав перемикатися на інші платформи. Уже за рік у мене була можливість розібратися як з іншими мовами програмування, як-от C# та Java, так і з різними мобільними платформами — J2ME, Windows Phone та Android. І все це в межах одного проєкту 😊
На той час, поглинаючи знання як губка, освоївши ази нової мови і розуміння платформи, я безпосередньо починав працювати над втіленням дизайну в реальність. І як це зазвичай буває, згідно з так званим правилом 80/20 — 20% часу йде на написання 80% програми, а далі починаються години шліфування коду і відлагодження. Хочу наголосити, що саме відлагодження й тестування програми з різними параметрами та розуміння того, як працює система, дало мені експертність у розробці програм під конкретну платформу.
У цій статті я хочу розібрати важливість відлагодження застосунку й поділитися одним із підходів, який може значно покращити ефективність цього процесу.
Насамперед це може бути корисним для розробників під мобільні платформи, проте основні ідеї можна застосувати в різних сферах. Головне — зрозуміти суть. Тож давайте розбиратись.
Важливість налаштування процесу відлагодження та тестування
Чи замислювалися ви колись, скільки разів на день ви запускаєте програму під час активної розробки? А якщо помножити цю цифру на час компіляції змін і встановлення застосунку, вийде досить пристойний проміжок часу. Якось під час інтеграції DexGuard (комерційного інструменту для захисту Android-застосунків) я відчув це повною мірою. Тоді тривалість перекомпіляції застосунку доходила до 10 хвилин.
Звісно, це залежить від підходу до розробки та процесів на вашому проєкті. Наприклад, із підходом TDD (Test Driven Development) це можна мінімізувати. Проте ефективність процесу відлагодження може доволі суттєво впливати на терміни розробки й, відповідно, на якість продукту.
З іншого боку, як це часто буває під час активної фази розробки нового продукту, не завжди всі частини системи доступні або готові. У моїй практиці мені часто доводилося писати front-end частину, коли back-end ще був до цього зовсім не готовий. Щоб бути відносно незалежним від змін і наявності одного з компонентів системи, важливо ефективно налаштувати процес тестування й відлагодження.
Коли продукт стає багатофункціональний, юніт- та інтегрейшн тести не завжди можуть покрити всі сценарії. Часто буває, що для перевірки якогось конкретного випадку потрібно відтворити певний стан застосунку. Під час відлагодження і розробки деякі сценарії доводиться повторювати десятки разів, тому виникає бажання якось це оптимізувати: відкривати відразу той екран чи функціонал, який зараз тестується, пропускаючи попередні дії. Знову ж таки, це заощаджує час і зменшує монотонність ручної роботи.
Один із варіантів, як це часто буває, — тимчасово модифікувати код. Умовно кажучи, відкрити одразу потрібний екран застосунку під час запуску. Ми економимо час, але, з іншого боку, є ймовірність, що ці тимчасові зміни потраплять в основну гілку, якщо ми забудемо їх відкотити. Також ми не можемо ці зміни залишити після відлагодження і тестування певного сценарію. Якщо в майбутньому виникне схожа потреба — доведеться додавати ці тимчасові зміни знову. Проте існують альтернативні рішення.
Підсумовуючи сказане, ось основні моменти, чому налаштування ефективного процесу відлагодження та тестування є важливим:
- Час розробки — звичайно, це основний фактор.
- Гнучкість системи — незалежність від компонент, які можуть бути не готові.
- Ефективність тестування — можливість підлаштовувати систему під конкретний випадок для перевірки сценарію.
Як казав мій викладач в університеті, «програмісти — то ліниві люди», у тому сенсі, що вони завжди прагнуть автоматизації та оптимізації процесів. Розгляньмо один із варіантів, як можна зробити процес відлагодження і тестування більш ефективним.
Developer options як частина додатку
Усім, хто працює з Android-системою, відомо, що таке Developer options — це набір функціоналу в системі, який дозволяє змінювати її поведінку, впливаючи на внутрішній стан або відображаючи параметри. З їхньою допомогою можна ефективно відлагоджувати й тестувати застосунок, встановлюючи певні обмеження (як-от стан мережі чи розширення екрана), а також відображаючи стан застосунку. Наприклад, відображення контурів UI-елементів, чи використання CPU/GPU.
Якщо такі інструменти доступні в системі, то чи можна зробити щось схоже в самому застосунку? Звісно, так, але тут є свої нюанси. Розгляньмо, як таке можна реалізувати.
Нюанси реалізації
Ідея створити в застосунку Developer options доволі проста, проте є певні нюанси, які варто врахувати та коректно реалізувати.
Розмежування функціоналу
На відміну від Developer options в Android-системі, в мобільному застосунку неприпустимо мати таке меню у production версії. Це порушує безпеку й непотрібне звичайним користувачам, оскільки такі інструменти призначені суто для розробників і тестувальників. Тобто перше, що потрібно зробити, — розмежувати функціонал такого меню в окремий білд. І тут є два основні варіанти:
- Використовувати debug версії застосунку.
- Використовувати окремий build variant (flavor).
Перший варіант підходу очевидний: debug build дозволяє маніпулювати застосунком так, як зручно розробникам і тестувальникам, тоді як release build цього функціоналу не має.
У другому варіанті ми виділяємо Developer options меню під окремий flavor. Це дає змогу тестувати як debug, так і release варіанти застосунку. У деяких випадках це може бути важливим, наприклад, при використанні додаткових особливостей для release версії, як-от ProGuard/DexGuard (обфускація коду впливає на роботу саме релізної версії застосунку).
У моїй практиці використання цього меню лише для debug варіантів було цілком прийнятним, тому надалі будемо розглядати реалізацію цього варіанту.
Розмежування debug/release типу функціоналу (зрештою, як і різних product flavors) в Android відбувається через різні набори файлів (source sets)
- src/main — спільний код/ресурси для всіх збірок;
- src/debug — код/ресурси для debug конфігурації;
- src/release — код/ресурси для release конфігурації.
Тобто один і той самий клас можна створити як у debug, так і у release конфігураціях, і реалізувати окремі функціонали: для debug ми показуємо наше меню, а для release — не робимо нічого.
Де розмістити меню і як до нього доступитись — це вже на ваш розсуд, залежно від інтерфейсу. Якщо розглядати підхід Jetpack Compose до побудови графічного інтерфейсу, то один із цікавих варіантів — обгортка навколо Scaffold, де в release версії це оригінальний UI-елемент без жодних змін, а у debug ми додаємо NavigationDrawer меню. Таким чином, доступ до Developer options буде із будь якого екрану — просто свайпом убік.

Структура файлів при debug та release конфігураціях
Приклад можливого вмісту ScaffoldWrapper для debug конфігурації:
@Composable
fun ScaffoldWrapper(
modifier: Modifier = Modifier,
content: @Composable ((PaddingValues) -> Unit)
) {
ModalNavigationDrawer(
drawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
drawerContent = {
ModalDrawerSheet {
DeveloperOptionsContent()
}
}
) {
Scaffold(
modifier = modifier,
content = content
)
}
}
А для release, звісно, буде чистий Scaffold без жодних змін:
@Composable
fun ScaffoldWrapper(
modifier: Modifier = Modifier,
content: @Composable ((PaddingValues) -> Unit)
) {
Scaffold(
modifier = modifier,
content = content
)
}
За аналогією, можна зробити не через NavigationDrawer, а, наприклад, меню в NavigationBar зверху, на якомусь конкретному екрані. Чи навіть через окрему Activity, яку можна викликати через intent ззовні, через action. Відповідно, debug версія матиме цю activity в маніфесті, а release — ні. Тут уже на ваш креатив 🙂
Багатомодульний проєкт
Часто реалізація такого функціоналу передбачає доступ і зміну внутрішнього стану класів і компонентів систем. У великих багатомодульних проєктах, щоб не порушувати Open/Disclose принцип, було б зручно реалізовувати Developer Option функціонал у тому ж модулі. А основний (app) модуль просто збиратиме весь функціонал в одному меню.
Щоб зменшити зв’язність і залежність модулів, є чудовий варіант реалізувати це через dependency injection. Тоді основний модуль просто збиратиме всі provided функціонали, які доступні, і відображатиме їх списком. Для цього можна використати @IntoSet анотацію в hilt/dagger.
Схематично це може буде виглядати так:

Приклад дата класу для опису Developer option:
data class DeveloperOption( val name: String, val content: @Composable () -> Unit, )
А також dependency injection частина:
// DeveloperModule.kt
@Module
@InstallIn(SingletonComponent::class)
class DeveloperModule {
@Provides
@IntoSet
fun provideStorageDeveloperOptions(): DeveloperOption {
return DeveloperOption(
name = "Storage"
) {
// content of Storage Developer Optionі screen
}
}
}
// DeveloperOptionsViewModel.kt
@HiltViewModel
class DeveloperOptionsViewModel @Inject constructor(
val devOptions: Set<DeveloperOption>
): ViewModel() {
// ViewModel content
}
Перспективи та можливості розширення
Розглянемо деякі варіанти можливого developer option функціоналу:
- Preferences / Storage management. Практично будь-яка програма має постійну пам’ять, у якій зберігаються різноманітні дані — від налаштувань (Preferences) до баз даних. По суті, ці дані і є основним джерелом інформації про внутрішній стан програми.
А тепер уявіть, що в будь який момент життєвого циклу програми ви маєте можливість бачити її внутрішній стан, а то й більше — змінювати його. Це відкриває великі можливості як для розробки, так і тестування різних компонент системи. І це набагато швидше, ніж вносити тимчасові зміни в програму та перезапускати застосунок. До того ж, немає ризику, що ці зміни потраплять у production реліз. - App state. Ця опція може бути досить корисною, коли ваш застосунок підтримує setup flow, як-от автентифікацію користувача або логування в системі. Якщо коретно її реалізувати, можна автоматизувати цей процес і, умовно, запускати застосунок з потрібного вам місця. Чи просто мати можливість «скинути» стан програми, зберігши необхідні дані для швидкого налаштування потрібного стану. Clear app storage чи перевстановлення застосунку явно займе більше часу. У перспективі десятків повторень на день це суттєва економія часу, а отже, і збільшення вашої продуктивності.
- Navigation. Можливість відкрити будь який екран/activity в програмі, що надзвичайно економить час. Особливо під час розробки/тестування UI/UX частини.
- Logging. Відображення логів у самому застосунку. Ще один чудовий інструмент для розробника. А можливість зберегти логи та завантажити їх файлом буде прекрасним доповненням для тестувальників під час створення тікета.
- Future toggle. Одним із цікавих рішень може бути можливість вмикати у програмі різні фічі, які ще перебувають у процесі розробки чи alpha/beta тестуванні.
- Server/device mocking. У випадку, коли функціонал програми залежить від інших пристроїв чи серверної частини, досить зручною та корисною може бути функція емуляції (або mocking) залежної частини системи. Особливо, коли вона ще перебуває в розробці або не вірно функціонує на цей момент.
- Push notifications. Якщо у застосунку показуються push notifications, можливість показати їх вручну із різними параметрами також буде корисною для розробки та тестування.
Mocking частин системи
У нових проєктах чи системах, які пишуть з нуля, часто виникає ситуація, коли розробка front-end частини відбувається паралельно із back-end. Це досить обмежує можливості відлагодження програми та тестування різних сценаріїв під час розробки. Або ситуація, коли робота застосунку передбачає взаємодію з якимось пристроєм через BLE. У таких випадках корисно мати можливість імітувати (mocking) непрацюючу/відсутню частину системи. Ба більше, можна імітувати різного роду затримки між взаємодією застосунку та сторонніх компонентів, наприклад, для запитів на сервер. Так можна протестувати випадки, які в реальності досить важко повторити.
Варіанти, як можна реалізувати mocking компонент системи:
- Через dependency injection. Досить коректний з архітектурної точки зору підхід, коли для тестового режиму ми провайдимо кастомну реалізацію класів, які відповідають за взаємодію з потрібною частиною системи. Проте у великих проєктах такі класи можуть бути досить складними, і реалізація повністю внутрішнього стану може потребувати певних зусиль.
- Вбудований хендлер зміни поведінки в production коду. Інколи простіше додати невеличку частину коду в production частину, яка точково змінюватиме поведінку потрібного функціоналу. Саме такий варіант можна пов’язати з відповідним developer option функціоналом, через який робити потрібні налаштування хендлера в реальному часі роботи застосунку.
У випадку хендлера в production коді, щоб мінімізувати вплив на production версію застосунку, можна обгорнути це все через BuildConfig:
if (BuildConfig.DEBUG) {
// execute handler
}
У результаті, під час збірки production (release) конфігурації застосунку, механізми оптимізації коду (R8, DexGuard та схожі) видалять цю частину як ту, що ніколи не буде викликана, через if (false).
Висновки
Процес відлагодження та тестування є невід’ємною частиною життєвого циклу розробки програмного забезпечення. Ефективність цього процесу безпосередньо впливає на час розробки та якість продукту.
Кожен із розробників стикався з цим процесом: ти змінюєш щось незначне в програмі й перезапускаєш її, щоб перевірити, чи працюють ці зміни. І цей цикл є особливо активним під час розробки нового функціоналу. Оптимізація цього процесу за допомогою вбудованого Developer options меню робить його в рази ефективнішим. З власного досвіду скажу, що це суттєво заощаджує час як для розробників, так і для тестувальників.
- Швидше та ефективніше відлагодження — troubleshooting проблеми стає простішим та зручнішим.
- Точкова перевірка — можливість безпосередньо відкрити певний функціонал, оминаючи попередні налаштування, економлячи час та ресурси.
- Експерименти з різними станами — Developer options дозволяє експериментувати з різними станами застосунку, що є особливо корисним під час тестування corner випадків, які буває досить важко відтворити у звичайній роботі системи.
- Mocking частин системи — можливість протестувати функціонал застосунку без залежності від інших частин системи робить процес гнучкішим.
Сподіваюсь, запропонований підхід був для вас корисним.
Есперементуйте. Втілюйте у життя дійсно круті та ефективні рішення, щоб реалізовувати продукти, які змінюють цей світ на краще!
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівЦікава стаття, дякую! Як для non-tech спеціаліст, мені було просто цікаво почитати про те, які складнощі можуть виникати під час розробки, коли ще не готовий бекенд і як можна адаптуватися до ситуації з технічної сторони. І, я думаю, розділ «Mocking частин системи» — це щось що може зрозуміти кожен спеціаліст, який залежить від інших.
Короче кажучи, мені просто було цікаво! Дякую автору! :)
Будь ласка!
Дякую за відгук 😌
Дякую за статтю, запамʼятала що в більшості випадків дебагінг проходить на продакшині. Тепер все стає зрозумілим. Цитую:
«Один із варіантів, як це часто буває, — тимчасово модифікувати код»
Будь ласка. Сподіваюсь почерпнули для себе щось цікаве та корисне
У моїй практиці бували випадки. коли «тимчасовий код для дебагу» попадав в продакшин, правда із мінімальними наслідками :)
Подолала половину! Таку статтю прочитаєш — почнеш андроїд писати ;) стаття дуже Advanced