Маленька історія додатку на Kotlin Multiplatform Mobile + Compose UI для Android та iOS

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всім привіт. Сьогодні поговоримо про те, як було написано маленький додаток, а-ля щоденник, для двох платформ за допомогою KMM + Compose.

Спойлер: з костилями, незрозумілими багами та дивними проблемами. І ще не до кінця.

Передісторія. В пошуках нової роботи захотілося погратися з улюбленою технологією і подивитися, на що наразі була здатна версія Compose в плані шерінгу UI для обох платформ. Раніше були варіанти для перевикористання вʼюх на Native та Web, навіть збочені варіанти запуску на iOS, але нещодавно в світі мобільної розробки з’явилася новина, що вже перший додаток пішов у реліз, де було використано повноцінно UI на двох платформах. Тому, взявши за ідею простий додаток-щоденник, надихаючись Presently, та встановивши собі 7 днів умовного дедлайну, почалась робота над проєктом.

Задумка та базові ідеї

Оскільки я більше працював в Android-розробці, то робився спочатку додаток на цій платформі, а вже потім дописував зараз ще дописую iOS-версію. Перш за все хотілося погратися з анімаціями (типу сплеш скрін) та додати якусь маленьку приємність для потенційних користувачів. Так в мене ще з енної роботи був код, який вмів малювати сніжинки, що гарно падають, маючи різні параметри поведінки (швидкість падіння, розмір, прозорість, швидкість обертання, відхилення вліво-вправо) та векторні малюнки листочків сакури, які і стали основою для іконки додатку.

Назва «Please, Remember Me» якось прийшла сама і +/- підходила для задачі. В голові була задумка мати сплеш-екран, головний екран зі списком днів із записами, екран створення запису та якась примітивна сторінка налаштувань. З часом приїхав також функціонал пошуку записів по тексту.

:shared

В своїх проєктах я наразі використовую власне рішення в плані архітектури, хоча вже і придумали багато бібліотек, якими можна користуватися. Причини прості: максимальний контроль і розуміння того, як працює той чи інший код, чи то для iOS чи для Android. В :shared модуль було всунуто ось такі частини коду:

Тобто там було все, починаючи з модельок бази даних (SqlDelight), різного DI (Koin) начиння, Intercatorʼів, DataSource, аж до рівня ViewModelʼів. В платформо-залежних реалізаціях був використаний мішаний підхід реалізації. Деякі компоненти були реалізовані через інтерфейси та провайди в DI (як логер, хоча так, існують реалізації логерів для KMM також), інші ж — через реалізацію механізму expect/actual. В моєму випадку останній підхід здійснений для реалізації Firebase аналітики, диспатчерів, драйверів баз даних, платформо-залежних модулів для DI та ще деяких дрібниць.

З приємного, що в процесі розробки відкрив для себе, — це те, що іноді можна звертатися в Koin до глобального контексту. Це іноді допомагає скоротити написання коду. Наприклад, якщо мені треба отримати звідкись Context, але не дуже хочеться його прокидувати, то можна скористатися таким хаком:

В даному випадку ми отримуємо версію додатку з будь-якого місця в коді. Може бути не дуже гарне рішення, але для скорочення коду +/- підійде.

В цьому модулі також ще лежать ресурси, які шеряться між платформами (переклади та шрифти в моєму випадку).

:shared-ui

Тут вже буде трішки більше страждань. Початково весь UI писався на Android з використанням нативних рішень та Jetpack-бібліотек. Основними проблемами при переносі UI в спільну папку були такі:

1) Переписування Splash-екрану, щоб все працювало для двох платформ; оскільки Android-рішення працювало на Canvas та потребувало різних ViewPropertyAnimator, я мусив його порухати на Compose Canvas, де використав функції rememberInfiniteTransition, animateFloat, infiniteRepeatable та інші дрібниці для промалювання тих самих падаючих листочків; вийшло доволі непогано.

З дурного — не хотілося гратися з вектором на Android та PNG на iOS для промалювання листочків, тому я використав ImageVector.Builder, в якому і прописав vector path тих зображень, які малювалися в додатку. Такий самий механізм використовується для Material іконок від Google. Тільки в мене ці іконки мали набагато більше коду всередині.

2) Робота зі шрифтами; якщо бібліотека moko-resources допомагала шерити шрифти на різні платформи, то як це робити на стороні спільного коду було незрозуміло. Тут допоміг механізм expect/actual. Для Android все було більш-менш просто.

А от для iOS трохи складніше. Справа в тому, що шрифти кладуться в спеціальний Bundle, що має динамічний ідентифікатор. Трохи полазивши по коду бібліотеки і запозичивши певний код, вийшло обійти цю проблему.

3) Навігація. Для Android було використано NavHost зі звичної нам androidx.navigation бібліотеки, на iOS поки це був просто Surface, який залежно від поточного екрану показував ту чи іншу @Composable. Також треба було написати певну обгортку а-ля NavigationController, щоб залежно від платформи можна оперувати навігацією.

4) Календар. На Android, як не крути, вибір дати і часу завжди був болючим місцем, тому в даному випадку я просто обрав бібліотеку від Google та використовував її, оскільки вона +/- підходить і можна там виставляти констрейнти по часу (ми не можемо зробити запис у майбутньому). На iOS ця частина поки в мене залишається недоробленою.

5) windowInsetsPadding; в мене були екрани, що використовують як і контент за статус/навігейшн баром, так і зважають на них; якщо для Android для цього випадку є така сама функція, то в iOS для цього треба було трохи погратися з інсетами:

Ще одним косяком тут мене порадувала поведінка на iOS: при кліку на текстове поле, в мене так званий Toolbar залітав за межі екрану і клавіатура перекривала кнопку внизу. Теоретично це можна вирішити, якщо підписуватися на keyboardWillShowNotification подію та змінювати розмір вʼюхи, щоб вона займала розмір висоти екрану — розмір клавіатури, але я це ще поки не вирішив до кінця.

6) internal для всіх @Composable в модулі; мабуть, тут я згаяв найбільше часу, тому що спочатку помітив в проєкті, але і забив на цей visibility modifier, хоча, як виявилося — дуже дарма, тому що якщо не проставити його всім функціям, то компілятор буде просто плюватися невідомою помилкою, яка в неті навіть адекватно не гуглиться.

Дякуючи цьому провтику, було згаяно півтора дні, або це просто втома давалася взнаки. Через цей модифікатор всі @Preview також лежать в цьому gradle модулі, в Android-реалізації. Щодо реалізації на платформах все доволі просто: для Android ви огортаєте вашу основну вʼюху в іншу @Composable, яка може лежати в androidMain, а для iOS використовується androidx.compose.ui.window.Application() функція, яка повертає UIViewController. Завдяки таким махінаціям вдалося скоротити код додатків до такої структури:

7) Gradle, iOS та XCode; в якийсь момент виникла проблема, що в мене не збиралися модулі та плювалися на помилку, що була повʼязана зі збиранням на X64. В душі не гребу, де я наплужив, тому що не торкався нічого з подібних налаштувань, але останнє рішення було перестворити просто XCode проєкт. І на дивно, все запрацювало.

Висновки

Цей проєкт задумувався чисто заради цікавості й розваги у вільний час та щоб подивитися, наскільки Compose готовий для проду. Якщо говорити про Android, то тут проблем відносно мало було, якщо були взагалі. Хіба що можу сказати, що іноді не показуються залежності від нижчого рівня у вищому в androidMain, хоча при компіляції все ок і імпорти також робочі. На диво, в commonMain чи iosMain такого немає, там все добре. Хоча, можливо, це Gradle просто дуріє, ви ж знаєте, що він може.

Також не певен, що iOS зараз готовий до цього. Коли ми говоримо про Android (Compose) + iOS (SwiftUI), ця варіація виглядає більш робочою, принаймні, наразі. В попередньому додатку з використанням схожого підходу, ті пари відчувалися більш контрольовано. Також з Compose на iOS я часто отримую лаги та просідання в рендері. Може, це і я щось не так роблю чи до релізу це пофіксять, але поки виглядає страшно, особливо, коли Ripple на половині «замерзає». Та і певні костилі, які робляться для того, щоб завести той чи інший код на цій платформі, не дуже радують.

А ще відчувається відсутність певних бібліотек під iOS-частину.

До речі, про них. На проєкті було використано такі штуки:
kotlinx datetime;

moko resources;

sqldelight;

multiplatform settings;

koin.

Виглядає так, що це буде +/- стандартний набір на більшість проєктів. Дійсно допомагають в скороченні написання коду під різні платформи.

Наразі додаток під Android прямує в реліз в Google Play, а під iOS я намагаюся дофіксити різні баги і, може, опублікую його, коли більш-менш він буде не таким страшним. На цьому експеримент вважаю успішним, але буду повертатися до свого попереднього додатку, де використовується Compose та SwiftUI окремо.

PS. Як показує аналітика, easter egg з лапками, які починають топати після кліка на пусте місце, полюбили більше за сам функціонал додатку.



Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному3
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

Когда в maui-приложении делал material DatePicker, то пришлось повоевать с андроидными датами.

В якому саме розумінні?

Щоб сконвертувати .NET DateTime в андроїдну дату довелось писати таке. Нічого такого суперскладного, але довелось витратити час, щоб доперти.

using Java.Text;
using Java.Util;
using ADatePicker = Google.Android.Material.DatePicker.MaterialDatePicker;
using TimeZone = Java.Util.TimeZone;

string text = VirtualView.Date.ToString("dd-MM-yyyy");
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
TimeZone timeZone = TimeZone.GetTimeZone("UTC");
formatter.TimeZone = timeZone;
Date date = formatter.Parse(text);
long milliseconds = date.Time;
ADatePicker dialog = ADatePicker.Builder
.DatePicker()
.SetSelection(milliseconds)
.SetTitleText(VirtualView.Title)
.Build();
return dialog;

Десь подібну штуку і я використовував, але то для показу. В іншому проекті, де є робота з сервером, також використовував SDF класи для цих задач.

Висновки

Юзайте Флюхтер)

Бо РН гівно)
А точніше — РН не вдобний, глючний, погано розвивається

А можна якісь приклади ?

погано розвивається

Скоро замість моста буде c++ ліба тому я думаю розвивається
І чим погана kotlin multiplatform ?

Вэб и десктоп уже из коробки поддерживает?

А из коробки?)
Сторонние форки сегодня есть завтра нет.

Тобто флаттер з коробки підтримує ?

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

Так точно)
Все из коробки

А доступ до файлової системи , вай фаю , до камери теж з коробки ?

Список плагинов поддерживаемых флаттер тимой
github.com/...​lugins/tree/main/packages

Камера имеется)
И видео плейер и вэбвью

Ну це добре що все з коробки , але в RN нова архітектура яка набагато збільшить перфоманс , що може запропонувати flutter ?

Правда и у флаттера не без нюансов с перформансом)

А у флаттера новий рушій що ще збільшить перформанс. Шо там у РН?)

Можна посилання на статтю ?

А дебаг прямо в ИДЕ, могёт?)
Чтобы не держать консоль хрома еще в дополнение

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

Видимо через жопу работало

В случае Флаттера ничего и делать не нужно.
Просто прейкпоинт поставил в ИДЕ и готово

И ВСкод поддерживается на отличном уровне)

Привіт! Василь, а чого Флатер чи Дарт плагін для Андроїд Студії на Вінді 11 забирає 30 відсотків процесорного часу навіть без відкритого проекту? Коре ай5 12600.

Хз)
Винду не видел с 2017 года)

А забороли ли по разному выглядящий рендеринг в эджкейсах на айос и андроид?

А забороли ли по разному выглядящий рендеринг в эджкейсах

Це що таке ?

В РН ЕМНИП рендерится флекс лейаут через йога энджин
Стало быть на айос и андроид бывало по разному выглядит чуть в сложных случаях

А можно в одной аппе сделать юай идентичный на всех платформах?)
А в след аппе сделать платформ специфик?)

І чим погана kotlin multiplatform ?

Вроде в выводах написано на сколько все готово

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