Public number: byte array

Hope to help you 🤣🤣

After two years of work, Google finally released the official version of Jetpack Compose at the end of July. I was interested in many of Jetpack Compose’s features and thought it would become the preferred technology for Android native app development, so I started a trial run ahead of the official release. I spent two months on and off writing an IM APP for Jetpack Compose, which implemented basic chat functions. At the same time, I also wrote an article to introduce it: “Learn not to move, Jetpack Compose to write an IM APP.

Using compose_chat as an example, I went into detail about many of Jetpack Compose’s key concepts and basic features, including:

  • Imperative and declarative
  • Combinable function
  • state
  • State of ascension
  • Pure functions
  • Side effects
  • layout
  • Animation & gesture manipulation
  • The theme

I was talking about the big difference between Jetpack Compose’s new declarative development and the imperative development of the traditional View system, and the changes that this difference brings to the development philosophy. In my opinion, this article is a good guide for those who want to learn Jetpack Compose, and compose_chat is a good way to quickly learn the development process

It’s been a few months now, and in that time I’ve made a lot of improvements to Compose_chat and added a few new features, the biggest of which is that compose_Chat now supports group chat. There are three chat groups in Compose_Chat for those interested to check out

In my last article, I focused on concepts, but I didn’t cover much of the specific code. This article will fill in the details, as well as some of the Jetpack libraries and third-party open source libraries that can be used seamlessly with Jetpack Compose. Because there are still few learning materials about Jetpack Compose, I may make some mistakes in my understanding. If there are any mistakes, I hope readers can point them out and help you 🤣🤣

A, MutableState

Jetpack Compose draws the screen view by nesting multiple composable functions to describe the entire screen State. The only way to update the view is to generate a new input parameter and call the composable function again. The new input parameter is the view State we want to update. Each time the State is updated, the composable functions are triggered to regroup, thereby achieving the UI refresh

In order for the system to be aware that State has been updated, Compose provides an observable type called MutableState

that reorganizes all composable functions that depend on that value when its value changes

@Stable
interface MutableState<T> : State<T> {
    override var value: T
    operator fun component1(a): T
    operator fun component2(a): (T) -> Unit
}
Copy the code

We can create a MutableState using the mutableStateOf method

var nickname = mutableStateOf("")
Copy the code

MutableState

is not the only way to store State. Observable types such as LiveData, Flow, and RxJava are supported and need to be converted to State

in order to automatically trigger recombination when State changes. For example, the LiveData method is observeAsState(), and the Flow method is collectAsState(). Both automatically call state.value when the value changes

@Composable
fun <T> LiveData<T>.observeAsState(a): State<T? > = observeAsState(value)@Composable
fun <T> StateFlow<T>.collectAsState(
    context: CoroutineContext = EmptyCoroutineContext
): State<T> = collectAsState(value, context)
Copy the code

Second, remember

The order of execution, the thread of execution, and even the number of executions of composable functions are highly uncertain. For example, a composable function might be executed N times per second during animation, which would cause the object we declared in the function to be constantly reinitialized and the assigned value to be lost

In order to remember an object value during composition, the remember method is used. The value pointed to by Remember is stored in the composition during initial composition and returned during reorganization to ensure that the value is not lost. Remember can be used to store both mutable and immutable objects

var nickname by remember() {
    mutableStateOf("")}Copy the code

Take compose_chat as an example, the personal data modification page HomeDrawerScreen is entered through OutlinedTextField, at this time, MutableState and Remember need to be used simultaneously

When the user enters a new content, the nickname is changed in the onValueChange callback to trigger the DrawerFrontLayer reorganization. Remember is used to ensure that the nickname is not lost during the reorganization

@Composable
private fun DrawerFrontLayer(
    backdropScaffoldState: BackdropScaffoldState,
    homeScreenDrawerState: HomeScreenDrawerState
) {
    Column() {
        var nickname by remember {
            mutableStateOf(
                homeScreenDrawerState.userProfile.nickname
            )
        }
        CommonOutlinedTextField(
            value = nickname,
            onValueChange = {
                if (it.length > 16) {
                    return@CommonOutlinedTextField
                }
                nickname = it
            },
            label = "nickname")}}Copy the code

Remember is a guarantee that the value will not be lost, but sometimes we want that value to update itself

For example, when the user enters a new content, the nickname is changed, and the changed value is always displayed on the view. However, the user did not save the Settings, so the fake nickname is always displayed, as shown below

What I want to achieve is that when the float is closed, the nickname can be reset to the user’s real nickname instead of dirty data. This can be achieved by adding a key parameter to the Remember method. The remember method supports passing in multiple key parameters and reinitializes itself when a key changes

For example, the following state nickname is displayed in a floating layer backdropScaffoldState. IsRevealed as one of the key, when the supernatant was closing, key from true to false, will trigger a nickname to initialization, Discard the previously entered value

@Composable
private fun DrawerFrontLayer(
    backdropScaffoldState: BackdropScaffoldState,
    homeScreenDrawerState: HomeScreenDrawerState
) {
    Column() {
        var nickname byremember( key1 = backdropScaffoldState.isRevealed, key2 = homeScreenDrawerState ) { mutableStateOf( homeScreenDrawerState.userProfile.nickname ) } CommonOutlinedTextField(  value = nickname, onValueChange = {if (it.length > 16) {
                    return@CommonOutlinedTextField
                }
                nickname = it
            },
            label = "nickname")}}Copy the code

Third, CompositionLocal

Typically, data is transferred between composable functions across the view tree in the form of passing parameters

For example, in order to set the text content and text color that TextLabel will display, ChatApp needs to actively pass explicit parameter values to it. This form of data transfer also conforms to Jetpack Compose’s coding principle: a qualified composable function should be pure, idempotent, and have no side effects

@Composable
fun ChatApp(a) {
    val colors = lightColors()
    TextLabel(labelText = "compose_chat", color = colors.primary)
}

@Composable
fun TextLabel(labelText: String, color: Color) {
    Text(
        text = labelText,
        color = color
    )
}
Copy the code

But this approach can be cumbersome in some situations

For example, an application theme is a clear specification, including the text size, color, text shadows, rounded corner radian, etc., the global unification and change the class attribute value of the probability is very low, if the call each combination function need to explicitly pass these attribute values, then makes the function call cost is very high

Jetpack Compose addresses this situation by providing CompositionLocal to implicitly pass data down through composition. The CompositionLocal element is provided as a value at a node in the view tree for implicit use by all nested composable functions without having to explicitly declare CompositionLocal as a parameter in composable functions. And each composable function uses its nearest CompositionLocal element

Take the official code for example. LocalContentAlpha is used to define the Alpha value of the embedded content, and Column is divided by CompositionLocal into three different Alpha levels: default transparency, Medium, and Disabled

@Composable
fun CompositionLocalExample(a) {
    MaterialTheme {
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    Text("This Text uses the disabled alpha now")}}}}}Copy the code

The final text content contains three different levels of transparency

The theme of Jetpack Compose is also implemented via CompositionLocal. For example, compose_chat, whose theme is defined by ChatTheme, contains three basic theme attributes, namely: Colors, typography, and shapes are automatically reflected in all components used to build the application. Composable functions can also proactively read properties assigned by the upper view

And the entire application package in ChatTheme internal, internal combination function can pass MaterialTheme. Shapes. The large way to directly obtain ChatTheme defined in the attribute value, avoids the explicit arguments

@Composable
fun ChatTheme(
    appTheme: AppTheme,
    content: @Composable() - >Unit
) {
    val colors = ...
    val typography = ... 
    val shapes = ...
    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}

ChatTheme {
    Text(
        text = "compose_chat",
        modifier = Modifier.clip(shape = MaterialTheme.shapes.large),
        color = MaterialTheme.colors.secondary,
        style = MaterialTheme.typography.body1,
    )
}
Copy the code

Looking at the source of the MaterialTheme method, you can see that the colors we pass are stored in LocalColors, which is what the MaterialTheme. Colors references. By wrapping the highest level composable functions representing the entire application in the MaterialTheme, all nested composable functions implicitly contain a unified set of topics, that is, avoiding explicit parameter passing and making it easy to adjust the theme configuration of the entire application

@Composable
fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable() - >Unit
) {
    val rememberedColors = remember {
        // Explicitly creating a new object here so we don't mutate the initial [colors]
        // provided, and overwrite the values set in it.
        colors.copy()
    }.apply { updateColorsFrom(colors) }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColors)
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography
    ) {
        ProvideTextStyle(value = typography.body1) {
            PlatformMaterialTheme(content)
        }
    }
}

internal val LocalColors = staticCompositionLocalOf { lightColors() }

object MaterialTheme {

    val colors: Colors
        @Composable
        @ReadOnlyComposable
        get() = LocalColors.current

    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current

    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current
}
Copy the code

In our project, we can follow the MaterialTheme approach by providing custom composable functions with access to certain system resources, such as the LocalInputMethodManager below, which can be used to access the system keyboard

val LocalInputMethodManager = staticCompositionLocalOf<InputMethodManager> {
    error("CompositionLocal InputMethodManager not present")}@Composable
fun ProvideInputMethodManager(content: @Composable() - >Unit) {
    val context = LocalContext.current
    val inputMethodManager = remember {
        context.getSystemService(
            Context.INPUT_METHOD_SERVICE
        ) as InputMethodManager
    }
    CompositionLocalProvider(LocalInputMethodManager provides inputMethodManager) {
        content()
    }
}

@Composable
fun ChatScreen(a) {
    ProvideInputMethodManager {
        val inputMethodManager = LocalInputMethodManager.current
        //TODO}}Copy the code

Fourth, the ViewModel

In Jetpack Compose, our business processing logic should all be in the ViewModel. The composable function should not contain any business processing logic and should only update the view based on changes in the input parameter

In order to be able to create or retrieve an existing ViewModel instance in a composable function, a necessary prerequisite is that the current composable function should have an instance of ViewModelStoreOwner. See another article on the relationship between the two: Jetpack (6) -ViewModel

To put it simply, ViewModel needs to rely on ViewModelStoreOwner to create and store, all ViewModel instances need to be saved in ViewModelStoreOwner, details can go to see my source code interpretation article, here is no longer repeated

The Compose package provides an extension to the ViewModel function, ViewModel, which makes it easy to retrieve ViewModel instances in composable functions, as shown below

class LoginViewModel() : ViewModel()

@Composable
fun LoginScreen(navController: NavHostController) {
    val loginViewModel = viewModel<LoginViewModel>()
}
Copy the code

The viewModel method also relies on CompositionLocal to obtain the ViewModelStoreOwner instance. Compose automatically assigns a value to LocalViewModelStoreOwner when the Activity is created. The default value is the ViewModelStoreOwner associated with the Activity

@Composable
public inline fun <reified VM : ViewModel> viewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    factory: ViewModelProvider.Factory? = null
): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory)

@Composable
public fun <VM : ViewModel> viewModel(
    modelClass: Class<VM>,
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    factory: ViewModelProvider.Factory? = null
): VM = viewModelStoreOwner.get(modelClass, key, factory)
Copy the code

In addition, if we want to get is the ViewModel instance of structure parameters, then need to take the initiative to the incoming ViewModelProvider. Factory parameters, just like the following

class ChatViewModel(private val chat: Chat) : ViewModel()

@Composable
fun ChatScreen(chat: Chat) {
    val chatViewModel = viewModel<ChatViewModel>(factory = object :
        ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return ChatViewModel(chat = chat) as T
        }
    })
}
Copy the code

This works perfectly, but there’s template code, because most of the time we’re using the NewInstanceFactory, and we need to do type enforcement. The only difference is that we instantiate the ViewModel, and we can wrap an extension function around the ViewModel method. Simplify the code

@Composable
inline fun <reified VM : ViewModel> viewModelInstance(crossinline create: () -> VM): VM =
    viewModel(factory = object : ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return create() as T
        }
    })
Copy the code

Ultimately, you only need to declare the process of instantiating the ViewModel, saving a lot of code

@Composable
fun ChatScreen(
    navController: NavHostController,
    listState: LazyListState,
    chat: Chat
) {
    val chatViewModel1 = viewModelInstance {
        ChatViewModel(chat = chat)
    }
}
Copy the code

Fifth, OnBackPressedDispatcher

OnBackPressedDispatcher is androidx. Activity: the activity in the package provides a set of mechanisms used to implement interception return key events, the Jetpack is also applied in the composer

Take compose_chat as an example, HomeScreen provides a floating popover HomeScreenSheetContent for adding friends and joining groups in the main interface. When the floating popover is visible, Because HomeScreen and HomeScreenSheetContent are one, when you click the Back button, the entire Activity exits directly

What I want is: when the suspension popover is visible, clicking the back button will only make the suspension popover hide, and clicking the back button will allow you to exit the Activity directly when it is not visible

The OnBackPressedDispatcher, which is the BackHandler method in the Compose package, is used to proactively intercept the return event

When HomeScreenSheetContent in visible, namely modalBottomSheetState isVisible to true, will return to intercept events, Put HomeScreenSheetContent step by step in the onBack callback to the Hidden state, and unblock HomeScreenSheetContent when it is Hidden

@Composable
fun HomeScreenSheetContent(
    homeScreenSheetContentState: HomeScreenSheetContentState.) {
    val coroutineScope = rememberCoroutineScope()
    
    fun expandSheetContent(targetValue: ModalBottomSheetValue) {
        coroutineScope.launch {
            homeScreenSheetContentState.modalBottomSheetState.animateTo(targetValue = targetValue)
        }
    }

    BackHandler(enabled = homeScreenSheetContentState.modalBottomSheetState.isVisible, onBack = {
        when (homeScreenSheetContentState.modalBottomSheetState.currentValue) {
            ModalBottomSheetValue.Hidden -> {

            }
            ModalBottomSheetValue.Expanded -> {
                expandSheetContent(targetValue = ModalBottomSheetValue.HalfExpanded)
            }
            ModalBottomSheetValue.HalfExpanded -> {
                expandSheetContent(targetValue = ModalBottomSheetValue.Hidden)
            }
        }
    })
}
Copy the code

Is really can see out from source BackHandler method for OnBackPressedDispatcherOwner register a callback to inform, when enabled for true responsible for incoming call our onBack method

@Composable
public fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)
    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        object : OnBackPressedCallback(enabled) {
            override fun handleOnBackPressed(a) {
                currentOnBack()
            }
        }
    }
    // On every successful composition, update the callback with the `enabled` value
    SideEffect {
        backCallback.isEnabled = enabled
    }
    val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
        "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
    }.onBackPressedDispatcher
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(lifecycleOwner, backCallback)
        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}
Copy the code

Six, Effect of the API

Many articles and books on programming best practices recommend a coding principle: design programs using Val, immutable objects, and pure functions whenever possible. This principle also applies to Jetpack Compose, because a qualified composable function should be pure, idempotent, and have no side effects

What is a pure function? A function is called pure if it always gets the same result when executed repeatedly with the same input arguments, without affecting any external state or worrying about external changes

Pure functions have no side effects and referential transparency. A side effect is a modification of something somewhere, for example:

  • Referenced or modified the value of an external variable
  • IO operations, such as reading and writing SharedPreferences, are performed
  • UI operations are performed, such as changing the actionable state of a button

The following example is not a pure function. Due to the influence of external variables, the count function executed multiple times with the same input parameter may not all have the same result, and each execution will affect the external environment (using println). These are side effects

var count = 1

fun count(x: Int): Int {
    count += x
    println(count)
    return count
}
Copy the code

Note when using Jetpack Compose:

  • Composable functions can be executed in any order
  • Composable functions can be executed in parallel
  • Recombination skips as many composable functions and lambdas as possible
  • Restructuring is an optimistic move and may be cancelled
  • Composable functions may run as frequently as each frame of an animation

But in some cases, composable functions cannot be completely side-effect-free. For example, if we want the status bar to change the background color when switching the application theme, then the composable function has a side effect. To handle this situation, Jetpack Compose provides an Effect API to execute these spin-off effects in a predictable manner and avoid over-executing them in an unsafe form

Here are some of the more common Effect apis

LaunchedEffect

LaunchedEffect is used to safely call coroutines in composable functions. When LaunchedEffect enters a composition, it launches a coroutine via coroutinescope.launch and passes the developer’s declared code block to the coroutine as a parameter for execution. When LaunchedEffect exits the composite, the started coroutine is also cancelled for security. LaunchedEffect also supports binding to multiple keys, canceling existing coroutines and restarting them when a key changes

Compose_chat uses LaunchedEffect in several places. For example, in HomeActivity, LaunchedEffect launches a coroutine to monitor the current account status serverConnectState. The login page is automatically redirected when the account is deactivated

class HomeActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            val appViewModel = viewModel<AppViewModel>()
            val navController = rememberAnimatedNavController()
            LaunchedEffect(key1 = Unit) {
                withContext(Dispatchers.Main) {
                    appViewModel.serverConnectState.collect {
                        when (it) {
                            ServerState.KickedOffline -> {
                                showToast("This account has been logged in to another client, please log in again")
                                AccountCache.onUserLogout()
                                navController.navToLogin()
                            }
                            ServerState.Logout -> {
                                navController.navToLogin()
                            }
                            else -> {
                                showToast("Connect State Changed : $it")}}}}}}}}Copy the code

rememberCoroutineScope

Since LaunchedEffect is a composable function, we can’t use it outside of a composable function. To safely use coroutines outside of composable functions, you need the rememberCoroutineScope method. The rememberCoroutineScope method returns a CoroutineScope. This scope has a lifetime limit and all started coroutines are cancelled when the composable function exits the composition. To ensure safety

For example, compose_chat uses the rememberCoroutineScope method in the group profile page GroupProfileScreen. The quitGroup callback is a common lambda method, LaunchedEffect cannot be used directly, so the suspend method in the GroupProfileViewModel is invoked via coroutineScope in the callback

@Composable
fun GroupProfileScreen(
    navController: NavHostController,
    groupId: String
) {
    val groupProfileViewModel = viewModelInstance {
        GroupProfileViewModel(groupId = groupId)
    }
    val coroutineScope = rememberCoroutineScope()
    Scaffold(
        modifier = Modifier
            .fillMaxSize(),
    ) {
        Box {
            GroupProfileScreenTopBar(quitGroup = {
                coroutineScope.launch {
                    when (val result = groupProfileViewModel.quitGroup()) {
                        ActionResult.Success -> {
                            showToast("Removed from group chat")
                            navController.navToHomeScreen()
                        }
                        is ActionResult.Failed -> {
                            showToast(result.reason)
                        }
                    }
                }
            })
        }
    }
}
Copy the code

snapshotFlow

SnapshotFlow is used to convert any block of code containing a return value into a cold flow, which emits a new value to the collector when a change is detected

For example, compose_chat’s sidebar HomeScreenDrawer uses snapshotFlow to automatically turn off the DrawerFrontLayer, the hover window for modifying a user’s profile. When it is determined that HomeScreenDrawer is turned off and DrawerFrontLayer is displayed, the DrawerFrontLayer is actively hidden

@Composable
fun HomeScreenDrawer(homeScreenDrawerState: HomeScreenDrawerState) {
    val drawerState = homeScreenDrawerState.drawerState
    val scaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Revealed)

    LaunchedEffect(key1 = Unit) { snapshotFlow { ! drawerState.isOpen && scaffoldState.isConcealed }.collect { scaffoldState.reveal() } } BackdropScaffold( scaffoldState =  scaffoldState, frontLayerContent = { DrawerFrontLayer( backdropScaffoldState = scaffoldState, homeScreenDrawerState = homeScreenDrawerState ) } ) }Copy the code

rememberUpdatedState

SideEffect

DisposableEffect

  • RememberUpdatedState. If you want to capture a value in an effect that will not cause the effect to restart if the value changes, you can do so using a rememberUpdatedState
  • SideEffect. If you need to share the Compose state with non-compose managed objects, you can use SideEffect to do so because this composable item is called every time a successful reorganization occurs
  • DisposableEffect. If a spin-off effect needs to be disposed of after a key has changed or a composable item has left the DisposableEffect, you can do so with the DisposableEffect

Compose’s BackHandler method uses all three of these methods:

  • The effect simply needs to have its latest value. Even if that value changes, it will not affect the current effect execution, so capture the onBack callback with a rememberUpdatedState. To avoid the derestart effect. If you do not use a rememberUpdatedState, you will need to use onBack as the incidental effect key, which will result in the need for a restart effect whenever the onBack changes
  • The external enabled parameter passed in when the BackHandler method is called may be passed in based on some logical expression and may change at any time, while the SideEffect is executed on each reorganization, BackHandler therefore uses SideEffect to ensure that OnBackPressedCallback points to the latest enabled state in real time
  • DisposableEffect provides the onDispose callback, which is called when a key changes or a composable item exits a combination, so it can automatically remove listening to the OnBackPressed event by DisposableEffect. Note that onDispose statement must be added to DisposableEffect, and must be last, otherwise the compiler will report an error
@Composable
public fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)
    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        object : OnBackPressedCallback(enabled) {
            override fun handleOnBackPressed(a) {
                currentOnBack()
            }
        }
    }
    // On every successful composition, update the callback with the `enabled` value
    SideEffect {
        backCallback.isEnabled = enabled
    }
    val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
        "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
    }.onBackPressedDispatcher
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(lifecycleOwner, backCallback)
        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}
Copy the code

Seven, Coroutines

Kotlin coroutines are also used a lot in compose_chat, especially when sending messages

When it is used to click send message, a TextMessage will be constructed, and the state of the TextMessage has three possibilities: sending, sending success, sending failure. To avoid the need for the upper-level business logic to asynchronously retrieve the results of the underlying IM SDK through multiple callbacks, compose_CHAT synchronously completes the process through a Channel

The specific message Sending logic of IM SDK corresponds to MessageProvider. Before formally Sending messages, text is constructed into a TextMessage with Sending status, and the upper view can obtain the message first for UI display. The message is then formally sent to the IM SDK and the final status (success or failure) of the message is called back through the Channel

class MessageProvider : IMessageProvider.Converters {

    override suspend fun send(chat: Chat, text: String, channel: Channel<Message>) {
        val id = chat.id
        val messageId = RandomUtils.generateMessageId()
            msgId = messageId, state = MessageState.Sending,
            timestamp = V2TIMManager.getInstance().serverTime, msg = text,
            sender = AppConst.personProfile.value,
        )
        channel.send(sendingMessage)
        val callback = object : V2TIMValueCallback<V2TIMMessage> {
            override fun onSuccess(t: V2TIMMessage) {
                coroutineScope.launch {
                    val msg = convertMessage(t) as? TextMessage
                    // Send the final result to channel
                    if (msg == null) {
                        channel.send(sendingMessage.copy(state = MessageState.SendFailed))
                    } else {
                        channel.send(msg)
                    }
                    // Close the channel, indicating the end of the callback
                    channel.close()
                }
            }

            override fun onError(code: Int, desc: String?). {
                coroutineScope.launch {
                    // Send the final result to channel
                    channel.send(
                        sendingMessage.copy(state = MessageState.SendFailed).apply {
                            tag = "code: $code desc: $desc"
                        })
                    // Close the channel, indicating the end of the callback
                    channel.close()
                }
            }
        }
        when (chat) {
            is Chat.C2C -> {
                V2TIMManager.getInstance().sendC2CTextMessage(text, id, callback)
            }
            is Chat.Group -> {
                V2TIMManager.getInstance()
                    .sendGroupTextMessage(text, id, V2TIMMessage.V2TIM_PRIORITY_HIGH, callback)
            }
        }
    }

}
Copy the code

The ChatViewModel only needs to send text to the MessageProvider, and the subsequent Construction process and sending logic of TextMessage need not be concerned. Instead, the UI state of the messageChannel value needs to be updated synchronously

class ChatViewModel(private val chat: Chat) : ViewModel() {

    fun sendMessage(text: String) {
        viewModelScope.launch {
            val messageChannel = Channel<Message>()
            launch {
                ComposeChat.messageProvider.send(
                    chat = chat,
                    text = text,
                    channel = messageChannel
                )
            }
            var sendingMessage: Message? = null
            for (message in messageChannel) {
                when (message.state) {
                    MessageState.Sending -> {
                        sendingMessage = message
                        attachNewMessage(newMessage = message, mushScrollToBottom = true)
                    }
                    MessageState.Completed, MessageState.SendFailed -> {
                        valsending = sendingMessage ? :return@launch
                        val sendingMessageIndex =
                            allMessage.indexOfFirst { it.msgId == sending.msgId }
                        if (sendingMessageIndex > -1) {
                            allMessage[sendingMessageIndex] = message
                            refreshViewState()
                        }
                        if (message.state == MessageState.SendFailed) {
                            (message.tag as? String)? .let { showToast(it) } } } } } } } }Copy the code

For a primer on Kotlin coroutines, see my other article: a quick primer on Kotlin coroutines

Eight, Coil

Glide is one of the most common image loading frameworks used by most developers, but for Jetpack Compose, Glide is not an option. Jetpack Compose Support · Issue #4459 · BumpTech/Glide

In order to load network images in Jetpack Compose and support the necessary memory and disk caching capabilities, we can choose another image-loading framework: Coil

Coil implements all of Glide’s features and also supports Jetpack Compose. Coil is relatively new to Glide, with version 1.0 being released in October 2020. If you’re already using Jetpack, Kotlin Coroutines, and OkHttp in your projects, it’s a better fit

A simple comparison between Coil and Glide:

  1. Implementation language
    • Glide is implemented entirely in the Java language and is about as friendly to Java as it is to Kotlin
    • Coil is fully implemented in Kotlin, which is much more Kotlin friendly, with multiple extension functions declared for ImageView to load images
  2. Network request
    • Glide uses HttpURLConnection by default, but it also provides an entry point to change the way network requests are implemented
    • The Coil uses OkHttp by default, but it also provides an entry point to switch network request implementations
  3. Lifecycle monitoring
    • Glide does this by injecting a UI-less Fragment into an Activity or Fragment
    • The Coil listens directly through Lifecycle
  4. Memory cache
    • Glide’s MemoryCache is divided into ActiveResources and MemoryCache
    • The Coil’s memory cache is classified into WeakMemoryCache and StrongMemoryCache, which is essentially the same as Glide
  5. Disk cache
    • Glide in the image after loading by DiskLruCache to the disk cache, and provides whether to cache, whether to cache the original picture, whether to cache the converted picture and so on
    • Coil implements disk caching through OkHttp’s network request caching mechanism. The disk caching only applies to the original images loaded through network requests, and does not cache images from other sources or converted images
  6. Web caching
    • Glide does not exist
    • The Coil, which has a network cache over Glide, can be used to force a local cache instead of a network load (an error will occur if the local cache does not exist, of course)
  7. Thread framework
    • Glide uses a native ThreadPoolExecutor for background tasks and a Handler for thread switching
    • Coil uses Coroutines for background tasks and thread switching

It is also easy to use the Coil to load images. You only need to pass in the required data, which can be HttpUrl, Uri, or File

@Composable
fun CoilImage(
    modifier: Modifier = Modifier,
    data: Any,
    contentScale: ContentScale = ContentScale.Crop,
    builder: ImageRequest.Builder. () - >Unit= {}, {
    val imagePainter = rememberImagePainter(data = data, builder = builder)
    Image(
        modifier = modifier,
        painter = imagePainter,
        contentDescription = null,
        contentScale = contentScale,
    )
}
Copy the code

For more on the source code, see my article, which I wrote shortly after the launch of Coil 1.0: Tripartite Library Source Notes (13) – probably the first source analysis of The entire network

Nine, the end

That’s the end of Jetpack Compose and compose_chat. If you’re done reading these two articles, you should be able to get started at 🤣🤣

Compose_chat’s GitHub address: 🎁🎁🎁 For an IM APP with Jetpack Compose

🎁🎁🎁 Implement an IM APP with Jetpack Compose

The free VERSION of Tencent CLOUD IM SDK can only register 100 accounts at most. The number of compose_chat users of version 1.0 is already full, so I changed a new AppId for version 2.0. If you cannot register, you can use the following accounts that I have pre-registered, but if you log in to multiple devices at the same time, you will be disconnected from each other

  • Google
  • Android
  • Jetpack
  • Compose
  • Flutter
  • Kotlin
  • Java
  • Dart
  • ViewModel
  • LiveData