我在构建一个小型电影库应用程序时遇到了一个问题。我的结构如下:我有一个 MovieSearchScreen 函数,它调用一个 SearchBar 函数以及一个 MovieSearchR...
我在构建一个小型电影库应用程序时遇到了一个问题。我的结构如下:我有一个 MovieSearchScreen 函数,它调用一个 SearchBar 函数和一个 MovieSearchResults 函数。在 Searchbar 函数中,我有一个 TextField、一个 leadingIcon 和一个 trailingIcon,后者有一个可点击修饰符,它调用我的 viewModel 中的 updateMovieResource 函数:
@Composable
fun Create(navController: NavHostController, viewModel: MovieViewModel) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.DarkGray)
) {
SearchBar(viewModel = viewModel)
MovieSearchResults(navController = navController)
}
}
@Composable
private fun SearchBar(viewModel: MovieViewModel) {
var userInput: String by remember {
mutableStateOf("")
}
TextField(
value = userInput,
onValueChange = {
userInput = it
},
modifier = Modifier
.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Gray,
unfocusedContainerColor = Color.Gray,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White
),
placeholder = {
Text(
text = stringResource(id = R.string.search_bar),
color = Color.White
)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Clear,
contentDescription = "Clear",
modifier = Modifier
.clickable {
userInput = ""
},
tint = Color.White
)
},
trailingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = "Search",
tint = Color.White,
modifier = Modifier.clickable {
if (userInput.isNotBlank()) {
viewModel.updateMovieResource(userInput)
}
}
)
}
)
}
在这个 viewModel 中,我有几个值,现在重要的是我的 _movieRepository,我从中管理我的 api 调用,_movieResource,movieResource 和 updateMovieResource 方法:
class MovieViewModel: ViewModel() {
private lateinit var _selectedMovie: MutableState<Movie>
var selectedMovie: Movie
get() = _selectedMovie.value
set(movie){
_selectedMovie.value = movie
}
private val _movieRepository: MovieRepository = MovieRepository()
val movieResource: LiveData<Resource<ResponseResult>>
get() = _movieResource
private val _movieResource: MutableLiveData<Resource<ResponseResult>> =
MutableLiveData(Resource.Empty())
fun updateMovieResource(movie: String) {
_movieResource.value = Resource.Loading()
viewModelScope.launch {
_movieResource.value = _movieRepository.getMovie(movie)
}
}
}
我使用 movieResource 来确定我的 api 调用是否成功、仍在加载或返回错误,以便我知道要显示什么。我在这里的 MovieSearchResults 可组合中使用了这个 movieResource:
@Composable
private fun MovieSearchResults(
navController: NavHostController,
viewModel: MovieViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
){
val movieResource: Resource<ResponseResult>? by viewModel.movieResource.observeAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when(movieResource) {
is Resource.Success ->
LazyVerticalGrid(
columns = GridCells.Fixed(3)
) {
items(
items = (movieResource as Resource.Success<ResponseResult>).data!!.results,
itemContent = {
AsyncImage(
model = it.backdropPath,
contentDescription = it.title,
modifier = Modifier.clickable {
viewModel.selectedMovie = it
navController.navigate(MovieLibraryScreens.MovieDetailsScreen.name)
}
)
}
)
}
is Resource.Error ->
Text(
text = (movieResource as Resource.Error<ResponseResult>).message!!,
fontSize = 30.sp,
color = Color.Red
)
is Resource.Empty ->
Text(
text = "Search for a movie!",
fontSize = 30.sp,
color = Color.White
)
is Resource.Loading ->
Text(
text = "Loading...",
fontSize = 30.sp,
color = Color.White
)
else ->
Text(
text = "Something went wrong...",
fontSize = 30.sp,
color = Color.White
)
}
}
}
我从这里的存储库中检索这些数据:
class MovieRepository {
private val _apiService: ApiService = Api().createApi()
private val _apiKey: String = API_KEY
suspend fun getMovie(movie: String): Resource<ResponseResult> {
val response = try {
withTimeout(5_000) {
_apiService.getMovie(movie, _apiKey)
}
} catch (e: Exception) {
Log.e("MovieRepository", e.message ?: "No exception message available")
return Resource.Error("An unknown error occurred while fetching data for the movie: $movie")
}
Log.d("Result", response.results.toString())
return Resource.Success(response)
}
}
当我 Log.d 我的 response.results 时,它会记录我从 api 调用的正确数据,但当我返回 Resource.Success(response) 时。viewModel 中的 _movieResource 似乎没有更新为 Resource.Success(response)。此外,回到我的 MovieSearchResults 可组合项中,当调用 updateMovieResource 方法时,when 语句似乎没有注意到 _movieResource 在开始时已更新为 Resource.Loading()。我完全被这个问题困住了,感觉我对 MutableLiveData 做了一些错误的事情,但我并不完全确定。请帮帮我!
我记录了返回预期 api 响应的结果以及我的 movieResource,当 updateMovieResource 完成运行时,最终返回 Resource.Empty()。此后,我尝试将 MutableLiveData 更改为标准 mutableStateOf(),但这是不可能的,因为我有多个不同的资源响应。
虽然您显然已经解决了当前问题,但我想更深入地解释一下问题所在。
但首先,您应该更新视图模型。您仍在使用 LiveData
(这是 Compose 时代之前的遗留,现已过时)和 MutableState
,它们仅应在可组合项中使用。两者都需要替换为 Flows:
class MovieViewModel : ViewModel() {
private val _selectedMovie = MutableStateFlow<Movie?>(null)
val selectedMovie = _selectedMovie.asStateFlow()
private val _movieRepository: MovieRepository = MovieRepository()
private val _movieResource = MutableStateFlow<Resource<ResponseResult>>(Resource.Empty())
val movieResource: StateFlow<Resource<ResponseResult>> = _movieResource.asStateFlow()
fun updateMovieResource(movie: String) {
_movieResource.value = Resource.Loading()
viewModelScope.launch {
_movieResource.value = _movieRepository.getMovie(movie)
}
}
fun selectMovie(movie: Movie) {
_selectedMovie.value = movie
}
}
您不需要调用 observeAsState
可 collectAsStateWithLifecycle()
组合项来将流转换为状态对象。
解决了这个问题,回到最初的问题。 MovieSearchResults
有一个 viewModel
参数。罪魁祸首是默认值 viewModels()
:这将创建 新 实例,因此实际上涉及两个视图模型:第一个由使用 SearchBar
,第二个由使用 MovieSearchResults
。只有第一个从存储库中检索数据,因此第二个将始终为空。
要解决这个问题,您可以将传递给 SearchBar 的相同视图模型实例传递给 MovieSearchResults。但实际上,您 永远不应 将视图模型传递给可组合项。相反,在最顶层的可组合项中检索需要视图模型的实例(似乎 Create
在您的例子中是这样的),然后仅将视图模型的 状态 和 回调 给其他可组合项:
@Composable
fun Create(navController: NavHostController) {
val viewModel: MovieViewModel = viewModel()
val movieResource: Resource<ResponseResult> by viewModel.movieResource.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.DarkGray),
) {
SearchBar(
updateMovieResource = viewModel::updateMovieResource,
)
MovieSearchResults(
navController = navController,
movieResource = movieResource,
selectMovie = viewModel::selectMovie,
)
}
}
SearchBar 和 MovieSearchResults 声明如下:
@Composable
private fun SearchBar(
updateMovieResource: (String) -> Unit,
) { /*...*/ }
@Composable
private fun MovieSearchResults(
navController: NavHostController,
movieResource: Resource<ResponseResult>,
selectMovie: (Movie) -> Unit,
) { /*...*/ }
这样,您的可组合项就独立于视图模型,对于重组更加稳健(因为视图模型不稳定 ) ,更易于测试(因为您不需要视图模型,您可以提供简单的虚拟参数),可以更容易地重用并可以更好地预览(@Preview)。
它还解决了您遇到的问题。