現代化 Android — Jetpack Compose Part 3 — 導入現有 UI

Roy Huang
Pinkoi Engineering
Published in
11 min readOct 28, 2021
Photo by Markus Winkler on Unsplash

你可能和我一樣,迫不及待的想要把 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 的,我們需要把 textonClickListener 改為 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 你要放入什麼 view
  • update :你要對這個 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

工商時間

Pinkoi 強力徵才中,如果你對做好一個產品充滿熱情,對技術成長充滿渴望,Pinkoi APP 團隊是一個能讓你充分發揮的舞台,期待你的加入💪

了解更多 Pinkoi APP Team:

--

--