片側顔面痙攣の手術を受けてから3ヶ月半くらい経った
今でもたまにピクピクと痙攣するが、ポコポコ音が鳴ることはなくなり夜はぐっすり眠れている。 先月頭くらいまでは手術の影響らしき症状もあったが、今は痙攣以外はほとんど感じない。 顔の左側の筋肉も戻ってきたので、( ᐛ👐)パァも今はもううまくできない。
本当に手術を受けてよかった。
症状が薄れると同時に段々手術と入院の記憶が薄れてきたので、忘れないうちにメモしておこうと思う。
入院準備
特に役に立ったもの
手術〜入浴できるようになるまで数日あり、入浴できるようになっても時間や頻度は自由ではない。 ボディシートはあったほうが良いというか必須だった。
シャンプーシートも必須だった。
入浴や洗顔に必要というより入浴スペースに荷物をもっていったりするのに便利だった。 床に物を直置きしなくて済むのも心理的に良かった。
家族と着替えと洗濯物をやりとりするときに便利だった。 取っ手がついていて掛けたりしておけるのが手術直後は良かった。
折りたたみスマホは入院時にはとても便利だった。 PCも持っていったが、ほとんどの作業はこれ1台でできるのでほぼ引き出しにしまっていた。 重いので次に入院するときは首さげできるようにすると良いと思った。
入院中
手術直後
手術自体は寝ている間に終わるので何もわからない。 ICUに移動する前に家族と一瞬会って✌✌したのは覚えている。 手術の後、ICUで一晩過ごすのが本当に辛かった。 自由に水が飲めないし発熱しているしできることがない。 寝て過ごそうにも喉が渇いて30分置きに目が覚めてしまう。 頼むと乾きを癒すために水に浸したスポンジ棒みたいなものをもらえるのだが、全然癒やされなくて乾いていった。 水のことが大好きになった。
病室
病室に戻ってから2日くらいはベッドから出られない状態だった。 夜中も点滴の交換があってあんまり休まらない。 ときどき他の病室から叫び声がするのが結構怖かった。
術後3日目くらいからリハビリがはじまり、同時にカテーテルと点滴が全部終了になった。 お風呂もこのときからやっと入れるようになった。 自分の足でコーヒーもおやつも買いに行けるようになったが、数日寝ている間に体力が無くなっていて常にハアハア言っていた。
ここから数日間はひたすら頭痛との戦いだった。 1日に痛み止めが飲める回数が決まっているので、昼間に薬が切れるタイミングがくるようにして読書で気を紛らわせていた。 夜も頭痛と微熱でうまく寝られないのでアイスノンを借りて脇に当てながら寝ていた。
術後1週間くらい経過して手術跡のホチキスを抜いたあたりから頭痛と熱が少し収まり、左耳の違和感が気になるようになった。 飛行機に乗ったときのような鼓膜が張るような感じと、耳に水が入ったような感じが同時にしていてかなり気持ちが悪かった。 結局この耳の違和感は10月末くらいまで続く。
片側顔面痙攣の手術では、耳の後ろ以外にも頭部固定器の傷が生じる。 僕の場合は額に2箇所、後頭部に1箇所だったが、この傷口からとにかく汁が出る。 額の傷口は絆創膏を貼っていたが、貫通してジワジワ垂れて出てくるのでとても困った。 後頭部の傷も枕を汚すのでかなり気が滅入った。 後頭部の傷は退院前に塞がったが、額の傷は退院後もしばらく汁が出て困った。 耳の違和感と頭痛は残るものの、検査に異常はないので10日間で予定通り退院。
退院後
僕は愚かにも退院したらフルパワーに戻っていると思っていたのだが、全然フルパワーではない。 頭は痛いし耳の違和感が強くて歩くとふらつくし、退院後1ヶ月くらいはかなり弱っていた。 それでも手術前と比べると夜はちゃんと眠れるようになったので、本当に手術して良かったと思う。
退院後もずっと耳に違和感があり、ちゃんと治るのか不安だったのだが、退院後2ヶ月くらい経ったあたりで驚くほど急に治ってしまった。 退院直後に後頭部の固定器痕の周りが円形にハゲたが、こちらも2ヶ月後くらいに髪の毛が生えだしていまではほぼわからない。 大体退院から2ヶ月くらいかかって元の状態になるんだな〜というのが今回の知見だった。
手術を受けて本当に良かった。 おかげで年末年始もぐっすり眠れそうです。
片側顔面痙攣の手術を受けることにした。
1年ほど前から片側顔面痙攣という病気になった。 顔の左側がピクピクと痙攣して、意図せず顔を顰めてしまう。 脳の近くの血管がなにかの拍子で神経を圧迫するようになるとこのような症状が出るらしい。 だいぶ症状に波がある病気で、調子が良いときは全然気にならないが、調子が悪いときには耳の中で痙攣にあわせてポコポコ音が鳴ることもある。 耳の奥の筋肉が痙攣すると、鼓膜のあたりが動いてポコポコ言うらしい。 夜中にポコポコ鳴りだすと最悪で、顔の違和感とポコポコ音で全然寝られない時もあった。
ずっと手術を考えていたわけではなく、症状が出始めて8ヶ月くらいはボトックス治療をしていた。 ボツリヌス毒で筋肉を麻痺させ、痙攣を中和する感じの治療法なのだが、これははじめのうちはかなり体験が良かった。 治療し始めのころ、痙攣が始まるな~という予兆を感じたあと一瞬で痙攣が引く感覚には感動を覚えた。 しかし僕の片側顔面痙攣は進行性だったらしく、段々口の周りも引き攣るようになってきた。 こうなるとボトックス治療は難しくなっていく。
口元の痙攣を抑えるために口の周りにボトックス注射を打つと、口の周りが麻痺して口をまっすぐ開きにくくなってくる。 僕は片側だけボトックス注射を打っていたので、口を開こうとすると ( ᐛ👐)パァ みたいな感じで口が斜めに開くようになっていった。 いまは手術前でボトックス治療を止めているのでだいぶ開くようになったが、4月頃の僕はおそらく知人の中でも一番うまく ( ᐛ👐)パァ ができていたと思う。 一番強めにボトックスを打っていた時は頑張らないと左側の唇を歯より上に持ち上げることができず、厚めのカツサンドを食べようとすると唇を巻き込むようになっていた。 人前で食事するのも避けるようになってしまい、この治療を続けるのは無理だなと思い始めたので手術を検討するようになった。
手術は後頭下開頭微小血管減圧術というもので、耳の後ろあたりに小さい穴を開けて脳幹あたりで神経を圧迫している血管を移動させるらしい。 いかにも根本解決できそうな治療法で、効果はかなり期待できそう。期待できそうというか、期待している。 片側顔面痙攣自体は命に別状がない病気で、手術を受けるかどうか悩む人も多いらしいが、個人的にはこの病気と付き合っていくのは体験が悪すぎて無理だった。 耳元(というか耳の中)で不定期にポコポコされるのが治るなら、頭蓋骨に穴をあけて脳に触れてでも治したい。 なんといっても、頭蓋骨の穴はチタンプレートで補強して埋めるらしい。 チタンは一番好きな金属なので自分の骨と結合させられる機会ができて少しうれしい。自分に埋め込まれる前のチタンを見られないのが残念だ。
手術は来月の予定だが、はやく手術を受けたくてこの日記を書いている。
BottomNavigation のタブごとに Fragment の遷移履歴残すやつ
Fragment を使った画面遷移メモ を書いたときに後から追記しようと思って完全に忘れていた。
言葉ではわかりにくいのですが、以下のような画面遷移を実現するやつです。
基本的にAndroidアプリではタブごとに深い階層を持たずにActivityで遷移していったほうが良いと思っていますが、どうしても必要な場合には FragmentManager の primary navigation fragment を利用するとシンプルに実装できます。
サンプルコードは以下に置いてあります。 github.com
はじめに
前提として、 FragmentManager の以下の仕組みを理解しておく必要があります
detach()
https://developer.android.com/reference/android/support/v4/app/FragmentTransaction#detach
detach()
は Fragment を UI から取り外すことができるメソッドです。このとき、対象FragmentにネストされたFragmentも同時にUIから取り外されます。
UIから取り外された Fragment ではライフサイクルが onDestroyView()
まで進み、Viewが破棄されます。
重要なのは、 detach()
された Fragment やネストされた Fragment の savedInstanceState はそのまま残るということです。
childFragmentManager のバックスタックなどの情報も savedInstanceState に保存されるため、 detach() されたFragmentはネストされたFragmentの遷移履歴を残したままUIから非表示にできます。
detach()
した Fragment は後述する attach()
で再度UIに追加することができます。
attach()
https://developer.android.com/reference/android/support/v4/app/FragmentTransaction#attach
detach()
された Fragment を再度 UI に追加することができます。このとき、ネストされたFragmentも含めすべてのViewが再生成されます。
また、detach()
でも説明したとおり、ネストされたFragmentの遷移履歴も復元されます。
つまり、タブごとに Fragment を用意してその childFragmentManager でアプリ内の画面遷移を行い、タブ切り替えをタブFragmentの detach()
/ attach()
で行えば遷移履歴を残したままタブ表示を切り替えることができるようになります。
ただし、 Activity が持つ FragmentManager はそのままでは popBackStack()
したときに childFragmentManager のバックスタックを見てくれないので、後述の setPrimaryNavigationFragment()
を組み合わせる必要があります。
setPrimaryNavigationFragment()
setPrimaryNavigationFragment()
は画面遷移を処理するFragmentを指定するメソッドです。
対象Fragmentを setPrimaryNavigationFragment()
しておくことで popBackStack() したときにそのFragmentの childFragmentManager のバックスタックを戻れる ようになります。
さきほど説明したタブ切り替えの際、 attach()
と同時に setPrimaryNavigationFragment()
しておくことで バックボタン押下時にタブFragmentの childFragmentManager の popBackStack() を呼び出してくれます。
バックボタンを押した際の挙動まとめ:
- PrimaryNavigationFragment.childFragmentManager.popBackStack()
- PrimaryNavigationFragment が設定されていなければ 2. へ
- バックスタックがなければ 2. へ
- FragmentActivity.supportFragmentManager.popBackStack()
- バックスタックがなければ 3. へ
- アプリ終了
サンプル解説
ここまでで説明したとおり、実装方法はシンプルです。
- タブごとに Fragment (TabFragment)を生成し、 childFragmentManager を管理させる
- タブ切り替えの実装を各TabFragment の detach/attach によって行う
- コンテンツ間の画面遷移に TabFragment の childFragmentManager を利用する
- Fragment.getFragmentManager は自身の遷移に利用された FragmentManager を返す
- TabFragment で最初のFragmentを追加する際に利用した childFragmentManager がそのまま利用できる
- Fragment.getId は自身の遷移に利用された layout id を返すを返す
- TabFragment で最初のFragmentを追加する際の id がそのまま利用できる
- Fragment.getFragmentManager は自身の遷移に利用された FragmentManager を返す
タグの切り替え、および初回の TagFragment の初期化
private fun changeBottomNav(tag: String) { // 各タブごとの TabFragment を検索 val homeFragment = supportFragmentManager.findFragmentByTag("home"); val dashboardFragment = supportFragmentManager.findFragmentByTag("dashboard"); val notificationFragment = supportFragmentManager.findFragmentByTag("notification"); // 遷移先の TabFragment を設定 val targetFragment = when (tag) { "home" -> homeFragment "dashboard" -> dashboardFragment "notification" -> notificationFragment else -> throw IllegalArgumentException() } supportFragmentManager.beginTransaction().apply { supportFragmentManager.primaryNavigationFragment?.let { // すでに primaryNavigationFragment が set されている場合は detach // detach された Fragment の View は破棄されるが childFragmentManager の backStack などは保持される detach(it) } if (targetFragment == null) { // 初回のみタブごとの TabFragment を作成して add する val primaryNavigationFragment = TabFragment().apply { arguments = Bundle().apply { putString("title", tag) } } add(R.id.contents, primaryNavigationFragment, tag) // TabFragment に設定することで popBackStack() する際に TabFragment の childFragmentManager の backStack があればそれを pop するようになる setPrimaryNavigationFragment(primaryNavigationFragment) } else { // すでに TabFragment が存在する場合は attach attach(targetFragment) // TabFragment 再設定 setPrimaryNavigationFragment(targetFragment) } }.commit() }
TabFragment の初回表示時に childFragmentManager と layout id を利用して初期表示Fragmentを設定
// 初回のみ初期表示Frgmentを設定 if (childFragmentManager.findFragmentById(R.id.tab_contents) == null) { childFragmentManager.beginTransaction().apply { val firstFragment = ContentsFragment() replace(R.id.tab_contents, firstFragment) }.commit() }
コンテンツ間の遷移で TabFragment の childFragmentManager および layout id を利用する
// Fragment.requireFragmentManager() は自分自身の遷移に利用された FragmentManagerを返す // (ここでは PrimarynavigationFragment の childFragmentmanager ) requireFragmentManager().beginTransaction().apply { val nextFragment = ContentsFragment().apply { arguments = Bundle().apply { // 現在のページ数に +1 して次のページへ putInt("key", currentPage + 1) } addToBackStack(null) } // Fragment.getId() は自分自身の遷移に利用された id // (ここでは R.id.tab_contents) replace(id, nextFragment) }.commit()
これだけで BottomNavigation のタブごとに Fragment の遷移履歴残すやつ の完成です。
注意点
ViewPager などでコンテンツFragmentのchildFragmentManager を利用してさらにFragmentをネストする場合、各Fragmentの getFragmentManager が TabFragment.childFragmentManager と一致しなくなります。
その場合は getActivity().supportFragmentManager().getPrimaryNavigationFragment.childFragmentManager()
のような呼び出しで TabFragment.childFragmentManager にアクセスするしかありません。
Kotlin の場合は以下のような拡張関数を生やすと便利かもしれません。
fun Fragment.requirePrimaryNavigationFragmentManager() =
requireActivity().supportFragmentManager.primaryNavigationFragment?.let {
it.childFragmentManager
} ?: requireFragmentManager()
Fragment を使った画面遷移メモ
最近 Fragment 周りの実装を追う機会があり、 Android Developers の FragmentTransaction ページは各メソッドの説明が若干分かりづらいと思ったので自分用のメモとして主要なメソッドをまとめてみることにしました。
add
名前の通り Fragment を追加するときに利用します。
特定の container(ViewGroup) に Fragment を追加する場合の他、UIを持たない Fragment を追加する際にも利用します。
DialogFragment#show()
も内部的には FragmentTransaction#add() を呼び出しています。
注意点として、画面遷移の際に add()
を利用した場合、遷移前の Fragment に重ねて表示されるため、別途remove()
する必要があります。
通常は画面遷移の用途としては add() と remove() を同時に行う replace() を利用するのが良いでしょう。
remove
指定したFragmentを削除するときに使います。
削除されたFragmentはActivityとの関連付けが解除され、 onDestroyView() -> onDestroy() -> onDetach()
というライフサイクルを辿ります。
対象のFragmentに対する参照を自前で持っていない場合、Fragmentインスタンスは自動的に破棄され、再表示することはできません。
ただし、 remove()
操作の大きな特徴として、同時に addToBackStack()
されている場合には Activity との関連付け解除が行われず、 onDestroyView()
までしか実行されません。
これにより、遷移前のFragmentのインスタンスを維持したまま画面の完全な切り替えを行うことが可能です。
replace
replace()
は remove()
と add()
を同時に行うメソッドで、主に画面遷移のために利用します。
同時に addToBackStack()
を呼んでいない場合、遷移元の Fragment は Activity との関連付けが解除されますが、呼んでいる場合は View だけが破棄されている状態になり、同一Fragmentを再利用可能です。
ちなみに、対象の container に Fragment が何も表示されていない状態でも replace()
によって画面遷移する事は可能なので、初期表示画面への遷移も replace()
で実装することは可能です。
稀に初期表示画面を add()
で実装して Activity 再生成のたびに初期表示画面が追加されていくアプリがあるので、画面遷移では replace()
に統一してしまっても良いかもしれません。
addToBackStack
指定したトランザクションをコミットした後で元に戻したい場合に利用します。
バックスタックに積まれたトランザクションは back ボタンを押下することで自動的に pop され、トランザクションで指定されていた動作は元に戻ります。
(ただし、自動的に pop されるのは Activity が持つ FragmentManager のバックスタックに積まれた場合のみです。 getChildFragmentManager()
を利用した場合は後述します)
基本的には replace()
あわせて利用することで画面遷移を戻れるようにするために利用します。
detach(), attach()
detach()
を利用すると Activity に追加済みの Fragment をUIから取り外すことができます。
具体的には、 remove()
を addToBackStack()
付きで呼び出した場合と同じく、 onDestroyView()
までライフサイクルが進んだ状態になり View が破棄されつつFragment自体は残る状態になります。
ややこしいですが、 Fragment のライフサイクルである Fragment#onAttach()
、 Fragment#onDetach()
とは関係ありません。
attach()
は一度 detach()
されたFragmentのUIを再度生成して表示する場合に利用します。
なかなか使いどころがないと思うかもしれませんが、 Fragment が持つ OptionsMenu やネストされた Fragmentなども View と同時に破棄/再生成されるため、タブの切替処理などで有用です。
FragmentTabHostやFragmentPagerAdapterではこの仕組を利用して表示Fragmentの切り替えを行っています。
show(), hide()
Fragmentの表示状態を切り替えます。
detach(), attach() と違い、Viewの破棄や再生成が行われず、ライフサイクルも変化しません。
Fragment側のフラグで管理しているため、hide()
したFragmentをremove() -> add()
し直した場合でもView要素は表示されません。
OptionsMenu も同時に制御されて便利ですが、View自体は生成されたままなので画面遷移の多くをこの仕組で行う場合はメモリ消費に注意が必要です。
setPrimaryNavigationFragment()
setPrimaryNavigationFragment は v26.1.0 で追加された比較的新しいメソッドです。
このメソッドを呼び出すと、これ以降のbackボタン操作において指定したFragmentが childFragmentManager の popBackStack()
を処理するようになります。
- fragmentA に遷移(replace)
- fragmentA の childFragmentManager で childFragmentB に遷移(replace & addToBackStack)
- fragmentA の childFragmentManager で childFragmentC に遷移(replace & addToBackStack)
という遷移を行う時、 1.
の replace()
と同時に setPrimaryNavigationFragment(fragmentA)
を呼び出しておくことで childFragmentC -> childFragmentB -> fragmentA
というバックスタックの解決を backボタンだけで行うことができて非常に便利です。
特にタブ実装などで複雑なFragmentの入れ子を管理する場合に detach()
, attach()
と組み合わせて利用することになりそうです。
commit(), commitNow()
どちらのメソッドも設定中のトランザクションを実行します。
commit()
と commitNow()
の違いは即時実行されるかどうかですが、 どちらの場合でも onSaveInstanceState()
以降に呼ばれた場合は IllegalStateException
が発生するので非同期処理から呼び出す場合には注意が必要です。
commitAllowingStateLoss(), commitNowAllowingStateLoss()
どちらのメソッドも設定中のトランザクションを実行します。
commit()
, commitNow()
と違い、 onSaveInstanceState()
以降に呼ばれた場合でも IllegalStateException
を発生させません。
state
は Fragment のsavedInstanceState
ではなく、Activity側のstateとして保存されるこのコミット自体を指していることに注意してください。
このメソッドを使って onSaveInstanceState()
以降に行った操作は Activity に保存されません。
何らかの理由でActivity自体が再生成された場合、このメソッドによって行われた操作は復元されなくなります。
Android開発をする上で知っておいてほしいなと思うこと 2
上記の記事の続き。 開発時に引っかかりがちないくつかの注意点と、リリース時に知っておいたほうが良いと思うことについてまとめてみる。
開発時の知見
主にチーム開発時の注意点と標準ツールの活用、よく嵌りがちな落とし穴について。 前回記事の アプリの実装について でも少し触れていたが、もう少し広い範囲について必要だと思うことを書いてみた。
チーム開発の注意点
他の開発者と開発環境を揃えることで開発を効率化できる他、他のメンバーに自分の変更による差分をチェックして貰う必要がある。 また、Android開発者ではない人に対する技術的な説明もできたほうが良い。
- ビルド設定を整備する
- productFlavors, buildTypes でビルド設定を定義しておくと共通言語化できて開発時に役立つ
- よくある例: productFlavors で本番/開発の環境切り替え、 buildTypes で proguard の有無や署名設定などを切り替える
- ビルド設定ごとに applicationId を変更すると共存できるようになる
- その際アイコンやアプリ名も変更しないと区別がつかなくなるので注意する
- リソース定義について共通認識を作っておく
- リソースで提供できるコンテンツの種類、代替リソースの提供方法について人に説明できるレベルで知っておく
- 特にスタイルとテーマについて予めルールを作っておけると良い
- 具体的な方法論は公式ドキュメントには書いていないので先人の知恵を活用する
- デザイナーとのやりとりについて考える
- 「サイズ指定はdpで」だけでは不十分で、アプリが想定している画面サイズ(dpでみた解像度)を説明する必要がある
- 360dp以外の画面が存在すること、それらの端末で拡縮する部分についてわかるような指定をしてもらうようにする
- Vector画像リソースが利用できることを把握する
- 9patchの拡大ルールについて把握する
- 意外と注意点が多く、向かない用途もある
- 変更差分ごとにレビュアーが手元で確認できるようにする
- 開発者がapkをインストールして見せに行く方法では時間効率が悪すぎる
- DeployGate などでPRごとに配信するように設定すると良い
- 開発用サーバの用意などで常にレビュアーが変更内容について確認しやすいようにする
raw/
やassets/
を利用したデバッグ用の画面の用意なども検討する- コードを変更せずにデバッグメニューでAndroidアプリの動作を変更する - Qiita
標準ツールの活用
Android Sdk(とそこからダウンロードできる各種ツール)とAndroid Studioに付属しているデバッグ機能について。
- 最新機能のチェック
- コマンドラインツール
- APK Analyzer
- APKに含まれているリソースの確認やAndroidManiest定義の確認が簡単にできる
- Android Profiler
- CPU、メモリ、ネットワークなどのリソース消費がリアルタイムでわかる
- Layout Inspector
- 実行中アプリのレイアウト階層をみる時に使うやつ
- Hierarchy Viewer
- Layout Inspector と似たツール(こちらが古い)
- レイアウト描画時のパフォーマンス計測をしたいときに使う(そのうち Layout Inspector に統合されそう)
標準ツールではないがstetho などのデバッグ用ツールについても知っておくとより良い。
よく嵌まる落とし穴
- Auto Backup
- uses-feature の暗黙的設定
- 一部の uses-permission 設定は自動的に対応したデバイス機能の uses-feature 設定を追記する
- 通常あまり問題にならないが、AndroidTV向けの実装を同一apkに同梱する場合などに問題が起きる
リリース時に知っておいたほうが良いこと
リリース時にチェックしておくべき項目やアプリの公開に関する設定項目のうち知っておいたほうが良い項目についてまとめる。 基本的にほとんどの内容は Android Developers に書いてあるが、Playコンソールの新機能についてはPlay Console ヘルプ(英語版)のほうが詳しい時もある。 Playコンソールに関して迷ったり疑問に思ったらまずは公式のヘルプをあたってみると良い。
- Distribute on Google Play
- リリースに関する基本的な項目は Distribute 以下にある
- アプリのリリース前に一度は見ておいた方が良い
- Core app quality
- アプリが満たしておくべき品質のリスト
- 特に戻るボタンの動作の一貫性などは実装時から気をつけておいたほうが良い
- Launch checklist
- アプリ公開時に必要な項目のチェックリスト
- 必須項目ばかりではないが、知識として知っておいたほうが良いこともある
- Release updates progressively
- 段階的リリースでは公開率を段階的に上げるだけでなく問題のあるリリースを途中で中止することができる
- 問題なければ再開することもできる
- モバイルアプリは簡単にロールバックできないので、公開時のリスクを抑える方法として有用
- 段階的リリースでは公開率を段階的に上げるだけでなく問題のあるリリースを途中で中止することができる
- Run alpha and beta tests | Android Developers
- アルファ/ベータリリースについての説明
- インストールまでにユーザー操作が必要なため若干利用し辛い印象がある
- 後述のリリース前クラッシュレポートとあわせて一般向けではなく社内向けの開発版配信として使うのも良いかもしれない
- Use pre-launch and crash reports
- アルファ/ベータリリースとしてapkをアップロードすることでリリース前クラッシュレポートを利用できる
- Firebase Test Lab と同様のものだが、リリースフローの一貫に組み込むことができるのは便利
- Engage
- より利用されるアプリにする方法のまとめ
- どれでも利用できるというわけではないが、リンク先も含めて知識として有用
- Grow
- ユーザー数を増やす方法のまとめ
- Developer stories
- 成功者のストーリー一覧 -InstantApps でドーンみたいな話だけでなくPlayコンソールの細かいA/Bテストを繰り返したという話も乗っていてわりと実用的
まとめ
これで当初書きたかった内容は大体書いた。 ここまで書いてきた内容を振り返っても Android Developers は開発〜リリースまでを支えてくれる優秀な公式ドキュメントだと思えるので、安心して頼っていけそう。
Android開発をする上で知っておいてほしいなと思うこと
現在の Android Developers の情報は非常に充実していて、Developer Guides を順に読み進んでいくだけで開発に必要な知識とGoogleが想定している(であろう)最も基本的な実装を学ぶことができる。 特にこの「基本的な実装」というものが重要で、これを知っておかないと開発者間の意思疎通がスムーズに行えなかったり、そもそも気をつけておくべき注意点を見落としがちになってしまう。
とはいえ、今の膨大な公式ドキュメントをただ読めというのは厳しいので、Android開発をする上で最低限理解しておいてほしい(と僕が思っている)事柄と、それについて知ることができるドキュメント類についてまとめてみることにする。
2018/03/25 : リリース周りについて別記事に追記した。
公式ドキュメントの重要ページ
公式ドキュメントと言った場合、 Android Developers を指す。 最近は日本語化されている記事も増えてきているが、日本語ページは稀に内容が古かったりするので人からリンクを渡された場合は念のため英語ページも見ておいたほうが良い。
Android SDK Search という Chrome拡張を入れておくと Android Developers の検索をショートカットできたりクラスリファレンスからソースコードに飛べたりして非常に便利でオススメ。
最新の情報や問題を追いたいのであれば公式サイトだけでなく以下のサイトも見たほうが良い。
- Android Developers Blog
- 公式の開発者ブログ
- 新機能や重要な変更点について説明されることが多い
- android Git repositories - Git at Google
- Android Studio Release Updates
- Android Studio の更新情報
- Android, the world's most popular mobile platform | Android Developers
- Android プラットフォームの解説ページ
- OSバージョンごとの差分がまとまっているので非常に重要(後述)
- Recent Support Library Revisions | Android Developers
- Support Library の更新情報
- 少なくともバージョンを上げる際には Important Changes は必ず目を通したほうが良い
- Android Issue Tracker の情報
- 去年 IssueTracker が変更されてコンポーネントごとに整理された
- 何か問題を見つけて、それがOSやSupport Library側の問題だと感じるときはググるだけでなく Issue Tracker も見たほうが良い
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 以上の端末をタブレットとみなす
- スクリーンサイズが large, xlarge 以上の端末をタブレットとみなす
- おすすめしない
- xlarge はほぼ確実にタブレットなのだが、 large はタブレットとは断言されず
640dp x 480dp and bigger
となっていて非常に歯切れが悪い - sw480dp とほぼ等しい設定になるが、411dp(最近増えているスマホ幅)と480dp では近すぎ、480dp と 600dp では遠すぎてレイアウトの境目にし辛い
- <compatible-screens> に記載することでインストール端末から除外することができるが、ファブレットのような例外を設定できない
- Playストアの端末カタログから除外
- もし区別の目的が「インストールさせないこと」である場合、配信設定で除外することができる
- <compatible-screens> に記載するよりは細かいコントロールができるが、アプリだけでなくPlayコンソール側の設定も把握しておく必要があり運用が面倒になりがち
個人的な気持ちとしては、「タブレットをサポートしない」という判断は思ったほどコストが下がらない上に考えることが複雑なのであまりおすすめしない。
特に「画面回転を考慮したくない」という理由でタブレットを除外したいという意見が出ることがあるが、画面回転でクラッシュするアプリは何かしらライフサイクル周りの処理をミスっているのでどのみちクラッシュする可能性が高い。
タブレットレイアウトに対応するのが大変という意見もあるが、320-480dp のスマートフォンサイズまで全幅表示に対応し、それ以上の幅になった場合は中央寄せにでもしておけばそこまで手間ではない。
以前は android:maxWidth
がこのような用途に使えなかったため不便だったが、現在はConstraintLayoutに maxWidth
の設定ができるため、簡単に設定できる。
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 |
---|---|
もちろんデザイナーの判断でこういった余白の使い方がダメな場合もあるが、この程度の実装で回避策が取れることは把握しておいたほうが良いと思う。
追記: タブレットや回転をサポートした場合にテスト工数も上がるのではという懸念に関してはその通りで、ちゃんと工数を積む必要がある。 ただしきちんとボタン押下イベントからのメソッド呼び出しなど単体テストが書かれていればスマートフォン/タブレットでの動作はそこまで違いがないし、表示差異は自分で手動テストしなくても Firebase Test Lab やPlayコンソールの リリース前テスト を利用すればいろいろなタブレットでの表示結果をスクリーンショット付きでテストすることができる。これらの仕組みはログインが必要な画面でもちゃんと設定すればログインしてくれるので、早い段階で導入できれば画面構成のチェックなどはかなり楽になるはずだ。
画面実装について
もともと用意されている画面部品に沿った実装にすることで工数を削減でき、システムUIと一体感のある画面になる。 Android開発者はデザイナーに対して「Androidの標準的な画面実装はこういうもの」と説明できなければいけないと思っているので、基本的なUI部品について知っている、あるいは説明されているページの存在を覚えておく必要がある。
具体的には以下のようなページとその内容を知っていてほしい。
- ConstraintLayout
- ConstraintLayout を使うと描画領域に応じた再配置・リサイズが非常にやりやすくなる
- 反面、Viewサイズではなく他のViewとの関係性によってViewを配置するため、デザイナーに画面構成の指示方法について慣れてもらう必要がある
- ConstraintLayout で画面実装ができ、画面デザインの指定方法についてデザイナに説明できるレベルまでは達していてほしい
- FlexboxLayout
- Design Support Library に入っているView全般
- 記述されていないものでも TextInputLayout など重要なものが多い
- Material Design
- Material Design全体の仕様なので Android だけに限らないが、Androidの標準実装として知っておく必要はある
- Design Support Library は Material Design の表現を目的としているので、Material Designに従うことで簡単に再現できるものもある
また、画面実装をする上で dp, dpi と alternative resource の仕組みについては他の人に説明できるレベルで理解していてほしい。
アプリの実装について
○○アーキテクチャや○○ライブラリを使っていればOKというものはないので自分やチームがテンション上がるものを使っていれば良いと思う。 とはいえ、Androidに関しては Espresso でのテストが増えていくと厳しいのでとにかく UnitTest 可能な部分を増やせる構造にしておいたほうが良い。既存のアーキテクチャを流用する場合、テストの書き方がわかる(または自分で理解できている)ものを採用しないと後々テスト書かなくなるので要注意。 Architecture Components は ドキュメントにテストに関する記述があり、 サンプルコードもじわじわ増えているようなので、段々導入しやすくなっていくのかなという気がしている。
その他のよく陥りがちなところ、把握しておいてほしい注意点については雑にまとめる
- 非同期処理+画面遷移 の組み合わせは難しい
- 何らかの非同期処理の結果を受けて Fragment を遷移させる実装をすると IllegalStateException で死ぬ
- 可能なら遷移後の画面で非同期処理を行うようにするか、ライフサイクルに応じて処理を遅延させると良い
- LiveData を利用するとコンポーネントがactiveなときだけ変更が通知されるのであまり意識しなくてよくなる
- FragmentからActivity/Fragment へのコールバックは難しい
- 操作結果をコールバックしたいだけなら Activity として実装して
startActivityForResult()
させるほうが良い - Fragment で実装する場合はコールバック用の Interface を定義して呼び出し元Activity/Fragment に実装させるものがシンプル
- 呼び出し元の指定は targetFragment を利用可能
- 操作結果をコールバックしたいだけなら Activity として実装して
- バックグラウンド処理の実装は公式ドキュメントをしっかり読んでからやる
- バックグラウンド処理はOSの更新で挙動変更が入りやすいので、なるべく公式の方針に従うのが吉
- minSdkVersion や 仕様面で許されるなら JobScheduler、定時に実行する必要があれば AlarmManager
- Firebase JobDispatcher も実行条件を満たせるなら検討して良さそう
- 一意な識別子について理解する
- Android でインストール/アンインストールを跨いだ端末識別子は広告IDを利用するか、アプリ外の領域に保存しておくしかない
- 生成したUUIDなどを保存しておく場合、Auto Backup によって保存されてよいかどうか検討する
アプリのセキュリティについて
埋め込み値
Android ではapkに埋め込んだ値はどのような難読化をしたとしても完全に安全ではない。ただし、文字列リソースや定数としてわかりやすい名前で平文埋め込みしてしまうと簡単に解析できてしまうので避けたほうが良い。 一番良いのはそもそもアプリ内に固定値を埋め込むような実装を避けることなので、なるべくこういった実装をしなくてすむようにしたい。
どうしてもやる場合は以下のいずれかを選択することになりそう。
- 何らかの複合処理が必要な形でコード内に埋め込む
- ソースコードを読まないと解析できないようになる
- 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ストアからアプリをインストールさせないための仕組みで、こちらは特に実装が不要なので最初から利用しても良いかもしれない。
メモ
簡単なまとめにするつもりだったが思いの外長くなってきたので一旦ここで投稿する。 まだいくつか出てきそうなので思いついたら追記する予定。
これまでに発売されたAndroid端末のdp解像度まとめ
はじめに
今年のGooglePlayコンソールのアップデートにより端末カタログという機能が追加されました。 これは以前から存在していた配信対象から特定端末を除外する機能に、端末の詳しいスペックを閲覧・検索する機能が追加されたものです。
GooglePlayコンソールのアプリ管理画面からリリース管理→端末カタログで遷移してアクセスできます。
この端末カタログはDPI値は表示できて便利なのですがDP解像度は記載されていないため、イレギュラーswdp端末を探す目的には若干不便でした。 そこで、端末カタログによって得た大量のデータからGoogle Play コンソールが認識している全端末のdp解像度まとめを作ってみました。
Android端末カタログ - Google スプレッドシート
作り方
端末カタログはAPIなどが用意されておらず、ページ内のリクエストなどをアレして端末情報のリストを探しました。
端末カタログにアクセスした際、1.6MBくらいのjsonレスポンスを受け取っている箇所があり、それが端末カタログの中身になります。
このJSONファイルはほとんどの key
が 1 とか 2 なので頑張って解析する必要があります。
以下が端末1件分のデータです。
{ "1": "NuAns/neoreloaded", "2": { "1": "NEO [Reloaded]" }, "3": [ { "1": { "1": "NEO2", "2": [ { "1": 1080, "2": 1920, "3": 420, "5": "2991501312", "6": "3072", "7": 25, "8": 196610, "9": 2 } ], "4": "Qualcomm", "5": "MSM8953", "6": "NuAns", "7": [ "NEO [Reloaded]" ], "8": true, "9": "https://lh3.googleusercontent.com/HLh04wdpYNiMf3WyzBUgJzbcvDb7ZxaMYHOy7Wpp7FaXbeGLbwPakW7MgKpaTns83MmU0FuPAxn_", "11": 1, "12": { "1": 4, "2": [ 4, 5, 6 ] } }, "2": { "1": true } } ] },
同一端末であってもOSバージョンの更新などによって別物扱いできるようになっていて、OSアップデートによるデフォルトDPIの変更などもキャッチできそうです。 このあたりはさすがGoogleという感じですね。
解像度やDPIなどの値は特徴的なのですが、フォームファクタ情報などはいろんな端末の詳細ページを見比べながらアタリをつけました。 残念ながら端末発売日や液晶サイズがなく、発売日での絞込やDPI値の乖離チェックができないのが残念です。 液晶サイズに関しては今後のアップデートでぜひ追加してほしいですね。
調査していく途中で、Android端末が スマートフォン、タブレット、ウェアラブル、TV、PC、STB、Auto、その他
という区分にわけられていることがわかったのですが、swdpで区分されているわけでもなく、外見による分類?という感じでした。
一部のファブレットやTV/PCなど分類が難しそうなものもあり、誰がどうやってフォームファクタを判定しているのか非常に気になりました。
このJSONファイルをそれっぽく並べ替えてcsvに変換し、スプレッドシートに突っ込んだものがこちら)になります。
雑感
端末カタログのデータを集計している途中、ノイズがかなり多いのが気になりました。 製造メーカーが不明な端末(OSバージョンによる重複があるため、厳密に端末数ではない)だけで5000台以上あり、本当に世に出ている端末なのかわからないものも多いです。 また、端末の解像度が一桁だったり明らかにおかしいものもあり、開発中の端末かなにかでBuild.MANUFACTURERが正しく入力されていないものも拾われているのでは…という気持ちです。
謎の端末が多く見つかる一方で、国内で流通している端末が見つからないこともあります。
ヘンテコDP解像度で有名な MediaPad T2 7.0 Pro は PLE-701L
という型番ですが、これは名前、型番のどちらで探しても見つかりませんでした。
代わりに Huawei/华为揽阅m2青春版7_0
という端末(型番 hwple703l
) という同一画面解像度で同一DPI端末があることがわかり、国内モデルはこれのローカライズモデルかな?と思ったりしました。
もう少し情報が増えてくれれば絞込による謎端末探しが捗りそうなので、Googleさんには頑張ってほしいですね。
スマートフォンとタブレットのswdp分布を見てみましたが、やはりスマートフォンでは 320dp, 360dp
、タブレットでは 600dp, 800dp
に大きく偏っていました。
ただし、スマートフォンでは 411dp, 480dp
といった情報量の多い端末もそこそこ増えてきているようで、今後はAPIレベルによるswdp分布などもみたほうが良いかな?という気がしました。
タブレットでは768dp端末が600dp,800dpに次いで多くなっていて、 アスペクト比 4:3 端末も意識する必要がありそうです。
今後GooglePlayコンソールの機能が拡張されてスペック検索やdp解像度のチェックができればスプレッドシートも不要になりそうですが、それまでは定期的に更新しようと思っています。