Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

281
Vistas
Práctica recomendada de TopAppBar condicional para toda la aplicación de Jetpack Compose

Tengo una aplicación Jetpack Compose de Android que usa elementos componibles BottomNavigation y TopAppBar . Desde la pestaña abierta a través BottomNavigation , los usuarios pueden navegar más profundamente en el gráfico de navegación.

El problema

El componible de TopAppBar debe representar la pantalla actual, por ejemplo, mostrar su nombre, implementar algunas opciones que son específicas de la pantalla abierta, el botón Atrás si la pantalla es de alto nivel. Sin embargo, Jetpack Compose parece no tener una solución lista para usar y los desarrolladores deben implementarla por sí mismos.

Entonces, las ideas obvias vienen con inconvenientes obvios, algunas ideas son mejores que otras.

La línea de base para el seguimiento de la navegación, como sugiere Google (al menos para BottomNavigation ), es una clase sealed que contiene object que representan la pantalla activa actual. Específicamente para mi proyecto, es así:

 sealed class AppTab(val route: String, @StringRes val resourceId: Int, val icon: ImageVector) { object Events: AppTab("events_tab", R.string.events, Icons.Default.EventNote) object Projects: AppTab("projects_tab", R.string.projects, Icons.Default.Widgets) object Devices: AppTab("devices_tab", R.string.devices, Icons.Default.DevicesOther) object Employees: AppTab("employees_tab", R.string.employees, Icons.Default.People) object Profile: AppTab("profile_tab", R.string.profile, Icons.Default.AccountCircle) }

Ahora TopAppBar puede saber qué pestaña está abierta, siempre que remember el objeto AppTab , pero ¿cómo sabe si una pantalla está abierta desde una pestaña determinada?

Solución 1: obvia y obviamente incorrecta

Proporcionamos a cada pantalla su propia TopAppBar y dejamos que maneje toda la lógica necesaria. Además de una gran cantidad de duplicación de código, la TopAppBar de cada pantalla se recompondrá al abrir la pantalla y, como se describe en esta publicación, parpadeará.

Solución 2 - no muy elegante

A partir de ahora, decidí tener una sola TopAppBar en el componente componible de nivel superior de mi proyecto, que dependerá de un state con la pantalla actual guardada. Ahora podemos implementar fácilmente la lógica para las pestañas.

Para resolver el problema de las pantallas abiertas desde una pestaña, amplié la idea de Google e implementé una clase AppScreen general que representa cada pantalla que se puede abrir:

 // This class represents any screen - tabs and their subscreens. // It is needed to appropriately change top app bar behavior sealed class AppScreen(@StringRes val screenNameResource: Int) { // Employee-related object Employees: AppScreen(R.string.employees) object EmployeeDetails: AppScreen(R.string.profile) // Events-related object Events: AppScreen(R.string.events) object EventDetails: AppScreen(R.string.event) object EventNew: AppScreen(R.string.event_new) // Projects-related object Projects: AppScreen(R.string.projects) // Devices-related object Devices: AppScreen(R.string.devices) // Profile-related object Profile: AppScreen(R.string.profile) }

Luego lo guardo en un state en el componente componible de nivel superior en el ámbito de TopAppBar y paso currentScreenHandler como un argumento onNavigate a mis componentes componibles de pestaña:

 var currentScreen by remember { mutableStateOf(defaultTab.asScreen()) } val currentScreenHandler: (AppScreen) -> Unit = {navigatedScreen -> currentScreen = navigatedScreen}
 // Somewhere in the bodyContent of a Scaffold when (currentTab) { AppTab.Employees -> EmployeesTab(currentScreenHandler) // And other tabs // ... }

Y desde dentro de la pestaña componible:

 val navController = rememberNavController() NavHost(navController, startDestination = "employees") { composable("employees") { onNavigate(AppScreen.Employees) Employees(it.hiltViewModel(), navController) } composable("employee/{userId}") { onNavigate(AppScreen.EmployeeDetails) Employee(it.hiltViewModel()) } }

Ahora TopAppBar en el componible raíz conoce las pantallas de nivel superior y puede implementar la lógica necesaria. ¿Pero hacer esto para cada subpantalla de una aplicación? Una cantidad considerable de duplicación de código y arquitectura de comunicación entre esta barra de aplicaciones y un componible que representa (cómo reacciona el componible a las acciones realizadas en la barra de aplicaciones) aún no se ha compuesto (juego de palabras).

Solución 3: ¿la mejor?

Implementé un viewModel para manejar la lógica necesaria, ya que parecía la solución más elegante:

 @HiltViewModel class AppBarViewModel @Inject constructor() : ViewModel() { private val defaultTab = AppTab.Events private val _currentScreen = MutableStateFlow(defaultTab.asScreen()) val currentScreen: StateFlow<AppScreen> = _currentScreen fun onNavigate(screen: AppScreen) { _currentScreen.value = screen } }

Raíz componible:

 val currentScreen by appBarViewModel.currentScreen.collectAsState()

Pero no resolvió el problema de duplicación de código de la segunda solución. En primer lugar, tuve que pasar este viewModel de vista al componible raíz desde MainActivity , ya que parece que no hay otra forma de acceder a él desde dentro de un componible. Así que ahora, en lugar de pasar un currentScreenHandler a Tab componibles, les paso un viewModel y, en lugar de llamar al controlador en el evento de navegación, llamo a viewModel.onNavigate(AppScreen) , ¡así que hay aún más código! Al menos, tal vez pueda implementar un mecanismo de comunicación mencionado en la solución anterior.

La pregunta

Por ahora, la segunda solución parece ser la mejor en términos de cantidad de código, pero la tercera permite la comunicación y una mayor flexibilidad en el futuro para algunas funciones que aún no se han solicitado. Puede que me esté perdiendo algo obvio y elegante. ¿Cuál de mis implementaciones consideras la mejor, y si ninguna, qué harías para solucionar este problema?

Gracias.

over 4 years ago · Santiago Trujillo
1 Respuestas
Responde la pregunta

0

Utilizo una sola TopAppBar en Scaffold y uso un título, menú desplegable, íconos, etc. diferentes al generar eventos desde Composables. De esa forma, puedo usar solo una TopAppBar con diferentes valores. Aquí hay un ejemplo:

 val navController = rememberNavController() var canPop by remember { mutableStateOf(false) } var appTitle by remember { mutableStateOf("") } var showFab by remember { mutableStateOf(false) } var showDropdownMenu by remember { mutableStateOf(false) } var dropdownMenuExpanded by remember { mutableStateOf(false) } var dropdownMenuName by remember { mutableStateOf("") } var topAppBarIconsName by remember { mutableStateOf("") } val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope() val tourViewModel: TourViewModel = viewModel() val clientViewModel: ClientViewModel = viewModel() navController.addOnDestinationChangedListener { controller, _, _ -> canPop = controller.previousBackStackEntry != null } val navigationIcon: (@Composable () -> Unit)? = if (canPop) { { IconButton(onClick = { navController.popBackStack() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "Back Arrow" ) } } } else { { IconButton(onClick = { scope.launch { scaffoldState.drawerState.apply { if (isClosed) open() else close() } } }) { Icon(Icons.Filled.Menu, contentDescription = null) } } } Scaffold( scaffoldState = scaffoldState, drawerContent = { DrawerContents( navController, onMenuItemClick = { scope.launch { scaffoldState.drawerState.close() } }) }, topBar = { TopAppBar( title = { Text(appTitle) }, navigationIcon = navigationIcon, elevation = 8.dp, actions = { when (topAppBarIconsName) { "ClientDirectoryScreenIcons" -> { // search icon on client directory screen IconButton(onClick = { clientViewModel.toggleSearchBar() }) { Icon( imageVector = Icons.Filled.Search, contentDescription = "Search Contacts" ) } } } if (showDropdownMenu) { IconButton(onClick = { dropdownMenuExpanded = true }) { Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null) DropdownMenu( expanded = dropdownMenuExpanded, onDismissRequest = { dropdownMenuExpanded = false } ) { // show different dropdowns based on different screens when (dropdownMenuName) { "ClientDirectoryScreenDropdown" -> ClientDirectoryScreenDropdown( onDropdownMenuExpanded = { dropdownMenuExpanded = it }) } } } } } ) }, ... ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { NavHost( navController = navController, startDestination = Screen.Tours.route ) { composable(Screen.Tours.route) { TourScreen( tourViewModel = tourViewModel, onSetAppTitle = { appTitle = it }, onShowDropdownMenu = { showDropdownMenu = it }, onTopAppBarIconsName = { topAppBarIconsName = it } ) }

Luego configure los valores de TopAppBar desde diferentes pantallas como esta:

 @Composable fun TourScreen( tourViewModel: TourViewModel, onSetAppTitle: (String) -> Unit, onShowDropdownMenu: (Boolean) -> Unit, onTopAppBarIconsName: (String) -> Unit ) { LaunchedEffect(Unit) { onSetAppTitle("Tours") onShowDropdownMenu(false) onTopAppBarIconsName("") } ...

Probablemente no sea la forma perfecta de hacerlo, pero no hay código duplicado.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda