Can Picasso queue for me?
Asked Answered
S

4

15

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.

Shedevil answered 9/9, 2017 at 14:58 Comment(3)
I am not picasso expert but I am wandering. Can't a timer solve your problem? developer.android.com/reference/java/util/Timer.htmlSile
hey @sabsab, no, that's not really relevant to the issueShedevil
I don't think this is possible purely using Picasso but the RxJava answer below is a good approach to solving you issue.Emblazonry
R
3

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

Result answered 16/9, 2017 at 19:26 Comment(3)
Looks like a great answer that deserves one million upvotesShedevil
@Shedevil btw, you should look at glide library too! Most google apps presented at IO sessions use it! :)Result
@Shedevil could you also mark it as correct to help future readers.Result
P
7

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.

Perceive answered 12/9, 2017 at 8:33 Comment(3)
while this is highly admirable and deserves 1000 votes, I believe Gut below has given the answer inside Picasso ?Shedevil
@Fattie, quite close to true, expect timing aspect, which I have mentioned in the comment.Perceive
.flatMap replace to .concatMapMonteria
T
5

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.

Tann answered 18/9, 2017 at 13:32 Comment(10)
If I'm not mistaken this is the answer!Shedevil
I've tested this and have got this output: the ordering is preserved, but the timing is interesting: there are 4 resources that are downloaded on exact same time, which is not possible. It seems like there is a window, where resourceN is being downloaded with resourceN-1 simultaneously.Perceive
@Shedevil I already mentioned the ExecutorService part in my answer!Result
This looks even odd on larger data set - see here, where rx approach gives meaningful output - see here.Perceive
@SarthakMittal your answer rocks!Shedevil
that is priceless testing @PerceiveShedevil
one issue @SarthakMittal and azizbekian it would seem to be there is now very little reason to use Picasso (which was King for 10 years) now that Fresco is here :OShedevil
@Shedevil Thanks! :D I have used nearly every famous image loading library in live projects. Fresco is good but it has the most methods ~14k, Picasso is the most lightweight and it does its work quite neatly! I would recommend that you define your use case first and then decide which library to use! :) feel free to discuss anything you would like to! :)Result
Hi @SarthakMittal - that's great; yes the absolute issue I have is queueing. So, imagine a "slide show" with 20 images. Say, each image shows for 5 seconds. Of course, the ideal behavior would be: as soon a the use lands on a given slide show, it starts downloading them one at a time, in order. if the user leaves that slide show, ideally you should be able to flush the queue. Based on your excellent experience, which of the three do you think is best !?!? incredible thanksShedevil
@Shedevil Truth is, you can use any library to achieve this, but since picasso is the most lightweight, I would recommend that. If you really like to use some other library I would recommend to go for Glide rather than Fresco (Fresco is also quite good, but they have a different approach and may not suit all needs). here is a very good post signifying the differences b/w picasso and glide: inthecheesefactory.com/blog/…Result
R
3

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

Result answered 16/9, 2017 at 19:26 Comment(3)
Looks like a great answer that deserves one million upvotesShedevil
@Shedevil btw, you should look at glide library too! Most google apps presented at IO sessions use it! :)Result
@Shedevil could you also mark it as correct to help future readers.Result
D
-1

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

Delimitate answered 18/9, 2017 at 11:44 Comment(1)
How sure you are, that Picasso.with(iv.getContext()).load(urls[index]).into(iv) would succeed sooner, than the next displayNextSlide()?Perceive

© 2022 - 2024 — McMap. All rights reserved.