LazyColumn doing infinite number of recompositions (Jetpack Compose) - kotlin

I want to display a list of items using LazyColumn. I wrote the needed code but when I ran the app I observed that the UI is very laggy. I put some Logs to find out where the problem may be and I discovered that the LazyColumn inifinitely recomposes the items. I don't know why LazyColumn behaves like it did
list composable:
fun listOfReceivers(
receivers: List<ReceivedPingItem>,
) {
if (receivers.isNotEmpty()) {
val listState = rememberLazyListState()
val firstVisibleIndex = listState.firstVisibleItemIndex
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastIndex + listState.firstVisibleItemIndex
if (listState.isScrolledToTheEnd()) {
Log.d("*****", "Scrolled to the End")
viewModel.onRecyclerViewScrolledToLast()
}
//LazyColumn -> equivalent of the RecyclerView
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(receivers) { receiverItem ->
Log.d("*****", "ADDED $receiverItem")
ReceiversListItem(
receiver = receiverItem,
modifier = Modifier.fillMaxWidth()
)
}
}
// AppViewState
if (firstVisibleIndex == 0) {
appViewState.viewState = ViewState.ReceivedPingsViewState(firstVisibleIndex)
} else {
appViewState.viewState = ViewState.DefaultViewState
}
// Seen status check
if (firstVisibleIndex != -1 && lastVisibleItemIndex != -1) {
viewModel.onRecyclerViewScrolledSetViewed(
receivers.subList(
firstVisibleIndex,
lastVisibleItemIndex
)
)
}
}
else{
EmptyPingsScreen()
}
}

Due to this video - youtu.be/EOQB8PTLkpY?t=284 , i did smthn like this, and it works perfectly:
#Composable
fun ListOfReceivers(
receivers: List<ReceivedPingItem>,
) {
if (receivers.isNotEmpty()) {
val listState = rememberLazyListState()
val derivedListState by remember { derivedStateOf { listState.isScrolledToTheEnd() } }
if (derivedListState) {
viewModel.onRecyclerViewScrolledToLast()
}
// LazyColumn -> equivalent of the RecyclerView
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(receivers) { receiverItem ->
Log.d("*****", "ADDED $receiverItem")
ReceiversListItem(
receiver = receiverItem,
modifier = Modifier.fillMaxWidth()
)
}
}
// derivedStateOf - prevents infinite recomposition
val visibleItemsCount by remember { derivedStateOf { listState.layoutInfo.visibleItemsInfo.size } }
val firstVisibleItemIndex = listState.firstVisibleItemIndex
val lastVisibleItemIndex = firstVisibleItemIndex + visibleItemsCount - 1
// AppViewState
if (firstVisibleItemIndex == 0) {
appViewState.viewState = ViewState.ReceivedPingsViewState(firstVisibleItemIndex)
} else {
appViewState.viewState = ViewState.DefaultViewState
}

Related

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

Wait for result from Coroutine and then use it in Composable function

I am creating a video scraper, and it has the following function which scrapes the video source from a URL that has been given as the parameter:
fun scrapeVideoSrcFromUrl(url: String): String? {
val document = Jsoup.connect(url).get()
for (element in document.getElementsByTag("script")) {
if (element.attr("type") == "application/ld+json") {
val content = element.data()
val array = JsonParser.parseString(content).asJsonArray
val embedUrl = Gson().fromJson(array.get(0).asJsonObject.get("embedUrl"), String::class.java)
var embedId = ""
for (char in embedUrl.dropLast(1).reversed()) {
if (char != '/') {
embedId += char
} else {
break
}
}
val doc = Jsoup.connect("$RUMBLE_API_URL${embedId.reversed()}").ignoreContentType(true).get()
val jsonData = doc.getElementsByTag("body").first()?.text()
val mp4 = JsonParser.parseString(jsonData).asJsonObject.get("u").asJsonObject.get("mp4").asJsonObject.get("url").toString()
return mp4.replace("\"", "")
}
}
return null
}
I want to show this in a dialog for a certain link using ExoPlayer, so I did the following:
#Composable
fun VideoPlayer(videoSrc: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(
videoSrc
)
)
prepare()
playWhenReady = true
}
}
Box(modifier = Modifier.fillMaxSize()) {
DisposableEffect(key1 = Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
}
Then, in the main Composable:
if (openDialog) {
AlertDialog(
onDismissRequest = {
openDialog = false
},
title = {
Column {
Text(
text = viewModel.currentRumbleSearchResult?.title ?: ""
)
Spacer(
Modifier.height(8.dp)
)
Text(
text = "By ${viewModel.currentRumbleSearchResult?.channel?.name ?: ""}",
style = MaterialTheme.typography.titleSmall
)
}
},
text = {
VideoPlayer(RumbleScraper.create().scrapeVideoSrcFromUrl("https://rumble.com/v1m9oki-our-first-automatic-afk-farms-locals-minecraft-server-smp-ep3-live-stream.html")!!)
},
confirmButton = {
TextButton(
onClick = {
openDialog = false
}
) {
Text("Exit")
}
}
)
}
After running that code I keep getting NetworkOnMainThread exceptions, and I tried many things to fix it but nothing worked.
So I am unsure what to do as to how I can go around fixing this. I was wondering how I would go around waiting in the background for a result and then show it in the Compose function when it returns the value?
You can do something like this:
var videoSrc by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
videoSrc = RumbleScraper.create().scrapeVideoSrcFromUrl("")
}
}
text = { VideoPlayer(videoSrc) }
You can also call the scrapeVideoSrcFromUrl inside your viewModel and update some state that you will use in UI.
If you want to run it in response to some event like item click, you will be better of with something like this:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
withContext(Dispatchers.IO) { ... }
}
}
)

Infinite looping row of images

How can I create a scrolling row that scrolls automatically at a fixed speed that loops around the content of a list of images?
I have a lazy row of images as defined below, but haven't found a good way to loop it (like a carousel).
var images: List<String> = listOf()
repeat(8) {
images = images.plus("https://place-puppy.com/300x300")
}
val state = rememberLazyListState()
LazyRow(
modifier = modifier.fillMaxWidth(),
state = state
) {
items(count = images.size) { i ->
val image = images.get(i)
Column(
modifier = Modifier
.width(40.dp)
.aspectRatio(1f)
) {
Image(
painter = rememberImagePainter(image),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
}
}
}
firstly, create an infinite auto-scrolling effect that will be running as long as the composable is active & displayed:
LazyRow() {
....
}
LaunchedEffect(Unit) {
autoScroll(lazyListState)
}
private tailrec suspend fun autoScroll(lazyListState: LazyListState) {
lazyListState.scroll(MutatePriority.PreventUserInput) {
scrollBy(SCROLL_DX)
}
delay(DELAY_BETWEEN_SCROLL_MS)
autoScroll(lazyListState)
}
private const val DELAY_BETWEEN_SCROLL_MS = 8L
private const val SCROLL_DX = 1f
Secondly, update positions of items in the list accordingly:
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
modifier = modifier,
) {
items(images) {
...
if (it == itemsListState.last()) {
val currentList = images
val secondPart = currentList.subList(0, lazyListState.firstVisibleItemIndex)
val firstPart = currentList.subList(lazyListState.firstVisibleItemIndex, currentList.size)
rememberCoroutineScope().launch {
lazyListState.scrollToItem(0, maxOf(0, lazyListState.firstVisibleItemScrollOffset - SCROLL_DX.toInt()))
}
images = firstPart + secondPart
}
}
}
That should give you the looping behavior.
Credits: https://proandroiddev.com/infinite-auto-scrolling-lists-with-recyclerview-lazylists-in-compose-1c3b44448c8

Used an authlistener firebase but still does not work to delete user

I have followed this tutorial Tutorial by Alex mamo and got it finnished. When i now check if a user exist and i have a clean build of my emulator it works but if i run it on an already hot build of my emulator the firebase database still answers me with a user id why is this? this is my code...
AuthListener:
override fun getFirebaseAuthState() = callbackFlow<Boolean>{
val authStateListener = FirebaseAuth.AuthStateListener { auth ->
println("Current user:" + auth.currentUser)
trySend(auth.currentUser == null)
}
auth.checkIfUserExists()?.addAuthStateListener(authStateListener)
awaitClose {
auth.checkIfUserExists()?.removeAuthStateListener(authStateListener)
}
}
ViewModel:
#HiltViewModel
class SplashViewModel #Inject constructor(private val repository: IAuthRepository) : ViewModel() {
private val _event : MutableStateFlow<AuthEvent> = MutableStateFlow(AuthEvent.notBeingUsed)
val event : StateFlow<AuthEvent> = _event
fun checkIfUserExists() = viewModelScope.launch {
repository.getFirebaseAuthState().collectLatest{task ->
println("Task2:" + task)
if(task){
_event.value = AuthEvent.Failure("User is not logged in")
}else{
_event.value = AuthEvent.Success
}
}
}
}
UI:
#Composable
fun SplashScreen(navController: NavController, viewModel : SplashViewModel = hiltViewModel()){
val coroutineScope = rememberCoroutineScope()
Surface(modifier = Modifier.fillMaxSize()) {
val overshootInterpolator = remember {
OvershootInterpolator(2f)
}
val scale = remember {
Animatable(0f)
}
LaunchedEffect(key1 = true){
scale.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = 500,
easing = {
overshootInterpolator.getInterpolation(it)
}
)
)
delay(Constants.SPLASH_SCREEN_DURATION)
viewModel.checkIfUserExists()
viewModel.event.collect{
println("Task it: " + it)
when(it){
is AuthEvent.Success -> {
navController.popBackStack()
navController.navigate(PaperSellerScreens.CustomerListScreen.name)
}
is AuthEvent.Failure ->{
navController.popBackStack()
navController.navigate(PaperSellerScreens.LoginScreen.name)
}
}
}
}
val painterIcon = painterResource(R.drawable.logo_size_invert)
val painterBackground = painterResource(id = R.drawable.paper_seller_background)
Box(modifier = Modifier.fillMaxSize()){
Image(painter = painterBackground, contentDescription = "SplashScreen",contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
Image(painterIcon, "Icon Image", modifier = Modifier
.size(200.dp, 200.dp)
.scale(scale.value)
.align(
Alignment.Center
)
.clip(RoundedCornerShape(10.dp)))
}
}
}

Is there a way to make two way HorizontalPager with auto scroll?

The HorizontalPager from accompanist library does a job of creating a simple ViewPager; is there a way to swipe infinitely in both ends?
#ExperimentalPagerApi
#Composable
fun AutoScrollPagerHorizontal(d: List<Stem>?) {
var data: MutableList<Stem> = d?.toMutableList() ?: mutableListOf()
if (data.isNullOrEmpty()) return
val pageState = rememberPagerState(pageCount = data.size)
HorizontalPager(
state = pageState
) {
Card(
Modifier
.height(240.dp)
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
) {
Image(
painter = rememberGlidePainter(
request = data[it].icon,
),
contentDescription = data[it].title,
contentScale = ContentScale.FillBounds
)
}
}
}
This code generates the viewpager correctly, but does not scroll to 0th index after reaching the last index of data.
You can use this snippet in the composable to auto scroll the pager:
LaunchedEffect(key1 = pagerState.currentPage) {
launch {
delay(3000)
with(pagerState) {
val target = if (currentPage < pageCount - 1) currentPage + 1 else 0
animateScrollToPage(
page = target,
animationSpec = tween(
durationMillis = 500,
easing = FastOutSlowInEasing
)
)
}
}
}
PointerInput helped in building the bi-directional Pager; below is the snippet that worked for me.
Modifier.pointerInput(Unit) {
detectHorizontalDragGestures { change, dragAmount ->
change.consumeAllChanges()
when {
dragAmount < 0 -> {
coroutineScope.launch { /* right */
if (pageState.currentPage == data.lastIndex) {
pageState.animateScrollToPage(0)
} else {
pageState.animateScrollToPage(pageState.currentPage + 1)
}
}
}
dragAmount > 0 -> { /* left */
coroutineScope.launch {
if (pageState.currentPage == 0) {
pageState.animateScrollToPage(data.lastIndex)
} else {
pageState.animateScrollToPage(pageState.currentPage - 1)
}
}
}
}
}
}