Android-Architecture-Components ViewModelのとにかくシンプルなKotlinサンプル

# 目標

KotlinでViewModelのシンプルなサンプルコードを書く

# 理由

GoogleのドキュメントにあるのがJavaだったので。

# 前提

あるデータの配列をリスト表示します。
リスト表示にはRecycleView + LinearLayoutManagerでやります。

# 注意

Kotlin始めたばかりなのでいろいろと怪しいところがあると思います。ご指摘下さい。

# 準備

build.gradle。Kotlin設定は割愛。
あ、kotlin-android-extensions使ってます。

```gradle:app/build.gradle
dependencies{

  def lifecycle_version = "1.1.1"
  // ViewModel and LiveData
  implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}
```

# 実装

* データクラス  
IDとタイトル、サマリーを持つだけの簡単なデータクラスです。

```kotlin:MyContent.kt
object MyContent {
	val ITEMS: MutableList<MyContent.Item> = ArrayList()
	
	private val COUNT = 25
	
	init {
	    // Add some sample items.
	    for (i in 1..COUNT) {
	        addItem(MyContent.createDummyItem(i))
	    }
	}
	
	private fun addItem(item: MyContent.Item) {
	    MyContent.ITEMS.add(item)
	}
	
	private fun createDummyItem(position: Int): MyContent.Item {
	    return MyContent.Item(position, 10 + position, "タイトル$position", "サマリー$position")
	}
	
	data class Item(val id: Int, val type: Int, val title: String, val summary: String?)
}
```

* ViewModel  
上記のデータのリストを持つViewModelを作成します。

```kotlin:MyListModel.kt
class MyListModel : ViewModel() {

    private var listData: MutableLiveData<List<MyContent.Item>>? = null

    fun getListData(): LiveData<List<MyContent.Item>> {
        if (listData == null) {
            listData = MutableLiveData()
            loadListData()
        }
        return listData as MutableLiveData<List<MyContent.Item>>
    }

    private fun loadListData() {
        listData!!.value = MyContent.ITEMS
    }
}
```

* Activity 

レイアウト。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <fragment
        android:id="@+id/fragment"
        android:name="my.sample.kotlinviewmodel.fragment.MyListFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
```

クラス。

```kotlin:MainActivity.kt
class MyListActivity : AppCompatActivity(), MyListFragment.OnListFragmentInteractionListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onListFragmentInteraction(item: MyContent.Item?) {
        Log.d(Const.TAG, "onListFragmentInteraction")
        
        val builder = AlertDialog.Builder(this)
        builder.setMessage(item.toString()).setTitle("選択")
                .setPositiveButton(android.R.string.ok, null)
        builder.show()
    }
}
```
  
* Fragment  

レイアウトファイル。

```xml:fragment_list.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        app:layoutManager="LinearLayoutManager"
        tools:context="my.sample.kotlinviewmodel.fragment.MyListFragment"
        tools:listitem="@layout/layout_item"/>

    <TextView
        android:id="@+id/emptyView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="32dp"
        android:text="データがありません。"
        android:textAlignment="viewStart"
        android:textSize="16sp"
        android:textStyle="bold"
        android:visibility="invisible"
        tools:ignore="RtlCompat" />
</FrameLayout>
```

layout_itemのレイアウト。

```xml:layout.item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/item_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:textAppearance="?attr/textAppearanceListItem" />

    <TextView
        android:id="@+id/content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
```

サマリー表示してませんが気にしないで下さい。(二行にするのが面倒くさかった)

Fragmentクラス。ItemRecyclerViewAdapterは載せませんがAndroidStudioが自動生成したほぼそのままです。DataBinding使うとまた違ってくるのでしょうけれど。

```kotlin:MyListFragment.kt
class MyListFragment : Fragment() {

    private var listener: OnListFragmentInteractionListener? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    private lateinit var listModel: MyListModel

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_jobitem_list, container, false)

        // Set the adapter
        if (view.list is RecyclerView) {
            view.list.addItemDecoration(DividerItemDecoration(activity, DividerItemDecoration.VERTICAL))

            with(view.list, {
                layoutManager = LinearLayoutManager(context)

                listModel = ViewModelProviders.of(activity!!).get(MyListModel::class.java)
                listModel.loadListData().observe(activity!!, Observer { data ->
                    if(data==null){
                        // show empty view
                        view.emptyView.visibility = View.VISIBLE
                        visibility = View.INVISIBLE
                    }else {
                        view.emptyView.visibility = View.INVISIBLE
                        visibility = View.VISIBLE
                        adapter = ItemRecyclerViewAdapter(data, listener)
                    }
                })
            })
        }
        return view
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (context is OnListFragmentInteractionListener) {
            listener = context
        } else {
            throw RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener")
        }
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }

    /**
     * Listの行選択コールバック用リスナ
     */
    interface OnListFragmentInteractionListener {
        fun onListFragmentInteraction(item: MyContent.Item?)
    }
}
```

胆はやはり、ViewModelクラスの実装と、それを使う場所(今回はFragment。Activityでも使えます)でのコーディングでしょう。

```kotlin
listModel = ViewModelProviders.of(activity!!).get(MyListModel::class.java)
listModel.loadListData().observe(activity!!, Observer { data ->
               if(data==null){
                        // show empty view
                        view.emptyView.visibility = View.VISIBLE
                        visibility = View.INVISIBLE
                    }else {
                        view.emptyView.visibility = View.INVISIBLE
                        visibility = View.VISIBLE
                        adapter = ItemRecyclerViewAdapter(data, listener)
                    }
                })
```

ここ、javaコードを書き換えるのに苦労しました(Kotlinをまだよく理解してないから)。

これだけだと本当に画面回転時くらいにしか恩恵を受けませんが、たとえば、このデータをActivity側でAPIで受信、他のFragmentでも使う、というようなシチュエーションだと生きてくるのではないでしょうか。

APIと書いたのはまさにそれを想定していて、API受信データをDBやキャッシュファイルなどに永続化することなく、Activity-Fragment生存期間に簡単に受け渡しするために使おうと考えています。(まだその前段階ですが)

setArgumentsで渡せるBundleにもサイズ制限があることを最近知りましたので、Fragmentへのパラメータ渡しの代替にも使えそうです。