The paper

Compose Is a pure Kotlin Android App that uses Compose for almost all of its UI components. The App is called Compose Many because it was originally intended to be a tool for implementing a variety of small features, but also to learn from the Compose development process. But so far only the music has been finished.

Compose has been officially released to 1.0.2 (Alpha is 1.1.0), so the official update speed is not too fast, and the first official release should focus on stability. We hope that the next major release will be as comprehensive as Flutter2.0 with a richer ecosystem.

The effect

Completed function mainly is the song of a single player and comment to see, because many interfaces and APP main use of the development of leisure time, time is limited only recommend playlists and personal playlists for, often listen to the song can listen to the first – and then comment function temporarily can only view, thumb up, reply behind these have time to do it again.

  

Implemented functions

  • Netease Cloud mobile phone account + password login
  • Recommended playlist, personal playlist display
  • Playlist playing of songs
  • Song review view, building reply review

The main implementation

The server side

Using the NeteaseCloudMusicApi compiled by Binaryify, you can easily access various data interfaces through the RESTful API. The warehouse also provides out-of-box deployment solutions. Here is the Vercal solution:

So I turned to treasureVercel, have their own domain name for free, and can deploy their own code on it.Of course, Vercel isn’t completely free, and it limits access over a period of time. When you reach a high volume of access, it will be deemed too high for personal use, and the domain entry may be shut down. Therefore, for learning purposes, it is best to register a Vercel account, and then the App calls its own API address

Client architecture

  • Interface representation layer

There are not many interfaces in the application. MVVM is used in the interface presentation layer, the music function is single Activity+ multiple fragments, and the Fragment content is constructed by Compose.

Where PlaySongsViewModel lifecycle follows the Activity:

private val playSongsViewModel by activityViewModels<PlaySongsViewModel>()
Copy the code

In this way, both the home page and the player widget at the bottom of the playlist page (PlayWidget), or song playing interface, their music playing states are all from the same sourcePlaySongsViewModel, any play operation in one place can be correctly displayed in other pages. A single source of state is naturally achieved by properly partitioning the ViewModel scope without the need for state-confusing notifications like EventBus.

Projects using Compose to build interface, try to follow the official website of the state of the ascension “one-way” data flow, specific reference website: developer. The android, Google. Cn/jetpack/com…

  • Dependency injection

The project uses Jetpack Hilt to manage all dependencies, which, along with most of the other Jetpack components, makes it easy to get dependencies from the View, ViewModel, and Repository layers.

  • Data warehouse layer

Network data source using Retrofit, database ORM using Jetpack ROOM, application persistent data using Jetpack DataStore(ProtoBuf implementation)

Interface navigation

Interface jump uses Jetpack Navigation, scheme selection goes through several iterations:

  • Compose integration using Navigation only

Originally intended to use the Compose integration only with navigation, it is very easy to jump between the two Composable interfaces as follows:

val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoutes.AboutUs.path) {
    composable(ScreenRoutes.AboutUs.path) {
        AboutUsPage(onBackClick = { finish() }) {
            navController.navigate(ScreenRoutes.Privacy.path)
        }
    }
    composable(ScreenRoutes.Privacy.path) {
        HtmlDocumentViewer(title = "Privacy Policy")}... . }Copy the code

The advantage of fully using ComposeNavigator is the ComposeNavigator transition to the Compose interface and the ability to share elements between interfaces in later releases.

  • FragmentNavigator is used with composenavigators

However, in practice, it is found that the above method cannot pass the parameters of the custom type between the destination, so WE want to mix the Fragment/Activity Navigator and ComposeNavigator. However, the ComposeView recreated is always blank when the jump Fragment is returned (Navigation uses replace by default for Fragment Navigation jumps, so onCreateView is called again when returned). NavHost:

// NavHost.kt
// 
// Lifecycle# render when currentState is greater than STARTED
valbackStackEntry = transitionsInProgress.lastOrNull { entry -> entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } ? : backStack.lastOrNull { entry -> entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } ... .if(backStackEntry ! =null) { Crossfade(backStackEntry, modifier) { currentEntry -> ... }}Copy the code

The state returned from the rollback stack is CREATED when the NavHost is reorganized. BackStackEntry cannot be obtained and therefore cannot be displayed. The solution is to change the state of NavHost when Lifecycle enters the STARTED to actively trigger the reassembly, such as transparency from 0F -> 1F:

var navAlpha by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = true, block = {
    lifecycle.whenStarted { navAlpha = 1.0 f}})Copy the code
  • For the final scenario, use only FragmentNavigator

The ComposeNavigator was temporarily abandoned in order to use a pure FragmentNavigator for interface navigation in order to unify navigation. Compose support does not seem to be fully stable on the current navigation release (2.4.0-Alpha06).

Collapsible title bar

Compose does not temporarily collapse Toolbarlayout and CoordinatorLayout in View system. Or a custom slide collapsible title control like the CustomScrollView+SliverAppBar in Flutter, so finally find some other implementations:

  1. In the official documentation forModifierthenestedScrollHere is an example of a collapsing title barandroidx.compose.ui.input.nestedscroll  |  Android Developers (google.cn)
val nestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
            val delta = available.y
            val newOffset = toolbarOffsetHeightPx.value + delta
            toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
            returnOffset.Zero } } } ... . TopAppBar( modifier = Modifier .height(toolbarHeight) .offset { IntOffset(x =0, y = toolbarOffsetHeightPx.value.roundToInt()) },
    title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}")})Copy the code

The main problem is that the offset calculated by nested sliding is associated with the TopAppBar. The main problem is that the collapse is expanded as soon as the pull-up starts, rather than when the pull-up reaches the top of the list. Maybe you can do it in other ways, but it’s complicated.

  1. LazyColumnYou can get the status of the slide, and then make the title bar separateitem{}On top of that, you have the flexibility to implement your own foldable status bar
@Composable
fun CollapsingEffectScreen(a) {
    val items = (1.100.).map { "Item $it" }
    val lazyListState = rememberLazyListState()
    var scrolledY = 0f
    var previousOffset = 0
    LazyColumn(
        Modifier.fillMaxSize(),
        lazyListState,
    ) {
        item {
            Image(
                modifier = Modifier
                    .graphicsLayer {
                        scrolledY += lazyListState.firstVisibleItemScrollOffset - previousOffset
                        translationY = scrolledY * 0.5 fpreviousOffset = lazyListState.firstVisibleItemScrollOffset } ) } items(items) { ... . }}}Copy the code

Using graphicsLayer for association offsets, folds, transparency, and so on can avoid frequent reorganizations.

  1. A third party library maintained by a Korean developer makes it easy to collapse the title bar:

onebone/compose-collapsing-toolbar: A simple implementation of collapsing toolbar for Jetpack Compose (github.com)

CollapsingToolbarScaffold(
    state = rememberCollapsingToolbarScaffoldState(), // provide the state of the scaffold
    toolbar = {
        // contents of toolbar go here...{})// main contents go here...
}
Copy the code

Record animation

Composing will find it much simpler and much more expressive than the View system. For example, the infinite rotation of the image animation can be easily implemented using the following code:

val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(initialValue = 0f, targetValue = 360f,
    animationSpec = infiniteRepeatable(
        animation = tween(15 * 1000, easing = LinearEasing)
    )
)
Image(
    modifier = Modifier
        .graphicsLayer {
            rotationZ = rotation
        }
)
Copy the code

But the record animation has a feature that the song can be paused, and the animation also needs to be paused and resumed in place. For example, Flutter can pause and resume animation using the Stop and forward methods of the AnimateController. However, Compose’s animation system is more convenient to use but lacks the API to directly control the animation flow. The lower-level Animatable was used. Since the starting point and end point of infinite animation must differ 360 degrees in order to have infinite loop effect, and the starting Angle is the current Angle value, so it can achieve visual seamless infinite rotation animation within the range of 0 ~ 720 degrees through Angle remainder:

/** * infinite loop rotation animation */
@Composable
private fun infiniteRotation(
    startRotate: Boolean,
    duration: Int = 15 * 1000
): Animatable<Float, AnimationVector1D> {
    var rotation by remember { mutableStateOf(Animatable(0f)) }
    LaunchedEffect(key1 = startRotate, block = {
        if (startRotate) {
            // From last pause Angle -> Execute animation -> target Angle (+360°)
            rotation.animateTo(
                (rotation.value % 360f) + 360f, animationSpec = infiniteRepeatable(
                    animation = tween(duration, easing = LinearEasing)
                )
            )
        } else {
            rotation.stop()
            // The initial Angle is mod to prevent the target Angle from increasing indefinitely after each pause
            rotation = Animatable(rotation.value % 360f)}})return rotation
}
Copy the code

The picture is rounded and blurry

Rounded corners, rounded crop, and blurred images can be easily achieved with Coil:

Image( painter = rememberImagePainter(song? .picUrl? .limitSize(200), builder = {
        transformations(
            CircleCropTransformation(),
            BlurTransformation(LocalContext.current, 16f),)}))Copy the code

One of the drawbacks of the fuzzy background implementation was that the white ICONS (buttons) would blend into the background and not be visible on a light background. Observing the original App of netease Cloud Music, it was found that even with a white background, it was darkened to accommodate the light-colored foreground buttons and ICONS. So along those lines, the method I’ve used here is to cover a translucent gray-and-black layer over a blurred background:

// Blurred cover as background
Image(
    ...
    modifier = Modifier
        .drawWithContent {
            drawContent()
            // Cover the background with translucent color to improve the display of white buttons against a bright background
            drawRect(Color.Gray, alpha = 0.7 f)},...).Copy the code

This makes it easier to see the light buttons on the blurred background, even if the overall color is bright.In addition, it should also be possible to use the Image colorFilter to blend the dimming color to achieve better results (untested) :colorFilter = ColorFilter.tint(Color.Gray, BlendMode.Darken)

At the bottom of the popup window

The bottom popover is also very simple to implement in Compose

ModalBottomSheetLayout(
    // Popover content
    sheetContent = { ReplySheet(floorComment) },
    sheetState = sheetState,
    sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
    / / the main content
    CommentMain(song, commentCount, commentList, sortType) {
        viewModel.loadFloorReply(it.commentId)
        scope.launch { sheetState.show() }
    }
}
// Close the popover when you need to return. This handles the return key behaviorBackHandler(sheetState.currentValue ! = ModalBottomSheetValue.Hidden) { scope.launch { sheetState.hide() } }Copy the code

The last

The first time nuggets on the article typesetting is more messy. This project is my first Compose application and a major practice. Many of the features in the APP are not perfect, and the knowledge of Compose is not very deep, and some parts of the implementation may not be the best practice. The overall use of the development of the intuitive feeling is still very different from the traditional View, especially through a variety of modifiers can be customized built-in controls for their desired style, and then combined layout, repetitive boilerplate code is much less.

For example, the LayzColumn list is not as smooth as RecyclerView, and many apis have experimental annotations (i.e. the API is not stable yet and may change later). The performance aspect is also the official focus in the future version of the optimization point.

It is predictable that modern declarative UI will be an efficient UI development model in the future, but whether it becomes mainstream on Android will depend on the level of official development and the acceptance of developers

Finally, there is the source address of this APP, and more functions will be added when available:

Mr-lin930819/ComposeMany: Jetpack Compose Built app (github.com)

Note: The project can be compiled and run directly, Android Studio requires Arctic Fox or later to use Compose. If you are willing to learn or improve app together, welcome to submit PR~

The resources

  • Jetpack Compose  |  Android Developers (google.cn)
  • Fluttercandies/NeteaseCloudMusic: Flutter – NeteaseCloudMusic Flutter version of netease cloud music (github.com)
  • Jetpack MVVM is a breath of fresh air! – the nuggets (juejin. Cn)
  • android – Jetpack Compose collapsing toolbar – Stack Overflow