如何是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
                    )
                    
                }
            }
        }
    }
}
- 请注意,我们在按钮点击时使用 onClicklambda 参数导航到HomeScreenDestination。
- 请确保您已经创建了 HomeScreencomposable,请检查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



















