androidlint #droidkaigi

89
Android Lintで正しさ を学ぼう 2015.02.18 DroidKaigi 2016 Yukiya Nakagawa / @Nkzn ROOM:B 13:00-13:50 #DroidKaigiB

Upload: yukiya-nakagawa

Post on 16-Apr-2017

14.306 views

Category:

Technology


1 download

TRANSCRIPT

Page 1: AndroidLint #DroidKaigi

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

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

Page 2: AndroidLint #DroidKaigi

Who are you?

• Yukiya Nakagawa / @Nkzn

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

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

• エンジニア募集中です

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

Page 3: AndroidLint #DroidKaigi

Recent Activity

• C89コミケ

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

• https://techbooster.booth.pm/

• Android Lintネタ書いてました

Page 4: AndroidLint #DroidKaigi

Target

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

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

Page 5: AndroidLint #DroidKaigi

Agenda

• Android Lintとは

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

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

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

Page 6: AndroidLint #DroidKaigi

Android Lintとは

Page 7: AndroidLint #DroidKaigi

Lintとは

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

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

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

Page 8: AndroidLint #DroidKaigi

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

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

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

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

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

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

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

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

Page 9: AndroidLint #DroidKaigi

Javaの場合?

Page 10: AndroidLint #DroidKaigi

Android Lintとは

• Google公式

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

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

• 結構手広い

• 不具合予備軍の検出

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

Page 11: AndroidLint #DroidKaigi

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

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

Security セキュリティ

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

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

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

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

Accessibility アクセシビリティ

Internationalization 国際化・多言語化

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

Page 12: AndroidLint #DroidKaigi

Severity

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

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

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

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

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

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

Page 13: AndroidLint #DroidKaigi

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

• 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

Page 14: AndroidLint #DroidKaigi

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

Page 15: AndroidLint #DroidKaigi

Priority

• 1から10まで

• あまりあてにならない

Page 16: AndroidLint #DroidKaigi

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

Page 17: AndroidLint #DroidKaigi

お作法分かりづらい問題

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

Page 18: AndroidLint #DroidKaigi

Android Way is どこ

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

Page 19: AndroidLint #DroidKaigi

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

Android Lint

Page 20: AndroidLint #DroidKaigi

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

Page 21: AndroidLint #DroidKaigi

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

(Orientation)

Page 22: AndroidLint #DroidKaigi

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

<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>

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

Page 23: AndroidLint #DroidKaigi

Orientation

• Summary: Missing explicit orientation

• Priority: 2 / 10

• Severity: Error

• Category: Correctness

Page 24: AndroidLint #DroidKaigi

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

Page 25: AndroidLint #DroidKaigi

こう書きましょう

<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>

Page 26: AndroidLint #DroidKaigi

Case 2: Fragmentが表示されない

Page 27: AndroidLint #DroidKaigi

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

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

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

Page 28: AndroidLint #DroidKaigi

CommitTransaction

• Summary: Missing commit() calls

• Priority: 7 / 10

• Severity: Warning

• Category: Correctness

Page 29: AndroidLint #DroidKaigi

何故いけないのか?

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

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

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

Page 30: AndroidLint #DroidKaigi

こう書きましょう

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

Page 31: AndroidLint #DroidKaigi

Case 3. SharedPreferenceが 保存されない

Page 32: AndroidLint #DroidKaigi

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

SharedPreferences pref = PreferenceManager .getDefaultSharedPreferences(this);

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

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

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

Page 33: AndroidLint #DroidKaigi

CommitPrefEdits

• Summary: Missing commit() on SharedPreference editor

• Priority: 6 / 10

• Severity: Warning

• Category: Correctness

Page 34: AndroidLint #DroidKaigi

何故いけないのか?

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

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

Page 35: AndroidLint #DroidKaigi

こう書きましょう

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

Page 36: AndroidLint #DroidKaigi

おまけ: commit()とapply()

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

Editor XMLMemory Cache

commit()は待つ

apply()は待たない

Page 37: AndroidLint #DroidKaigi

こう書くとなおよい

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 // }

Page 38: AndroidLint #DroidKaigi

Case 4: Toastが出ない

Page 39: AndroidLint #DroidKaigi

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

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

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

Page 40: AndroidLint #DroidKaigi

ShowToast

• Summary: Toast created but not shown

• Priority: 6 / 10

• Severity: Warning

• Category: Correctness

Page 41: AndroidLint #DroidKaigi

こう書きましょう

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

Page 42: AndroidLint #DroidKaigi

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

Page 43: AndroidLint #DroidKaigi

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

<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>

Page 44: AndroidLint #DroidKaigi

ButtonOrder

• Summary: Button order

• Priority: 8 / 10

• Severity: Warning

• Category: Usability

Page 45: AndroidLint #DroidKaigi

何故いけないのか?

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

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

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

Page 46: AndroidLint #DroidKaigi

こう書きましょう

<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>

Page 47: AndroidLint #DroidKaigi

おまけ: 検出条件

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の関係で順序が分かる

Page 48: AndroidLint #DroidKaigi

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

Page 49: AndroidLint #DroidKaigi

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

<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日じゃ無理だよ!

Page 50: AndroidLint #DroidKaigi

HardcodedText

• Summary: Hardcoded text

• Priority: 5 / 10

• Severity: Warning

• Category: Internationalization

Page 51: AndroidLint #DroidKaigi

検出対象

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

• android:text

• android:contentDescription

• android:hint

• android:prompt

• AndroidManifest.xml

• android:label

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

• android:title

Page 52: AndroidLint #DroidKaigi

何故いけないのか?

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

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

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

Page 53: AndroidLint #DroidKaigi

こう書きましょう

<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“ />

Page 54: AndroidLint #DroidKaigi

おまけ

• デモ

• クイックフィックス

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

Page 55: AndroidLint #DroidKaigi

事例紹介 Interlude: konifar/droidkaigi2016

Page 56: AndroidLint #DroidKaigi

./gradlew lint

app/build/outputs/lint-results.html

Page 57: AndroidLint #DroidKaigi

👍

Page 58: AndroidLint #DroidKaigi

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

Page 59: AndroidLint #DroidKaigi

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

Page 60: AndroidLint #DroidKaigi

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

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

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

Page 61: AndroidLint #DroidKaigi

ExportedContentProvider

• Summary: Content provider does not require permission

• Priority: 5 / 10

• Severity: Warning

• Category: Security

Page 62: AndroidLint #DroidKaigi

何故いけないのか?

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

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

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

Page 63: AndroidLint #DroidKaigi

こう書きましょう

<!—- 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

Page 64: AndroidLint #DroidKaigi

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

Page 65: AndroidLint #DroidKaigi

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

<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" />

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

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

Page 66: AndroidLint #DroidKaigi

TextFields

• Summary: Missing inputType or hint

• Priority: 5 / 10

• Severity: Warning

• Category: Usability

Page 67: AndroidLint #DroidKaigi

何故いけないのか?

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

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

Page 68: AndroidLint #DroidKaigi

こう書きましょう

<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" />

Page 69: AndroidLint #DroidKaigi

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

Page 70: AndroidLint #DroidKaigi

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

色がついててみづらい

色がついててみづらい

Page 71: AndroidLint #DroidKaigi

IconColors

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

• Priority: 6 / 10

• Severity: Warning

• Category: Usability:Icons

Page 72: AndroidLint #DroidKaigi

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

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

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

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

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

Page 73: AndroidLint #DroidKaigi

こうしましょう

Page 74: AndroidLint #DroidKaigi

おまけ

// 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ずつ色を評価していて執念を感じた

Page 75: AndroidLint #DroidKaigi

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

Page 76: AndroidLint #DroidKaigi

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

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

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

Page 77: AndroidLint #DroidKaigi

TypographyEllipsis

• Summary: Ellipsis string can be replaced with ellipsis character

• Priority: 5 / 10

• Severity: Warning

• Category: Usability:Typography

Page 78: AndroidLint #DroidKaigi

何故いけないのか

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

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

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

Page 79: AndroidLint #DroidKaigi

こう書きましょう

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

Page 80: AndroidLint #DroidKaigi

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

Page 81: AndroidLint #DroidKaigi

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

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

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

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

Page 82: AndroidLint #DroidKaigi

品質指標に使う場合

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

directional Textカテゴリなど)

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

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

Page 83: AndroidLint #DroidKaigi

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

Page 84: AndroidLint #DroidKaigi

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

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

Page 85: AndroidLint #DroidKaigi

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

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

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

Page 86: AndroidLint #DroidKaigi

droidkaigi2016/app/build.gradle

lintOptions { abortOnError false disable 'InvalidPackage'}

😡

Page 87: AndroidLint #DroidKaigi

さいごに

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

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

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

Page 88: AndroidLint #DroidKaigi

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

Page 89: AndroidLint #DroidKaigi

参考文献• 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