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:
- 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
- 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
- Lifecycle monitoring
- Glide does this by injecting a UI-less Fragment into an Activity or Fragment
- The Coil listens directly through Lifecycle
- 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
- 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
- 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)
- 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
- Android
- Jetpack
- Compose
- Flutter
- Kotlin
- Java
- Dart
- ViewModel
- LiveData