araki tech

for developers including me

Android Kotlin日本語チュートリアル-②ライフサイクルとFragment

Android Kotlin日本語チュートリアル

Android Kotlin日本語チュートリアル

本連載記事はこれからAndroidアプリ開発を始める人に向けたチュートリアルです。

コンセプトは

  • プログラミングをあまり知らない人でも完走できる
  • プログラミングにある程度詳しい人にも満足できる
  • 実用的な知識を提供する
  • とにかくわかりやすく

で、全9回と長めですが頑張っていきましょう。

このチュートリアルを終える頃には、Android開発の土台が形成されているだけでなくアプリケーションアーキテクチャの知識が出来上がっているはずです。

作成するのは以下のようなメモアプリです。

Android Kotlin日本語チュートリアル

完成品は HiroshiARAKI/AndroidKotlinTutrialで公開していますので適宜参考にしてください。

第2回 : ライフサイクルとFragment

第2回は、Fragmentと呼ばれるActivityと同じくらい大事な土台となるUIについて覚えましょう。

また、ActivityとFragmentをうまく使いこなす上で、覚えておかなければならないライフサイクルについてもここで学んでいきます。

少し複雑で、「ちょっとすぐには理解できなさそう…」と思うかもしれませんが大丈夫です。

このチュートリアルを通して、少しずつその概念を理解していきましょう。

今回はその大枠を解説するに過ぎません。

ライフサイクルについて

早速ライフサイクルについて触れます。

ライフサイクル (Lifecycle)とはその名の通り、”ある特定のUI”が持つ生存に関する概念です。

“ある特定のUI”とは、第1回で説明したActivityと、今回説明するFragmentがそれに当たります。

Android Kotlin日本語チュートリアルAndroid Kotlin日本語チュートリアル
Activityのライフサイクル外観

Activityにも上記のようなライフサイクルがあり、onCreate()というメソッドから始まり、Activityが破棄されるときはonDestroy()メソッドが呼ばれます。

なぜライフサイクルがあるかと言うと、スマートフォンのハードウェア制限は普通のPCよりも大きく、常にメモリの使用を最適にし続ける必要があります

さらにスマホはバックグラウンドでいくつも動作させられたりするので、バックグラウンドで最低限何が機能していれば良いのかなどを考慮してアプリ開発しなくてはいけません。

そのためのライフサイクルという概念です。

このライフサイクルを持ったUIのベースとなるのがActivityとFragmentであり、この2つの上にLayoutやViewが配置されます。

これらLayoutやViewはその土台となるUIのライフサイクルに依存し、土台のActivityが破棄されれば当然その上のViewも破棄されます。

Fragmentについて

FragmentはActivityと同様に、ライフサイクルを独自に持つUIです。

Activityとの違いは、FragmentはActivity上に複数立ち上げることができ、複雑で細かなUIを構築するのに使います。

大事なのはFragmentはActivity上で生成できる、ということです。

FragmentのライフサイクルはActivityと似ていますが、メソッド名など多少差異があります。

Android Kotlin日本語チュートリアル
Fragmentのライフサイクル外観

Activityより少し細かく複雑ですが、最初のうちはActivityもFragmentも上記の特定メソッドしか使わないので、今はなんとなく把握しておく程度で良いです。

Fragmentを作成してみる

前置きが長くなりましたが、早速Fragmentを実装してみましょう。

app > java > [パッケージ] を右クリック (Macなら2本指タップ) で新たにKotlinファイルを作成します。

Android Kotlin日本語チュートリアル
「Fragment」を直接生成する選択肢もありますが、一から作っていくのを推奨します

名前は、とりあえず「Class」から、名前をNewMemoFragmentにしておきましょう。

Android Kotlin日本語チュートリアル

class NewMemoFragment {
}

最初は空っぽのクラスが生成されます。

早速Fragmentを継承して、Fragmentを作りましょう。

import androidx.fragment.app.Fragment

class NewMemoFragment : Fragment() {
}

これだけではまだレイアウトも無いので、最初にレイアウトをActvityのように作成する必要があります。




Fragmentのレイアウトを作る

res > layout ディレクトリに新たに「Layout Resource File」を作成します。

.src/
├── androidTest
├── main
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── tech.araki.smartmemo
│   │               ├── NewMemoFragment.kt
│   │               └── MainActivity.kt
│   └── res
│       ├── drawable
│       ├── layout
│       │   ├── fragment_new_memo.xml
│       │   └── activity_main.xml
│       ├── values
│       └── values-night
└── test

名前は「fragment_new_memo」として、あとはデフォルトでOKです。

Android Kotlin日本語チュートリアル

中身は空っぽのConstraintLayoutだと思います。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</androidx.constraintlayout.widget.ConstraintLayout>

とりあえず、適当なTextViewを置いておきましょう。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/new_memo_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/new_memo_text"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

文字列はstring.xmlに書いておきます。

<resources>
    <string name="app_name">SmartMemo</string>

    <string name="main_text">Hello Android Kotlin!</string>
    <string name="main_add_button">メモを追加</string>

    <!--  NewMemoFragment  -->
    <string name="new_memo_text">新しいメモを追加</string>
</resources>

シンプルですが、これでFragmentのレイアウトが決まりました。

レイアウトをNewMemoFragmentに適用する

FragmentへのレイアウトXMLファイルの適用は簡単で、親クラスFragmentのコンストラクタでそれを引数に取るものがあるので、それを使います。

import androidx.fragment.app.Fragment

class NewMemoFragment : Fragment(R.layout.fragment_new_memo) {
}

特にまだViewの操作を行うわけではないので、とりあえずこれだけの追記でOKです。

ちなみに、FragmentライフサイクルのうちのonCreateViewで、以下のように直接inflate (レイアウトを中に入れ込む)こともできます。

class NewMemoFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // onCreateViewでレイアウトをinflate (中に入れ込む)
        return inflater.inflate(R.layout.fragment_new_memo, container, false)
    }
}

MainActivityからNewMemoFragmentを追加する

前回はボタンが押されたらテキストの表示を行いましたが、今回はボタンが押されたらNewMemoFragmentを生成し表示してみます。

まずは、activity_main.xmlにFragmentを生成する土台となるFrameLayoutを追加してIDを割り当てて参照できるようにします。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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=".MainActivity">

    <FrameLayout
        android:id="@+id/main_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <TextView/>  <!-- 省略 -->

    <Button/>  <!-- 省略 -->

</androidx.constraintlayout.widget.ConstraintLayout>

そうしたら、Activity側でFragmentを生成する処理を追記します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // findViewById<T>()の <T>は型推論できるので省略可能
        // activity_main.xmlで割り当てたIDからViewを探す
        val mainTextView: TextView = findViewById(R.id.main_text)
        val addButton: Button = findViewById(R.id.add_button)

        addButton.setOnClickListener {
            supportFragmentManager.beginTransaction().apply {
                add(R.id.main_container, NewMemoFragment())
                addToBackStack(null)
                commit()
            }
        }
    }
}

このように、ActivivtyでFragmentを生成するときは、FragmentManager経由でFragmentTransactionのメソッドで追加を行います。

Activityでは、supportFragmentManagerから、beginTransaction()でそれを参照することができます。

add(R.id.main_container, NewMemoFragment())

は、先ほどIDを割り当てたFrameLayout上にNewMemoFragmentを追加しており、

addToBackStack(null)

でバックスタックにこのトランザクションを追加したのち、

commit()

でコミット、という流れです。

バックスタックにトランザクションを追加しなくとも、正常に動作はしますが、後々たくさんのFragmentを積み上げると、「戻るキーイベント」が起こった時に、期待したFragmentやActivityに戻れなくなりますので、記録しておきます。

引数で、nullになっている箇所はFragmentTransactionの名前で、つけるとトランザクションをこの名前で管理できるようになります。

名前をつけると複数積み上がったFragmentの最前面から、n個前の「〇〇」という名前のついたトランザクションで追加したFragmentに戻る、のような操作が可能になります。

とりあえず今は、名前をつけても付けなくてもどちらでも構いません。

早速、実行して「メモを追加する」ボタンを押してみると、どうやら文字が被っていますね。

Android Kotlin日本語チュートリアル

Fragmentはデフォルトだと、背景は透過していて、背後にあるはずのボタンも押せてしまうので、すこしだけコードを追記する必要があります。

また、他のTextViewButtonといったViewとFrameLayout (NewMemoFragment) がz軸方向で同じ階層にいるのも、原因です。

Fragmentの背景を調整する

FragmentをActivityの完全に前面に綺麗に配置するには、

  • 背景色を設定する
  • z軸方向の階層を設定する
  • 裏のActivityにタッチ処理が流れないようにする

といった設定が必要です。

まずは、背景色はfargment_new_memo.xmlで以下のように設定すればOKです。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="#ffffff">

    <TextView
        android:id="@+id/new_memo_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/new_memo_text"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

次に、z軸方向の設定はactivity_main.xmlのFrameLayoutで以下のようにelevationを設定してあげます。

<FrameLayout
    android:id="@+id/main_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:elevation="10dp"/>

なぜ"10dp"かというと、Buttonは他のViewと違いデフォルトである程度のelevationを持っているので、やや大きめに設定してあげます。

ここまでで以下のように、レイアウトの重複は解消されると思います。

ちなみに、Buttonなどelevationが特殊なものを除き、基本的にレイアウトの前後関係はXMLで定義された順になります

もしButtonがなければ、FrameLayout一番最後に定義するだけでOKです。

(AppCompatButtonというもっと扱いやすいButtonもありますが、これは後々触れます)

Android Kotlin日本語チュートリアル

現在はFragmentが前面に配置されるようになりましたが、実はActivityに設置されている背後のButtonがまだタッチ操作をハンドリングしてしまいます。

これを解決するには、前面のFragmentでタッチイベントを消費してしまって、背後に流さないようにします。

いくつか方法はありますが、一番シンプルなのはFragmentをClickableにすることです。

fragment_new_memo.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="#ffffff"
    android:clickable="true">

    <TextView />

</androidx.constraintlayout.widget.ConstraintLayout>

これでFragmentが適切に設定できました。

第2回のまとめ

  • ActivityとFragmentはライフサイクルを持った特別なUI
  • ライフサイクルはアプリがメモリ消費を適切に保つためのシステム
  • Activity上には複数のFragmentを生成することができる

おわりに

第2回はここで終わりになります。

今回は、Androidアプリで大事な概念であるライフサイクルについてまずは説明しました。

そしてライフサイクルを持った特別なUIには、ActivityとFragmentがあります。

これら土台となるUIの使い方を覚えて、やっと複雑でおしゃれなUIレイアウトを構築できるようになります。

次回は、Androdでリストを扱う時によく使われるRecyclerViewで、もっと複雑なUIを構築していきます。

 

Android Kotlin日本語チュートリアル