本記事は 個人開発 Advent Calendar 2019 11日目の記事です。
概要
個人開発ではないですが、副業でAndroidアプリ開発した時のお話しです。
私は、普段(主務で)サーバサイド開発がメインですが、
以前はAndroidアプリ開発を2〜3年くらいやっていました。
かなり久々の状態でAnroidアプリをフルスクラッチ開発した時のお話しを書いてみようかと思います。
開発したアプリ
Runtrip という 走るモチベーションになるランナー向けSNSアプリ
のAndroidアプリを1から開発しました。
開発体制
- 下記の3名体制
- 本業の方が1名: この方も以前はAndroid開発をしたことあり
- 私: 以前はAndroid開発していたが、1から作るのは久しぶり
- 副業メンバーがもう1人: Androidアプリ開発をしたことはあるがかなり久々
- このように普段はAndroid開発をしていないメンバーで開発を始め、約4ヶ月間でリリース^^
開発方針
- 開発する我々が久々にやる人達だったこともあり、下記の方針で開発
- Googleさん純正のものをなるべく使う
- 全部自分たちで作ろうとせず、OSSに頼るところは頼る
- 使用するものはなるべく新しいバージョンを使うこと
- 特別な実装をしなければいけない際は話し合うようにしてましたが、基本的にはこの方針に従いつつ、画面単位で担当を決めて開発を進めていきました
使用したもの
そんな方針の中で使用したもの一部が下記になります。
対象 | 使用物 |
---|---|
言語 | Kotlin |
DI | Dagger2 |
画像処理 | Glide |
通信処理 | Retrofit, OkHttp |
リアクティブ | RxAndroid |
jsonライブラリ | moshi |
pub/sub | EventBus |
ログ | Timber |
メモリリーク検出ツール | LeakCanary |
一部抜粋ですが、これでもかというくらい世の中のAndroidアプリ開発で使われているもので開発を行いました。そのおかげで大分開発しやすかったので、これらのOSSに感謝しかないですmm
そして、以前自分が開発していた時に比べると、上記に加えてAndroid標準で用意してくれているものを組み合わせればかなり開発しやすくなったなと実感する日々だったことを思い出します。
あとは、ViewModelを使いつつ、レイヤードアーキテクチャーチックな構成で開発してみました。
ちょっとした苦労話
こちらのアプリ、基本的にはこれまでに話させて頂いたものを使わせて頂いたおかげで、そんなに苦労せず?に開発することができましたが、2点だけ苦労した部分があります。
その部分は、下記の ジャーナル
と言われるものの開発で、
その中でも、下記の2点に時間がかかりました。
- ジャーナル投稿部分
- ジャーナル一覧の画像表示処理
1つ目に関しては、自分が担当しなかったので(いずれ関わった誰かが苦労話を書くと信じて割愛)、2点目の画像表示部分についてちょっとだけ書きたいと思います。
仕様
- 仕様としてちょっとだけ大変だったのが下記
- 画像は下記の3タイプがある
- 正方形
- 縦長の長方形
- 横長の長方形
- これはジャーナル投稿時にユーザが選択する
- 一覧で表示する際は、画像の縦横比をいい感じに計算して、それぞれに合わせて表示する
- 画像は下記の3タイプがある
- つまり、縦長の画像の場合は、画像の横の長さを端末の横幅に合わせつつ、一覧上では縦長に表示されるといった処理が必要でした
利用ライブラリ
- 利用しているのは下記
- 画像表示は
Glide
- リスト表示は
RecyclerView
- 画像表示は
-
RecyclerView
の表示サイクルに合わせつつ、画像サイズを適宜計算して表示
思いついた方法
- Android開発が久々の自分が思いついた方法は下記の3つでした
-
RecyclerView.Adapter
のonBindViewHolder
でよしなにできない? -
Glide
のcallbackでよしなにできない? -
ImageView
のライフサイクルでよしなにできない?
-
- Android開発に詳しい方や日頃から開発している方は
これでいけるじゃん!
というのを思いつくかもしれませんが、久々の自分はこれを全て試してみました><
RecyclerView.AdapterのonBindViewHolder
実装イメージは下記のような感じ
class JournalAdapter(<<省略>>) : RecyclerView.Adapter<BindingViewHolder> {
/////
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
// holderのImageViewを、読み込んだ画像の大きさをいい感じに計算して表示
}
/////
・・・・当たり前ですが、Bindした段階で画像が読み込まれていない可能性があるので、この段階で計算しても大きさが0とかになり得るので、この方法ではダメ(じゃない方法があるかもしれませんが、このままだとダメでした><)
Glideのcallback
Glideには画像を読み込んだ際のCallback関数 onResourceReady
を使ってみたのが下記。
// imageViewという変数にImageViewがある想定
GlideApp.with(context)
.load(url)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
resource?.let {
val params = this@imageView.layoutParams
params.width = resource.intrinsicWidth
params.height = resource.intrinsicHeight
this@imageView.layoutParams = params
this@imageView.invalidate()
}
return false
}
})
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView)
→ この書き方、一見すると良さそうでしたが、これはあくまで 読み込みが完了したタイミングで呼ばれる
ので、その前に描画されるとこの計算は反映されずorz
各種callbackの整理
- 一度各種callbackがどのような順序で呼ばれているか、特にadapter、Glide、ImageViewで、それぞれどのような順番で呼ばれるか見てみました
- adapter.onBindView
- ImageView.onMeasure
- (画像の読み込み完了時) Glide.onResourceReady
- 計算がどのようにされるかというと、
-
onBindView
でImageViewを計測すると、高さも横も0となる(要は画像が読み込めてない) - ImageView(正確にはView)の
onMeasure
だと、最初は表示するdrawable
がないのでnullになる(ここも画像が読み込めていない状態) - onResourceReadyで表示すべき画像の大きさがわかり、高さと横などがわかる
- そのあとにレイアウトで表示される領域などが決まるので、再度ImageViewの
onMeasure
が呼ばれる
-
- これらのことから、onResourceReadyが読み終わったあとであれば、onBindViewでもimageViewの縦横はある
- が、、、そのImageViewの高さと鵜呑みにしてはいけないTT
- それは 前回表示したもの の高さや横の長さになっている可能性がある(RecyclerViewは、一度使われたもののlayoutは再利用されるため)
- Glideの場合、cacheから取得した画像にしろ、リモートから読み込んだ画像にしろ、読み込んだタイミングのcallbackとして、
onResourceReady
が読み込まれる
ImageView(View)のcallback
上記の整理から、ImageView(View)の onMeasure
で計算すれば良いのでは?ということで、下記のような処理を入れてみました
class CustomImageView : AppCompatImageView {
/////
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Parent width ( this case, device width )
val width = MeasureSpec.getSize(widthMeasureSpec)
if (drawable != null) {
when {
// Square image
drawable.intrinsicWidth == drawable.intrinsicHeight -> {
// deviceの横幅より小さい画像の場合の対応
if (drawable.intrinsicWidth < width) {
setMeasuredDimension(width, width)
} else {
setMeasuredDimension(drawable.intrinsicWidth, drawable.intrinsicHeight)
}
this.invalidate()
}
// Portrait image
drawable.intrinsicHeight > drawable.intrinsicWidth -> {
val height: Int = (drawable.intrinsicHeight * width) / drawable.intrinsicWidth
setMeasuredDimension(widthMeasureSpec, height)
this.invalidate()
}
// Landscape image
else -> {
val height: Int = (drawable.intrinsicHeight * width) / drawable.intrinsicWidth
setMeasuredDimension(widthMeasureSpec, height)
this.invalidate()
}
}
} else {
setMeasuredDimension(widthMeasureSpec, widthMeasureSpec)
}
}
/////
}
・・・・うまく表示されたーーー!!(と喜んだことを覚えてますw)
現状、縦長と横長は同じ処理で表示される(基本的には端末の横幅に画像幅を合わせ、それに応じて縦幅を変えるのみ)ので、そこで処理をわけなくても良いのですが、あとで見た人でもわかりやすいようにわけております。
最後に
個人開発ではないですが、副業でAndroidアプリ開発をしてみた話を書かせて頂きました。
画像表示部分、もっとうまくやれる方法があるかもしれませんが、自分の思いつく中では上記が限界でした(時間的にも知識的にも><)ので、
もし「こうするともっとうまくできます!」などありましたら、是非アドバイスいただけると嬉しいです!!
そして最後まで読んで頂きまして、誠にありがとうございましたmm