Picasso is a popular open-source Android library for loading both local and remote images. Learn how to easily use it for handling your image loading needs.
1. What Is Picasso?
Picasso (name inspired by the famous French artist Pablo Picasso) is a very popular open-source Android library for loading images in your Android app. According to the official docs, it states:
…Picasso allows for hassle-free image loading in your application—often in one line of code!
Note that Picasso uses OkHttp (a network library from the same developer) under the hood to load the images over the internet.
2. So Why Use Picasso?
Now you have learned what Picasso is all about, the next question you might ask is why use it?
Developing your own media loading and display functionality in Java or Kotlin can be a real pain: you have to take care of caching, decoding, managing network connections, threading, exception handling, and more. Picasso is an easy to use, well planned, well documented, and thoroughly tested library that can save you a lot of precious time—and save you some headaches.
Here are many of the common pitfalls of loading images on Android that are dealt with for you by Picasso, according to the official docs:
- handling
ImageView
recycling and download cancellation in an adapter - complex image transformations with minimal memory use
- automatic memory and disk caching
Adding images to your app can make your Android app come alive. So in this tutorial, we’ll learn about Picasso 2 by building a simple image gallery app. It will load the images via the internet and display them as thumbnails in a RecyclerView, and when a user clicks on an image, it will open a detail activity containing the larger image.
A sample project (in Kotlin) for this tutorial can be found on our GitHub repo so you can easily follow along.
Good artists copy, great artists steal. — Pablo Picasso
3. Prerequisites
To be able to follow this tutorial, you’ll need:
- a basic understanding of core Android APIs and Kotlin
- Android Studio 3.1.1 or higher
- Kotlin plugin 1.2.30 or higher
Fire up Android Studio and create a new project (you can name it PicassoDemo
) with an empty activity called MainActivity
. Make sure to also check the Include Kotlin support check box.
4. Declare Dependencies
After creating a new project, specify the following dependencies in your build.gradle. At the time of writing, the latest version of Picasso is 2.71828
.
Or with Maven:
com.squareup.picasso picasso 2.71828
Make sure you sync your project after adding Picasso and the RecyclerView
v7 artifacts.
5. Add Internet Permission
Because Picasso is going to perform a network request to load images via the internet, we need to include the permission INTERNET
in our AndroidManifest.xml.
So go do that now!
Note that this is only required if you’re going to load images from the internet. This is not required if you are only loading images locally on the device.
6. Create the Layout
We’ll start by creating our RecyclerView
inside the activity_main.xml layout file.
Creating the Custom Item Layout
Next, let’s create the XML layout (item_image.xml) that will be used for each item (ImageView
) within the RecyclerView
.
Now that we have created the layouts needed for our simple gallery app, the next step is to create the RecyclerView
adapter for populating data. Before we do that, though, let’s create our simple data model.
7. Create a Data Model
We are going to define a simple data model for our RecyclerView
. This model implements Parcelable for high-performance transport of data from one component to another in Android. In our case, data will be transported from SunsetGalleryActivity
to SunsetPhotoActivity
.
data class SunsetPhoto(val url: String) : Parcelable { constructor(parcel: Parcel) : this(parcel.readString()) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(url) } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator{ override fun createFromParcel(parcel: Parcel): SunsetPhoto { return SunsetPhoto(parcel) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } }
Note that this model SunsetPhoto
has only a single field called url
(for demo purposes), but you can have more if you want. This class implements Parcelable
, which means we have to override some methods.
We can make use of Android Studio IDEA to generate these methods for us, but the downside to this is maintenance. How? Anytime we add any new fields to this class, we might forget to explicitly update the constructor
and writeToParcel
methods, which can lead to some bugs if we don’t update the methods.
Now, to circumvent updating or writing these boilerplate methods, Kotlin 1.1.14 introduced the @Parcelize
annotation. This annotation will help us generate the writeToParcel
, writeFromParcel
, and describeContents
methods automatically under the hood for us.
@Parcelize data class SunsetPhoto(val url: String) : Parcelable
Now, our code SunsetPhoto
class is just two lines! Awesome!
Remember to add the following code to your app module build.gradle
:
androidExtensions { experimental = true }
In addition, I included a companion object (or static method in Java) getSunsetPhotos()
in the SunsetPhoto
model class that will simply return an ArrayList
of SunsetPhoto
when called.
@Parcelize data class SunsetPhoto(val url: String) : Parcelable { companion object { fun getSunsetPhotos(): Array{ return arrayOf (SunsetPhoto("https://goo.gl/32YN2B"), SunsetPhoto("https://goo.gl/Wqz4Ev"), SunsetPhoto("https://goo.gl/U7XXdF"), SunsetPhoto("https://goo.gl/ghVPFq"), SunsetPhoto("https://goo.gl/qEaCWe"), SunsetPhoto("https://goo.gl/vutGmM")) } } }
8. Create the Adapter
We’ll create an adapter to populate our RecyclerView
with data. We’ll also implement a click listener to open the detail activity—SunsetPhotoActivity
—passing it an instance of SunsetPhoto
as an intent extra. The detail activity will show a close-up of the image. We’ll create it in a later section.
class MainActivity : AppCompatActivity() { //... private inner class ImageGalleryAdapter(val context: Context, val sunsetPhotos: Array) : RecyclerView.Adapter () { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageGalleryAdapter.MyViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val photoView = inflater.inflate(R.layout.item_image, parent, false) return MyViewHolder(photoView) } override fun onBindViewHolder(holder: ImageGalleryAdapter.MyViewHolder, position: Int) { val sunsetPhoto = sunsetPhotos[position] val imageView = holder.photoImageView } override fun getItemCount(): Int { return sunsetPhotos.size } inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { var photoImageView: ImageView = itemView.findViewById(R.id.iv_photo) init { itemView.setOnClickListener(this) } override fun onClick(view: View) { val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val sunsetPhoto = sunsetPhotos[position] val intent = Intent(context, SunsetPhotoActivity::class.java).apply { putExtra(SunsetPhotoActivity.EXTRA_SUNSET_PHOTO, sunsetPhoto) } startActivity(intent) } } } } }
Notice that we used the apply
extension function to put an object as extra to the intent. As a reminder, the apply
function returns the object passed to it as an argument (i.e. the receiver object).
9. Loading Images From a URL
We’re going to need Picasso to do its job in this section—not to paint us a work of art, but to fetch images from the internet and display them. We’ll display these images individually in their respective ImageView
s inside our RecyclerView
onBindViewHolder()
method as the user scrolls the app.
override fun onBindViewHolder(holder: ImageGalleryAdapter.MyViewHolder, position: Int) { val sunsetPhoto = sunsetPhotos[position] val imageView = holder.photoImageView Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .into(imageView) }
Step by step, here’s what the calls to Picasso
are doing:
The get()
Method
This returns the global Picasso
instance (singleton instance) initialized with the following default configurations:
- LRU memory cache of 15% the available application RAM.
- Disk cache of 2% storage space up to 50MB but no less than 5MB. Note: this is only available on API 14+.
- Three download threads for disk and network access.
Note that if these settings do not meet the requirements of your application, you’re free to construct your own Picasso
instance with full control of these configurations by using Picasso.Builder
.
val picassoBuilder = Picasso.Builder(context) // do custom configurations // Specify the {@link Downloader} that will be used for downloading images. picassoBuilder.downloader() // Specify the ExecutorService for loading images in the background. picassoBuilder.executor() // Specify the memory Cache used for the most recent images. picassoBuilder.memoryCache() // and more val picasso = picassoBuilder.build()
Finally, you call the build()
method to return you a Picasso
instance with your own configurations.
It’s recommended you do this in your Application.onCreate
and then set it as the singleton instance with Picasso.setSingletonInstance
in that method—to make sure that the Picasso
instance is the global one.
The load()
Method
load(String path)
starts an image request using the specified path. This path can be a remote URL, file resource, content resource, or Android resource.
-
placeholder(int placeholderResId)
: a local placeholder resource id or drawable to be used while the image is being loaded and then displayed. It serves as a good user experience to display a placeholder image while the image is downloading.
Note that Picasso first checks if the image requested is in the memory cache, and if it is, it displays the image from there (we’ll discuss caching in Picasso more in a later section).
Other Methods
-
error(int errorResId)
: a drawable to be used if the requested image could not be loaded—probably because the website is down. -
noFade()
: Picasso always fades in the image to be displayed into theImageView
. If you don’t want this fade-in animation, simply call thenoFade()
method. -
into(ImageView imageView)
: the target image view into which the image will be placed.
Image Resizing and Transformation
If the server you are requesting the image from doesn’t give you the image you need in the required size, you can easily resize that image using resize(int targetWidth, int targetHeight)
. Calling this method resizes the image and then displays it on the ImageView
. Note that the dimensions are in pixels (px), not dp.
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .resize(400, 200) .into(imageView)
You can pass in an Android dimension resource for both the width and height using the method resizeDimen(int targetWidthResId, int targetHeightResId)
. This method will convert the dimension size to raw pixels and then call resize()
under the hood—passing the converted sizes (in pixels) as arguments.
Picasso.get() //... .resizeDimen(R.dimen.list_detail_image_size, R.dimen.list_detail_image_size) //...
Note that these resize methods won’t respect aspect ratio. In other words, your image aspect ratio can be distorted.
Fortunately, Picasso
gives us some useful methods to solve this issue:
-
centerCrop()
: scales the image uniformly (maintaining the image’s aspect ratio) so that the image fills up the given area, with as much of the image showing as possible. If needed, the image will be cropped horizontally or vertically to fit. Calling this method crops an image inside of the bounds specified byresize()
. -
centerInside()
: scales the image so that both dimensions are equal to or less than the requested bounds. This will center an image inside of the bounds specified byresize()
. -
onlyScaleDown()
: only resize an image if the original image size is bigger than the target size specified byresize()
. -
fit()
: attempt to resize the image to fit exactly into the targetImageView
‘s bounds.
Image Rotation
Picasso has an easy API to rotate an image and then display that image. The rotate(float degrees)
method rotates the image by the specified degrees.
Picasso.get() //... .rotate(90f) //...
In the example above, this would rotate the image by 90 degrees. The rotate(float degrees, float pivotX, float pivotY)
method rotates the image by the specified degrees around a pivot point.
Picasso.get() //... .rotate(30f, 200f, 100f) //...
Here we are going to rotate the image by 30 degrees around the pivot point 200, 100 pixels.
Transformation
Apart from just manipulating an image by rotating it, Picasso also gives us the option to apply a custom transformation to an image before displaying it.
You simply create a class that implements the Picasso Transformation
interface. You then have to override two methods:
-
Bitmap transform(Bitmap source)
: this transforms the source bitmap into a new bitmap. -
String key()
: returns a unique key for the transformation, used for caching purposes.
After you’re done creating your custom transformation, you simply execute it by invoking transform(Transformation transformation)
on your Picasso instance. Note that you can also pass a list of Transformation
to transform()
.
Picasso.get() // ... .transform(CropCircleTransformation()) .into(imageView)
Here, I applied a circle crop transformation to the image from the Picasso Transformations open-source Android library. This library has many transformations you can apply to an image with Picasso—including transformations for blurring or grey-scaling an image. Go check it out if you want to apply some cool transformations to your images.
10. Initializing the Adapter
Here, we simply create our RecyclerView
with GridLayoutManager
as the layout manager, initialize our adapter, and bind it to the RecyclerView
.
class MainActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView private lateinit var imageGalleryAdapter: ImageGalleryAdapter override fun onCreate(savedInstanceState: Bundle?) { //... val layoutManager = GridLayoutManager(this, 2) recyclerView = findViewById(R.id.rv_images) recyclerView.setHasFixedSize(true) recyclerView.layoutManager = layoutManager imageGalleryAdapter = ImageGalleryAdapter(this, SunsetPhoto.getSunsetPhotos()) } override fun onStart() { super.onStart() recyclerView.adapter = imageGalleryAdapter } // ... }
11. Creating the Detail Activity
Create a new activity and name it SunsetPhotoActivity
. We get the SunsetPhoto
extra and load the image—inside onStart()
—with Picasso as we did before.
class SunsetPhotoActivity : AppCompatActivity() { companion object { const val EXTRA_SUNSET_PHOTO = "SunsetPhotoActivity.EXTRA_SUNSET_PHOTO" } private lateinit var imageView: ImageView private lateinit var sunsetPhoto: SunsetPhoto override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_sunset_photo) sunsetPhoto = intent.getParcelableExtra(EXTRA_SUNSET_PHOTO) imageView = findViewById(R.id.image) } override fun onStart() { super.onStart() Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .into(imageView) } }
The Detail Layout
Here’s a layout to display the detail activity. It just displays an ImageView
that will show the full-resolution version of the loaded image.
12. Caching Mechanism in Picasso
If you observe carefully, you’ll notice that when you revisit an image that was previously loaded, it loads even faster than before. What made it faster? It’s Picasso’s caching mechanism, that’s what.
Here is what’s going on under the hood. After an image has been loaded once from the internet, Picasso will cache it both in memory and on disk, saving repeated network requests and permitting faster retrieval of the image. When that image is needed again, Picasso will first check if the image is available in memory, and if it’s there, it will return it immediately. If that image is not in memory, Picasso will check the disk next, and if it’s there, it returns it. If it’s not there, Picasso will finally do a network request for that image and display it.
In summary, here’s what goes on (under the hood) for an image request: memory -> disk -> network.
Depending on your application, though, you might want to avoid caching—for example, if the images being displayed are likely to change often and not be reloaded.
So How Do You Disable Caching?
You can avoid memory caching by calling memoryPolicy(MemoryPolicy.NO_CACHE)
. This will simply skip the memory cache lookup when processing an image request.
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .memoryPolicy(MemoryPolicy.NO_CACHE) .into(imageView)
Note that there is another enum: MemoryPolicy.NO_STORE
. This is useful if you are very certain that you will only request an image once. Applying this will also not store the image in the memory cache—thereby not forcing out other bitmaps from the memory cache.
But be very aware that the image will still be cached on the disk—to prevent that also, you use networkPolicy(@NonNull NetworkPolicy policy, @NonNull NetworkPolicy... additional)
, which takes one or more of the following enum values:
-
NetworkPolicy.NO_CACHE
: skips checking the disk cache and forces loading through the network. -
NetworkPolicy.NO_STORE
: skips storing the result into the disk cache. -
NetworkPolicy.OFFLINE
: forces the request through the disk cache only, skipping the network.
To avoid both memory and disk caching altogether, just call both methods one after the other:
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE) .networkPolicy(NetworkPolicy.NO_CACHE) .into(imageView)
13. Request Listeners
In Picasso, you can implement a listener or callback to monitor the status of the request you made as the image loads. Only one of these methods will be called if you implement the Target
interface on a request.
-
void onBitmapFailed(e: Exception?, errorDrawable: Drawable?)
: triggered whenever the image could not be successfully loaded. Here, we can access the exception that was thrown. -
void onBitmapLoaded(Bitmap bitmap, LoadedFrom from)
: fired whenever an image has been successfully loaded. Here, we get the Bitmap to show the user. -
void onPrepareLoad(Drawable placeHolderDrawable)
: invoked right before your request is submitted.
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .into(object : Target { override fun onPrepareLoad(placeHolderDrawable: Drawable?) { } override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) { } override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) { } })
Here you could also show and then hide a progress dialog if you had one.
There is another callback listener you can implement if you want, called Callback
. This interface has just two methods: onSuccess()
and onError(Exception e)
. The former is called when the image request load was successful, and the later is called when there is an error in processing the request.
Going back to our image gallery app (inside SunsetPhotoActivity
), let’s modify the display a little by using a Callback
object that will set the bitmap to the ImageView
and also change the background colour of the layout by extracting the dark and vibrant colour of our image using the Android Palette API.
So include the palette artifact in your app module’s build.gradle:
dependencies { //... implementation 'com.android.support:palette-v7:27.1.1' }
Let’s now implement the Callback
interface in our Picasso request.
override fun onStart() { super.onStart() Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .into(imageView, object : Callback { override fun onSuccess() { val bitmap = (imageView.drawable as BitmapDrawable).bitmap onPalette(Palette.from(bitmap).generate()) } override fun onError(e: Exception?) { } }) } fun onPalette(palette: Palette?) { if (null != palette) { val parent = imageView.parent.parent as ViewGroup parent.setBackgroundColor(palette.getDarkVibrantColor(Color.GRAY)) } }
14. Testing the App
Finally, you can run the app! Click on a thumbnail to get a full-sized version of the image.
15. Prioritizing Requests
When you want to load different images at the same time on the same screen, you have the option to order which one is more important than the other. In other words, you can load important images first.
You simply call priority()
on your Picasso request instance and pass in any of the enums: Priority.LOW
, Priority.NORMAL
, or Priority.HIGH
.
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .priority(Picasso.Priority.HIGH) .into(imageView) Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .priority(Picasso.Priority.NORMAL) .into(imageView) Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .priority(Picasso.Priority.LOW) .into(imageView)
16. Tagging Requests
By tagging your Picasso requests, you can resume, pause, or cancel requests which are associated with specific tags. Depending on your use case, you can tag your requests with a string or objects that should define the scope of the request as a Context
, an Activity
, or a Fragment
. You can easily tag a Picasso request by calling tag(@NonNull Object tag)
on one. Pass it an instance of Object
which serves as the tag.
Here are the following operations you can perform on tagged Picasso requests:
-
pauseTag(Object tag)
: pause all requests associated with the given tag. -
resumeTag(Object tag)
: resume paused requests with the given tag. -
cancelTag(Object tag)
: cancel any existing requests with the given tag.
Picasso.get() // ... .tag(context
Though tagging your requests gives you some control over your requests, you should be very careful when using tags because of the potential for memory leaks. Here’s what the official documentation says:
Picasso will keep a reference to the tag for as long as this tag is paused and/or has active requests. Look out for potential leaks.
Loading From the File System
It’s straightforward to load images locally in your app.
File file = new File("your/pic/file/path/file.png") Picasso.get() .load(file) .fit() .into(imageView)
Conclusion
Nice job! In this tutorial, you’ve built a complete image gallery app with Picasso, and along the way you’ve learned how the library works and how you can integrate it into your own project.
You’ve also learned how to display both local and remote images, tagging requests, prioritizing requests, and how to apply image transformations like resizing. Not only that, but you’ve seen how easy it is to enable and disable caching, error handling, and custom request listeners.
To learn more about Picasso, you can refer to its official documentation. To learn more about coding for Android, check out some of our other courses and tutorials here on Envato Tuts+!
Powered by WPeMatico