A movie App was opened last year, using the mature (outdated) MVP architecture. As the Jetpack framework became more and more popular, the idea of re-developing entirely using the Jetpack framework came to mind. Along with the official release of the Compose Beta, the timing couldn’t be better.

Compose is used in general to implement the UI. Data requests rely on Coroutines to invoke the Retrofit interface, and then reflect the results through LiveData.

The finished product

Not so much. Let’s see how it works.

Launch page, search page and movie details page.

Shop page, favorites page and profile page.

Github address below, welcome reference, do not hesitate to STAR⭐️.

Github.com/ellisonchan…

Implementation scheme

Before we talk about this implementation, let’s review how the previous MVP version did it.

The function point Technical solution
The overall architecture MVP
UI ViewPager + Fragment
The View into the ButterKnife
Asynchronous processing RxJava
Data request Retrofit
The image processing Glide

The previous approach can be said to be more mature, more traditional (light spray πŸ˜‰).

If Jetpack’s Compose is used as the UI base disk, what solution will I give?

The function point Technical solution
The overall architecture MVVM
UI Compose
The View into the Don’t need 😎
Asynchronous processing Coroutines + LiveData
Data request Retrofit
The image processing coil

In actual combat

Just like in a movie, you have a script, and then you let the characters work their way up.

ACTION…

The UI navigation

The overall UI uses the BottomNavigation component as the BottomNavigation bar, and puts several preset TAB pages in Compose. The TopAppBar is also provided as a TITLE bar to display the page TITLE and return navigation.

// Navigation.kt
@Composable
fun Navigation(a){... Scaffold( topBar = { TopAppBar( ... ) }, bottomBar = {if(! isCurrentMovieDetail.value) { BottomNavigation { ... } } } ) { NavHost(navController, startDestination = Screen.Find.route) { composable(Screen.Find.route) { FindScreen(navController, setTitle, movieModel) } composable( route = Constants.ROUTE_DETAIL, arguments = listOf(navArgument(Constants.ROUTE_DETAIL_KEY) { type = NavType.StringType }) ) { backStackEntry -> DetailScreen( backStackEntry.arguments? .getString(Constants.ROUTE_DETAIL_KEY)!! , setTitle, movieModel ) } composable(Screen.Store.route) { StoreScreen(setTitle) } composable(Screen.Favourite.route) { FavouriteScreen(setTitle) } composable(Screen.Profile.route) { ProfileScreen(setTitle) } } } }Copy the code

There are two things to note here.

  • The movie details page is a jump from the search page, showing a strange navigation bar at the bottom. So we need to declareStateControls that this page does not display a navigation bar
  • The bottom navigation bar navigating to other pages such as the store is logged in the stack, causing the TITLE bar to display the back button. There is no need to provide a return operation for individual TAB pages. That’s the same statementStateMake sure these pages don’t show the back button

Search page

The search page first makes sure the network is working and gives an AlertDialog in case the network is unavailable.

TextField is used on the UI to provide the input area, LaunchedEffect watches the input update, and automatically executes the coroutine of the search request.

After the data is successfully obtained, it is reflected through LiveData to the LazyVerticalGrid that provides the GRID list. LazyVerticalGrid component API is still experimental, may be deleted at any time, use the words you need to add @ ExperimentalFoundationApi annotation.

// Find.kt
@ExperimentalFoundationApi
@Composable
fun Find(movieModel: MovieModel, onClick: (Movie) - >Unit){...if(! Utils.ensureNetworkAvailable(context,false)) ShowDialog(R.string.search_dialog_tip, R.string.search_failure) Column { Row() { TextField( value = textFieldValue, . trailingIcon = { IconButton( onClick = {if (textFieldValue.text.length > 1) {
                                searchQuery = textFieldValue.text
                            } else Toast.makeText(
                                context,
                                warningTip,
                                Toast.LENGTH_SHORT
                            ).show()
                        }
                    ) {
                        Icon(Icons.Outlined.Search, "search", tint = Color.White)
                    }
                },
                ...
            )
        }

        LaunchedEffect(searchQuery) {
            if (searchQuery.length > 0) {
                movieModel.searchMoviesComposeCoroutines(searchQuery)
            }
        }
        val moviesData: State<List<Movie>> = movieModel.movies.observeAsState(emptyList())
        val movies = moviesData.value
        val scrollState = rememberLazyListState()

        LazyVerticalGrid(
            ...
        ) {
            items(movies) { movie ->
                MovieThumbnail(movie, onClick = { onClick(movie) })
            }
        }

    }
}
Copy the code

In addition, whether the UI display in Compose depends on the update of State, so does the AlertDialog with poor network. You still need to rely on State to trigger the disappearance of the Dialog after clicking cancel, otherwise it will always be there at πŸ˜….

// Dialog.kt
@Composable
fun ShowDialog(
    title: Int,
    message: Int
) {
    val openDialog = remember { mutableStateOf(true)}if (openDialog.value)
        AlertDialog(
            onDismissRequest = { openDialog.value = false },
            title = {
                ...
            },
            text = {
                ...
            },
            confirmButton = {
                TextButton(onClick = { openDialog.value = false{...}) } }, shape = shapes.large, ) }Copy the code

The movie poster, in turn, relies on the coil loading function for Compose.

// LoadImage.kt
@Composable
fun LoadImage(
    url: String,
    contentDescription: String? , modifier:Modifier = Modifier,
    contentScale: ContentScale = ContentScale.Crop,
    placeholderColor: Color? = MaterialTheme.colors.compositedOnSurface(0.2f)
) {
    CoilImage(
        data = url,
        modifier = modifier,
        contentDescription = contentDescription,
        contentScale = contentScale,
        fadeIn = true,
        onRequestCompleted = {
            when (it) {
                is ImageLoadState.Success -> ...
                is ImageLoadState.Error -> ...
                ImageLoadState.Loading -> Utils.logDebug(Utils.TAG_NETWORK, "Image loading")
                ImageLoadState.Empty -> Utils.logDebug(Utils.TAG_NETWORK, "Image empty")
            }
        },
        loading = {
            if(placeholderColor ! =null) {
                Spacer(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(placeholderColor)
                )
            }
        }
    )
}
Copy the code

Details page

The layout of the movie details page is relatively complex, mainly because it wants to show a lot of content. The simple layout is bloated, without a sense of hierarchy.

So flexible use of Box, Card, Column, Row and IconToggleButton components to achieve horizontal and vertical nested multi-level layout.

The IconToggleButton, which is used as a display button, relies on State to update the Toggle State, just like the AlertDialog before it. The concept of State is ubiquitous in the Compose toolkit πŸ‘.

// Detail.kt
@Composable
fun Detail(moviePro: MoviePro) {
    Box(
        modifier = Modifier
            .fillMaxHeight(),
    ) {
        Column(
            ...
        ) {
            Box(
                modifier = Modifier
                    .fillMaxHeight(),
                contentAlignment = Alignment.TopEnd
            ) {
                LoadImage(
                    url = moviePro.Poster,
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(380.dp),
                    contentScale = ContentScale.FillBounds,
                    contentDescription = moviePro.Title
                )

                val checkedState = remember { mutableStateOf(false) }
                Card(
                    modifier = Modifier.padding(6.dp),
                    shape = RoundedCornerShape(50),
                    backgroundColor = likeColorBg
                ) {
                    IconToggleButton(
                        modifier = Modifier
                            .padding(6.dp)
                            .size(32.dp),
                        checked = checkedState.value,
                        onCheckedChange = {
                            checkedState.value = it
                        }
                    ) {
                        ...
                    }
                }
            }

            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Text(
                        modifier = Modifier
                            .weight(0.9 f)
                            .align(Alignment.CenterVertically),
                        text = moviePro.Title,
                        style = MaterialTheme.typography.h6,
                        color = nameColor,
                        overflow = TextOverflow.Ellipsis,
                        maxLines = 1)... }... }}}}Copy the code

The store page

This page currently shows a list of recommended movies and a list of movies by actor, so calling it a Store would be inappropriate, but let’s leave it at that.

The UI uses vertical layout columns and horizontally scrolling LazyRow to show the nested layout. One thing I recommend is that if you want to show a round image, you can do it with RoundedCornerShape.

// Store.kt
@Composable
fun Store(a) {
    Column(Modifier.verticalScroll(rememberScrollState())) {
        Spacer(Modifier.sizeIn(16.dp))
        Text(
            modifier = Modifier.padding(6.dp),
            style = MaterialTheme.typography.h6,
            text = stringResource(id = R.string.tab_store_recommend)
        )

        Spacer(Modifier.sizeIn(16.dp))
        MovieGallery(recommendedMovies, width = 220.dp, height = 190.dp)

        CastGroup(cast = testCast1)
        CastGroup(cast = testCast2)
    }
}

@Composable
fun CastGroup(cast: Cast) {
    Column {
        Spacer(Modifier.sizeIn(32.dp))
        CastCategory(cast)
        Spacer(Modifier.sizeIn(6.dp))
        MovieGallery(cast.movies)
    }
}

@Composable
fun CastCategory(cast: Cast) {
    Row(
        modifier = Modifier
            .height(40.dp)
            .padding(16.dp, 2.dp, 2.dp, 16.dp)
    ) {
        Card(
            modifier = Modifier.wrapContentSize(),
            shape = RoundedCornerShape(50),
            elevation = 8.dp ) { ... }.. }}@Composable
fun MovieGallery(movies: List<Movie>, width: Dp = 130.dp, height: Dp = 136.dp) {
    LazyRow(modifier = Modifier.padding(top = 2.dp)) { items(movies.size) { RowItem( ... ) }}}@Composable
fun RowItem(modifier: Modifier, width: Dp = 130.dp, height: Dp = 1306.dp, movie: Movie){ Card( ... ) { Box { LoadImage( url = movie.Poster, modifier = Modifier .width(width) .height(height), contentScale = ContentScale.FillBounds, contentDescription = movie.Title ) Text( ... ) }}}Copy the code

This page has three horizontal scroll views nested in columns, which can be incomplete if the screen is not high enough. Nature came up with similar ScrollView components, detected the ScrollableColumn at the beginning, but AS there is no the tip over and over again.

A look at the website shows that this component and ScrollableRow were removed from πŸ˜“ in previous releases due to performance concerns. Fortunately, the official tip can use Modifier. VerticalScroll or LazyColumn can achieve the purpose of rolling.

Collection page

The favorites page shows only a list of favorites, which is the simplest. Use LazyColumn to cover.

// Favourite.kt
@Composable
fun Favourite(moviePros: List<MoviePro>, onClick: () -> Unit) {
    LazyColumn(modifier = Modifier.padding(top = 2.dp)) {
        items(moviePros.size) {
            LikeItem(
                moviePro = moviePros[it],
                onClick
            )
        }
    }

}

@Composable
fun LikeItem(moviePro: MoviePro, onClick: () -> Unit) {
    Box(
        modifier = Modifier
            .wrapContentSize()
            .padding(8.dp)
    ) {
        Card(
            modifier = Modifier
                .border(1.dp, Color.Gray, shape = MaterialTheme.shapes.small)
                .shadow(4.dp),
            shape = shapes.small,
            elevation = 8.dp,
            backgroundColor = itemCardColor
        ) {
            Row(
                modifier = Modifier
                    .clickable(onClick = onClick)
                    .fillMaxWidth()
                    .height(100.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                LoadImage(
                    url = moviePro.Poster,
                    modifier = Modifier
                        .width(80.dp)
                        .height(100.dp), contentScale = ContentScale.FillBounds, contentDescription = moviePro.Title ) ... }}}}Copy the code

Profile page

The profile page needs to provide cover image, name, profile, nickname and social account information, a little bit of work.

For my lack of design talent, I refer to the profile page for Jetchat, the Compose example project.

Recommended is the BoxWithConstraints component, which provides an effect similar to ConstraintsLayout in that it can dynamically change the size of the constraint once the rule or direction is specified.

// Profile.kt
@Composable
fun Profile(account: Account) {
    val scrollState = rememberScrollState()

    Column(modifier = Modifier.fillMaxSize()) {
        BoxWithConstraints(modifier = Modifier.weight(1f)) {
            Surface {
                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .verticalScroll(scrollState),
                ) {
                    ProfileHeader(
                        scrollState,
                        this@BoxWithConstraints.maxHeight,
                        account.Post
                    )

                    NameAndPosition(
                        stringResource(id = account.FullName),
                        stringResource(id = account.About)
                    )

                    ProfileProperty(
                        stringResource(R.string.display_name),
                        stringResource(id = account.NickName)
                    )
                    ...
                    EditProfile()
                }
            }
        }
    }
}

@Composable
fun ProfileProperty(label: String, value: String, isLink: Boolean = false) {
    Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) {
        Divider()
        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            Text(
                text = label,
                modifier = Modifier.paddingFromBaseline(24.dp),
                style = MaterialTheme.typography.caption
            )
        }
        val style = if (isLink) {
            MaterialTheme.typography.body1.copy(color = Color.Blue)
        } else{ MaterialTheme.typography.body1 } ... }}Copy the code

I’ve covered most of the implementation details of the App, so it’s a small amount of code. In addition to being relatively simple, the simplicity and ease of use of the Compose toolkit is definitely a factor.

insufficient

Let’s talk about the shortcomings of the App, including UI interaction, functionality and so on.

1. Chinese keyword search is not supported

The data source of THE App is foreign OMDB. Its movie library is still sound and the content related to movies is rich enough. But its birthplace also decided that it is only good at English keyword queries, but in other languages such as Chinese, Japanese, almost no movies.

In order to improve the Chinese function, it is necessary to import the Interface of Chinese film. Do not find, before the use of good Douban API has been abandoned.

Understand the friend can educate me, thank πŸ™.

2.UI design style needs to be strengthened

At present, the overall UI design uses beige as the background, blue as the highlight, and light gray, white and purple as the display of other content. Give a person feeling still have a little thing, but total kind of say not disorderly, cannot immerse in. Do not know the screen in front of you have the same feeling πŸ˜‚?

In the future, I plan to deeply learn and understand the Material design language, and perfectly integrate its design concept into Compose. Ok, speak Human language. Over time, I’ll be watching a couple of good movie apps like Netflix, Disney+, etc., for a good parody of the mature, friendly visuals.)

3. The TITLE bar of the search page is a bit redundant

The search page displays the search icon in order to provide the same TITLE bar effect as other pages. For the user, this overlaps the function of the input box below and takes up the display area of the movie list.

So you can delete the TITLE bar of this page and directly provide the input box.

4. IME can automatically hide after search

The IME panel does not automatically hide after clicking the search button, which is not a good experience. A better experience would be to automatically hide the IME after clicking or searching.

Briefly check the data, it seems to be using TextInputService to achieve, tamper with for half an hour has not been implemented, temporarily shelved. The friend that knows can reply next, compare heart ❀️.

5. The store page needs to strengthen the recommendation

First of all, the name of this page might need to be changed. Wouldn’t it be better to change it to “Home”? “Home” better understand you, give you some precise advice.

OMDB does not provide an interface for recommendation movies, so the current recommendation list data is analog. Then it may be necessary to record and analyze data such as the keywords searched by users, the types of movies clicked on, and the film directors and actors they followed to produce a set of intelligent recommendation results. Finally, it is presented in terms of genre, director, actor and other dimensions.

Use the Room framework with a set of algorithms.

6. The collection and data should be persisted

The current favorite movie data is not persisted to the local, and the profile page does not provide entry for editing. Later, you need to support the data through the Room or DataStore framework.

Of course, in front of the screen you feel there is no shortage of can not hesitate to give advice, I will be all ears.

conclusion

I have written so many words in one breath. Finally, I want to share some real feelings.

  1. Compose version vs. MVP version?
  • In the Compose version, the code is much leaner, and the declarative UI programming approach is innovative, with a focus on declarations and states everywhere. Its seamless integration with the Jetpack framework and Material themes gives developers who are used to XML layouts a quick start

  • The Compose toolkit isn’t perfect, and I’m a little skeptical about its performance. And there is no guarantee that companies and products will respond to the nascent technology

  • The COMPLEXITY of interfaces in the MVP architecture is a problem, but it’s not all bad. Combine product positioning and demand, dialectical view these two ways

  1. Is there any pain point for Compose?
  • Lack of logs: You can’t see any logs at the debug and error levels, making it difficult to control processes and locate problems

  • Principles learning difficulties: there are many packages for UI and logic, and very few articles on principles (I hope I can contribute to this in the future πŸ’ͺ)

  1. What should we do in the face of new Android technologies?
  • Stick your head in the ground and ignore it. Keep paying attention and give it a try

  • Don’t take easy coding as the whole story, but realize that the framework and compiler behind it do a lot of the work

  • Don’t be obsessed with the frame, don’t rely on the frame, understand and master the principles, and be comfortable when the pit comes

In this paper, the DEMO

The above explains only the key details, and you should refer to the full code if necessary. Github.com/ellisonchan…

The resources

Subject to official

The official documentation is professional and detailed, and the following home page can guide you to each point. Developer. The android. Google. Cn/jetpack/com…

Two articles in particular are recommended, which can help us understand the programming ideas and core state management of Compose.

  • Developer. The android. Google. Cn/jetpack/com…
  • Developer. The android. Google. Cn/jetpack/com…

Master in the folk

For Compose, private developers have been very enthusiastic about it. The number of posts they’ve posted isn’t huge, but it’s high quality. I would like to share with you some of the excellent articles I know.

In this simple example, you can easily understand the difference between the XML layout and the Compose declaration, which is well worth reading.

Juejin. Cn/post / 693522…

From the perspective of principle, ZNJW has explained in detail the advantages and disadvantages of Compose, React, Vue and Swift, which is worth chewing over and over again.

www.jianshu.com/p/7bff0964c…

Tino Balint and Denis Buketa share the details of how the various UI components are used in Compose, and they’re incredibly professional. It should be eaten with translation software.

www.raywenderlich.com/books/jetpa…

In this post, ZhuJiangs explains how to navigate the display in Compose, and how to intertune with traditional Android views and other frameworks.

Blog.csdn.net/haojiagou/a…

Fundroid_ Fang Zhuo, with his smooth writing style, wonderfully restores the carefree experience of creating animations and themes with Compose.

Blog.csdn.net/vitaviva/ar… Blog.csdn.net/vitaviva/ar…

With its rich drawing experience, O0 has vividly demonstrated that you can also customize various fancy effects with Compose, which is worth collecting and learning.

Juejin. Cn/post / 693770…