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

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

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

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

Volley request inside recyclerview adapter

I am using Volley image request inside recyclerview adapter.
Request appears to work fine until a fast scroll is done, whenever I scroll the recyclerview fast up or down, the app crashes with following error :
java.lang.OutOfMemoryError: Could not allocate JNI Env: Failed anonymous mmap(0x0, 8192, 0x3, 0x22, -1, 0): Permission denied. See process maps in the log.
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:883)
at com.android.volley.RequestQueue.start(RequestQueue.java:134)
at com.android.volley.toolbox.Volley.newRequestQueue(Volley.java:91)
at com.android.volley.toolbox.Volley.newRequestQueue(Volley.java:67)
at com.android.volley.toolbox.Volley.newRequestQueue(Volley.java:102)
at com.squadtechs.hdwallpapercollection.main_activity.fragment.WallpaperAdapter.populateViews(WallpaperAdapter.kt:60)
at com.squadtechs.hdwallpapercollection.main_activity.fragment.WallpaperAdapter.onBindViewHolder(WallpaperAdapter.kt:38)
at com.squadtechs.hdwallpapercollection.main_activity.fragment.WallpaperAdapter.onBindViewHolder(WallpaperAdapter.kt:21)
Following is my onBindViewHolder() code:
override fun onBindViewHolder(holder: WallpaperHolder, position: Int) {
populateViews(holder, position)
}
private fun populateViews(holder: WallpaperHolder, position: Int) {
val requestQueue = Volley.newRequestQueue(context)
val imageRequest = ImageRequest(
list[position].wallpaper_image_url,
Response.Listener { response ->
holder.imgGrid.scaleType = ImageView.ScaleType.CENTER
holder.imgGrid.setImageBitmap(response)
holder.progress.visibility = View.GONE
},
1024,
860,
ImageView.ScaleType.CENTER,
null,
Response.ErrorListener { error ->
Toast.makeText(context, "Error loading Image", Toast.LENGTH_LONG).show()
holder.progress.visibility = View.GONE
}).setRetryPolicy(
DefaultRetryPolicy(
20000,
DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
)
)
requestQueue.add(imageRequest)
holder.txtCategory.visibility = View.GONE
}
According to log, error is thrown at line where request queue is declared, i.e val requestQueue = Volley.newRequestQueue(context)
Remember: the app works fine when scroled normally but crashes when scrolled fast
Your recycler view will fire your adapter's onBindViewHolder every time an element is supposed to be displayed that was not bound to a view before (or that was un-bound).
When you scroll fast, binds and unbinds will happen fast. Each bind generates an HTTP request, which is a relatively expensive IO operation that consumes memory.
This is a recipe for disaster. Do not send HTTP requests based on a regular user interaction like this. If someone keeps scrolling up and down, the app is guaranteed to run out of memory.
Instead, think of a better strategy. Possibly pre-load data asynchronously, or at least cache data once loaded.
#fjc pointed out correct the HTTP request is resource intensive. If you look at your populateViews function's first line
val requestQueue = Volley.newRequestQueue(context)
This is the main reason of OOM. You are creating multiple request queue for each image request which is therefore occupying all the resources thus result in OOM. In order to overcome that you need to use a single requestqueue for all of your application. it is also recommended by Google to use a Singleton class for handling the requestqueue.DOC
if your application makes constant use of the network, it's probably
most efficient to set up a single instance of RequestQueue that will
last the lifetime of your app. You can achieve this in various ways.
The recommended approach is to implement a singleton class that
encapsulates RequestQueue and other Volley functionality. Another
approach is to subclass Application and set up the RequestQueue in
Application.onCreate(). But this approach is discouraged; a static
singleton can provide the same functionality in a more modular way.
A quick way to solve your problem is to copy the following class in your project
class MySingleton constructor(context: Context) {
companion object {
#Volatile
private var INSTANCE: MySingleton? = null
fun getInstance(context: Context) =
INSTANCE ?: synchronized(this) {
INSTANCE ?: MySingleton(context).also {
INSTANCE = it
}
}
}
val imageLoader: ImageLoader by lazy {
ImageLoader(requestQueue,
object : ImageLoader.ImageCache {
private val cache = LruCache<String, Bitmap>(20)
override fun getBitmap(url: String): Bitmap {
return cache.get(url)
}
override fun putBitmap(url: String, bitmap: Bitmap) {
cache.put(url, bitmap)
}
})
}
val requestQueue: RequestQueue by lazy {
// applicationContext is key, it keeps you from leaking the
// Activity or BroadcastReceiver if someone passes one in.
Volley.newRequestQueue(context.applicationContext)
}
fun <T> addToRequestQueue(req: Request<T>) {
requestQueue.add(req)
}
}
Replace the first line of your populateViews function with
val requestQueue = MySingleton.getInstance(context).requestQueue
This should solve your problem
Another way is to use NetworkImageView from Volley's ToolBox
How to use
Replace your ImageView with NetworkImageView
<com.android.volley.toolbox.NetworkImageView
android:id="#+id/imgGrid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="centerInside"
/>
and load the image using
holder.imgGrid.setImageUrl(list[position].wallpaper_image_url,MySingleton.getInstance(context).imageLoader);

Loading picture to correct position in recycler view

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.

How to display a 360 panorama from android application

I want to display a panorama from my android application, this panorama is online and I have its url I load it on a webview but it won't work properly. It just appears a portion of it, and it won't turn or move upside down. I have no idea where to start with this, can you point me at the right direction? thank you in advance
After a lot of research I found out this library it is still pretty new but sure can help you start with something. I'm posting it just to save time for other searchers! cheers
PanoramaGl
I had a similar situation in Kotlin, but I needed to get the image from a URL. I resolved it using Picasso. I leave this here for future reference:
val options = VrPanoramaView.Options()
val url = "https://urltoyourimage.com/image.jpg"
Picasso.get().load(url)
.into(object : Target {
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
options.inputType = VrPanoramaView.Options.TYPE_MONO
vrPanoramaView.loadImageFromBitmap(bitmap, options)
}
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
print(e?.localizedMessage)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
print(placeHolderDrawable)
}
})
Add dependency in gradle (app-level)
compile 'com.google.vr:sdk-panowidget:1.80.0'
Add VrPanoramaView in your xml and get a reference via findViewById() method in android
Below is the code for loading the image from asset.
VrPanoramaView takes input as Bitmap so we need to first
convert it into right format. Here, the loadPhotoSphere functions
in called in onCreate()
private void loadPhotoSphere() {
VrPanoramaView.Options options = new VrPanoramaView.Options();
InputStream inputStream = null;
try {
inputStream = assetManager.open("image.jpg");
options.inputType = VrPanoramaView.Options.TYPE_MONO;
mVRPanoramaView.loadImageFromBitmap(BitmapFactory.decodeStream(inputStream), options);
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Read about Google VR SDK for Android here

Categories

Resources