Инкапсулируйте свой навигационный код

При использовании Kotlin DSL для построения графика сохранение пунктов назначения и событий навигации в одном файле может быть затруднено. Это особенно актуально, если у вас есть несколько независимых функций.

Извлечь пункты назначения

Вам следует переместить пункты назначения в функции расширения NavGraphBuilder . Они должны жить рядом с определяющими их маршрутами и экранами, которые они отображают. Например, рассмотрим следующий код уровня приложения, который создает пункт назначения, отображающий список контактов:

// MyApp.kt

@Serializable
object Contacts

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
    composable<Contacts> { ContactsScreen( /* ... */ ) }
  }
}

Вам следует переместить код навигации в отдельный файл:

// ContactsNavigation.kt

@Serializable
object Contacts

fun NavGraphBuilder.contactsDestination() {
    composable<Contacts> { ContactsScreen( /* ... */ ) }
}

// MyApp.kt

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
     contactsDestination()
  }
}

Определения маршрутов и пунктов назначения теперь отделены от основного приложения, и вы можете обновлять их независимо. Основное приложение зависит только от одной функции расширения. В данном случае это NavGraphBuilder.contactsDestination() .

Функция расширения NavGraphBuilder образует мост между компонуемой функцией на уровне экрана без сохранения состояния и логикой, специфичной для навигации. Этот уровень также может определять, откуда берется состояние и как вы обрабатываете события.

Пример

В следующем фрагменте представлен новый пункт назначения для отображения сведений о контакте и обновляется существующий пункт назначения списка контактов, чтобы предоставить событие навигации для отображения сведений о контакте.

Вот типичный набор экранов, которые могут быть internal по отношению к собственному модулю, чтобы другие модули не могли получить к ним доступ:

// ContactScreens.kt

// Displays a list of contacts
@Composable
internal fun ContactsScreen(
  uiState: ContactsUiState,
  onNavigateToContactDetails: (contactId: String) -> Unit
) { ... }

// Displays the details for an individual contact
@Composable
internal fun ContactDetailsScreen(contact: ContactDetails) { ... }

Создание направлений

Следующая функция расширения NavGraphBuilder создает пункт назначения, в котором отображается компонуемый ContactsScreen . Кроме того, теперь он соединяет экран с ViewModel , который предоставляет состояние пользовательского интерфейса экрана и обрабатывает бизнес-логику, связанную с экраном.

События навигации, такие как переход к месту назначения контактных данных, предоставляются вызывающему объекту, а не обрабатываются ViewModel .

// ContactsNavigation.kt

@Serializable
object Contacts

// Adds contacts destination to `this` NavGraphBuilder
fun NavGraphBuilder.contactsDestination(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToContactDetails: (contactId: String) -> Unit
) {
  composable<Contacts> {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ContactsViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ContactsScreen(
      uiState,
      onNavigateToContactDetails
    )
  }
}

Вы можете использовать тот же подход для создания пункта назначения, который отображает ContactDetailsScreen . В этом случае вместо получения состояния пользовательского интерфейса из модели представления вы можете получить его непосредственно из NavBackStackEntry .

// ContactsNavigation.kt

@Serializable
internal data class ContactDetails(val id: String)

fun NavGraphBuilder.contactDetailsScreen() {
  composable<ContactDetails> { navBackStackEntry ->
    ContactDetailsScreen(contact = navBackStackEntry.toRoute())
  }
}

Инкапсулировать события навигации

Точно так же, как вы инкапсулируете пункты назначения, вы можете инкапсулировать события навигации, чтобы избежать ненужного раскрытия типов маршрутов. Сделайте это, создав функции расширения в NavController .

// ContactsNavigation.kt

fun NavController.navigateToContactDetails(id: String) {
  navigate(route = ContactDetails(id = id))
}

Соберите это вместе

Код навигации для отображения контактов теперь четко отделен от графа навигации приложения. Приложению необходимо:

  • Вызов функций расширения NavGraphBuilder для создания пунктов назначения.
  • Соедините эти пункты назначения, вызвав функции расширения NavController для событий навигации.
// MyApp.kt

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
     contactsDestination(onNavigateToContactDetails = { contactId ->
        navController.navigateToContactDetails(id = contactId)
     })
     contactDetailsDestination()
  }
}

В итоге

  • Инкапсулируйте код навигации для связанного набора экранов, поместив его в отдельный файл.
  • Раскройте пункты назначения, создав функции расширения в NavGraphBuilder
  • Предоставляйте события навигации, создавая функции расширения в NavController
  • Используйте internal , чтобы сохранить конфиденциальность экранов и типов маршрутов.