An impudent preface

This article is based on the first article about Compose, and it’s best to read it first before reading it.

Because of the length of my last article, I did not introduce many things, for Compose is very large and definitely not enough to finish in one article. Let’s take our time. In this article, we are going to take a closer look at the core of Compose: Navigation, State, and interoperability between Compose and Android View. Then the article will write about playing Android project some implementation, after all, actual combat is still very important, if these knowledge points you will, just don’t know how to really use it, directly move to the bottom of the actual combat on the line, so let’s start now!

Before writing, put the Github address in the main branch:

Github address: github.com/zhujiang521…

Navigation

In the last article, I also wanted to write a brief description, but after thinking about the Navigation content, IT is better to keep it simple in the first article, otherwise it will be difficult to read, so I put it in this article.

Add the dependent

I already added it in the last post, but I’ll write it again:

dependencies {
  implementation "Androidx. Navigation: navigation - compose: 1.0.0 - beta01"
}
Copy the code

Introduction to use

NavController is the central API of the Navigation component. This API is stateful, keeping track of the return stack of composable items that make up the application screen, as well as the state of each screen.

You can create a NavController by using the rememberNavController() method in a composable entry:

val navController = rememberNavController()
Copy the code

You should create a NavController in an appropriate place in the composable item hierarchy, making it accessible to all composable items that need to reference it.

Create NavHost

Each NavController must be associated with a NavHost composable. NavHost associates the NavController with the navigation diagram. NavController specifies the page you need to navigate. As you navigate between pages, NavHost’s content is automatically reorganized, and each page in the navigation map is associated with a route.

NavHost(navController, startDestination = "one") {
    composable("one") { Profile(...) }
    composable("two") { FriendsList(...) }... }Copy the code

I was a little confused when I looked at this, what is this?? Why is jump written like this? It is also good to understand later, and routing is similar, are through the string to define the path to the combinable item, and is unique.

jump

The path for each Composable has been defined above, so how to jump? It’s easy, just pass NavController:

fun One(navController: NavController){... Button(onClick = { navController.navigate("two") }) {
        Text(text = "One")}... }Copy the code

The caveat here is that navigate() should only be called in the callback, and try not to call it in the composable item itself to avoid calling navigate() every time a reorganization occurs.

By default, navigate() adds a new page to the return stack. Additional navigation options can be appended to the navigate() call to modify the behavior of navigate:

{popUpTo navController. Navigate (" one "),"home")}Copy the code

The code above means pop everything from the background stack to home and navigate to the ONE page.

navController.navigate("one") {
    popUpTo("home") { inclusive = true}}Copy the code

What this code means is to pop up all the information containing the home page and navigate to the back stack before and after one.

navController.navigate("search") {
    launchSingleTop = true
}
Copy the code

This last code means that we navigate to the “search” page only if we don’t have the “search” page open, avoiding going back to the stack.

Jump to pass parameters

Navigation Compose also supports passing parameters between composable item pages. To do this, add parameter placeholders to the route:

NavHost(startDestination = "profile/{userId}") {... composable("profile/{userId}") {...}
}
Copy the code

Does that sound familiar? Hahaha, have you used Retrofit? Is it similar? Or write backend code? Is it written a little bit like SpringMVC?

By default, all parameters are parsed as strings. But you can use the arguments argument to set type to specify other types:

NavHost(startDestination = "profile/{userId}") {... composable("profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}
Copy the code

The NavArguments can be extracted from the NavBackStackEntry provided in the composable() function’s lambda:

composable("profile/{userId}") { backStackEntry -> Profile(navController, backStackEntry.arguments? .getString("userId"))}Copy the code

To pass parameters to a page, add a route value instead of a placeholder in the navigate call:

navController.navigate("profile/user1234")
Copy the code

What do you mean by that? You call the jump, you pass the placeholder, you pass the parameters you need to pass. What? Don’t know how to pass various types of parameters? Don’t worry. I’ll tell you right away.

The Navigation library supports the following parameter types:

type App: argType syntax Is the default value supported? Are null values supported?
The integer app:argType=”integer” is no
Floating point Numbers app:argType=”float” is no
Long integer app:argType=”long” Yes – The default value must always end with an “L” suffix (for example, “123L”). no
Boolean value app:argType=”boolean” Is – “true” or “false” no
string app:argType=”string” is is
The resource reference app:argType=”reference” Is the default value must be a “@ resourceType/resourceName” format (for example, “@ style/myCustomStyle”) or “0” no
Custom Parcelable App :argType=””, whereParcelableFully qualified class name of the The default value @null is supported. No other default values are supported. is
Custom Serializable App :argType=””, whereSerializableFully qualified class name of the The default value @null is supported. No other default values are supported. is
Custom Enum App :argType=””, where is the fully qualified name of Enum Yes – The default value must match the unqualified name (for example, “SUCCESS” matches MyEnum.SUCCESS). no

The table above is copied directly from Navigation on the official website for your convenience. You can see that there are many types, which can basically meet most development needs.

Pass a jump with optional parameters

Sometimes you need to specify parameters to pass, especially Kotlin’s sweet syntax sugar, which is very cool to use, but how to jump to optional parameters? Optional parameters differ from required parameters in two ways:

  • Optional parameters must use query parameter syntax ("? argName={argName}") to add
  • Optional parameters must be availabledefaultValueSet ornullability = true(Set the default value implicitly tonull)

Therefore, all optional parameters must be explicitly added to the composable() function as a list:

composable(
    "profile? userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me"}) ) { backStackEntry -> Profile(navController, backStackEntry.arguments? .getString("userId"))}Copy the code

The system uses defaultValue of “me” even if no arguments are passed to the destination. Is it so easy?

Deep links

Navigation Compose supports implicit deep links, which can also be defined as part of the Composable () function. Use navDeepLink() to add deep links in the form of lists:

val uri = "https://example.com"

composable(
    "profile? id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}"}) ) { backStackEntry -> Profile(navController, backStackEntry.arguments? .getString("id"))}Copy the code

These deep links make it possible to associate specific web addresses, operations, and/or MIME types with composable items.

By default, these deep links are not exposed to external applications. To provide these deep links externally, you must add the corresponding

element to the application’s Androidmanifest.xml file:

<activity... >
  <intent-filter>.<data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>
Copy the code

When the deep link is triggered by another application, Navigation will automatically deep link to the corresponding composable item.

These deep links can also be used to build pendingIntents that contain related deep links in composable items:

val id = "exampleId"
val context = AmbientContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
Copy the code

You can then open the application in the corresponding deep link using deepLinkPendingIntent as you would with any other PendingIntent.

Small Navigation summary

Navigation should be used for most applications. Don’t worry about the lack of examples. The examples will be included in the following sections, as you will need to use ViewModel, State, etc, so take your time. A watched pot never boils ๐Ÿ˜‚.

Manage State — State

A state in an application is any value that can change over time. This is a very broad definition, ranging from Room database to class variables to viewModels, LiveData and so on that we usually use.

Let’s start with a picture:This is where all Android apps have a core interface update loop, as described in the figure.

But in Jetpack Compose, the state and the event are separate. Status represents values that can be changed, and events represent notifications that something has happened. By separating the state from the event, you can decouple the presentation of the state from how the state is stored and changed.Take a look at the official advantage:

By following one-way data flow, you can decouple the composable items that display state in the interface from the parts of the application that store and change state.

The interface update loop for an application that uses one-way data flow looks like this:

  • Events: Events are generated by a part of the interface and passed up.
  • Update state: Event handling scripts can change state.
  • Display state: The state is passed down, and the interface observes the new state and displays it.

Following this pattern when using Jetpack Compose provides several advantages:

  • Testability: By decoupling the state from the interface that displays the state, it is easier to test both separately.
  • State encapsulation: Because state can only be updated in one place, it is less likely to create inconsistent state (or generate errors).
  • Interface consistency: With the use of an observable state store, all state updates are immediately reflected in the interface.

The common Observable types used in the Compose application include State, LiveData, StateFlow, Flow, and Observable Supported State, and can be observed.

Use the ViewModel

MVVM is now used in many projects, and the benefits are numerous, especially with LiveData. Let’s take a look at how to use MVVM in Compose:

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
    val name: String by helloViewModel.name.observeAsState("")
    Column {
        Text(text = name)
        TextField(
            value = name,
            onValueChange = { helloViewModel.onNameChanged(it) },
            label = { Text("Name")})}}Copy the code

Yes, it’s as simple as that. You can just grab the ViewModel and do whatever you want!

But… Not ah. What about LiveData? In Compose, you can observe an Activity or Fragment, but not in Compose because you need a LifecycleOwner parameter.

For example, in Compose, you need to convert LiveData to the State that can be seen. How do you use it?

val position by viewModel.position.observeAsState()
Copy the code

By implicitly treats State

as an object of type T in Jetpack Compose, which is a sweet syntactic candy, if you think it’s too sweet for you

val position: State<Int> = helloViewModel.name.observeAsState(0)
Copy the code

This can be used, how to use how to use, like the previous LiveData, when the data changes will help you to refresh, you do not have to worry about it, this is now always said data-driven page refresh.

States in composable items

Composable items remember individual objects, but the system stores the value calculated by Remember in the composition during initial composition and returns the stored value during reorganization.

I don’t know if I don’t know how to use it, but I don’t think it’s going to work, and the useful data is still in the ViewModel.

But I used it last time, because I didn’t use the ViewModel in the first article, so I had to use it for functionality! Let me tell you:

var expanded by remember { mutableStateOf(false)}Copy the code

The mutableStateOf() thing creates an observable MutableState, so you can drive the page to refresh.

The State of small summary

Now that I’ve covered managing state, it’s time to look at the interoperability of Compose and Android View.

Compose and Android View interoperate

Compose in Android View

Check out the official description:

Jetpack Compose has been carefully designed to work with an established view-based interface approach. If you’re building a new application, the best option is probably to implement the entire interface using Compose. However, if you are modifying an existing application, you may not want to migrate the entire application, but instead can combine Compose with your existing interface design.

You can combine Compose with a view-based interface in two main ways:

  • You can add the Compose element to an existing interface by creating a new screen entirely based on Compose, or by adding the Compose element to an existing Fragment or view layout.
  • You can add view-based interface elements to composable functions. Doing so lets you add non-compose widgets to your Compose based design.

The first article briefly described how to add Compose to the Android View, so I won’t repeat that.

Android View in Compose

You can add an Android View hierarchy to the Compose interface. What if you want to use an interface element (such as a WebView or MapView) that is not already provided in Compose? Don’t know, do you?

Since you ask from the bottom of your heart, I will tell you with great mercy! What? Didn’t you ask? Then I’ll tell you! Here it is:

@Composable
fun LoadingContent(a) {
    val context = LocalContext.current
    val progressBar = remember {
        ProgressBar(context).apply {
            id = R.id.progress_bar
        }
    }
    progressBar.indeterminateDrawable =
        AppCompatResources.getDrawable(LocalContext.current, R.drawable.loading_animation)
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        // Adds view to Compose
        AndroidView(
            { progressBar }, modifier = Modifier
                .width(200.dp)
                .height(110.dp)
        ) {}
    }

}
Copy the code

Ha ha ha, isn’t that easy? One might ask, why do I set an ID? Because each element must have a unique ID for savedInstanceState to work.

One more thing that needs to be noted in the above code is that the method of obtaining the Context was searched on the Internet, and the result was not found. Finally, the official Demo of Google found the answer. Do not believe the official documentation, the official documentation for such access, but there is no access, can not find the class to use what ah?

val context = AmbientContext.current // Error
val context = LocalContext.current // Write it correctly
Copy the code

The above is a simple use, let’s look at a slightly more complex use method, to see how to call the WebView, just in preparation for the following article details:

@Composable
fun rememberX5WebViewWithLifecycle(a): X5WebView {
    val context = LocalContext.current
    val x5WebView = remember {
        X5WebView(context).apply {
            id = R.id.web_view
        }
    }

    // Makes MapView follow the lifecycle of this composable
    val lifecycleObserver = rememberX5WebViewLifecycleObserver(x5WebView)
    vallifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { lifecycle.addObserver(lifecycleObserver)  onDispose { lifecycle.removeObserver(lifecycleObserver) } }return x5WebView
}

@Composable
private fun rememberX5WebViewLifecycleObserver(x5WebView: X5WebView): LifecycleEventObserver =
    remember(x5WebView) {
        LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> x5WebView.onCreate()
                Lifecycle.Event.ON_START -> x5WebView.onStart()
                Lifecycle.Event.ON_RESUME -> x5WebView.onResume()
                Lifecycle.Event.ON_PAUSE -> x5WebView.onPause()
                Lifecycle.Event.ON_STOP -> x5WebView.onStop()
                Lifecycle.Event.ON_DESTROY -> x5WebView.destroy()
                else -> throw IllegalStateException()
            }
        }
    }
Copy the code

The code is simple enough that everyone should be able to understand it, binding the control to the life cycle to avoid memory leaks.

If you need to add view elements or hierarchies, you can use AndroidView composables. The system passes a lambda to the AndroidView that returns the View. AndroidView also provides update callbacks that are called when the view is bloated. AndroidView is reorganized whenever the State read in this callback changes.

@Composable
fun CustomView(a) {
    val selectedItem = remember { mutableStateOf(0)}// Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        viewBlock = { context ->
            CustomView(context).apply {
                myView.setOnClickListener {
                    selectedItem.value = 1
                }
            }
        },
        update = { view ->
            view.coordinator.selectedItem = selectedItem.value
        }
    )
}

@Composable
fun ContentExample(a) {
    Column(Modifier.fillMaxSize()) {
        Text("Look at this CustomView!")
        CustomView()
    }
}
Copy the code

One thing to note here: Try to construct your view in the AndroidView viewBlock. Don’t save or remember direct references to views outside of AndroidView.

If you want to embed XML layouts, you need to use AndroidViewBinding. This should not be difficult. Make! Should be enough for use.

@Composable
fun AndroidViewBindingExample(a) {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}
Copy the code

Asynchronous operation in Compose

Compose provides mechanisms for performing asynchronous operations from composables.

For callback-based apis, you can use a combination of MutableState and onCommit(). Use MutableState to store the results of the callback and reorganize the affected interface when the results change. Whenever a parameter changes, onCommit() is used to perform the operation. If the composition of the interface is finished before the operation is complete, you can also define onDispose() method to clean up all pending operations. The following example shows how these apis work together.

@Composable
fun fetchImage(url: String): ImageBitmap? {
    var image byremember(url) { mutableStateOf<ImageBitmap? > (null) }

    onCommit(url) {
        val listener = object : ExampleImageLoader.Listener() {
            override fun onSuccess(bitmap: Bitmap) {
                image = bitmap.asImageBitmap()
            }
        }

        val imageLoader = ExampleImageLoader.get()
        imageLoader.load(url).into(listener)

        onDispose {
            imageLoader.cancel(listener)
        }
    }
    return image
}
Copy the code

If the asynchronous operation is a suspend function, use LaunchedEffect instead:

suspend fun loadImage(url: String): ImageBitmap = TODO()

@Composable
fun fetchImage(url: String): ImageBitmap? {
    var image byremember(url) { mutableStateOf<ImageBitmap? > (null) }

    LaunchedEffect(url) {
        image = loadImage(url)
    }

    return image
}
Copy the code

Summary of interoperability

Interoperation is just calling each other, whatever you want to call it, this is important… Let’s take a look at the inescapable library.

Compose and other libraries

Compose and ViewModel Navigation Hilt Paging can both be used together. There is no generation gap at all…

The ViewModel and Navigation mentioned above have been used before, so I don’t need to elaborate on it here. If you need to read it, you can read my previous article.

Hilt Paging is a library that I have not used before, and Hilt Paging is a library that I have written about before, but why not? Because it’s still alpha and unstable…

The next one needs to be said.

Image loading frame

Now if you ask an Android developer: What image do you use to load frames? More than 90 percent will definitely say Glide without hesitation.

However, it seems that the official preference is Coil, and the official Demo also uses Coil, which can be directly used in Compose, but the dependency needs to be added first:

implementation "Dev. Chrisbanes. Accompanist: accompanist - coil: 0.6.0"
Copy the code

Next, take a look at the use method:

@Composable
fun MyExample(a) {
    CoilImage(
        data = "https://picsum.photos/300/300",
        loading = {
            Box(Modifier.fillMaxSize()) {
                CircularProgressIndicator(Modifier.align(Alignment.Center))
            }
        },
        error = {
            Image(painterResource(R.drawable.ic_error), contentDescription = "Error")})}Copy the code

How about, is it very simple, when using directly call, of course, can also be implemented by themselves, or write a Composable call Glide to implement it is also ok.

But the Coil is lighter, so it’s up to you!

We practice

As the saying goes: keep troops for a thousand days, use troops for a while. After learning, you should use it, even if you write a Demo! At least it’s better than just reading and not writing. Some people may say, this thing doesn’t need to look at, when using it can’t be checked! This I have to stop you, that is you, my brain is not good, or write to deepen the impression of the good!

So let’s write the jump login page first.

Have you forgotten what you look like? Let me show you some more:How’s that๏ผŸ Is it still pretty good, ha ha ha!

Home page ViewModel is used

GIF above also has the appearance of the home page, the data is still provided by playing Android, because there is already a ViewModel before, so directly use it:

val viewModel: HomePageViewModel = viewModel()
val result by viewModel.state.observeAsState(PlayLoading)
Copy the code

I forgot to mention that the argument in observeAsState means what the default value is, you can write it or you can’t write it, and the default value is what you write, and the default value is null if you don’t write it, depending on the case.

I actually changed my ViewModel a little bit. The return value of LiveData in my ViewModel was Result, but… Note that there is a bug in the beta version, if you use Result, there will be an error, so try not to use Result as return value!!

So I changed Result to a custom sealed class:

sealed class PlayState
object PlayLoading : PlayState()
data class PlaySuccess<T>(val data: T) : PlayState()
data class PlayError(val e: Throwable) : PlayState()
Copy the code

Very simple, three states, loading, loading success, loading failure, different states display different layouts.

Next we need to call the method that loads the data:

viewModel.getArticleList(1.true)
Copy the code

I haven’t loaded more data for the moment, but I will do it later when I have time, because there is no ready-made control in Compose, so I need to customize it, or I can use the original native control directly, but I always feel that it is not good to use the original control after using Compose. I will not use the native control until I have to. However, the article details page in the next article will have to use a native WebView because there is no ability to customize it.

Let’s look at State in action:

Column(modifier = Modifier.fillMaxSize()) {
    PlayAppBar(stringResource(id = R.string.home_page), false)
    when (result) {
        is PlayLoading -> {
            LoadingContent()
        }
        is PlaySuccess<*> -> {
            val data = result as PlaySuccess<List<Article>>
            LazyColumn(modifier) {
                itemsIndexed(data.data) { index, article ->
                    ArticleItem(
                        article,
                        index,
                        enterArticle = { urlArgs -> enterArticle(urlArgs) })
                }
            }
        }
        is PlayError -> {
            loadState = true
            viewModel.onRefreshChanged(REFRESH_STOP)
            ErrorContent(enterArticle = { viewModel.getArticleList(1.true)})}}}Copy the code

In fact, very simple, with the previous use of the basic same, but the idea of writing code to change.

Before writing a layout you need include if you want to reuse it. Now you can just reuse it. For example, LoadingContent and ErrorContent used in the above code can also be used elsewhere:

@Composable
fun ErrorContent(
    enterArticle: () -> Unit
) {
    Column(
        modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            modifier = Modifier.padding(vertical = 50.dp),
            painter = painterResource(id = R.drawable.bad_network_image),
            contentDescription = "Network loading failed"
        )
        Button(onClick = enterArticle) {
            Text(text = stringResource(id = R.string.bad_network_view_tip))
        }
    }
}
Copy the code

Here I just put down the ErrorContent, LoadingContent is similar to this, because of the length of the code will not be posted, if necessary, you can directly download from Github.

Navigation using

To use this, you need to modify the previous code. The above also introduces the general method of using it, but if you really want to use it, you may still be confused. It is one thing to read it and another to write it.

First, define Destinations:

object MainDestinations {
    const val HOME_PAGE_ROUTE = "home_page_route"
    const val ARTICLE_ROUTE = "article_route"
    const val ARTICLE_ROUTE_URL = "article_route_url"
    const val LOGIN_ROUTE = "login_route"
}
Copy the code

Let’s define the Action we need to use:

/** * Models the navigation actions in the app. */
class MainActions(navController: NavHostController) {
    val homePage: () -> Unit = {
        navController.navigate(MainDestinations.HOME_PAGE_ROUTE)
    }
    val enterArticle: (String) -> Unit = { url ->
        navController.navigate("${MainDestinations.ARTICLE_ROUTE}/$url")}val toLogin: () -> Unit = {
        navController.navigate(MainDestinations.LOGIN_ROUTE)
    }
    val upPress: () -> Unit = {
        navController.navigateUp()
    }
}
Copy the code

It says it has been used ah!

And then finally NavHost:

@Composable
fun NavGraph(startDestination: String = MainDestinations.HOME_PAGE_ROUTE) {
    val navController = rememberNavController()

    val actions = remember(navController) { MainActions(navController) }
    NavHost(
        navController = navController,
        startDestination = startDestination
    ) {
        composable(MainDestinations.HOME_PAGE_ROUTE) {
            Home(actions)
        }
        composable(MainDestinations.LOGIN_ROUTE) {
            LoginPage(actions)
        }
        composable(
            "${MainDestinations.ARTICLE_ROUTE}/ {$ARTICLE_ROUTE_URL}",
            arguments = listOf(navArgument(ARTICLE_ROUTE_URL) { type = NavType.StringType })
        ) { backStackEntry ->
            valarguments = requireNotNull(backStackEntry.arguments) ArticlePage( url = arguments.getString(ARTICLE_ROUTE_URL) ? :"www.baidu.com",
                onBack = actions.upPress
            )
        }
    }
}
Copy the code

A composable is equivalent to our previous Activity or Fragment, I’m going to pass navController directly to the Home and LoginPage, navController is the MainActions defined above, just call it if you want to jump to the page.

See how to jump from my page to the login page:

@Composable
fun ProfilePage(onNavigationEvent: MainActions){
    valScrollState = rememberScrollState() Column(modifier = mysql.fillmaxSize ()) {...... NameAndPosition (onNavigationEvent toLogin)// Pass in the jump event... }}@Composable
private fun NameAndPosition(toLogin: () -> Unit) {
    Column(modifier = if (Play.isLogin) {
        Modifier.padding(horizontal = 16.dp)
    } else {
        Modifier
            .padding(horizontal = 16.dp)
            .clickable { toLogin() } // Jump
    }) {
        Name(
            modifier = Modifier.baselineHeight(32.dp)
        )
        Position(
            modifier = Modifier
                .padding(bottom = 20.dp)
                .baselineHeight(24.dp)
        )
    }
}
Copy the code

Does that make sense to you now?! Ha ha ha ๐Ÿ˜„

Write the article details page

In fact, above we have put the most troublesome WebView to write good, direct call can be:

@Composable
fun ArticleScreen(
    url: String,
    onBack: () -> Unit
) {
    val x5WebView = rememberX5WebViewWithLifecycle()
    Scaffold(
        topBar = {
            PlayAppBar("Article Details", click = {
                if (x5WebView.canGoBack()) {
                    // Return to the previous page
                    x5WebView.goBack()
                } else {
                    onBack.invoke()
                }
            })
        },
        content = {
            AndroidView(
                { x5WebView },
                modifier = Modifier
                    .fillMaxSize()
                    .padding(bottom = 56.dp),
            ) { x5WebView ->
                x5WebView.loadUrl(url)
            }
        },
        bottomBar = {
            BottomBar(
                post = url,
                onUnimplementedAction = { showDialog = true})})}Copy the code

Does this page now look very simple, or use scaffolding, direct slot type, easy and quick.

Above PlayAppBar I have extracted a control, very simple, here will not post code, if you need to go to Github to check, finally look at the article details page prepared to look like it:

A delicate ending

Compose has already written two articles, but feels like there’s a lot more to it, like controls, layouts, themes, lists, animations, etc… Take your time.

All of the above code is in my Github, remember the main branch. Don’t forget to like and follow this article if it helps you. Thank you ๐Ÿ™.