1、简介
1.1、学习内容
您将学习:
- 你能够遵从的不同搬迁路径
- 怎么逐渐将运用搬迁到Compose
- 怎么将Compose增加到运用View构建的现有界面
- 怎么在Compose中运用View
- 怎么在Compose中运用根据View的主题
- 怎么测验运用View和Compose编写的混合界面
1.2、前提条件
- 有运用Kotlin语法(包括lambda)的经验
- 了解Jetpack Compose(第二趴)——Compose 基础知识(上) 、Jetpack Compose(第二趴)——Compose 基础知识(下)。
所需条件
- 最新版Android Studio
2、搬迁简介
JetPack Compose从设计之初就考虑到了View互操性能。如需搬迁到Compose,咱们建议您履行增量搬迁(Compose和View在代码库中共存),直到运用彻底搬迁至Compose停止。
推荐的搬迁战略如下:
- 运用Compose构建新功用
- 在构建功用时,确认可重复运用的元素,并开端创立常见界面组件库。
- 以此替换一个界面的现有功用。
2.1、运用Compose构建新功用
运用它Compose构建新功用是进步Compose选用率的最佳办法。这样,您增加的新功用就能够利用Compose的优势了。
一项新功用或许包括整个界面,在这种状况下,整个界面都是Compose中。假如您运用的是根据fragment的导航,这意味着您需求创立新的fragment,并在Compose中增加其内容。
另一方面,假如您构建的新功用是现有界面的一部分,则View和Compose将共存在同一个界面上。例如,假定您要增加的功用是RecyclerView中的一种新的视图类型。在这种状况下,新的视图类型将坐落Compose中,而其他项目保持不变。
2.2、构建常见界面组件库
运用Compose构建功用时,您很快就会意识到,您最终会构建组件库。您需求确认可重复运用的组件,促使在运用中重复运用这些组件,以便共享组件具有单一可信来历。您构建的功用随后能够依赖于这个库。
2.3、运用Compose替换现有功用
除了构建新功用之外,您还需求逐渐将运用中的现有功用搬迁到Compose。详细选用哪种办法由您决定,下面是一些合适的办法:
- 接单界面 – 包括少量界面元素和动态元素(例如欢迎界面、确认界面或设置界面)的简略界面。这些界面非常合适搬迁到Compose,因为只需几行代码就能搞定。
- 混合View和Compose界面 – 已包括少量Compose代码的界面是另一个不错的挑选,因为您能够继续逐渐搬迁该界面中的元素。假如您的某各界面在Compose中只需一个子树,您能够继续搬迁该树的其他部分,知道整个界面坐落Compose中。这称为自下而上的搬迁办法。
3、准备工作
原始展现UI:
4、Sunflower中的Compose
咱们以一个例子来解说
首要我沃恩翻开运用级build.gradle
文件后,查看该文件怎么导入Compose依赖项,以及怎么导入Compose依赖项,以及怎么运用buildFeatures { compose true }
标志让Android Studio能够运转Compose。
app/build.gradle
android {
// ...
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
// ...
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
}
dependencies {
// ...
// Compose
def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
implementation(composeBom)
androidTestImplementation(composeBom)
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.material:material"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
implementation "com.google.accompanist:accompanist-themeadapter-material:0.28.0"
// ...
}
这些依赖项的版别在项目级build.gradle
文件中界说。
5、欢迎运用Compose
在植物概况界面中,咱们需求将对植物的说明书搬迁到Compose,一起让界面的整体结构保持完好。
Compose需求有宿主activity或fragment才干出现界面。在Sunflower中,一切界面都运用fragment,因而你需求运用ComposeView
:这是Android View能够运用其setContent办法办法保管setContent
办法保管Compose界面内容。
5.1、移除XML代码
咱们先从搬迁开端!翻开fragment_plant_detail.xml
并履行以下操作:
1. 切换到代码视图
2. 移除NestedScrollView
中的ConstraintLayout
代码和嵌套的4个TextView
3. 增加一个ComposeView
,它会改为保管Compose代码,并以compose_view
作为视图ID
fragment_plant_detail.xml
<androidx.core.widget.NextedScrollView
android:id="@+id/plant_detail_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_bottom_padding"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<!-- Step 2) Comment out ConstraintLayout and its children -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
<TextView
android:id="@+id/plant_detail_name"
...
</androidx.comstraintLayout.widget.ConstraintLayout>
<!-- End Step 2) Comment out until here -->
<!-- Step 3) Add a ComposeView to host Compose code -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.core.widget.NestedScrollView>
5.2、增加Compose代码
现在,您能够开端将植物概况页界面搬迁到Compose了!
在整个Codelab中,您都需求将Compose代码增加到plantdetail
文件夹下的PlantDetailDescription.kt
文件夹中。翻开该文件,看看项目中是否有占位符Hello Compose
文本。
PlantDetailDescription.kt
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
咱们从在上一步中增加ComposeView
中调用此可组合项,即可在界面上显现此内容。翻开PlantDetailFragment.kt
。
界面运用的是数据绑定,因而您能够直接拜访composeView
并调用setContent
,以便在界面上显现Compose代码。您需求在MaterialTheme
哪调用PlantDetailDescription
可组合项,因为Sunflower
运用的是Material Design。
PlantDetailFragment.kt
class PlantDetailFragment: Fragment() {
// ...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(inflate, R.layout.fragment_plant_detail, container, false
).apply {
// ...
composeView.setContent {
// You're in Compose world!
MaterialTheme {
PlantDetailDescription()
}
}
}
// ...
}
}
6、运用XML创立可组合项
咱们首要搬迁植物的称号。更切当地说,就是您在fragment_plant_detail.xml
中移除的ID为@+id/plant_detail_name
的TextView
。XML代码如下:
<TextView
android:id="@+id/plant_detail_name"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
android:text="@{viewModel.plant.name}"
android:textAppearance="?attr/textAppearanceHeadline5"
.../>
请查看他是否为textAppearanceHandline5
款式,水平外边距为8.dp
,以及是否在界面上水平居中。不过,要显现的标题是从由代码库层PlantDetailViewModel
公开的LiveData
中观察到的。
怎么观察LivData
将稍后介绍,因而先假定咱们有可用的称号,并以参数形式将其传递到咱们在PlantDetailDescription.kt
文件中创立的新PlantName
可组合项。稍后,将从PlantDetailDescription
可组合项调用此可组合项。
PlantDetailDescription.kt
@Composable
private fun PlantName(name: String) {
Text(
text = name,
style = MaterialTheme.typography.h5,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
@Preview
@Composable
private fun PlantNamePreview() {
MaterialTheme {
PlantName("Apple")
}
}
预览如下:
其中:
-
Text
的款式为MaterialTheme.typography.h5
,相似于XML代码中的textAppearanceHeadline5
。 -
润饰符会润饰Text,使其看起来像XML版别:
-
运用
fillMaxWidth
润饰符,使其占据最大可用宽度。此润饰符对应XML代码中layout_width
特点的match_parent
值。 -
运用
padding
润饰符,以便运用水平内边距距值margin_small
。这对应于XML中的marginStart
和marginEnd
声明。margin_small
值也是运用dimensionResource
辅佐函数提取的现有尺度资源。 -
wrapContentWidth
润饰符用于对齐文本,以使其水平居中。这相似于在XML中gravity
为center_horizontal
。留意:Compose提供了从dimens.xml和strings.xml文件获取值的简略办法,即由此一来,您能够将View体系视为可信来历。
7、ViewModel和LiveData
现在,咱们将标题链接到界面。如需履行此操作,您需求运用PlantDetailViewModel
加载数据。为此,Compose集成了ViewModel
和LiveData
。
7.1、ViewModel
因为在fragment中运用PlantDetailViewModel
的实例,因而咱们能够将其作为参数传递给PlantDetailDescription
,就这么简略。
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
// ...
}
现在,请在从fragment调用此可组合项时传递ViewModel实例:
PlantDetailFragment.kt
class PlantDetailFragment: Fragment() {
...
override fun onCreateView(...): View? {
...
composeView.setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
7.2、LiveData
有了LiveData,您已有权拜访PlantDetailViewModel
的LiveData<Plant>
字段,以获取植物的称号。
如需从可组合项观察LiveData,请运用LiveData.observeAsState()
函数。
留意:LiveData.observeAsState()开端观察LiveData,并以State目标表示它的值。每次向LiveData发布一个新值时,回来的State都会更新,这会导致一切State.value用例重组。
因为LiveData发出的值能够是null
,因而您需求将其用例封装在null
查看中。有鉴于此,以及为了完成可重用性,最好将LiveData的运用和监听拆分到不同的可组合项中。因而,咱们来创立一个名为PlantDetailContent
的新可组合项,用于显现Plant
信息。
完成这些更新后,PlantDetailDescription.kt
文件现在应如下所示:
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
// Observes values coming from the VM's LiveData<Plant> field
val plant by plantDetailViewModel.plant.observeAsState()
// If plant is not null, display the content
plant?.let {
PlantDetailContent(it)
}
}
@Composable
fun PlantDetailContent(plant: Plant) {
PlantName(plant.name)
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
PlantNamePreview
应反响咱们的更改,而无需直接更新,因为PlantDetailContent
仅调用PlantName
:
现在,您已连接ViewModel,使植物称号在Compose中显现。在接下来的几部分中,您将构建其余可组合项,并以相似的办法将它们连接到ViewModel。
8、更多XML代码搬迁
现在,咱们能够更轻松地将界面中缺少的内容补充完整:洒水信息和植物说明。您现已能够按照之前相似的办法搬迁界面的其余部分了。
您之前从fragment_plant_detail.xml
移除的洒水信息XML代码由两个ID位plant_watering_header
和plant_watering
的TextView组成。
<TextView
android:id="@+id/plant_watering_header"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_normal"
android:gravity="center_horizontal"
android:text="@string/watering_needs_prefix"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
.../>
<TextView
android:id="@+id/plant_watering"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
app:watingText="@{viewModel.plant.wateringInterval}"
.../>
与您之前的操作相似,请创立一个名为PlantWatering
的新可组合项并增加Text
可组合项,以在界面上显现洒水信息:
PlantDetailDescription.kt
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
Column(Modifier.fillMaxWidth()) {
// Same modifier used by both Texts
val centerWithPaddingModifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.align(Alignment.CenterHorizontally)
val normalPadding = dimensionResource(R.dimen.margin_normal)
Text(
text = stringResource(R.string.watering_needs_prefix),
color = MaterialTheme.colors.primaryVariant,
fontWeight = FontWeight.Bold,
modifier = centerWithPaddingModifier.padding(top = normalPadding)
)
val wateringIntervalText = pluraStringResource(
R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
)
Text(
text = wateringIntervalText,
modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
)
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
MaterialTheme {
PlantWatering(7)
}
}
预览图如下:
需求留意以下几点:
- 因为
Text
可组合项会共享水平内边距和对齐润饰,因而您能够将润饰符跟配给局部变量(即centerWithPaddingModifier
),以重复运用润饰符。润饰符是标准的Kotlin目标,因而能够重复运用。 - Compose的
MaterialTheme
与plant_watering_header
中运用的colorAccent
不彻底匹配。现在,咱们能够运用将在互操作性主题设置部分中加以改进的MaterialTheme.colors.primaryVariant
。 - 在Compose1.2.1中,有必要挑选启用
ExperimentalComposeUiApi
才干运用pluralStringResource
。在将来的Compose版别中,或许不再需求这样做。
咱们将各个部分组合在一起,然后相同从PlantDetailContent
调用PlantWatering
。咱们一开端移除的ConstraintLayout XML
代码的外边距16.dp
,咱们需求将该值增加到Compose代码中。
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
请在PlantDetailContent
中创立一个Column
以一起显现称号和洒水信息,并将其作为内边距。别的,为了保证布景色彩和所用的文本色彩均合适,请增加Surface
用于处理这种设置。
PlantDetailDescription.kt
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.waterInterval)
}
}
}
改写预览后,你会看到以下内容:
9、Compose代码中的View
现在,咱们来搬迁植物说明。fragment_plant_detail.xml
中的代码具有包括app:renderHtml="@{viewModel.plant.description}"
的TextView
,用于奉告XML在界面上显现哪些文本。renderHtml
是一个绑定适配器,可在PlantDetailBindingAdapter.kt
文件中找到。该完成运用HtmlCompat.fromHtml
在TextView
上设置文本!
但是,Compose目前不支持Spanned
类,也不支持显现HTML格局的文本。因而,咱们需求在Compose代码中运用View体系中的TextView
来绕过此限制。
因为,Compose目前还无法出现HTML代码,因而你需求运用AndroidView API
程序化地创立一个TextView
,然后完成此目的。
AndroidView
使您能够在Vire的factory
lambda中构建该View
。它还提供了一个update
lambda,它会在View
膨胀和后续重组时被调用。
为此,请创立新的PlantDescription
可组合项。可组合项将调用AndroidView
,后者会在factory
lambda中结构TextView
。在factory
lambda中,初始化显现HTML格局文本的TextView
,然后将movementMethod
设置为LinkMovementMethod
的实例。最后,在update
lambda中的TextView
的文呢么射中为htmlDescription
。
PlantDetailDescription.kt
@Composable
private fun PlantDescription(description: String) {
// Remembers the HTML formatted description. Re-executes on a new description
val htmlDescription = remember(description) {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
}
//Displays the TextView on the screen and updates with the HTML description when inflated
// Updates to htmlDescription will make AndroidView recompose and update the text.
AndroidView(
factory = { context ->
TextView(context).apply {
movementMethod = LinkMovementMethod.getInstance()
}
},
update = {
it.text = htmlDescription
}
)
@Preview
@Composable
private fun PlantDescriptionPreview() {
MaterialTheme {
PlantDescription("HTML<br><br>description")
}
}
预览:
请留意,htmlDescription
会记住作为参数传递的指定description
的HTML说明。假如description
参数产生变化,体系会再次履行remember
中的htmlDescription
代码。
因而,假如htmlDescription
产生变化,AndroidView
更新会回调将重组。在update
lambda中读取的任何状况都会导致重组。
咱们将PlantDescription
增加到PlantDetailContent
可组合项,并更改预览代码,以便相同显现HTML说明:
PlantDetailDescription.kt
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
PlantDescription(plant.description)
}
}
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
预览如下:
现在,您现已将原始ConstraintLayout
中的一切内容搬迁到Compose。您能够运转运用,查看其是否按预期运转。
10、ViewCompositionStrategy
只需ComposeView
与窗口别离,Compose就会处理组合。假如Fragment中运用了ComposeView
,这种状况是不可取的,原因有两个:
- 组合有必要遵从fragment的视图生命周期,Compose界面
View
类型才干保存状况。 - 产生过渡时,底层
ComposeView
将处于别离状况。不过,在这些过渡期间,Compose界面元素仍然可见。
假如修改此行为,请运用适当的ViewCompositionStrategy
调用setViewCompositionStrategy
,使其改为遵从fragment的视图生命周期。详细而言,您需求在fragment的LifecycleOwner
被销毁时运用DisponseOnViewTreeLifecycleDestroyed
战略处置组合、
因为PlantDetailFragment
包括进入和退出过渡,并且咱们稍后会在Compose中运用View
类型,因而咱们需求保证ComposeView
运用DisposeOnViewTreeLifecycleDestroyed
战略。不过,在fragment中运用ComposeView
时,最好一直设置此战略。
PlantDetailFragment.kt
import androidx.compose.ui.platform.ViewCompositionStrategy
...
class PlantDetailFragment: Fragment() {
...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(inflate, R.layout.fragment_plant_detail, container, false
).apply {
...
composeView.apply {
// Dispose the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
}
}
11、互操作性主题设置
咱们已将朱武概况的文本内容搬迁到Compose。不过,您或许现已留意到,Compose运用的主题色彩有误。当植物称号应该运用绿色时,它运用的是紫色。
在这个搬迁的早期阶段,您或许需求Compose承继View体系中可用的主题,而不是从头开端在Compose中重新编写您自己的Material主题。Material主题可与Compose附带的一切Material Design组件完美配合运用。
如需在Compose中重复运用View体系的Material Design Components(MDC)主题,您能够运用Accompanist Material Theme Adapter库。MdcTheme
函数将主动读取逐级上下文的MDC主题,并代表您将它们传递给MateriamTheme
,以用于淡色和深色主题。即使您只需求适用于此Codelab的主题色彩,该库也会读取Vide体系的形状和排版。
该库已包括在app/build.gradle
文件中,如下所示:
...
dependencies {
...
implementation "com.google.accompanist:accompanist-themeadapter-material:$rootProject.accompanistVersion"
}
如需运用词库,请不要运用MaterialTheme
,改为运用MdcTheme
。例如,在PlantDetailFragment
中
PlantDetailFragment.kt
class PlantDetailFragment: Fragment() {
...
composeView.apply {
...
setContent {
MdcTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
此外还有PlantDetailDescription.kt
文件中的一切预览可组合项:
PlantDetailDescription.kt
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
MdcTheme {
PlantDetailContent(plant)
}
}
@Preview
@Composable
private fun PlantNamePreview() {
MdcTheme {
PlantName("Apple")
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
MdcTheme {
PlantWatering(7)
}
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
MdcTheem {
PlantDescroption("HTML<br><br>description")
}
}
在预览中您能够看到,MdcTheme
会从style.xml
文件中的主题中提取色彩。
您卡能够在深色主题中预览界面,办法是创立新函数并将Configuration.UI_MODE_NIGHT_YES
传递给预览的uiMode
:
import android.content.res.Configuration
...
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
MdcTheme {
PlantDetailContent(plant)
}
}
预览如下:
假如您运转运用,它在淡色主题和深色主题下的行为都将与搬迁前彻底相同:
12、测验
将植物概况界面的各个部分搬迁到Compose之后,务必要进行测验,保证您没有损坏任何内容。
留意:在真实运用中,假如没有测验,则不应该重写旧代码。将代码搬迁到Compose时,您还应该重构测验并保证测验结果合格。
在Sunflower中,坐落androidTest
文件夹的PlantDetailFragmentTest
用于测验运用的某些功用。ging翻开该文件并查看当时的代码:
-
testPlantName
用于查看界面上的植物称号。 -
testShareTextIntent
用于查看点按共享按钮后是否触发了正确的intent
当activity或fragment运用Compose时,您不需求运用ActivityScenarioRule
,而需求运用createAndroidComposeRule
,它将ActivityScenarioRule
与ComposeTestRule
集成,让您能够测验Compose代码。
在PlantDetailFragmentTest
中,将用法ActivityScenarioRule
替换为createAndroidComposeRule
。假如需求运用activity规矩来装备测验,请运用createAndroidComposeRule
中的activityRule
特点,详细代码如下所示:
@RunWith(AndroidJunit4::class)
class PlantDetailFragmentTest {
@Rule
@JvmField
val composeTestRule = createAndroidComposeRule<GardenActivity>()
...
@Before
fun jumpToPlantDetailFragment() {
populateDatabase()
composeTestRule.activityRule.scenario.onActrivity { gardenActivity ->
activity = gardenActivity
val bundle = Bundle().apply {
putString("plantId", "malus-pumila")
}
findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
}
}
}
假如您运转测验,testPlantName
会失利!testPlantName
查看界面上是否存在TextView。不过,您已将这部分的界面搬迁Compose。因而,您需求改用Compose断语:
@Test
fun testPlantName() {
composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}
假如运转测验,您会看到一切测验均会经过。
13、恭喜
恭喜,您已成功完成此 Codelab!
原始 Sunflower GitHub 项目的compose
分支会将植物详细信息界面彻底搬迁到 Compose。除了您在此 Codelab 中完成的操作之外,该分支还会模拟 CollapsingToolbarLayout 的行为。这些行为包括:
- 运用 Compose 加载图片
- 动画
- 更超卓的尺度处理
- 等等!
翻译原文:Compose 基础知识