【Android】画面コンポーネント / Fragment

■ はじめに

https://dk521123.hatenablog.com/entry/2020/07/20/232009

の続き。

単語帳だけでなく、TOEICの文法問題も扱えるようにするために
質問⇒回答・解説のような画面を作りたい。

そこで、画面を切り替えられる Fragment(フラグメント)について扱う。

■ サンプル

* 一つのActivityに対して、2つのFragmentを切り替えていく画面を考える
* 以下のサイトがイメージ付きやすいかも。

https://qiita.com/daichi77/items/09119cdba435ead4a08c

画面レイアウト

[Activity] activity_grammar_exam.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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=".exam.grammars.GrammarExamActivity">

    <FrameLayout
        android:id="@+id/grammarsExamFrameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:foregroundGravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"></FrameLayout>
</LinearLayout>

[Fragment] fragment_grammar_exam_question.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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"
    android:orientation="vertical"
    tools:context=".exam.grammars.GrammarExamQuestionFragment">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingStart="20dp"
        android:paddingTop="20dp"
        android:padding="30dp">

        <TextView
            android:id="@+id/grammarQuestionTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="24sp" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="20dp">

        <TextView
            android:id="@+id/candidateTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="24sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="bottom|center"
        android:orientation="horizontal"
        android:padding="5dp">

        <Button
            android:id="@+id/selectAButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/select_a_button"
            android:layout_margin="5dp"/>

        <Button
            android:id="@+id/selectBButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/select_b_button"
            android:layout_margin="5dp"/>

        <Button
            android:id="@+id/selectCButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/select_c_button"
            android:layout_margin="5dp"/>

        <Button
            android:id="@+id/selectDButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/select_d_button"
            android:layout_margin="5dp"/>
    </LinearLayout>
</LinearLayout>

[Fragment] fragment_grammar_exam_answer.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".exam.grammars.GrammarExamAnswerFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp">
        <TextView
            android:id="@+id/correctOrNoTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="24sp" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp">
        <TextView
            android:id="@+id/questionTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="24sp" />
        <TextView
            android:id="@+id/questionTranslationTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="12sp" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp">
        <TextView
            android:id="@+id/commentaryTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="24sp" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp">

        <Button
            android:id="@+id/nextQuestionButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/next_question_button" />
    </LinearLayout>
</LinearLayout>

画面制御

GrammarExamActivity.kt

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.dk.englishcards.R
import com.dk.englishcards.commons.BaseSubPageActivity

class GrammarExamActivity :
    BaseSubPageActivity(),
    GrammarExamQuestionFragment.OnSelectedAnswer,
    GrammarExamAnswerFragment.OnClickNextButton {

    private var questionNo: Int = 1
    private lateinit var grammarExams: List<GrammarExam>
    private lateinit var grammarExamDbHandler: GrammarExamDbHandler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_grammar_exam)

        this.grammarExamDbHandler = GrammarExamDbHandler()
        this.grammarExams = this.grammarExamDbHandler.readAll().toList().shuffled()
        this.showQuestion()
    }

    override fun onSelectedAnswer(isCorrectAnswer: Boolean) {
        Log.d("Grammar", "isCorrectAnswer = $isCorrectAnswer")
        val grammarExam = this.getTargetGrammarExam()
        val collectAnswer = when (grammarExam.answer) {
            "A" -> grammarExam.candidateA
            "B" -> grammarExam.candidateB
            "C" -> grammarExam.candidateC
            "D" -> grammarExam.candidateD
            else -> ""
        }
        val grammarExamAnswerFragment =
            GrammarExamAnswerFragment.newInstance(
                this.questionNo,
                isCorrectAnswer,
                collectAnswer,
                grammarExam.question,
                grammarExam.questionTranslation,
                grammarExam.commentary)
        grammarExamAnswerFragment.setClickNextButtonListener(this)
        this.replaceFragment(grammarExamAnswerFragment)
    }

    override fun onClickNextButton() {
        Log.d("Grammar", "Called onClickNextButton")
        if (this.grammarExams.size <= this.questionNo) {
            Toast.makeText(this, "Try again...", Toast.LENGTH_SHORT).show()
            this.questionNo = 1
            this.grammarExams = this.grammarExams.shuffled()
        } else {
            this.questionNo++
        }
        this.showQuestion()
    }

    private fun replaceFragment(fragment: Fragment) {
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.replace(R.id.grammarsExamFrameLayout, fragment)
        fragmentTransaction.commit()
    }
    private fun getTargetGrammarExam(): GrammarExam {
        val index = this.questionNo - 1
        return this.grammarExams[index]
    }

    private fun showQuestion() {
        val grammarExam = this.getTargetGrammarExam()
        val grammarExamQuestionFragment =
            GrammarExamQuestionFragment.newInstance(
                this.questionNo,
                grammarExam.question,
                grammarExam.candidateA,
                grammarExam.candidateB,
                grammarExam.candidateC,
                grammarExam.candidateD,
                grammarExam.answer)
        grammarExamQuestionFragment.setSelectedAnswerListener(this)
        this.replaceFragment(grammarExamQuestionFragment)
    }
}

GrammarExamQuestionFragment.kt

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.dk.englishcards.R
import kotlinx.android.synthetic.main.fragment_grammar_exam_question.*

private const val ARG_NO = "no"
private const val ARG_QUESTION = "question"
private const val ARG_CANDIDATE_A = "candidateA"
private const val ARG_CANDIDATE_B = "candidateB"
private const val ARG_CANDIDATE_C = "candidateC"
private const val ARG_CANDIDATE_D = "candidateD"
private const val ARG_ANSWER = "answer"

/**
 * A simple [Fragment] subclass.
 * Use the [GrammarExamQuestionFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class GrammarExamQuestionFragment : Fragment() {
    private var no: Int = 1
    private var question: String? = ""
    private var candidateA: String? = ""
    private var candidateB: String? = ""
    private var candidateC: String? = ""
    private var candidateD: String? = ""
    private var answer: String? = ""
    private var listener: OnSelectedAnswer? = null

    interface OnSelectedAnswer {
        fun onSelectedAnswer(isCorrectAnswer :Boolean)
    }

    companion object {
        @JvmStatic
        fun newInstance(
            no: Int,
            question: String,
            candidateA: String,
            candidateB: String,
            candidateC: String,
            candidateD: String,
            answer: String) =
            GrammarExamQuestionFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_NO, no)
                    putString(ARG_QUESTION, question)
                    putString(ARG_CANDIDATE_A, candidateA)
                    putString(ARG_CANDIDATE_B, candidateB)
                    putString(ARG_CANDIDATE_C, candidateC)
                    putString(ARG_CANDIDATE_D, candidateD)
                    putString(ARG_ANSWER, answer)
                }
            }
    }

    fun setSelectedAnswerListener(listener: OnSelectedAnswer) {
        this.listener = listener
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments.let {
            no = it!!.getInt(ARG_NO)
            question = it.getString(ARG_QUESTION)
            candidateA = it.getString(ARG_CANDIDATE_A)
            candidateB = it.getString(ARG_CANDIDATE_B)
            candidateC = it.getString(ARG_CANDIDATE_C)
            candidateD = it.getString(ARG_CANDIDATE_D)
            answer = it.getString(ARG_ANSWER)
        }
        Log.d("Grammar", "${this.no}, ${this.question}")
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_grammar_exam_question, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        grammarQuestionTextView.text = "Q${this.no}. ${this.question}"
        candidateTextView.text =
            "(A) ${this.candidateA}\n" +
                    "(B) ${this.candidateB}\n" +
                    "(C) ${this.candidateC}\n" +
                    "(D) ${this.candidateD}"

        selectAButton.setOnClickListener {
            val isCorrectAnswer = this.isCorrectAnswer(this.answer, "A")
            this.listener?.onSelectedAnswer(isCorrectAnswer)
        }
        selectBButton.setOnClickListener {
            val isCorrectAnswer = this.isCorrectAnswer(this.answer, "B")
            this.listener?.onSelectedAnswer(isCorrectAnswer)
        }
        selectCButton.setOnClickListener {
            val isCorrectAnswer = this.isCorrectAnswer(this.answer, "C")
            this.listener?.onSelectedAnswer(isCorrectAnswer)
        }
        selectDButton.setOnClickListener {
            val isCorrectAnswer = this.isCorrectAnswer(this.answer, "D")
            this.listener?.onSelectedAnswer(isCorrectAnswer)
        }
    }

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

    private fun isCorrectAnswer(answer: String?, selectedAnswer: String): Boolean {
        return answer!!.toUpperCase() == selectedAnswer
    }
}

GrammarExamAnswerFragment.kt

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.dk.englishcards.R
import kotlinx.android.synthetic.main.fragment_grammar_exam_answer.*

private const val ARG_NO = "no"
private const val ARG_IS_COLLECT_ANSWER = "isCollectAnswer"
private const val ARG_ANSWER = "answer"
private const val ARG_QUESTION = "question"
private const val ARG_QUESTION_TRANSLATIONS = "questionTranslation"
private const val ARG_COMMENTARY = "commentary"
/**
 * A simple [Fragment] subclass.
 * Use the [GrammarExamAnswerFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class GrammarExamAnswerFragment : Fragment() {
    private var no: Int = 1
    private var isCollectAnswer: Boolean = false
    private var answer: String? = ""
    private var question: String? = ""
    private var questionTranslation: String? = ""
    private var commentary: String? = ""
    private var listener: OnClickNextButton? = null

    interface OnClickNextButton {
        fun onClickNextButton()
    }

    companion object {
        @JvmStatic
        fun newInstance(
            no: Int,
            isCollectAnswer: Boolean,
            answer: String,
            question: String,
            questionTranslation: String,
            commentary: String
        ) =
            GrammarExamAnswerFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_NO, no)
                    putBoolean(ARG_IS_COLLECT_ANSWER, isCollectAnswer)
                    putString(ARG_ANSWER, answer)
                    putString(ARG_QUESTION, question)
                    putString(ARG_QUESTION_TRANSLATIONS, questionTranslation)
                    putString(ARG_COMMENTARY, commentary)
                }
            }
    }

    fun setClickNextButtonListener(listener: OnClickNextButton) {
        this.listener = listener
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            no = it.getInt(ARG_NO)
            isCollectAnswer = it.getBoolean(ARG_IS_COLLECT_ANSWER)
            answer = it.getString(ARG_ANSWER)
            question = it.getString(ARG_QUESTION)
            questionTranslation = it.getString(ARG_QUESTION_TRANSLATIONS)
            commentary = it.getString(ARG_COMMENTARY)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_grammar_exam_answer, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        correctOrNoTextView.text =
            "Q${this.no} " + if (this.isCollectAnswer) "Collect!!!" else "Incorrect..."
        questionTextView.text = this.question.toString().replace(
            "-------", "( " + this.answer.toString() + " )")
        questionTranslationTextView.text = this.questionTranslation
        commentaryTextView.text = this.commentary

        nextQuestionButton.setOnClickListener {
            this.listener?.onClickNextButton()
        }
    }

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

データオブジェクト(あまり本質じゃないが)

GrammarExam.kt

import com.dk.englishcards.cards.EnglishCard
import com.dk.englishcards.exam.words.EnglishWordsExam
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import io.realm.annotations.Required
import java.util.*

open class GrammarExam : RealmObject() {
    @PrimaryKey
    var grammarExamId: String = UUID.randomUUID().toString()
    @Required
    var question: String = ""
    @Required
    var candidateA: String = ""
    @Required
    var candidateB: String = ""
    @Required
    var candidateC: String = ""
    @Required
    var candidateD: String = ""
    @Required
    var answer: String = ""
    var commentary: String = ""
    var questionTranslation: String = ""

    companion object {
        const val ID_FIELD = "grammarExamId"

        @JvmStatic
        fun newInstance(
            question: String,
            candidateA: String,
            candidateB: String,
            candidateC: String,
            candidateD: String,
            answer: String,
            commentary: String = "",
            questionTranslation: String = ""
        ) = GrammarExam().apply {
            this.question = question
            this.candidateA = candidateA
            this.candidateB = candidateB
            this.candidateC = candidateC
            this.candidateD = candidateD
            this.answer = answer
            this.commentary = commentary
            this.questionTranslation = questionTranslation
        }
    }
}

関連記事

レイアウト ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2015/08/23/165632
Kotlin / Realm で英単語帳を作る https://dk521123.hatenablog.com/entry/2020/07/20/232009
Android / Kotlin で画像検索を実装する
https://dk521123.hatenablog.com/entry/2020/09/21/224542