現代化 Android — Jetpack Compose Part 3 — 導入現有 UI
你可能和我一樣,迫不及待的想要把 Compose 應用在自己的專案 or 公司產品上,但是又不知道怎麼開始下手?
這篇要介紹的是我們如何把既有的專案「無痛」導入 compose 。
一開始導入的方式大致可以分三種:
- 新頁面用 compose 實作
- 舊頁面重構成 compose
- 從單獨 custom view 開始使用 compose
我會推薦從「單獨 custom view 開始使用 compose」開始,原因是 compose 可以和既有的 custom view 、 xml 無痛混合使用!
既不會影響到原有的 view 也不需要一次到位,可以循序漸進的導入 compose,聽起來是不是很不錯?那就直接動手開始吧~
(p.s. 建議要先把 compose 基本觀念弄清楚哦!)
開始改改看
假設今天我今天有一個 custom view 叫 ProductCardView
class ProductCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attrs, defStyle) {
fun setData(btnText:String, onClickListener: () -> Unit) {
this.text = btnText
this.setOnClickListener(onClickListener)
}
}
這個 View 是一個簡單的 商品卡片 view
,裡面有一個 setData()
的方法可以設置按鈕文字和 onClickListener。
接下來我要把它改為 compose~
Step 1 : 先用 compose 刻出這個 ProductCardView
我們需要先依照這個 ProductCardView 原本在 xml 是什麼模樣,試著用 compose 做出一個一樣的 UI。(同先前文章刻出商品卡 UI,這邊不多解釋)
@Composable
fun GridProductCard(
name: String = "Star War IIIStar War IIIStar War IIIStar War IIIStar War IIIStar War III",
click: () -> Unit
) {
ComposeDemoActivityTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.clickable {
click.invoke()
}
) {
Box {
Image(
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop,
painter = painterResource(id = drawable.ic_launcher_background),
contentDescription = ""
)
Image(
modifier = Modifier
.align(Alignment.TopStart)
.padding(all = 10.dp),
painter = painterResource(id = drawable.baseline_check_circle_outline_red_300_24dp),
contentDescription = ""
)
Image(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(all = 10.dp),
painter = painterResource(id = drawable.baseline_thumb_up_cyan_300_24dp),
contentDescription = ""
)
}
Text(
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.height(40.dp),
fontSize = 14.sp,
text = name,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(modifier = Modifier.padding(0.dp, 4.dp)) {
Text(text = "Shop Name", fontSize = 14.sp)
Text(text = "・", fontSize = 14.sp)
Text(text = "AD", fontSize = 14.sp)
}
Row() {
Text(text = "NT$1,250", fontSize = 14.sp)
Text(
fontSize = 14.sp,
text = "NT$5,000",
style = TextStyle(textDecoration = TextDecoration.LineThrough),
color = Color.Gray,
modifier = Modifier.padding(10.dp, 0.dp)
)
}
}
}
}
}
Step 2 : 把原有的 custom view 改成 compose 的樣子
- 首先我們要把原有繼承
View
改成繼承AbstractComposeView
- 接下來依照先前學過的觀念,讓 compose 是 Stateful 的,我們需要把
text
和onClickListener
改為mutableStateOf
並且傳進去給寫好的Composable
內
class ProductCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var text by mutableStateOf<String>("")
var onClick by mutableStateOf<() -> Unit>({}) fun setData(btnText:String, onClickListener:() -> Unit) {
this.text = btnText
this.onClick = onClickListener
} @Composable
override fun Content() {
//TODO
}
}
Step 3 : 把 Step 1 的東西放進 Step 2 的 Content() 內
把上面兩步驟合在一起吧!
class ProductCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var text by mutableStateOf<String>("")
var onClick by mutableStateOf<() -> Unit>({}) fun setData(btnText:String, onClickListener:() -> Unit) {
this.text = btnText
this.onClick = onClickListener
} @Composable
override fun Content() {
//放在這
GridProductCard(text, onClick)
}
}
Step 4 : 測試是否正常
沒錯,做完上面的話就已經把既有的 view 改為 compose 了 🎊,我們可以回到使用 view 的地方看看是不是可以正常執行,一般來說是可以無縫接軌不需要改外面調用的地方的,例如:
class ExampleActivity : Activity() {
private lateinit var binding: ActivityExampleBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityExampleBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.productCardView.setData(
btnText = getString(R.string.something)
onClickListener = { /* Do something */ }
)
}
}
額外補充
如果我的 custom view 裡面有用到別的 custom view,但是我不想要現在全部改為 compose 怎麼辦?別擔心, compose 為了讓你無痛接軌(?) 所以也有提供 AndroidView
這個 Composable
,用途是你可以 直接引用既有的 view 到 compose 內 !
拿上面的範例來說,如果我的 GridProductCard
就是想要在裡面放 ImageView
,那麼你可以這樣做:
@Composable
fun GridProductCard(
text: String,
onClick: () -> Unit
) {
AndroidView(
factory = { context ->
ImageView(context)
},
update = {
it.setImageResource(R.drawable.your_image)
}
)
}
AndroidView 內主要有兩個重點參數:
factory
:用以告訴 compose 你要放入什麼 viewupdate
:你要對這個 view 做什麼操作
完整範例可參考 GitHub Repo:
看起來很簡單吧,當然上面這是一個簡單的例子,實際上如果有實踐一些 Design Pattern 例如 MVP、MVVM 的話,我們常常會需要與 ViewModel 或 Presenter 互動協作,又或是有用到 DI、Paging 等外部 Lib 時該怎麼做呢?
建議可以參考官方說明 sample 如下:
https://developer.android.com/jetpack/compose/libraries
結語
使用 Compose 雖然一開始可能有一些學習成本,但是上手後會發現用 Compose 刻 UI 是非常的簡潔,沒有過多繼承、複雜的 xml 結構需要了解,且 Compose 和 Flutter 和 Swift UI 非常的像(我聽說的),等於學一套就可以會很多種~應該是好處多多,建議大家若還在觀望的話可以開始試試看!
Ref:
https://developer.android.com/jetpack/compose/interop/compose-in-existing-arch