Меню
Обвіска або смерть: Як я приручив Gemini CLI в Android-проєкті

Обвіска або смерть: Як я приручив Gemini CLI в Android-проєкті

від Nikolay Vlasov

Генерувати код стало занадто просто.

У мене був показовий досвід: в одному pet-проєкті я спробував вайбкодинг майже без рев'ю — за принципом «що агент написав, те й приймаємо». Спочатку це здавалося швидким і зручним. Але досить швидко проєкт почав розвалюватися: код став громіздким, погано підтримуваним, частина функціональності ламалася, з'явилися затримки, витоки пам'яті й загалом увесь набір проблем, який зазвичай накопичується місяцями — тут він виник майже відразу.

У якийсь момент стало очевидно: проблема не в тому, що агент пише «поганий» код. Проблема у швидкості, з якою цей код накопичується без контролю.


Що ламається при такому підході

Коли прибираєш ручний контроль, починають проявлятися типові патерни:

  • обхід архітектурних шарів;
  • дублювання логіки;
  • розмиті межі модулів;
  • випадкові залежності;
  • ускладнення коду без потреби;
  • поступова деградація продуктивності.

Важливо, що все це відбувається не одномоментно. Кожен окремий крок виглядає «нормально», але сумарно система швидко виходить з-під контролю.


Чому одного промпту виявилося недостатньо

Моя перша спроба була цілком очевидною — я зібрав великий rules.md, куди виніс:

  • архітектурні обмеження;
  • naming conventions;
  • правила шарів;
  • локальні домовленості проєкту.

Це частково допомогло, але не розв'язало проблему.

На практиці виявилося:

  • довгий контекст працює нестабільно;
  • частина правил з часом ігнорується;
  • модель не завжди послідовно застосовує обмеження;
  • вартість і час відповіді зростають разом із розміром промпту.

У підсумку я дійшов прагматичного висновку: Важливі правила потрібно перевіряти, а не тільки описувати.


1. Konsist: архітектура як виконуваний контракт

Архітектурні обмеження та контроль ШІ в Android-розробці
Архітектурні обмеження (guardrails) створюють безпечний контур для роботи ШІ-агента

Агент за замовчуванням вибирає найпростіший шлях реалізації. Якщо можна обійти шар — він це зробить.

Щоб це обмежити, я почав описувати архітектуру через тести за допомогою Konsist (інструмент для перевірки архітектурних правил Kotlin-коду через тести).

Приклад:

@Test
fun `use cases should have UseCase suffix and reside in domain package`() {
    val classes = Konsist.scopeFromProject().classes()

    val violations = classes
        .filter { classDeclaration ->
            classDeclaration.name?.endsWith("UseCase") != true ||
            !classDeclaration.resideInPackage("com.core.domain")
        }

    if (violations.isNotEmpty()) {
        val message = buildString {
            appendLine("VIOLATION: UseCase naming conventions not followed")
            appendLine("FIX: Rename class to end with 'UseCase'")
            appendLine("FIX: Move class to com.core.domain package")
        }
        fail(message)
    }
}

Тут для мене важливі дві речі:

  1. Перевірка фіксує порушення;
  2. Повідомлення дає зрозумілий напрям, як його виправити.

Агент починає працювати не «у вакуумі», а в контурі зворотного зв'язку.


2. Стиснення логів: менше шуму — швидші ітерації

Одна з проблем, з якою я зіткнувся — обсяг логів.

Якщо віддавати агенту повне виведення Gradle або JUnit, він просто губиться в цьому обсязі. Контекст заповнюється шумом, а не сигналом.

Тому я зробив простий шар стиснення:

  • залишаю тільки тести, що впали;
  • беру коротке повідомлення про помилку;
  • обмежую стектейс;
  • прибираю все зайве.
Фільтрація та стиснення логів для ефективної роботи контекстного вікна LLM
Стиснення логів: фільтрація «шуму» дає змогу моделі фокусуватися на реальних помилках

Приклад:

def parse_xml_reports(root_dir):
    summary = []

    for testcase in root.findall(".//testcase"):
        failure = testcase.find("failure")
        if failure is not None:
            message = failure.get("message", "No message")
            text = failure.text or ""

            stacktrace = "\n".join(text.strip().split("\n")[:15])

            summary.append(f"FAILED: {testcase.get('name')}")
            summary.append(f"Message: {message}")
            summary.append(f"Stacktrace:\n{stacktrace}\n")

    return summary

Після цього цикл «зламалося → полагодили» став помітно швидшим і передбачуванішим.


3. Spotless та detekt: базова гігієна без участі людини

Наступний шар — автоматична перевірка якості:

  • Spotless (універсальний інструмент для автоматичного форматування коду);
  • detekt (статичний аналізатор для пошуку потенційних проблем і складних конструкцій у Kotlin).

Я перестав сприймати це як «додаткові інструменти». Це просто частина процесу.

Якщо код не проходить ці перевірки — він не вважається готовим. Агент сам повертається і править.

Це прибирає:

  • дрібні правки на рев'ю;
  • суперечки про стиль;
  • поступову деградацію читабельності.

4. Переклади: усунення механічних помилок

strings.xml виявився несподіваною точкою проблем.

LLM регулярно помиляються з:

  • апострофами;
  • лапками;
  • escape-послідовностями.

Я не намагався «навчити» модель цьому через текст. Простіше виявилося додати:

  • перевірку;
  • автоматичне виправлення;
  • роботу через парсинг XML.

Важливо: без грубих глобальних замін — тільки точкові виправлення.


5. Продуктивність і розмір

Окремий ефект, який проявився з часом — непомітна деградація продукту.

Агент може розв'язати задачу, але при цьому:

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

Щоб це відстежувати, я додав:

  • базові бенчмарки;
  • контроль розміру збірки.
Моніторинг продуктивності та розміру Android-додатка
Постійний моніторинг метрик запобігає «тихій» деградації продукту

Це не дає повної гарантії, але дає змогу хоча б вчасно побачити відхилення.


6. Pre-commit: швидкий фільтр (Mercurial/hg або Git)

Частину перевірок я виніс у pre-commit:

  • автоформатування;
  • точковий лінтинг;
  • архітектурні перевірки для критичних модулів.

Ідея не в тому, щоб «заборонити все», а в тому, щоб:

  • швидко відловлювати базові проблеми;
  • не тягнути їх у репозиторій;
  • не навантажувати рев'ю зайвим шумом.

Чому це працює

Сама суть розробки не змінилася. Тести, лінтери та архітектурні обмеження завжди були нормою.

Змінилося інше: швидкість, з якою з'являється код.

Коли зміни генеруються швидко і у великому обсязі, ручний контроль перестає масштабуватися. Те, що раніше можна було «зловити очима», тепер просто не встигаєш обробити.

У цьому контексті автоматичні перевірки перестають бути «хорошою практикою» і стають базовою необхідністю — просто щоб тримати систему в стійкому стані.


Підсумок

Я не сприймаю цей підхід як універсальний чи обов'язковий для всіх.

Але в моєму випадку він дав досить відчутний ефект — насамперед за рахунок зниження когнітивного навантаження.

Мені більше не потрібно:

  • тримати в голові всі обмеження;
  • вручну вичитувати кожну зміну;
  • розбирати величезні логи;
  • постійно переперевіряти базові речі.

Система бере це на себе, а я підключаюся там, де це дійсно має сенс.

З практичних бонусів, які я для себе зазначив:

  • стабільніші ітерації без випадкових регресій;
  • передбачувана поведінка агента через зворотний зв'язок;
  • швидший цикл виправлень;
  • менше шуму на рев'ю;
  • простіше масштабувати використання агента.

Це не розв'язує всі проблеми, але робить процес роботи з агентом помітно керованішим — принаймні в моєму досвіді.


Корисні посилання

  • Konsist — актуальна документація з архітектурного лінту для Kotlin.
  • Spotless — інструмент для автоматичного форматування коду.
  • detekt — статичний аналізатор коду для Kotlin.
  • Mercurial (hg) — система контролю версій.
  • Maestro — платформа для автоматизації UI-тестування мобільних додатків.

У наступній статті розберу, як я додав до цього автоматичне проходження UI-сценаріїв і перевірку інтерфейсу через MCP Mobile та Maestro (фреймворк для простого і декларативного UI-тестування мобільних додатків).