Як зробити навігацію в Jetpack Compose зручнішою
Привіт! Мене звати Вадим, я Android Developer у Welltech, ми займаємось розробкою мобільних застосунків у категорії Health&Fitness. Я хотів би вам розказати про наш досвід роботи із навігацією у Jetpack Compose.
Стаття буде корисною для тих, хто тільки починає вивчати Compose або кого не влаштовує стандартне рішення від Google — Navigation Сomponent for Jetpack Compose.
Історія
За часів View з’явилась бібліотека Navigation Component (далі NC) і стала дефакто стандартом, переважна більшість розробників, включаючи нас, використовувала його і «горя не знала». З появою Compose Google також адаптував NC і рекомендував до використання.
Коли ми починали писати новий проєкт на Compose, то звісно обрали NC для побудови нашої навігації, оскільки досвід роботи з бібліотекою вже був. Виглядало все звично: ті самі NavController, NavHost. Відмінністю NavHost став його опис в коді замість xml файлу.
Новим же став підхід до навігації: тепер замість згенерованих ідентифікаторів екранів у класі R ми маємо задати шлях із вказанням аргументів (якщо наявні) у вигляді String:
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(/*...*/) }
/*...*/
}
//navigate
navController.navigate("home")
Ми одразу створили sealed class для зручної роботи, щоб винести перелік екранів, логіку створення шляху з аргументами, також отримання переданих аргументів із Bundle:
sealed class Destination(
val path: String,
/*...*/
) {
object Home : Destination("home", /*...*/)
/*...*/
}
NavHost(
navController = navController,
startDestination = Destination.Home.path
) {
composable(Destination.Home.path) { HomeScreen(/*...*/) }
/*...*/
}
//navigate
navController.navigate(Destination.Home.path)
Перші незручності почались, коли нам знадобилось передавати аргументи. У NC для View фреймворку аргументи описувались в XML (примітивні типи, Serializable, Parcelable). При збірці генерувався клас із цими аргументами і потрібно було тільки створити екземпляр з потрібними даними і передати NavController. А от в Compose світі нас чекало розчарування...
Передача аргументів в NC for Compose і недоліки даної реалізації
Щоб передавати аргументи, нам потрібно:
- описати аргументи, які може приймати екран (ключ, тип);
- змінити шлях екрана з вказанням ключів аргументів;
- навігуючись, потрібно створити шлях з потрібними параметрами;
- отримати передані аргументи із Bundle по ключу.
composable(
"other_screen/{param1}", //path with args
arguments = listOf(
navArgument("param1") { type = NavType.StringType }
) //args descriptions
) { backStackEntry ->
//obtain our args
val param1 = backStackEntry.arguments?.getString("param1")
OtherScreen(param1)
}
//navigate (with param1="hello_world")
navController.navigate("other_screen/hello_world")
Ми перенесли в окремий клас логіку створення url-екрану, перерахування аргументів, а також отримання значень аргументів із Bundle. Отримали такий вигляд:
sealed class Destination(
val path: String,
val args: List<NamedNavArgument>
) {
object OtherScreen : Destination(
path = "other_screen/{param1}",
args = listOf(
navArgument("param1") {
type = NavType.StringType
defaultValue = null
nullable = true
}
)
) {
class Arguments(val param1: String?)
fun getRoute(arguments: Arguments): String {
return "other_screen/${arguments.param1}"
}
fun getArguments(bundle: Bundle): Arguments {
return Arguments(
param1 = bundle.getString("param1")
)
}
}
}
Перший час користуватись цим було досить зручно. Наш опис графа і навігація були такими:
NavHost(
navController = navController,
startDestination = Destination.Home.path
) {
composable(
route = Destination.OtherScreen.path,
arguments = Destination.OtherScreen.args
) { backStackEntry ->
val args = Destination.OtherScreen.getArguments(
bundle = requireNotNull(backStackEntry.arguments)
)
OtherScreen(param1 = args.param1)
}
}
//navigate
val route: String = Destination.OtherScreen.getRoute(
args = Destination.OtherScreen.Arguments(param1 = param1)
)
navController.navigate(route)
Але як показав час, цей підхід має доволі вагомі недоліки:
- бойлерплейт (коли у проєкті багато екранів — це починає набридати);
- можна банально зробити одруківку (у шляху або назві аргументу);
- оскільки шлях парситься як Uri — потрібно слідкувати за тим, щоб String параметри, які передаються, були без спеціальних символів. Або закодувати в формат Url (наприклад, за допомогою URLEncoder.encode);
- рано чи пізно вам знадобиться передати Serializable або Parcelable. Це звісно можливо зробити, попередньо серіалізувавши об’єкт у String, але погодьтеся, писати кожен раз серіалізацію не дуже цікаве заняття.
Compose-destinations
Йшов час, ми мирилися з вищеописаними незручностями, допоки одного дня не натрапили на цікаву бібліотеку — «Compose-destinations». Після прочитання документації стало зрозуміло — це воно!
Для себе виділили наступні переваги:
- Це не окрема бібліотека для навігації, а надбудова над navigation-сompose, основною задачею якої є кодогенерація.
- Оскільки це тільки надбудова, її легко використати у вже існуючому проєкті, де використовується NC. Для нових проєктів поріг входження низький у випадку, якщо вже є досвід зі стандартним NC.
- Не потрібно вручну описувати граф навігації (додавши анотацію @Destination на composable функцію екрана і бібліотека його згенерує).
- Не потрібно думати за те, як передати Serializable/Parcelable.
- Аргументи, які передаються у String форматі кодуються у формат Url.
- Аргументи, які потрібні на певному екрані просто перераховують як параметри composable функції. Додатково можна вказати і NavController — тоді буде прокинуто потрібний контролер для управління навігацією.
- Відпадає потреба щось міняти в описі графів (У нас цей файл був немаленьким — близько 1000 рядків, щось шукати в ньому — ще те задоволення).
Є й декілька мінусів:
- Використовується кодогенерація, а це збільшує час збірки проєкту (для нашого проєкту це не критичний мінус, до того ж не помітили значного впливу).
- Розробляється і підтримується однією особою, тож невідомо, що буде з проєктом в майбутньому.
Міграція на Compose-destinations
Зваживши всі за і проти було зрозуміло — треба затягувати в наш проєкт. Процес міграції був неважким, але не швидким, оскільки екранів у нас було чимало. Мігруємо наш приклад навігації:
@RootNavGraph
@Destination
@Composable
fun OtherScreen(
param1: String?,
param2: Boolean,
navigator: DestinationsNavigator
) { /*...*/ }
Зауважу, що @RootNavGraph використовується для позначення, що наш екран відноситься до дефолтного графу. Якщо у вас більше ніж один граф — створіть свою анотацію, наслідувавши її від @NavGraph, і використайте її.
DestinationsNavigator — це бібліотечна абстракція над NavController, ви можете використовувати і звичайний NavController.
Після того, як додали новий екран, потрібно зібрати проєкт, щоб спрацювала кодогенерація. Потім зможемо використати згенерований граф:
DestinationsNavHost(navGraph = NavGraphs.root)
Лаконічно, чи не так? Далі розглянемо, як же навігуватись на певний екран і передавати аргументи:
val navigateToOtherScreen = { param1: String?, param2: Boolean ->
navigator.navigate(
OtherScreenDestination(param1, param2)
)
}
OtherScreenDestination — також згенерований клас, екземпляр якого ми створюємо з аргументами і передаємо NavController/DestinationsNavigator.
Висновок
У даній статті ми розглянули використання бібліотеки, що значно покращує (суб’єктивна думка) використання navigation-compose. Можливо це рішення не усім підійде, адже я показав лише простий кейс використання, проте, як мінімум, бібліотека варта уваги. Ми з командою були б надзвичайно раді, якби нам хтось її показав перед початком нашої розробки, тому можливо, що для когось тут вона стане відкриттям.
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівА є приклади застосунків, бажано вільних, на цьому вашому Compose? Хочу потицяти, наскільки воно юзабельне.
Бо той же Flutter виглядає як сира поробка рівня графічних бібліотек для написання ігор; у FluffyChat, приміром, стрічаються дитячі проблеми штибу непереносу рядків, які в англійській локалізації влазять, а в инших раптово можуть пробивати екран
і висіти у повітрі. Та й узагалі виглядає ненативно й аляпувато — хоча це поки штатний софт Android не переведений на Flutter поступово (втім, для мене «нативним» завжди буде софт часів Gingerbread, чи принаймні Jelly Bean із Holo ×D).github.com/android/nowinandroid
Відвал мозку і злам UX... я очікую, що плюсики спойлери розгортатимуть, а вони замість прапорців там.
апки на будь який смак github.com/android/compose-samples
Бібліотека
просто біль в проекті. Багато багів, проблеми з навігацією, різні костилі треба було впроваджувати для того, щоб ця хрінь просто працювала. Як показала практика, її часто тягнуть в проект люди, які не вміють працювати з чистою бібліотекою навігації чи appcompanist-варіацією. Якби була можливість, то викинув би її з проекту і забувся б як страшний сон.