昨天完成了 PirateListViewModel
並且和他的 View
,今天就要接續完成 GetPirate Fragment
的部份,大致上的行為都和昨天會是一樣的,因此直接上 Code
首先一樣先在 viewmodel
下建立 GetPirateViewModel.kt
,而pirateListLiveData
中會帶一個參數 - pirateId
class GetPirateViewModel(application: Application) :
AndroidViewModel(application) {
private val pirateDetailRepository: PirateDetailRepository by lazy { PirateDetailRepository() }
fun pirateListLiveData(pirateId: Int): LiveData<PirateInfo> =
liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emitSource(
pirateDetailRepository.fetchPirateInfo(
pirateId = pirateId,
onSuccess = { },
onError = { }
).asLiveData()
)
}
}
class GetPirateFragment : Fragment() {
private val TAG: String = tag.toString()
private val getPirateViewModel: GetPirateViewModel by lazy {
ViewModelProvider(this).get(GetPirateViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_get_pirate, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setGetPirateViewModel()
}
private fun setGetPirateViewModel() {
getPirateViewModel.pirateListLiveData((1 .. 10).random()).observe(viewLifecycleOwner, Observer { pirate ->
})
}
companion object {
fun newInstance(): GetPirateFragment = GetPirateFragment()
}
}
這邊需要注意到在 pirateListLiveData()
中我採用 (1..10).random()
隨機產生 id
,再撈資料回來
那完成到這裡後,就可以進到生成 海盜卡 的步驟了!!!
接下來 Main Activity 會長成這個模樣(很醜我知道...QQ
那在這個 View
中包含了兩個 Component
因此我們就一步步把這個 UI 建起來,首先先從 Card View
開始
那 Material Design
有出 CardView
元件,元件名稱就叫作 MaterialCardView
,那我回希望這個 CardView
是可以被重複在很多地方利用的,因此我不會把這個 CardView
建立在特定的 Fragment
中,而是拉出來成為令一個 xml
檔
首先先到 res/layout
下面新建一個 Layout Resource File
,為他命名為 card_view_pirate
那 Parent Tag
的部份就選擇 com.google.android.material.card.MaterialCardView
,那可以看到裏面還有很多奇怪的 attribute
,例如允許使用者點擊 CardView
,那其實是我為了後面的鋪陳暫時先留下來的坑,暫時先保密(拖稿的好藉口~
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_half"
app:strokeWidth="1dp"
app:strokeColor="@color/primaryDarkColor"
app:cardElevation="0dp"
android:clickable="true"
android:focusable="true"
android:checkable="true"
app:cardBackgroundColor="@color/primaryLightColor">
</com.google.android.material.card.MaterialCardView>
首先先來觀察一下 CardView
中會需要哪些元件
可以看到其實需要的元件不多,就只有
因此我們可以依照這樣的方式把 UI 結構產生出來,並擺到 MaterialCardView
中
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/aoe_arbalester"/>
<TextView
android:id="@+id/pirate_name"
android:text="Arbalester"
android:textSize="@dimen/item_text_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_half"
android:layout_marginStart="@dimen/margin_half_plus_eight"
android:textColor="@color/secondaryLightColor"/>
<TextView
android:id="@+id/pirate_height"
android:text="100 cm"
android:textSize="@dimen/item_text_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_half"
android:layout_marginStart="@dimen/margin_half_plus_eight"
android:textColor="@color/secondaryLightColor"/>
<TextView
android:id="@+id/pirate_weight"
android:text="100 kg"
android:textSize="@dimen/item_text_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_half"
android:layout_marginStart="@dimen/margin_half_plus_eight"
android:textColor="@color/secondaryLightColor"/>
<TextView
android:id="@+id/pirate_attack"
android:text="Attack"
android:textSize="@dimen/item_text_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_half"
android:layout_marginStart="@dimen/margin_half_plus_eight"
android:layout_marginBottom="@dimen/margin_half"
android:textColor="@color/secondaryLightColor"/>
</LinearLayout>
另外提一下,這邊的 ImageView
我有先塞一張圖片當作預設,所以如果照做會找不到圖片路徑是正常的
那我的圖片是在 AOE Wiki 上面抓下來的,抓了 10 張圖來當作海賊的圖片
在 activity_main.xml
中就要把剛剛完成的 CardView
加進來,並且要創建一個按鈕讓使用者按下去後更新 CardView
內容,因此佈局會長這樣
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:background="@color/primaryColor"
tools:context=".ui.fragment.GetPirateFragment">
<include
android:id="@+id/pirate_card"
layout="@layout/card_view_pirate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/margin_base"
android:backgroundTint="@color/primaryLightColor"
android:text="Get Pirate"
android:textColor="@color/secondaryColor"
android:textSize="@dimen/sub_title_text_size" />
用 include
把元件加進來,就可以達到重複使用的目的
那海賊的圖片並不會跟著 Api 資料回來,因此剛剛有提到我有先抓 10 張 AOE 的角色圖當作海賊的圖片
因此這邊要做到的是,當 Api 回傳 id
和 data
時,我需要呈獻相對應的圖片給使用者,因此這邊可以採用 Enum Class
達到這個目的
就把圖片加到 res/drawable
中,記得先 Rebuild Project
不然可能會找不到圖
到 data
下建立一個 enum
的資料夾,之後這個資料夾都會放 enum class
,那我們先建立一個 PirateImage.kt
的 enum class
那我們接下來就是要把回傳的 pirateId
映設到對應的 Image
上,這邊做個分段的講解
PirateImage
有兩個屬性是 pirateId
和 pirateImage
pirateId
對應到 IDpirateImage
對應到 drawable 位置static function
叫作 getPirateImage()
,當有人呼叫他和傳 pirateId
進來時,就會用 when-else
映射到相對應的 enum
並且回傳enum class PirateImage(val pirateId: Int, val pirateImage: Int) {
BULBASAUR(1, R.drawable.aoe_imperialskirmisher),
IVYSAUR(2, R.drawable.aoe_petard),
VENUSAUR(3, R.drawable.aoe_battle_elephant),
CHARMANDER(4, R.drawable.aoe_arbalester),
CHARMELEON(5, R.drawable.aoe_legionary),
CHARIZARD(6, R.drawable.aoe_bombard_cannon),
TORRENT(7, R.drawable.aoe_tarkan),
WARTORTLE(8, R.drawable.aoe_camelrider),
BLASTOISE(9, R.drawable.aoe_hand_cannoneer),
CATERPIE(10, R.drawable.aoe_monk);
companion object {
fun getPirateImage(pirateId: Int): PirateImage {
return when (pirateId) {
1 -> BULBASAUR
2 -> IVYSAUR
3 -> VENUSAUR
4 -> CHARMANDER
5 -> CHARMELEON
6 -> CHARIZARD
7 -> TORRENT
8 -> WARTORTLE
9 -> BLASTOISE
10 -> CATERPIE
else -> BULBASAUR
}
}
}
}
透過這樣的方式我們就完成了 1 to 1 mapping
了,接下來就只要在 pirateListLiveData()
時,更新 CardView 內容
那昨天有證明 Observer Pattern
會在 被觀察者 資料更新的時候打 Event
通知 觀察者,因此這邊我們就要去更新 pirateInfoLiveData()
裡面的邏輯,讓他可以達到我們希望的行為
首先是打資料回來的時候我們要改變 CardView 的 UI 內容,那可以注意到
pirateId
的時候,會先去剛剛完成的 enum class 中將圖片的 drawable id
取回來 PirateImage.getPirateImage(pirate.id).pirateImage
,在透過 ContextCompat.getDrawable()
取得圖片內容,最後在 set
到 CardView
的 Image
中class GetPirateFragment : Fragment() {
...
private fun setGetPirateViewModel() {
getPirateViewModel.pirateInfoLiveData((1 .. 10).random()).observe(viewLifecycleOwner, Observer { pirate ->
val pirateCard = pirate_card
pirateCard.image.setImageDrawable(context?.let { ContextCompat.getDrawable(it, PirateImage.getPirateImage(pirate.id).pirateImage) })
pirateCard.pirate_name.text = pirate.name
pirateCard.pirate_height.text = pirate.getHeightString()
pirateCard.pirate_weight.text = pirate.getWeightString()
pirateCard.pirate_attack.text = pirate.getAttackString()
})
}
...
}
那上個步驟完成後接下來就很單純了,就只要幫按鈕設定 setOnClickListener
事件,當使用者按下去的時候呼叫 setGetPirateViewModel()
,接下來就交給 setGetPirateViewModel()
去更新 UI 即可
class GetPirateFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
button.setOnClickListener {
setGetPirateViewModel()
}
}
...
}
如果內容有任何問題或錯誤,歡迎提出與指教