Androidの通知で音を鳴らすときに注意したいこと
こんなことで困る人なんていないような気もするが、ネタとしては面白いと思ったのでまとめてみる。困っていることというのは、res/raw以下にある複数のファイルのうち任意の1つを消すと通知の着信音が変わってしまうということ。何を言っているか分からないと思うのでどうしたらそんなことになるのか一つずつ説明していく。
Androidの通知について
本題に入る前に通知の基本的なことを抑えておく。
- 通知は通知チャンネルを通してユーザーに届けられる。
- 通知チャンネルはアプリが生成する。このとき、通知の着信音や振動、ライトのパターンなどを初期設定として渡せる。
- 通知チャンネルは通知音や振動パターンの設定をユーザーが変えられる。
- 通知チャンネルの設定はアプリから変更できない。
- 通知チャンネルはプログラムで削除できるが、削除したことはアプリの設定画面に表示される。また、設定をリセットしたいと考えて一旦削除し、再び同じIDのチャンネルを作ろうとしたとしても、削除する前の通知チャンネルが復元されるだけで設定をリセットすることはできない。
ポイントは、通知チャンネルはアプリで生成した後はすべてユーザーの手にゆだねられるということで、ユーザーがOffにした通知をアプリから勝手にOnにするようなことはできない*1。また、間違って設定してしまってもアプリの側から修正することはできない。状態を是正したければアプリの設定からユーザーに依頼してデータを消してもらうしかない(アプリ情報>ストレージとキャッシュ>ストレージを消去)。
通知の着信音
通知チャンネルに通知が届いた際に、設定した着信音を鳴らすことができる*2。この音は次のAPIで、チャンネルを生成する際にファイルのURIを渡すことで指定できる: https://developer.android.com/reference/android/app/NotificationChannel#setSound(android.net.Uri,%20android.media.AudioAttributes)。また、ユーザーがアプリ設定の画面から音を設定することもできる。
このAPIはおそらくコンテンツプロバイダ経由でアクセスできるファイルのURIが渡されることを想定しているのではないかと思うのだが、実際にはアプリで抱えているファイルもファイルパスをURIで表現して渡せる。例えば、res/rawの下のファイルは次のようなURIで表現できるが、これを渡すことでアプリ独自の通知音を再生できるようになる。
android.resouce://<application ID>/raw/<resource name>
あるいは、
android.resource://<application ID>/<resource ID>
今回の問題の原因と対策
今回の敗因はリソースIDを使う方のURIでファイルを指定していたこと。リソースIDはビルドするたびに変わる可能性がある。今回は不要になったファイルを一つ消してビルドしたところ、そのファイルよりもアルファベット順で後にあるファイルのリソースIDが一つずつずれて、通知音が軒並み変わってしまった*3。この現象には開発中に気が付いたのでリリースされることはなかったが、やろうと思っていたことが一つできなくなってしまった。前述の通り、アプリから通知の設定を変えることはできないので、アプリ更新時にマイグレーションをすることはできない。
こんなことが起きるのは私の環境だけだろうか。ビルドするときにリソースIDを極力変えないみたいなフラグがあったりするのだろうか。デフォルトでそうなっていてほしいという気もする。ファイルを追加するときはリソースIDが変わらないのも不思議に思っている。
Androidの通知で音を鳴らしたいときは、リソースIDを使うURIで指定するのは避ける。また、できれば音ファイルをコンテンツプロバイダに登録して、そのURIを指定した方がよい。そうしておくと、ユーザーが誤って着信音の設定を変えてしまっても、ユーザー自身で元の着信音に戻すことができる。
effective dartを一言ぐらいでまとめていく(Usage編)
続きです:akihito104.hatenablog.com
Usage
Library
複数のファイルに分かれたプログラムを、一貫性のあるメンテナンス可能な方法で構成するのに役立つ。import
についてのみ言及しているが、export
ディレクティブに対しても応用できる。
part of
ディレクティブではライブラリ名ではなくURIの文字列を使う(part
自体がもうほとんど使われなくなっちゃったけど)。- ほかのパッケージのsrcディレクトリの下のファイルをimportしてはいけない。lib/src以下のファイルはパッケージの実装という仕様になっている。
- libディレクトリの外から相対パスのURIを使ってlib以下のファイルをimportしてはいけない。そのような場合は
package:
でimportする。 - lib内のファイルをimportする(libをまたがない)場合は相対パスを使うのが好ましい。
Null
- 変数を明示的にnullで初期化してはいけない。Dartにはuninitialized memoryという概念は無く、暗にnullで初期化される。
- デフォルト値に明にnullを指定しない。これも暗にnullで初期化される。
bool?
をbool
に変換する際は??
を使うのが好ましい。ただし、if
の条件式の中でnull-awareness演算子を使ってbool?
をbool
にするのは避ける。そのような場合は例えばlist != null && list.isEmpty
のようにnullチェックをする。- 初期化されたかどうかをチェックしたい変数に対して
late
を使うのは避ける。Dartではlateの変数が初期化されたかをチェックする方法は提供していない。late
の変数が初期化されたかどうかをbool
のフラグによって管理するのは冗長(Dartは内部的にそのように管理している)なので、シンプルにnullableな変数として定義し、nullであるかどうかをチェックする。 - 型の上位変換を可能にするために、nullableなフィールド変数をローカル変数にコピーすることを検討する。上位変換はローカル変数に対してのみ有効であるため。ただし、コピーした後で値をフィールド変数の方に書き戻す必要がある場合は、忘れずに書き戻すこと。また、フィールドの方の値がスコープの外で更新される可能性がある場合は、単純にフィールド変数を直接使う(
!
を使って値を取り出す)ことを検討する。
Strings
- 文字列リテラルを連結するときにはadjusent strings(隣接文字列?)を使う。+を使う必要はない。長い文章を折り返すのに便利。
- 文字列と値とを組み合わせるときは内挿を使う。+で連結することもできるけど、内装の方がきれいで手短に済むので好ましい。
- 内挿を使う時に不要な波カッコを使わないようにする。
Collections
Dartでサポートされているリスト、マップ、キュー、セットについての話題。
- 可能な時はコレクションリテラルを使う。MapとSetには無名コンストラクタがあるが、便利なビルトインの構文を使った方がよい。
- コレクションが空であるかチェックするときに
length
を使わない。そもそもIterable
は長さが分からない。かわりにisEmpty
やisNotEmpty
を使う。 Iterable.forEach()
は関数リテラルとともに使わないようにする。ただし、すでにある関数を渡すのは良い(names.forEach(print)
のような)。また、Map
はIterable
ではないのでforEach()
を使ってよい。- 型を変えたいときだけ
List.from()
を使う。単にコピーしたい場合にはtoList()
を使う。 List
のフィルターを型で行いたい場合はwhereType()
を使う。- それに近い処理ができる場合には
cast()
は使わない。toList().cast()
はList.from()
で置き換えられるmap().cast()
はmap<T>()
で置き換えられる
cast()
を避ける。前述のルールをやや一般化する。cast()
を使うのが正解であることもあるが、このメソッドがリスキーで思わぬ処理の遅延やケアレスによる実行時の失敗を引き起こすということを考慮しておく。- 最初から正しい型を指定する
- 値を取り出すときにキャストする
List.from()
を使って即時に変換する。cast()
は遅延評価のコレクションを返すので、オペレーションごとに型のチェックを行う。要素も処理も少ない場合には有効だが、多くの場合この遅延評価のオーバーヘッドが重くなる。
Functions
Dartでは関数もオブジェクト。
- 名前付きの関数を定義するときは関数と名前をバインドする。
final func = () => ...
ではなく、void func() { ... }
のような。 - tear-offができるときはクロージャを作らない。tear-offとは関数、メソッド、名前付きコンストラクタと同じ引数をとり、呼び出そうとした関数と同じ関数を呼ぶクロージャのこと。Dartではこのtear-offを自動で作るので、わざわざクロージャを書いてラップする必要はない。
- 名前付き引数にデフォルト値を与えるときは=を使う。過去の経緯から
:
も使うことができるが、一貫性の観点から=
を使うのが望ましい。
Variables
- 一貫性のあるルールに従ってローカル変数に
var
やfinal
を付けること。ローカル変数には型アノテーションは付けず、必ずvar
かfinal
で定義する。次の2つのルールのいずれかが広く使われている。- 再代入されないものには
final
を使い、それ以外にはvar
を使う。 - 再代入されないものであっても
var
を使い、final
は一切使わない(フィールド変数やトップレベル変数についてはこの限りではない)。
- 再代入されないものには
- 計算可能な値を保存しないようにする。パフォーマンスの観点でキャッシュする場合にはコメントを付けておくとよい。
Members
Dartではオブジェクトは関数(メソッド)やデータ(インスタンス変数)といったメンバーをもつ。
- 不必要なgetterやsetterでラップしない。
- 読み取り専用フィールドはfinalを付けるのが望ましい。
- シンプルなメンバでは
=>
の使用を検討する。ただし、2行以上になるものや条件文が複雑なものはブロック形式で記述する。値を返さないようなsetterについても=>
を使える。同じく=>
を使っているgetterのメンバと対応させるとわかりやすい。 - 名前付きコンストラクタにリダイレクトしたりシャドウイングを避ける以外の用途で
this
を使わない。 - 可能な場合は宣言時にフィールドを初期化する。
Constructors
- 可能な場合はinitializing formalsを使う。できない時は仕方ないけどできるときはそうすべき。
- コンストラクタのリストイニシャライザを使う時はlateを使わない。
- コンストラクタのボディが空の時は
{}
ではなく;
を使う。 new
を使わない。- 無駄に
const
を使わない。次のようなケースは暗黙的にconst
なので書く必要はないし、無駄なので書くべきではない(ただし、デフォルト値については後続のリリースでconst
でないものにも対応するのでこのリストには入れていない)。
Error handling
on
節を使わずにcatch
するのは避ける。on
節を付けないとtry
ブロック内で起きたすべての例外をキャッチすることになる。StackOverflowError
やOutOfMemoryError
を適切に処理するのは難しいし、意図的に出しているArgumentError
やassert()
の例外を握りつぶすのは本意ではないはず。catch
にon
節をつけて、自分が認識していて正しく処理できる例外だけをキャッチする。まれに、すべての実行時例外をキャッチしたいという事があるが、そういうものは普通はフレームワークや低レイヤにおいて、それらのコードが原因でプログラムに問題が起きるのを防ぐのに使われる。こうした場合でもすべての例外をキャッチするよりもException
だけをキャッチする方がよい。Exception
はすべての実行時例外の基底クラスである。on
節なしでキャッチしたエラーを捨ててはいけない。もしすべてのエラーをキャッチした方がいいと考えるなら、キャッチしたときにログをとったり、ユーザーに知らせたりするべきで、ただ静かに捨てていいものではない。- プログラムのエラーのために実装した
Error
を投げる。Error
クラスはプログラムのエラーの基底クラス。Exception
は実行時例外の基底クラス。Exception
を投げるべきところでErrorを投げるとミスリーディングになるので気を付ける。 Error
やその実装型をキャッチしない。Error
はプログラミングのミスなので、プログラムを停止させてスタックトレースを表示し、ミスの原因に立ち返ってこれを修正する。- キャッチした例外を投げなおすときは
rethrow
を使う。rethrow
を使うと元のスタックトレースをそのまま保存できるが、throw
で投げてしまうと最後に投げた地点でこれがリセットされてしまう。
Asynchrony
Future
をそのまま使うよりasync
/await
を使うのが好ましい。非同期のコードはFuture
を使ったとしても読んだりデバッグするのは難しいが、async
/await
構文を使うことで読みやすくできる。- 無駄なところで
async
を使わない。便利なのは次のような場合:await
を使う時(これは自明)、Error
を非同期に返したい時(Future.error()
より短く書ける)、値をFuture
でラップして返したい時(Future.value()
より短く書ける)。 - ストリームを変換するために高階関数を使うことを検討する。(Iterableに関する上記の議論って何?)
Completer
を直接使うのは避ける。普通はFuture
やasync
/await
で事足りる。Completer
は低レイヤのコードにおける次の2つの場合に必要である:非同期のプリミティブを新しく作る時、Future
を使わない非同期のコードのインタフェースに使う時。Object
であるかもしれないFutureOr<T>
の型を解決する際には、まずFuture<T>
であるかどうかをチェックする。もしT
がint
であるなら、FutureOr<int> value
はvalue is int
とvalue is Future<int>
で見分けられる。しかし、T
がObject
である場合、Future<Object>
もまたObject
であるのでint
の場合のように見分けることができない。
effective dartを一言ぐらいでまとめていく(Document編)
続きです:akihito104.hatenablog.com
Document
- そのコードを書いている間はコードの意図が明白であったとしても、そのコードを初めて見る人や、将来の自分でさえも、その時の文脈を知らないかもしれない。
- コメントを書くのには数秒程度しかかからないが、それによってたくさんの人の時間を節約できる。
- コメントは自分たちが思っている以上にもっと書いた方がいい。
comment
- コメントは文章のように書く。大文字で始めて、ピリオドで終わる。
- コメントにはブロックコメントを使わない。ブロックコメントはコードの一時的なコメントアウトの時だけにする。
doc comments
///
で書き始めるとdartdocにできていい感じの見た目のページを作れる。
- メンバや型のドキュメントには
///
を使う。歴史的経緯でJavadoc風の /* ... /も使えるけど、///の方が*
を箇条書きのマークとして使えるのでおすすめ。 - publicなAPIにはドキュメントを書くのが望ましい。
- ライブラリレベルのdoc commentを検討する。クラスがプログラムの唯一の構成単位であるJavaとは違い、Dartにおけるライブラリは、ユーザーがそれを直接使い、importし、それについて考える実態である。
library
ディレクティブは、ライブラリのコンセプトや提供する機能を説明するのによい場所である。ドキュメントはファイルの先頭にあるlibraryディレクティブのすぐ上に書く。libraryディレクティブがない時は追加しよう。次のようなことを書くとよい- ライブラリが何をするためのものなのか、1行で表す。
- ライブラリ全体を通して使う用語を説明する
- APIをくまなく使うようないくつかのコードサンプル
- 最も重要な、または共通的なクラスや関数へのリンク
- このライブラリで取り扱う領域に関する外部への参照
- private APIにもドキュメントを書くよう考える。
- ドキュメントは1行の要約から始める。ただの断片的な文だけで十分な場合もあるが、読み手がこの先も読むに値するか判断できるようなものが望ましい。
- 最初の文章とdoc commentとの間に空行を入れて分ける。
- 周囲の文脈を冗長に書かない。doc commentを読む人はそのクラスや、そのメンバのシグネチャなら簡単に把握できるので、そういったことをわざわざ特定のメンバのdoc commentに書く必要はない。
- 関数やメソッドのコメントは三人称の動詞で書き始めるのが望ましい。
- 変数やgetter, setterのコメントは名詞で書き始めるのが望ましい。何が得られるのかを強調するべき。もしプロパティにgetterとsetterが両方ともある時、dartdocはそれらを1つのプロパティとみなすので片方にだけドキュメントを書けばよい。両方に書いてある場合は、setterの方が無視される。
- ライブラリや型のコメントは名詞で書き始めるのが望ましい。クラスのドキュメントがしばしば最も重要になる。型の不変性を記載したり、使う用語を確立したり、クラスのメンバに対するコンテキストを提供することにもなる。少しでいいのでクラスのドキュメント作成を頑張ると、ほかのドキュメンテーションをシンプルにできることもある。
- サンプルコードを付けるのを考慮する。
- 大かっこをつかってスコープ内識別子を参照する。クラス内のメンバや名前付きコンストラクタを参照するときはドットでつなげればよい。
- パラメータや戻り値、例外の説明は文章の中で行う。特別なシンタックスを使うなどして個別に詳細に書く言語もあるが、Dartはではパラメータなどを大かっこで括って本文内に統合する。
- doc commentはメタデータアノテーションの上に書く。
markdown
- 過度に使いすぎるのを避ける。
- フォーマットにHTMLを使うのを避ける。
- コードブロックにはバックティックフェンス (```) を使うのが望ましい。
writing
- 簡潔に書く。
- 略語や頭字語は使わない。
- メンバのインスタンスに対して言及するときは、theではなくthisを使うのが好ましい。