CursorAdapterとContentProviderの関係

AndroidでCursorのデータを一覧表示する場合、CursorAdapterが使用されていました。
このとき、データの更新をViewに反映するということについてフレームワークがなにを提供しているのかがわかりづらかったので、メモとして整理しておきます。

MediaStoreのデータをListViewで表示した場合

ContentProvider経由でCursorを取得し、CursorAdapterで表示するとどうなるか確認します。

そこで今回は、MediaStoreに保存された画像すべてを、ListViewで一覧表示してみます。

(サンプルなので、UIスレッドでクエリを発行しちゃいます)

public class SampleCursorAdapterActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ListView listView = (ListView) findViewById(R.id.listView1);
        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
        SimpleCursorAdapter mAdapter = new SimpleCursorAdapter(this, 
        		android.R.layout.simple_list_item_2, 
        		cursor, 
        		new String[] {MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID}, 
        		new int[] {android.R.id.text2,android.R.id.text1});
        listView.setAdapter(mAdapter);
        
    }
}

これで、ListViewに画像一覧が表示されます。

このアクティビティが起動したまま、画像をギャラリーに追加されるとどうなるでしょうか。
(カメラアプリで写真を撮るなどして)
実際に写真を撮って、バックグラウンドのアクティビティを再度表示してみると、ListViewにはきちんとレコードが追加され、表示が更新されていることが確認できます。
notifyDatasetChangedや、requeryなどは明示的に行っていないにも関わらずです。

CursorAdapterは関連づけられたUriの変更を検知する

このような動作になるのは、CursorAdapterがCursorに関連づけられたUriの変更を検知できるからです。

CursorとCONTENT_URIの紐付けは、各URIのContentProvider内部で実装されています。
正しく実装されたContentProviderは、クエリの結果としてCursorを返却するときにCursor#setNotificationUriによってCursorにnotificationUri(ギャラリー画像ならMediaStore.Images.Media.EXTERNAL_CONTENT_URI)が設定されています。
これにより、ギャラリーへの画像追加がContentProvider経由で行われると、ContentProvider#notifyChangeでcursorに変更を通知します。onContentChangedで検知し、自身のデータを更新します。

CursorAdapterが変更を検知できるのは、正しく実装されたContentProvider経由でCursorを取得したときだけ

ContentProviderを経由せず直接SQLiteとCursorをやりとりしたり、ContentProviderを経由していてもsetNotificationUriの実装が抜けていたりすると、CursorAdapterのデータ更新機能は動作しません。

HoneyCombからはCursorLoader

サンプルコードではmanagedQueryなど細かい部分には触れていませんが、honeyCombではmanagedQueryが廃止され、CursorLoaderへの移行が促されています。これについては後日あらためて書こうと思います。

AlertDialog#show()とActivity#showDialog()

Androidでダイアログを表示するときに利用するのが、AlertDialogクラスです。
Webでダイアログの表示について書かれた記事の多くは、AlertDialog#show()を用いています。

一方、本家のAndroid Developersでは、Activity#showDialog()を用いたサンプルコードが記載されています。
Dialogs | Android Developers
6.4 ダイアログの作成 - ソフトウェア技術ドキュメントを勝手に翻訳

Activity#showDialog()で表示するダイアログは、Activity#onCreateDialog()にて別途生成を行います。

show()とshowDialog()の違い

両者の違いは、「DialogがActivityに管理されるかどうか」です。show()だけでは、ダイアログはActivityに管理されません。

Activityにダイアログを管理させておくと、Activity自身の状態変化に応じて、適宜ダイアログを出し入れしてくれます。

管理されないダイアログの問題

よく遭遇するケースは、画面の回転時です。
Activityは回転時にonDestroyされますが、このときshowされっぱなしのDialogはリークを引き起こします。
このとき、logcatには下記のエラーメッセージが表示されます。

05-17 18:24:57.069: ERROR/WindowManager(18850): Activity xxx has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@44c46ff0 that was originally added here
05-17 18:24:57.069: ERROR/WindowManager(18850): android.view.WindowLeaked: Activity xxx has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@44c46ff0 that was originally added here
05-17 18:24:57.069: ERROR/WindowManager(18850): at android.view.ViewRoot.(ViewRoot.java:231)
05-17 18:24:57.069: ERROR/WindowManager(18850): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:148)
05-17 18:24:57.069: ERROR/WindowManager(18850): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
05-17 18:24:57.069: ERROR/WindowManager(18850): at android.view.Window$LocalWindowManager.addView(Window.java:424)
05-17 18:24:57.069: ERROR/WindowManager(18850): at android.app.Dialog.show(Dialog.java:239)

強制終了時のようなスタックトレースですが、Exceptionが発生しているわけではありません。(Exception発生ならアプリが落ちますが、この場合は落ちません)
Androidフレームワークは、Activityを破棄する際にリークの有無を検査します。
リークを検出すると、あらかじめ保持していたwindowのトレース情報を表示します。

AndroidManifest.xmlにて、Activityが回転時に破棄されないようにすれば回転時の問題は回避できます*1が、それ以外でonDestroyが呼ばれるケースには対処できません。
onDestroy内でdismissするという対処もありますが、Activityに管理させておけばそのような記述は必要ありません。さらに、回転時も勝手にダイアログの状態を復元してくれます。
これは、savedInstanceStateとrestoreInstanceState内で、管理されたDialogの保存/復元処理が行われるからです。

結論

ダイアログは特別な理由がなければ、onCreateDialogにより管理させましょう。

*1:android:configChangesに"orientation|keyboardHidden|screenSize"を指定

スクリーンサイズごとにレイアウトxmlを切り替える方法について

Screen-size Buckets

OS3.1までの方法。

buckets dp 具体的な機種
small 426dp × 320dp QVGA[320 * 240] + ldpi = 426*320dp
normal 470dp × 320dp
large 640dp × 480dp WVGA[800 * 480] + mdpi(Dell Streak)、7 インチ タブレット全般。
xlarge 960dp × 720dp WXGA[1280*800] + mdpi (Xoomなどの典型的な10inch tablet)

初代Galaxy Tabは、[1024*600]+hdpi=682*400dpで本来はnormalだが、フレームワークのミスによりlargeとして出荷されてしまった。

The original Samsung Galaxy Tab is an interesting case. Physically it is a 1024x600 7” screen and thus classified as “large”. However the device configures its screen as hdpi, which means after applying the appropriate ⅔ scaling factor the actual space on the screen is 682dp x 400dp. This actually moves it out of the “large” bucket and into a “normal” screen size. The Tab actually reports that it is “large”; this was a mistake in the framework’s computation of the size for that device that we made. Today no devices should ship like this.
Android Developers Blog: New Tools For Managing Screen Sizes

Numeric Selectors

OS 3.2から、指定可能になった方法。

dp type 解釈
320 a phone screen(240*320 ldpi, 320*480 mdpi, 480*800 hdpi, etc) 短辺が320dp
480 a tweener tablet like the Streak(480*800 mdpi) 短辺が480dp
600 7inch tablet(600 * 1024) 短辺が600dp
720 10inch tablet(720 * 1280, 800*1280, etc) 短辺が720dp以上

Combinations and Versions

とりあえず、phoneとtabletのレイアウト対応は下記のようにする。

res/layout/main_activity.xml # For phones
res/layout-xlarge/main_activity.xml # For pre-3.2 tablets
res/layout-sw600dp/main_activity.xml # For 3.2 and up tablets

ただし上記の方法だと、xlargeとsw600dpが共通の場合に冗長になってしまう。
そこでvaluesを使う方法がある。

res/values-xlarge/layout.xml
res/values-sw600dp/layout.xml

NewsReaderの場合

valuesフォルダ layout 対応するレイアウト
values onepane_with_bar phone
values-sw600dp-port onepanes OS3.2以上、7インチ以上のタブレット
values-xlarge-port twopanes_narrow 0S3.2未満、7(?)インチ以上のタブレット
values-xlarge-land twopanes OS3.2未満、7(?)インチ以上のタブレット
values-sw600dp-land twopanes OS3.2以上、7インチ以上のタブレット

※values-v11は、3.0以降のphoneにstyle Holoを指定するためのようだ。

具体例

1280 * 800 OS3.2のタブレット

フォルダ 適用レイアウト
layout, layout-large layout-large
layout, layout-large, layout-xlarge layout-xlarge
layout, layout-large, layout-xlarge, layout-sw600dp layout-sw600dp
layout, layout-large, layout-xlarge, layout-sw600dp, layout-sw720dp layout-sw720dp

OS3.2以降の場合は、swがあればlarge/xlargeより優先されるということ?

LayoutPanelはコンパイルされるとどうなるか

LayoutPanelと、LayoutじゃないPanelを混在させると、意図したレイアウトにならないことが多くて困ってます。
どうもちゃんと理解できていないので、理解をすすめるべく、コンパイルされたhtmlを見てみることにします。

以下で、DockLayoutPanelでJavaのコードとhtmlコード両方を確認してみます。

Javaコード

		DockLayoutPanel panel = new DockLayoutPanel(Unit.PX);
		panel.setStyleName("panel");
		panel.addWest(new Button("west-100"), 100);
		panel.add(new Button("center"));
		RootLayoutPanel.get().add(panel);

コンパイルされたhtml

コメントは、後から挿入したものです。

        <!-- RootLayoutPanel start-->
        <div style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; ">
            <div style="position: absolute; z-index: -32767; top: -20ex; width: 10em; height: 10ex; ">&nbsp;</div>
            <div style="position: absolute; overflow-x: hidden; overflow-y: hidden; left: 0px; top: 0px; right: 0px; bottom: 0px; ">
                <!--  DockLayoutPanel start -->
                <div style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; " class="panel">
                    <div style="position: absolute; z-index: -32767; top: -20ex; width: 10em; height: 10ex; ">&nbsp;</div>
                    
                    <!--  west start (※1) -->
                    <div style="position: absolute; overflow-x: hidden; overflow-y: hidden; left: 0px; top: 0px; bottom: 0px; width: 100px; ">
                        <button type="button" class="gwt-Button" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; ">
                            west-100
                        </button>
                    </div>
                    <!--  west end -->
                    
                    <!--  center start(※2) -->
                    <div style="position: absolute; overflow-x: hidden; overflow-y: hidden; left: 100px; top: 0px; right: 0px; bottom: 0px; ">
                        <button type="button" class="gwt-Button" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; ">
                            center
                        </button>
                    </div>
                    <!--  center end -->
                </div>
                <!--  DockLayoutPanel end -->
            </div>
        </div>
        <!--  RootLayoutPanel end -->

基本はposition:absoluteで四方に0pxが指定されたものをベースに構成されているようです。
west要素(※1)とcenter要素(※2)が要注目です。素のLayoutpanelはtop/bottom/left/rightの四方に0pxが指定されます。
これに対し、west要素のdivはright指定がなく、代わりにwidthがsizeで指定した値になっています。
また、center要素のdivはwestのsize100pxぶんをleftに指定しています。
これでDockLayoutのようなことが実現されているのですね。

CellTree

ShowCaseとjavadocを参考にCellTreeの使い方を調べる。

xmlの定義

 <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
	xmlns:g="urn:import:com.google.gwt.user.client.ui"   xmlns:c="urn:import:com.google.gwt.user.cellview.client">
	(中略)
		      <c:CellTree ui:field="cellTree" />

コードの定義

デフォルトコンストラクタはエラーになるので、
provided = trueにしてコードでインスタンスをprovideする。

    @UiField(provided = true)
    CellTree cellTree;

javadocにサンプルがある

CellTree (Google Web Toolkit Javadoc)
trial sampleとcomplex sampleが載っている。

public class CellTreeExample implements EntryPoint {

  /**
   * The model that defines the nodes in the tree.
   */
  private static class CustomTreeModel implements TreeViewModel {

    /**
     * Get the {@link NodeInfo} that provides the children of the specified
     * value.
     */
    public <T> NodeInfo<?> getNodeInfo(T value) {
      /*
       * Create some data in a data provider. Use the parent value as a prefix
       * for the next level.
       */
      ListDataProvider<String> dataProvider = new ListDataProvider<String>();
      for (int i = 0; i < 2; i++) {
        dataProvider.getList().add(value + "." + String.valueOf(i));
      }

      // Return a node info that pairs the data with a cell.
      return new DefaultNodeInfo<String>(dataProvider, new TextCell());
    }

    /**
     * Check if the specified value represents a leaf node. Leaf nodes cannot be
     * opened.
     */
    public boolean isLeaf(Object value) {
      // The maximum length of a value is ten characters.
      return value.toString().length() > 10;
    }
  }

  public void onModuleLoad() {
    // Create a model for the tree.
    TreeViewModel model = new CustomTreeModel();

    /*
     * Create the tree using the model. We specify the default value of the
     * hidden root node as "Item 1".
     */
    CellTree tree = new CellTree(model, "Item 1");

    // Add the tree to the root layout panel.
    RootLayoutPanel.get().add(tree);
  }
}

選択された行を取得

selectionModelを使うらしい。SingleSelectionModelやMultiSelectionModelがある。ShowCaseはMultiSelectionModel。
引数なしのデフォルトコンストラクタで生成したselectionModelを使ったら、選択がうまく動作しなかった。ProvidesKeyを渡すコンストラクタだとうまく動作した。

#ajn16 のUSTREAMと関連ページ

行けなかったのでUSTREAMで見る。注目はyanzmさんのGWT

内容

セッション1 「Favapp紹介/GWTのUiBinderとか」

発表者:あんざいゆき(@yanzm)さん http://y-anz-m.blogspot.com/
参考URL: こちら
概要:GWT 2.0 から追加された UiBinder を使って Favapp というサービスをわりと簡単に作れたのですが、日本語のドキュメントが全然なかったり、本家のドキュメントもあまり UiBinder にについて詳しくかいてなかったので、そのさいの苦労した点やはまった点を紹介します。
セッション2 「Google I/O 報告会」

発表者:IanMLewis@IanMLewisさん、某Googlerさん、shin1ogawa
参考URL: こちら
概要:5/10, 5/11に開催されたGoogle I/Oで発表されたGoogle App
Engine周辺の詳細な報告と、それに関する考察などをみんなでガヤガヤやろうと思っています。

BeerTalk(懇親会)
21時以降は、会場でピザとビールをいただきながら懇親したりBeerTalkを聞いたりします。

@bluerabbit777jp さん
FrontEndのキャッシュ機能を利用する方法、使ってみた結果について説明します。
@knj77 さん
参加費の決済もできるイベント開催支援サービス「Zusaar」をappengineで作りました。
@kissrobber さん
appengine APIを使った新しいアルゴリズムを発明&実装
appengine ja night #16 : ATND

USTREAM録画のリンク

Ustream.tv: ユーザー kazunori_279: ajn 16 1, Recorded on 5/27/11. 会議

  • GWTについて
  • 落とし穴その1サンプルが少ない(8:00〜)
  • 落とし穴その2 DatePickerはxmlで配置できない(13:00〜)
  • 落とし穴その3使える属性がわかりません(14:28〜)
    • setXXXと対応している
  • 落とし穴その4 DockLayoutPanelの使い方がわからない(17:00〜) ※残念ながら途中で切れてる

Ustream.tv: ユーザー kazunori_279: ajn 16 2, Recorded on 5/27/11. 会議

  • これもできるよ4 PCとモバイルの切り替え

Ustream.tv: ユーザー kazunori_279: ajn 16 3, Recorded on 5/27/11. 会議
Ustream.tv: ユーザー kazunori_279: ajn 16 4, Recorded on 5/27/11. 会議

This application is out of date, please click the refresh button on your browser.

This application is out of date, please click the refresh button on your browser.

Devモードでプロジェクトをrunさせたら、上記のエラーが出た。

Check if the gwt-servlet.jar is uptodate. The gwt-user.jar and gwt-servlet.jar must have the same version.

gwt - This application is out of date, please click the refresh button on your browser. ( Expecting version 4 from server, got 5. ) - Stack Overflow

gwtなjarファイルのバージョンが異なると起こるらしい。
プロジェクトのGWT SDKが2.2.0のままだったので、2.3.0にあげたらエラーは起こらなくなった。