カテゴリー
Kotlin言語

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

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


WordListFragment クラスの書き換え

アプリのもうひとつのアクティビティも、フラグメントに移動させますね

  • DetailActivity クラスの内容を WordListFragment クラスに移動させます
  • 次のような手順になります

WordListFragment クラスに移植する手順

やることがいっぱい

うん。でも、さいしょに作ったフラグメントと作業はだいたい一緒。思ったよりは簡単なの

  1. DetailActivity から WordListFragment にコンパニオンオブジェクト(定数)をコピーします
  2. WordAdapter にある SEARCH_PREFIX への参照が WordListFragment への参照に更新されていることを確認します
  3. _binding 変数を追加します(この変数は nullable で初期値は null です)
  4. _binding 変数と同じ内容の binding という get-only な変数を追加します
  5. onCreateView() の中でレイアウトを生成します。_bindingの値をセットして、ルートview を返します
  6. onViewCreated() の中で次の設定をします
    • RecyclerView への参照を取得します
    • レイアウトマネージャとアダプターを設定します
    • 項目に装飾を加えます
  7. インテントから文字を取得します
    • フラグメントは intent プロパティを持っていません。フラグメントは通常、親となるアクティビティのインテントにアクセスすべきではありません
    • ここではとりあえず extras を得るのに activity.intent(DetailActivity の intent ではなく親アクティビティの intent)を参照するように変更します
  8. onDestroyView() の中で _binding を null にリセットします
  9. DetailActivity() は onCreate() メソッドだけを残して他を削除します
    • 最終的に AndroidManifest.xml を修正した上で DetailActivity.kt と activity_detail.xml は削除してしまいます

実際に DetailActivity クラスの内容を WordListFragment クラスに移動させる

さっそく行ってみますね

実際に DetailActivity の内容を WordListFragment に移動させてみます

WordListFragment に定数を移動する

プログラムで使う定数ですね

  • コンパニオンオブジェクトを WordListFragment に移動します
companion object {
   val LETTER = "letter"
   val SEARCH_PREFIX = "https://www.google.com/search?q="
}

定数を使っているクラスの方も修正します

定数を参照している LetterAdapter クラスを修正する

  • LetterAdapter クラスを開きます
  • onClickListener() のところに移動します
  • DetailActivity をインテントで起動するようになっています。putExtra() で DetailActivity.LETTER を参照していますが、定数は移動したのでこれを WordListFragment.LETTER に書き換えます
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())

定数を参照している WordAdapter クラスを修正する

  • WordAdapter クラスを開きます
  • onClickListener() のところに移動します
  • DetailActivity.SEARCH_PREFIX を WordListFragment.SEARCH_PREFIX に置き換えます
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")

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

View Binding でUI部品を参照できるように設定しますね

  • WordListFragmentに戻って、FragmentWordListBinding?型の _binding を追加します
private var _binding: FragmentWordListBinding? = null
  • つづけて次のように binding を get-only(取得専用)で設定します。これによって ? を使わずに view を参照できるようになります
private val binding get() = _binding!!

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

レイアウトを生成しますね

  • WordListFragment クラスに onCreateView() を実装します
  • レイアウトを生成(inflate)して _binding 変数を割り当ててます
  • ルートview を返します
override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentWordListBinding.inflate(inflater, container, false)
   return binding.root
}

onViewCreated() にUI部品への処理を記述する

UI部品を操作したときの処理はここに記述するんですね

うん。インテントが直接扱えなくなるので注意してくださいね

  • WordListFragment クラスに onViewCreated() を実装します
  • DetailActivity クラスの onCreate() にある recyclerView の設定をほぼそのまま移動させます
  • インテントの extras からデータを取得している部分を activity?.intent?.extras? に修正します
    • フラグメントには intent プロパティがありません
    • かわりに、親になるアクティビティのインテントを activity?.intent? で参照できるようになっています
  • onCreateView() でのレイアウト生成時に activity?.intent? で参照するデータが利用されます。しかし onCreateView() の時点でアクティビティが存在している保証はないのでこのコードは要注意です
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   val recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(requireContext())
   recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

   recyclerView.addItemDecoration(
       DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
   )
}

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

さいごにバインディングをリセットしますね

onDestroyView() で _binding 変数をリセットするようにします

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

おもったより簡単に書き換えられたかも

DetailActivity クラスを整理する

  • すべての機能を DetailActivity クラスから WordListFragment クラスに移動しました
  • onCreate() メソッド以外の部分を削除してしまいます
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

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

おおっ、すっきり

ここまでのまとめ

Android Studio でメニューから Run を選択して、エラーが出ないか確認しておくといいですね

この時点でビルドして実行することは可能です。ただし、期待するようには動作しません

書き換えたファイルはこんなかんじになりました

WordListFragment.kt

class WordListFragment : Fragment() {

    companion object {
        val LETTER = "letter"
        val SEARCH_PREFIX = "https://www.google.com/search?q="
    }

    private var _binding: FragmentWordListBinding? = null
    private val binding get() = _binding!!

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val recyclerView = binding.recyclerView
       recyclerView.layoutManager = LinearLayoutManager(requireContext())
        recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

        recyclerView.addItemDecoration(
            DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
        )
    }

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

LetterAdapter.kt

class LetterAdapter :
    RecyclerView.Adapter<LetterAdapter.LetterViewHolder>() {

    // Generates a [CharRange] from 'A' to 'Z' and converts it to a list
    private val list = ('A').rangeTo('Z').toList()

    /**
     * Provides a reference for the views needed to display items in your list.
     */
    class LetterViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        val button = view.findViewById<Button>(R.id.button_item)
    }

    override fun getItemCount(): Int {
        return list.size
    }

    /**
     * Creates new views with R.layout.item_view as its template
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LetterViewHolder {
        val layout = LayoutInflater
            .from(parent.context)
            .inflate(R.layout.item_view, parent, false)
        
        // Setup custom accessibility delegate to set the text read
        layout.accessibilityDelegate = Accessibility
        return LetterViewHolder(layout)
    }

    /**
     * Replaces the content of an existing view with new data
     */
    override fun onBindViewHolder(holder: LetterViewHolder, position: Int) {
        val item = list.get(position)
        holder.button.text = item.toString()

        // Assigns a [OnClickListener] to the button contained in the [ViewHolder]
        holder.button.setOnClickListener {
            val context = holder.view.context
            // Create an intent with a destination of DetailActivity
            val intent = Intent(context, DetailActivity::class.java)
            // Add the selected letter to the intent as extra data
            // The text of Buttons are [CharSequence], a list of characters,
            // so it must be explicitly converted into a [String].
            // intent.putExtra(DetailActivity.LETTER, holder.button.text.toString())
            intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
            // Start an activity using the data and destination from the Intent.
            context.startActivity(intent)
        }
    }

    // Setup custom accessibility delegate to set the text read with
    // an accessibility service
    companion object Accessibility : View.AccessibilityDelegate() {
        @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
        override fun onInitializeAccessibilityNodeInfo(
            host: View?,
            info: AccessibilityNodeInfo?
        ) {
            super.onInitializeAccessibilityNodeInfo(host, info)
            // With `null` as the second argument to [AccessibilityAction], the
            // accessibility service announces "double tap to activate".
            // If a custom string is provided,
            // it announces "double tap to <custom string>".
            val customString = host?.context?.getString(R.string.look_up_words)
            val customClick =
                AccessibilityNodeInfo.AccessibilityAction(
                    AccessibilityNodeInfo.ACTION_CLICK,
                    customString
                )
            info?.addAction(customClick)
        }
    }
}

DetailActivity を削除する

MainActivity とちがって、DetailActivity はアプリの中でもうまったく使わないので削除してしまいますね

ファイルを消しちゃうだけじゃだめ?

うん、使っていたファイルを消したときは AndroidManifest.xml を修正するの

  • DetailActivity のすべての機能を WordListFragment に移したので DetailActivity は削除できます
  • DetailActivity.kt と activity_detail.xml を削除して、AndroidManifest.xml を修正します

削除の手順

  1. Project ペインで DetailActivity.kt を右クリックして Delete を選択します。ダイアログがでたら Safe Delete のチェックを外して OK します
  2. activity_detail.xml も同様に削除します
  3. AndroidManifest.xml を開いて次の DetailActivity の部分を削除します
<activity
   android:name=".DetailActivity"
   android:parentActivityName=".MainActivity" />
  • DetailActivity.kt を削除したので、フラグメント2つ(LetterListFragment , WordListFragment)とアクティビティひとつ(MainActivity)になりました
  • コードには DetailActivity を参照する部分がある(LetterAdapterクラスの中)ので、この時点でビルドしようとするとエラーになります

次回はナビゲーションを作成しちゃいますね