ハイブリッドAndroidアプリ開発7つの工夫 | サイバーエージェント 公式エンジニアブログ
こんにちは。サイバーエージェントでアプリケーションエンジニアをしているkamiyaU(@fuzzy31u)です。
デビュー作girls picに続きCandyというAndroidアプリの開発を担当しました。
今回は「ハイブリッドAndroidアプリ開発7つの工夫」と題して実装上の創意工夫について触れたいと思います。

Candyとは。
女子中高生をメインターゲットとしたスマホで自分のページをデコれるサービスです。

サイバーエージェント 公式エンジニアブログ-0

ひとり当たりプロフィール、フォト、ブログ(アメブロと連携)の3つのページを持ちます。

さて。一見通常のWebサービスなのでWebVIewで表示するだけじゃないの?と思われるかもしれません。が、これらのデコデコしたページを管理編集するのがネイティブのお仕事。

サイバーエージェント 公式エンジニアブログ-0
デザイン編集に使用するパレットツールだったり

サイバーエージェント 公式エンジニアブログ-0
アルバムを作るための画像投稿・編集画面だったり

サイバーエージェント 公式エンジニアブログ-0
デコ絵文字をふんだんに使ったブログエディタだったり…

これ以外にも多くのカスタマイズ機能を持ち、非常に多機能なサービスです。
# 現代の女子中高生の心をつかむには細かく自分のページをカスタマイズできるのが魅力だそう

こんなAndroidアプリ、見たことない。。ということで、ほぼフルスクラッチ開発でしたが、仕様を実現するために工夫した点についていくつかご紹介します。



1: デザイン情報を共有する「StylePaletteオブジェクト」


devUのブログ
下にちょこんと出てくるViewをスタイルパレットと呼んでいます。

スタイルパレットはネイティブ実装ですが後ろのプレビューがWebのため、選択したデザインを反映させるにはWeb面へ通知してやる必要があります。
現在選択中のデザインをWebと共有するために、StylePaletteオブジェクトを定義しています。

1: 任意のViewをタップ(ユーザアクション)
 ↓↓↓
2: JavaからStylePaletteオブジェクトへ値をセット
 ↓↓↓
3: javascriptのfunctionをキックしてユーザが選択したことを通知
 ↓↓↓
4: javascriptからセットされている値を参照してプレビューに反映
このような流れで実現しています。
StylePaletteは編集中常に上書きされ、確定ボタンをタップしたタイミングで最終的に保持していたデータがサーバへ送信される仕組みです。



2: タブフレームワーク「TabManager」の実装


Candyでは下記のような限られたView空間でタブ切り替えをしてたくさんのコンテンツを詰め込むケースが多々あります。

サイバーエージェント 公式エンジニアブログ-2 サイバーエージェント 公式エンジニアブログ-2
こちらのエディタのようにネストしたタブの切り替えをするには、親のタブや子のタブの情報を関連付けておく必要があります。単純にViewをVisible/Goneで切り替えする実装ですと、5(上段)+5(下段)+4(下段)=14パターンの情報を保持しておかなければならなくなり、管理上手に負えないことになります。

そこで例えタブが増えたり減ったりしても作り直し…なんてことにならないようタブ切り替えの仕組みを抽象化し、再利用可能なフレームワークとして実装することにしました。
タブフレームワークといっても実態はただのLinearLayout。
LinearLayoutを継承したabstractなクラスです。

TabManagerでは大きく2つの役割を果たします。
・ onFinishInflate のタイミングで初期化メソッド(実装はサブクラスに委譲)を走らせます。
・ setCurrentTab(View currentView)メソッドによってタップされた View を選択状態にし、以前選択状態だった View を非選択状態にします。

下記は初期化メソッドです。
サイバーエージェント 公式エンジニアブログ-2

・setup - Viewの初期化など各サブクラスで必要な処理を行います。
・setTabClickListener - 自分の持っている子View(各タブ)にView#setOnClickListener()を仕込みます。
・initFragmentManager - コンテンツの表示はFragmentで切り替えるので自分の所属しているActivityからFragmentManagerをもらってTabManagerにセットします。
・setDefaultTab - Overrideは任意。最初にselectedにしておきたいタブがあれば、ここでセットします。サブクラスでは対象のView#performClick()を実行します。

サブクラスを実装したらlayout.xmlでは
サイバーエージェント 公式エンジニアブログ-2

のように通常のLinearLayoutの如く使用できます。この中に必要なViewを好きな数配置することでタブ切り替えで任意のViewを管理することができます。
# 他にもsetTabClickListenerの中でタップされた際に表示するFragmentをセットしたりする必要がありますが、スペースの関係上ここでは割愛させていただきます。

この仕組みにより4パターンあるデザイン編集と3パターンあるブログエディタのタブ切り替えでぐっとメンテナンス性が向上しました。



3: サイズ指定は「3で割り切れる数」で


サイズはdpで指定するので高解像度端末(Densityが1.5)用には3で割り切れるピクセル値で指定するとdp変換しやすいでしょう。という話がありますね。普段のレイアウトではそこまで意識することのなかったこの数字ですが、あるレイアウトを表現する際に綿密に計算する必要が出てきました。

サイバーエージェント 公式エンジニアブログ-3
これはローカルフォルダをカスタマイズした画面です。
このように細かいレイアウトですと1pxの違いが顕著にViewに反映されてしまいます。
この場合、横を480px(320dp)でデザインしており、1つのフォルダ幅が150px(100dp)です。320-100×3=20dpで4つの余白を埋めるようにします。

またフォルダ内の割り振りに関してですが、150pxのフォルダの中に2つの画像を均等に配置する…さてどのように割り振るべきでしょう。
150pxのうち右3pxはシャドウ部分なので、見た目上白い余白を均等に見せるためには下記のようになります。
・左:6px(=4dp)
・中央:6px(=4dp)
・右:9px(=6dp)←実際は9px-3pxで白い部分は6pxに見えるため

サイバーエージェント 公式エンジニアブログ-3
画像部分は{100dp-(4dp+4dp+6dp)}/2で43dpと算出できます。
ここで例えば5pxと指定された場合、「3dp=4.5px…4dp=6px…」となってしまい計算がややこしくなってしまいます。
このように細かいレイアウトはデザイナーさんに3で割り切れるサイズを指定してもらうとベターです。



4: 大量な画像のメモリ戦略


Candyではこれでもかというぐらい画像を大量に扱います。Webでできることと同じノリでAndroidでも表示させるということで、これはもうOOMとの戦いでした。
アルバム画像から細かいデコ画像まで大量の画像を安全にBitmap化するため、また画像投稿時の送信時間を少しでも軽減するために下記の処理を入れています。

★画像アップロード時のオートリサイズ

Candyでは画像ストレージとしてAmebaを使用しています。下記はその投稿フローです。
1: Amebaのユーザ画像フォルダ、またはgifなどのデザイン画像を配置するストレージへAmebaSDK経由で投稿 
 ↓↓↓
2: Candyサーバへ画像とのMapping情報をPOST
 ↓↓↓
3: 呼び出し元ごとに異なる投稿後の処理
※ネイティブから来た場合はアップロードした画像の情報をViewに反映させる、Webから来た場合はjavascriptのfunctionを実行する…等

Amebaへ画像を投稿する際に、端末のカメラで撮影したような画像をそのまま送っていてはTimeoutしてしまいます。かと言って「画像は××以下で投稿してください」などとアラートを出しても女子中高生にはいらっとされることでしょう。
なので一定の閾値を設け、そのサイズ以上だったらリサイズするというロジックを入れています。

Amebaへ画像を投稿する際にローカルファイルからPOSTする必要があるためtmpフォルダを作り、一時的にそこに保存します。またその際に少し圧縮してサイズを軽減しています。tmpフォルダはActivityの終了と共に削除される仕組みです。
これにより10Kから30K、多くても40Kぐらいまでには抑えることに成功しました。

★キャッシュ機構

大量の画像はもちろんキャッシュして逐一Bitmapを作ることは避けています。
1: 画像を表示する際、キャッシュになければSDカードとメモリにキャッシュ 
 ↓↓↓
2: 次回はメモリ上にあればメモリからBitmapを取得
 ↓↓↓
3: なければSDカードから取得

メモリ上のキャッシュはWeakHashMap、SDカード上のキャッシュはアプリの終了時に破棄します。
それでも100%OOMが解消できるとは言い難いのが現状です。ここはこれからも課題として取り組んでいこうと思います。



5: デコ絵文字の時差式取得


ブログエディタではたくさんのデコ絵文字(ほぼgifアニ)により表現豊かなブログが書けるようになっています。
デコ絵文字とミニデコは各々最大300もの絵文字をCandyサーバから取得し表示させています。
みんなの絵文字と顔文字はassetsファイルから読み込みます。
画像自体の取得はAsyncTaskで各々キックしています。これらを一度に表示しようとするとあっという間にRejectedExecutionExceptionが発生します。
なのでここはHandlerのpostDelayedを駆使することで一定時間置きに少しずつ取得しています。
これにより最初はぱらぱらと取得できた順に表示され、暫く経つとすべて表示されるようになります。
2回目以降はキャッシュに乗るのですぐに表示されます。



6: デコ絵文字表示の相対配置


デコ絵文字の表示は下記のような順序で表示されます。

サイバーエージェント 公式エンジニアブログ-6
GridViewでもLinearLayoutでも表現することが難しかったため、JavaからRelativeLayoutで相対的に配置しています。

サイバーエージェント 公式エンジニアブログ-6



7: TDDの失敗とConverter用Testクラス


開発当初、今度こそテストファースト!と意気込んでおりましたがテスト駆動開発には至りませんでした。
Candyプロジェクトは最初に詳細な仕様があったわけではなくその都度できるできないを判断しながら進めており、アジャイルどころかモックファーストな開発スタイルでした。
Androidの経験がほぼない私としては、できるできないの判断は「他のAndroidアプリがやってればできるんじゃん」というスタンスでした。そのためデザインが下りてきてから初めて「どうやって実現するんだろう」と悩んでおり、testプロジェクトは作成したものの「先に仕様を満たすテストコードを書く」というプロセスには不向きでした。

しかし今回testプロジェクトは別の用途で役に立ちました。それがConverterTestというTestクラスです。
ConverterはjavascriptとJavaとのブリッジの役目を果たすオブジェクトです。WebページからActivityをキックする、タイトルをセットする、メーラを立ち上げる…等あらゆるネイティブコードへのフックをjavascriptから行うクラスです。
開発中、Converterの挙動が正しいかを確認する際手動でWebページを開いて確かめるのは非効率でした。
そこでActivityInstrumentationTestCase2クラスを継承したConverterTestクラスを使うことで容易にデバッグを行うことができました。
ConverterTestでは外からConverterを生成してjavascriptのfunctionに扮した各メソッドをコールしています。


以上、何度も投げ出しそうになりながらも創意工夫を繰り広げたハイブリッドアプリの紹介でした。壁にぶつかりながらも独自実装を行うことで「できること/できないこと」というより「簡単にできること/できるけど厳しいこと」を身を持って体験しました。
まだリリース直後でパフォーマンス改善等の課題は山積しておりますが、今後とも最善策を模索していこうと思います。