昨天我們已經接觸 Firebase,今天先把 Android App 很重要的另一塊拼圖補起來:Fragment(片段)。
從今天實作可以看到,我們不再只是「一個 Activity 一個畫面」,而是讓一個 MainActivity 裡面放入不同的 Fragment,並透過按鈕在 Fragment 1 / Fragment 2 / Fragment 3 之間操作。除此之外,今天的程式也實際觀察 Fragment 生命週期,以及 Fragment 與 Activity 之間如何交換資料。你提供的 Fragment_1.kt 已完整加入 onAttach()、onCreate()、onCreateView()、onViewCreated()、onStart()、onResume()、onStop()、onDestroyView()、onDestroy()、onDetach() 等生命週期紀錄。
參考資料:Android Developers-Fragments
前面學 Android 時,我們大量使用 Activity。
可以簡單想成:
Activity = 一個完整畫面
Fragment = Activity 裡面可以替換的小畫面
例如今天的畫面:
┌────────────────────────────────────┐
│ MainActivity │
│ │
│ [ Frag-1 ] [ Frag-2 ] [ Frag-3 ] │
│ │
│ Input data │
│ │
│ ┌────────────────────────────────┐ │
│ │ │ │
│ │ Fragment │ │
│ │ │ │
│ │ TextView │ │
│ │ │ │
│ │ ImageView │ │
│ │ │ │
│ └────────────────────────────────┘ │
└────────────────────────────────────┘
最大的好處就是:不用一直開新的 Activity,也能替換畫面的一部分。
這也是之後做 Bottom Navigation、Tab 分頁、平板雙欄畫面時非常重要的基礎。
參考資料:Android Developers-Fragment overview
今天第一個練習的 Fragment_1.kt,是利用 Android Studio 建立 Fragment 後,再加入自己的元件與操作。
核心宣告:
class Fragment_1 : Fragment() {
這裡和 Activity:
class MainActivity : AppCompatActivity()
最大的差異,就是 Fragment 繼承:
Fragment()
Fragment 自己不是完整 Activity,因此它的 UI 必須透過 onCreateView() 建立。
參考資料:Android Developers-Create a fragment
今天程式裡非常重要的一段:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d("main", "onCreateView-F")
val fragView =
inflater.inflate(R.layout.fragment_1, container, false)
imageViewPic =
fragView.findViewById<ImageView>(R.id.imageView_fragPic)
textViewName =
fragView.findViewById<TextView>(R.id.textView_fragName)
buttonPic =
fragView.findViewById<Button>(R.id.button_frag)
editTextInput =
fragView.findViewById<EditText>(R.id.editTextText_frag)
buttonData =
fragView.findViewById<Button>(R.id.button_FragData)
return fragView
}
這裡的:
inflater.inflate()
就是把:
fragment_1.xml
轉成真正可以顯示的 View。
所以 Fragment 的關係可以理解成:
Fragment_1.kt
│
│ inflate
▼
fragment_1.xml
│
▼
View
│
▼
顯示到 Activity
而 findViewById() 也不是直接從 Activity 找,而是從:
fragView.findViewById()
尋找 Fragment 自己 XML 裡面的元件。
參考資料:Android Developers-Fragment lifecycle
Fragment 並不是只能顯示靜態內容。
今天的範例使用:
private var imgFlag: Boolean = true
記錄目前圖片狀態。
按下按鈕:
buttonPic.setOnClickListener {
if (imgFlag) {
textViewName.text = "Flower 2"
imageViewPic.setImageResource(R.drawable.flower2)
imgFlag = false
} else {
textViewName.text = "Flower 1"
imageViewPic.setImageResource(R.drawable.flower1)
imgFlag = true
}
}
因此每按一次:
Flower 1
↓
按 Button
↓
Flower 2
↓
按 Button
↓
Flower 1
Fragment 本身就可以管理自己的 TextView、ImageView、Button 等 UI 元件。
參考資料:Android Developers-Fragments
Activity 有生命週期:
onCreate()
onStart()
onResume()
onPause()
onStop()
onDestroy()
Fragment 也有自己的生命週期,而且比 Activity 更細。
今天程式特別利用:
Log.d("main", "...")
把各階段印到 Logcat。
例如:
override fun onAttach(context: Context) {
super.onAttach(context)
Log.d("main", "onAttach-F")
}
以及:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
Log.d("main", "onCreate-F")
}
}
建立畫面:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d("main", "onCreateView-F")
// ...
}
畫面建立完成:
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
Log.d("main", "onViewCreated-F")
}
開始顯示:
override fun onStart() {
super.onStart()
Log.d("main", "onStart-F")
}
進入可互動狀態:
override fun onResume() {
super.onResume()
Log.d("main", "onResume-F")
}
離開畫面:
override fun onStop() {
super.onStop()
Log.d("main", "onStop-F")
}
View 被銷毀:
override fun onDestroyView() {
super.onDestroyView()
Log.d("main", "onDestroyView-F")
}
Fragment 本身被銷毀:
override fun onDestroy() {
super.onDestroy()
Log.d("main", "onDestroy-F")
}
最後離開 Activity:
override fun onDetach() {
super.onDetach()
Log.d("main", "onDetach-F")
}
參考資料:Android Developers-Fragment lifecycle
Fragment 建立
│
▼
onAttach()
│
▼
onCreate()
│
▼
onCreateView()
│
▼
onViewCreated()
│
▼
onStart()
│
▼
onResume()
│
▼
使用中
│
▼
onStop()
│
▼
onDestroyView()
│
▼
onDestroy()
│
▼
onDetach()
這個觀念之後非常重要。
因為:
Fragment 本身的生命週期與 Fragment 裡 View 的生命週期並不完全相同。
這也是為什麼後面學 ViewBinding、LiveData、Flow、Navigation Component 時,常常會看到 viewLifecycleOwner。
參考資料:Android Developers-Fragment lifecycle
今天的 MainActivity 同時也加入 Log。
override fun onStart() {
super.onStart()
Log.d("main", "onStart-M")
}
override fun onResume() {
super.onResume()
Log.d("main", "onResume-M")
}
override fun onRestart() {
super.onRestart()
Log.d("main", "onRestart-M")
}
override fun onPause() {
super.onPause()
Log.d("main", "onPause-M")
}
override fun onStop() {
super.onStop()
Log.d("main", "onStop-M")
}
override fun onDestroy() {
super.onDestroy()
Log.d("main", "onDestroy-M")
}
這樣就可以在 Logcat 同時觀察:
-M = MainActivity
-F = Fragment
也就是比較:
Activity Lifecycle
VS
Fragment Lifecycle
這個實驗比單純背生命週期圖更容易理解。
參考資料:Android Developers-Activity lifecycle
這次練習還有一個很有意思的地方。
Fragment 裡取得 Activity 的 TextView:
val textViewMain =
activity?.findViewById<TextView>(R.id.textView_mainData)
接著設定 Fragment 裡按鈕:
buttonData.setOnClickListener {
val input = editTextInput.text.toString()
val data = if (input.isEmpty())
"no input data in fragment"
else
input
textViewMain?.text = data
}
所以流程變成:
Fragment EditText
│
│ 使用者輸入
▼
按下 Button
│
▼
取得 input 字串
│
▼
Activity TextView
│
▼
顯示 Fragment 傳來的資料
這個範例直接證明:
Fragment 與 Activity 並不是完全獨立的兩個世界,它們可以互相溝通。
這裡是今天教材採用的直接操作方式;實務專案若資料互動越來越複雜,後續通常會進一步使用 Fragment Result API、共用 ViewModel 等方式降低耦合。
參考資料:Android Developers-Communicate with fragments
接下來的第二個 Fragment 範例更加重要。
我們建立:
Fragment_1
Fragment_2
Fragment_3
三個 Fragment。
每個 Fragment 都提供:
newInstance()
例如 Fragment 1:
companion object {
fun newInstance(
param1: String,
param2: String
): Fragment_1 {
val fragment = Fragment_1()
val args = Bundle()
args.putString("param1", param1)
args.putString("param2", param2)
fragment.arguments = args
return fragment
}
}
這裡的核心就是:
Bundle()
我們把資料放進 Bundle:
args.putString("param1", param1)
args.putString("param2", param2)
再交給 Fragment:
fragment.arguments = args
參考資料:Android Developers-Pass data between destinations
在 Fragment1.kt 裡:
arguments?.getString("param1")?.let {
textView.text = it
}
所以資料流其實是:
MainActivity
│
│ "Fragment 1"
▼
newInstance()
│
▼
Bundle
│
▼
arguments
│
▼
Fragment_1
│
▼
TextView
同樣的寫法也出現在 Fragment_2 與 Fragment_3。
例如 Fragment 2:
arguments?.getString("param1")?.let {
textView.text = it
}
Fragment 3:
arguments?.getString("param1")?.let {
textView.text = it
}
因此只要 MainActivity 傳不同文字,就可以讓相同概念的 Fragment 顯示不同資料。
參考資料:Android Developers-Fragments
真正把 Fragment 放進 Activity 的關鍵,在今天最後的 MainActivity。
首先建立 Fragment:
frag1 = Fragment_1.newInstance(
"Fragment 1",
"no data"
)
接著:
val fragTransit =
supportFragmentManager.beginTransaction()
這裡出現兩個重要角色:
FragmentManager
FragmentTransaction
可以理解成:
FragmentManager
│
│ 管理
▼
FragmentTransaction
│
│ 執行
▼
add / replace / remove
│
▼
Fragment
參考資料:Android Developers-FragmentManager
今天程式:
fragTransit.add(
R.id.frameLayout_id,
frag1,
"frag-1"
)
意思就是:
把 frag1
↓
加入
↓
R.id.frameLayout_id
所以 XML 裡面的 FrameLayout 可以想成:
Fragment 的舞台。
MainActivity
│
├── Button Frag-1
├── Button Frag-2
├── Button Frag-3
├── EditText
│
└── FrameLayout
│
└── Fragment 顯示區
從你今天的實際執行畫面,也已經可以看到上方 Frag-1 / Frag-2 / Frag-3,下方則是 Fragment 的內容顯示區。
參考資料:Android Developers-Fragment transactions
今天程式還有:
fragTransit.addToBackStack(null)
這個非常重要。
它的用途是把 Fragment 的 Transaction 放進返回堆疊:
Fragment 1
↓
Fragment 2
↓
Fragment 3
如果 Transaction 有加入 Back Stack,使用者按 Android 的返回鍵時,就能依交易紀錄返回先前狀態。
最後:
fragTransit.commit()
才是真正送出這次 Fragment Transaction。
因此完整寫法:
val fragTransit =
supportFragmentManager.beginTransaction()
fragTransit.add(
R.id.frameLayout_id,
frag1,
"frag-1"
)
fragTransit.addToBackStack(null)
fragTransit.commit()
可以簡單記成:
beginTransaction()
↓
add()
↓
addToBackStack()
↓
commit()
參考資料:Android Developers-Fragment transactions
今天提供的另一組程式仍然保留 Firebase Activity 的練習。
FirebaseActivity.kt 目前先建立一個 ListView,產生 20 筆資料,再使用 SimpleAdapter 顯示。
資料建立:
val data = ArrayList<Map<String, String>>()
for (i in 1..20) {
val item = HashMap<String, String>()
item["title"] = "Item $i"
item["subtitle"] = "Sub Item $i"
data.add(item)
}
接著建立:
val adapter = SimpleAdapter(
this,
data,
android.R.layout.simple_list_item_2,
arrayOf("title", "subtitle"),
intArrayOf(
android.R.id.text1,
android.R.id.text2
)
)
listView.adapter = adapter
目前這部分資料仍然是本機建立的測試資料,你提供的程式碼尚未看到真正讀取 Firebase 資料庫的程式,因此文章這裡不能直接說成「已從 Firebase 抓回 20 筆資料」。
今天 Firebase Activity 還加入:
val fab =
findViewById<FloatingActionButton>(
R.id.floatingButton_id
)
並設定:
fab.setOnClickListener {
Toast.makeText(
this,
"Floating button clicked",
Toast.LENGTH_SHORT
).show()
}
所以目前:
FloatingActionButton
│
▼
Click
│
▼
Toast
│
▼
"Floating button clicked"
這也替後續「按 + 新增 Firebase 資料」預留了一個非常合理的 UI 入口。
參考資料:Material Design-Floating Action Button
把今天練習整理起來,大致可以畫成:
MainActivity
│
┌──────────────┼──────────────┐
│ │ │
[Frag-1] [Frag-2] [Frag-3]
│ │ │
└──────────────┼──────────────┘
│
▼
FragmentManager
│
▼
FragmentTransaction
│
┌────────┴────────┐
│ │
add() replace()
│ │
└────────┬────────┘
▼
FrameLayout
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Fragment_1 Fragment_2 Fragment_3
│
├── TextView
├── ImageView
├── EditText
└── Button
│
▼
Activity TextView
這也就是 Fragment 的核心精神:
Activity 負責整體畫面,Fragment 負責可以獨立管理、替換的局部 UI。
參考資料:Android Developers-Fragments
今天不是單純「學會建立 Fragment」,而是開始把 Android UI 從單一 Activity 拆成比較接近真正 App 的架構。
今天實際碰到的重點包括:
Activity 與 Fragment 的差異Fragment() 的建立方式onCreateView() 與 inflate()
Button / TextView / ImageView / EditText
Bundle 與 arguments
newInstance() Factory MethodFragmentManager
FragmentTransaction
add()
addToBackStack()
commit()
ListView + SimpleAdapter
FloatingActionButton
今天的 MainActivity 已經建立三個 Fragment 按鈕以及 Fragment_1 的初始 Transaction;目前提供的這份程式尚未看到三個按鈕的 setOnClickListener 切換實作,因此下一步正好可以繼續完成真正的 Frag-1 → Frag-2 → Frag-3 動態切換。
參考資料:Android Developers-Fragments
Day 27|Android Fragment 實戰:生命週期、FragmentManager 與 Activity 資料傳遞
副標題可以使用:
一個 Activity 不只一個畫面!從 Fragment Lifecycle 到三頁動態切換
今天最大的收穫,就是開始理解 Android App 並不一定需要「換畫面就開 Activity」。透過 Fragment,我們可以將 UI 拆成多個可以獨立管理的區塊,再利用 FragmentManager 動態加入、替換及管理。這個觀念也是接下來可能能會的學習 Navigation Component、BottomNavigationView、ViewModel,甚至更完整 App 架構的重要基礎。
參考資料:Android Developers-Fragment 官方指南