Як зробити навігацію в 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 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів