AndroidLint #DroidKaigi

Preview:

Citation preview

Android Lintで正しさを学ぼう2015.02.18 DroidKaigi 2016 Yukiya Nakagawa / @Nkzn

ROOM:B 13:00-13:50 #DroidKaigiB

Who are you?

• Yukiya Nakagawa / @Nkzn

• ウォーターセル株式会社@新潟

• 農業向けサービス「アグリノート」

• エンジニア募集中です

• Androidは2009年からチマチマと

Recent Activity

• C89コミケ

• とびだそう!Androidプログラミングレシピ

• https://techbooster.booth.pm/

• Android Lintネタ書いてました

Target

• まだAndroidプログラミングのベストプラクティスが分からない初学者

• Androidアプリの品質を表す指標がほしい品質担当者

Agenda

• Android Lintとは

• 初学者がやりがちなミス

• もっと上を目指す人のために

• 品質指標としてのAndroid Lintを考える

Android Lintとは

Lintとは

Another HTML-lint HTMLの構文チェックツール

JSLint, JSHint, ESLint JavaScriptの構文チェックツール

textlint 日本語の構文チェックツール

lintとは、主にC言語のソースコードに対し、コンパイラより詳細かつ厳密なチェックを行うプログラムである。

• 型の一致しない関数呼び出し

• 初期化されていない変数の参照

• 宣言されているが使われていない変数

• 同じ関数を参照しているが、戻り値を使う場合と使わない場合がある

• 関数が戻り値を返す場合と返さない場合がある

など、コンパイラではチェックされないが、バグの原因になるような曖昧な記述についても警告される。

https://ja.wikipedia.org/wiki/Lint

Javaの場合?

Android Lintとは

• Google公式

• 222個のルール(ver25.0.4現在)

• Category, Severity, Priorityで分類される

• 結構手広い

• 不具合予備軍の検出

• ユーザビリティチェック

Categoryカテゴリ名 チェックする内容Correctness SDKの使い方の正しさ

Correctness:Messages Correctnessのうち、特にメッセージ

Security セキュリティ

Performance パフォーマンス(動作速度)

Usability ユーザビリティ(使い勝手)

Usability:Icons Usabilityのうち、特にアイコン

Usability:Typography Usabilityのうち、特に文字

Accessibility アクセシビリティ

Internationalization 国際化・多言語化

Bi-directional Text Right-to-Leftモードでの見た目

Severity

レベル 意味 アプリへの影響

Fatal 致命的 ビルドや実行に必ず失敗する

Error エラー ビルドはできるが実行時エラーを引き起こす可能性が高い

Warning 警告 動作はするが修正したほうが より良いアプリになる

Information 情報 ほぼ問題ないが頭の片隅に 置いておいたほうがよい

Ignore 無視 問題があったとしても 検出しない

チェックできるファイルの種類

• https://android.googlesource.com/platform/tools/base

• com.android.tools.lint.detector.api.Detector https://android.googlesource.com/platform/tools/base/+/master/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/Detector.java

1. Manifest file

2. Resource files, in alphabetical order by resource type

3. Java sources

4. Java classes

5. Gradle files

6. Generic files

7. Proguard files

8. Property files

Priority

• 1から10まで

• あまりあてにならない

どんな問題を解決するのか

お作法分かりづらい問題

https://twitter.com/konifar/status/698760224409169920

Android Way is どこ

https://twitter.com/konifar/status/698760496837603328

Googleによる正解集 (あるいはアンチパターン集)

Android Lint

事例紹介 Part 1. 初学者がやりがちなミス

Case 1. LinearLayoutに並べたビューが 表示されないのは何故?

(Orientation)

こんなコード書いてませんか

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="one" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="two" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="three" /> </LinearLayout>

何故かアイテムがひとつしか出ない・・・?

Orientation

• Summary: Missing explicit orientation

• Priority: 2 / 10

• Severity: Error

• Category: Correctness

何故いけないのか?デフォルトのorientationはhorizontal

こう書きましょう

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="one" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="two" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="three" /> </LinearLayout>

Case 2: Fragmentが表示されない

こんなコード書いてませんか

@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_commit_transaction); getSupportFragmentManager().beginTransaction() .add(R.id.container, new MyFragment());}

あれー? Fragmentが表示されないなー?

CommitTransaction

• Summary: Missing commit() calls

• Priority: 7 / 10

• Severity: Warning

• Category: Correctness

何故いけないのか?

• beginTransaction()から始まるメソッドチェーンは、commit()が呼び出されてから描画処理に入ります

• そりゃcommit()を呼んでなきゃ何も表示されません

• DBのトランザクションみたいなイメージ

こう書きましょう

@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_commit_transaction); getSupportFragmentManager().beginTransaction() .add(R.id.container, new MyFragment()) .commit(); }

Case 3. SharedPreferenceが 保存されない

こんなコード書いてませんか

SharedPreferences pref = PreferenceManager .getDefaultSharedPreferences(this);

SharedPreferences.Editor editor = pref.edit();editor.putLong("now", new Date().getTime());

pref.getLong("now", -1); // -1

SharedPreferencesに保存したはずの データが取れないなあ

CommitPrefEdits

• Summary: Missing commit() on SharedPreference editor

• Priority: 6 / 10

• Severity: Warning

• Category: Correctness

何故いけないのか?

• SharedPreferencesにデータを保存する場合、実際に保存されるタイミングはcommit()のとき

• そりゃcommit()を呼んでなきゃ何も表示されません

こう書きましょう

SharedPreferences pref = PreferenceManager .getDefaultSharedPreferences(this);

SharedPreferences.Editor editor = pref.edit();editor.putLong("now", new Date().getTime()); editor.commit(); // consider using apply() instead

pref.getLong("now", -1); // 1455620005841

おまけ: commit()とapply()

• SharedPreferencesはアプリ内領域のXMLを読み書きすることでデータを永続化している/data/data/[applicationId]/shared_prefs/hoge.xml

Editor XMLMemory Cache

commit()は待つ

apply()は待たない

こう書くとなおよい

SharedPreferences pref = PreferenceManager .getDefaultSharedPreferences(this);

SharedPreferences.Editor editor = pref.edit();editor.putLong("now", new Date().getTime()); editor.apply();

// OR

// if(editor.commit()) { // pref.getLong(“now”, -1); // 1455620005841 // }

Case 4: Toastが出ない

こんなコード書いてませんか

Toast.makeText( this, “this line is passed”, Toast.LENGTH_LONG)

なんでだ、絶対にこの行を通ってるはずなのに! なんでトーストが出ないんだ!!!!

ShowToast

• Summary: Toast created but not shown

• Priority: 6 / 10

• Severity: Warning

• Category: Correctness

こう書きましょう

Toast.makeText( this, “this line is passed”, Toast.LENGTH_LONG).show()

Case 5: なんかこのOKボタン 押しづらくない?

こんなコード書いてませんか

<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@android:string/ok" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@android:string/cancel" /> </LinearLayout>

ButtonOrder

• Summary: Button order

• Priority: 8 / 10

• Severity: Warning

• Category: Usability

何故いけないのか?

• デザインガイドラインに書いてある https://www.google.com/design/spec/components/dialogs.html#dialogs-specs

• 単にダイアログを閉じるだけの「キャンセル」のようなものは左、本来やりたかった操作を続ける「OK」のようなものは右に置く

• 「削除」はネガティブなイメージなので左に置いてしまいがちだけど右

こう書きましょう

<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@android:string/cancel" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@android:string/ok" /> </LinearLayout>

おまけ: 検出条件

1. targetSdkがAPI Level 14以上

• API Level 13まではOKが左で正しかった

2. ラベルが”OK”, “Cancel”, android.R.string.ok, android.R.string.cancelのいずれか

3. 親レイアウトの設定上、並びが明らか

• LinearLayoutかTableRowで、orientationがhorizontal

• RelativeLayoutで、toRightOf, toLeftOfの関係で順序が分かる

• RelativeLayoutで、alignParentLeft, alignParentRightの関係で順序が分かる

Case 6: このアプリ英語化して

こんなコード書いてませんか

<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ユーザーを選択してください" /><ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="犬のアイコン" android:src=“@drawable/ic_dog” /><EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="入力してください" />

日本語が表示されるところを全部探して 英語に書き直す・・・? 1日じゃ無理だよ!

HardcodedText

• Summary: Hardcoded text

• Priority: 5 / 10

• Severity: Warning

• Category: Internationalization

検出対象

• レイアウトXML(res/layout/)

• android:text

• android:contentDescription

• android:hint

• android:prompt

• AndroidManifest.xml

• android:label

• メニューXML(res/menu/)

• android:title

何故いけないのか?

• 文字列リソースに抜き出したほうが総合的にメリットが多い

• Java側で言語情報を切り替えることもできなくもないがかなり煩雑

• リソースファイルの自動切り替えに頼ったほうが遥かに楽

こう書きましょう

<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=“@string/select_a_user“ /><ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription=“@string/dog_icon“ android:src=“@drawable/ic_dog” /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint=“@string/input_please“ />

おまけ

• デモ

• クイックフィックス

• 言語コードごとにstrings.xmlを用意した場合のAndroid Studioの挙動

事例紹介 Interlude: konifar/droidkaigi2016

./gradlew lint

app/build/outputs/lint-results.html

👍

事例紹介 part2. もっと上を目指す人のために

Case 7: アプリが管理していた個人情報が 別のアプリから奪われた!

こんなコード書いてませんか

<!—- AndroidManifest.xml —-> <provider android:name=".provider.MyContentProvider" android:authorities="info.nkzn.mycontentprovider" />

ContentProviderを使うときはマニフェストに 書くんだろ? そんなこと知ってるよ!

ExportedContentProvider

• Summary: Content provider does not require permission

• Priority: 5 / 10

• Severity: Warning

• Category: Security

何故いけないのか?

• 実は<provider>にはexportedというパラメータがあり、デフォルトでtrue

• 外部のアプリからでも content://hogehoge のようなURIでデータにアクセスできてしまう

• Dropboxが2011年にやらかしてたhttp://codezine.jp/article/detail/6286

こう書きましょう

<!—- AndroidManifest.xml —-> <provider android:name=".provider.MyContentProvider" android:authorities=“info.nkzn.mycontentprovider" android:exported=“false" />

※ minSdkVersion, targetSdkVersionが   両方とも17以上なら、デフォルトでfalseになります   http://developer.android.com/intl/ja/guide/topics/manifest/provider-element.html

Case 8: これ、何を入力する欄?

こんなコード書いてませんか

<EditText android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id=“@+id/phone_number" android:hint=“電話番号を入力してください" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/pin" android:hint=“PINコードを入力してください" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/site_url" android:hint=“サイトのURLを入力してください" android:layout_width="match_parent" android:layout_height="wrap_content" />

電話番号を入れたいのに日本語キーボードが 出てきて切り替えるの面倒くさい・・・

何入れたらいいのか わからない・・・

TextFields

• Summary: Missing inputType or hint

• Priority: 5 / 10

• Severity: Warning

• Category: Usability

何故いけないのか?

• ユーザーはhintを見て何を入力する欄なのかを判断するので、無いと不便

• inputTypeを指定すれば適切なキーボードが現れることが多いので、キーボードを切り替える手間が省けて便利

こう書きましょう

<EditText android:hint="メモを入力してください" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id=“@+id/phone_number" android:hint=“電話番号を入力してください” android:inputType=“phone” android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/pin" android:hint=“PINコードを入力してください” android:inputType="numberPassword" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/site_url" android:hint=“サイトのURLを入力してください” android:inputType="textUri" android:layout_width="match_parent" android:layout_height="wrap_content" />

Case 9: やっぱりアイコンは カラフルじゃなきゃ

こんなアイコン作ったことありませんか

色がついててみづらい

色がついててみづらい

IconColors

• Summary: Icon colors do not follow the recommended visual style

• Priority: 6 / 10

• Severity: Warning

• Category: Usability:Icons

何故いけないのか• マテリアルデザインの登場などもあり、ステータスバーや

ActionBarの色はアプリごとに鮮やかに変わるようになった

• アイコンと同系色の背景になったときに見づらくなってしまう

• 通知アイコンは白、アクションアイコンはグレーにしておけば、なんとなく大体の色に合う

• というような話が developer.android.com/design に載っていたような気がするのだけれど、www.google.com/design に移ったときにその辺の話がなくなってしまった気がする

こうしましょう

おまけ

// com.android.tools.lint.checks.IconDetector.java for (int y = 0, height = image.getHeight(); y < height; y++) { for (int x = 0, width = image.getWidth(); x < width; x++) { int rgb = image.getRGB(x, y); if ((rgb & 0xFF000000) != 0) { int r = (rgb & 0xFF0000) >>> 16; int g = (rgb & 0x00FF00) >>> 8; int b = (rgb & 0x0000FF); if (r != g || r != b) {

1pxずつ色を評価していて執念を感じた

Case 10: 文字は正しく使いましょう

こんなコード書いてませんか

<string name="continue_to_read">続きを読む...</string>

3点リーダ(…)の入れ方わかんないから ピリオド3つでいいや

TypographyEllipsis

• Summary: Ellipsis string can be replaced with ellipsis character

• Priority: 5 / 10

• Severity: Warning

• Category: Usability:Typography

何故いけないのか

• フォントによってはピリオドが都合よく…と見えてくれるかどうかはわからない

• ISO-8859-1で…は”&#8230;”として定義されており、各フォントも…が3点リーダとして見やすくなるようにしてくれているはず

• 用意されている文字はちゃんと使おうというスタンス

こう書きましょう

<string name=“continue_to_read”>続きを読む…</string> <!—- または —-> <string name=“continue_to_read”>続きを読む&#8230;</string>

品質管理担当者の方へ: 品質指標として使ってみませんか

エンジニアの勝手な言い分

• Googleが「これやると良くないアプリになるよ」と言っているものを回避しているので、相対的にアプリの品質は上がっていると言っていいのでは

• こんな重箱の隅を突くようなチェックを200項目以上も回避するのはそれなりの経験値がないと難しい

• 正しくAndroid SDKを使えるように気を使いながら開発できているという点は、品質上の成果として計上して、取引先や経営層が評価してもらえると嬉しい

品質指標に使う場合

• 優先度が低いルールは事前に設定でignoreしておく(Bi-

directional Textカテゴリなど)

• 予算やスケジュールに応じて事前に「Fatal, Errorはすべて直す」「Warningは15件以内の検出」などの品質目標を定めておく

• カテゴリ単位での採用・不採用を各プロジェクトごとに定めるのもよいと思います

エンジニアの方へ: abortOnErrorを切らないで

abortOnError, checkReleaseBuilds// build.gradleandroid { lintOptions { abortOnError false checkReleaseBuilds false }}

abortOnError: 危険度がErrorまたはFatalのルールに抵触したらビルドを中断する checkReleaseBuilds: リリースビルド時にFatalのルールに抵触したらビルドを中断する

趣味でやってたり プロトタイプならともかく

お仕事でやるときに falseにするのよくないと思います

本当に対応しないルールだけ ignoreしましょう

droidkaigi2016/app/build.gradle

lintOptions { abortOnError false disable 'InvalidPackage'}

😡

さいごに

• Android Studioがソースコードに黄色いのとか赤いのとか付けてたら、マウスオーバーして何が起きてるか見てね!

• すべてのルールを倒したら胸を張ろう

• Android Lintと俺達の戦いはこれからだ!

ご清聴ありがとうございました

参考文献• lint | Android Developers

http://developer.android.com/intl/ja/tools/help/lint.html

• Improving Your Code with lint | Android Developershttp://developer.android.com/intl/ja/tools/debugging/improving-w-lint.html

• Android Lint - Android Tools Project Site http://tools.android.com/tips/lint

• Writing a Lint Check - Android Tools Project Sitehttp://tools.android.com/tips/lint/writing-a-lint-check

• platform_tools_basehttps://android.googlesource.com/platform/tools/base

• LintOptions - Android Plugin 1.5.0 DSL Referencehttp://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.LintOptions.html