如何在带有流的可组合屏幕内使用协程?

How to use a coroutine inside a composable screen with flow?

我正在使用 Jetpack Compose 和 Flow,但在使用 LaunchedEffect

尝试在可组合屏幕中获取数据时遇到错误

@Composable 的调用只能在@Composable 函数的上下文中发生

这里详细介绍一下我的代码流程

此处会产生 LaunchedEffect

中的错误
屏幕

@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))
            }
        }
    }
}
视图模型
@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();

    }
}
用例
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)
        }
}

正确的做法是什么?

你必须使用 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))
            }
        }
    }
}

补充 Francesc 的回答,您可以将 viewModel 的方法作为参数传递,例如:

@Composable
fun LoginScreen(
    navController: NavController,
    clickButtonCallback: () -> Unit
) {
    Box(
        modifier = Modifier.fillMaxSize().fillMaxHeight()
    ) {
        Column(
            modifier = Modifier.fillMaxWidth().padding(15.dp)
        ) {
            //TextField username
            //TextField password
            Button(onClick = clickButtonCallback) {
                Text(text = stringResource(id = R.string.login))
            }
        }
    }
}

并在调用可组合方法时使用:

val scope = rememberCoroutineScope()
val viewModel: LoginViewModel = hiltViewModel()
LoginScreen(
    navController = navController,
    clickButtonCallback = {
        scope.launch {
            viewModel.getNewSessionToken()
        }
    }
)

这样做,您的 ViewModel 只会创建一次,因为 Android 系统有时会多次调用可组合方法。