引き続き、Android向けTODOアプリ作成についてまとめます。
前回の記事はKotlinでTODOアプリをつくる~その1~をご覧ください。
その2で扱う内容
その2では画面遷移周りを扱います。
- 画面遷移:Fragment
以下はその2では扱わず、続編で扱う予定です。
- リストの表示方法:RecyclerView
- データやデータの保存に関すること:内部ファイルへの保存を予定
用意するクラス
今回は新たに以下の2つのクラスを用意します。
- CategoryAdapter.kt
- ItemAdapter.kt
どちらもRecyclerViewのAdapterを継承したクラスです。
内容についてはその3でまとめます。
また、前回に引き続き、以下の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のいずれかになっているかをご確認ください。
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 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 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内に記載するのは、事前にインスタンスを生成せずにクラス名.メソッド名(引数)
の形でメソッドの実行ができるためです。
private var columnCount = 1
companion object {
@JvmStatic
// 値の受け渡しメソッド、戻り値はMainFragment型
fun newInstance(column: Int) =
// インスタンスを生成し、プロパティに値を代入
MainFragment().apply {
columnCount = column
}
}
以下のように呼び出すことで、上記が動きます。
MainFragment.newInstance(2)
画面遷移の判定
今回のアプリでは、カテゴリ一覧画面とタスク一覧画面はほとんど同じ作りになっています。
そのため、Fragmentとして描画するレイアウト(fragment_item_list.xml)は同じものを使用し、以下の要素はページによって出し分けるようにします。
- 画面タイトル
- 追加ボタンで追加する要素
- リスト要素
今回は、リストの列数が1列の場合はタスク一覧画面、2列の場合はカテゴリ一覧画面を表示します。
なお、数字の1、2は上記の判定に使用するのみで、実際のRecyclerViewの列数指定とは関係ありません。指定方法の詳細についてはその3でまとめます。
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のコード全体は以下のようになります。
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
}
}
}
続編に続きます...