如何是Jetpack Compose构建漂亮的应用程序
Jetpack compose 是在 Android 上构建 UI 的未来。
如果您完全不熟悉 android 并且不知道 Jetpack Compose 是什么——它基本上是一种构建本机用户界面的新方法。
Jetpack compose官方站点
https://developer.android.com/jetpack/compose
在本文中,您将了解如何使用 Jetpack Compose 遵循最佳实践进行 UI 开发。
我们正在建设什么
我们将构建一个显示 Apple Music 专辑列表的应用程序。让我们调用应用程序MyMusic。
从我的github 存储库中,您将学习如何:
https://github.com/ibrajix/MyMusic
- 使用推荐的方法(启动 API)构建启动画面
https://developer.android.com/guide/topics/ui/splash-screen
- 使用各种 UI 可组合项,例如行、列、惰性列、动画 API
- 将 MVVM 模式与 Jetpack Compose 结合使用(使用可观察对象和状态持有者,如 StateFlow)
- 使用Room 数据库从 JSON 文件保存本地数据
- 在应用程序上实现搜索功能
- 使用这个很棒的库实现带有过渡动画的简单导航
https://github.com/raamcosta/compose-destinations
- 如何使用这个很棒的库有效地显示 gif 等图像
https://github.com/skydoves/Landscapist
为了降低复杂性,本文重点介绍 UI。
SpashScreen
启动画面是在您的应用程序内容加载之前显示的内容。这是向用户展示您的品牌形象或徽标的一种方式。
可悲的是,没有办法专门用 compose 来实现当前的 splash API。我们仍然需要一些 xml 代码
- 在
values/themes.xml
下。添加启动画面主题
代码中注释了每个属性的作用
<!--Parent Theme-->
<style name="Theme.MyMusic" parent="android:Theme.Material.Light.NoActionBar">
......
</style>
<!--Splash Screen Theme-->
<style name="Theme.MyMusic.SplashScreen" parent="Theme.SplashScreen">
<!--splash screen background-->
<item name="windowSplashScreenBackground">@color/splash_screen_background_color</item>
<!--splash screen drawable-->
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<!--post splash screen - displayed after splash screen-->
<item name="postSplashScreenTheme">@style/Theme.MyMusic</item>
</style>
- 确保将主题包含在AndroidManifest.xml root 标记中
android:theme="@style/Theme.MyMusic.SplashScreen"
在您的 Launcher Activity 中安装主题,它应该是您的MainActivity.kt
class MainActivity : ComponentActivity()
.........
super.onCreate(savedInstanceState)
......
installSplashScreen()
- 运行该应用程序,您的初始屏幕应该可以正常工作。
StartSceen
创建一个新的composable,命名为StartScreen(代码路径 ui/screens/start/StartScreen.kt
)
@RootNavGraph(start = true)
@Destination(style = StartScreenTransitionAnimation::class)
@Composable
fun StartScreen(
modifier: Modifier = Modifier,
navigator: DestinationsNavigator
) {
..//we'll build the layout above here
}
@RootNavGraph
- 这是我们导航库中的一个注释,表示这是我们导航图的起始屏幕。
@Destination
- 也是来自我们导航库的注释,表示这个组合式是一个可以让用户往返导航的目的地。我们还包括了一个过渡动画的样式属性(请查看animations/StartScreenTransition.kt
)。
@Modifier
- 是传递给组合式的参数,用于装饰组合式(例如,大小、背景等)。
@Navigator
- 帮助我们从一个目的地或屏幕导航到另一个。
- 正如您从上面的图2中可以看到的那样,卡片被放置在肯伊·威斯特(Kanye West)的图像上方。
- 因此,我们将使用一个Box。
- Box是一个UI可组合,可以让您将项目放置在其他项目之上。
请在上面的{…}之间放置以下可组合:
Box(
modifier = modifier
.fillMaxSize()
){
.......
}
- modifier 说明该 Box 应填充整个屏幕的大小。
- 现在,在 Box 内部,我们放置我们的卡尼·韦斯特图片,这是一个 GIF(使用我们的图像加载库)。
Box(
modifier = modifier
.fillMaxSize()
){
GlideImage(
imageModel = R.drawable.kanye
)
........
}
- 接下来,我们需要创建一个放置在 Kanye 图片上方的卡片。我们可以使用 Card 组合来实现。
Box(
modifier = modifier
.fillMaxSize()
){
GlideImage(
imageModel = R.drawable.kanye
)
Card(modifier = modifier
.fillMaxWidth(0.8F)
.align(Alignment.BottomCenter)
.padding(bottom = 50.dp),
shape = RoundedCornerShape(50.dp),
backgroundColor = MaterialTheme.colors.secondary
) {
...........
}
}
- 我们指定卡片应该在屏幕底部居中,宽度恰好占据屏幕的 80%,并有 50dp 的 padding。
- 我们还指定了卡片应该有圆角,半径为 50dp,并具有辅助背景色。
- 从上图2中可以看出,红色箭头表示物品是从上到下(纵向)排列的。
- Column是一个UI组件,可将其子项垂直排列在一起。
Box(
modifier = modifier
.fillMaxSize()
){
GlideImage(
imageModel = R.drawable.kanye
)
Card(modifier = modifier
.fillMaxWidth(0.8F)
.align(Alignment.BottomCenter)
.padding(bottom = 50.dp),
shape = RoundedCornerShape(50.dp),
backgroundColor = MaterialTheme.colors.secondary
) {
Column(
modifier = modifier
.padding(bottom = 20.dp),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
...........
}
}
}
- 这些修饰符和属性都是很容易理解的。
- 我们确保在此列中放置的所有项目从上到下(垂直)均匀分布。
- 同时,确保项目从左到右(水平)居中。
- 现在,我们将在列中添加其他 UI 组合件——文本和按钮。
开始界面的完整代码如下:
//StartScreen.kt
@RootNavGraph(start = true)
@Destination(style = StartScreenTransitionAnimation::class)
@Composable
fun StartScreen(
modifier: Modifier = Modifier,
navigator: DestinationsNavigator
) {
Box(
modifier = modifier
.fillMaxSize()
){
GlideImage(
imageModel = R.drawable.kanye
)
Card(modifier = modifier
.fillMaxWidth(0.8F)
.align(Alignment.BottomCenter)
.padding(bottom = 50.dp),
shape = RoundedCornerShape(50.dp),
backgroundColor = MaterialTheme.colors.secondary
) {
Column(
modifier = modifier
.padding(bottom = 20.dp),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = modifier
.padding(30.dp),
text = stringResource(id = R.string.explore_your_world_of_music),
style = MaterialTheme.typography.h1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSecondary
)
Text(
modifier = modifier
.padding(horizontal = 30.dp),
text = stringResource(id = R.string.see_trending_songs_from_favs),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
fontSize = 14.sp,
color = MaterialTheme.colors.onSecondary
)
Button(
modifier = modifier
.fillMaxWidth(0.6f)
.height(80.dp)
.padding(top = 8.dp)
.align(Alignment.CenterHorizontally)
.padding(8.dp),
shape = RoundedCornerShape(50.dp),
onClick = {
navigator.popBackStack()
navigator.navigate(HomeScreenDestination)
}
) {
Text(
text = stringResource(id =R.string.get_started),
style = MaterialTheme.typography.h3
)
}
}
}
}
}
- 请注意,我们在按钮点击时使用
onClick
lambda 参数导航到HomeScreenDestination
。 - 请确保您已经创建了
HomeScreen
composable,请检查ui/screens/home/HomeScreen
。
它应该带有@Destination
注解。在成功构建后,这将自动为您创建HomeScreenDestination
文件。
Home Screen
- 您必须已经创建了HomeScreen可组合项(检查
ui/screens/home/HomeScreen.kt
) - 主屏幕基本上由从上到下排列的项目组成——垂直(如上面的黑色箭头所示)
@Destination(style = StartScreenTransitionAnimation::class)
@Composable
fun HomeScreen(
navigator: DestinationsNavigator,
albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel()
){
//--> Home screen layout here
}
个人而言,我喜欢干净、可重复使用的代码。所以,我不会将整个主屏幕项目放在此组合中。我们将创建另一个名为 HomeScreenItems.kt 的组合,并将其放在我们的 HomeScreen 中。
//HomeScreen.kt
@Destination(style = StartScreenTransitionAnimation::class)
@Composable
fun HomeScreen(
navigator: DestinationsNavigator,
albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel()
){
.............// Check full code for other variables
HomeScreenItems(navigator = navigator, albums = albums.value,
onCardClicked = {
shouldOpenAlbumDetails = true
albumUrl = it
},
onPopularAlbumClicked = {
shouldOpenTrendingAlbums = true
}
)
}
这是我们的HomeScreenItems.kt
文件
//HomeScreenItems.kt
@Composable
fun HomeScreenItems(
modifier: Modifier = Modifier,
navigator: DestinationsNavigator,
albums: List<Album>,
albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel(),
onCardClicked: (String) -> Unit,
onPopularAlbumClicked: () -> Unit
) {
........
}
-
该可组合接受许多参数。
我将强调一下之前没有解释过的参数: -
albums
- 来自Room数据库实体/表的专辑列表 -
albumDatabaseViewModel
- 数据源的viewModel -
onCardClicked
- 当卡片被点击时调用的Lambda函数 -
onPopularAlbumClicked
- 当点击热门专辑时调用的Lambda函数 -
由于我们希望我们的主屏幕可以滚动,并且随着我们滚动逐渐加载项目,因此我们将使用LazyColumn作为根布局。
-
LazyColumn是一个垂直滚动的可组合列表,仅组合和布局当前可见的项目。
//HomeScreenItems.kt
@Composable
fun HomeScreenItems(
modifier: Modifier = Modifier,
navigator: DestinationsNavigator,
albums: List<Album>,
albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel(),
onCardClicked: (String) -> Unit,
onPopularAlbumClicked: () -> Unit
) {
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colors.bgHome)
.padding(20.dp)
){
.........
}
}
- 因此,我们想在 LazyColumn 中放置什么?非动态内容(单个静态项)和动态内容(变化的项)。
- 从图3中,您会发现蓝色框和绿色框中的项目是非动态内容。它们不会改变。
- 因此,我们将使用单个项目 lambda 函数来显示这些项。
- 每个部分都有一个用于显示
UserHomeSection()
、SearchSection()
和PopularAlbumSection()
的组合体。
//HomeScreenItems.kt
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colors.bgHome)
.padding(20.dp)
){
/**
* Non-Dynamic Items
*/
item {
//first section
UserHomeSection()
//search home screen
SearchSection(
searchTextFieldValue = "",
onSearchTextFieldValueChange = { },
onSearchTextFieldClicked = { navigator.navigate(SearchScreenDestination) },
searchFieldPlaceHolder = R.string.search_albums,
searchEnabled = false,
showKeyboardOnStart = false
)
//popular item section
PopularAlbumSection(
cardTextTitle = R.string.popular,
cardTextItem = R.string.top_trending_albums,
cardImage = R.drawable.ic_character,
onPopularAlbumCardClicked = {
//popular album clicked, go to apple music
onPopularAlbumClicked()
}
)
}
item{
Text(
modifier = modifier
.fillMaxWidth()
.padding(top = 12.dp),
style = MaterialTheme.typography.h2,
fontSize = 18.sp,
color = MaterialTheme.colors.onSecondary,
text = stringResource(id = R.string.all_albums)
)
}
.................
}
}
- 这样做是为了分离关注点,并且主要是为了可重用性(例如,我可以在应用程序的其他位置使用SearchSection可组合部件,而无需复制SearchSection可组合部件中的整个代码)
- 实际上,我在SearchScreen中使用了相同的可组合部件。
- 请注意,在图3中,第一个框标记为蓝色。这只是表示项目从左到右(水平)放置。因此,我们需要使用名为Row的可组合部件。
- Row用于在屏幕上水平放置项目。
- 现在,对于我们从Room数据库获取的动态项目,我们将使用称为items的lambda函数来显示它们。
我们完整的HomeScreenItems.kt
如下:
//HomeScreenItems.kt
@Composable
fun HomeScreenItems(
modifier: Modifier = Modifier,
navigator: DestinationsNavigator,
albums: List<Album>,
albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel(),
onCardClicked: (String) -> Unit,
onPopularAlbumClicked: () -> Unit
) {
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colors.bgHome)
.padding(20.dp)
){
/**
* Non-Dynamic Items
*/
item {
//first section
UserHomeSection()
//search home screen
SearchSection(
searchTextFieldValue = "",
onSearchTextFieldValueChange = { },
onSearchTextFieldClicked = { navigator.navigate(SearchScreenDestination) },
searchFieldPlaceHolder = R.string.search_albums,
searchEnabled = false,
showKeyboardOnStart = false
)
//popular item section
PopularAlbumSection(
cardTextTitle = R.string.popular,
cardTextItem = R.string.top_trending_albums,
cardImage = R.drawable.ic_character,
onPopularAlbumCardClicked = {
//popular album clicked, go to apple music
onPopularAlbumClicked()
}
)
}
/**
* Dynamic Items
*/
item{
Text(
modifier = modifier
.fillMaxWidth()
.padding(top = 12.dp),
style = MaterialTheme.typography.h2,
fontSize = 18.sp,
color = MaterialTheme.colors.onSecondary,
text = stringResource(id = R.string.all_albums)
)
}
items(items = albums){ album->
AlbumCard(
album = album,
onClickCard = { albumUrl->
//card clicked, go to details screen
onCardClicked(albumUrl)
},
onClickLike = { isLiked, albumId->
albumDatabaseViewModel.doUpdateAlbumLikedStatus(!isLiked, albumId)
}
)
}
}
}
我创建了一个名为AlbumCard的可重复使用的可组合项,我们可以将其用作显示所有动态项目的模型。
确保检查完整代码以正确理解其工作原理。
我希望我已经能够解释基本 UI 如何与 Jetpack Compose 一起工作。有关 UI 的更多信息,请查看官方文档
github代码网址
https://github.com/ibrajix/MyMusic
参考
https://ibrajix.medium.com/how-i-built-this-nice-looking-app-using-jetpack-compose-3974db7eb9e