Tentei seguir os exemplos na documentação do Android, mas não consigo implementar a navegação usando Navigation Compose, ViewModel e Hilt. Nenhum erro é lançado e no NavigationScreen LaunchedEffect não é acionado em emissões do ViewModel.
Compostos:
@HiltAndroidApp
class MyApplication : Application() {}
@Serializable
object NavScreen
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val navController = rememberNavController()
PhotosSyncTheme {
NavHost(
navController = navController,
startDestination = NavScreen
) {
composable<NavScreen> { NavigationScreen(navController) }
composable<NavigationEvent.NavigateToHome> { HomeScreen() }
composable<NavigationEvent.NavigateToSettings> { Settings() }
}
}
}
}
}
@Composable
fun NavigationScreen(navController: NavController) {
val navViewModel = hiltViewModel<NavigationViewModel>()
val navigationEvent by navViewModel.navigationEvents.collectAsState(initial = NavigationEvent.NavigateToHome)
LaunchedEffect(navigationEvent) {
Log.d("Navigation Screen", navigationEvent.toString())
navigationEvent.let { event ->
Log.d("Navigation Screen", event.toString())
when (event) {
is NavigationEvent.NavigateBack -> navController.popBackStack()
NavigationEvent.NavigateToHome -> navController.navigate(NavigationEvent.NavigateToHome)
NavigationEvent.NavigateToSettings -> navController.navigate(NavigationEvent.NavigateToSettings)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen() {
val navViewModel = hiltViewModel<NavigationViewModel>()
var selectedItem by remember { mutableIntStateOf(0) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(title = { Text("Photos Sync") }, actions = {
IconButton(onClick = {
navViewModel.navigateToSettings()
}) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = "Settings icon"
)
}
})
}
)
{ }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Settings() {
val viewModel = hiltViewModel<NavigationViewModel>()
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBarWithBack(title = "Definições")
}
)
{ }
}
Modelo de exibição:
@HiltViewModel
class NavigationViewModel @Inject constructor() : ViewModel() {
private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
val navigationEvents: SharedFlow<NavigationEvent> = _navigationEvents.asSharedFlow()
fun navigateBack() {
viewModelScope.launch { _navigationEvents.emit(NavigationEvent.NavigateBack) }
}
fun navigateToHome() {
viewModelScope.launch { _navigationEvents.emit(NavigationEvent.NavigateToHome) }
}
fun navigateToSettings() {
viewModelScope.launch {
Log.d("Navigation ViewModel", "Emit Navigate To Settings")
try {
_navigationEvents.emit(NavigationEvent.NavigateToSettings)
} catch (e: Exception) {
Log.e("Navigation ViewModel", "Error emitting navigation event")
} finally {
Log.d("Navigation ViewModel", "done Emit Navigate To Settings")
}
}
}
fun navigateTo(event: NavigationEvent) {
viewModelScope.launch { _navigationEvents.emit(event) }
}
sealed class NavigationEvent {
@Serializable
object NavigateBack : NavigationEvent()
@Serializable
object NavigateToHome : NavigationEvent()
@Serializable
object NavigateToSettings : NavigationEvent()
}
}
Eu esperava que quando eu clicasse no botão de configurações, o composable de configurações fosse empurrado para o topo da pilha de composables usandonavController.navigate
Os modelos de visualização são delimitados para o destino de navegação atual (seja usando o Hilt ou não). Você usa o mesmo modelo de visualização para telas diferentes, então cada uma terá sua própria instância de modelo de visualização. Modificar o estado de um não afetará o estado dos outros.
Você pode definir o escopo do view model para outra coisa para que seus destinos compartilhem a mesma instância do view model (veja Posso compartilhar um ViewModel usando hiltViewModel() em uma rota de navegação do Compose diferente? ), mas não vejo por que você quer envolver um view model em primeiro lugar, já que a navegação não está relacionada ao estado da IU. Tudo o que seu view model faz atualmente é receber o evento de navegação e passá-lo de volta para o composable. Você nem mesmo persiste a rota atual ou seus parâmetros ( o que poderia ser feito usando um
SavedStateHandle
), mas isso também é útil principalmente para view models dedicados a uma tela específica, não um view model de navegação .Eu recomendaria remover o
NavigationViewModel
inteiramente. Você pode manter suaNavigationEvent
hierarquia sealed, embora eu ache o nome bastante enganoso e você nem precise da herança. Melhor substituí-lo por algo assim (declarado no nível superior, sem uma interface envolvente):Como você não os usa mais como eventos, eles agora são usados apenas para rotas no seu gráfico de navegação, daí o nome. Como
NavigateBack
não é uma rota, ele pode ser removido (basta chamarnavController.popBackStack()
quando necessário).Seu NavHost ficaria assim:
Note que em vez de
HomeScreen
depender do modelo de visualização para acessar a navegação, o parâmetro recém-criadonavigateToSettings
recebe a lógica de navegação necessária. Você não deve passar o navController diretamente.HomeScreen
then usa o novo parâmetro assim:Isso também torna o HomeScreen mais testável e reutilizável, já que não depende mais do modelo de visualização.
Toda a lógica de navegação agora está contida no NavHost.
NavigationScreen
Com o LaunchedEffect, é possível removê-lo.O Google fornece o projeto de amostra totalmente funcional Now in Android que - entre outras coisas - também mostra como usar uma navegação mais complexa. Se você quiser bisbilhotar, pode simplesmente importá-lo no Android Studio (" Obter do Controle de Versão... ") usando este link: https://github.com/android/nowinandroid