How can I get progress indicator to show up before the data being loaded completely - kotlin

This particular code dosent seems to work the way intended
#Composable
fun MainScreen(
navController: NavController,
mainViewModel: MainViewModel = hiltViewModel(),
city:String?
) {
Log.d("TAG", "MainScreen: $city")
val weatherData =
produceState<DataOrException<Weather, Boolean, Exception>>(
initialValue = DataOrException()
) {
value = mainViewModel.getWeatherData(city = city.toString())
}.value
if (weatherData.loading == true) {
CircularProgressIndicator()
}
if (weatherData.data != null ) {
MainScaffold(weather = weatherData.data!!, navController)
}
}
This particular code dosent seems to work the way intended

Related

My response data return " kotlinx.coroutines.flow.SafeFlow#1493a74"

I am trying to do dictionary app using kotlin language. I built the project with mvvm and clean architecture. I have been trying to pull vocabulary information from the internet using jsoap. I am using flow for data. I couldnt find where the issiue is. Normally, the words should appear on the screen or I should be able to see the data when I println on the console.But I can't see it on the screen or on the console, probably because the data coming from the internet is as follows.
kotlinx.coroutines.flow.SafeFlow#1493a74
I am sharing my codes below
ExtractedData
data class ExtractedData(
var id :Int = 0,
var word:String = "",
var meaning :String = ""
)
I created ExtractedData class to represent vocabulary or word data from internet
WordInfoRepositoryImpl
class WordInfoRepositoryImpl #Inject constructor(
private val api:DictionaryApi
) : WordInfoRepository {
//get words with meanings on the internet using jsoap
override fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>> = flow {
emit(Resource.Loading())
val listData = mutableListOf<ExtractedData>()
try {
val url = "https://ielts.com.au/australia/prepare/article-100-new-english-words-and-phrases-updated-2020"
val doc = withContext(Dispatchers.IO){
Jsoup.connect(url).get()//-->Here it gives the following warning even though I have it in withContext `Inappropriate blocking method call`
}
val table = doc.select("table")
val rows = table.select("tr")
val eventsSize = rows.size
for (i in 1 until eventsSize) {
val row = rows[i]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
listData.add(ExtractedData(i,word,meaning))
}
}
catch (e: IOException) {
emit(Resource.Error("IO Exception"))
}
catch (e : HttpException) {
emit(Resource.Error("HTTP EXCEPTION"))
}
emit(Resource.Success(listData))
}
}
getEventsList is in my WordInfoRepositoryImpl class in my data layer here I am pulling data from internet using jsoap
WordInfoRepository
interface WordInfoRepository {
fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>>
}
this is the interface that I reference wordInforepositoryImpl in the data layer in my interface domain layer
GetWordsAndMeaningsOnTheInternetUseCase
class GetWordsAndMeaningsOnTheInternetUseCase#Inject constructor(
private val repository: WordInfoRepository
){
operator fun invoke() : Flow<Resource<MutableList<ExtractedData>>> {
return repository.getEventsList()
}
}
GetWordsAndMeaningsOnTheInternetUseCase is my usecase in my domain layer
ViewModel
#HiltViewModel
class MostUsedWordScreenViewModel #Inject constructor(
private val getWordsAndMeaningsOnTheInternetUseCase: GetWordsAndMeaningsOnTheInternetUseCase
) : ViewModel() {
private var searchJob: Job? = null
private val _state = mutableStateOf(MostUsedWordState())
val state: State<MostUsedWordState> = _state
init {
fetchData()
}
private fun fetchData() {
searchJob?.cancel()
searchJob = viewModelScope.launch(IO) {
getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}
}
}
}
MostUsedWordScreen
#Composable
fun MostUsedWordScreen(viewModel: MostUsedWordScreenViewModel = hiltViewModel()) {
val state = viewModel.state.value
println("state --- >>> "+state.mostWordUsedItems)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.mostWordUsedItems.size) { i ->
val wordInfo = state.mostWordUsedItems[i]
if(i > 0) {
Spacer(modifier = Modifier.height(8.dp))
}
MostUsedWordItem(word = wordInfo)
if(i < state.mostWordUsedItems.size - 1) {
Divider()
}
}
}
}
#Composable
fun MostUsedWordItem(word : ExtractedData ) {
// println("this is MostUsedWordItem")
Column(modifier = Modifier
.padding(5.dp)
.fillMaxWidth()) {
Text(text = word.word,
modifier = Modifier.padding(3.dp),
textAlign = TextAlign.Center,
fontSize = 18.sp,
)
}
}
It is included in the MostUsedWordScreenViewModel and MostUsedWordScreen presententation layer
Where I println("state --- >>> "+state.mostWordUsedItems) in MostUsedWordScreen, the state console shows as empty like this System.out: state --- >>> []
I tried to explain as detailed as I can, I hope you can understand.
A Flow doesn't do anything until you call a terminal operator on it. You called onEach, which is not a terminal operator. You should use collect. Or you can avoid the nesting inside a launch block by using onEach and launchIn, which does the same thing as launching a coroutine and calling collect() on the flow. You don't need to specify Dispatchers.IO here because nothing in your Flow is blocking. You correctly wrapped the blocking call in withContext(Dispatchers.IO), and the warning is a false positive. That's a well-known bug in their compiler inspection.
searchJob = getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}.launchIn(viewModelScope)
By the way, you need to move your emit(Success...) inside your try block. The way it is now, when there is an error, the error will immediately get replaced by a Success with empty list.
Side note, I recommend avoiding passing MutableLists around between classes. You have no need for them and it's a code smell. Sharing mutable state between classes is error-prone. I don't think there is any justification for using a Flow<MutableList> instead of a Flow<List>.
You rarely even need a MutableList locally in a function. For example, you could have done in your try block:
val listData = List(eventsSize - 1) {
val row = rows[it + 1]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
ExtractedData(i,word,meaning)
}
emit(Resource.Success(listData))

Loading state adapter is always loading

My task is to load recyclerView and show pagination which I both implemented.
I also implemented LoadingState for adapter.
ApiCall:
#GET("top-headlines?sources=bbc-news,techcrunch&apiKey=${BuildConfig.API_KEY}")
suspend fun getTopHeadlinesArticles(
#Query("page") page:Int = 1,
#Query("q") query: String,
) : Response<ArticleListResponse>
I wont show paging because it is working so I will jump to repository:
fun getSearchResult(query: String) =
Pager(
config = PagingConfig(
pageSize = 1,
maxSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = { ArticlePaging(newsService, query)}
).flow
ViewModel:
#OptIn(ExperimentalCoroutinesApi::class)
val news = _currentQuery.flatMapLatest { query ->
articleRepository.getSearchResult(query).cachedIn(viewModelScope)
}
Fragment:
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
lifecycleScope.launchWhenCreated {
viewModel.news.collect { articles ->
adapter.submitData(articles)
}
}
And LoadStateViewHolder in LoadStateAdapter:
init {
binding.buttonRetry.setOnClickListener {
retry.invoke()
}
}
fun bind(loadState: LoadState) {
binding.apply {
progressBar.isVisible = loadState is LoadState.Loading
buttonRetry.isVisible = loadState !is LoadState.Loading
textViewError.isVisible = loadState !is LoadState.Loading
}
}
I already predefined DEFAULT_QUERY so I only get 1 Article.
Problem is that progressBar loading is always visible and I have no more articles to show.
Edit: when i have at least 10 items to show this works fine
Edit 2nd: Add ArticlePagingSource if that can help
override fun getRefreshKey(state: PagingState<Int, ArticleResponse>): Int? {
return state.anchorPosition?.let {
val anchorPage = state.closestPageToPosition(it)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
try {
val currentPageList = params.key ?: 1
response = if(query != ""){
newsService.getTopHeadlinesArticles(currentPageList, query)
} else{
newsService.getTopHeadlinesArticles(currentPageList)
}
val responseList = mutableListOf<ArticleResponse>()
val data = response.body()?.articleResponses ?: emptyList()
responseList.addAll(data)
val prevKey = if (currentPageList == 1) null else currentPageList - 1
return LoadResult.Page(
responseList,
prevKey,
currentPageList.plus(1)
)

Can I wrap collectAsState with remember when I use Jetpack Compose?

I hope to share a parameter val isCanAddRecord by mViewMode.isCanAddRecord.collectAsState() among #Composable functions.
The Code A is based the article How can I share info among #Composable function in Android Studio?
I know collectAsState() is wrapped with remember, you can see the Source Code.
Now you will find the object watchState is wrapped with remember, and watchState.isCanAddRecord which is assiged to mViewMode.isCanAddRecord.collectAsState() is wrapped with remember again.
Will the Code A cause error?
Code A
#Composable
fun ScreenHome_Watch(
modifier: Modifier = Modifier,
mViewMode: SoundViewModel,
watchState:WatchState = rememberWatchState(mViewMode)
){
...
}
class WatchState(
val isCanAddRecord: State<Boolean>,
...
){
...
}
#Composable
fun rememberWatchState(mViewMode: SoundViewModel): WatchState {
val watchState = WatchState(mViewMode.isCanAddRecord.collectAsState())
return remember {
watchState
}
}
Source Code
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
Of course you can remember your mutableState or any other remember or anything that is not Composable. You can remember measurePolicy or even another code block as lambda for drawing like Modifier.drawWithCache does. Jetnews App sample does what you about. This is a matter of preference, you can store anything that is not Composable inside your remember block.
/**
* Remembers the content for each tab on the Interests screen
* gathering application data from [InterestsViewModel]
*/
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
// UiState of the InterestsScreen
val uiState by interestsViewModel.uiState.collectAsState()
// Describe the screen sections here since each section needs 2 states and 1 event.
// Pass them to the stateless InterestsScreen using a tabContent.
val topicsSection = TabContent(Sections.Topics) {
val selectedTopics by interestsViewModel.selectedTopics.collectAsState()
TabWithSections(
sections = uiState.topics,
selectedTopics = selectedTopics,
onTopicSelect = { interestsViewModel.toggleTopicSelection(it) }
)
}
val peopleSection = TabContent(Sections.People) {
val selectedPeople by interestsViewModel.selectedPeople.collectAsState()
TabWithTopics(
topics = uiState.people,
selectedTopics = selectedPeople,
onTopicSelect = { interestsViewModel.togglePersonSelected(it) }
)
}
val publicationSection = TabContent(Sections.Publications) {
val selectedPublications by interestsViewModel.selectedPublications.collectAsState()
TabWithTopics(
topics = uiState.publications,
selectedTopics = selectedPublications,
onTopicSelect = { interestsViewModel.togglePublicationSelected(it) }
)
}
return listOf(topicsSection, peopleSection, publicationSection)
}
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
Remember lambda
fun Modifier.drawWithCache(
onBuildDrawCache: CacheDrawScope.() -> DrawResult
) = composed(
inspectorInfo = debugInspectorInfo {
name = "drawWithCache"
properties["onBuildDrawCache"] = onBuildDrawCache
}
) {
val cacheDrawScope = remember { CacheDrawScope() }
this.then(DrawContentCacheModifier(cacheDrawScope, onBuildDrawCache))
}
Remember layout policy which is widely used with layouts
#Composable
#UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
#Composable #UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}

Can I use mutableStateOf() instead of produceState in Compose?

The Code A is from the official sample project.
I don't think the produceState is necessary, so I think I can replace Code A with Code B, is it right?
BTW, the Code B can run.
Code A
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
...
}
else -> { onErrorLoading() }
}
}
Code B
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
var uiState by remember {mutableStateOf(DetailsUiState(isLoading = true))}
val cityDetailsResult = viewModel.cityDetails
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
...
}
else -> { onErrorLoading() }
}
}
Let's take a look at productState under the hood:
#Composable
fun <T> produceState(
initialValue: T,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
productState without a key in it's arguments, uses LaunchedEffect with Unit key which Create an effect that matches the lifecycle of the call site.
It means if DetailsScreen recomposes, the code that provides uiState won't start again.
But In code B, you are just remembering DetailsUiState across recomposition, and below line will be executed in every recomposition.
val cityDetailsResult = viewModel.cityDetails
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}

Jetpack Compose - UI not updating when flow is changed

I have the following Composable view and view model. The problem I'm having is that even though isLoading is being updated in the view model, it's not updating the view at all, it's stuck on the LoadingUi.
#Composable
fun MediaDetailsPage(
mediaId: Long?,
viewModel: DetailsViewModel
) {
LaunchedEffect(Unit, block = {
viewModel.fetchDetailsOf(mediaId!!)
})
val isLoading by viewModel.isLoading.collectAsState()
val isError by viewModel.error.collectAsState()
when {
isLoading -> LoadingUi()
isError || viewModel.model == null -> ErrorUi()
else -> MediaDetailsUi(viewModel.model!!)
}
}
class MovieDetailsViewModel(
private val useCase: MovieDetailsUseCase = config.movieDetailsUseCase
) : DetailsViewModel() {
override fun fetchDetailsOf(id: Long, dispatcher: CoroutineDispatcher){
if(model != null) return
fetchFrom({ useCase.getMovieDetailsOf(id) }, dispatcher)
}
}
abstract class DetailsViewModel : ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading
private val _error = MutableStateFlow(false)
val error: StateFlow<Boolean> = _error
var model: MediaDetailsModel? = null
abstract fun fetchDetailsOf(id: Long, dispatcher: CoroutineDispatcher = Dispatchers.IO)
protected fun fetchFrom(
useCaseCall: suspend () -> Either<MediaDetails?, ErrorEntity?>,
dispatcher: CoroutineDispatcher
) {
job( {
_isLoading.value = true
val result = useCaseCall.invoke()
if (result.isSuccess) model = result.body!!.convert()
else _error.value = true
_isLoading.value = false
}, dispatcher)
}
}
The most confusing thing is that I'm doing pretty much identically this elsewhere in the app and that is working perfectly. I was under the impression that a composable view refreshes when a flow variable is updated, or have I got the wrong end of the stick here?