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

Posted

技术标签:

【中文标题】如何在可组合的屏幕中使用协同程序?【英文标题】:How to use a coroutine inside a composable screen with flow? 【发布时间】:2021-11-10 03:55:52 【问题描述】:

我正在使用 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)
        

正确的做法是什么?

【问题讨论】:

当您需要在渲染视图时更改状态时,您应该使用LaunchedEffect,例如当视图刚刚出现或状态值之一已更改时。在你的情况下@Francesc 是对的。 【参考方案1】:

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

【讨论】:

【参考方案2】:

补充 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 系统有时会多次调用可组合方法。

【讨论】:

以上是关于如何在可组合的屏幕中使用协同程序?的主要内容,如果未能解决你的问题,请参考以下文章

移动oa软件如何实现小屏幕大协同?

Angular 和 Express 路由如何在 mean.js 应用程序中协同工作?

如何了解图书馆如何协同工作?

在没有JVM支持的情况下,如何在JVM语言中实现协同程序?

如何在 Xcode 4 中协同设计和沙盒助手应用程序?

在intelliJ项目中集成Kotlinx协同程序