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);
Related
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
I have still a little bit of trouble putting all information together about the thread-safety of using coroutines to launch network requests.
Let's say we have following use-case, there is a list of users we get and for each of those users, I will do some specific check which has to run over a network request to the API, giving me some information back about this user.
The userCheck happens inside a library, which doesn't expose suspend functions but rather still uses a callback.
Inside of this library, I have seen code like this to launch each of the network requests:
internal suspend fun <T> doNetworkRequest(request: suspend () -> Response<T>): NetworkResult<T> {
return withContext(Dispatchers.IO) {
try {
val response = request.invoke()
...
According to the documentation, Dispatchers.IO can use multiple threads for the execution of the code, also the request function is simply a function from a Retrofit API.
So what I did is to launch the request for each user, and use a single resultHandler object, which will add the results to a list and check if the length of the result list equals the length of the user list, if so, then all userChecks are done and I know that I can do something with the results, which need to be returned all together.
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
userList?.forEach {
checkUser(it, handler)
}
My question is: Is this implementation thread-safe? As far as I know, Kotlin objects should be thread safe, but I have gotten feedback that this is possibly not the best implementation :D
But in theory, even if the requests get launched asynchronous and multiple at the same time, only one at a time can access the lock of the thread the result handler is running on and there will be no race condition or problems with adding items to the list and comparing the sizes.
Am I wrong about this?
Is there any way to handle this scenario in a better way?
If you are executing multiple request in parallel - it's not. List is not thread safe. But it's simple fix for that. Create a Mutex object and then just wrap your operation on list in lock, like that:
val lock = Mutex()
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
lock.withLock {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
}
userList?.forEach {
checkUser(it, handler)
}
I have to add that this whole solution seems very hacky. I would go completely other route. Run all of your requests wrapping those in async { // network request } which will return Deferred object. Add this object to some list. After that wait for all of those deferred objects using awaitAll(). Like that:
val jobs = mutableListOf<Job>()
userList?.forEach {
// i assume checkUser is suspendable here
jobs += async { checkUser(it, handler) }
}
// wait for all requests
jobs.awaitAll()
// After that you can access all results like this:
val resultOfJob0 = jobs[0].getCompleted()
I noticed that when I create a separate Java file in my android studio project
and run it. It doesn't run like in a normal java project. I wanted to write
code for making HTTP call and read the response. Before integrating into the app
I need to test it out. One way could be to open Intellij Idea and write
a completely different small java project and then put that code inside the android
app.
There are several ways to do this. You can use Retrofit or okhttp.
using volley is one of the easiest ways:
implementation 'com.android.volley:volley:1.2.0'
manifest:
<uses-permission android:name="android.permission.INTERNET" />
in the fragment/activity/viewModel:
private var USGS_REQUEST_URL =
"example.com"
fun getResponse() {
volleySingleton = VolleySingleton.getInstance(application)
}
val stringRequest = StringRequest(
Request.Method.GET, REQUEST_URL,
{ response ->
//Handel your result
},
{
Log.d(TAG, "error")
}).setRetryPolicy(
DefaultRetryPolicy(
DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 3,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
)
).setShouldCache(true)
volleySingleton!!.addToRequestQueue(stringRequest)
VolleySingleton :
class VolleySingleton constructor(context: Context){
companion object {
/*
#Volatile: meaning that writes to this field are immediately made visible to other threads.
*/
#Volatile
private var instance: VolleySingleton? = null
fun getInstance(context: Context) = instance?: synchronized(this){
instance?: VolleySingleton(context)
}
}
/*
by lazy: requestQueue won't be initialized until this method gets called
*/
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)
}}
I'm currently trying to load an image using Volley's NetworkImageView:
<com.android.volley.toolbox.NetworkImageView
android:id="#+id/nivCharacterDetailPhoto"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
And in the back-end I've set it up like this:
private fun setImage(view: View) {
val photoView = view.findViewById<NetworkImageView>(R.id.nivCharacterDetailPhoto)
val imgLoader = VolleyRequestQueue.getInstance(view.context).imageLoader
photoView.setImageUrl("https://i.imgur.com/7spzG.png", imgLoader)
}
But whenever I try to load the page using this, I get a NullPointerException that cache.get(url) must not be null. The url is valid, so I surmised that the issue would need to be in the VolleyRequestQueue class. This class however, is exactly the same as the documentation describes here.
So:
class VolleyRequestQueue constructor(context: Context) {
companion object {
#Volatile
private var INSTANCE: VolleyRequestQueue? = null
fun getInstance(context: Context) = INSTANCE ?: synchronized(this) {
INSTANCE ?: VolleyRequestQueue(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)
}
}
I know for a fact that the url is a proper string and that it is set. I used the debugger to get down to the cache.get(url) statement and again found that a string is passed to the cache.get(url) function. This time the url contains a value like: "#W1440#H1916#S3https://i.imgur.com/7spzG.png". I did however also notice that the cache is completely empty, which explains why cache.get(url) returns null. However I assume (maybe wrongly?) that using this default implementation it would try to fetch the image if none is present in the cache.
Has anyone else run into this issue? It seems to be a pretty basic one but for some reason I just can't figure it out.
I'm running on Android 11, API 30
So, after a long search I eventually found the problem. The documentation is not really clear on this but:
override fun getBitmap(url: String): Bitmap {
return cache.get(url)
}
is supposed to be:
override fun getBitmap(url: String): Bitmap? {
return cache.get(url)
}
As the cache could return null this was causing the method to completely crash, as it's not allowed to return a nullable value. I don't know if this is only using the NetworkImageView, but should someone run across this issue again, simply make the getBimap method return a nullable Bitmap and it should work.
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).