降りつぶし.net~同期するWebアプリ・スマホアプリの開発・運用~

第6回(最終回) Android用アプリケーション「降りつぶしroid」開発

スマホ用アプリ第2弾

降りつぶし.netは、⁠日本の鉄道駅」にいつ乗下車したのか、を管理するソリューションです。前々回まで、過去の記録を管理するためのWebアプリケーションと、そのWebアプリケーションとデータを同期する仕組みについて説明しました。そして前回と今回にわたり、リアルタイムに記録を更新することができるスマホアプリの開発について説明します。最終回となる今回は、前回取り上げたiPhone用アプリの完成後に制作しリリースした、Android用アプリケーション「降りつぶしroid」です。

Androidアプリの開発

降りつぶしroidは、Android 2.2以降で動作する、降りつぶし管理のためのアプリケーションです。最初のバージョンは2012年12月にリリースしました。iPhoneアプリのリリースからまる1年かかってしまいました。

これは、Android開発には様々な落とし穴があり過ぎたこと、筆者自身がJavaに対して苦手意識を持っていたこと、の2つによります。

Androidアプリの経験は、2011年に既に動作しているアプリの修正作業などを手伝う機会があり、その後同年9月に(すなわち、i降りつぶしの開発と並行して)地図を操作するアプリのテスト開発を受託し、3ヵ月で一応完成させています。しかしこれが今考えると実にひどい実装で、要求仕様どおりには動作しているのですが少しでも機能拡張しようとするとうまく動かなくなる、そんな代物だったのです。

また、Javaについては、得意としていたC++でできることがあれもできないこれもできない、ということで面倒くさく感じていました。が、2012年、Javaの案件(おもにServlet)の受託が急に増えたためそうも言ってられなくなり、慣れてきて苦手意識もなくなりました。これも年末にリリースを間に合わせる原動力になりました。

Android Maps V2への対応

2012年12月リリースの初版は、地図APIとしてAndroid Mapsを使っていました。

このAPI、レイヤの追従描画がスムーズでない、そのままではタップジェスチャを独自に拾えない、マーカーは重なりも考慮しながらすべて自前で描画しなければならない、吹き出しは完全に自前で実装しなければならない、一部APIがドキュメントどおりに動かない、Honeycomb(3.0)からの新しい画面の仕組みであるフラグメントと共存できない、とないないづくしの恐るべき代物で、かなり苦労させられました。

ところが、その同じ2012年12月、Android Maps V2という新バージョンのAPIが公開されたのです。上記の問題をほぼすべて解決し、当時のGoogle Mapsアプリとほぼ同じ操作もできるというこのAPI。しかし、これは従来のAPIとまったく互換性がありませんでした。

その時点でアクティビティを切り替える実装などすべて済んでいたので、いまさらすべての画面をフラグメントにつくりかえることなど、年内リリースの目標上、絶対に不可能です。

そんなわけで、初版では古い地図APIによる古い描画の地図画面となっていました。

そして2013年11月、ついにV2への対応が完了しています。同年8月に、Jelly Beans(4.1)のUIを古いAndroidでも動作させることができるサポートライブラリの完成度が上がったことで、重い腰をあげることになりました。また、この連載に合わせて完成させるというのもきっかけの1つではあります(笑⁠⁠。

以下に紹介する画面は、その最新版のものとなります。

地図画面

最新版の降りつぶしroidは、Jelly Beansの標準的なUIである、アクションバーおよびナビゲーションドロワーを持つアプリケーションです。アプリの初回起動時の表示は地図画面となりますが、それ以降の起動時は最後に表示していた画面を再度開くようになっています。

図1 地図画面
図1 地図画面

i降りつぶしの地図画面UIを踏襲しつつ、操作感やミテクレをAndroid版の地図アプリに近づけた画面となっています。もちろん、スワイプやピンチで表示領域を変化させることができます。回転や傾斜も可能です。

上部にはコンパス(必要に応じて表示⁠⁠、検索ボックス、および現在位置移動ボタン(タップにより中心が現在位置に移動)があり、下には表示切り替え用の半透明なセグメンティッドコントロール風の(実は)ラジオボタン、またズームイン/アウトボタンがあります。地図部分をシングルタップすると、トグルで検索バーおよびラジオボタンの表示・非表示が切り換わります。決して広くない地図画面を少しでも広げるための機能です。

また画面上には、降りつぶしroidが管理する鉄道駅がピンでプロットされています。赤いピンは乗下車していない駅、緑のピンは乗下車済の駅です。

これらのピンをタップすると、情報ビュー(吹き出し)が表示されます。表示されている情報ビューをタップすると、後述する「駅情報画面」が表示されます。

図2 地図画面での表示の切り換え
図2 地図画面での表示の切り換え

地図の下部には2セットのセグメンティッドコントロール風のラジオボタンが配置されています。

これらの操作とその意味は、第5回の記事で説明したi降りつぶしとまったく同一です。

ただし、i降りつぶしにおいては、これらはツールバー上に置かれていました。降りつぶしroidではこれが地図上に半透明なボタンとして置かれています。一覧表示画面は単なるリストビューなのですが、これらのボタンを消してしまうと地図表示に戻る方法がなくなってしまいます。そのため、ぶかっこうなのですが、一覧表示でもボタンを表示したままにしています。

図3 検索画面とその結果
図3 検索画面とその結果

上部の検索ボックスをタップすると地図検索モードとなります。i降りつぶしでは、単純な地点検索と、駅の絞り込み表示の設定という、2つの機能が実装されていましたが、降りつぶしroidでは後者は未実装となっています。次期バージョンでは実装される予定です。

検索ボックスに地名などを入力して検索を実行すると、入力された語句をGoogle Maps APIで検索し、検索結果のうちユーザが選択した地点を地図の中心に移動させます。この機能は地図表示にのみ影響するもので、駅データベースへの操作を含みません。

駅情報画面・乗下車日画面・メモ画面

地図画面の情報ビュー(吹き出し)をタップした場合、あるいは地図画面または他の画面にて表示されている駅の一覧から特定の駅をタップした場合、この画面が開きます。

図4 駅情報画面・乗下車日画面・メモ画面
図4 駅情報画面・乗下車日画面・メモ画面

レイアウトは異なりますが、これらの画面の機能も第5回で解説したi降りつぶしとまったく同じです。

駅情報画面の「今日」ボタンをタップすると、乗下車日画面を出すことなく直接その時点での日付がデータベースに登録されます。

また「乗下車」をタップすると乗下車日画面が開きます。乗下車済か未乗下車かの切り換えはチェックボックスで行い、年・月・日の設定は数値ピッカーで行います。

なおこの画面は、Honeycomb以降の環境で実行した場合のものです。バージョン2.xには数値ピッカーが存在しません。そのためリフレクションで日付ピッカーからリソースを取り出し、各環境にマッチしたUIでの日付入力を実現しています。両者の切り分けは、ベタですがバージョン別にレイアウトファイルを用意し、さらにロジックも単純なif文での場合分けです。

図5 乗下車日画面のバージョン比較
図5 乗下車日画面のバージョン比較

全事業者画面・事業者画面・路線画面・駅一覧画面

ナビゲーションドロワーの「全事業者」を選択すると開く画面です。リストビューによる一覧表示で、Webアプリと全く同じ遷移第3回参照)を行います。各項目の表示順もWebアプリと完全に同一です。

図6 全事業者画面・事業者画面・路線画面・駅一覧画面
図6 全事業者画面・事業者画面・路線画面・駅一覧画面

この画面のレイアウトは、一覧表示系のすべての画面で共通です。共通のフラグメントをベースクラスとして実装しています。

リストビューの項目の表示および操作は、i降りつぶしとまったく同じです第5回参照⁠⁠。

全都道府県画面・読み画面・乗下車年月別画面

図7 全都道府県画面・読み画面・乗下車年月別画面
図7 全都道府県画面・読み画面・乗下車年月別画面

これらは、表示は前述の事業者画面などと同じ、また機能は同名のi降りつぶしの各画面とまったく同じであり第5回参照⁠⁠、画面のみの紹介とします。

ただし、第5回で触れた、⁠読みが『つ』の駅の統計表示がおかしくなる」というi降りつぶしの不具合は、降りつぶしroidにははじめから存在しません。

同じSQLiteデータベースを使っていても、すなわち同じSQL文を実行しているはずでも、結局アクセス部分の実装が言語からして異なるため、こういう問題も発生してしまいます(と居直り⁠⁠。

同期画面

第4回で解説した、本ソリューション最大のキモである同期機能を、実際に実行する画面です。

ナビゲーションドロワーからの遷移だけでなく、アクションバーに同期アイコンがあり、そのタップで直接この画面に入ることもできます。

図8 初期の同期画面
図8 初期の同期画面

初期の画面は、i降りつぶしのものとは若干異なっています。

同期画面を表示すると、より正確には「同期画面フラグメントが生成された直後に⁠⁠、バックグラウンドで降りつぶし.netへの照会を実行し、ログインユーザ名を取得します。

既にログイン済だった場合はログインユーザ名が表示され、その右のボタンはログアウトボタンとして機能します。ログインしていなかった場合は「ログインしていません」と表示され、右のボタンはログインボタンとして機能します。通信エラーの場合はその旨が表示され、右のボタンは再ロードボタンとなります。

図9 Webアプリ・Twitterからの認証画面
図9 Webアプリ・Twitterからの認証画面

ログインしていない状態でログインボタンをタップすると、ログインのための遷移となります。

これは、i降りつぶしと同様に、降りつぶし.netやTwitter OAuth認証のHTMLを、ウェブビューで表示しています。内部の実装の詳細も含めて、第5回3ページで解説したものと同じです。

加えて認証画面をキャンセルしたくなった場合、Androidの特徴の1つであるバックキーを押せば、初期画面に戻ることができます。

図10 同期実行中の画面
図10 同期実行中の画面

ログインしていると初期画面の「同期を開始する」ボタンがタップ可能となり、実際にタップすると同期が実行されます。

i降りつぶしでは同期の実行(もちろん、http通信を伴います)は、同期画面自身がワーカスレッド(厳密にはGCD)を立ててその中で実行しています。そして同期画面から離れたり、ホームボタンが押されてアプリがバックグラウンド化したりした場合、同期処理を中断します。

一方Androidには、正式にバックグラウンドで処理を実行するための仕組みとしてサービスがあります。同期機能はサービスとして実行されます。

後述するとおり、データベースアクセス自体もサービスが行わなければならないため、実際には「DBアクセス・同期の実行」をまとめ、1つのサービスとして実装しています。

同期画面はサービスを実行し、画面内のUIを操作不能とし(プログレスバー(という名前なんです、くるくる回るアイコンが!)つきの半透明なビューをかぶせています⁠⁠、後は何もしません。

フォアグラウンドサービスとして実行される同期は、その進行状態を自アプリの画面ではなく、ステータスバーに直接表示します。また、画面を切り替えても同期の実行は中断しません。

お知らせ画面・設定画面

降りつぶしroidにはあと2つの画面があります。

これらの画面にも、ナビゲーションドロワーからだけでなく、アクションバーのアイコンのタップでも遷移できます。

図11 お知らせ画面・設定画面
図11 お知らせ画面・設定画面

いずれもi降りつぶしと完全に同じ内容です第5回4ページ⁠。しかし内部の実装はいろいろ異なります。

まずお知らせ画面ですが、これは単に降りつぶし.netの特定のURLをリクエストし、その内容第3回参照。ただし現在の表示内容はまったく異なっており、Twtter APIで自サーバに取り込んだタイムラインを独自のHTMLで出力)を表示しているだけだ、という点はi降りつぶしの同画面と同じです。しかし、Androidには後述するフラグメントライフサイクル問題があり、そのままですと画面が再生成されるたびに、さほど更新されないお知らせの内容をサーバにリクエストしてしまうことになります。

そのため、最新の降りつぶしroidではこの画面も「インスタンス保持」に設定し、インスタンス作成時に非同期通信でHTMLを取得して内部に保持してしまい、画面再生成時にはその取得したHTMLをウェブビューに直接流し込むことで表示させています。

もう1つの設定画面は、本来ならば設定画面の構造をXMLで記述するだけで済み、コードをほとんど書かなくてよいのです。旧版の降りつぶしroidでは、設定画面用のアクティビティクラスを使い、お手軽に実現していました。

しかし、フラグメントなど最新の機能を古いAndroidで実行するためのサポートライブラリには、現在のところ設定画面用のフラグメントクラスが移植されていません。

そのため、最新の設定画面フラグメントのソースコードをコピペして、若干手直しをして使っています。

実装のポイント~Android開発という苦闘

ここからは実装の話となります。

i降りつぶしは、iOSが提供する機能をほぼ素直に使って実装できました。ややこしい実装はあまりせずに済んでいます。

しかし、降りつぶしroidではそうはいきませんでした。

よく「Androidはいろいろな解像度やデバイスがあるので対応が大変」と言われます。画面を作り込むホビーアプリなどはそのとおりだと思います。ただ、降りつぶしroidはぶっちゃけ、ビジネスアプリみたいなものなので(笑⁠⁠、その問題はほとんど意識しなくて済んでいます。アイコンの数だけは莫大になりましたが、これはデザインを無償でお願いしている伊藤壮氏(@souitoh)が泣いたという話で(すみませんでした⁠⁠、筆者自身の苦労にはなりませんでした。

それでも起こる問題は、提供されている機能にあるさまざまな制限や癖によるものです。スレッドの扱い、アニメーション中のタップ問題などは他のOSにも存在しますが、アクティビティやフラグメントのライフサイクルなどはAndroid独特のものです。

以下、筆者が検索した限りでは解決策があまり出てこなかった、いくつかのポイントに絞って挙げます。

実装のポイント1~データベース更新問題

まず最初の問題は、データベースの扱いです。

iOSでのSQLiteデータベースの初期処理としては、第5回4ページで触れたとおり、⁠データベースファイル自体をアプリが抱えていてそれを端末のデータとして上書きする」が定番です。

しかしこの方法には、既に存在するデータベースのアップデート時に発生するいくつかの問題があり、利用しないほうが無難と考えています。

まず、一部の機種において、データベースディレクトリへの書き込みに問題があるようです

また更新する場合に、いったんデータベースを開き、ユーザバージョンを確認し、更新が必要と判断した場合の処理に自信が持てません。当然ながらデータベースをいったん閉じ、その後データベースファイルを上書きし、改めて開く、という処理となりますが、AndroidのSQLiteライブラリはJNIで実装されており、close処理に関する問題も指摘されています。筆者がAndroidおよびSQLite3のソースコードを読んだ限りでは、今のところは上記処理には問題がなさそうですが、今後、ネイティブ層でのクローズ処理が終了しないうちにファイルの上書きが始まるような実装が登場しないとも言い切れません。

Androidはデータベースのセットアップ用に、SQLiteOpenHelperというヘルパークラスを提供しています。このクラスはコンストラクタにSQLiteのユーザバージョンを渡すと、アップデートが必要な場合にonUpgradeコールバックが呼び出されます。そこでSQL文を実行することで、データベースを更新してくださいね、ということです。

一般的なアプリなら、やはりこれを使っておくべきだ、と考え、アプリはデータベースファイルを直接抱えるのではなく、データベース構造を作成するSQL文を抱えることにしました。

しかしここで別の問題が発生します。降りつぶし.netのデータベース作成SQL文は最新バージョンで2.3Mバイトあるのですが、Androidアプリが抱えられるデータファイルは最大で1Mバイトなのです。

そしてZIPで圧縮するとこの制限を超えられますが、圧縮されたZIPファイルをアプリの中で解凍するための便利なクラスはありません。自力で解凍です。

実装のポイント2~データベースをサービス化する

先にも触れたとおり、データベースアクセス(と同期)は、Androidのサービスとして実装しました。

サービスは、アクティビティやフラグメントのライフサイクルとは無関係に、バックグラウンド実行を行うための仕掛けで、iOSになくてこちらにある、数少ない便利な機能の中の1つです。

サービスの利用で、前述のとおりに手間がかかるデータベースへの接続、また確実に多大な時間を要する同期機能を、安全に実行することができます。

ただしAndroidでは、サービスにもライフサイクルがあります。アクティビティやフラグメントから接続されているサービスやフォアグラウンドにあるサービス(ステータスバーに通知を送っている状態)は生存できますが、それ以外は常にシステムから強制終了される運命にあります。

そのことも含め、データベースクエリおよび同期実行を管理するキューを、PriorityBlockingQueueクラスで実装しました。データベースはサービス内で立てたワーカスレッドで動作させ、キューを使ってUIスレッドからリクエストを受け付けます。

図12 データベースサービス内でのリクエスト処理
図12 データベースサービス内でのリクエスト処理

もちろんその処理は単純なFIFOでよいため、キューは単純なBlockingQueueでもよいはずです。しかし、以前のアクティビティ主体の実装では、何らかの状態でアクティビティがすべて一時的に消滅し、結果としてアプリは動作中なのにサービスが終了させられてしまう可能性がありました。

その対処として、⁠すべてのサービス接続がなくなった」状態から10秒待ち、それまで再接続要求がない場合は自爆する、という設計を行ったのですが、その終了指示をワーカスレッドに送るためには単純なフラグでは不可能となり、ScheduledExecutorServiceによりキューに再優先で「終了せよ」というリクエストを行うこととしました。

それでも、これは優先順位が2種類なので本来ならBlockingDequeで済むのですが、なんとAndroidのBlockingDequeは2.3からの提供なのです。2.2に対応するために、わざわざPriorityBlockingQuereを使い、UIスレッドで正のシリアルナンバを発行して優先度を設定、終了リクエストは優先度0、という実装になったのです。

実装のポイント3~フラグメント永続化

3.0以降のAndroidの画面は、フラグメントという単位で構成されます。システム上のアプリの画面単位はアクティビティのままですが、1つのアクティビティの上に1つ以上のフラグメントが乗り、そのフラグメントをAndroidがスタック管理してくれます。もともとは、スマホだと2画面切り替えしていたものを広いタブレットの画面では1画面で同時に表示したい、というようなニーズに応えるための仕組みでしたが(2つのフラグメントを、スマホでは切り替えて表示し、タブレットでは同時に表示する⁠⁠、それ以外の大きなメリットが「永続化」です。

具体的には、Fragment#setRetainInstanceメソッドを、引数にtrueを指定して呼び出すだけなのですが、これにより「言語切り換えやフォント切り換え、またフラグメントの切り換えなどの際に、インスタンスが保持するデータが全消滅する」という、Android開発初心者が最も戸惑う問題が回避されます。

ただしこれを利用する場合には注意すべき点も多々あり、ライフサイクル問題への充分な理解は変わらず必要なのですが。

図13 フラグメント永続化によるデータベース処理
図13 フラグメント永続化によるデータベース処理

降りつぶしroidでは、データベースクエリや同期を実行するすべてのフラグメントは、共通のベースクラスDBAccessFragmentBaseを継承して実装されています。このクラスでは、onCreateで永続化を設定し、データベースサービスへの接続および結果通知を受け取る準備をし、onDestroyでサービス接続を解除します。これにより、そのインスタンスがある限り、データベース接続や更新通知を必ず受け取れる、という状態が保障されました。一方で、onDestroyViewコールバックで表示に関わるメンバにすべてnullを設定して参照を解除しなければならない、という、前近代的な対策も必要です。

実装のポイント4~バックキー処理とフラグメント

Android特有の、⁠前の画面に戻る」操作を実行するのがバックキーです。通常、その処理はシステムが行ってくれます。

フラグメントによる画面の世界では、フラグメントの切り換えは「フラグメントトランザクション」で行います。フラグメントの付け替え・追加・削除などの処理を、トランザクションとして登録すると、その後バックキーが押されたところで、トランザクションの内容の「逆」を自動的に実行します。

図14 フラグメントトランザクション
図14 フラグメントトランザクション

しかし降りつぶしroidにおいては、このままだと問題が起こります。同期画面です。

同期画面でログインを実行している途中で、ログインをやめたくなったとします。しかしログインの実行はウェブビュー内のHTMLで行われているため、その中からキャンセルを行うことはできません。そのため降りつぶしroidでは、バックキーでキャンセルとしています。もちろん、初期画面の状態でバックキーが押された場合は、前の画面に戻る通常の動作です。また、ログインの実行中に別の画面を表示し、そこからバックキーで戻ってきた場合は、ログイン画面ではなく初期画面を表示します。これらはさほど不自然でないUI操作だと考えています。

これらを実現するためには、バックキーを同期画面フラグメントが乗っ取らねばなりません。

ただ、アクティビティにはバックキーを横取りする仕組みが正式に用意されていますが、フラグメントにはそれは存在しないのです。

そこで、実際の横取りはアクティビティが行うこととして、同期画面フラグメントがその横取りの依頼と通知をアクティビティに行うこととしました。フラグメントは専用のリスナインターフェースを実装し、表示される寸前のonResumeで自分自身をアクティビティに登録し、表示終了直後のonPauseでアクティビティへの登録を削除します。アクティビティは複数フラグメントへの対応として登録をコレクションで管理し、onBackPressedで登録されているリスナを順に呼び出し、いずれかでバックキーが処理された場合はそこで処理を中断、いずれでも処理されなかった場合はデフォルトのバックキーの処理をシステムに行わせます。

実装のポイント5~Android Maps V2のアニメーション問題

初代に比べてまったく別物となったAndroid Maps V2ですが、描画をOpenGLで行っているらしく、地図画面の遷移にアニメーションを用いると、表示が乱れます。

地図画面に対して横のスライドイン・アウトアニメーションを実行

削除時は、地図部分のみが上に少しだけスライドアウトした後フェードアウトし、その後、ボタンなどのみが指定どおり左にスライドアウトします。また再登場時は、ボタンなどのみが指定どおり左からスライドインし、その後地図部分がフェードインで表示されます。ボタンと地図のアニメーションが一致しないのはかなり違和感があります。

いろいろな解決策を試みましたが、結局、他のフラグメントを表示する際、地図画面にアニメーションを設定しない、という消極的な策で逃げています。

そしてこの策により、全体への影響も出ました。駅情報画面もフラグメントですが、これは地図の吹き出しのタップで表示される場合と、一覧表示の駅項目のタップで表示される場合があります。そして後者については、リストビューの項目の右端に「>」を表示しており、駅情報画面は右からスライドインしてきます。ただ、これを地図の吹き出しタップに適用すると、⁠画面真っ黒→右から駅情報画面がスライドイン」となり、なんとも間の抜けたビジュアルになってしまうのです。

これについては、両者でアニメーションを分けることにしました。一覧からは従来通りの右スライドインとし、吹き出しからの場合はフェードインとしています。

おわりに

これまで6回にわたり、降りつぶしを管理するソリューションの紹介をさせていただきました。

ニッチすぎる用途のアプリケーション群ですが、ごく一部の鉄道マニアからはご評価もいただけましたし、また筆者にとっても、自身の記録管理に大いに役立つのみならず、その設計・実装・運用を通じて、スキルアップもできたと感じています。

降りつぶしという趣味が「知力体力時の運」のすべてを要求されているのと同じく、降りつぶし.netでも、データベースの整備、3言語での開発、同期機能など、総合的なスキルが必要でした。

つたない連載でしたが、なんらかのアプリを個人的に開発・運用するための一助となれば幸いです。

おすすめ記事

記事・ニュース一覧