Home > Back-end >  How to use a coroutine inside a composable screen with flow?
How to use a coroutine inside a composable screen with flow?

Time:09-17

I'm using jetpack Compose and flow, and I'm getting an error trying to get data within a composable screen with LaunchedEffect

Invocations of @Composable can only happen from the context of an @ Composable function

Here I detail the flow of my code

Here it generates the error in LaunchedEffect

Screen

@Composable
fun LoginScreen(
    navController: NavController,
    viewModel: LoginViewModel = hiltViewModel()
) {
  

    Box(
        modifier = Modifier.fillMaxSize().fillMaxHeight()
    ) {
        Column(
            modifier = Modifier.fillMaxWidth().padding(15.dp),
        ) {
            //TextField username
            //TextField password
 
            Button(
                onClick = {
                     // Error  
                     // @Composable invocations can only happen from the context of a @Composable function
                    LaunchedEffect(Unit) {
                        viewModel.login(
                            viewModel.passwordValue.value, viewModel.usernameValue.value
                        )
                    }

                },
               
            ) {
                Text(text = stringResource(id = R.string.login))
            }
        }
    }
}
ViewModel
@HiltViewModel
class LoginViewModel @Inject constructor(private val toLogin: ToLogin) : ViewModel() {

    private val _usernameValue = mutableStateOf("")
    val usernameValue: State<String> = _usernameValue

    private val _passwordValue = mutableStateOf("")
    val passwordValue: State<String> = _passwordValue

    fun setUsernameValue(username: String) {
        _usernameValue.value = username
    }

    fun setPasswordValue(password: String) {
        _passwordValue.value = password
    }

     suspend fun login(username: String, password: String) {

        val r = toLogin(username, password);
        r.collect {
            Log.d("XTRACE", it.toString());
        }

    }
}
API
class AuthApiSource @Inject constructor(
    private val loginApiService: LoginApiService,
) {
    suspend fun login(username: String, password: String): Result<AccessToken?> = runCatching {

        loginApiService.toLogin(
            username = username,
            password = password,
        ).body();

    }
}
Use case
class ToLogin @Inject constructor(private val apiAuth: AuthApiSource) {
    operator fun invoke(username: String, password: String): Flow<Result<AccessToken?>> =
        flow {
            val response = runCatching {
                val token = apiAuth.login(username, password)
                token.getOrThrow()
            }
            emit(response)
        }
}

What would be the correct way to do it?

CodePudding user response:

You have to use rememberCoroutineScope:

@Composable
fun LoginScreen(
    navController: NavController,
    viewModel: LoginViewModel = hiltViewModel()
) {

    val scope = rememberCoroutineScope()
    Box(
        modifier = Modifier.fillMaxSize().fillMaxHeight()
    ) {
        Column(
            modifier = Modifier.fillMaxWidth().padding(15.dp),
        ) {
            //TextField username
            //TextField password

            Button(
                onClick = {
                    // Error  
                    // @Composable invocations can only happen from the context of a @Composable function
                    scope.launch {
                        viewModel.login(
                            viewModel.passwordValue.value, viewModel.usernameValue.value
                        )
                    }

                },

                ) {
                Text(text = stringResource(id = R.string.login))
            }
        }
    }
}
  • Related