Picassoがplaceholderのままで画像を表示してくれない問題に見舞われました。
TL;DR
- Picassoで画像が表示されないことがあるよ
- 
into(Target)を使ってると起こるよ
- Picassoはtargetを弱参照で保持してるからGCでtargetがなくなりやすいよ
- targetを強参照で保持するか、そもそもinto(ImageView, Callback)使おうな
問題
Picassoで画像をロードしたらついでに何かしたいパターンってあるじゃないですか。
ImageView mImageView;
// (略)
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
        	mImageView.setImageBitmap(bitmap);
           
           // 画像がロードされたらついでにやる処理
        }
        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
			mImageView.setImageDrawable(errorDrawable);
        }
        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
			mImageView.setImageDrawable(placeHolderDrawable);
        }
    });
この書き方だと、動いたり動かなかったりします。具体的にはplaceholderだけ表示されて、その後なにも起こらなくなります。
Picassoのログを見ても画像のロードは成功したことになってるし、もうどうしたものやらという感じでした。
弱参照でした
↑の記事を見て知ったのですが、into(Target)に渡したtargetは弱参照扱いになるとのこと。タイミング次第でtargetがGCされて、ロードした画像を流し込めなくなってたようでした。(ログに出ないのつらい)
よく見たら、弱参照の件はちゃんとinto(Target)のjavadocに書いてあるんですよね。
Note: This method keeps a weak reference to the {@link Target} instance and will be garbage collected if you do not keep a strong reference to it. To receive callbacks when an image is loaded use {@link #into(android.widget.ImageView, Callback)}.
https://github.com/square/picasso/blob/5208d48afbdd4cb2b51dbfe11946a5a9e6c518e2/picasso/src/main/java/com/squareup/picasso/RequestCreator.java#L524-L526
既に強参照になっているオブジェクトを渡すか、into(ImageView, Callback)を使いましょう。
タイトルには無名クラスと限定して書いてしまいましたが、生存期間の短いローカル変数などに代入していれば、実装クラスから生み出したオブジェクトでも同様の問題が起こります。
class MyTarget implements Target { ... }
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(new MyTarget()); // GCで死ぬ
// or
Target target = new MyTarget();
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(target); // これもGCで死ぬ
正しい使い方
1. ついでに何かしたいだけの場合
ロード後についでに何かしたいだけなら、into(ImageView, Callback)を使うのが一番順当な方法です。
ImageView mImageView;
// (略)
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(mImageView, new Callback(){
		@Override
		public void onSuccess() {
		    // 画像がロードされたらついでにやる処理
		}
		
		@Override
		public void onError() {
		    // エラー時の処理
		}
    });
2. 独自の処理を挟みたい場合(力技)
どうしても無名クラスをintoに入れたい場合は、StackOverflowで紹介されていた、無理やり強参照にする解決方法が使えます。
ImageView mImageView;
// (略)
Target target = new Target() {
		@Override
		public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
			processBitmap(bitmap) // 非同期で独自の画像処理を行う
			.subscribe(newBitmap -> {
				mImageView.setImageBitmap(newBitmap);
			});
		}
		
		@Override
		public void onBitmapFailed(Drawable errorDrawable) {
			mImageView.setImageDrawable(errorDrawable);
		}
		@Override
		public void onPrepareLoad(Drawable placeHolderDrawable) {
			mImageView.setImageDrawable(placeHolderDrawable);
		}
	};
mImageView.setTag(target);
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into((Target) mImageView.getTag()); // これなら強参照で渡せる
これだといかにも力技で見栄えが悪いので、設計が許せば次のやり方のほうがいいと思います。
3. カスタムビューやViewHolderに実装する
公式javadocオススメの使い方です。これが本来のinto(Target)の使い方のようですね。
Implementing on a View
public class ProfileView extends FrameLayout implements Target {
  @Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
    setBackgroundDrawable(new BitmapDrawable(bitmap));
  }
  @Override public void onBitmapFailed() {
    setBackgroundResource(R.drawable.profile_error);
  }
  @Override public void onPrepareLoad(Drawable placeHolderDrawable) {
    frame.setBackgroundDrawable(placeHolderDrawable);
  }
}
Implementing on a view holder object for use inside of an adapter
public class ViewHolder implements Target {
  public FrameLayout frame;
  public TextView name;
  @Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
    frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
  }
  @Override public void onBitmapFailed() {
    frame.setBackgroundResource(R.drawable.profile_error);
  }
  @Override public void onPrepareLoad(Drawable placeHolderDrawable) {
    frame.setBackgroundDrawable(placeHolderDrawable);
  }
}
まとめ
というわけで、非常に親切なjavadocをガン無視したコードを書いた結果、画像が表示されないというバグになっていました。
PicassoをBlameしてみたところ、2013年の半ばにはinto(ImageView, Callback)や親切なjavadocが整備されていたようなので、それ以前に書いたまま放置しているコードがある人は、同様のバグを作りこんでいないか調べてみるといいかもしれません。
みんな、ちゃんとjavadocは読もうな。
宣伝
ウォーターセル株式会社では、ライブラリのソースコードを読んだりPullReq出したりしながらみんなの生産性を上げていけるモバイルエンジニアを募集しています。地球人口100億人時代の食糧問題を解決する礎になるお仕事に興味がある方はご連絡ください。