FragmentでAndroidでもSplitWindowを実現する

# 作るもの

<img width="991" alt="frag.png" src="https://qiita-image-store.s3.amazonaws.com/0/1782/d160db78-7bab-4349-f288-80b109aaf818.png">

iPadではおなじみの、こんな画面を作ります。

# はじめに

こちらのモジュールを利用します。
https://github.com/nborracha/com.yallaya.fragments
インストールは特別な手続きは不要なので[いつも通り](http://docs.appcelerator.com/platform/latest/#!/guide/Using_a_Module)にやっちゃってください。

# できること

* masterとdetailの画面分割ができるようになります。
* もちろんそれ以上に分割することもできます。

# できないこと

* iOSのSplitWindowと違って、分割されたWindowごとにNavigationBar（ActionBar）を持つことはできません。
* iOSのように自動的に分割されたWindowのボーダーが表示されません。
* ActionBarの操作にはコツが必要です。

# Fragmentとは

普段Titaniumを使っているとあまり意識することはありませんが、AndroidにはActivityという基本的な構成単位があります。１つのWindowが１つのActivityだと理解すればだいたい合っています。最小の構成単位が１つのアプリになっているiOSでは、アプリの起動中にまた別のアプリを起動して、用事が終わったら前のアプリの画面に戻ってくるなんて芸当は（Facebookのような一部の例外を除いて）出来ませんが、AndroidはActivityで構成されているので、Activity間を行き来するのは同一のアプリ内に用意されたものだろうが外部の公開されたActivityだろうが関係なく行き来することができるようになっています。

AndrdoidのUIはそんなActivityの上に各種のViewを乗せて画面を作り上げていくことになるわけですが、時々、それでは都合が悪くなることがあります。例えばTabGroupは複数のTabの下にWindowがぶら下がっています。

```xml

<Alloy>
  <TabGroup>
    <Tab title="Tab1">
      <Window />
    </Tab>
    <Tab title="Tab2">
      <Window />
    </Tab>
  </TabGroup>
</Alloy>
```

こうなった時、１つの画面の中に複数のActivityがあるわけで、結構大きいオブジェクト（HeavyWeightっていうくらいですからね）であるWindow(=Activity)をメモリ上に一気に展開するのは厳しいものがあります。

そのため、Titaniumを使っているとこの辺りはうまく隠蔽されていますが、実際にはこれらのWindowにはActivityではなくFragmentと呼ばれるコンポーネントが利用されています。複数のFragmentを合成して１つのActivityを作ることができるようになっているわけです。

## Fragmentのライフサイクル

それぞれのFragmentは独立して成り立っており、Activityのように自身のライフサイクルを持っています。

| 作成した時 | 閉じる時 | 
|:-----------|------------:|
|attach|pause|
|create|stop|
|createView|destroyView|
|activityCreated|destroy|
|start|deatch|
|resume||

![Fragmentのライフサイクル](http://developer.android.com/images/fragment_lifecycle.png)

[モジュール](https://github.com/nborracha/com.yallaya.fragments)からこれらのイベントを検知することができます。

# 作ってみよう

ファイル構成はこんな感じです。assetsなどは省略します。

```bash
app
├── alloy.js
├── controllers
│   ├── anotherView.js
│   ├── detailView.js
│   ├── index.js
│   └── masterView.js
├── styles
│   ├── app.tss
│   └── index.tss
└── views
    ├── anotherView.xml
    ├── detailView.xml
    ├── index.xml
    └── masterView.xml
```


まず、app/alloy.jsにモジュールの設定を追加します。

```js
Alloy.Globals.Fragments = require('com.yallaya.fragments');
```

次に、以下の手順でFragmentを画面に追加していきます。

1. Frameと呼ばれる、Fragmentを置く場所を作成する（公序良俗に反しない限りいくつでも可）
2. Frameに設置するFragmentと、Fragmentの中に表示するViewを作成
4. FragmentをFrameに設置する

これで完了します。まずindex.jsに２つのFrameを作成します。

```js
var masterView = Alloy.Globals.Fragments.createFragmentFrame({
  backgroundColor: '#56ACEE',
  width: '30%',
  height: Ti.UI.FILL,
  top: 0,
  left: 0
});

var detailView = Alloy.Globals.Fragments.createFragmentFrame({
  backgroundColor: 'Yellow',
  width: '70%',
  height: Ti.UI.FILL,
  top: 0,
  left: '30%'
});
```

左側にmasterViewとして設置するFrameには幅30%を指定しています。右側にdetailViewとして設置するFrameは左側の余白を30%にしています。

この２つのFrameの上に設置するFragmentと、そのFragmentの中に表示するViewを設定します。ここにはiPadのSplitViewのようにWindowやNavigationWindowを設置することはできません。

```js
var detailViewContent = Alloy.Globals.Fragments.createFragment({
  container: Alloy.createController('detailView', {frame: detailView, parentView: "FirstDetailViewContainer"}).getView(),
  tag: "FirstDetailViewContainer",
  putFragment: true,
  addToBackStack: true
});
detailViewContent.addEventListener('destroy', function(){
  //右側のdetailViewのrootなのでこのViewが閉じる時はアプリも終了する
  $.index.close();
});

var masterViewContent = Alloy.Globals.Fragments.createFragment({
  container: Alloy.createController('masterView', {frame: detailView}).getView(),
  tag: "FirstMasterViewContent",
  putFragment: true,
  addToBackStack: true
});
masterViewContent.addEventListener('destroy', function(){
  //左側のdetailViewのrootなのでこのViewが閉じる時はアプリも終了する
  $.index.close();
});
```

それから重要なのが、Fragmentはモジュールのオブジェクト自体が管理するので、`Alloy.Globals.Fragment`のメソッドを呼び出して追加します。これは設置するWindowが表示された後でないと実行できません。

```js
$.index.addEventListener('open', function(e){
  Alloy.Globals.Fragments.add({
    frameId: masterView.getUid(),
    fragment: masterViewContent,
    addToBackStack: true
  });
  Alloy.Globals.Fragments.add({
    frameId: detailView.getUid(),
    fragment: detailViewContent,
    addToBackStack: true
  });
});

// FrameをWindowsに追加します
$.index.add(masterView);
$.index.add(detailView);

$.index.open();
```

新しいViewをFrame内に表示するには、Fragmentを作成してaddメソッドを利用します。

```js
function addView(frame){
  var fragment = Alloy.Globals.Fragments.createFragment({
    container: Alloy.createController('anotherView').getView(),
    tag: 'anotherView',
    putFragment: true,
    addToBackStack: true
  });
  Alloy.Globals.Fragments.add({
    frameId: frame.getUid(),
    fragment: fragment,
    addToBackStack: true
  });
}
```

この時、Fragmentを表示するframeのidを指定するので、frameオブジェクトを利用しています。

逆に、Fragmentを閉じる場合は

```js
function close(){
  Alloy.Globals.Fragments.popBackStack('anotherView');
}
```

のように、各Fragmentの作成時に指定したtagを利用します。

# ActionBarとの連携

FrameやFragmentはそれ自体のActionBarを持つことはできません。そのため、ActionBarと連携する時は普通のWindowと違ってやり方を工夫しなければいけません。例えばタイトルを動的に変更する時は、Fragment（の中のView）からActionBarにアクセスする方法を確保する必要があります。

createViewする際の引数に親Windowオブジェクトを渡してもいいのですが、あまり深い階層まで引き廻すとコードの見通しが悪くなってしまいます。

そんな時は、親Windowがイベントリスナーを用意してそれぞれのFragmentとメッセージを交換し合うやり方を使うと便利です。

```js
// index.js
function updateTitle(e){
  var activity = $.index.getActivity();
  if(activity){
    activity.actionBar.title = e.title;
  }
}
Ti.App.addEventListener('actionBar:updateTitle', updateTitle);
$.index.addEventListener('close', function(e){
  $.index.removeEventListener('actionBar:updateTitle', updateTitle);
});

// anotherView.js
Ti.App.fireEvent('actionBar:updateTitle', {
  title: "We're gonna have a real good tile together"
});
```

# トラブルシューティング

Fragmentとして利用するViewがScrollViewやTableViewだとうまく動きません。

```xml
<Alloy>
  <ScrollView>
    ....
```

そんな時はViewで囲ってしまいましょう。

```xml
<Alloy>
  <View>
    <ScrollView>
    ....
```

以上、駆け足ですがTitaniumで扱うFragmentの紹介でした。