How to Build Beautiful Apps with Jetpack Compose

How to Build Beautiful Apps with Jetpack Compose

beatful ui

Jetpack compose is the future of building UIs on Android.

If you're completely new to android and don't know what Jetpack Compose is - it's basically a new way of building native UIs.
Jetpack compose official site

https://developer.android.com/jetpack/compose

In this article, you'll learn how to use Jetpack Compose to follow best practices for UI development.

What we're building
We're going to build an application that displays a list of Apple Music albums. Let's call the application MyMusic.

From my github repository you will learn how to:

https://github.com/ibrajix/MyMusic

  1. Use the recommended method (Launch API) to build the splash screen

https://developer.android.com/guide/topics/ui/splash-screen

  1. Use various UI composables such as rows, columns, lazy columns, animation API
  2. Using the MVVM pattern with Jetpack Compose (using observables and state holders like StateFlow)
  3. Save local data from JSON file using Room database
  4. Implement a search function on the application
  5. Use this awesome library for simple navigation with transition animations

https://github.com/raamcosta/compose-destinations

  1. How to efficiently display images like gifs using this awesome library

https://github.com/skydoves/Landscapist

To reduce complexity, this article focuses on the UI.

SpashScreen

A splash screen is what is displayed before your application content loads. This is a way of presenting your brand image or logo to users.

splashscreen

Sadly, there is no way to implement the current splash API exclusively with compose. We still need some xml code

  • nextvalues/themes.xml . Adding a splash screen theme
    The role of each property is commented in the code
<!--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>
  • Make sure to include the theme in the AndroidManifest.xml root tag
android:theme="@style/Theme.MyMusic.SplashScreen"

Install the theme in your Launcher Activity, it should be your MainActivity.kt

class MainActivity : ComponentActivity()
  .........
super.onCreate(savedInstanceState)
  ......
installSplashScreen()
  • Run the app and your splash screen should work fine.

StartScene

figure 2

Create a new composable, named StartScreen(code path 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- This is an annotation in our navigation library indicating that this is the starting screen for our navigation graph.

@Destination- is also a comment from our navigation library, indicating that this composite is a destination that the user can navigate to and from. We've also included a style property for transition animations (check it out animations/StartScreenTransition.kt).

@Modifier- is the parameter passed to the composite to decorate the composite (eg, size, background, etc.).

@Navigator- Help us navigate from one destination or screen to another.

  • As you can see from image 2 above, the card is placed above an image of Kanye West.
  • Therefore, we will use a Box.
  • Box is a UI composable that lets you place items on top of other items.

Please place the following composables between the {...} above:

Box(
    modifier = modifier
        .fillMaxSize()
){
    
    
.......
}
  • The modifier specifies that the Box should fill the entire screen size.
  • Now, inside the Box, we place our image of Carney West, which is a GIF (using our image loading library).
Box(
    modifier = modifier
        .fillMaxSize()
){
    
    

    GlideImage(
        imageModel = R.drawable.kanye
    ) 
    ........
}
  • Next, we need to create a card that will be placed on top of Kanye's picture. We can use the Card combination to achieve this.
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
        ) {
    
    

          ...........

        }

}
  • We specify that the card should be centered at the bottom of the screen, occupy exactly 80% of the screen width, and have 50dp padding.
  • We also specified that the card should have rounded corners with a radius of 50dp and a secondary background color.
  • As can be seen from Figure 2 above, the red arrows indicate that items are arranged from top to bottom (vertically).

picturea

  • Column is a UI component that arranges its children vertically together.
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
            ) {
    
    
                ...........
            }
            
       }
}
  • These modifiers and properties are pretty self-explanatory.
  • We ensure that all items placed in this column are evenly spaced from top to bottom (vertically).
  • Also, make sure the items are centered left to right (horizontally).
  • Now, we'll add other UI components - text and buttons - to the column.
    The complete code of the start interface is as follows:
//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
                    )
                    
                }
            }
        }
    }
}
  • Note that we use onClickthe lambda parameter to navigate to on button click HomeScreenDestination.
  • Please make sure you have created HomeScreencomposable, please check ui/screens/home/HomeScreen.
    It should be @Destinationannotated . This will automatically create HomeScreenDestinationthe file .

Home Screen

image 3

  • You must have created the HomeScreen composable (check ui/screens/home/HomeScreen.kt)
  • The home screen basically consists of items arranged from top to bottom - vertically (as indicated by the black arrow above)
@Destination(style = StartScreenTransitionAnimation::class)
@Composable
fun HomeScreen(
    navigator: DestinationsNavigator,
    albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel()
){
    
    
//--> Home screen layout here
}

Personally, I like clean, reusable code. So, I wouldn't put the entire home screen item in this group. We'll create another group called HomeScreenItems.kt and place it inside our 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
        }
    )

}

this is our HomeScreenItems.ktfile

//HomeScreenItems.kt
@Composable
fun HomeScreenItems(
    modifier: Modifier = Modifier,
    navigator: DestinationsNavigator,
    albums: List<Album>,
    albumDatabaseViewModel: AlbumDatabaseViewModel = hiltViewModel(),
    onCardClicked: (String) -> Unit,
    onPopularAlbumClicked: () -> Unit
) {
    
    

    ........

}
  • This composable accepts many parameters.
    I will highlight parameters that were not explained before:

  • albums- Album list from Room database entity/table

  • albumDatabaseViewModel - The viewModel of the data source

  • onCardClicked - Lambda function to invoke when the card is clicked

  • onPopularAlbumClicked- Lambda function to invoke when a hit album is clicked

  • Since we want our home screen to be scrollable and gradually load items as we scroll, we'll use LazyColumn as the root layout.

  • LazyColumn is a vertically scrollable composable list that only composes and lays out currently visible items.

//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)
    ){
    
    
    
       .........
     
    }

}
  • So what do we want to put in the LazyColumn? Non-dynamic content (single static item) and dynamic content (changing items).
  • From Figure 3, you will find that the items in the blue and green boxes are non-dynamic content. They don't change.
  • Therefore, we will use a single item lambda function to display these items.
  • Each section has a combination for display UserHomeSection(), andSearchSection() .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)
            )

        }

       .................


     }

}
  • This is done for separation of concerns, and mostly for reusability (e.g. I can use the SearchSection composable elsewhere in the app without duplicating the entire code in the SearchSection composable)
  • Actually, I use the same composable parts in SearchScreen.
  • Note that in Figure 3, the first box is marked in blue. This simply means that items are laid out from left to right (horizontally). Therefore, we need to use a composable widget called Row.
  • Row is used to place items horizontally on the screen.
  • Now, for the dynamic items that we get from the Room database, we'll use a lambda function called items to display them.
    Our complete HomeScreenItems.ktis as follows:
//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)
                }
            )

        }

    }

}

I created a reusable composable called AlbumCard that we can use as a model to display all dynamic items.
Make sure to check out the full code to get a proper understanding of how it works.

I hope I've been able to explain how the basic UI works with Jetpack Compose. For more information about the UI, check out the official docs

github code url

https://github.com/ibrajix/MyMusic

reference

https://ibrajix.medium.com/how-i-built-this-nice-looking-app-using-jetpack-compose-3974db7eb9e

Guess you like

Origin blog.csdn.net/u011897062/article/details/130561025