简体   繁体   English

如何在可组合屏幕内使用协同程序?

[英]How to use a coroutine inside a composable screen with flow?

I'm using jetpack Compose and flow, and I'm getting an error trying to get data within a composable screen with LaunchedEffect我正在使用 jetpack Compose 和 flow,但在尝试使用LaunchedEffect在可组合屏幕中获取数据时出现错误

Invocations of @Composable can only happen from the context of an @ Composable function @Composable 的调用只能在@Composable 函数的上下文中发生

Here I detail the flow of my code在这里我详细说明我的代码的流程

Here it generates the error in LaunchedEffect在这里它会在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?什么是正确的方法呢?

You have to use rememberCoroutineScope :你必须使用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))
            }
        }
    }
}

Complementing Francesc's answer, you can pass the viewModel's method as a parameter, like:补充 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))
            }
        }
    }
}

And use whenever you call the composable method:并在调用可组合方法时使用:

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

Doing so, your ViewModel is created only once, since composable methods sometimes are called multiple times by the Android system.这样做,您的 ViewModel 只会创建一次,因为 Android 系统有时会多次调用可组合方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM