提问者:小点点

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


我正在使用喷气背包撰写和流,并且尝试使用 LaunchedEffect 在可组合屏幕中获取数据时出错

@Composable的调用只能在 @ 可组合函数的上下文中进行

在这里,我详细介绍了我的代码流程

在这里,它在启动效果中生成错误


@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());
        }

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

正确的方法是什么?


共2个答案

匿名用户

你必须使用 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 系统有时会多次调用可组合方法。