Comprensión profunda de los principios de implementación de Compose Navigation

prefacio

Un proyecto de Compose puro no puede prescindir de la compatibilidad con la navegación de páginas, y navigation-compose es casi la única opción en este sentido, lo que también la convierte en una biblioteca estándar de segunda mano para proyectos de Compose. Hay muchos artículos sobre cómo usar navigation-compose , como este. De hecho, también vale la pena aprender Navegación en términos de diseño de código, por lo que este artículo lo llevará a profundizar en su principio de implementación.

Comience con Jetpack Navigation

Jetpack Navigatioin es un marco general de navegación de páginas, y navigation-compose es solo una implementación específica para Compose. Independientemente de la implementación específica, la navegación define los siguientes roles importantes en la capa pública principal:

Role ilustrar
servidor de navegación Define la entrada de navegación, y también es el contenedor que alberga la página de navegación.
controlador de navegación El administrador global de la navegación mantiene la información estática y dinámica de la navegación, la información estática se refiere a NavGraph, y la información dinámica se refiere a la pila trasera NavBackStacks generada cuando la navegación es demasiado larga.
gráfico de navegación Al definir la navegación, es necesario recopilar la información de navegación de cada nodo y registrarla uniformemente en el gráfico de navegación.
NavDestino Cada nodo en la navegación lleva información como ruta y argumentos.
Navegador El ejecutor específico de navegación, NavController, obtiene el nodo de destino en función del gráfico de navegación y ejecuta el salto a través de Navigator

NavHost, Navigatot, etc. en los roles anteriores NavDestinationtienen implementaciones correspondientes en diferentes escenarios. Por ejemplo, en la vista tradicional, usamos Activity o Fragment para alojar la página, tomando como ejemplo el fragmento de navegación :

  • Frament es un NavDestination en el gráfico de navegación. Definimos NavGraph a través de DSL o XMlL, y recopilamos información de Fragment en forma de NavDestination en el gráfico de navegación.
  • NavHostFragment sirve como NavHost para proporcionar un contenedor para la visualización de páginas de fragmentos
  • Implementamos la lógica de salto de página específica a través de FragmentNavigator. En la implementación de FragmentNavigator#navigate, el reemplazo de página se realiza en función de FragmentTransaction#replace. A través de la información de la clase Fragment asociada con NavDestination, se crea una instancia del objeto Fragment para completar el reemplazo.

Echemos un vistazo a nuestro protagonista navegación-redacción de hoy . Al igual que navigation-fragment , Compose tiene su propia implementación específica para Navigator y NavDestination. Un poco especial es NavHost, que es solo una función Composable, por lo que no tiene una relación de herencia con la biblioteca pública:

A diferencia de los componentes de objetos como Fragment, Compose usa funciones para definir páginas, entonces, ¿ cómo implementa navigation-compose Navigation en un marco declarativo como Compose? A continuación, presentamos escena por escena.

definir navegación

NavHost(navController = navController, startDestination = "profile") {
    
    
    composable("profile") {
    
     Profile(/*...*/) }
    composable("friendslist") {
    
     FriendsList(/*...*/) }
    /*...*/
}

NavHost en Compose es esencialmente una función Composable, que no tiene una relación de derivación con la interfaz del mismo nombre en el tiempo de ejecución de navegación , pero las responsabilidades son similares y el propósito principal es construir NavGraph. Después de crear NavGraph, NavController lo mantendrá y lo usará en la navegación, por lo que NavHost acepta un parámetro NavController y le asigna NavGraph

//androidx/navigation/compose/NavHost.kt
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    
    
    NavHost(
        navController,
        remember(route, startDestination, builder) {
    
    
            navController.createGraph(startDestination, route, builder)
        },
        modifier
    )
}

@Composable
public fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier
) {
    
    

    //...
    //设置 NavGraph
    navController.graph = graph
    //...
    
}

Como arriba, asigne NavGraph a NavController en NavHost y su función con el mismo nombre.

En el código, NavGraph se crea navController#createGraphmediante y el objeto NavGraph se creará internamente en función de NavGraphBuilder.Durante el proceso de compilación, NavHost{...}se llama al constructor en el parámetro para completar la inicialización. Este constructor es una función de extensión de NavGraphBuilder. Cuando usamos para NavHost{...}definir la navegación, definiremos la página de navegación en Compose a través de una serie de {…}. · También es una función de extensión de NavGraphBuilder, y la única ruta en la navegación de la página se pasa a través de parámetros.

//androidx/navigation/compose/NavGraphBuilder.kt
public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    
    
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
    
    
            this.route = route
            arguments.forEach {
    
     (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach {
    
     deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

compose(...)La implementación específica es la anterior, cree una ComposeNavigator.Destinationy NavGraphBuilder#addDestinationagréguela a los nodos de NavGraph.
Pase dos miembros al construir Destination:

  • provider[ComposeNavigator::class]: ComposeNavigator obtenido a través de NavigatorProvider
  • content: Función componible correspondiente a la página actual

Por supuesto, la información como la ruta, los argumentos y los enlaces profundos también se transmitirán para el Destino.

//androidx/navigation/compose.ComposeNavigator.kt
public class Destination(
    navigator: ComposeNavigator,
    internal val content: @Composable (NavBackStackEntry) -> Unit
) : NavDestination(navigator)

Es muy sencillo, es decir, además de heredar de NavDestination, se almacena un contenido Compsoable adicional. Destino muestra la página correspondiente al nodo de navegación actual llamando a este contenido, veremos cómo se llama a este contenido más adelante.

salto de navegación

Al igual que la navegación Fragment, Compose también es bueno para los saltos de página al NavController#navigateespecificar una ruta

navController.navigate("friendslist")

Como se mencionó anteriormente, NavController finalmente implementa una lógica de salto específica a través de Navigator, FragmentNavigatorcomo FragmentTransaction#replacecambiar las páginas de fragmentos a través de , luego echemos un vistazo a la implementación específica ComposeNavigator#navigatede :

//androidx/navigation/compose/ComposeNavigator.kt
public class ComposeNavigator : Navigator<Destination>() {
    
    

    //...
    
    override fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) {
    
    
        entries.forEach {
    
     entry ->
            state.pushWithTransition(entry)
        }
    }
    
    //...

}

El procesamiento aquí es muy simple, no hay un procesamiento específico como FragmentNavigator. NavBackStackEntryRepresenta un registro en la pila posterior durante el proceso de navegación, entriesque es la pila posterior de la navegación de la página actual. state es un NavigatorStateobjeto , que es un nuevo tipo introducido después de Navigation 2.4.0. Se usa para encapsular el estado en el proceso de navegación para que lo use NavController, etc. Por ejemplo, backStack se almacena NavigatorStateen

//androidx/navigation/NavigatorState.kt
public abstract class NavigatorState {
    
    
    private val backStackLock = ReentrantLock(true)
    private val _backStack: MutableStateFlow<List<NavBackStackEntry>> = MutableStateFlow(listOf())
    public val backStack: StateFlow<List<NavBackStackEntry>> = _backStack.asStateFlow()
    
    //...
    
    public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
    
    
        //...
        push(backStackEntry)
    }
    
    public open fun push(backStackEntry: NavBackStackEntry) {
    
    
        backStackLock.withLock {
    
    
            _backStack.value = _backStack.value + backStackEntry
        }
    }
    
    //...
}

Cuando salta la página Redactar, se creará la NavBackStackEntry correspondiente en función del Destino de destino y luego pushWithTransitionse insertará . backStack es un tipo StateFlow, por lo que se pueden monitorear los cambios en la pila posterior. Mirando hacia atrás en la implementación de NavHost{...}la función , encontraremos que el cambio de backState se monitorea aquí, y de acuerdo con el cambio de la parte superior de la pila, se llama a la función Composable correspondiente para realizar el cambio de página.

//androidx/navigation/compose/ComposeNavigator.kt
@Composable
public fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier
) {
    
    
    //...

    // 为 NavController 设置 NavGraph
    navController.graph = graph

    //SaveableStateHolder 用于记录 Composition 的局部状态,后文介绍
    val saveableStateHolder = rememberSaveableStateHolder()

    //...
    
    // 最新的 visibleEntries 来自 backStack 的变化
    val visibleEntries = //...
    val backStackEntry = visibleEntries.lastOrNull()

    if (backStackEntry != null) {
    
    
        
        Crossfade(backStackEntry.id, modifier) {
    
    
            
            //...
            val lastEntry = backStackEntry
            lastEntry.LocalOwnersProvider(saveableStateHolder) {
    
    
                //调用 Destination#content 显示当前导航对应的页面
                (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
            }
        }
    }

    //...
}

Como se mencionó anteriormente, además de configurar NavGraph para NavController en NavHost, el trabajo más importante es monitorear los cambios de backStack y actualizar la página.

El cambio de página en navigation-framgent se completa de manera imperativa en FragmentNavigator, mientras que el cambio de página en navigation-compose se actualiza de manera receptiva en NavHost, lo que también refleja las ideas de implementación de la IU declarativa y la diferencia de IU imperativa.

visibleEntriesSe basa en NavigatorState#backStackla entrada obtenida que debe mostrarse. Es un estado, por lo que cuando cambia, NavHost se reorganizará y Crossfademostrará la página correspondiente de acuerdo con las entradas visibles. La implementación específica de la visualización de la página también es muy simple, Destination#contentsolo , y este contenido es NavHost{...}la función Composable que definimos para cada página en .

guardar Estado

Anteriormente aprendimos sobre los principios de implementación específicos de la definición de navegación y el salto de navegación. A continuación, veamos la conservación del estado durante el proceso de navegación.
La conservación del estado de composición de navegación se produce principalmente en los siguientes dos escenarios:

  1. Cuando se hace clic en el botón Atrás del sistema o se llama a NavController#popup, aparece backStackEntry en la parte superior de la pila de navegación y la navegación regresa a la página anterior. En este momento, esperamos que el estado de la página anterior sea mantenido
  2. Cuando se usa con la barra de navegación inferior, haga clic en el elemento de la barra de navegación para cambiar entre diferentes páginas. En este momento, esperamos que la página anterior cambiada mantenga el estado anterior.

En el escenario anterior, esperamos que el estado de la página, como la posición de la barra de desplazamiento, no se pierda durante el proceso de cambio de página. Sin embargo, a través del análisis de código anterior, también sabemos que el cambio de página de la navegación de Compose es esencialmente una reorganización. y llamando a diferentes Composables. De forma predeterminada, el estado de Composable se pierde cuando deja una Composición (es decir, la recomposición ya no se ejecuta). Entonces, ¿ cómo evita la composición de navegación la pérdida de estado? La clave aquí es SaveableStateHolderlo que apareció .

SaveableStateHolder y recordarGuardable

SaveableStateHolder proviene de compose-runtime y se define de la siguiente manera:

interface SaveableStateHolder {
    
    
    
    @Composable
    fun SaveableStateProvider(key: Any, content: @Composable () -> Unit)

    fun removeState(key: Any)
}

No es difícil entender por el nombre SaveableStateHolderque mantiene un estado guardable (Saveable State), podemos SaveableStateProviderllamar a la función Composable dentro de ella proporcionada, y el estado rememberSaveabledefinido se guardará con la clave y no seguirá el ciclo de vida de Composable Cuando SaveableStateProvider se ejecuta la próxima vez, el estado guardado se puede restaurar a través de la clave. Usemos un experimento para comprender el rol de SaveableStateHolder:

@Composable
fun SaveableStateHolderDemo(flag: Boolean) {
    
    
    
    val saveableStateHolder = rememberSaveableStateHolder()

    Box {
    
    
        if (flag) {
    
    
             saveableStateHolder.SaveableStateProvider(true) {
    
    
                    Screen1()
            }
        } else {
    
    
            saveableStateHolder.SaveableStateProvider(false) {
    
    
                    Screen2()
        }
    }
}

En el código anterior, podemos cambiar entre Screen1 y Screen2 pasando diferentes banderas saveableStateHolder.SaveableStateProviderpara asegurarnos de que se guarde el estado interno de la pantalla. Por ejemplo, si usa en Screen1 para rememberScrollState()definir un estado de barra de desplazamiento, cuando Screen1 se vuelve a mostrar, la barra de desplazamiento todavía está en la posición cuando desaparece, porque RememberScrollState usa internamente RememberSaveable para guardar la posición de la barra de desplazamiento.

Si no sabe acerca de RememberSaveable, puede consultar https://developer.android.com/jetpack/compose/state#restore-ui-state. En comparación con la memoria normal, RememberSaveable puede guardar el estado durante un período más largo de tiempo a lo largo del ciclo de vida de Composable. La restauración del estado se puede lograr en el caso de traspaso o incluso reinicio del proceso.

Cabe señalar que si usamos RememberSaveable fuera de SaveableStateProvider, aunque el estado se puede guardar cuando se cambian las pantallas horizontal y vertical, el estado no se puede guardar en la escena de navegación. Debido a que el estado definido usando RememberSaveable se guardará automáticamente solo cuando cambie la configuración, pero no activará el guardado cuando cambie la estructura común de la interfaz de usuario, y la función principal de SaveableStateProvider es poder realizar el guardado de estado onDisposecuando

//androidx/compose/runtime/saveable/SaveableStateHolder.kt

@Composable
fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) {
    
    
    ReusableContent(key) {
    
    
        // 持有 SaveableStateRegistry
        val registryHolder = ...
        
        CompositionLocalProvider(
            LocalSaveableStateRegistry provides registryHolder.registry,
            content = content
        )
        
        DisposableEffect(Unit) {
    
    
            ...
            onDispose {
    
    
                //通过 SaveableStateRegistry 保存状态
                registryHolder.saveTo(savedStates)
                ...
            }
        }
    }

El pase en RememberSaveable SaveableStateRegistryse guarda. En el código anterior, puede ver que en el ciclo de vida onDispose, registryHolder#saveToel estado se guarda en SavedStates por y SavedStates se usa para la restauración del estado al ingresar a Composición la próxima vez.

Por cierto, LayoutNode se ReusableContent{...}puede reutilizar en función de la clave, lo que conduce a una reproducción de la interfaz de usuario más rápida.

Preservación del estado al navegar hacia atrás

Después de presentar brevemente el rol de SaveableStateHolder, veamos cómo funciona en NavHost:

@Composable
public fun NavHost(
    ...
) {
    
    
    ...
    //SaveableStateHolder 用于记录 Composition 的局部状态,后文介绍
    val saveableStateHolder = rememberSaveableStateHolder()
    ...
        Crossfade(backStackEntry.id, modifier) {
    
    
            ...
            lastEntry.LocalOwnersProvider(saveableStateHolder) {
    
    
                //调用 Destination#content 显示当前导航对应的页面
                (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
            }
            
        }

    ...
}

lastEntry.LocalOwnersProvider(saveableStateHolder)Llamado internamente Destination#content, LocalOwnersProvider es en realidad una llamada a SaveableStateProvider:

@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
    saveableStateHolder: SaveableStateHolder,
    content: @Composable () -> Unit
) {
    
    
    CompositionLocalProvider(
        LocalViewModelStoreOwner provides this,
        LocalLifecycleOwner provides this,
        LocalSavedStateRegistryOwner provides this
    ) {
    
    
        // 调用 SaveableStateProvider
        saveableStateHolder.SaveableStateProvider(content)
    }
}

Como se indicó anteriormente, antes de llamar a SaveableStateProvider, se inyectan muchos propietarios a través de CompositonLocal, y la implementación de estos propietarios es esta, que apunta a la NavBackStackEntry actual

  • LocalViewModelStoreOwner: puede crear y administrar ViewModel basado en BackStackEntry
  • LocalLifecycleOwner: Proporcione LifecycleOwner para facilitar operaciones como las suscripciones basadas en Lifecycle.
  • LocalSavedStateRegistryOwner: Registre la devolución de llamada para el ahorro de estado a través de SavedStateRegistry. Por ejemplo, el ahorro de estado en RememberSaveable en realidad se registra a través de SavedStateRegistry y se le devuelve la llamada en un momento específico.

Se puede ver que en la arquitectura de una sola página basada en la navegación, NavBackStackEntry tiene la misma responsabilidad que Fragment, como proporcionar ViewModel a nivel de página, etc.

Como se mencionó anteriormente, SaveableStateProvider necesita restaurar el estado a través de la clave, entonces, ¿cómo se especifica la clave?

El SaveableStateProvider llamado en LocalOwnersProvider no especifica la clave del parámetro, resulta que es un contenedor para la llamada interna:

@Composable
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
    
    
    val viewModel = viewModel<BackStackEntryIdViewModel>()
    
    //设置 saveableStateHolder,后文介绍
    viewModel.saveableStateHolder = this
    
    //
    SaveableStateProvider(viewModel.id, content)
    
    DisposableEffect(viewModel) {
    
    
        onDispose {
    
    
            viewModel.saveableStateHolder = null
        }
    }
}

El SaveableStateProvider real se llama aquí y la clave se administra a través de ViewModel. Debido a que NavBackStackEntry en sí mismo es ViewModelStoreOwner, cuando se inserta un nuevo NavBackStackEntry en la pila, el siguiente NavBackStackEntry y su ViewModel aún existen. Cuando NavBackStackEntry vuelve a la parte superior de la pila, puede obtener la identificación previamente guardada de BackStackEntryIdViewModel y pasarla a SaveableStateProvider.

La implementación de BackStackEntryIdViewModel es la siguiente:

//androidx/navigation/compose/BackStackEntryIdViewModel.kt
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
    
    

    private val IdKey = "SaveableStateHolder_BackStackEntryKey"
    
    // 唯一 ID,可通过 SavedStateHandle 保存和恢复
    val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also {
    
     handle.set(IdKey, it) }

    var saveableStateHolder: SaveableStateHolder? = null

    override fun onCleared() {
    
    
        super.onCleared()
        saveableStateHolder?.removeState(id)
    }
}

Aunque por el nombre, BackStackEntryIdViewModel se usa principalmente para administrar BackStackEntryId, pero de hecho también es el titular de saveableStateHolder de la BackStackEntry actual. ViewModel se pasa a saveableStateHolder en SaveableStateProvider. Mientras exista ViewModel, el estado de la interfaz de usuario no será perdido. Después de que el NavBackStackEntry actual esté fuera de la pila, se produce onCleared en el modelo de vista correspondiente. En este momento, el estado se borrará a través de saveableStateHolder#removeState removeState. Cuando vuelva a navegar a este destino, no se dejará el estado anterior.

Ahorro de estado cuando se cambia la barra de navegación inferior

navigation-compose se usa a menudo para cooperar con BottomNavBar para realizar el cambio de página de múltiples pestañas. Si usamos directamente NavController#navigate para cambiar de página de pestaña, hará que NavBackStack crezca infinitamente, por lo que debemos eliminar las páginas que no necesitan mostrarse de la pila a tiempo después del cambio de página, por ejemplo, de la siguiente manera:

val navController = rememberNavController()

Scaffold(
  bottomBar = {
    
    
    BottomNavigation {
    
    
      ...
      items.forEach {
    
     screen ->
        BottomNavigationItem(
          ...
          onClick = {
    
    
            navController.navigate(screen.route) {
    
    
              // 避免 BackStack 增长,跳转页面时,将栈内 startDestination 之外的页面弹出
              popUpTo(navController.graph.findStartDestination().id) {
    
    
                //出栈的 BackStack 保存状态
                saveState = true
              }
              // 避免点击同一个 Item 时反复入栈
              launchSingleTop = true
              
              // 如果之前出栈时保存状态了,那么重新入栈时恢复状态
              restoreState = true
            }
          }
        )
      }
    }
  }
) {
    
     
  NavHost(...) {
    
    
    ...
  }
}

La clave del código anterior es garantizar que el estado del Destino correspondiente se guarde cuando NavBackStack se saque de la pila configurando saveState y restoreState, y que se pueda restaurar cuando el Destino se vuelva a colocar en la pila.

Si el estado quiere guardarse, significa que el modelo de vista relacionado no se puede destruir, y sabemos que NavBackStack es ViewModelStoreOwner, ¿cómo seguir guardando el modelo de vista después de que NavBackStack se extrae de la pila? De hecho, ViewModel bajo la jurisdicción de NavBackStack se administra en NavController

En el diagrama de clases anterior, puede ver claramente su relación. NavController contiene un NavControllerViewModel, que es la implementación de NavViewModelStoreProvider, y administra el ViewModelStore correspondiente a cada NavController a través de Map. El ViewModelStore de NavBackStackEntry se toma de NavViewModelStoreProvider.

Cuando NavBackStackEntry aparece fuera de la pila, su contenido Destination# correspondiente sale de la pantalla, se ejecuta onDispose,

Crossfade(backStackEntry.id, modifier) {
    
    
    
    ... 
    DisposableEffect(Unit) {
    
    
        ...
        
        onDispose {
    
    
            visibleEntries.forEach {
    
     entry ->
                //显示中的 Entry 移出屏幕,调用 onTransitionComplete
                composeNavigator.onTransitionComplete(entry)
            }
        }
    }

    lastEntry.LocalOwnersProvider(saveableStateHolder) {
    
    
        (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
    }
}

Llame a NavigatorState#markTransitionComplete en onTransitionComplete:

override fun markTransitionComplete(entry: NavBackStackEntry) {
    
    
    val savedState = entrySavedState[entry] == true
    ...
    if (!backQueue.contains(entry)) {
    
    
        ...
        if (backQueue.none {
    
     it.id == entry.id } && !savedState) {
    
    
            viewModel?.clear(entry.id)  //清空 ViewModel
        }
        ...
    } 
    
    ...
}

De forma predeterminada, entrySavedState[entry] es falso, y viewModel#clear se ejecutará aquí para borrar el ViewModel correspondiente a la entrada, pero cuando establecemos saveState en verdadero en popUpTo { ... }, entrySavedState[entry] será verdadero, por lo que aquí ViewModel#clear no se ejecutará.

Si establecemos restoreState en verdadero al mismo tiempo, cuando el mismo tipo de destino ingrese a la página la próxima vez, k puede restaurar el estado a través de ViewModle.

//androidx/navigation/NavController.kt

private fun navigate(
    ...
) {
    
    

    ...
    //restoreState设置为true后,命中此处的 shouldRestoreState()
    if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
    
    
        navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
    } 
    ...
}

En restoreStateInternal, busque el BackStackId correspondiente anterior de acuerdo con DestinationId y luego use BackStackId para recuperar ViewModel y restaurar el estado.

Animación de transición de navegación

El fragmento de navegación nos permite especificar la animación especial al saltar a la página a través del archivo de recursos de la siguiente manera

findNavController().navigate(
    R.id.action_fragmentOne_to_fragmentTwo,
    null,
    navOptions {
    
     
        anim {
    
    
            enter = android.R.animator.fade_in
            exit = android.R.animator.fade_out
        }
    }
)

Dado que la animación de Compose no se basa en archivos de recursos, navigation-compose no es compatible con el anim anterior {...}, pero, en consecuencia, navigation-compose puede realizar una animación de navegación basada en la API de animación de Compose.

Nota: Las API de animación de Comopse en las que se basa la composición de navegación, como AnimatedContent, aún se encuentran en un estado experimental. Por lo tanto, las animaciones de navegación solo se pueden introducir a través de la animación de navegación complementaria por el momento. Una vez que se estabilice la API de animación, se moverá a navegación-componer en el futuro.

dependencies {
    
    
    implementation "com.google.accompanist:accompanist-navigation-animation:<version>"
}

Después de agregar dependencias, puede obtener una vista previa de la forma API de la animación de navegación de composición de navegación por adelantado:

AnimatedNavHost(
    navController = navController,
    startDestination = AppScreen.main,
    enterTransition = {
    
    
        slideInHorizontally(
            initialOffsetX = {
    
     it },
            animationSpec = transSpec
        )
    },
    popExitTransition = {
    
    
        slideOutHorizontally(
            targetOffsetX = {
    
     it },
            animationSpec = transSpec
        )
    },
    exitTransition = {
    
    
        ...
    },
    popEnterTransition = {
    
    
        ...
    }

) {
    
    
    composable(
        AppScreen.splash,
        enterTransition = null,
        exitTransition = null
    ) {
    
    
        Splash()
    }
    composable(
        AppScreen.login,
        enterTransition = null,
        exitTransition = null
    ) {
    
    
        Login()
    }
    composable(
        AppScreen.register,
        enterTransition = null,
        exitTransition = null
    ) {
    
    
        Register()
    }
    ...
}

La API es muy intuitiva, las animaciones de transición se pueden especificar AnimatedNavHostuniformemente o por separado en cada parámetro componible.

Recuerde que en NavHost Destination#contentse llama Crossfade. Aquellos que están familiarizados con las animaciones de Compose pueden imaginar fácilmente que puede usar AnimatedContent para especificar diferentes efectos de animación para el cambio de contenido. Esto es exactamente lo que hace navigatioin-compose :

//com/google/accompanist/navigation/animation/AnimatedNavHost.kt

@Composable
public fun AnimatedNavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
        {
    
     fadeIn(animationSpec = tween(700)) },
    exitTransition: ...,
    popEnterTransition: ...,
    popExitTransition: ...,
) {
    
    

    ...
    
    val backStackEntry = visibleTransitionsInProgress.lastOrNull() ?: visibleBackStack.lastOrNull()

    if (backStackEntry != null) {
    
    
        val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
    
    
            ...
        }

        val finalExit: AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition = {
    
    
            ...
        }

        val transition = updateTransition(backStackEntry, label = "entry")
        
        transition.AnimatedContent(
            modifier,
            transitionSpec = {
    
     finalEnter(this) with finalExit(this) },
            contentAlignment,
            contentKey = {
    
     it.id }
        ) {
    
    
            ...
            currentEntry?.LocalOwnersProvider(saveableStateHolder) {
    
    
                (currentEntry.destination as AnimatedComposeNavigator.Destination)
                    .content(this, currentEntry)
            }
        }
        ...
    }

    ...
}

Como se indicó anteriormente, la principal diferencia entre AnimatedNavHost y NavHost ordinario es que se reemplaza Crossfade Transition#AnimatedContent. finalEntery finalExitson animaciones de transición de composición calculadas de acuerdo con los parámetros transitionSpecespecificados por . Tome finalEnter como ejemplo para ver la implementación específica

val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
    
    
    val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination

    if (composeNavigator.isPop.value) {
    
    
        //当前页面即将出栈,执行pop动画
        targetDestination.hierarchy.firstNotNullOfOrNull {
    
     destination ->
            //popEnterTransitions 中存储着通过 composable 参数指定的动画
            popEnterTransitions[destination.route]?.invoke(this)
        } ?: popEnterTransition.invoke(this)
    } else {
    
    
        //当前页面即将入栈,执行enter动画
        targetDestination.hierarchy.firstNotNullOfOrNull {
    
     destination ->
            enterTransitions[destination.route]?.invoke(this)
        } ?: enterTransition.invoke(this)
    }
}

Como arriba, popEnterTransitions[destination.route]es la animación especificada en el parámetro composable(…), por lo que la prioridad de la animación especificada por el parámetro composable es más alta que la de AnimatedNavHost.

Empuñadura y navegación

Dado que cada BackStackEntry es un ViewModelStoreOwner, podemos obtener el ViewModel en el nivel de la página de navegación. El uso de hilt-viewmodle-navigation puede inyectar las dependencias necesarias en ViewModel a través de Hilt, lo que reduce el costo de construcción de ViewModel.

dependencies {
    
    
    implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
}

El efecto de obtener ViewModel basado en empuñadura es el siguiente:

// import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun MyApp() {
    
    
    NavHost(navController, startDestination = startRoute) {
    
    
        composable("example") {
    
     backStackEntry ->
            // 通过 hiltViewModel() 获取 MyViewModel,
            val viewModel = hiltViewModel<MyViewModel>()
            MyScreen(viewModel)
        }
        /* ... */
    }
}

Solo necesitamos MyViewModelagregar @HiltViewModely @Injectanotaciones , y su parámetro dependiente repositoryse puede inyectar automáticamente a través de Hilt, ahorrándonos la molestia de personalizar ViewModelFactory.

@HiltViewModel
class MyViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val repository: ExampleRepository
) : ViewModel() {
    
     /* ... */ }

Eche un breve vistazo al código fuente de hiltViewModel

@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
    
    
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    }
): VM {
    
    
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, factory = factory)
}

@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
    viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is NavBackStackEntry) {
    
    
    HiltViewModelFactory(
        context = LocalContext.current,
        navBackStackEntry = viewModelStoreOwner
    )
} else {
    
    
    null
}

Como se mencionó anteriormente, LocalViewModelStoreOwneres el BackStackEntry actual.Después de obtener viewModelStoreOwner, HiltViewModelFactory()obtenga ViewModelFactory a través de . HiltViewModelFactory es el alcance de hilt-navigation , por lo que no lo profundizaré aquí.

por fin

Algunas otras funciones de navigation-compose , como Deeplinks, Arguments, etc., no tienen un tratamiento especial para Compose en la implementación, por lo que no las presentaré aquí. Si está interesado, puede leer el código fuente de navigation-common . A través de una serie de introducciones en este artículo, podemos ver que navigation-compose sigue la idea básica de declarativo tanto en el diseño como en la implementación de la API. Cuando necesitamos desarrollar nuestra propia biblioteca tripartita de Compose, podemos consultar y aprender de ella.

Supongo que te gusta

Origin blog.csdn.net/vitaviva/article/details/126635257
Recomendado
Clasificación