KotlinでTODOアプリをつくる～その２～

引き続き、Android向けTODOアプリ作成についてまとめます。
前回の記事は[KotlinでTODOアプリをつくる～その１～](https://qiita.com/satona-oinuma/items/dfd9bebf9c2e00247c49)をご覧ください。

## その２で扱う内容
その２では画面遷移周りを扱います。
- 画面遷移：Fragment

以下はその２では扱わず、続編で扱う予定です。
- リストの表示方法：RecyclerView
- データやデータの保存に関すること：内部ファイルへの保存を予定


### 用意するクラス
今回は新たに以下の2つのクラスを用意します。
- CategoryAdapter.kt
- ItemAdapter.kt
どちらもRecyclerViewのAdapterを継承したクラスです。
内容についてはその３でまとめます。

また、前回に引き続き、以下の2つのクラス、2つのレイアウトファイルを使用します。
- MainActivity：Activityクラスを継承したクラス
- MainFragment：Fragmentクラスを継承したクラス
- activity_main.xml：MainActivityクラスで呼び出されるレイアウトファイル
- fragment_item_list.xml：MainFragmentクラス呼び出されるレイアウトファイル


## 画面遷移：Fragment
Fragmentとは、独自のレイアウトを定義および管理するためのクラスです。
アクティビティの部品の1つとしてFragmentを実装し、画面遷移を行います。

Androidでの主な遷移方法にはIntent（アクティビティ）を使用する方法もありますが、Fragmentはアクティビティをモジュール化したもののため、アクティビティと比べて必要な情報のみを考慮すれば良いという特徴があります。
アプリ内遷移で多く用いられている印象です。

今回は、Fragmentをホストする役割をActivityにまかせ、カテゴリ一覧画面、タスク一覧画面の2つのFragment間で遷移を行います。


### MainActivity.kt
アプリ起動後、すぐにカテゴリ一覧画面を表示するために、MainFragmentをMainActivityのonCreateで呼び出します。

Fragmentを表示する際にまずやることは、FragmentManagerにFragmentを紐づけることです。
`supportFragmentManager`でFragmentManagerを呼び出し、`beginTransaction`を実行することでフラグメントの編集を開始します。
次に、`add`を使用してFragmentを設置するレイアウトのIDと、Fragmentを管理するクラスであるMainFragmentのインスタンスを追加します。
最後に、`commit`を実行して変更内容を確定して完了です。

ちなみに`add`の第一引数はviewではなくIntのため、Bindingクラスのインスタンスから取得したレイアウトを設置するとエラーになります。

Activityクラスについて、`supportFragmentManager`を使用する場合はFragmentActivityかそのサブクラスを継承する必要があります。
もしも`supportFragmentManager`を実装しようとしてうまくいかない場合は、継承するクラスがFragmentActivity、AppCompatActivity、CarAppActivityのいずれかになっているかをご確認ください。

```Kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       val binding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(binding.root)

       // 1. 編集開始の呼び出し
       val transaction = supportFragmentManager.beginTransaction()
       // 2. FragmentManagerに設置場所とFragment管理クラスを紐づける
       transaction.add(R.id.container, MainFragment.newInstance(2))
       // 3. 変更内容を確定する
       transaction.commit()
   }
}
```

### activity_main.xml
Fragmentの設置場所となるレイアウトを定義するファイルです。
先ほどMainActivity.kt内で`add`した、R.id.containerがあります。

```XML:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity"
   android:id="@+id/container"/>
```
これを土台に、Fragmentを描画していきます。


### fragment_item.xml
Fragmentのレイアウトを定義するファイルです。
今回はカテゴリ一覧画面、タスク一覧画面の両方でこのレイアウトを使用します。
主にToolbarとRecyclerView、Toolbarの中に+ボタンがあるレイアウトです。

押さえておくと良い点は以下の3つです。
- +ボタンの右端配置
Buttonに`android:layout_gravity="end"`を指定することで実現しています。
- RecyclerViewの高さ
`android:layout_height="0dp"`を指定しています。
こうすることで、レイアウトの高さを自動調節し、全体の合計が親ビューの高さに合うようになります。
- backgroundなどの色を指定
`?colorPrimaryVariant`という書き方をしています。これはthemes.xmlで指定した色を反映するための書き方です。
colors.xmlで指定した色を直接参照する場合は`@color/色名`で書くことはできますが、themes.xmlから指定することで、アプリ全体の色の統一がしやすくなります。


```XML:fragment_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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">

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:titleTextColor="?android:textColorSecondary"
       android:background="?colorPrimaryVariant"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:layout_constraintBottom_toTopOf="@id/recycle">

       <Button
           android:id="@+id/addButton"
           android:layout_width="48dp"
           android:layout_height="48dp"
           android:layout_gravity="end"
           android:background="@drawable/ic_baseline_add_24"
           app:layout_constraintEnd_toEndOf="parent"
           android:contentDescription="@string/addButton"/>

   </androidx.appcompat.widget.Toolbar>

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycle"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_marginStart="16dp"
       android:layout_marginEnd="16dp"
       app:layoutManager="LinearLayoutManager"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/toolbar"
       tools:listitem="@layout/item_layout" />

</androidx.constraintlayout.widget.ConstraintLayout>

```


### MainFragment.kt
最後に、MainFragmentです。
このクラスではFragment間のライフサイクル制御、Fragmentの表示の切り替え制御を行います。
クラス全体のコードはやや長いため、以下の役割ごとにまとめます。
- インスタンスの生成
- 画面遷移判定

#### インスタンスの生成
Fragmentを継承するクラスでは、コンストラクタで値の受け渡しをしようとするとクラッシュする場合があります。
そのため、値を受け渡したい場合はインスタンス生成用のメソッドを用意し、そこで他クラスからの値を受け取る必要があります。
今回は、インスタンスを生成するnewInstanceメソッドをcompanion object内に記載します。
companion object内に記載するのは、事前にインスタンスを生成せずに`クラス名.メソッド名(引数)`の形でメソッドの実行ができるためです。

```Kotlin:MainFragment.kt
private var columnCount = 1

   companion object {
       @JvmStatic
       // 値の受け渡しメソッド、戻り値はMainFragment型
       fun newInstance(column: Int) =
          // インスタンスを生成し、プロパティに値を代入
           MainFragment().apply {
               columnCount = column
           }
   }
```
以下のように呼び出すことで、上記が動きます。
```Kotlin:MainActivity.kt
MainFragment.newInstance(2)
```


#### 画面遷移の判定
今回のアプリでは、カテゴリ一覧画面とタスク一覧画面はほとんど同じ作りになっています。
<img width="70%" alt="画面イメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1396691/b031027e-6277-87c0-a0d7-35eaf8833b69.png">


そのため、Fragmentとして描画するレイアウト(fragment_item_list.xml)は同じものを使用し、以下の要素はページによって出し分けるようにします。
- 画面タイトル
- 追加ボタンで追加する要素
- リスト要素

今回は、リストの列数が1列の場合はタスク一覧画面、2列の場合はカテゴリ一覧画面を表示します。
なお、数字の1、2は上記の判定に使用するのみで、実際のRecyclerViewの列数指定とは関係ありません。指定方法の詳細についてはその３でまとめます。

```Kotlin:MainFragment.kt
   override fun onCreateView(
       inflater: LayoutInflater, container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View? {
       _binding = FragmentItemListBinding.inflate(inflater, container, false)
       binding.let {
           // fragment_item_list.xmlに指定したRecycerViewをBindingクラスから取得
           val recyclerView = it.recycle
           if (columnCount == 1) {
               // タスク一覧画面を表示する処理
               with(recyclerView) { // RecyclerViewの設定
                   layoutManager = LinearLayoutManager(context)
                   if (itemAdapter == null) itemAdapter = ItemAdapter()
                   adapter = itemAdapter
                   addItemDecoration(
                       DividerItemDecoration(context, LinearLayoutManager(context).orientation)
                   )
               }
               // 画面タイトルを「Tasks」に変更
               it.toolbar.title = "Tasks"
               // ボタンタグを動的に変更（要素追加時の判定に利用予定）
               it.addButton.tag = 1
           } else {
               // カテゴリ一覧画面を表示する処理
               with(recyclerView) { // RecyclerViewの設定
                   layoutManager = GridLayoutManager(context, columnCount)
                   if (categoryAdapter == null) categoryAdapter = CategoryAdapter()
                   adapter = categoryAdapter
               }
               // 画面タイトルを「Category」に変更
               it.toolbar.title = "Category"
               // ボタンタグを動的に変更（要素追加時の判定に利用予定）
               it.addButton.tag = 2
           }
       }

       return binding.root
   }
```

MainFragmentのコード全体は以下のようになります。
```Kotlin:MainFragment.kt
class MainFragment : Fragment() {

   private var _binding: FragmentItemListBinding? = null
   private val binding get() = _binding!!

   private var columnCount = 1

   private var categoryAdapter: CategoryAdapter? = null
   private var itemAdapter: ItemAdapter? = null

   override fun onCreateView(
       inflater: LayoutInflater, container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View? {
       _binding = FragmentItemListBinding.inflate(inflater, container, false)
       binding.let {
           val recyclerView = it.recycle
           if (columnCount == 1) {
               with(recyclerView) {
                   layoutManager = LinearLayoutManager(context)
                   if (itemAdapter == null) itemAdapter = ItemAdapter()
                   adapter = itemAdapter
                   addItemDecoration(
                       DividerItemDecoration(context, LinearLayoutManager(context).orientation)
                   )
               }
               it.toolbar.title = "Tasks"
               it.addButton.tag = 1
           } else {
               with(recyclerView) {
                   layoutManager = GridLayoutManager(context, columnCount)
                   if (categoryAdapter == null) categoryAdapter = CategoryAdapter()
                   adapter = categoryAdapter
               }
               it.toolbar.title = "Category"
               it.addButton.tag = 2
           }
       }

       binding.addButton.setOnClickListener {
          when (it.tag) {
              1 ->  {/** タスクの追加処理を実装予定 **/}
              2 ->  {/** カテゴリの追加処理を実装予定 **/}
          }
       }

       return binding.root
   }

   override fun onDestroyView() {
       super.onDestroyView()
       _binding = null
   }
  
   companion object {
       @JvmStatic
       fun newInstance(column: Int) =
           MainFragment().apply {
               columnCount = column
           }
   }
}
```


[続編](https://qiita.com/satona-oinuma/items/f2ad68718da1d12cc82d)に続きます...


## 参考
- [フラグメント | Android デベロッパー | Android Developers](https://developer.android.com/guide/components/fragments?hl=ja)
- [Fragments | Android Developers](https://developer.android.com/guide/fragments)
- [FragmentActivity | Android Developers](https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentActivity?hl=ja)