KotlinでFragmentを理解する【Androidアプリ開発】

KotlinでFragmentを理解する【Androidアプリ開発】
KotlinでFragmentを理解する【Androidアプリ開発】

この記事では、Fragmentの使い方をKotlin言語で学んでいきます。ActivityFragmentの連携ができるようになるまでの解説となります。ActivityからFragmentViewへアクセスしたり、値を渡したりする方法を学べます。ActiviyFragmentの使い分けがしっかりできるようになると、Androidアプリ開発もだいぶラクになります。ぜひこの機会に、Fragmentを使いこなせるようになってください。

この記事でやること

この記事でやること
この記事でやること

この記事では、図のようにな2つの Fragment を、1つの Activity で操作できるように学習を行なっていきます。Fragmentの理解が目的ですので、できる限り最小限で簡単なプログラミングになるよう心がけました。

なお、本記事で説明しているソースコードは、 AndroidExercise/TryFragment · GitHub で公開中のサンプルプロジェクトの一部のみとなります。詳しくはGitHubのソースコードをご覧になさってください。 AndroidExercise/TryFragment · GitHub

また本記事を書くにあたって、こちらの記事を参考にさせて頂きました。 フラグメント - Androidデベロッパードキュメントガイド Fragment上のonClickとかをFragment内で受け取る - Qiita Fragmentでのイベントハンドリング - 忘れん坊のサンタクロース FragmentのViewをいじってみる - umegusa's blog [Android] アクティビティ内で動的にフラグメントを生成してレイアウトに追加する | rakuishi.com

ベースとなるレイアウトファイルの用意

ArduinoStudioでFragmentのテスト用の新規アプリケーションを作っていきましょう。 まずFragmentを動的に追加するためのコンテナとして、次のような空っぽの状態のレイアウトファイルを用意します。今回は1つだけしかActivityを使わないので、デフォルトで作成されているactivity_main.xmlを修正することにします。ConstraintLayoutidcontainerとしていることに注意してください。

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

Fragmentの用意

つぎに、Fragmentを作成します。ここでは次のうようなHogeFragmentクラスを用意しました。

kotlin
class HogeFragment : Fragment() {

    private var listener: OnHogeFragmentListener? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val fragmentView = inflater.inflate(R.layout.fragment_hoge, container, false)
        val addFragment = fragmentView.findViewById<Button>(R.id.addFragment)
        addFragment.setOnClickListener {
            addFragment()
        }
        return fragmentView
    }

    fun addFragment() {
        listener?.onHogeFragmentAddFragment()
    }

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

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

    interface OnHogeFragmentListener {
        fun onHogeFragmentAddFragment()
    }

}

ActivityからFragmentを呼び出す

Activityから先ほどのHogeFragmentを呼びだしてみましょう。ActivityonCreateで表示させたいFragmentをインスタンス化し、supportFragmentManagerを使ってFragmentを追加します。
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	setContentView(R.layout.activity_main)

	val hogeFragment = HogeFragment()
	val transaction = supportFragmentManager.beginTransaction()
	transaction.add(R.id.container, hogeFragment)
	transaction.commit()
}

Fragmentにボタンとイベントを追加する

さきほどのHogeFragmentクラスに、ボタンを追加してイベントを設定してみましょう。 次のようにクラスに実装しました。inflaterを使って作成したViewからfindViewByIdで検索し、ボタンの参照を取得してます。また、setOnClickListenerを使って、ボタンのクリックイベントを設定しました。

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

	val addFragment = fragmentView.findViewById<Button>(R.id.addFragment)
	addFragment.setOnClickListener {
		addFragment()
	}
	return fragmentView
}

レイアウトファイルのonClick属性を使ってメソッドを実装しようとすると、ボタンイベント時に次のエラーになってしまいました。クラス内のメソッドを見つけられないようです。

java.lang.IllegalStateException: Could not find method onButtonPressed(View) in a parent or ancestor Context for android:onClick attribute defined on view class android.support.v7.widget.AppCompatButton with id 'button' at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.resolveMethod(AppCompatViewInflater.java:424)

Fragmentのレイアウト中のViewにアクセスする

Fragmentのレイアウト中のViewにアクセスしてみましょう。 たとえば、TextViewのレイアウトがあったとして、次のようにしてViewの参照を保持することでアクセスできます。

kotlin
private var fugaText:TextView? = null

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

このFragment全体のコードは次のとおりです。名前をFugaFragmentとしました。

kotlin
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class FugaFragment : Fragment() {
    private var param1: String? = null
    private var param2: String? = null
    private var listener: OnFugaFragmentListener? = null
    private var fugaText:TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

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

        fugaText = view.findViewById(R.id.fugaText)

        val displayParams = view.findViewById<Button>(R.id.displayParams)
        displayParams.setOnClickListener {
            displayParams()
        }

        val finishButton = view.findViewById<Button>(R.id.finishButton)
        finishButton.setOnClickListener {
            finish()
        }
        return view
    }

    fun displayParams() {
        fugaText?.text = "$ARG_PARAM1: $param1 / $ARG_PARAM2: $param2"
    }

    fun finish() {
        listener?.onHugaFragmentFinish()
    }

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

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

    interface OnFugaFragmentListener {
        fun onHugaFragmentFinish()
    }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            FugaFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

ActivityからFragmentへ初期値を渡す

ActivityからFragmentへ初期値を渡すには、ファクトリーメソッドを使います。 AndroidStudioでFragmentを新規作成するときに、図のようにinclude fragment factory methods?の項目にチェックを入れます。これでFragmentにファクトリーメソッドが記述された状態で作ることができます。

ファクトリーメソッドの自動生成
ファクトリーメソッドの自動生成

次がファクトリーメソッドを実装したFragmentになります。companion objectというシングルトンで宣言されているのがファクトリーメソッドです。

kotlin
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class FugaFragment : Fragment() {
    private var param1: String? = null
    private var param2: String? = null

	...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

	...
	companion object {
		@JvmStatic
		fun newInstance(param1: String, param2: String) =
			FugaFragment().apply {
				arguments = Bundle().apply {
					putString(ARG_PARAM1, param1)
					putString(ARG_PARAM2, param2)
				}
			}
	}
}

さて、ActivityからFragmentを生成し、ファクトリーメソッドに引数を入れて初期値を渡してみましょう。

kotlin
val hugaFragment = FugaFragment.newInstance("りんご", "バナナ")

このように、ファクトリーメソッドを使うことでActivityからFragmentへ値を渡すことができました。

Fragmentの切り替えとバックスタックへ追加

Fragmentをバックスタックへ追加
Fragmentをバックスタックへ追加

今回のプロジェクトでは、FugeFragmentのボタンを押すとActivityonFugeFragmentAddFragmentがイベントされます。onFugeFragmentAddFragmentメソッドの中では、次のようにしてFragmentの切り替えを行なってます。これはバックスタックに追加する処理で、Fragmentを重ねるようなイメージです。

kotlin
override fun onHogeFragmentAddFragment() {
	val hugaFragment = FugaFragment.newInstance("りんご", "バナナ")
	val transaction = supportFragmentManager.beginTransaction()
	transaction.replace(R.id.container, hugaFragment)
	transaction.addToBackStack(null) // バックスタックに保存する。呼び出さなければ積まれない。
	transaction.commit()
}
transaction.addToBackStack(null)の一行がバックスタック処理です。これによって、ひとつ前に表示されていたHogeFragmentはバックスタックに追加され、FugaFragmentが表示されます。バックスタックに追加しておくと、たとえば、FugaFragmentから1つ前のHogeFragmentへ戻ることが可能となります。Android端末の「戻る」ボタンからも1つ前のFragmentへ戻ることが可能です。もしも戻ることを許可したくない場合は、addToBackStackの一行は省いてください。

ところでActivityFragmentのやり取りは、このようにインタフェースを介して実現してます。

kotlin
class HogeFragment : Fragment() {

    private var listener: OnHogeFragmentListener? = null

    ...

    fun addFragment() {
        listener?.onHogeFragmentAddFragment()
    }

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

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

    interface OnHogeFragmentListener {
        fun onHogeFragmentAddFragment()
    }
}

詳しくはこちらの サンプルプロジェクト をご覧ください。

バックスタックから1つ前のFragmentを取り出す。

バックスタックから1つ前のFragmentを取り出す。
バックスタックから1つ前のFragmentを取り出す。

バックスタックから1つ前のFragmentを取り出すと、いわゆる画面を「戻る」ことになります。次のようにしてpopBackStack関数を呼び出します。

kotlin
override fun onHugaFragmentFinish() {
	supportFragmentManager.popBackStack()
}

ちなみに、バックスタックから1つ前のFragmentを取り出すと、現在アクティブだったFragmentは非アクティブになり、onDetachが呼ばれ開放さます。

さいごに

FragmentActivityに似たライフサイクルをもつ事ができるクラスです。ここでは紹介しませんでしたが、ビューを持たないFragmentを作ることも可能です。 Fragmentを使うと、さまざまな処理をFragment内に任せることができるので、Activityが肥大化せずに済みます。 【Kotlin】DialogFragmentでダイアログ表示【Androidアプリ開発】 で紹介しましたが、ダイアログ表示もFragmentを継承したDialogFragmentで実現されてます。

▼ こんな記事も書いてます。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

関連記事