片側顔面痙攣の手術を受けてから3ヶ月半くらい経った

今でもたまにピクピクと痙攣するが、ポコポコ音が鳴ることはなくなり夜はぐっすり眠れている。 先月頭くらいまでは手術の影響らしき症状もあったが、今は痙攣以外はほとんど感じない。 顔の左側の筋肉も戻ってきたので、( ᐛ👐)パァも今はもううまくできない。

本当に手術を受けてよかった。

症状が薄れると同時に段々手術と入院の記憶が薄れてきたので、忘れないうちにメモしておこうと思う。

入院準備

特に役に立ったもの

手術〜入浴できるようになるまで数日あり、入浴できるようになっても時間や頻度は自由ではない。 ボディシートはあったほうが良いというか必須だった。

シャンプーシートも必須だった。

入浴や洗顔に必要というより入浴スペースに荷物をもっていったりするのに便利だった。 床に物を直置きしなくて済むのも心理的に良かった。

家族と着替えと洗濯物をやりとりするときに便利だった。 取っ手がついていて掛けたりしておけるのが手術直後は良かった。

折りたたみスマホは入院時にはとても便利だった。 PCも持っていったが、ほとんどの作業はこれ1台でできるのでほぼ引き出しにしまっていた。 重いので次に入院するときは首さげできるようにすると良いと思った。

store.google.com

入院中

手術直後

手術自体は寝ている間に終わるので何もわからない。 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から非表示にできます

github.com

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

https://developer.android.com/reference/android/support/v4/app/FragmentTransaction#setPrimaryNavigationFragment(android.support.v4.app.Fragment)

setPrimaryNavigationFragment() は画面遷移を処理するFragmentを指定するメソッドです。 対象Fragmentを setPrimaryNavigationFragment() しておくことで popBackStack() したときにそのFragmentの childFragmentManager のバックスタックを戻れる ようになります。

さきほど説明したタブ切り替えの際、 attach() と同時に setPrimaryNavigationFragment() しておくことで バックボタン押下時にタブFragmentの childFragmentManager の popBackStack() を呼び出してくれます。

バックボタンを押した際の挙動まとめ:

  1. PrimaryNavigationFragment.childFragmentManager.popBackStack()
    • PrimaryNavigationFragment が設定されていなければ 2. へ
    • バックスタックがなければ 2. へ
  2. FragmentActivity.supportFragmentManager.popBackStack()
    • バックスタックがなければ 3. へ
  3. アプリ終了

サンプル解説

ここまでで説明したとおり、実装方法はシンプルです。

  • タブごとに Fragment (TabFragment)を生成し、 childFragmentManager を管理させる
  • タブ切り替えの実装を各TabFragment の detach/attach によって行う
  • コンテンツ間の画面遷移に TabFragment の childFragmentManager を利用する
    • Fragment.getFragmentManager は自身の遷移に利用された FragmentManager を返す
      • TabFragment で最初のFragmentを追加する際に利用した childFragmentManager がそのまま利用できる
    • Fragment.getId は自身の遷移に利用された layout id を返すを返す
      • TabFragment で最初のFragmentを追加する際の id がそのまま利用できる

タグの切り替え、および初回の TagFragment の初期化

https://github.com/nein37/BottomNavigationSample/blob/master/app/src/main/java/com/github/nein37/bottomnavigationsample/MainActivity.kt

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を設定

https://github.com/nein37/BottomNavigationSample/blob/master/app/src/main/java/com/github/nein37/bottomnavigationsample/TabFragment.kt

// 初回のみ初期表示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 と同時に破棄/再生成されるため、タブの切替処理などで有用です。 FragmentTabHostFragmentPagerAdapterではこの仕組を利用して表示Fragmentの切り替えを行っています。

show(), hide()

Fragmentの表示状態を切り替えます。 detach(), attach() と違い、Viewの破棄や再生成が行われず、ライフサイクルも変化しません。 Fragment側のフラグで管理しているため、hide()したFragmentをremove() -> add()し直した場合でもView要素は表示されません。 OptionsMenu も同時に制御されて便利ですが、View自体は生成されたままなので画面遷移の多くをこの仕組で行う場合はメモリ消費に注意が必要です。

setPrimaryNavigationFragment()

setPrimaryNavigationFragment は v26.1.0 で追加された比較的新しいメソッドです。 このメソッドを呼び出すと、これ以降のbackボタン操作において指定したFragmentが childFragmentManager の popBackStack() を処理するようになります。

  1. fragmentA に遷移(replace)
  2. fragmentA の childFragmentManager で childFragmentB に遷移(replace & addToBackStack)
  3. 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

nein37.hatenablog.com

上記の記事の続き。 開発時に引っかかりがちないくつかの注意点と、リリース時に知っておいたほうが良いと思うことについてまとめてみる。

開発時の知見

主にチーム開発時の注意点と標準ツールの活用、よく嵌りがちな落とし穴について。 前回記事の アプリの実装について でも少し触れていたが、もう少し広い範囲について必要だと思うことを書いてみた。

チーム開発の注意点

他の開発者と開発環境を揃えることで開発を効率化できる他、他のメンバーに自分の変更による差分をチェックして貰う必要がある。 また、Android開発者ではない人に対する技術的な説明もできたほうが良い。

標準ツールの活用

Android Sdk(とそこからダウンロードできる各種ツール)とAndroid Studioに付属しているデバッグ機能について。

標準ツールではないがstetho などのデバッグ用ツールについても知っておくとより良い。

よく嵌まる落とし穴

  • Auto Backup
    • Auto Backup が有効な場合、 SharedPreferencesやSQLiteデータベースなどが自動的にバックアップされる
    • 復元されると困る情報は除外設定を必ず行う
    • 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 : リリース周りについて別記事に追記した。

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ストアからアプリをインストールさせないための仕組みで、こちらは特に実装が不要なので最初から利用しても良いかもしれない。

メモ

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

これまでに発売されたAndroid端末のdp解像度まとめ

はじめに

今年のGooglePlayコンソールのアップデートにより端末カタログという機能が追加されました。 これは以前から存在していた配信対象から特定端末を除外する機能に、端末の詳しいスペックを閲覧・検索する機能が追加されたものです。

GooglePlayコンソールのアプリ管理画面からリリース管理→端末カタログで遷移してアクセスできます。

f:id:nein37:20180102191720p:plain

この端末カタログは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 ProPLE-701L という型番ですが、これは名前、型番のどちらで探しても見つかりませんでした。 代わりに Huawei/华为揽阅m2青春版7_0 という端末(型番 hwple703l) という同一画面解像度で同一DPI端末があることがわかり、国内モデルはこれのローカライズモデルかな?と思ったりしました。

もう少し情報が増えてくれれば絞込による謎端末探しが捗りそうなので、Googleさんには頑張ってほしいですね。

スマートフォンタブレットのswdp分布を見てみましたが、やはりスマートフォンでは 320dp, 360dpタブレットでは 600dp, 800dp に大きく偏っていました。 ただし、スマートフォンでは 411dp, 480dp といった情報量の多い端末もそこそこ増えてきているようで、今後はAPIレベルによるswdp分布などもみたほうが良いかな?という気がしました。 タブレットでは768dp端末が600dp,800dpに次いで多くなっていて、 アスペクト比 4:3 端末も意識する必要がありそうです。

今後GooglePlayコンソールの機能が拡張されてスペック検索やdp解像度のチェックができればスプレッドシートも不要になりそうですが、それまでは定期的に更新しようと思っています。