Can Picasso queue for me?

1.4k Views Asked by At

Here's a critical point I don't know concerning the behavior of Picasso.

Imagine you are, say, showing a slide-show of ten items. Say, they are on-screen for ten seconds each.

The ideal behavior would be this: at the start of the slide show, I simply perform following:

picasso.get( url1 )
picasso.get( url2 )
picasso.get( url3 )
picasso.get( url4 )
picasso.get( url5 )
picasso.get( url6 )
picasso.get( url7 )
picasso.get( url8 )
picasso.get( url9 )
picasso.get( url10 )

And, in fact, Picasso would do those one at a time, in a queue.

What is the behavior of Picasso, if I tell it to pre-warm 10 urls all at once?

Is it possible to have Picasso do things only one at a time, in order - is there such an option?

(Other questions arising are, can you cancel the queue, or ...?)


Fresco

thanks to an amazing answer @alicanozkara on this page I learned for the first time about

https://github.com/facebook/fresco

(13k stars) for better or worse I think the Picasso era is probably over.

4

There are 4 best solutions below

3
On BEST ANSWER

Using only Picasso, what I think you can achieve is:

1) Load all the images asynchronously in the cache using fetch() like so:

Picasso.with(context).load(URL).fetch();

You can also add priority for images you want to load early: (Maybe mention high priority for first few images of the slide)

Picasso.with(context)
.load(URL)
.priority(Picasso.Priority.HIGH) // Default priority is medium
.fetch();

2) About cancelling the queue, you can add a common tag() to your images and you can pause/cancel/resume anytime!

private static final Object TAG_OBJECT = Object();

Picasso.with(context)
.load(URL)
.tag(TAG_OBJECT) 
// can be any Java object, must be the same object for all requests you want to control together.

Then we can control the tag like so:

Picasso.with(context)
.pauseTag(TAG_OBJECT)
//.resumeTag(TAG_OBJECT)
//.cancelTag(TAG_OBJECT)

3) Another important thing I would like to suggest is when you are pre-loading your images, only save them into your disk-cache, and load them to your memory-cache only while displaying. It will prevent flushing other important images from the memory-cache:

Picasso  
.with(context)
.load(URL)
.memoryPolicy(MemoryPolicy.NO_STORE) //Skips storing the final result into memory cache.
.fetch()

4) For sequentially loading your images in a queue, you can pass your own ExecutorService (SingleThreadExecutor in your case) using executor(ExecutorService) method, present in Picasso.Builder

You can even change the size of disk cache by using downloader(Downloader) method and your memory cache using memoryCache(Cache) method, both found in Picasso.Builder class.

Other Awesome Libraries:

Glide

Fresco

3
On

Is it possible to have Picasso do things only one at a time, in order - is there such an option?

I am not sure it can be done with Picasso itself, but at least RxJava may be applicable to this problem.

I'll post a snippet of code with comments:

public class MainActivity extends AppCompatActivity {

    public static final List<String> urlList = Arrays.asList(
            "http://i.imgur.com/UZFOMzL.jpg",
            "http://i.imgur.com/H981AN7.jpg",
            "http://i.imgur.com/nwhnRsZ.jpg",
            "http://i.imgur.com/MU2dD8E.jpg"
    );

    List<Target> targetList = new ArrayList<>();
    List<Completable> completables = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final long start = System.currentTimeMillis();
        // emit each url separately
        Observable.fromIterable(urlList)
                // flatmap to an Observable<Completable>
                .flatMap(url ->
                        // fromCallable ensures that this stream will emit value as soon as it is subscribed
                        // Contrary to this, Observable.just() would emit immediately, which we do not want
                        Observable.fromCallable(() ->
                                // We need to know whether either download is
                                // completed or no, thus we need a Completable
                                Completable.create(e -> {
                                    Target target = new Target() {
                                        @Override
                                        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
                                            Log.i("vvv", "downloaded " + url + ", " + (System.currentTimeMillis() - start));
                                            e.onComplete();
                                        }

                                        @Override
                                        public void onBitmapFailed(Drawable errorDrawable) {
                                            e.onError(new IllegalArgumentException("error happened"));
                                        }

                                        @Override
                                        public void onPrepareLoad(Drawable placeHolderDrawable) {

                                        }
                                    };
                                    // need to keep a strong reference to Target, because Picasso holds weak reference
                                    targetList.add(target);
                                    Picasso.with(MainActivity.this)
                                            .load(url)
                                            .into(target);
                                })))
                // collecting all Completables into a list
                .collectInto(completables, List::add)
                // flatmap-ing this Observable into a Completable, concatenating each completable
                // to next, thus they will be downloaded in order
                .flatMapCompletable(Completable::concat)
                // clearing the strong reference we retained earlier
                .doFinally(() -> {
                    targetList.clear();
                    targetList = null;
                })
                .subscribe(
                        () -> Log.i("vvv", "done: " + (System.currentTimeMillis() - start)),
                        throwable -> Log.e("vvv", "err " + throwable.getMessage()
                        ));
    }

}

This will be the output in logcat:

enter image description here

This is the ideal case scenario, when each image gets successfully loaded. This snippet doesn't handle the case, when one of images cannot be loaded. As soon as Picasso fails to load one of them - the stream will be interrupted and onError() would be called.

10
On

In Picasso.Builder you can give a specific ExecutorService: https://square.github.io/picasso/2.x/picasso/com/squareup/picasso/Picasso.Builder.html#executor-java.util.concurrent.ExecutorService-

If you give a new single thread executor, https://developer.android.com/reference/java/util/concurrent/Executors.html#newSingleThreadExecutor(), Picasso will download all your images one at a time.

1
On

There is no one-method-call fix to chain with Picasso, but you could create a helper like follows:

public PicassoSlideshow {

    private static PicassoSlideshow instance;
    private WeakReference<ImageView> view;
    private Handler handler;
    private int index;
    private String[] urls;
    private long delay;

    private PicassoSlideshow() {
       //nothing
    }

    public static PicassoSlideshow with(ImageView view) {
        if (instance == null) {
            instance = new PicassoSlideshow();
        }
        instance.setView(view);
    }

    private void setView(ImageView view) {
        this.view = new WeakReference<>(view);
    }

    //Note: I'm only suggesting varargs because that's what you seem to have in the question  
    public void startSlideshow(long interval, String... urls) {
        if (handler == null) {
            handler = new Handler();
        }
        index = 0;
        this.urls = urls;
        delay = interval;
        displayNextSlide();
    }

    private void displayNextSlide() {
        //display one 
        ImageView iv = view.get();
        if (iv != null) {
            Picasso.with(iv.getContext())
                   .load(urls[index]).into(iv);
            index++;
            if (index < urls.length) {
                //preload next
                Picasso.with(iv.getContext()).fetch(urls[index]); 
                //on timer switch images
                handler.postDelayed(PicassoSlideshow::displayNextSlide, delay); 
            }
        }
    }

}

Usage:

PicassoSlideshow.with(view).startSlideshow(10000, url1, url2, url3, url9);

Please note, I've just written that from the top of my head while my IDE invalidates its caches, so you might need to tweak it a little