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

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

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

この記事でやること

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

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

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

また本記事を書くにあたって、こちらの記事を参考にさせて頂きました。

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

ArduinoStudioでFragmentのテスト用の新規アプリケーションを作っていきましょう。

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

<?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クラスを用意しました。

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を追加します。

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を使って、ボタンのクリックイベントを設定しました。

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の参照を保持することでアクセスできます。

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としました。

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というシングルトンで宣言されているのがファクトリーメソッドです。

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を生成し、ファクトリーメソッドに引数を入れて初期値を渡してみましょう。

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

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

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

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

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

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のやり取りは、このようにインタフェースを介して実現しています。

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関数を呼び出します。

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

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

さいごに

FragmentActivityに似たライフサイクルをもつ事ができるクラスです。ここでは紹介しませんでしたが、ビューを持たないFragmentを作ることも可能です。

Fragmentを使うと、さまざまな処理をFragment内に任せることができるので、Activityが肥大化せずに済みます。【Kotlin】DialogFragmentでダイアログ表示【Androidアプリ開発】で紹介しましたが、ダイアログ表示もFragmentを継承したDialogFragmentで実現されています。ご参考になさってみてください。

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

「キッチンノート.fun」という料理サイトを立ち上げました!このサイトで紹介していた料理記事は、そちらへ移動しました。
記事に関するご質問などがあれば、
Twitter または お問い合わせ までご連絡ください。
関連記事