Android Kotlin日本語チュートリアル-②ライフサイクルとFragment
Android Kotlin日本語チュートリアル
本連載記事はこれからAndroidアプリ開発を始める人に向けたチュートリアルです。
コンセプトは
- プログラミングをあまり知らない人でも完走できる
- プログラミングにある程度詳しい人にも満足できる
- 実用的な知識を提供する
- とにかくわかりやすく
で、全9回と長めですが頑張っていきましょう。
このチュートリアルを終える頃には、Android開発の土台が形成されているだけでなくアプリケーションアーキテクチャの知識が出来上がっているはずです。
作成するのは以下のようなメモアプリです。
完成品は HiroshiARAKI/AndroidKotlinTutrialで公開していますので適宜参考にしてください。
第2回 : ライフサイクルとFragment
第2回は、Fragmentと呼ばれるActivityと同じくらい大事な土台となるUIについて覚えましょう。
また、ActivityとFragmentをうまく使いこなす上で、覚えておかなければならないライフサイクルについてもここで学んでいきます。
少し複雑で、「ちょっとすぐには理解できなさそう…」と思うかもしれませんが大丈夫です。
このチュートリアルを通して、少しずつその概念を理解していきましょう。
今回はその大枠を解説するに過ぎません。
ライフサイクルについて
早速ライフサイクルについて触れます。
ライフサイクル (Lifecycle)とはその名の通り、”ある特定のUI”が持つ生存に関する概念です。
“ある特定のUI”とは、第1回で説明したActivityと、今回説明するFragmentがそれに当たります。
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と似ていますが、メソッド名など多少差異があります。
Activityより少し細かく複雑ですが、最初のうちはActivityもFragmentも上記の特定メソッドしか使わないので、今はなんとなく把握しておく程度で良いです。
Fragmentを作成してみる
前置きが長くなりましたが、早速Fragmentを実装してみましょう。
app > java > [パッケージ] を右クリック (Macなら2本指タップ) で新たにKotlinファイルを作成します。
名前は、とりあえず「Class」から、名前をNewMemoFragmentにしておきましょう。
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です。
中身は空っぽの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()
でコミット、という流れです。
apply { }
などのスコープ関数は、Kotlinの拡張関数とスコープ関数を使いこなそう【初学者向け】でまとめています
バックスタックにトランザクションを追加しなくとも、正常に動作はしますが、後々たくさんのFragmentを積み上げると、「戻るキーイベント」が起こった時に、期待したFragmentやActivityに戻れなくなりますので、記録しておきます。
引数で、null
になっている箇所はFragmentTransactionの名前で、つけるとトランザクションをこの名前で管理できるようになります。
名前をつけると複数積み上がったFragmentの最前面から、n個前の「〇〇」という名前のついたトランザクションで追加したFragmentに戻る、のような操作が可能になります。
とりあえず今は、名前をつけても付けなくてもどちらでも構いません。
早速、実行して「メモを追加する」ボタンを押してみると、どうやら文字が被っていますね。
Fragmentはデフォルトだと、背景は透過していて、背後にあるはずのボタンも押せてしまうので、すこしだけコードを追記する必要があります。
また、他のTextView
やButton
といった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もありますが、これは後々触れます)
現在は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を構築していきます。