Створюємо гнучкі компоненти на Compose

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Вітаю! Мене звати Володимир, я Android-розробник. У будь-якій розробці доволі часто буває, що приходять дизайнери зі своїми божевільними ідеями та просять додати ще одну варіацію компоненти, яку ви тільки недавно дописали. І не факт, що ця нова компонента не зламає всі попередні.

Сьогодні поговоримо про принципи створення гнучких компонентів за допомогою Compose і будемо намагатися це зробити гарно, думаючи на перспективу.

Вхідні дані

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

  • фото, імʼя, прізвище;
  • фото, імʼя, прізвище, час оновлення даних, дії;
  • фото, імʼя, прізвище, час оновлення даних, посада, фото менеджера.

ContactFullNameView

Для початку потрібно зрозуміти, яку структуру має цей layout.

Row {
  Image
  Text
}

Оскільки це найпростіший варіант і нічого особливого тут немає, це просто Row з компонентами всередині.

Після прилизування коду вийшло щось таке:

Давайте відразу спробуємо це покрити і @Preview-анотаціями, щоб можна було бачити та порівнювати результати.

Але після першого ж превʼю виникає проблема — як подивитися результат, не запускаючи перегляд на телефоні чи емуляторі? Та і зберігати посилання в коді якось не дуже гарний має вигляд, навіть якщо і для @Preview. Тут нам потрібно змінити підхід до створення @Composable-функцій.

Загалом, якщо ми намагаємось створити якусь гнучку компоненту, нам варто трохи абстрагуватися від моделі, з якою ми працюємо.

Змінивши посилання на тип класу Painter ми можемо розв’язувати три задачі одночасно: перегляд результату в @Preview, перегляд на пристрої зображення за певним посиланням (за бажанням) і повний контроль за завантаженням зображення для дійсних @Composable.

ContactActionsView

Знову визначаємо грубу структуру нашої нової компоненти.

Row {
  Image
  Column {
    Text
    Text
  }
  IconButton
  IconButton
  IconButton
}

Після певного звіряння з дизайном вона трішки зміниться, але суть залишається та ж — в нас в порівнянні з попередньою компонентою додається нове поле і зʼявляються кнопки з діями. Вже користуючись нашими знаннями, ми правильно описуємо цю функцію. З додаткових змін — відступи та мінімальний розмір самого контейнера.

Наче схоже на правду, а?

ContactDetailsView

Ми тільки додали ці кнопки, а тут вже їх треба викидати.. Ото ж ці дизайнери!

Знову попередньо обдумуємо структуру нашої компоненти.

Row {
  Image
  Column {
    Text
    Text
    Row {
      Image
      Text
    }
  }
}

Тут також нічого особливого, доволі простий layout, до початкового варіанту додається текст і нове фото. В іншому відмінність є у відступах, мінімальному розмірі самого контейнера і вирівнюванні фотографії контакту.

Робимо Франкенштейна

Перша ідея, яка може нам спасти на думку — позбирати всі параметри, які взагалі існують в наших компонентах і зробити одну дуже велику і всемогутньому @Composable, що вміє і знає все. Але під час збору цієї солянки ми можемо помітити, що в коді починають зʼявлятися різні when, що у великій кількості може означати, що щось йде не так.

Мінусом тут можемо бачити доступ до багатьох параметрів, які можуть і не бути логічно звʼязані з нашою компонентою в певній конфігурації, а також нестабільність нашої компоненти. Якщо ми знову отримаємо нову компоненту, поточної гнучкості не вистачить, щоб додати бажані функції.

Трохи теорії

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

Першим правилом побудови компонентів є використання modifier як першого опціонального параметра вашої View. Це критична вимога гайдлайнів, що дає нам можливість керувати нашою компонентою ззовні. Якщо у вас виникає потреба додати другий Modifier, вам варто в цьому випадку передивитися структуру вашої функції, оскільки по рекомендаціях функція має містити тільки один Modifier, а це означає що дизайн десь не є правильним. Саме тому цей Modifier має застосовуватися тільки до головної View.

Компоненти можуть мати різні механізми представлення їх UI та функціональності. Обовʼязкові параметри нашої функції визначають її основну суть та мають бути без default-значень. Необовʼязкові параметри з вже визначеними параметрами виступають як опціональні модифікації, які можуть бути використані у нашій компоненті, а можуть і ні... 😢

Також варто звернути увагу на спосіб декларації опціонального значення. Nullable означає, що ці функції можуть бути використані, але можна і взагалі обійтися без них. Пуста імплементація означає, що функціональність є обов’язковою, але ви можете використовувати пусте значення чи використати власну логіку. Значення за замовчуванням же мають бути non nullable і чітко зрозумілими.

Коли ми працюємо зі стилями компонентів, деякі параметри можна залишати в тілі функції, якщо вони короткі та очевидні, хоча для більш складних компонентів варто використовувати групування.

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

Загалом все потрібне згадали, все інше будемо розбирати на ходу.

Робимо красу

Перш за все логічно розділяємо наші компоненти на так звані слоти. Тут це доволі просто, в нас є лого, слот з інформацією і слот з діями. Від цього і будемо відштовхуватися.

У нас завжди є повне імʼя, а також фото контакту, тому ці дані можна використати як обовʼязкові параметри для нашої @Composable-функції. Для фото будемо передавати Painter, щоб потім мати змогу контролювати завантаженням фото ззовні та переглядати результат в @Preview. Також прикинемо на око чорнову структуру компоненти та зробимо всі слоти пустими.

Розглядаємо перший слот. В нас тут фото є завжди присутнім, змінюється тільки його вирівнювання, тому лишаємо ці параметри для нашої функції, все інше є завжди однаковим.

З другим слотом ми маємо один обовʼязковий параметр, який назвемо title, щоб забезпечити майбутню сумісність, аби потім, якщо ми будемо передавати туди не тільки імʼя та прізвище, залишалась незмінна структура.

Третій слот буде з діями, що має три різних колбеки. Вони тут позначенні nullable, щоб мати можливість контролювати їх присутність, не додаючи нових змінних.

Після того, як ми зберемо всі слоти та оголосимо їх як основу для нашої функції, має вийти такий комплекс:

Новим в цій структурі є ContactViewConfig, що контролює різні налаштування нашої компоненти.

В ідеальному світі вони мають бути погруповані, тому не робіть так, як я це зробив.

Ну все, тепер в нас готова компонента, що може бути кастомізована, має справний @Preview та готова до розширення.

Тепер ми можемо зробити поверх базової функції додаткові компоненти, які будуть розширювати ці функції. Як один з варіантів — використання вже не простих типів, а domain-класів.

Трохи висновків

Якщо глянути @Preview, то різниці не буде видно між компонентами, що було до і після. По коду також майже різниці не буде — максимально гнучка компонента із додатковими функціями та конфігами буде займати стільки ж рядків коду, як і три різні незалежні функції. Маю надію, що ця стаття допомогла вам краще зрозуміти, як писати гнучкі компоненти. Для загального розвитку також рекомендую поглянути це відео і код за потреби тут.

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Нема за що, тут просто довго її публікували, хоча і була першою зробив 😅

Схоже, що не дарма «Compose»-ом назвали)

Підписатись на коментарі