2012年4月26日木曜日

Android Fragment + ViewPager には id が必要

ViewPager を Fragment と一緒に使うときの注意として ViewPager には id がセットされていなければならない、というものがあります。

例えば、次の用に XML レイアウトを使わないで ViewPager を画面に追加した場合、アプリが強制終了します。

public class MainActivity2 extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewPager mViewPager = new ViewPager(this); setContentView(mViewPager); PagerAdapter mAdapter = new PagerAdapter(this); mViewPager.setAdapter(mAdapter); } class PagerAdapter extends FragmentPagerAdapter { private final Context mContext; public PagerAdapter(FragmentActivity activity) { super(activity.getSupportFragmentManager()); mContext = activity; } @Override public int getCount() { return 5; } @Override public Fragment getItem(int position) { Bundle args = new Bundle(); args.putInt("num", position * position); return Fragment.instantiate(mContext, SimpleFragment.class.getName(), args); } } }

public class SimpleFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { TextView tv = new TextView(inflater.getContext()); String message = "Message : " + getArguments().getInt("num"); tv.setText(message); return tv; } }

次のように IllegalArgumentException が投げられるのですが、

04-26 20:56:16.739: E/AndroidRuntime(28770): java.lang.IllegalArgumentException: No view found for id 0xffffffff for fragment SimpleFragment{4183dd90 #0 id=0xffffffff android:switcher:-1:0}
04-26 20:56:16.739: E/AndroidRuntime(28770): at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:864)

ここの部分のコードを見ると次のようになっています。

712 void moveToState(Fragment f, int newState, int transit, int transitionStyle) { ... 722 if (f.mState < newState) { ... 737 switch (f.mState) { ... 781 case Fragment.CREATED: 782 if (newState > Fragment.CREATED) { 784 if (!f.mFromLayout) { 785 ViewGroup container = null; 786 if (f.mContainerId != 0) { 787 container = (ViewGroup)mActivity.findViewById(f.mContainerId); 788 if (container == null && !f.mRestored) { 789 throw new IllegalArgumentException("No view found for id 0x" 790 + Integer.toHexString(f.mContainerId) 791 + " for fragment " + f); 792 } 793 }

これをみると、 Fragment (この場合は ViewPager の中身に Fragment のこと)がレイアウトから生成されていない場合で、Fragment の コンテナの Id が 0 でない場合に、その Id に対応する View が見つからないと上記のエラーの IllegalArgumentException が発行されることがわかります。

上記のエラーメッセージでは Fragment のコンテナの Id として 0 ではなく、0xffffffff が渡されていることがわかります。

ViewPager に明示的に Id をセットするとこのエラーはなくなります。

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewPager mViewPager = new ViewPager(this); setContentView(mViewPager); mViewPager.setId(100); PagerAdapter mAdapter = new PagerAdapter(this); mViewPager.setAdapter(mAdapter); }

ここでは適当に 100 としましたが、こうするのは良くないです。

次のように res で id を定義し、それを使います。

<?xml version="1.0" encoding="utf-8"?> <resources> <item name="viewpager" type="id"/> </resources>

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewPager mViewPager = new ViewPager(this); setContentView(mViewPager); mViewPager.setId(R.id.viewpager); PagerAdapter mAdapter = new PagerAdapter(this); mViewPager.setAdapter(mAdapter); }

2012年4月20日金曜日

Android Up の振る舞いパターンを実装する

Android Design での Up と Back のガイドライン の振る舞いのパターンの実装方法を紹介します。

まず、Up と Back はそれぞれ次のように使い分けます。

・Up : 画面間の階層関係に基づいたアプリ内のナビゲーションに使う
・Back : 最近行った操作を逆時系列順にさかのぼるナビゲーションに使う

そのため、次のような違いがあります。

遷移範囲
・Up : アプリ内の遷移だけ
・Back : アプリ内だけでなく別のアプリやホームアプリにも遷移する

振る舞い
・Up : 画面遷移だけ
・Back : 画面遷移の他に、フローティングウィンドウ(ダイアログやポップアップ)のキャンセル、Action Mode のキャンセルや選択中のアイテムのキャンセル、ソフトキーボードを隠すなどの操作にも使われる


基本的には、Up は一つ上の階層に戻るナビゲーションに使います。


http://developer.android.com/design/media/navigation_up_vs_back_gmail.png

Up はアプリ内の遷移にだけ使うので、アプリのホーム画面には Up ボタンは置きません。



アプリ内のナビゲーション


・同じアプリの複数画面から遷移される画面での Up ボタンは、前の画面に戻るようにする

例えば、設定画面は同じアプリのいろいろな画面から遷移できるようになっていることが多いです。このようなエントリポイントがたくさんある画面では、Up ボタンが押されたときに前の画面に戻るようにします。つまり Back ボタンと同じ振る舞いです。

実装としては、finish() が適当でしょう。
@Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: finish(); return true; } return super.onOptionsItemSelected(item); }

・画面内での View の切り替えでは、Up や Back の振る舞いを変えない

例えば

  - タブやスワイプによる View の切り替え
  - ドロップダウンによる View の切り替え
  - リストのフィルタリング
  - リストのソート
  - ズームなどによる表示切り替え

ではアプリの階層は変わらず、Back 用のナビゲーション履歴も増えません。そのため、Up のナビゲーションをこの切り替えによって変更することはしません。



・同じ Activity 内での連続する画面の切り替えでは、Up や Back の振る舞いを変えない

Gmail アプリのように、リスト画面からその詳細画面に遷移し、そこからスワイプで前後のリストアイテムの詳細画面に遷移する場合も、アプリの階層は変わらず Back 用のナビゲーション履歴も増えません。

http://developer.android.com/design/media/navigation_between_siblings_gmail.png

この場合、詳細画面上の Up ボタンでは 1階層上に戻るようにします。リスト画面からしか詳細画面に遷移しないのであれば finish() でもいいでしょう。
Intent.FLAG_ACTIVITY_CLEAR_TOP と Intent.FLAG_ACTIVITY_SINGLE_TOP を組み合わせてstartActivity() を使ってリスト画面を呼び出す方法もあります。

ConversationDetails @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: Intent intent = new Intent(this, ConversationList.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); return true; } return super.onOptionsItemSelected(item); }
いずれもスタック上は
| ConversationList |
 ↓ 
| ConversationList   ConversationDetails |
 ↓ Up
| ConversationList |

という動作になります。



・同じ階層の別 Activity への切り替えでは、Up の振る舞いを変えない

http://developer.android.com/design/media/navigation_between_siblings_market1.png

別 Activity への遷移のため、Back 用のナビゲーション履歴が増えます。しかし、アプリの階層として同じ位置にあたる画面なので、Up ボタンの振る舞いは Back と異なり一つ上の階層に遷移するようにします。

スタック上の動作としては次のようになります。
| BookList | 
 ↓
| BookList  Book1Details |
 ↓    ↓ Up
 ↓   | BookList |
 ↓ 
| BookList  Book1Details  Book2Details |
 ↓ Up
| BookList |

この場合、Book2Details の Up の振る舞いとして finish() は使えません。いずれも Intent.FLAG_ACTIVITY_CLEAR_TOP と Intent.FLAG_ACTIVITY_SINGLE_TOP を組み合わせてstartActivity() を使ってリスト画面を呼び出す方法がいいでしょう。


Book1Details
Book2Details @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: Intent intent = new Intent(this, BookList.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); return true; } return super.onOptionsItemSelected(item); }
アプリの内部構成が複数のカテゴリにわかれていて、あるカテゴリの下の階層の画面から別のカテゴリの下の階層に遷移した場合、遷移先の画面の Up の振る舞いはそのカテゴリ内での1つ上の階層に戻ることです。

http://developer.android.com/design/media/navigation_between_siblings_market2.png

実装としては、次のように BookDetails と MovieDetails で startActivity() 先を変えます。


Book1Details
Book2Details @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: Intent intent = new Intent(this, BookList.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); finish(); return true; } return super.onOptionsItemSelected(item); }
Movie1Details @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: Intent intent = new Intent(this, MovieList.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); finish(); return true; } return super.onOptionsItemSelected(item); }
スタック上の動作はこうなります。

| Top  BookList | 
 ↓
| Top  BookList  Book1Details |
 ↓    ↓ Up
 ↓   | Top  BookList |
 ↓ 
| Top  BookList  Book1Details  Book2Details |
 ↓    ↓ Up
 ↓   | Top  BookList |
 ↓ 
| Top  BookList  Book1Details  Book2Details  Movie1Details |
 ↓ Up
| Top  BookList  Book1Details  Book2Details  MovieList |



ホープアプリ上のウィジェットやノーティフィケーションからのナビゲーション

ウィジェットやノーティフィーケションから遷移した画面上の Up ボタンの振る舞いは、次のようにします。

 ・同じアプリの特定の画面にいるときにノーティフィーケションから遷移した場合は、その特定の画面に戻る
 ・それ以外は、アプリのホーム画面(トップ画面)に遷移する


http://developer.android.com/design/media/navigation_from_outside_back.png

タスクのバックスタックにホーム画面を挿入しておくことで、Back ボタンでアプリを抜ける際に、どうやって遷移先の画面に移動したのかを忘れているユーザーをホーム画面にナビゲートすることができます。

API Level 11 から追加された PendingIntent#getActivities(Context context, int requestCode, Intent[] intents, int flags) を使うと、スタックに複数の Activity を挿入した状態にすることができます。

例えば、 public class WidgetProvider extends AppWidgetProvider { @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); Intent[] intents = new Intent[2]; intents[0] = new Intent(context, MainActivity.class); intents[1] = new Intent(context, MainActivity2.class); PendingIntent pendingIntent = PendingIntent.getActivities(context, 0, intents, Intent.FLAG_ACTIVITY_NEW_TASK); views.setOnClickPendingIntent(R.id.btn, pendingIntent); appWidgetManager.updateAppWidget(appWidgetIds, views); } } のようにすると

| ホームアプリ |
 ↓
 ↓ ホーム画面のウィジェットの R.id.btn をタップ
 ↓
| ホームアプリ | MainActivity  MainActivity2 |

のようになります。

そのため、ここで Back ボタンを押すと MainActivity に遷移します。

MainActivity2 を @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: Toast.makeText(this, "Hello", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); finish(); return true; } return super.onOptionsItemSelected(item); } としておけば、Up を押した場合も MainActivity に遷移するようになります。

ちなみに MainActivity, MainActivity2 両方とも launchMode="standard" で、すでにアプリを起動している状態からだと

| ホームアプリ | MainActivity |
 ↓
| ホームアプリ | MainActivity  MainActivity2 |
 ↓ Home ボタン
| MainActivity  MainActivity2 | ホームアプリ |
 ↓
 ↓ ホーム画面のウィジェットの R.id.btn をタップ
 ↓
| ホームアプリ | MainActivity  MainActivity2 | MainActivity(2)  MainActivity2(2) | 

のようなスタックになります。

MainActivity だけ launchMode="singleTask" にすると

| ホームアプリ | MainActivity |
 ↓
| ホームアプリ | MainActivity  MainActivity2 |
 ↓ Home ボタン
| MainActivity  MainActivity2 | ホームアプリ |
 ↓
 ↓ ホーム画面のウィジェットの R.id.btn をタップ
 ↓
| ホームアプリ | MainActivity  MainActivity2(2) | 

のように MainActivity2 は新しく作られ、そこから Back で戻った先の MainActivity は以前のインスタンスになります。



・インダイレクトノーティフィケーション

複数のイベントの情報を同時に提供するときに、単一のノーティフィケーションを通知して、そこから複数のイベントについてまとめた中間画面(interstitial screen)にユーザーを導き、そこからイベントに対する処理や対応するアプリ画面への遷移を提供するスタイルをインダイレクトノーティフィケーション(indirect notifications)といいます。

中間画面で Back ボタンが押されたときは、間によけいなスタックを入れずノーティフィケーションを呼び出した画面に戻るようにします。
中間画面からアプリの対応する画面に遷移したら、そこから Back や Up ボタンでは上記と同じようにアプリのホーム画面(トップ画面)を経由するようにします。中間画面には戻りません。

http://developer.android.com/design/media/navigation_indirect_notification.png



・ポップアップノーティフィケーション

電話がかかってきたときや、Gtalk でビデオチャットの招待がきたときなど、すぐに対応するべき通知はポップアップで表示されることがあります(すぐに対応しなければならない通知以外では使ってはいけない)。

ポップアップノーティフィケーションでの Up / Back の振る舞いはインダイレクトノーティフィケーションと同じような感じで、ポップアップが出たときに Back でキャンセルするとポップアップが消え、ポップアップからアプリに遷移した後は、Up と Back ボタンではアプリのホーム画面(トップ画面)を経由するようにします。

http://developer.android.com/design/media/navigation_popup_notification.png




アプリ間のナビゲーション

Android の大きな特徴として別のアプリの画面をあたかも自分のアプリの続きのように遷移することができる、という点があります。

アプリA から アプリB のある画面を呼び出してその結果を受け取りたい場合は、アプリB のある画面(の Activity)を アプリA のタスク内で起動しなければなりません。 例えば、launchMode="singleTask" の Activity を startActivityForResult() で起動した場合、すぐに RESULT_CANCELED が返ってきてしまいます。

例えば、共有をサポートするアプリを呼び出した場合、

http://developer.android.com/design/media/navigation_between_apps_inward.png

呼び出し先の Activity は呼び出し元と同じタスクになります。

呼び出し先で Back ボタンが押されたり、呼び出し先での処理が完了して finish() した場合は、呼び出し元の画面に戻ります。

http://developer.android.com/design/media/navigation_between_apps_back.png

一方、呼び出し先で Up ボタンが押された場合、ユーザーは呼び出し先のアプリに留まりたいということなので、新しいタスクとして呼び出し先のホーム画面(もしくは1階層上の画面)に遷移するようにします。そこからさらに Back ボタンでアプリを終了した場合は、呼び出し元のアプリではなくホームアプリに戻るようにします。

http://developer.android.com/design/media/navigation_between_apps_up.png

最初の呼び出し元のタスクはバックグラウンドで保持され、ユーザーは後で Recent apps から戻ることができます。すでに呼び出し先のアプリ自身のタスクが走っている状態だと、Up ボタンが押された場合は、そのタスクが新しいタスクに置き換えられます。

新しくタスクを起動し、それ以外のタスクをホームアプリのバックグラウンドに移すには、次のように Intent.FLAG_ACTIVITY_NEW_TASK と Intent.FLAG_ACTIVITY_TASK_ON_HOME を組み合わせます。 @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: Toast.makeText(this, "Hello", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); finish(); return true; } return super.onOptionsItemSelected(item); }


2012年4月19日木曜日

Android Layout Cookbook と Android UI Cookbook for 4.0 の電子書籍がでました!



インプレスジャパンダイレクト ショッピング&サービス -

■キャンペーン期間(4/19~4/25)
・¥1,600 Android UI Cookbook for 4.0 ICS(Ice Cream Sandwich)アプリ開発術
・¥1,600 Android Layout Cookbook アプリの価値を高める開発テクニック
・¥3,000 2点セット販売

■キャンペーン終了後(4/26~)
・¥2,600 Android UI Cookbook for 4.0 ICS(Ice Cream Sandwich)アプリ開発術
・¥2,600 Android Layout Cookbook アプリの価値を高める開発テクニック

4/26 まで割引価格(紙の書籍の半額)です!

Android カスタム ViewGroup 用XMLのルートタグを <merge> にする

レイアウトXMLファイルから View を生成するときに LayoutInfalter の inflate() メソッドを使いますが、inflate() メソッドには引数が2つのものと3つのものがあります。 引数が2つのメソッドは、内部で inflate(resource, root, root != null) のように引数が3つのメソッドを呼んでいます。
この attachToRoot に true を指定した場合と false にした場合で返ってくるルートビューが異なります。

例えば <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout> LayoutInflater inflater = getLayoutInflater(); FrameLayout root = new FrameLayout(this); に対して View v = inflater.inflate(R.layout.main, root, true); とした場合は v は FrameLayout (つまり root)になります。

一方、 View v = inflater.inflate(R.layout.main, root, false); または View v = inflater.inflate(R.layout.main, null, true); or View v = inflater.inflate(R.layout.main, null, false); とした場合は v は LinearLayout (つまり R.layout.main のルートビュー)になります。

inflater.inflate(R.layout.main, root, false)



inflater.inflate(R.layout.main, null, false)

の違いは、root が null でない場合はそれに合うような LayoutParams が返ってくるルートビューにセットされるという点です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/LayoutInflater.java#471 471 if (root != null) { 472 if (DEBUG) { 473 System.out.println("Creating params from root: " + 474 root); 475 } 476 // Create layout params that match root, if supplied 477 params = root.generateLayoutParams(attrs); 478 if (!attachToRoot) { 479 // Set the layout params for temp if we are not 480 // attaching. (If we are, we use addView, below) 481 temp.setLayoutParams(params); 482 } 483 } さて、この inflate() メソッドをみると、<merge> タグのチェックを行っている箇所があります。 424 public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { ... 453 if (TAG_MERGE.equals(name)) { 454 if (root == null || !attachToRoot) { 455 throw new InflateException("<merge /> can be used only with a valid " 456 + "ViewGroup root and attachToRoot=true"); 457 } 458 459 rInflate(parser, root, attrs, false); 460 } else { XML の最初のスタートタグが <merge> だった場合、root が null だったり attachToRoot が false だと InflateException が投げられます。

考えてみれば当たり前ですね。<merge> タグは addView されたときに親の View と合体するということなので、合体対象がいない場合戻り値の View として返すものがなくなってしまいます。



どういう場面で <merge> タグのレイアウトを inflate() することがあるかというとオリジナルの ViewGroup を作る場合です。

例えば、ボタンが縦に3つ並んだ LinearLayout をオリジナルの ViewGroup にしたいとします。

つまり、 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> </LinearLayout> </LinearLayout> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <yanzm.example.viewgroupmerge.MyViewGroup android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> にしたいということです。

そのためには、この MyViewGroup 自身に縦に並ぶボタン3つを持たせる必要があります。


初心者がやりがちなのがこういうコードです。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> </LinearLayout> public class MyViewGroup extends FrameLayout { public MyViewGroup(Context context) { super(context); init(context); } public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public MyViewGroup(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflater.inflate(R.layout.custom_layout, null, false); addView(v); } } これでも動きますが、View 階層が一つ多くなってしまうのでよくありません。 そこで、<merge> タグを使って次のようにすると、元と同じ View 階層にとどめておけます。 <?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> </merge> public class MyViewGroup extends LinearLayout { public MyViewGroup(Context context) { super(context); init(context); } public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public MyViewGroup(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { setOrientation(LinearLayout.VERTICAL); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflater.inflate(R.layout.custom_layout, this, true); } } こうすれば <merge> 部分が MyViewGroup と合体してくれます。


さらに、View には inflate(Context context, int resource, ViewGroup root) という static メソッドがあり、 このメソッドを使ってさらに簡単に書けます。 private void init(Context context) { setOrientation(LinearLayout.VERTICAL); View.inflate(context, R.layout.custom_layout, this); } LayoutInflater のインスタンスを取得するとしては以下の方法をよく使います。


2012年4月18日水曜日

DartEditor の Dart Web Launch に Arguments がついた!

DartEditor の Dart Web Launch が

Version 0.1.0.201203261044, Build 5845 だと


だったのが

Version 0.1.0.201204121423, Build 6479 では


Arguments が増えてる!

これで何がうれしいかというと、DartEditor で less を使う で書いためんどいテクニックがいらないということです。
単にこの Arguments に --allow-file-access-from-files を入れればよくなったのでらくちん!



他にも

・Preferences に Enable AnalysisServer のチェックボックスが増えた



・Tools に Callers が増えて、Help にあった Welcome がここに移動した

・Force Recompile が Re-Analyze Sources に変わった



・Help に API Reference... が増えた






Android Fragment は破棄時に保持している View の状態を保存させている

前々回(Android Fragment で setArguments() してるサンプルが多いのはなぜ?)に Argument を使えば再生成時に値を引き継げることを書きました。
前回(Android レイアウトから生成した Fragment は FragmentTransaction の対象にしてはいけない)はレイアウトから生成した Fragment には setArguments() できないことを書きました。

では、レイアウトから生成した Fragment で再生成時に以前の状態を引き継ぐにはどうしたらいいのか、ということなんですが、Activity と同じように onSaveInstanceState(Bundle outState) 時に引数で渡される Bundle に入れておけば onCreate(Bundle), onCreateView(LayoutInflater, ViewGroup, Bundle), onActivityCreated(Bundle) のときに渡される Bundle から取り出すことができます。

自分の Fragment が持つ状態(例えばフィールドの値など)はこの方法で引き継ぐのが普通ですが、独自の View を使っていて、その View の状態(つまりカスタムビュークラスのフィールド値など)を引き継ぐには、View の onSaveInstanceState() で引き継ぎたい値を格納した Parcelable を返すのが普通です。

例えば、TextView では現在のカーソルの位置を、ListView では現在選択されているアイテムのIDなどを Parcelable として格納しています。

この View の onSaveInstanceState() は saveHierarchyState(SparseArray<Parcelable> container) をトリガーとして呼ばれます。

Fragment が破棄される部分のコード(#873以後)を見てみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#874 712 void moveToState(Fragment f, int newState, int transit, int transitionStyle) { ... 722 if (f.mState < newState) { ... 781 case Fragment.CREATED: 782 if (newState > Fragment.CREATED) { ... 819 if (f.mView != null) { 820 f.restoreViewState(); 821 } ... 850 } else if (f.mState > newState) { 851 switch (f.mState) { ... 873 case Fragment.STOPPED: 874 case Fragment.ACTIVITY_CREATED: 875 if (newState < Fragment.ACTIVITY_CREATED) { 876 if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f); 877 if (f.mView != null) { 878 // Need to save the current view state if not 879 // done already. 880 if (!mActivity.isFinishing() && f.mSavedViewState == null) { 881 saveFragmentViewState(f); 882 } 883 } Fragment が View を持っている場合は saveFragmentViewState() というメソッドを呼んでいます。

このメソッドでは

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1435 1435 void saveFragmentViewState(Fragment f) { 1436 if (f.mView == null) { 1437 return; 1438 } 1439 if (mStateArray == null) { 1440 mStateArray = new SparseArray<Parcelable>(); 1441 } else { 1442 mStateArray.clear(); 1443 } 1444 f.mView.saveHierarchyState(mStateArray); 1445 if (mStateArray.size() > 0) { 1446 f.mSavedViewState = mStateArray; 1447 mStateArray = null; 1448 } 1449 } このように、View の saveHierarchyState() が呼ばれていることがわかります。

View の saveHierarchyState() では

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/View.java#9787 9787 public void saveHierarchyState(SparseArray<Parcelable> container) { 9788 dispatchSaveInstanceState(container); 9789 } 9802 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 9803 if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { 9804 mPrivateFlags &= ~SAVE_STATE_CALLED; 9805 Parcelable state = onSaveInstanceState(); 9806 if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) { 9807 throw new IllegalStateException( 9808 "Derived class did not call super.onSaveInstanceState()"); 9809 } 9810 if (state != null) { 9811 // Log.i("View", "Freezing #" + Integer.toHexString(mID) 9812 // + ": " + state); 9813 container.put(mID, state); 9814 } 9815 } 9816 } このように、onSaveInstanceState() を呼んで返ってきた Parcelable を引数の SparseArray に格納しています。

ViewGroup ではこの dispatchSaveInstanceState() が Override されており

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/ViewGroup.java#2294 2294 @Override 2295 protected void dispatchSaveInstanceState(SparseArray container) { 2296 super.dispatchSaveInstanceState(container); 2297 final int count = mChildrenCount; 2298 final View[] children = mChildren; 2299 for (int i = 0; i < count; i++) { 2300 View c = children[i]; 2301 if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) { 2302 c.dispatchSaveInstanceState(container); 2303 } 2304 } 2305 } 自身だけでなく子 View の dispatchSaveInstanceState() も呼ぶようになっています。

onSavedInstanceState() で返した Parcelable は onRestoreInstanceState(Parcelable state) で取り出すことができます。このメソッドは restoreHierarchyState(SparseArray<Parcelable> container) をトリガーとして呼ばれます。そして、Fragment が生成されるときに呼ばれる restoreViewState() メソッド内で restoreHierarchyState() を呼んでいます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#586 586 final void restoreViewState() { 587 if (mSavedViewState != null) { 588 mView.restoreHierarchyState(mSavedViewState); 589 mSavedViewState = null; 590 } 591 }

つまり、カスタムビューを作る場合、その中だけで引き継ぎが完結する状態(private なフィールド値など)は View の onSaveInstanceState() を利用しましょう。そうすることで Fragment が View のフィールド値をわざわざ取り出して引き継ぐような依存性を回避できます。





2012年4月17日火曜日

Android レイアウトから生成した Fragment は FragmentTransaction の対象にしてはいけない

■ レイアウトから作成した Fragment には setArguments できない

前回のエントリで Fragment の Arguments の利点をいろいろ紹介しましたが、レイアウト内に <fragment> タグで定義して生成した Fragment には setArguments() をすることができません。

まず、Fragment.java のコードをみると Arguments を保持するフィールドである mArguments のコメントとして“生成時の引数である”と書いてあります。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#370 370 // Construction arguments; 371 Bundle mArguments; つまり、生成したあとの任意のタイミングでセットするようなものではない、ということです。

さらに、setArguments() の実装をみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#652 659 public void setArguments(Bundle args) { 660 if (mIndex >= 0) { 661 throw new IllegalStateException("Fragment already active"); 662 } 663 mArguments = args; 664 } 665 Fragment がアクティブになっている( = mIndex が 0 より大きい)ときに呼ぶと IllegalStateException が投げられることがわかります。

では、mIndex (初期値は -1)はいつセットされるのかというと、FragmentManager の makeActive() メソッドで行われます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1009 1009 void makeActive(Fragment f) { 1010 if (f.mIndex >= 0) { 1011 return; 1012 } 1013 1014 if (mAvailIndices == null || mAvailIndices.size() <= 0) { 1015 if (mActive == null) { 1016 mActive = new ArrayList(); 1017 } 1018 f.setIndex(mActive.size()); 1019 mActive.add(f); 1020 1021 } else { 1022 f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1)); 1023 mActive.set(f.mIndex, f); 1024 } 1025 }

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#593 593 final void setIndex(int index) { 594 mIndex = index; 595 mWho = "android:fragment:" + mIndex; 596 } この makeActive() メソッドは FragmentManager の addFragment() から呼ばれています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1042 1042 public void addFragment(Fragment fragment, boolean moveToStateNow) { 1043 if (mAdded == null) { 1044 mAdded = new ArrayList(); 1045 } 1046 if (DEBUG) Log.v(TAG, "add: " + fragment); 1047 makeActive(fragment); 1048 if (!fragment.mDetached) { 1049 mAdded.add(fragment); 1050 fragment.mAdded = true; 1051 fragment.mRemoving = false; 1052 if (fragment.mHasMenu && fragment.mMenuVisible) { 1053 mNeedMenuInvalidate = true; 1054 } 1055 if (moveToStateNow) { 1056 moveToState(fragment); 1057 } 1058 } 1059 } レイアウトで定義された <fragment> は、 Activity の onCreateView() メソッドからこの addFragment() を呼ぶことで生成されます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4189 4199 public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { 4200 if (!"fragment".equals(name)) { 4201 return onCreateView(name, context, attrs); 4202 } ... 4223 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; ... 4234 if (fragment == null) { 4235 fragment = Fragment.instantiate(this, fname); 4236 fragment.mFromLayout = true; 4237 fragment.mFragmentId = id != 0 ? id : containerId; 4238 fragment.mContainerId = containerId; 4239 fragment.mTag = tag; 4240 fragment.mInLayout = true; 4241 fragment.mFragmentManager = mFragments; 4242 fragment.onInflate(this, attrs, fragment.mSavedFragmentState); 4243 mFragments.addFragment(fragment, true); ... 4263 } 4265 if (fragment.mView == null) { 4266 throw new IllegalStateException("Fragment " + fname 4267 + " did not create a view."); 4268 } 4269 if (id != 0) { 4270 fragment.mView.setId(id); 4271 } 4272 if (fragment.mView.getTag() == null) { 4273 fragment.mView.setTag(tag); 4274 } 4275 return fragment.mView; 4276 } このとき、引数が2つの Fragment.instantiate(Context context, String fname) で Fragment のインスタンスを生成していることに注目してください。このメソッドは第3引数の Bundle を null として instantiate(Context context, String fname, Bundle args) を呼びます。そのため、レイアウトに定義された Fragment は Argument なし(つまり null)で生成されることがわかります。

この onCreateView() は Activity 内での setContentView() をトリガーとして呼ばれます。

レイアウトから生成される Fragment に Argument をセットしないようになっているのは、必要がないからだと思います。そもそも単体で破棄される Fragment はバックスタックにある場合で、 Fragment のもっている View が Activity のレイアウトの一部になっている場合は Activity と一緒に破棄、再生成されます。 それならば setContentView() の後に FragmentManager#getFragmentById() で Fragment を取得して setter なりで値を渡せばいいわけです。

■ レイアウトから生成される Fragment はバックスタックに移動させないのが普通

実は、レイアウトに定義している Fragment に対し、単に FragmentTransaction の remove() を呼んだ場合、Fragment の保持している View のフィールドは null にセットされますが、レイアウトから View は削除されません。

FragmentManager の moveToState() メソッドを見てみましょう。

初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。

生成の段階(#781)では、レイアウトから生成していない Fragment であればコンテナ(Fragment の View の追加先の ViewGroup)の ID からコンテナのインスタンスを取得して mContainer として保持し、onCreateView() から生成した View をこのコンテナに追加しています。

破棄の段階(#874)では、Fragment の持つ View とそのコンテナが両方とも null ではない場合にコンテナから View を削除しています。レイアウトから生成した Fragment はコンテナが null のままなので、この if 文のなかには入らず View は削除されません。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#712 712 void moveToState(Fragment f, int newState, int transit, int transitionStyle) { ... 722 if (f.mState < newState) { ... 737 switch (f.mState) { 738 case Fragment.INITIALIZING: ... 769 if (f.mFromLayout) { 770 // For fragments that are part of the content view 771 // layout, we need to instantiate the view immediately 772 // and the inflater will take care of adding it. 773 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState), 774 null, f.mSavedFragmentState); 775 if (f.mView != null) { 776 f.mView.setSaveFromParentEnabled(false); 777 if (f.mHidden) f.mView.setVisibility(View.GONE); 778 f.onViewCreated(f.mView, f.mSavedFragmentState); 779 } 780 } 781 case Fragment.CREATED: 782 if (newState > Fragment.CREATED) { ... 784 if (!f.mFromLayout) { 785 ViewGroup container = null; 786 if (f.mContainerId != 0) { 787 container = (ViewGroup)mActivity.findViewById(f.mContainerId); ... 793 } 794 f.mContainer = container; 795 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState), 796 container, f.mSavedFragmentState); 797 if (f.mView != null) { 798 f.mView.setSaveFromParentEnabled(false); 799 if (container != null) { ... 806 container.addView(f.mView); 807 } 808 if (f.mHidden) f.mView.setVisibility(View.GONE); 809 f.onViewCreated(f.mView, f.mSavedFragmentState); 810 } ... 811 } 812 ... 849 } 850 } else if (f.mState > newState) { 851 switch (f.mState) { ... 873 case Fragment.STOPPED: 874 case Fragment.ACTIVITY_CREATED: 875 if (newState < Fragment.ACTIVITY_CREATED) { ... 890 if (f.mView != null && f.mContainer != null) { ... 918 f.mContainer.removeView(f.mView); 919 } 920 f.mContainer = null; 921 f.mView = null; 922 } ... 970 } 971 } 972 973 f.mState = newState; 974 } 975 remove() ではなく replace() で別の Fragment に置き換えた場合、Activity を再生成する(画面回転など)と IllegalStateException で落ちます。

例えば <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <Button android:id="@+id/button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="show dialog" /> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" > <fragment android:id="@+id/fragment" android:layout_width="match_parent" android:layout_height="match_parent" class="yanzm.example.dialogfragmentsample.MainActivity$MyFragment" /> </FrameLayout> </LinearLayout> に対してボタンが押されたら R.id.container 内の Fragment を入れ替えるようにします。 public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { switchFragment(); } }); } private void switchFragment() { Fragment fragment = new MyFragment2(); getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit(); } public static class MyFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.main2, container, false); } } public static class MyFragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.main4, container, false); } } } ボタンを押して MyFragment を MyFragment2 に入れ替えた状態で画面を回転させると落ちます。

Activity の onCreateView() をもう一度みてみましょう。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4223 4223 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; 4224 if (fragment == null && tag != null) { 4225 fragment = mFragments.findFragmentByTag(tag); 4226 } 4227 if (fragment == null && containerId != View.NO_ID) { 4228 fragment = mFragments.findFragmentById(containerId); 4229 } ... 4234 if (fragment == null) { 4235 fragment = Fragment.instantiate(this, fname); ... 4263 } 4264 4265 if (fragment.mView == null) { 4266 throw new IllegalStateException("Fragment " + fname 4267 + " did not create a view."); 4268 } fragment のインスタンスを見つける順番として次の段階を踏みます。

1. android:id で指定されているID
2. 1. で見つからなかったら android:tag で指定されているタグ名
3. 2. でも見つからなかったら親 View の ID

ここで思いだして欲しいのが Fragment は id が明示的に指定されていない場合、親の id を自分の id として持つ、ということです。

上記のコードでは Fragment fragment = new MyFragment2(); getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit(); によって、MyFragment2 の id には R.id.container が入ることになります。そのため、画面回転時には

3. 2. でも見つからなかったら親 View の ID

の段階で MyFragment2 のインスタンスが見つかってしまうということです。そのため、#4234 の if 文には入らず、Fragment はクラス名から生成されません。そのまま #4265 に行くのですが、MyFragment2 はレイアウトから生成されたわけではないので、この段階ではまだ View は生成されていません。そのため、IllegalStateException が投げられてしまうのです。

この流れについては、上記の

初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。


の部分を思い出してください。



結論としては

レイアウトから生成する Fragment は FragmentTransaction に対象にしない。FragmentTransaction で入れ替える Fragment はコードから生成する。

ということですね。







2012年4月16日月曜日

Android Fragment で setArguments() してるサンプルが多いのはなぜ?

Fragment のサンプルでは、setArguments() を使って Bundle を介して値を渡している例を多く見かけます。

HogeFragment f = new HogeFragment(); Bundle args = new Bundle(); args.putInt("num", num); f.setArguments(args); とやるより

HogeFragment f = new HogeFragment(num);

HogeFragment f = new HogeFragment(); f.setNum(num); とかやった方がいいんじゃない? Arguments 介するのは面倒じゃない?なにがいいの? と思う人も多いのではないでしょうか。

そこで、Arguments がどういいのかを説明したいと思います。


1. Fragment のコンストラクタで引数を渡すのはダメ

Fragment のリファレンス に書いてあるように、

All subclasses of Fragment must include a public empty constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the empty constructor is not available, a runtime exception will occur in some cases during state restore.


Fragment を継承したクラスは空のコンストラクタを用意しないと行けません。メモリが足りなくなったときに Activity スタック内の(バックグラウンドの)Activity が破棄されるように、Fragment もメモリが足りなくなったときは破棄されます。破棄された Fragment が再び必要になったときにシステムは空のコンストラクタから Fragment のインスタンスを再生成します。
*そのため、空のインスタンスの用意されていない Fragment が上記の状態に遭遇するとアプリが落ちます。


2. setter では再生成時に値を渡せない

上記の再生成時に使われるのが Fragment#instantiate(Context context, String fname, Bundle args) メソッドです。 このメソッドは引数で渡されたクラス名の Fragment の空のコンストラクタを呼び出してインスタンスを生成し、第3引数でセットした Bundle を setArguments() でセットしてから返します。

つまり、② だと、再生成されたときに引数に num を取るコンストラクタが呼ばれないし、③ だと、再生成されたときに setter が呼ばれないため num を Fragment に渡せません。 しかし、① であれば、HogeFragment が破棄されるときにシステムが getArguments() で num の値を Bundle に保存して再生成のときにその Bundle を setArguemnts() でセットしてくれるので、再生成時も num の値を HogeFragment に渡すことができるのです。


この再生成時に値を渡せないという問題は DialogFragment ではより顕著になります。
例えば、次のようにボタンを押したらダイアログが表示されるというコードがあるとします。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <Button android:id="@+id/button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="show dialog" /> </LinearLayout> public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showDialog(); } }); } void showDialog() { MyAlertDialogFragment newFragment = MyAlertDialogFragment.newInstance(R.string.hello); newFragment.setNum(10); newFragment.show(getFragmentManager(), "dialog"); } public static class MyAlertDialogFragment extends DialogFragment { private int mNum; public static MyAlertDialogFragment newInstance(int title) { MyAlertDialogFragment frag = new MyAlertDialogFragment(); Bundle args = new Bundle(); args.putInt("title", title); frag.setArguments(args); return frag; } public void setNum(int num) { mNum = num; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { int title = getArguments().getInt("title"); return new AlertDialog.Builder(getActivity()) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(title) .setMessage(mNum + "") .setPositiveButton(android.R.string.ok, null) .setNegativeButton(android.R.string.cancel, null) .create(); } } } DialogFragment を使うと、ダイアログが表示されている状態で Activity が再生成された場合に自動でダイアログも再表示してくれます。

例えば、上記のコードでボタンを押してダイアログが表示された状態で画面を回転させると新しく onCreate() が呼ばれますが、ボタンは押されてないので本来はダイアログも消えてしまいます。しかし Dialog ではなく DialogFragment を使っているのでシステムが DialogFragment を再生成して再び表示してくれるのです。

この際の再生成は上記コードの showDialog() を通らず、空のコンストラクタから生成されるため setNum() は呼ばれません。よって回転させると、Arguments としてセットしたタイトルは回転前と同じですが、メッセージ(mNum の値)は 10 ではなく 0 になります。


このように、Fragment はシステムから再生成されることが多いので、setter を使うよりも Arguments を介したほうが最終的にいろいろ楽になります。
ということで、オレオレ setter ではなく Arguemts を使うようにしましょう!



2012年4月12日木曜日

Android ListView でデータが空のときもヘッダー・フッターを表示する

ListView には addHeaderView()addFooterView() でヘッダーやフッターをつけることができます。

また、ListView にはリストのデータが空の時に表示させる emptyView を指定することができます。データがないときに画面が真っ黒になるとユーザーはアプリが壊れたと思ってしまうかもしれないので、空のときにはメッセージをだしましょうとよく言われます。

ただ、この emptyView を指定するとリストのデータが空のときに、ヘッダーやフッターも表示されなくなります。

emptyView を指定している状態でヘッダーやフッターを表示できるのか調べてみました。

まず、ListView で emptyView への切り替えをどこでしているかいうと AdapterView の updateEmptyStatus(boolean empty) です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717 717 private void updateEmptyStatus(boolean empty) { 718 if (isInFilterMode()) { 719 empty = false; 720 } 721 722 if (empty) { 723 if (mEmptyView != null) { 724 mEmptyView.setVisibility(View.VISIBLE); 725 setVisibility(View.GONE); 726 } else { 728 setVisibility(View.VISIBLE); 729 } 730 734 if (mDataChanged) { 735 this.onLayout(false, mLeft, mTop, mRight, mBottom); 736 } 737 } else { 738 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 739 setVisibility(View.VISIBLE); 740 } 741 } ListView には setFilterText() でフィルターをセットできるのですが、このフィルターモードの場合はリストに表示するデータがなくても、フィルターに一致するデータがないというだけで実際のデータが空というわけではないので、最初の if 文で false にしています。

その次の if else 文が本体と emptyView の表示・非表示の切り替えをしているところです。
これをみると empty が true でも emptyView があれば本体が表示されることがわかります。

では、この updateEmptyStatus() がどこから呼ばれているかというと

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#643
setEmptyView() 643 public void setEmptyView(View emptyView) { 644 mEmptyView = emptyView; 645 646 final T adapter = getAdapter(); 647 final boolean empty = ((adapter == null) || adapter.isEmpty()); 648 updateEmptyStatus(empty); 649 }

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717
checkFocus() 698 void checkFocus() { 699 final T adapter = getAdapter(); 700 final boolean empty = adapter == null || adapter.getCount() == 0; 701 final boolean focusable = !empty || isInFilterMode(); 705 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 706 super.setFocusable(focusable && mDesiredFocusableState); 707 if (mEmptyView != null) { 708 updateEmptyStatus((adapter == null) || adapter.isEmpty()); 709 } 710 } の2カ所です。

いずれも adapter が null もしくは adapter.isEmpty() が true なら updateEmptyStatus() の引数として true が渡されています。

つまりまとめると

1. adapter != null && adapter.isEmpty == false → 本体が表示される
2. adapter == null or adapter.isEmpty == true
  → emptyView != null → 本体 が表示される
  → emptyView == null → emptyView が表示される

なので、結論としては、 isEmpty() で常に false を返すように Override するか、emptyView を null にすればよい。

ListView を単体で使うときは明示的に setEmptyView() するか、android/id:empty の View を XML で定義するので意識できるますが、Android で用意されている ListView 用の ListActivity と ListFragment を使うときにはちょっと注意が必要です。

Android 4.0 では、ListActivity でのデフォルトのレイアウトとして com.android.internal.R.layout.list_content_simple をセットしています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListActivity.java#308 308 private void ensureList() { 309 if (mList != null) { 310 return; 311 } 312 setContentView(com.android.internal.R.layout.list_content_simple); 313 314 }
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content_simple.xml 20
ちなみに Android 2.3.4 では、com.android.internal.R.layout.list_content をセットしていますが、なかのレイアウトは 4.0 の list_content_simple と同じです。

http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/java/android/app/ListActivity.java#308 308 private void ensureList() { 309 if (mList != null) { 310 return; 311 } 312 setContentView(com.android.internal.R.layout.list_content); 313 314 }
http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml 20 <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" 21 android:layout_width="match_parent" 22 android:layout_height="match_parent" 23 android:drawSelectorOnTop="false" 24 />
一方、ListFragment のデフォルトのレイアウトとしては com.android.internal.R.layout.list_content がセットされています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListFragment.java#191 190 @Override 191 public View onCreateView(LayoutInflater inflater, ViewGroup container, 192 Bundle savedInstanceState) { 193 return inflater.inflate(com.android.internal.R.layout.list_content, 194 container, false); 195 }
こっちはちょっと複雑なレイアウトになっています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml 18 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 19 android:layout_width="match_parent" 20 android:layout_height="match_parent"> 21 22 <LinearLayout android:id="@+id/progressContainer" 23 android:orientation="vertical" 24 android:layout_width="match_parent" 25 android:layout_height="match_parent" 26 android:visibility="gone" 27 android:gravity="center"> 28 29 <ProgressBar style="?android:attr/progressBarStyleLarge" 30 android:layout_width="wrap_content" 31 android:layout_height="wrap_content" /> 32 <TextView android:layout_width="wrap_content" 33 android:layout_height="wrap_content" 34 android:textAppearance="?android:attr/textAppearanceSmall" 35 android:text="@string/loading" 36 android:paddingTop="4dip" 37 android:singleLine="true" /> 38 39 </LinearLayout> 40 41 <FrameLayout android:id="@+id/listContainer" 42 android:layout_width="match_parent" 43 android:layout_height="match_parent"> 44 45 <ListView android:id="@android:id/list" 46 android:layout_width="match_parent" 47 android:layout_height="match_parent" 48 android:drawSelectorOnTop="false" /> 49 <TextView android:id="@+android:id/internalEmpty" 50 android:layout_width="match_parent" 51 android:layout_height="match_parent" 52 android:gravity="center" 53 android:textAppearance="?android:attr/textAppearanceLarge" /> 54 </FrameLayout> 55 56 </FrameLayout>

注目してほしいのが @+android:id/internalEmpty という ID の TextView です。
ListFragment には setEmptyText() という、データが空のときに表示する文字をセットするメソッドが用意されています。このメソッドが呼ばれると、次のように ListView の setEmptyView() が呼ばれます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListFragment.java#289 289 public void setEmptyText(CharSequence text) { 290 ensureList(); 291 if (mStandardEmptyView == null) { 292 throw new IllegalStateException("Can't be used with a custom content view"); 293 } 294 mStandardEmptyView.setText(text); 295 if (mEmptyText == null) { 296 mList.setEmptyView(mStandardEmptyView); 297 } 298 mEmptyText = text; 299 }
ここの mStandardEmptyView というのが上記の @+android:id/internalEmpty という ID の TextView に対応しています。


ということで、ListFragment でなんとなくやってた setEmptyText() をコメントアウトしたらヘッダーでるようになったー!

ただし、残念ながらこの場合も

-----
ヘッダー
empty message
フッター
-----

のようにはできないです。ヘッダー・フッターと emptyView は一緒に出すことはコードを見た限りではできないですねー

updateEmptyStatus が protected だったらいろいろできたのに。。。

やるとしたらこんな感じかな。
ヘッダーと、emptyView を同じレイアウトXMLから生成するくらいしか方法がないかな。

public class MainActivity extends ListActivity implements View.OnClickListener{ ArrayAdapter<String> mAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1); LayoutInflater inflater = getLayoutInflater(); View header = inflater.inflate(R.layout.header, null, false); getListView().addHeaderView(header); setListAdapter(mAdapter); View emptyHeader = getListView().getEmptyView(); emptyHeader.setOnClickListener(this); header.setOnClickListener(this); } @Override public void onClick(View v) { if(mAdapter.isEmpty()) { mAdapter.add("Test"); } else { mAdapter.remove("Test"); } } } <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" android:drawSelectorOnTop="false" /> <LinearLayout android:id="@android:id/empty" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <include layout="@layout/header"/> <TextView android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center" android:text="No data" android:textSize="30sp" /> </LinearLayout> </FrameLayout> <?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Header" /> ListFragment でも同じ感じ。

public class MainFragment extends ListFragment implements View.OnClickListener { ArrayAdapter<String> mAdapter; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.main, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1); LayoutInflater inflater = getActivity().getLayoutInflater(); View header = inflater.inflate(R.layout.header, null, false); getListView().addHeaderView(header); setListAdapter(mAdapter); View emptyHeader = getListView().getEmptyView(); emptyHeader.setOnClickListener(this); header.setOnClickListener(this); } @Override public void onClick(View v) { if(mAdapter.isEmpty()) { mAdapter.add("Test"); } else { mAdapter.remove("Test"); } } }

もちろん Adapter を extends して isEmpty() で true を返すようにすれば emptyView がセットされていても本体が表示されるようになります。 具体的な用途は思いつかないですが、データのあるなしにかかわらず独自の基準で emptyView の表示・非表示を切り替えたい場合には便利だと思います。


ちなみに、ListFragment で emptyView を使う場合ははまりポイントがいっぱいなので、 このエントリを見ておくことをオススメします!
Y.A.M の 雑記帳: Android ListFragment でカスタムレイアウトを使うと setEmptyText() が使えない -





2012年4月10日火曜日

DartEditor で zen-coding を使う

DartEditor は Eclipse ベースのエディターなので、試しに zen-coding の Eclipse のプラグインを移したら、できるようになりました!わーい。

zen-coding のサイト
zen-coding - Set of plugins for HTML and CSS hi-speed coding - Google Project Hosting -

から、Eclipse 用の plugin-in のページ
sergeche/eclipse-zencoding - http://goo.gl/3n7aZ

にいくと、Eclipse へのインストール方法が書いてあります。
簡単にいうと、[Help] - [Install New Software...] で http://zen-coding.ru/eclipse/updates/ (もしくは http://media.chikuyonok.ru/eclipse/updates/) を指定してインストールします。

このプラグインがインストールされた Eclipse のディレクトリの中の
  • features/ru.zencoding.eclipse_xxx
  • plugins/ru.zencoding.eclipse_xxx
をそれぞれ DartEditor の features/ と plugins/ にコピーします。

さらに、configuration/org.eclipse.equinox.simpleconfigurator/bundles.info の中の

ru.zencoding.eclipse,0.8.0.201201232221,plugins/ru.zencoding.eclipse_0.8.0.201201232221.jar,4,false

を DartEditor の configuration/org.eclipse.equinox.simpleconfigurator/bundles.info の中に追加します。


ここまでやったら後は DartEditor を起動するだけ。

DartEditor の Prefereces に Zen Coding の項目が出ていれば成功!



html ファイルで html:5 と打ってタブを押したら↓に変換されるー! <!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> </html>





DartEditor で less を使う

less の javascript ファイルを読み込んで動的に css に変換する場合、その script タグが書かれている html ファイルに file:///... でアクセスすると Chromium や Chrome ではうまくうごきません。

css - less.js not working in chrome - Stack Overflow -

これは Chrome のセッキュリティ機能の1つで、ローカルの javascript ファイル読み込むときに起こる問題として知られているものです。

--allow-file-access-from-files フラグをつけて Chromium や Chrome を起動すれば less の javascript ファイルが正しく動きます。
List of Chromium Command Line Switches

Dart のアプリケーションで less を使いたい場合、デフォルトで Dartium (Chromium をベースにしている) が起動するので、上記と同じ問題が起こります。

そこで、Dartium を起動するときに --allow-file-access-from-files フラグをつけられればいいということになります。

Dart アプリケーションの起動は、DartEditor の [Tools] - [Manage Launches...] でカスタマイズできます。
  • Dartium Launch ではフラグも設定できないし、起動するブラウザも指定できません(=ショートカットとか作れない)
  • Dart Web Launch ではフラグは設定できませんが、起動するブラウザを指定できます。さらに指定されたブラウザがすでに起動している場合は、新しいタブで実行されます。


Dartium Launch


Dart Web Launch


そのため、
  • 1. DartEditor の起動設定で dark-sdk 内の Chromium を起動する Dart Web Launch を作っておく(起動ターゲットは less を使った Dart アプリケーション)
  • 2. コンソールから --allow-file-access-from-files をつけて dart-sdk 内の Chromium を起動する
  • 3. DartEditor から less を使った Dart アプリケーションを 1. の Dart Web Launch で起動する

これで DartEditor で作成しているローカルの Dart アプリケーションで less が動くようになります。

*アプリ毎に Dart Web Launch 作るのは面倒なので、デフォルトの Dartium に flag 指定できるようにならないかなー。。。



2012年4月5日木曜日

Dart Canvas を使う

dart で Canvas を使うには、document.query() で canvas タグのエレメントを CanvasElement として取得し、その CanvasElement に対して getContext() を呼んで CanvasRenderingContext2D を取得します。

あとは、CanvasRenderingContext2D オブジェクトに対して fillRect() などの Canvas のメソッドを呼べば OK

#import('dart:html'); class Droid { CanvasRenderingContext2D ctx; static final PI = Math.PI; static final String ORANGE = "orange"; static final String WHITE = "white"; static final int gap = 5; static final int bodyWidth = 150; static final int bodyHeight = 120; static final int armWidth = 30; static final int armHeight = 70; static final int legWidth = 30; static final int legHeight = 35; Droid() { CanvasElement canvas = document.query("#canvas"); ctx = canvas.getContext("2d"); drawFrame(); } // Draw the complete figure for the current number of seeds. void drawFrame() { ctx.clearRect(0, 0, 300, 300); ctx.lineWidth = 2; ctx.fillStyle = ORANGE; ctx.strokeStyle = ORANGE; ctx.save(); // head ctx.beginPath(); ctx.translate(0, bodyWidth / 2 * 0.13); ctx.scale(1, 0.9); ctx.arc(100 + bodyWidth / 2, 100 - gap, bodyWidth / 2, PI, 0, false); ctx.fill(); ctx.closePath(); ctx.restore(); // body ctx.beginPath(); ctx.moveTo(100, 100); ctx.lineTo(100 + bodyWidth, 100); ctx.arc(100 + bodyWidth - 15, 100 + bodyHeight - 15, 15, 0, PI / 2, false); ctx.arc(100 + 15, 100 + bodyHeight - 15, 15, PI / 2, PI, false); ctx.fill(); ctx.closePath(); // legs left drawLeg(100 + bodyWidth / 2 - 15 - legWidth, 100 + bodyHeight); // legs rihgt drawLeg(100 + bodyWidth / 2 + 15, 100 + bodyHeight); // arms left drawArm(100 - gap - armWidth, 100 + armWidth / 2); // arms right drawArm(100 + bodyWidth + gap, 100 + armWidth / 2); // nidle right ctx.lineWidth = 5; ctx.lineCap = "round"; ctx.beginPath(); ctx.moveTo(100 + bodyWidth / 2 - 45, 12); ctx.lineTo(100 + bodyWidth / 2 - 20, 60); ctx.closePath(); ctx.stroke(); // nidle left ctx.beginPath(); ctx.moveTo(100 + bodyWidth / 2 + 45, 12); ctx.lineTo(100 + bodyWidth / 2 + 20, 60); ctx.closePath(); ctx.stroke(); // eye ctx.fillStyle = WHITE; ctx.beginPath(); ctx.arc(100 + bodyWidth / 2 - 34, 65, 6, 0, 2 * PI, false); ctx.fill(); ctx.closePath(); ctx.beginPath(); ctx.arc(100 + bodyWidth / 2 + 34, 65, 6, 0, 2 * PI, false); ctx.fill(); ctx.closePath(); } void drawArm(int left, int top) { ctx.beginPath(); ctx.arc(left + armWidth / 2, top, armWidth / 2, PI, 0, false); ctx.lineTo(left + armWidth, top + armHeight); ctx.arc(left + armWidth / 2, top + armHeight, armWidth / 2, 0, PI, false); ctx.lineTo(left, top); ctx.fill(); ctx.closePath(); } void drawLeg(int left, int top) { ctx.beginPath(); ctx.moveTo(left, top); ctx.lineTo(left, top + legHeight); ctx.arc(left + legWidth / 2, top + legHeight, legWidth / 2, PI, 0, true); ctx.lineTo(left + legWidth, top); ctx.fill(); ctx.closePath(); } } void main() { new Droid(); }

<!DOCTYPE html> <html> <head> <title>Droid</title> </head> <body> <h1>Droid</h1> <div> <canvas id="canvas" width="300" height="300"></canvas> </div> <script type="application/dart" src="MoveDroid.dart"></script> <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> </body> </html>







2012年4月4日水曜日

Dart DOMを操作する

#import('dart:html'); void main() { document.query("#status").innerHTML = "Hello World!"; } Dart で DOM 操作をするには html ライブラリを使います。
SVG や WebGL のインタフェースなども用意されています。

Library html

Document クラスのオブジェクトを取得する getter が定義されていて、

html_dartium.dat #library('html'); ... Document get document() { if (__document == null) { _initialize(); } return __document; } main() のなかの document はこの getter を呼んでいる = Document のオブジェクトということです。

id でエレメントを見つける document.query('#selector');
class で最初のエレメントを1つ見つける document.query('.classname');
class で複数のエレメントを見つける document.queryAll('.classname');
tag で最初のエレメントを1つ見つける document.query('div');
tag で複数のエレメントを見つける document.queryAll('div');
name で最初のエレメントを1つ見つける document.query('[name="form"]');
name で複数のエレメントを見つける document.queryAll('[name="form"]');

最初の child node を取得する element.nodes[0];
最初の child element を取得する element.elements[0];

エレメントを tag から作成 Element element = new Element.tag('div');
エレメントを html から作成 Element element = new Element.html('

html string

');
* html の引数の html 文字が正しい文法になっていないといけない。


エレメントに子エレメントを追加 element.elements.add(newElement);

エレメントにイベントハンドラを追加 element.on.click.add(handleClick);
エレメントからイベントハンドラを削除 element.on.click.remove(handleClick); * element.on.hoge.add(handleEvent); / element.on.hoge.remove(handleEvent);
element.on は ElementEvents で例えば
  • click
  • doubleClick
  • drag
  • drop
  • focus
  • keyDown
  • mouseDown ...
などたくさんある。

#import('dart:html'); void main() { // id でエレメント取得 document.query("#status").innerHTML = "Find one element by id"; // class で最初のエレメント取得 document.query(".message").innerHTML = "Find one element by class"; // class で複数のエレメント取得 ElementList elements = document.queryAll(".message2"); for(Element e in elements) { e.innerHTML = "Find many elements by class"; } // tag で最初のエレメント取得 document.query('h2').style.background = "#9999ff"; // tag で複数のエレメント取得 document.queryAll('div').forEach((Element e) { e.style.background = "#ccccff"; }); // name で最初のエレメント取得 document.query('[name="form"]').style.background = "#ffeeee"; // name で複数のエレメント取得 document.queryAll('[name="form"]'); // child nodeを取得 Element elem = document.query("#main"); if(!elem.nodes.isEmpty()) { Node node = elem.nodes[0]; } // element を tag から作成 Element element = new Element.tag('div'); element.innerHTML = "create new an element from tag"; // element を html から作成 Element element2 = new Element.html('

create new an element from html

'); // element を追加 elem.elements.add(element); elem.elements.add(element2); // element を削除 elem.elements[0].remove(); // イベントハンドラ Element btn = document.query("#button"); btn.on.click.add((e) { btn.innerHTML = "Clicked!"; }); }
<!DOCTYPE html> <html> <head> <title>hellodart</title> </head> <body> <h2 id="status">dart is not running</h2> <div class="message"></div> <div class="message2"></div> <div class="message2"></div> <input name="form"></input> <div id="main"> <div>First</div> </div> <div id="button">Click here!</div> <script type="application/dart" src="hellodart.dart"></script> <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> </body> </html>

2012年4月3日火曜日

Hello Dart 2

変数の定義
  • var hoge;
  • final hoge;
_ で始まる変数は private になる。それ以外は public になる。
初期化されていない変数は初期値は null
main() { final name = "droid"; var age = 4; var favor = 'ics'; // コンソールに出力 print("Hello, my name is ${name}."); print('I like ' + favor + '!'); } 文字列に変数を埋め込む方法
  • "Hello, ${name}"
  • 'Hello, $name'
  • 'Hello' + name
複数行の文字列は '''...''' もしくは """...""" で表現できる
@"..." で /n などが無視される raw string になる


■ クラス

class Hoge {}
class Droid { var name = 'droid'; greet(favor) { print("Hello, my name is ${name}."); print('I like ' + favor + '!'); } } main () { var droid = new Droid(); droid.greet('ics'); }
dart では全てのクラスは Object クラスを継承している non-Object クラスを継承したい場合は extends を使う

通常の変数と同じように、生成したクラスのインスタンス変数には var か final もしくは各タイプを使う必要がある

コンストラクタを明示的に定義していない場合は、引数なしのコンストラクタが定義されていることになる
コンストラクタを明示的に定義した場合は、引数なしのコンストラクタも必要な場合は明示的に定義する必要がある class Droid { var name = 'droid'; // コンストラクタ Droid(); Droid.withName(this.name); greet(favor) { print("Hello, my name is ${name}."); print('I like ' + favor + '!'); } } main () { var droid = new Droid.withName('dronjo'); droid.greet('gingerbread'); } getter と setter

_ で始まる変数は private になる。それ以外は public になる。 class Droid { String _name = 'droid'; // private String get name() => _name; // getter void set name(String name) { // setter if(name == null) name = ""; if(name.length > 20) throw 'name is too long!'; _name = name; } greet(favor) { print("Hello, my name is ${name}."); print('I like ' + favor + '!'); } } main () { var droid = new Droid(); droid.name = 'dronjo'; droid.greet('gingerbread'); }

■ Interfaces

implements Hoge class Droid implements Comparable { String name = 'droid'; // private // コンストラクタ Droid(); Droid.withName(this.name); greet(String text) => print('$name, $text'); int compareTo(Droid d) => name.compareTo(d.name); } main () { var droid = new Droid(); droid.name = 'dronjo'; var droid2 = new Droid.withName('dronjo'); droid.greet('gingerbread'); num result = droid2.compareTo(droid); if(result == 0) { droid2.greet('you are the same.'); } else { droid2.greet('you are different.'); } } int は primitive ではなく、interface!

Optional types (実態は interfaces)
String, num, int (num を継承), double (num を継承), 等
自分で static な type を追加することもできる
final を加えて定義できる final String hoge;


Built-in types
  • String
  • StringBuffer
  • num
  • int (num を継承)
  • double (num を継承)
  • bool
  • List (JavaScript の array にコンパイルされる)
  • Map
String <--> in, double 変換 // string -> int var one = Math.parseInt("1"); // 1 // string -> double var onePointOne = Math.parseDouble("1.1"); // 1.1 // int -> string String oneAsString = 1.toString(); // "1" // double -> string String piAsString = 3.14159.toStringAsFixed(2); // "3.14" JavaScript と違って、bool は 1 や non-null オブジェクトを true としては扱わない



■ List // 可変長リスト var list = [1,2,3]; // リストの長さ print(list.length); // 要素の取得 print(list[1]); // 要素の追加 list.add(4); // 要素の削除 list.removeRange(2, 1); // 固定長リスト var list = new List(4); Iterating

for...in var list = [1,2,3]; for (final x in list) { print(x); }
forEach() var list = [1,2,3]; void printElement(element) => print(element); list.forEach(printElement); var list = [1,2,3]; list.forEach((element) => print(element)); List document
Collection document


■ Map var gifts = { // keys values "first" : "partridge", "second" : "turtledoves", "fifth" : "golden rings"};
key は string でなければならない
Map のコンストラクタを使ったり、Hashable を実装した別のオブジェクトを使えば key の他のタイプを使うことができる // Map コンストラクタを使う var map = new Map(); map[1] = "partridge"; map[2] = "turtledoves"; map[5] = "golden rings"; value は任意のオブジェクトもしくは null を取ることができる
存在しない key の value を取得しようとすると null が返ってくるが、そもそも value は null を取ることができるので、 containsKey()putIfAbsent() でチェックする var gifts = { "first": "partridge" }; gifts["fourth"] = "calling birds"; // 長さ print(gifts.length); // 削除 gifts.remove('first'); // コピー var regifts = new Map.from(gifts); print(regifts['first']);
Iterating

forEach() var gifts = { "first" : "partridge", "second": "turtledoves", "fifth" : "golden rings"}; gifts.forEach((k,v) => print('$k : $v')); forEach() で返ってくる key-value ペアの順番は保証されていないので、依存しないようにする

keys や values だけ必要な場合は getKeys(), getValues() でそれぞれの Collection オブジェクトが取得できる var gifts = {"first": "partridge", "second": "turtledoves"}; var values = gifts.getValues(); values.forEach((v) => print(v)); Map document
Hashable document


■ Functions

=> e シンタックスは {return e;} の短縮形

関数引数を [] を囲むと、オプショナルな引数になる String say(String from, String msg, [String device]) { var result = "$from says $msg"; if (device != null) { result = "$result with a $device"; } return result; } オプショナルな引数に初期値をセットすることもできる String say(String from, String msg, [String device='carrier pigeon']) { var result = "$from says $msg"; if (device != null) { result = "$result with a $device"; } return result; } オプショナルな引数の名前を指定してセットすることもできる print(say("Bob", "Howdy", device: "tin can and string")); function をパラメータとして別の function に渡すことができる bool isOdd(num i) => i % 2 == 1; List ages = [1,4,5,7,10,14,21]; List oddAges = ages.filter(isOdd); より短縮したバージョン List ages = [1,4,5,7,10,14,21]; List oddAges = ages.filter((i) => i % 2 == 1); function を変数に割り当てることもできる var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!'; print(loudify('hello')); Lexical closures もつくれる Function makeAdder(num n) { return (num i) => n + i; } main() { var add2 = makeAdder(2); print(add2(3)); // 5 }


■ Exceptions try { breedMoreLlamas(); } catch (final OutOfLlamasException e) { // a specific exception buyMoreLlamas(); } catch (final Exception e) { // anything that is an exception print("Unknown exception: $e"); } catch (final e) { // no specified type, handles all print("Something really unknown: $e"); } finally try { breedMoreLlamas(); } catch (final e) { print("Error: $e"); // handle exception first } finally { cleanLlamaStalls(); // then run finally } Exception document


■ 参考