なるようになるかも

力は多くの場合、その人の思いを超えない。

今更AndroidのToolbarについて理解できたような気がするのでメモ。

気のせいかもしれません。

Toolbar

Toolbar は Action Bar を一般化させたもので、同様の機能を備えつつ、より高い柔軟性を提供します。通常の Action Bar と異なる点は、Toolbar は階層内のビューの 1 つであるということです。そのため、Toolbar のインスタンスは好きなところに配置することができ、他のビューと共存します。さらに、動きを与えたり、スクロール イベントに反応させたりもできます。Activity.setActionBar() をコールすることで、Toolbar を Activity の Action Bar として機能させることもできます。

Android アプリにマテリアル デザインを導入する - Google Developer Japan Blog

これがいまいち意味分からなかったので、原文読んでみました。

Toolbar

The toolbar is a generalization of the action bar pattern, providing similar functionality, but much more flexibility. Unlike the standard action bar, toolbar is a view in your hierarchy just like any other, so you can place instances wherever you like, interleave them with the rest of your views, animate, react to scroll events and so on. You can make the Toolbar act as your Activity’s Action Bar by calling Activity.setActionBar().

Implementing Material Design in Your Android app | Android Developers Blog

generalization of the action bar patternを「Action Bar を一般化」は全然意味違ってないです?「ActionBarパターンを普遍化させた」とかそんな感じのニュアンスに読めます。

toolbar is a view in your hierarchy just like any otherの部分、「他のビューと同じ階層にある」という点についてちょっと曖昧な書き方です。タイトルバー階層と、アプリが管理しているビュー階層が今までは別だったという前提が重要だと思いますので。

interleave them with the rest of your viewsを「他のビューと共存」は意訳なのか自分の訳が悪いのか微妙ですけど、「他のビューの余白に挿入」ってだけに読めます。

ActionBarはビュー階層に解放されました

この記事がとても参考になりました。

~2.XからAndroidにはActivity単位でラベルを表示できるタイトルバーの機能がありました。標準の見た目が残念すぎて、requestWindowFeature()でカスタマイズができるにも関わらず、大抵非表示にされていました。

3.Xでタイトルバーは多機能なActionBarに置き換えられました。しかし「2.Xでタイトルバー非表示にしつつ、3.X~ではActionBarを表示」と分岐を入れるのは好まれず、またActionBarをバックポートできるのはActionBarSherlockという非公式ライブラリしかなかったので、依然非表示にされたまま、ActionBarパターンの認知度はいまいちでした。

その状況が変わったのが2013年7月のAndroid Support Libraryrevision18によるActionBarのバックポートです。時期的には4.3(Jelly Beanの最後のマイナーアップデート)になって、ようやく公式でActionBarがバックポートされ、「iOSとは違うAndroidのパターンを見直そう」という機運が高まります。

そして5.0になるわけですが、ここにきてActionBarの定義が大きく変わったように思います。ロゴ画像ではなくカラーを利用したブランディング、ユーザーの管理するビュー階層への移動、ActionBar組み込みのナビゲーション機能(setNavigationMode())の廃止。

もはやActionBarはユーザービュー階層にあるのですから、その実装はユーザー次第であると考えると、NavigationModeが廃止されたのも理解できます。

しかしタイトルバーを非表示にしてToolbarを使うというのは、直感に反していて非常に微妙…。

2つあるToolbar

Toolbarは2つあります。android.widget.Toolbarandroid.v7.widget.Toolbarです。

Toolbar は AppCompat で完全にサポートされ、フレームワーク ウィジェットと同等の機能と API をもちます。

AppCompat v21 — Lollipop 搭載前のデバイスにマテリアル デザインを! - Google Developer Japan Blog

これ、前提としてandroid.widget.Toolbarがあって、その同等の機能がandroid.v7.widget.Toolbarで提供されている、という意味なのですね…。実際使ってみるまでは、v7ライブラリ専用の機能だとばかり。

両者は実機能としては違いがないのですが、本家がandroid:Theme.Materialテーマを前提としているのに対して、v7版はTheme.AppCompatテーマを前提としており、属性参照している値が異なります。

例によって、android:toolbarStyletoolbarStyleに分裂してるんでしょう。

Toolbarを使うときの注意

setSupportActionbar()を呼んで、ActionBarとして使うときは、以下のようにレイアウト指定します。

<android.support.v7.widget.Toolbar
    android:id="@+id/my_awesome_toolbar"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:minHeight="?attr/actionBarSize"
    android:background="?attr/colorPrimary" />

そうではなく、普通にViewGroupとして使うときには指定が変わります。

<android.support.v7.widget.Toolbar  
   android:layoutheight="wrapcontent"
   android:layoutwidth="matchparent"
   android:minHeight="?attr/actionBarSize"
   app:theme="@style/ThemeOverlay.AppCompat.ActionBar" /> 

画面に複数Toolbarを表示する場合に、それぞれに異なるスタイルを指定するためなのだと思いますが、トラップ感満載です。

ところで単純なViewGroupとしてみた場合、layout_gravityがレイアウトエディタ上でサジェストされなくて使い勝手が悪いんですけど、バグなんでしょうねー…。

どうでもいいカレンダーの話。

iOSをやってるとあるあるなのが、ユーザーがOSの端末設定を「和暦」に設定してるせいで、取得されるカレンダーが日本のものになってしまい、NSDateFormatterが期待通りに動作しないというもの。

海外のアプリは和暦を考慮してないので上手くいけばクラッシュさせることができます。

西暦

現在普及しているのは西暦です。

個人的に、西暦という呼び方は物凄い雑だと思います。一般にグレゴリオ暦を指しますが、グレゴリオ暦そのものは暦法なのに対して、和暦は紀年法なので、暦法紀年法の区別が付いていないケースが見られます。

1年の長さを定義するのが暦法で、今が何年なのか紀元を定義するのが紀年法です。

単に西暦と呼んだ場合、グレゴリオ暦という暦法と、イエス生誕の翌年を西暦元年とする紀年法両方を指す気がします。

Before Christ(キリスト生誕前)は英語なのに、Anno Domini(主の年)はラテン語という、この良く分からない紀年法は1500年くらい前に考案されるも実際に普及したのは19世紀で、西洋諸国の植民地支配によって世界標準な地位を築きました。

また、実際的にユリウス暦が使われるケースがあることにも注意です。例えば天文学で使われる「1光年」が指すのは、グレゴリオ暦の1年ではなくユリウス暦の1年なのです。

仏暦

仏教徒を中心に、釈迦が入滅した翌年を元年とする仏暦が利用されています。

歴史は浅く、100年くらい前にタイで考案され使われ始めました。

タイは意地でもこの暦を使う気なのか、公文書などにも使いやがります。しかもタイ数字です。読めません。

Wikipediaには太陽暦とのずれがあると書いてあるけどこれは間違ってる気がする?今は普通に543で算出できたような…。

OSの表記などで「タイ仏暦」とあるのは、国によって釈迦入滅の年の解釈、つまり紀元年が違うためです。本当は複数仏暦があるのですがそのうち利用頻度の高い「タイ仏暦」のみ扱われることが多いです。

和暦

日本では1500年ほど元号による紀年法を使い続けています。また、使われていた暦法は以下の通りです。

  • 元嘉暦
  • 儀鳳暦

日本書紀で使われていたとされる暦です。

  • 大衍暦
  • 宣明暦

このころまでは中国で使われていた暦をそのまま日本でも利用していました。日本では暦学や天体観測技術が発達していなかったという事情があります。

  • 貞享暦

ここで初めて国内独自の暦法が採用されました。この辺の話は天地明察が面白いので読むべきです。

あの話だと授時暦の扱いはよくないけど、授時暦の観測精度そのものは、ユリウス暦を超える部分もありました。

  • 宝暦暦

そんな貞享暦は、800年以上続いた宣明暦とは裏腹に、たった70年しか持ちませんでした。

貞享暦による改暦は幕府と朝廷の間の軋轢を生んで、天文方の政治力がなくなったところで、再び土御門家が席捲、この残念な暦を制定してしまったとかどうとか。

天地明察は安井知哲の次男を天文方3代にしたところでハッピーエンドみたいな終わり方してますけど、その後の渋川家の顛末はなんとも切ない。

  • 寛政暦

宝暦暦があまりにも酷すぎたので改定された暦。

わりと現役。今でもカレンダーに書いてある「旧暦」が天保暦のことなのです。

最後の暦ということでかなりの高精度だったながら、2033年に突入するとバグります。

現在使われている暦法。日本で制定されたのは明治6年からなので、明治5年以前を正しく算出するには旧暦への変換が必要で面倒です。

なおグレゴリオ暦が完全な暦法かというとそうではなくて、3000年で1日というレベルながらズレがあり、そもそもグレゴリオ暦の根拠となっている太陽年自体が不変ではないという問題がありますが、生きてるうちは関係なさそうです。

その他

週番号

ヨーロッパのカレンダーで使われます。週に連番振ってるだけなんですけど、月曜始まりなので注意です。

iOS5と6とで、NSDateFormatterYYYYyyyyで取得できる値が変わる現象があって、YYYYの方は週番号ベースだから年が違うみたいなstackoverflow見て信じてたんですけど、あれただのバグだったんですね…(iOS7で直った。でもyyyyを使いましょう)。

iOS8で使えるカレンダー

これらは西暦カレンダーに併記されるという、雑な扱いを受けています。

  • 中国暦

民国紀元のことかと思ったら、時憲暦っぽい。

ヒジュラ暦っぽい。

こちらは普通にユダヤ暦

Androidのカレンダー

Javaは1.4で仏暦を、1.6で和暦をサポートしました。

しかし AndroidJavaじゃない ので、実装を見る限り両方とも使えなさそうです。

今でも和暦を使うところって結構多い(行政とか銀行とか保険屋とか)と思うんですけど、JapaneseImperialCalendarがないAndroidで和暦が使えなくて困った、みたいな話はあんまり聞かないですね。

マテリアルなナビゲーションドロワー。

今のGoogle製のアプリはおおよそこういう見た目になっています。

記事の端末は若干古く、記事のコメントにあるSSのようにステータスバー領域までナビゲーションドロワーが被ってくるようなのがマテリアルなやつらしいです。

で、これどう作るんでしょうね。

NavigationDrawerテンプレート

NavigationDrawerテンプレートでプロジェクトを作ってもこういう動きにはなりません。それどころか、ハンバーガーボタンが動かなくて違和感が酷いです。(自分の環境が最新になってないだけだったらすみません)

これはandroid.support.v4.app.ActionBarDrawerToggleが廃止され、android.support.v7.app.ActionBarDrawerToggleへ移行したのが原因です。

ドキュメントがまだ整備されていなくて、Creating a Navigation Drawer | Android Developersの記述も古いままなんですよね…。

ic_drawer.pngをゴミ箱に投げ捨てて、インポートするActionBarDrawerTogglev4からv7に変えましょう。

//android.support.v7.ActionBarDrawerToggle
mDrawerToggle = new ActionBarDrawerToggle(getActivity(), 
    mDrawerLayout, 
    R.string.navigation_drawer_open, 
    R.string.navigation_drawer_close)

ただこれも過渡期の、ハンバーガーボタンが矢印にアニメーションするやつで、今風ではないのです。

ActionBarの上にドロワーを載せるには?

どう頑張っても無理だと思うんです。ビュー階層がそもそも違うので。

アプリケーションの最上層には、Window#getDecorView()で取得できる、android:id/dector_content_parentがあって、android:id/action_bar_containerandroid:id/contentxmlで指定したレイアウト)はその子要素となっているはずなのです。

というわけで、適当にGmailアプリのビュー階層をダンプしてみました。

f:id:quesera2:20150110133817p:plain

com.google.android.gm:id/action_bar_rootの下にandroid:id/contentがあります。ドロワーはその子要素となっています。

ActionBarっぽく見えてるのはさらにその下にあるcom.google.android.gm:id/mail_toolbarというただのViewです。

これはサポートパッケージのActionBarActivityを使った上で、

<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>

ActionBarを消し去った上で、さらに代替としてToolbarを使っているっ挙動っぽい。

ioschedの実装を読もう

マテリアルデザインのお手本とされているioschedを読んでみます。

google/iosched · GitHub

前読んだときは、UI部分はスルーしてました。

src/main/res/values/styles.xmlより、

<style name="Theme.IOSched.Base" parent="Theme">
  ...(省略)
  <item name="windowActionBar">false</item>
  <item name="android:windowNoTitle">true</item>
  ...(省略)
</style>

<style name="Theme.IOSched.WithNavDrawer" parent="Theme.IOSched" />

src/main/res/values-v21/styles.xmlより、

<style name="Theme.IOSched.WithNavDrawer" parent="Theme.IOSched">
  <item name="android:statusBarColor">@android:color/transparent</item>
</style>

アクションバー消してるのは予想通り。stylesで空のテーマを作っておいて、v21だけステータスバーを透明にしてます。v21なんですね。4.4のimmersive mode(あまり知らない)だと思ってたので意外。

src/main/com/google.samples/apps/iosched/ui/BaseActivity.javaより、

// Primary toolbar and drawer toggle
private Toolbar mActionBarToolbar;

protected Toolbar getActionBarToolbar() {
  if (mActionBarToolbar == null) {
    mActionBarToolbar = (Toolbar) findViewById(R.id.toolbar_actionbar);
      if (mActionBarToolbar != null) {
        setSupportActionBar(mActionBarToolbar);
      }
    }
  return mActionBarToolbar;
}

ですよねー。

setSupportActionBar()ってもうこのために存在するメソッドとしか思えませんでしたし…。

setSupportActionBar()というのは、引数に渡したandroid.support.v7.widget.ToolbarをActionBarとして利用するメソッドです。

本物のActionBarをThemeで非表示にした上で、レイアウト上にあるToolbarをActionBarとして利用することによって、画面全体にナビゲーションドロワーが表示されるようにしているのです。

private void goToNavDrawerItem(int item) {
  Intent intent;
  switch (item) {
    case NAVDRAWER_ITEM_MY_SCHEDULE:
      intent = new Intent(this, MyScheduleActivity.class);
      startActivity(intent);
      finish();
      break;
...(以下略)

ドロワーからアイテム選択されたらstartActivity()した後、自身をfinish()してます。

つまり、ドロワーのルートになる全てのレイアウトがToolbarNavigationDrawerのリスト部分をincludeしてるんですね。ドロワー内のリストの選択状態は各Activityで管理なんでしょうか。

ドロワーが閉じてる最中にstartActivity()したら見た目の整合性取れないよね?と思ったら、postDelayed()startActivity()にディレイを掛けて、ドロワーが閉じた後に移動するよう見せかけてます。

これ微妙すぎるのでは…。

ActionBarActivityを使わない場合

Activity | Android Developers

API Lv21以降はsetActionBar()setSupportActionBar()と同じことができます。

ただし、この引数のToolbarandroid.support.v7.widget.Toolbarではなく、 android.widget.Toolbarです。両者は全く別物です…。

Androidのサポートパッケージについて

v17 Leanback Libraryの存在はとりあえず無視しておくとして、Androidのサポートパッケージは何種類かあります。

たぶんv13パッケージはなぜ存在するのか知らない人が多いと思いますので備忘録も兼ねて。

v4 Support Library

まず、サポートパッケージのv4とは「サポートパッケージのversion4」ではなく、「AndroidAPI Lv4以上で利用できるサポートパッケージ」を意味しています。

v4はAndroid 1.6 Donut以上で使えるライブラリで以下の機能を持ちます。

互換性のための機能

  • 3.0で追加されたFragmentのサポート
  • 3.0で追加されたLoaderのサポート
  • Notification系のメソッド@Deprecatedが多く、通知できるスタイルも4.1で大幅に増えたのですが、NotificationCompatを使うことでその辺をそれほど意識しなくて済む…かも?

新機能

  • LocalBroadcastManager。ブロードキャストレシーバーは端末全体に通知するけど、「ローカル」な「ブロードキャストレシーバー」はアプリ内部にのみ通知するためセキュア。実装がすごい短くて面白い。アプリ内部で完結する通知を実装したいときにどうぞ。
  • ViewPager。スワイプでページを切り替えるインターフェース。
  • SlidingPaneLayout。マルチペインレイアウトを実現するレイアウト。
  • DrawerLayout。ナビゲーションドロワーの、ドロワーの部分を実現しているレイアウト。「ナビゲーションドロワーにはActionBarが必要」みたいな固定概念がありますが、実はこれ単品でも使えます。

興味がないのであまり知らない

  • アクセシビリティ系の互換ヘルパ
  • FileProvider複数アプリ間でファイルを共有する機能を提供する。標準の連絡帳やギャラリーでは、パーミッションがなくてもIntentで呼び出されたときのみ、ユーザーの選択したアイテムだけ、呼び出し元アプリに一時的にアクセス権限を与える機能があるけれど、それのファイルバージョンみたいなものとふわふわ理解してる。

v7 Support Library

v7はAndroid 2.1 Eclair 以上で使えるライブラリで以下の機能を持ちます。

互換性のための機能

  • 3.0で追加されたActionBarのサポート
  • ActionBarActivity
  • ShareActionProviderSNS連携を実装するときに便利っぽいのだけれど…。

現在は生のActivityを使わずに、ActionBarActivityを継承するスタイルを推奨しています。

この場合アプリテーマはAppCompatを継承する必要があって、これのせいでリソースファイルの見通しが悪くなるのが辛いところ(リソースに名前空間がないから、重複回避にabc__を使ってるせいで候補として真っ先に上がってくるのが辛い)。

AppCompatでいくつかのコンポーネントを作ると、マテリアルな感じにtintカラーを付与してくれます。

  • 3.0で追加されたGridLayoutのサポート
  • ついでにSpaceもバックポート。(GridLayoutのために存在するのだけど、それ以外で使っても割と便利)
  • Switchがようやくバックポート…。

新機能

  • Cardview。カードっぽく見せることができるリストビューのアッパーバージョン。これ必要あります?
  • RecyclerView。削除・追加にアニメーションを付けられるリストビューのアッパーバージョン。5.0以降で使う場合、LayoutManagerを設定しないとクラッシュすることに注意。ただ、LayoutManagerを設定することでグリッド、横方向のリストも作れます。

ListViewに変わるコンポーネントとしていくつか提案されてますが、歴史あるListViewと比べるとノウハウが蓄積されていないので、変なところでハマって無駄な時間を食ったりします。

興味がないので知らない

  • v7 mediarouter library。Choromecastとかその辺?

v8 Support Library

API Lv13から提供された、RenderScriptをv8以降で使えるようにするというものです。

RenderScriptについては…迷走してる感があってもにょい。

この辺、自分の理解度が高いと言えない領域なのでなんとも言えないのですが、当初はOpenGLを置き換えるようなアピールがされてたんですけど、JellyBeanからは描画関連の処理がバッサリカットされて、GPGPUによる高速計算を行うクラスになったっぽい??

あとGPCPUを使えない端末にまでバックポートできてしまうのだけど、そのときは普通にCPU使うらしいですね。

C99頑張って書く労力に見合ってないと思うんですよ…。

v13 Support Library

ついに本題。

v13 Support Libraryって何のためにあるのか知らない人多いと思うんですよ。

これは「v4でバックポートされたFragmentを使わない」ためにあります。

例えば、ViewPagerFragmentPagerAdapterで使う場合を考えると、こいつのコンストラクタandroid.support.v4.app.FragmentManagerを要求するんです。

// OK
mAdapter = new FragmentPageAdapter(getSupportFragmentManager()){
    ...(省略)
});

API Lv13以降の普通のActivitygetFragmentManager()で取得されるFragmentManagerandroid.app.FragmentManagerなので、型の不一致でビルドが通らないわけです。

// NG
mAdapter = new FragmentPageAdapter(getFragmentManager()){
    ...(省略)
});

ではViewPagerを使うときには、必ずFragmentActivityActionbarActivityを使わなければならないのでしょうか?

いいえ、そこでv13サポートライブラリが使えるのです。

//OK
mAdapter = new android.support.v13.app.FragmentPagerAdapter(getFragmentManager()){
    ...(省略)
});

SyncResultメモ。

どうせ自分しか使わないアプリなら、Lollipop限定のJobSchedulerでいいんじゃないかなという気がしてきました。

SyncResultの概要

SyncResult | Android Developersより、必要そうな部分を抜粋。完全な情報はリンク先参照

このクラスはSyncManagerへ同期操作の結果を伝達するために利用される。この値に基づいて、SyncManagerは同期の配置(disposition)と、将来的に新しい同期操作をスケジューリングする必要があるかを決定する。

フィールド

type name description
public boolean databaseError ストレージ層の操作に伴うハードエラーがSyncAdapterに発生したことを示すのに利用します。
public long delayUntil リクエストのAccountAuthorityに一致する将来の同期要求が、少なくともこの秒数だけ遅延させる必要があることを示します。
public boolean fullSyncRequested 設定した場合、SyncManagerはリクエストに利用したのと同じAccountAuthorityと同じ(ただし空のextrasのBundle)で即座に同期を要求します。
public final SyncStats stats 同期操作について、追加の統計情報を保持するために利用します。
public final boolean syncAlreadyInProgress SyncAdapterが既に同期操作を実行していることを示すために利用します。必ずしもAccountAuthorityの要求が処理できたわけではありません(?)。(原文:Used to indicate that the SyncAdapter is already performing a sync operation, though not necessarily for the requested account and authority and that it wasn't able to process this request.)
  • syncAlreadyInProgressの翻訳に全然自信がないです。
  • ソフトエラー(通信失敗)とハードエラー(データベースへの書き込み失敗)の2段階でエラーを管理していることが分かります。
  • 上では端折ってますが、"(as defined by the SyncAdapter)"の箇所はSyncAdapterが定義するので、プログラマが制御する必要はなさそうです。逆に言えば上記のフィールドはユーザー側である程度制御する必要があるのだと思います。

SyncStatsの概要

SyncStats | Android Developersより、必要そうな箇所を抜粋。この辺日本語ソースの情報は、50 Android Hacksという翻訳本にちょっとだけ載っているのを確認したくらいで、ドキュメントもtypoが多い…。

雑訳なので、ちゃんとした情報はリンク先参照。

同期操作の結果について、さまざまな統計を記録するのに利用します。SyncManagerSyncResultを経由してこれらへアクセスし、同期の配置(disposition)を決定するため、いくつかの情報を使用しています。

SyncManagerがこれらの値をどう扱うのかについて、更なる議論はSyncResultを参照してください。

フィールド

type name description
public long numAuthExceptions ハードエラー。リクエストで指定されたAccountでの認証に失敗した。問題を解決するために、ユーザーの何らかの操作が必要であることを意味する。
public long numConflictDetectedExceptions ハードエラー。サーバー上のリソースを更新または削除しようとしたとき、回復不能なバージョン競合が発生した。ローカルストレージをクリアして最初からやり直すようユーザーの介入が必要かもしれませんが、サーバーから新しい状態を取得することで解決することが期待される。
public long numIoExceptions ソフトエラー。ネットワークの接続、またはタイムアウトなどの問題が発生した場合など、再試行すれば成功する可能性があるリクエストである。
public long numParseExceptions ハードエラー。サーバーまたはそれ以降のストレージから受信したデータで問題が発生した。
public long numSkippedEntries 何件入力されたか追跡するためのカウンターで、同期操作中にサーバーまたはローカルストアに無視された件数を意味する。解析不能なデータを検出しても、即座に失敗とせず、スキップして次へ進めることを決めた場合。
  • エントリ(numEntries)=挿入件数(numInserts)+更新件数(numUpdates)+削除件数(numDeletes)という解釈でいいんだろうか。これらは"as defined by the SyncAdapter"とあるので、設定してはいけない。
  • numSkippedEntriesはサーバー側起因で解析不能なデータを処理した場合のみ設定するのか、CONFLICT_IGNOREで重複データを無視した場合も含めていいのか説明が曖昧。numSkippedEntriesを加算してるコードを検索しても3件しか出てこない…。
  • numParseExceptionsは詳細な説明と例示が書かれているのですけど、「サーバーから不正なエントリを受信した場合、そのエントリを削除して進捗を進めた上で、エラーが発生したことを記録するためにnumParseExceptionsを加算することができる」という感じで、実装者の判断に委ねる感じ。

同期結果

クリーン、ソフトエラー、ハードエラーの3種の結果になります。

クリーン

何の問題もなく同期に成功したことを意味します。

生成された時点のSyncResult()の結果はクリーンです。

またclear()メソッドを呼び出すことでクリーンにすることができますが、syncAlreadyInProgressが設定されている状態でclear()を呼び出すと、UnsupportedOperationExceptionがスローされます。

ソフトエラー

軽微なエラーが生じたことを意味し、指数バックオフで再試行します。

いずれかがtrueのとき、ソフトエラーと判定されます。

  • numIoExceptions > 0
  • syncAlreadyInProgress

なお、ソフトエラーとハードエラーの条件を同時に満たした場合、ハードエラー扱いになります。

ハードエラー

重篤なエラーが生じていることを意味し、SYNC_EXTRAS_UPLOADフラグが設定されている場合を除き、再試行しません。

いずれかがtrueのとき、ハードエラーと判定されます。

  • numParseExceptions > 0
  • numConflictDetectedExceptions > 0
  • numAuthExceptions > 0
  • tooManyDeletions
  • tooManyRetries
  • databaseError

SyncAdapterのメモ。

年末暇なAndroid開発者は是非SyncAdapterを実装しましょう。

バックグラウンドフェッチはこれでやるべきです。

startForeground()とかStickyBroadcastを悪用した死ににくいサービスとかで延々通信させる闇アプリは滅びてください。ていうか他人が実装した通信処理を信用できないので、バックグラウンドで何か通信するアプリは極力入れない主義です。

しかしSyncAdapterは本当に資料がないです。以下、実装するときにいくつか疑問だったことのメモです。

android:process=":sync"

SyncAdapterのトリガとなるサービスには:syncというプロセス名を付けなければいけません。

<service> | Android Developers

If the name assigned to this attribute begins with a colon (':'), a new process, private to the application, is created when it's needed and the service runs in that process.

:から始まるプロセス名はそのアプリのプライベートプロセスとして起動されることを意味します。そうでない場合、グローバルプロセスになります。

実動作を見てみると、hogehoge.yourpackage:syncというプロセス名で同期処理が行われていることを確認できると思います。ここまでは知ってる人も多いかと思います。

Androidが闇なのは、ここで「グローバルプロセスとプライベートプロセスって何が違うの?」という当然の疑問を抱いても、それを知ってる人がまずいないことです…。

ContentProviderClient

これがずっと疑問で、Stackoverflow日本語版のおかげで解決したんですけど、

SyncAdapterのサンプルだとContentResolverをプライベート変数にしたり、onPerformSync()のタイミングで取得したりしてますけど、単一のContentProviderを使う限りにおいては、引数で取得できるContentProviderClientを利用した方が高速です。

@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
  // フェッチを実行する、結果が取得できなければ更新しない
  List<FeedItem> result = FeedConnection.execute();
  if (result == null) {
     syncResult.stats.numIoExceptions++;
     return;
  }

  try {
    ContentValues[] values = FeedDAO.getInstance().convertFromFeedList(result);
    provider.bulkInsert(Constants.CONTENT_URI, values);
  } catch (RemoteException e) {
    syncResult.databaseError = true;
  }
}

また、バルクインサートを使うとより効率的です。

テーブルを正規化してたりすると、複数ContentProviderを扱うことになると思いますので、この手は使えないのですけど。

DAO は自作しない方がいいです

Data Access Objectとかgetter/setterとか人間が書くものじゃない…。

素直にLombokとORMで楽しましょう。

…と考えてるのですけど、どのORMを使うかが難しい。

ActiveAndroidは見えてる地雷っぽいので回避したいです。greenDaoがよさげなのですけど、資料の漁りやすさに難がありそう。

AuthenticatorもContentProviderもスタブ実装でいい

SyncAdapterが面倒なのは、AuthenticatorContentProviderを実装しなければいけないという点で、AccountManagerなど前提知識が多くこの辺で詰まる人が多いと思いますけど、これらを実装する必要はありません

しかしContentProviderは実装しておくといいかもしれないです。DataBaseHelperを直接利用したほうが利便性は高いですし、開発者全体のレベル感に合わせて敢えて採用しないみたいなケースがありがちで、ちゃんとContentProviderを実装する経験というのは意外に得がたいです。

CursorAdapterCursorLoaderと連携させると非同期でデータを更新し、アップデートの通知を受け取って自動的に更新されるタイムラインが感動的なくらいに楽に作れます!(CursorAdapterRecyclerViewの相性が悪いことには目を背けつつ…)

スタブ実装で構わないということは、これらは不要なのでしょうか?

そうではなく、両者を実装することによってセキュアな同期処理を行えるのです。

SyncAdapterによって、システムがContentProviderを更新する」という説明からセキュリティ的にどうなの?と思うかもしれませんが、そのためのAuthenticatorです。AccountManagerによって認可された権限でContentProviderを操作することこそが、SyncAdapterの肝なのです。

たぶん。

SyncResultって何書けばいいの?

SyncAdapterを使えばいつ同期を行えばいいのか、システムが面倒を見てくれる」という触れ込みですが、これはSyncResultに結果を返したケースだと考えています。

SyncResultをちゃんと書くことで、システムに「いつ次の同期を行えばいいのか」の判断を与えることができます。

でも公式ドキュメントではこのことが完全にスルーされており、どう書けばいいのやら、他の実装を参考にしたり、実際の同期処理がいつ走っているのか確かめて手探りでやる必要があります…。

どうデバッグすればいいの?

SyncAdapterによる同期処理は別プロセスで起動するため、単にブレークポイントを貼ってもスルーされます。:syncのプロセスが起動したタイミングでDDMSからデバッガをアタッチする必要があります。

が、そんなことやってられません。

いつ同期されたのか、同期したときにエラーがなかったかなど、診断ログを出力する機能を作っておくのが一番いいのかなーと思ってます。

コンテナViewControllerについて

A-Liaison BLOG: Container View Controllerを作ってみよう

この記事の間違いが多すぎてやばいので。

iOS 5からはこのContainer View Controllerを自作する事が可能になりましたが、実装が面倒なのと大体の場合においてUIKitが用意しているContainer View Controllerを使うかcocoapodsあたりからそれっぽいライブラリを拾ってくれば解決するためかあまり具体的な実装方法が話題になっていないようです。

iOS5の頃は単に英語資料しか存在しなかったので、普通の人は存在を知らないし、存在を知っている人は英語をある程度読めるので、海外の記事や実装を直接読んでいただけじゃないのかなぁと思う。マジでスライドで概要を説明している人が1人いるだけとかそういうレベルだったんですよ…。

UIViewControllerプログラミングガイドに邦訳が載ったiOS6以降だと具体的な実装方法は日本語で読めるようになりましたし、実装方法を話題にしている人も圧倒的に増えたと思うんですけど…。

本題

didMoveToParentViewController:willMoveToParentViewController:の実装を間違える人は多いです。そもそもカスタムなコンテナUIViewControllerの実装者の責務(must)であるにも関わらず、呼んでないコードを見たこともあります。

上の記事も間違えています。

しかしご安心ください。まず問題になりません。

なぜか。

全ての初期化処理を単一のライフサイクルメソッドに押し込める、viewDidLoad病か、viewDidAppear病の患者が極めて多く、didMoveToParentViewController:のタイミングで何らかの処理を行うこと自体が極めて稀なためです。

独創的なUIViewControllerのサブクラスを基底クラスとして案件ごとに用意して、独自の初期化処理を行うようなコードも見ます。回転系のライフサイクル系メソッドがバージョンによって変わったり、UITableViewControllerを継承する必要が出てきて詰むやつです。

will/didMoveToParentViewController:とは

ViewControllerのライフサイクルメソッドのひとつです。

UIViewControllerがネストする構造になっているときに、これから子として追加、もしくは親から削除されるタイミングで何らか処理を行いたい場合に記述できます。

引数のUIViewControllerは親となるViewControllerです。nilの場合、親から取り除かれることを意味しています。

何に使うのか

独自のコンテナViewControllerを作る場合、子から親へ何らかの通知を行いたい場合があります。

任意のタイミングでparentViewControllerプロパティを利用し、直接親のViewControllerへアクセスすることは可能なのですが、何らかのプロトコルに準拠させて操作に制約を付けた方がスマートです。

そのような実装を考えたとき、いつdelegateとして親のViewControllerへの参照を保持/解放するかを考えると、親子関係が紐付く/解除されるこのタイミングが最も適切です。

addChildViewController:を呼び出す親側で、delegateの登録を行うことも当然できます。しかし親がインターフェースを規定してしまうので、子の実装に制約がかかります。

一方、子側でdelegateの登録を行う場合は後からプロトコルを増やしたり、delegateの登録を行わない特殊な子の存在を許容できたりと、柔軟性が大きく変わります。

ただしこれは、コンテナViewControllerが正しく実装されている という前提での話です。たとえばdidMoveToParentVieController:を呼び出さないコンテナViewControllerでは、didMoveToParentVieController:が呼び出されるのはViewControllerの階層から削除されるタイミングだけです。諦めましょう。

コンテナViewController実装者の責務

2年くらい前に書いた記事のコピペですけど、

  • addChildViewController:の後にdidMoveToParentVieController:
  • removeFromParentViewController:の前にwillMoveToParentViewController:

を、それぞれ手動で呼び出す必要があります。一方で、

  • addChildViewController:の前のwillMoveToParentViewController:
  • removeFromParentViewController:の後のdidMoveToParentViewController:

は、デフォルトでは自動的に呼ばれますので、何もしなくていいです。

なぜ手動で呼び出す必要があるメソッドと、そうでないメソッドがあるのでしょう?

答えは簡単で、ViewController追加/削除のトランジションがいつ始まり、いつ終わるのかはシステムには分からないからです。

addChildViewController:が呼び出されたということは、これからViewControllerのトランジションが始まるのだということはシステムにも分かります。そこでwillMoveToParentViewController:は自動的に呼び出されます。コンテナ実装者はトランジションが完了したタイミングで、didMoveToParentVieController:をコールする必要があります。特にアニメーションなどない場合は即座に呼び出して問題ありません。

removeFromParentViewController:の考え方も同様です。removeFromParentViewController:が呼び出されるタイミングは既にViewControllerのトランジションが終了し、ViewControllerの階層から取り除かれた後なので、didMoveToParentViewController:は自動的に呼び出してくれます。しかしトランジションが始まるタイミングはシステムには分からないので、コンテナ実装者はnilを引数にしてwillMoveToParentViewController:を呼び出す必要があります。

begin/endAppearanceTransition:animated:について

呼び出さなくていいです。

なぜ存在するのか

iOS5ではautomaticallyForwardAppearanceAndRotationMethodsToChildViewControllersiOS6以降はshouldAutomaticallyForwardAppearanceMethodsというBOOLを返すメソッドがあり、コンテナViewController実装者は必要に応じてこのオーバーライドすることができます。

デフォルトはYESですが、NOにすることで

  • viewWill/DidAppear:animated
  • viewWill/DidDisappear:animated
  • willRotateToInterfaceOrientation:duration:など回転系のメソッド(バージョンによって違いがありすぎるので割愛)

などのトランジションが発生した際の通知を、子ViewControllerに伝播させないようにできます。(逆に言えば、通常は自動的に子に伝播されます)

親と子とでトランジションのタイミングを変えたい場合、または複数の子ViewControllerにディレイを掛けてトランジションさせたい場合など、子ViewControllerの外観の制御を親が完全にコントロールしたい場合のみ利用する特別なオプションです。まず考慮に入れなくていいので、確かに記憶から抹消しても構いません。

ただ以下のことを知らずにコンテナViewControllerを実装するのは論外です。

このとき、自動的に呼び出されなくなったwillRotateToInterfaceOrientation:duration:などはコンテナViewControllerが任意のタイミングで、手動で呼び出すことができます。

しかし、viewWill/DidAppear:animatedおよびviewWill/DidDisappear:animatedについては、絶対に手動で呼び出してはいけません。これらのメソッドを呼び出す特権は、システムのみが持ちます。

そこでこれらのメソッドを直接呼び出す代替として、begin/endAppearanceTransition:animated:を経由して通知を行うのです。

ですので、shouldAutomaticallyForwardAppearanceMethodsを記憶から抹消する場合、beginAppearanceTransition:animated:の存在も一緒に抹消してください。

shouldAutomaticallyForwardAppearanceMethodsとしてYESを返す場合(つまりデフォルト)では、トランジションの開始と終了の通知は、will/didMoveToParentViewController:を適切なタイミングで呼び出すだけで構わないのです。

余談

iOS5でviewDidAppear:内でaddChildViewController:するとChildViewControllerのviewWillAppear:が呼ばれない - Qiita

shouldAutomaticallyForwardAppearanceMethodsメソッドの命名から考えると、外観変更系のメソッドは「子に転送される」のでiOS5の実装が正しいような。

viewDidAppearaddChildViewController:するということは、既に終了している親のviewWillAppear:を子側へ転送するタイミングがないわけで、これでちゃんと呼び出してくれるiOS6以降がイレギュラーな感じ。