adrielcafe / voyager

🛸 A pragmatic navigation library for Jetpack Compose
https://voyager.adriel.cafe
MIT License
2.27k stars 109 forks source link

[Question] How to navigate from ScreenModel/ViewModel? #354

Open rovkinmax opened 2 months ago

rovkinmax commented 2 months ago

Hello! Do you have an understanding of how to implement navigation from a ScreenModel? I've studied the documentation and examples, but they all describe quite simple cases where all the navigation is done within compose functions. In slightly more complex applications, at least in my case, a click on a UI element can hide a lot of logic (form validation, permissions checking, network requests, etc.), and to navigate further, the ScreenModel needs to expose something to let the compose function know that it's time to change the screen. May be you have some workaround for that?

akardas16 commented 1 month ago

You can use Kotlin callback functions.

YourViewModel / ScreenModel {

 suspend fun loginUser(context: Context, response:(succes: LoginResponseModel?,error: String?) -> Unit){
        withContext(Dispatchers.IO){
            repository.loginUser() { response, error ->
                response?.let {
                    response(it,null)
                }
                error?.let {
                   response(null,it)
                }
            }
        }
    }
}

call above function in your compose like

 scope.launch{
      viewModel.login(context){response, error -> 

             response?.let{
                //Succed navigate Home Screen
             }

            error?.let{
               //failed navigate bottomsheet to show fail message
             }
        }
}
rovkinmax commented 2 days ago

I've created this solution after small investigation This is not completed yet, but can show the main idea Idea is simple: build proxy VoyagerRouter for voyager navigator and ViewModel, DI provide instance of VoyagerRouter to ViewModel and also to compose component

class VoyagerRouter {
    private val sharedFlow = MutableSharedFlow<VoyagerEvent>(extraBufferCapacity = 1)

    fun events(): SharedFlow<VoyagerEvent> {
        return sharedFlow.asSharedFlow()
    }

    fun navigateTo(screen: Screen) {
        sharedFlow.tryEmit(VoyagerEvent.NavigateTo(screen))
    }

    fun newRootScreen(screen: Screen) {
        sharedFlow.tryEmit(VoyagerEvent.NewRootScreen(screen))
    }

    fun back() {
        sharedFlow.tryEmit(VoyagerEvent.Back)
    }
}

sealed class VoyagerEvent {
    data class NavigateTo(val screen: Screen) : VoyagerEvent()
    data class NewRootScreen(val screen: Screen) : VoyagerEvent()
    data object Back : VoyagerEvent()
}
@Composable
fun FlowComponent(
    screen: Screen,
    scopeName: String = "",
    content: @Composable () -> Unit = {},
) = DIScope(scopeName = scopeName) { // DIScope is my own DI wrapper for compose
    Navigator(screen) {
        val navigator = LocalNavigator.currentOrThrow

        // here my way to get router from DI, will depend on yours DI
        val router = LocalDIScope.current!!.getInstance<VoyagerRouter>()

        LaunchedEffect(scopeName) {
            router.events()
                .collect { event ->
                    when (event) {
                        is VoyagerEvent.NavigateTo -> navigator.push(event.screen)
                        is VoyagerEvent.NewRootScreen -> navigator.replaceAll(event.screen)
                        is VoyagerEvent.Back -> navigator.pop()
                    }
                }
        }

        val currentScreen = navigator.lastItem
        navigator.saveableState("currentScreen") {
            currentScreen.Content()
        }
    }

    // here will be content for your screen
    content()
}