カテゴリー
Kotlin言語

ゼロから始めるAndroidアプリ(Kotlin編)Unit3 – 2:フラグメント3(ホームフラグメントの作成)

Android Developers > スタートガイド > Kotlin と Android をゼロから学ぶ > Unit3 – ナビゲーションUnit3-2 の内容をまとめたものです(翻訳ではありません)


単語アプリ(アクティビティ版)をフラグメント版に書き換える

アクティビティとインテントで作ったアプリを、フラグメントを使って書き替えますね

まずは MainActivity の内容を ホームフラグメント に移しちゃうんですね

  • アクティビティ版の2つのアクティビティを2つのフラグメントに置き換えます
  • アクティビティと同じく、フラグメントもレイアウト(XML)とクラス(Kotlinコード)から成ります
  • 具体的なファイルの書き換え対応はつぎの通りです
    • MainActivity.kt -> LetterListFragment.kt
    • activity_main.xml -> fragment_letter_list.xml
    • DetailActivity.kt -> WordListFragment.kt
    • activity_detail.xml -> fragment_word_list.xml
  • LetterListFragment がアプリのホームフラグメントになります

フラグメントのひな型を生成する

フラグメントのひな型、Android Studio が作ってくれちゃうんですね

  • Android Studio の Project で、app を選択します
  • File > New > Fragment > Fragment (Blank) と選択してフラグメントを追加します
  • フラグメントそれぞれに、クラスファイル(Kotlinファイル)とレイアウトファイル(XMLファイル)が生成されます
  • フラグメント1・・・ダイアログには次のように入力します
    • Fragment Name:LetterListFragment
    • Fragment Layout Name:fragment_letter_list(フラグメントの名前を入れると自動で設定されます)
  • フラグメント2・・・ダイアログには次のように入力します
    • Fragment Name:WordListFragment
    • Fragment Layout Name:fragment_word_list(フラグメントの名前を入れると自動で設定されます)

Kotlinファイルを修正する

今回は練習なので、いちど中身をすっきりさせちゃいますね

  • 生成されたKotlinクラスには、一般的によく使われるお決まりのひな型コードが最初から設定されています
  • 今回はコードがどんなふうに動作するのかを知るため、クラス宣言の部分以外をすべて削除して一からフラグメントを実装してみます
  • Kotlinファイルは次のようになります

LetterListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class LetterListFragment : Fragment() {
   
}

WordListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class WordListFragment : Fragment() {
   
}

レイアウトファイルを修正する

レイアウトファイルには、まず RecyclerView を移動させちゃいます

  • fragment_letter_list.xml
    • activity_main.xml のコンテンツをコピーします
    • tools:context のところを .LetterListFragment とします
  • fragment_word_list.xml
    • activity_detail.xml のコンテンツをコピーします
    • tools:context のところを .WordListFragment とします
  • フラグメントのレイアウトファイルは次のようになります

fragment_letter_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".LetterListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp" />

</FrameLayout>

fragment_word_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp"
       tools:listitem="@layout/item_view" />

</FrameLayout>

LetterListFragment クラスの書き換え

MainActivity の内容のほとんどは、ホームフラグメントに移動させちゃいます

やることがいっぱいかも

ひとつひとつ進めるので安心してね

  • アクティビティ版の MainActivity が行っていた処理の多くは LetterListFragment が行います
  • レイアウトの生成(inflate)、view のバインドなどを行います
  • view のバインドは View Binding で行います
    • LetterListFragment で View Binding を利用するには FragmentLetterListBinding(Gradle に View Binding の設定がされていれば自動生成されている)の nullable な参照を作成します
    • nullable にする理由は、onCreateView() が呼ばれるまでレイアウトが生成(inflate)できないためです
    • フラグメントが生成される(ライフサイクルはonCreateではじまる)のと、このプロパティが実際に利用可能になるまでにタイムラグがあるのです
  • フラグメントの view は、フラグメントライフサイクルの中で何度も生成(created)されたり破棄(destroyed)されたりするので、onDestroyView() の中で値をリセットする必要があります

LetterListFragment.kt の修正の手順

View Binding のプロパティを設定する

フラグメントの View Binding の設定って、いつもよりちょっと込み入ってるかも

  • クラスの最初に FragmentLetterListBinding を参照するための _binding というプロパティを用意します
private var _binding: FragmentLetterListBinding? = null
  • このプロパティは nullable なので、_binding のプロパティにアクセスするときは、null安全のためにかならず ? をつけるようにします(たとえば _binding?.viewの名前)
    • ある値にアクセスするときそれが null でないことが確かならば、変数名に !! を付けて対処することもできます(変数が null のときには例外が発生します)
    • このようにすると、他のプロパティとおなじように ? なしでアクセスできます
  • nullable な変数に !! を付けておくのは、使う場所を制限するのに便利です
  • 使う場所は、値が null でないと分かっている場所になります。今回、_binding が onCreateView() の中で割り当てられた後がそれにあたります
  • しかしこの方法で nullable な値にアクセスするのはクラッシュの可能性があり危険なことに違いありません。利用は控えめにすることをおすすめします
  • アンダースコアなしの binding というプロパティを新たに生成して _binding!! をセットします
private val binding get() = _binding!!
  • get() はこのプロパティが “get-only” であることを意味します
  • binding から値を得る(get)ことはできるけれど、ひとたび割り当てがされたら他の値を割り当てることはできません
  • 一般的に、アンダースコアで始まるプロパティ名はプロパティが直接アクセスされることを意図していないことを意味します
  • 今回の場合、LetterListFragment にある view には _binding ではなく、binding プロパティを使ってアクセスすることになります

onCreate の実装(オプションメニューの設定)

フラグメントの onCreate() ってすっきりしてるんですね

  • onCreate() ではオプションメニューの設定をします
  • setHasOptionsMenu() を呼び出すだけで設定できます
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setHasOptionsMenu(true)
}

onCreateView の実装(レイアウトの生成)

レイアウトは onCreateView() で生成するの

  • フラグメントでは、レイアウトは onCreateView() の中で生成(inflate)します
  • _binding の設定、view の生成もこの中で行います
  • onCreateView() は、最後に ルートview を返すようにします
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentLetterListBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}

プロパティの追加(RecyclerView)

クラス全体で使いたいプロパティ(変数)は、クラスの最初に設定しておくんですね

  • bindingプロパティの下に、recyclerView プロパティを追加します
  • MainActivity にあった、表示中のレイアウトの判定に使っている isLinearLayoutManager プロパティもここに移動します
private lateinit var recyclerView: RecyclerView
private var isLinearLayoutManager = true

onViewCreated の実装(RecyclerView)

UI部品(View)への設定は onViewCreated() に書き込んでくださいね

  • onViewCreated() で recyclerView の値をセットします
  • MainActivity にあるように chooseLayout() を呼び出して レイアウトマネージャとアダプタをセットします
  • MainActivity にある chooseLayout() メソッドを LetterListFragment に移動します。レイアウトマネージャの引数にあった this を context に修正します
    • アクティビティと違い、フラグメントは Context ではないので、this としてレイアウトマネージャに context を渡すことができません
    • 代わりに、フラグメントは context プロパティを用意しているので、これを設定します
  • 次のようになります
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   chooseLayout()
}

private fun chooseLayout() {
   when (isLinearLayoutManager) {
       true -> {
           recyclerView.layoutManager = LinearLayoutManager(context)
           recyclerView.adapter = LetterAdapter()
       }
       false -> {
           recyclerView.layoutManager = GridLayoutManager(context, 4)
           recyclerView.adapter = LetterAdapter()
       }
   }
}

onDestroyView の実装(バインディングのリセット)

使い終わったあとの処理も忘れません

最後に view が存在しなくなったときに _binding プロパティを null にリセットします

override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}

オプションメニューの実装

アプリバー(アクションバー)に使うオプションメニューの設定も移動するんですね

うん、ちょっと書き換えるところもあるので注意してね

  • onCreateOptionsMenu() をオーバーライドしてオプションメニューを生成します
  • onCreateOptionsMenu() メソッドは、アクティビティで使うときと、フラグメントで使うときで多少違いがあるので注意します
    • Activity クラスには menuInflater プロパティがあり、これを使ってメニューを生成(inflate)します
    • フラグメントにはこのプロパティがありません。かわりに引数として MenuInflater が渡されるのでこれを使ってメニューを生成(inflate)します
  • またフラグメントで使う onCreateOptionsMenu() は戻り値が必要ありません
  • onOptionsItemSelected() メソッドはオプションメニューが選択されたときの処理です。これはアクティビティ版の MainActivity にあったものをそのまま移動させます
  • onOptionsItemSelected() で setIcon() を使っているので、これもそのまま MainActivity にあったものを移動させます
  • 次のようになります
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.layout_menu, menu)

   val layoutButton = menu.findItem(R.id.action_switch_layout)
   setIcon(layoutButton)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           isLinearLayoutManager = !isLinearLayoutManager
           chooseLayout()
           setIcon(item)

           return true
       }

       else -> super.onOptionsItemSelected(item)
   }
}

private fun setIcon(menuItem: MenuItem?) {
   if (menuItem == null)
       return

   menuItem.icon =
       if (isLinearLayoutManager)
           ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
       else ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_linear_layout)
}

MainActivity クラスを整理する

ほとんど移動できたかも

うん、移動が終わったら MainActivity の onCreate 以外のところは消しちゃって大丈夫

  • すべての機能を MainActivity クラスから LetterListFragment クラスに移動しました
  • MainActivity クラスは、フラグメントを表示するためのレイアウトを生成するだけでよくなりました
  • onCreate() 以外のすべての部分をすべて削除してしまいます
  • MainActivity の onCreate() は次のようになります
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

フラグメントを使うと、MainActivity ってすごくすっきりしちゃうんですね

ここまでのまとめ

最終的にはこんなかんじになりますね

LetterListFragment クラス

class LetterListFragment : Fragment() {
    private var _binding: FragmentLetterListBinding? = null
    private val binding get() = _binding!!

    private lateinit var recyclerView: RecyclerView
    private var isLinearLayoutManager = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentLetterListBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        recyclerView = binding.recyclerView
        chooseLayout()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.layout_menu, menu)

        val layoutButton = menu.findItem(R.id.action_switch_layout)
        setIcon(layoutButton)
    }

    private fun chooseLayout() {
        when (isLinearLayoutManager) {
            true -> {
                recyclerView.layoutManager = LinearLayoutManager(context)
                recyclerView.adapter = LetterAdapter()
            }
            false -> {
                recyclerView.layoutManager = GridLayoutManager(context, 4)
                recyclerView.adapter = LetterAdapter()
            }
        }
    }

    private fun setIcon(menuItem: MenuItem?) {
        if (menuItem == null)
            return

        menuItem.icon =
            if (isLinearLayoutManager)
                ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
            else ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_linear_layout)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_switch_layout -> {
                isLinearLayoutManager = !isLinearLayoutManager
                chooseLayout()
                setIcon(item)

                return true
            }

            else -> super.onOptionsItemSelected(item)
        }
    }
}

MainActivity クラス

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

}

おつかれさまです。チュートリアルはまだもうちょっと続きます。このあとも頑張ってくださいね