Loading picture to correct position in recycler view - android

I make a network call in my recycler adapter to retrieve the url for a picture. After the url is received, I use universal image loader to load the picture into an image view. The problem is when I don't scroll the pictures are loaded into the right place but as soon as I scroll the pictures are inflated in the wrong place.
Here's my adapter:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val article = feeds[position]
holder.articleTitle.setFont("SourceSansPro-SemiBold.ttf")
holder.articleDescription.setFont("OpenSans-Regular.ttf")
holder.articleTime.setFont("OpenSans-Light.ttf")
mAnimator?.onBindViewHolder(holder.itemView, position)
holder.apply {
article.apply {
articleTitle.text = title
articleDescription.text = Html.fromHtml(description)
articleTime.text = TimeUtils.convertLongToTime(pubDate)
if (image.isBlank()){
//load picture url when it's empty
mContext?.doAsync {
ImageExtractor.extractImageUrl(link, object : OnImageExtractorListener {
override fun onSuccess(url: String) {
v("imaaaage success $title $url")
mContext?.runOnUiThread {
article.image = url
//use uil to load the image didn't work so I tried just updating the model
//articleImage.displayImage(url)
feeds[position] = article
notifyItemChanged(position)
}
val dbo = context.getDatabase()
dbo.updateArticleImage(dbo,url,article.id)
}
override fun onError() {
}
})
}
}else{
articleImage.displayImage(image)
isRead?.let {
if (isRead!! && !isSaved){
grayScale(holder)
}
}
}
container.setOnClickListener {
itemClick(this)
if (!isSaved){
article.isRead = true
feeds[position] = article
notifyItemChanged(position)
}
}
}
}
}else if (holder is LoadingViewHolder){
holder.progressBar.isIndeterminate = true
}
}
I need a way to load the images in their right places if the user is scrolling or not.

Consider using a library for async image loading, e.g. Picasso. There everything is handled for you, like caching, placeholder ...
In your adapter:
Picasso.with(context).load("url")
.placeholder(R.drawable.user_placeholder).into(imageView);
Gradle:
compile 'com.squareup.picasso:picasso:2.5.2'
That's all!

you nedd to put setHasStableIds(true); in your Adapter's constructor and put :
#Override
public int getItemViewType(int position) {
return position;
}
#Override
public long getItemId(int position) {
return position;
}
So it will keep all images at exact positions even after scroll.

The RecyclerView will reuse ViewHolders to reduce inflation and memory usage.
The correct way is in the onBindViewHolder is to clear the old state of the View (seting image as null) and seting the new ones.
Make sure to clear it all times it come onBind (before testing)
articleImage.displayImage(null)
if (image.isBlank()){
Since downloading is a async task it will not be at the way when binding, that's why you just clear the old one, for texts it is immediately available to be set.

Related

Combine data from Kotlin Flow/LiveData

I've got a flow from my repository that looks something like this:
val userListFlow: Flow<List<User>> = channelFlow<List<User>> {
source.setOnUserUpdatedListener { userList ->
trySend(userList)
}
awaitClose {
logger.info("waitClose")
source.setOnUserUpdatedListener(null)
}
}.stateIn(
scope = externalScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
suspend fun getUserThumbnail(user: User): File {
return getUserThumbFromCache(user) ?: run {
fetchUserThumbnailRemote()
}
}
private suspend fetchUserThumbnailRemote(user: User): Bitmap {
thumbnailService.getUserThumbnailBitmap(user.id)
}
fun getUserThumbFromCache(user: User) {
val thumbFile = getThumbFile(user)
return if (thumbFile.exists() && thumbFile.size() > 0) {
thumbFile
} else null
}
private fun getThumbFile(user: User): File {
return File(cacheDir, "${user.id}.jpg")
}
}
For each of these users I can call the suspend function to get a thumbnail for the user.
I don't want to wait for the thumbnail before showing the list of users though, I'd rather it show the users and then when the thumbnail is fetched, update the list.
However I'd like the list to be updated when a thumbnail is fetched..
From my ViewModel I have something like
data class UserWithThumb(user: User, thumb: File?)
val userLiveData = repo.userListFlow.map {
UserWithThumb(it, repo.getUserThumbFromCache(it))
}.asLiveData()
So then from my Fragment I do
viewModel.userLiveData.observe(viewLifecycleOwner) {
userListAdapter.submitList(it)
}
My thumbnails are all null though as I need to fetch them from remote. However if I call that function then that will delay my list from getting to the UI until the thumbnail is fetched. How can I get the thumbnail to the UI in a clean way? I realize that I need to have my livedata or flow update itself once the thumbnail is fetched but I have no idea how to hook that into my code. Any ideas would be appreciated.
I suppose one way to think about this is I'd like my upstream (repository) flow to contain the list of users but then I'd like to update the list given to the view not just when the upstream (repo) flow gets new data but when new thumbnails are downloaded as well..
What I understood from the question is, you have a list of UserWithThumb that is created once you set Users list and you want to show it to the UI immediately. In the background you want to fetch User thumbnails and once you receive them, you want to update the list again.
One way to achieve what you want is:
val userLiveData = flow {
repo.userListFlow.collect { users ->
val initialList = users.map { UserWithThumb(it, repo. getUserThumbFromCache(it)) }
emit(initialList)
coroutineScope {
val finalList = users.map {
async(Dispatchers.IO) { // fetch all thumbnails in parallel
UserWithThumb(it, repo. getUserThumbnail(it))
}
}.awaitAll() // wait until all thumbnails have been fetched
emit(finalList)
}
}
}.asLiveData()

Picasso Fetching Images in Activity, then show them into Fragment's ImageView

I am trying to fetch multiple images in an Activity with FragmentStateAdapter in it.
Then I need to show them into some Fragment's ImageView.
I want to preload them into the cache ( after I receive an API response with the info about images like imageID's)
Do I need to use do something else after .fetch() in Activity and do I need to create again same URL request and then load it into the right image view?
Currently, I am seeing images normally but I guess that they are not preloaded in Activity and I fetch them in the Fragment. I am not sure, how to check it?
Thank you for your help in advance!
class FavouriteActivity: - here I want to preload them:
#Subscribe
fun onCoolingImageInfoEvent(coolingEvent: FreezerImageEvent) {
viewModel.retrieveCoolingImage(coolingEvent.data)
val applianceID = viewModel.haID
viewModel.shownCoolingImages.value?.forEach {
picasso.load("https:/.../api/$applianceID/images/${it.imagekey}")
?.fetch() // does I need to do something else?
}
This is my adapter in which I have some fragments (for each image different one)
class FavouriteAdapter(
activity: BaseWearActivity,
private val viewModel: FavouriteViewModel
) : FragmentStateAdapter(activity) {
enum class FavouriteFragmentsEnum(
val position: Int,
val fragment: Fragment
) {
FAVOURITES(0, FavouritesFragment()),
COOLING_IMAGE(1, CoolingImageFragment(imageIndex = 0)),
COOLING_IMAGE_2(2, CoolingImageFragment(imageIndex = 1)),
//(...)
and Fragment code with images where I need to load already fetched images with Picaso
private fun initPicassoImage(coolingImageData: List<CoolingImageData>) {
applianceID = viewModel.haID
imageID = viewModel.getImageIDByIndex(imageIndex)
picasso.load("https:/.../api/$applianceID/images/$imageID")
?.into(current_image, object : Callback {
// (...)
} // does I need to do something else?
)
}
Your code seemed to me ok.
However, you are fetching images from your server. Picasso may invalidate the cache for that URL whenever your server change http headers such as etag, document size, etc in future for that URL.
You may use picasso.setIndicatorsEnabled(true) to see if an image loaded from cache. This adds a little indicator at top left of your image.
Color
Picasso loaded from
Red
Network
Green
Memory
Blue
Disk
I did it like that:
In Activity:
#Subscribe
fun onCoolingImageInfoEvent(coolingEvent: FreezerImageEvent?) {
(...)
viewModel.shownCoolingImages.forEach {
picasso
.load("https://(...)/$applianceID/images/${
viewModel.getImageIDByIndex(index)
}")
.priority(Picasso.Priority.HIGH)
?.fetch()
}
(...)
}
And in fragment:
private fun initPicassoImage() {
(...)
val imageUrl = "https://(...)/$applianceID/images/$imageID"
picasso
.load(imageUrl)
.networkPolicy(NetworkPolicy.OFFLINE)
.into(current_image, object : Callback {
override fun onSuccess() {
Log.d("PICASSO", "success load image from memory")
}
override fun onError(e: Exception?) {
//Try again online if cache failed
picasso
.load(imageUrl)
.into(current_image, object : Callback {
override fun onSuccess() {
Log.d("PICASSO", "load image from network")
}
override fun onError(e: Exception?) {
Log.e("Picasso", "Could not fetch image")
}
});
}
})
}
Take a Note: as #ocos said, you can check if it loads Image from Memory/Network.
However, you are fetching images from your server. Picasso may
invalidate the cache for that URL whenever your server change http headers such as etag, document size, etc in future for that URL.
You may use picasso.setIndicatorsEnabled(true) to see if an image
loaded from cache. This adds a little indicator at top left of your
image.
Color
Picasso loaded from
Red
Network
Green
Memory
Blue
Disk

Paging 3 - How to scroll to top of RecyclerView after PagingDataAdapter has finished refreshing AND DiffUtil has finished diffing?

I'm using Paging 3 with RemoteMediator that shows cached data while fetching new data from the network.
When I refresh my PagingDataAdapter (by calling refresh() on it) I want my RecyclerView to scroll to the top after the refresh is done. In the codelabs they try to handle this via the loadStateFlow the following way:
lifecycleScope.launch {
adapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
.distinctUntilChangedBy { it.refresh }
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter { it.refresh is LoadState.NotLoading }
.collect { binding.list.scrollToPosition(0) }
}
This indeed does scroll up, but before DiffUtil has finished. This means that if there is actually new data inserted at the top, the RecyclerView will not scroll all the way up.
I know that RecyclerView adapters have an AdapterDataObserver callback where we can get notified when DiffUtil has finished diffing. But this will cause all kinds of race conditions with PREPEND and APPEND loading states of the adapter which also cause DiffUtil to run (but here we don't want to scroll to the top).
One solution that would work would be to pass PagingData.empty() to the PagingDataAdapter and rerun the same query (just calling refresh won't work because the PagingData is now empty and there is nothing to refresh) but I would prefer to keep my old data visible until I know that refresh actually succeeded.
In cases, such as searching a static content, we can return false inside areItemsTheSame of DiffUtil.ItemCallback as a workaround. I use this also for changing sorting property.
#Florian I can confirm we don't need the postDelayed to scroll to top using version 3.1.0-alpha03 released on 21/07/2021.
Also, I managed to make further filter the loadStateFlow collection so it doesn't prevent StateRestorationPolicy.PREVENT_WHEN_EMPTY to work based on #Alexandr answer. My solution is:
By the time I am writing this, the latest version of Paging3 is 3.1.0-alpha03 so import:
androidx.paging:paging-runtime-ktx:3.1.0-alpha03
Then set the restoration policy of your adapter as following:
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
If you have compilation error for the above mentioned change, make sure you are using at least version 1.2.0-alpha02 of RecyclerView. Any version above that is also good:
androidx.recyclerview:recyclerview:1.2.0-alpha02
Then use the filtered loadStateFlow to scroll the list to top only when you refresh the page and items are prepended in the list:
viewLifecycleOwner.lifecycleScope.launch {
challengesAdapter.loadStateFlow
.distinctUntilChanged { old, new ->
old.mediator?.prepend?.endOfPaginationReached.isTrue() ==
new.mediator?.prepend?.endOfPaginationReached.isTrue() }
.filter { it.refresh is LoadState.NotLoading && it.prepend.endOfPaginationReached && !it.append.endOfPaginationReached}
.collect {
mBinding.fragmentChallengesByLocationList.scrollToPosition(0)
}
}
The GitHub discussion can be found here: https://github.com/googlecodelabs/android-paging/issues/149
adapter.refresh()
lifecycleScope.launch {
adapter.loadStateFlow
.collect {
binding.recycleView.smoothScrollToPosition(0)
}
}
Take a look on the code the condition if loadtype is refreshed.
repoDatabase.withTransaction {
// clear all tables in the database
if (loadType == LoadType.REFRESH) {
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
}
val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = repos.map {
Log.e("RemoteKeys", "repoId: ${it.id} prevKey: $prevKey nextKey: $nextKey")
RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)
}
repoDatabase.remoteKeysDao().insertAll(keys)
repoDatabase.reposDao().insertAll(repos)
}
You should delete the condition if LoadType is refresh clear all the tables.
if (loadType == LoadType.REFRESH) {
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
}
I've managed the way to improve base code snippet from the topic question.
The key is to listen non-combined variants of properties inside CombinedLoadStates
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
adapter?.loadStateFlow
?.distinctUntilChanged { old, new ->
old.mediator?.prepend?.endOfPaginationReached.isTrue() ==
new.mediator?.prepend?.endOfPaginationReached.isTrue()
}
?.filter { it.refresh is LoadState.NotLoading }
..
// next flow pipeline operators
}
where isTrue is Boolean extension fun
fun Boolean?.isTrue() = this != null && this
So the idea here is to track mediator.prepend:endOfPagination flag states. When mediator has completed his part of paging load for the current page, his prepend state will not change (in case if you are loading pages after scroll down).
Solution works well both in offline & online modes.
If you need to track prepend paging or paging in both directions it is a good starting point to play around with the another CombinedLoadStates properties append,refresh,mediator and source
Follow https://developer.android.com/reference/kotlin/androidx/paging/PagingDataAdapter
val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
// User ID serves as unique ID
oldItem.userId == newItem.userId
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
// Compare full contents (note: Java users should call .equals())
oldItem == newItem
}
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder.create(parent)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val repoItem = getItem(position)
// Note that item may be null, ViewHolder must support binding null item as placeholder
holder.bind(repoItem)
}
}

Create coroutine inside onBindViewHolder creates a mess

I am developing a chat application and there is a specific API so some things i must implement them with a specific way. For example (and the case that i have a problem...)
When i have to display an Image the API says that i have to split the Image in small chunks and store them as a message with a byteArray content. There is also a header message that its body is the messageIds of the fileChunks. So in the RecyclerView inside the onBindViewHolder, when i see a header file message (msgType == 1) then i start a coroutine to fetch the chunkFile messages by the ids, construct the File and then switch to the MainDispatcher, and so the Image with Glide using a BitmapFactory.decodeByteArray. The code is shown below
messageItem.message?.msgType == MSG_TYPE_FILE -> {
holder.sntBody.text = "Loading file"
val fileInfo = Gson().fromJson(URLDecoder.decode(messageItem.message?.body, "UTF-8"), FileInformation::class.java)
job = chatRoomAdapterScope.launch(Dispatchers.IO) {
// i get the messageIds of the chunks from Header message
val segSequence = fileInfo.seg.split(",").map { it.toLong() }
// i get the fileChunks from Database
val fileChunks = AppDatabase.invoke(mContext).messageDao().getMessageById(segSequence)
val compactFile = ByteArrayOutputStream()
// Reconstruct the file
for (chunk in fileChunks)
compactFile.write(Base64.decode(chunk.fileBody, Base64.DEFAULT))
withContext(Dispatchers.Main) {
val bitmapOptions = BitmapFactory.Options().apply {
inSampleSize = 8
}
Glide.with(mContext).asBitmap()
.load(BitmapFactory.decodeByteArray(compactFile.toByteArray(), 0, compactFile.size(), bitmapOptions)!!)
.fitCenter()
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
holder.sntImageView.setImageBitmap(resource)
holder.sntImageView.visibility = View.VISIBLE
}
})
holder.sntBody.text = fileInfo.filename
}
}
}
My problem is that when i scroll fast the image that is supposed to be loaded in an item appears in another item. My first guess is that the Coroutine that started from a specific item didnt complete as soon as the item was recycled so when the coroutine finished it had a reference to a new item, so i added the
holder.itemView.addOnAttachStateChangeListener method as some people commented. However i didn't work.
Is there any idea of why that may happens and if there is a better implementation of the proccess according to the specific API...?
You can cancel the coroutine in override fun onViewRecycled(holder: EventViewHolder).

viewPager.setCurrentItem() is not working

setCurrentItem() function isnt working for following android(java) code how to make it work ?
Intent i = getIntent();
int position = i.getIntExtra("img", 0);
viewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
ImageAdapter adapter = new ImageAdapter(this);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(position);
Frank is right. To provide the the scroll device independent, you should use onGlobalLayoutListener. Thereby, you can guarantee that the view is rendered when you call viewPager.setCurrentItem(position). Otherwise setCurrentItem won't have any effect since the view is not rendered yet.
viewPager.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
viewPager.setCurrentItem(position, false);
}
});
I know the question is already old but maybe someone else will find this usefull!
private ViewPager viewPager;
#Override
public void onResume() {
Intent i = getIntent();
int position = i.getIntExtra("img", 0);
viewPager.postDelayed(new Runnable() {
#Override
public void run() {
viewPager.setCurrentItem(position);
}
}, 100);
}
just adding a shorter answer with androidx using view extension in kotlin
view_pager.doOnLayout {
view_pager.setCurrentItem(position, false)
}
note that the previos answer using global layout is not removing the OnGlobalLayoutListener afterwords which is not advisable.
androidx ext doOnLayout does that.
Please, make sure you have a gradle dependency:
implementation 'androidx.core:core-ktx:1.3.2'
None of these answers worked for me - it took hours for me to find out that I'd forgotten to update getCount().
My getCount() method in my implementation of the FragmentPagerAdapter was displaying the incorrect count, so it could not change page since it was not part of its count.
This worked with me
if (mViewPager.getAdapter() != null)
mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);
Ok, for me, I have been troubleshooting this issue like for 3 hours stright and I got to a point that I made it work this way
Before, I had this inside my ViewPagerStateAdapter
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
1 -> myFragmentInstance1()
else -> myFragmentInstance2()
}
}
Then for changing the pages I was using the following
private fun navigateTo1() {
binding.viewPager.setCurrentItem(1, false)
}
private fun navigateTo2() {
binding.viewPager.setCurrentItem(2, false)
}
First of all after initializing my viewpager adapter like this
private fun setupViewPager() {
binding.viewPager.apply {
adapter = MyViewPagerStateAdapter(this#MainFragment)
}
}
the initial position inside createFragment was 0 , so the else block was always called starting my viewpager with the inscance of myFragmentInstance2()
This is not all, after clicking and trying to setCurrentItem to another position the viewpager was changing only once and then it did not work anymore to switch pages
After long times of debug and research I tried to change the positions of the fragments like this
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> myFragmentInstance1()
else -> myFragmentInstance2()
}
}
which in first place worked for loading 0 as the default destination, and then switching with currentItem worked as expected
private fun navigateTo1() {
binding.viewPager.setCurrentItem(0, false)
}
private fun navigateTo2() {
binding.viewPager.setCurrentItem(1, false)
}
I also tried with
private fun navigateTo1() {
binding.viewPager.currentItem = 0
}
private fun navigateTo2() {
binding.viewPager.currentItem = 1
}
which puts a smoothTransition to true as default and is working fine.
So my conclusion is that the createFragment initializes our different fragments in a position-based way, starting from 0 and if we start creating our fragments at position 1 it will mess the order and then we will end up having just 1 fragment in the stack to switch.
I did lots of research on the issue but did not found anything related to this issue because if we use TabLayoutMediator this issue is not happening, it only happens when we try to switch the viewPager programatically with currentItem

Categories

Resources