Android開発をする上で知っておいてほしいなと思うこと

現在の Android Developers の情報は非常に充実していて、Developer Guides を順に読み進んでいくだけで開発に必要な知識とGoogleが想定している(であろう)最も基本的な実装を学ぶことができる。 特にこの「基本的な実装」というものが重要で、これを知っておかないと開発者間の意思疎通がスムーズに行えなかったり、そもそも気をつけておくべき注意点を見落としがちになってしまう。

とはいえ、今の膨大な公式ドキュメントをただ読めというのは厳しいので、Android開発をする上で最低限理解しておいてほしい(と僕が思っている)事柄と、それについて知ることができるドキュメント類についてまとめてみることにする。

2018/03/25 : リリース周りについて別記事に追記した。

nein37.hatenablog.com

公式ドキュメントの重要ページ

公式ドキュメントと言った場合、 Android Developers を指す。 最近は日本語化されている記事も増えてきているが、日本語ページは稀に内容が古かったりするので人からリンクを渡された場合は念のため英語ページも見ておいたほうが良い。

Android SDK Search という Chrome拡張を入れておくと Android Developers の検索をショートカットできたりクラスリファレンスからソースコードに飛べたりして非常に便利でオススメ。

最新の情報や問題を追いたいのであれば公式サイトだけでなく以下のサイトも見たほうが良い。

OSのバージョンについて

(5.x 以降では) Android のメジャーバージョンアップは大体一年に一度行われる。 過去のOSアップデートの差分については公式ドキュメントに詳しく書いてある他、最新のOSは公式リリースの前に Developers Preview が公開され、エミュレータや一部のデバイスで動作を確認することができる。

OSアップデート差分は大まかに「APIの変更点」「挙動の変更点」「サンプルコード」にわけられている他、特に重要な変更点については別ページで説明している場合もある。

以下では Oreo のアップデート差分を例に説明する

  • Android 8.0 Features and APIs | Android Developers
    • 変更(主に追加)になるAPIの概要
    • いわゆる新機能についてはここで説明される
    • ここで説明される機能のほとんどはそのバージョン以降でしか利用できないが、 Support Library 側であとからバックポートされるものもある。
  • Android 8.0 Behavior Changes | Android Developers
    • OS側の挙動変更の概要
    • 挙動変更には2種類あり、「すべてのアプリに適用されるもの」と「targetSdkVersionがある値以上の時に適用されるもの」がある
    • targetSdkVersion を変更する際はこの差分をただしく把握していないと不具合の元になるので、ここに説明があることを覚えておく
  • Code Samples | Android Developers
    • 新機能などの実装サンプル
    • 必要最低限の実装だったり過去のOSでの挙動がフォローされていなかったりするので注意
    • 新機能を利用する際はサンプルコードだけでなくAPIリファレンスを必ず参考にすること

毎月更新されるDashboardsにOSバージョンごとのシェアなどがまとまっているが、これは全世界での集計のようなので国内シェアとはずれている可能性があるので注意。

Android端末の特性について

Androidではスマートフォンタブレットの確実な判定方法が存在しない。Android Developers ではsw600dpが閾値っぽい表現がされていたとしても、メーカーがそこから逸脱した端末を出す ことがあるので確実ではない。

もし何らかの理由でタブレットを区別したい場合、「このアプリにおけるタブレットの条件」を決めておく必要がある。以下にその条件になりそうなものをいくつか挙げる。

  • sw600dp 以上の端末をタブレットとみなす
    • sw(smallest width) とは端末の向きに関わらず算出される画面の狭い方のdp値
    • Android Developers でも一応の閾値として扱われていて、システムUIとの親和性が高い
    • ただし端末そのものではなく表示領域を計算するものなので、端末側のdpi設定を大きく変更したり Nougat から導入された画面分割機能を利用した場合は一致しなくなる
    • Play ストア上のフィルタでは利用できないため、APK分割やインストール端末からの除外に利用することはできない
  • スクリーンサイズが large, xlarge 以上の端末をタブレットとみなす
    • おすすめしない
    • xlarge はほぼ確実にタブレットなのだが、 large はタブレットとは断言されず 640dp x 480dp and bigger となっていて非常に歯切れが悪い
    • sw480dp とほぼ等しい設定になるが、411dp(最近増えているスマホ幅)と480dp では近すぎ、480dp と 600dp では遠すぎてレイアウトの境目にし辛い
    • <compatible-screens> に記載することでインストール端末から除外することができるが、ファブレットのような例外を設定できない
  • Playストアの端末カタログから除外
    • もし区別の目的が「インストールさせないこと」である場合、配信設定で除外することができる
    • <compatible-screens> に記載するよりは細かいコントロールができるが、アプリだけでなくPlayコンソール側の設定も把握しておく必要があり運用が面倒になりがち

個人的な気持ちとしては、「タブレットをサポートしない」という判断は思ったほどコストが下がらない上に考えることが複雑なのであまりおすすめしない。 特に「画面回転を考慮したくない」という理由でタブレットを除外したいという意見が出ることがあるが、画面回転でクラッシュするアプリは何かしらライフサイクル周りの処理をミスっているのでどのみちクラッシュする可能性が高い。 タブレットレイアウトに対応するのが大変という意見もあるが、320-480dp のスマートフォンサイズまで全幅表示に対応し、それ以上の幅になった場合は中央寄せにでもしておけばそこまで手間ではない。 以前は android:maxWidth がこのような用途に使えなかったため不便だったが、現在はConstraintLayoutmaxWidth の設定ができるため、簡単に設定できる。 Android Studio の Scrolling Activity テンプレートで例を示すと、コンテンツ部分の content_scrolling.xml を以下のように変更すれば ConstraintLayout の最大幅を超えた場合はコンテンツの中央寄せ表示ができる。

content_scrolling.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.github.nein37.myapplication.ScrollingActivity"
    tools:showIn="@layout/activity_scrolling">

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:maxWidth="480dp">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:text="@string/large_text"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </android.support.constraint.ConstraintLayout>

</android.support.v4.widget.NestedScrollView>
Nexus5X Nexus9
f:id:nein37:20180318191803p:plain f:id:nein37:20180318191526p:plain

もちろんデザイナーの判断でこういった余白の使い方がダメな場合もあるが、この程度の実装で回避策が取れることは把握しておいたほうが良いと思う。

追記: タブレットや回転をサポートした場合にテスト工数も上がるのではという懸念に関してはその通りで、ちゃんと工数を積む必要がある。 ただしきちんとボタン押下イベントからのメソッド呼び出しなど単体テストが書かれていればスマートフォン/タブレットでの動作はそこまで違いがないし、表示差異は自分で手動テストしなくても Firebase Test Lab やPlayコンソールの リリース前テスト を利用すればいろいろなタブレットでの表示結果をスクリーンショット付きでテストすることができる。これらの仕組みはログインが必要な画面でもちゃんと設定すればログインしてくれるので、早い段階で導入できれば画面構成のチェックなどはかなり楽になるはずだ。

画面実装について

もともと用意されている画面部品に沿った実装にすることで工数を削減でき、システムUIと一体感のある画面になる。 Android開発者はデザイナーに対して「Androidの標準的な画面実装はこういうもの」と説明できなければいけないと思っているので、基本的なUI部品について知っている、あるいは説明されているページの存在を覚えておく必要がある。

具体的には以下のようなページとその内容を知っていてほしい。

  • ConstraintLayout
    • ConstraintLayout を使うと描画領域に応じた再配置・リサイズが非常にやりやすくなる
    • 反面、Viewサイズではなく他のViewとの関係性によってViewを配置するため、デザイナーに画面構成の指示方法について慣れてもらう必要がある
    • ConstraintLayout で画面実装ができ、画面デザインの指定方法についてデザイナに説明できるレベルまでは達していてほしい
  • FlexboxLayout
    • FlexboxLayout は CSS の Flexbox を Android のレイアウトで利用可能にしたもので、特に似たようなコンテンツを画面幅に応じて並べる画面構成に向いている
    • RecyclerView 用の LayoutManager も用意されておりいろんなケースで利用できる
    • 実際に利用したことがなくても存在は知っていてほしい
  • Design Support Library に入っているView全般
    • 記述されていないものでも TextInputLayout など重要なものが多い
  • Material Design
    • Material Design全体の仕様なので Android だけに限らないが、Androidの標準実装として知っておく必要はある
    • Design Support Library は Material Design の表現を目的としているので、Material Designに従うことで簡単に再現できるものもある

また、画面実装をする上で dp, dpialternative resource の仕組みについては他の人に説明できるレベルで理解していてほしい。

アプリの実装について

○○アーキテクチャや○○ライブラリを使っていればOKというものはないので自分やチームがテンション上がるものを使っていれば良いと思う。 とはいえ、Androidに関しては Espresso でのテストが増えていくと厳しいのでとにかく UnitTest 可能な部分を増やせる構造にしておいたほうが良い。既存のアーキテクチャを流用する場合、テストの書き方がわかる(または自分で理解できている)ものを採用しないと後々テスト書かなくなるので要注意。 Architecture Components は ドキュメントにテストに関する記述がありサンプルコードもじわじわ増えているようなので、段々導入しやすくなっていくのかなという気がしている。

その他のよく陥りがちなところ、把握しておいてほしい注意点については雑にまとめる

  • 非同期処理+画面遷移 の組み合わせは難しい
    • 何らかの非同期処理の結果を受けて Fragment を遷移させる実装をすると IllegalStateException で死ぬ
    • 可能なら遷移後の画面で非同期処理を行うようにするか、ライフサイクルに応じて処理を遅延させると良い
    • LiveData を利用するとコンポーネントがactiveなときだけ変更が通知されるのであまり意識しなくてよくなる
  • FragmentからActivity/Fragment へのコールバックは難しい
  • バックグラウンド処理の実装は公式ドキュメントをしっかり読んでからやる
    • バックグラウンド処理はOSの更新で挙動変更が入りやすいので、なるべく公式の方針に従うのが吉
    • minSdkVersion や 仕様面で許されるなら JobScheduler、定時に実行する必要があれば AlarmManager
    • Firebase JobDispatcher も実行条件を満たせるなら検討して良さそう
  • 一意な識別子について理解する
    • Android でインストール/アンインストールを跨いだ端末識別子は広告IDを利用するか、アプリ外の領域に保存しておくしかない
    • 生成したUUIDなどを保存しておく場合、Auto Backup によって保存されてよいかどうか検討する

アプリのセキュリティについて

埋め込み値

Android ではapkに埋め込んだ値はどのような難読化をしたとしても完全に安全ではない。ただし、文字列リソースや定数としてわかりやすい名前で平文埋め込みしてしまうと簡単に解析できてしまうので避けたほうが良い。 一番良いのはそもそもアプリ内に固定値を埋め込むような実装を避けることなので、なるべくこういった実装をしなくてすむようにしたい。

どうしてもやる場合は以下のいずれかを選択することになりそう。

  1. 何らかの複合処理が必要な形でコード内に埋め込む
  2. C/C++ のネイティブ実装から値を受け取るようにする
    • 平文で埋め込むとsoファイルから解析可能らしいので要注意

Android向けの著名なライブラリなどで何らかの値が必要な場合、ApplicationID、apkの証明書とセットでしか動作しない場合が多い。 その場合はもしその値が盗まれても拡散する恐れが少ないのであまり神経質にならなくても良いかもしれない(個人の感想)。

秘匿すべき値

認証トークンなどの秘匿値は基本的に暗号化して保存する必要がある。 SharedPreference は private mode であっても root 化された端末では(ユーザーだけでなく悪意のあるアプリからも)アクセス可能なので、あまり信用しないほうが良い。 KeyStore を利用して鍵を生成し、その鍵で暗号化/復号化するのがシステムが想定している挙動なのだが、生成した鍵が消えるタイミングがある ので完全に信頼するのは難しい。

個人的には、ユーザーID、パスワードなどは SmartLock for Passwords などアプリ外の仕組みに記憶させ、トークンなどをKeyStoreで暗号化した上で SharedPreference などに保存し、復号化に失敗するようになったら再度ログインからやり直し(SmartLock に情報があれば自動再試行)というのが良いのかなと思っている。

もっと言えば認証系の処理はシステム側のGoogle認証を利用するか Firebase Auth あたりに丸投げするのが一番楽で良いと思う。

海賊版対策など

apkの一部を差し替えて再署名した、いわゆる海賊版apk問題にアプリ側だけで対応するのは難しい。対応コード自体を削除されると何の意味もなくなるからだ。 Googleから提供されている仕組みとしてはSafetyNet Verify Apps API というものがあり、これは端末とアプリの情報を精査してGoogleのサーバにチェックしてもらい、信頼がおける環境かどうかをチェックすることができる。 ただし、この結果をアプリだけで判定すると脆弱なので、確実にチェックするなら(自サービスの)サーバに検証結果を送りそちらでもチェックさせる必要がある。 かなり複雑な仕組みなので最初から入れる必要はないかもしれないが、機能としては知っておいたほうが良いだろう。

また、これ以外にも Google Play コンソール側でSafetyNet に基づく除外設定を行うことができる。 これはGoogleの認定を受けていない変な端末やエミュレータ、root取得済みの端末にPlayストアからアプリをインストールさせないための仕組みで、こちらは特に実装が不要なので最初から利用しても良いかもしれない。

メモ

簡単なまとめにするつもりだったが思いの外長くなってきたので一旦ここで投稿する。 まだいくつか出てきそうなので思いついたら追記する予定。