“Live up to the time, the creation of non-stop, this article is participating in 2021 year-end summary essay competition”
preface
Jetpack Compose officially released version 1.0 at the end of July 2021. In mid-August, just in time for the company’s overseas project restructuring, it took the initiative to apply for the development opportunity to the leader. Because I had been focusing on Compose, I threatened to use Compose for the entire UI (which I did).
The original project was based on Java+ vp ++ architecture in 2017, but this time, it was all over again, based on Kotlin+MVVM/MVI+Jetpack++ architecture.
The technical points involved in this project reconstruction are as follows:
- Kotlin
Coroutines, Flow, Coil (Image loading library), Moshi (Json parsing library)
- Jetpack
Compose, FIRST-chair IST, Paging, ViewModel, Lifecycle, Room, Hilt, LiveData, ViewBinding, ConstraintLayout
- architecture
The MVVM, MVI
- other
Retrofit, ViewPager2, ARouter, MMKV, Firebase SDK, Google SDK, Facebook SDK, AWS SDK, etc
The above is basically the knowledge point involved in the development, several of which marked the underlined, this is because the later development found that their role is becoming smaller and smaller, basically can be completely removed.
The details are coming up, starting with Compose, which has completely changed our UI development experience. In addition, many problems have been encountered in the use, so this paper will introduce my solution in the project, and hope that I can go to a higher level with your suggestions.
Compose articles
You’ve probably read some of the various articles about Compose. This article will not focus on the use of Compose. Instead, it will focus on analyzing and solving some of the problems encountered in the actual development of Compose.
Note that the Compose project is not composed with a single Activity, and does not use Navigation to handle the Compose Navigation problem, so those expecting a full Compose implementation will be disappointed.
TextField And Keyboard
I’m sure you’ve all dealt with some of the weird EditText and Keyboard issues in the View system, as well as in Compose. The simplest implementation of a TextField is as follows:
TextField(
value = "This is TextField",
onValueChange = {}
)
Copy the code
It renders like this:
As you can see, the TextField simply doesn’t fit most UI requirements, and its customizability is almost zero. When we change its Shape to a rounded corner and force the height of the TextField, it will render something like this:
TextField, TextStyle, TextFieldColors, Shape, etc. TextFieldLayout: MinHeight = 56.dp
Therefore, to sum up, we strongly recommend unified customization of TextField using BasicTextField to meet the needs of project UI. As for how to customize, online articles have been a lot, here is no longer redundant.
OK, style issues aside, there’s the keyboard issue.
When we create the chat page, the input box is at the bottom of the screen, and when we pop up the keyboard, we will encounter problems, as shown below:
The keyboard will be aligned to the bottom of the text in the input box, which we definitely don’t want. The normal thing is to display the rounded rectangle completely. At this point in the listing file set soft keyboard mode for android: windowSoftInputMode = “adjustResize” can (and XML). When your phone has a navigation bar or below the input box need to add other UI, you can refer to the sections below Insets rely on libraries provide relevant modifiers, such as imePadding (), navigationBarsWithImePadding () optimization.
After setting up the TextField, it will display normally. But I’m just too busy, popping up the keyboard when I want to get to the page and expecting the user to type. Click on the non-input field to hide the keyboard, so we can use the following method to get focus when the input field is displayed:
val focusRequester = FocusRequester()
LaunchedEffect(key1 = Unit. block = { focusRequester.requestFocus() }) TextField( modifier = Modifier .focusRequester(focusRequester = focusRequester) .onFocusChanged {} .onFocusEvent {} )Copy the code
The code has been simplified so that we first need to create a FocusRequester object and pass it to the FocusRequester operator. LaunchedEffect is triggered to request focus when the compositing function first enters the compositing function, so the TextField gets focus and the keyboard pops up (see section 8, Side-Effects for LaunchedEffect and so on).
So what happens when the user wants to hide the keyboard? Using LocalFocusManager:
val localFocusManager = LocalFocusManager.current
TextField(
onValueChange = {
if (it == "sss") {
localFocusManager.clearFocus()
}
},
)
Copy the code
As shown above, when we enter in the TextField SSS trigger condition, after LocalFocusManager. ClearFocus () clears the focus, the keyboard will be synchronized to hide, effect as shown below:
2. LazyVerticalGrid And Paging
In addition to LazyRow and LazyColumn, Compose also provides a LazyVerticalGrid for implementing a list of tables. So there is no problem with the use of Swipe to Refresh which is similar to LazyColumn and strings IST.
I believe that in the era of XML, we must be RecyclerView, Adapter dominated, plus pull refresh, pull up load, code is pulled a whole body. In Compose, however, the amount of code needed to develop such a situation drops dramatically and efficiency increases dramatically!! The following are simple examples of obtaining Paging data locally and remotely using Paging dependency libraries:
- Local paging data
For example, Room supports DataSource.Factory<Int, T> pagination as follows:
@Query("SELECT * FROM message ORDER BY timestamp DESC")
fun queryMessageList(a): DataSource.Factory<Int, MessageEntity>
Copy the code
Then we’ll use the **Pager<Key: Any, Value: Any> ** class which returns data of type Flow<PagingData> :
protected fun <T : Any> pageDataLocal(
dataSourceFactory: DataSource.Factory<Int, T>
): Flow<PagingData<T>> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = dataSourceFactory.asPagingSourceFactory()
).flow
Copy the code
Compose then converts the Flow<PagingData> data into LazyPagingItems for use by LazyColumn, LazyRow, or LazyVerticalGrid, as provided in the Paging – Compose dependency. The entire basic list of chat messages might look something like this:
val messageList = vm.messageList.collectAsLazyPagingItems()
LazyColumn {
items(messageList) {
//your item content}}Copy the code
- Remote paging data
Of course, there is also a lot of list data that needs to be requested from the server, so implementing this is a bit more complicated. Factory<Int, T>, we have to inherit the PagingSource to process the data. The pseudo-code is as follows: Focus on the load() function:
abstract class BasePagingSource<T : Any> : PagingSource<Int, T>() {
/ /... Leave out the rest
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
return try {
// Next page of data, such as business starting from page 1
valnextPage = params.key ? :1
// Get the request result
val apiResponse = apiPage(nextPage)
/ / the total number of pages
val totalPage = apiResponse.result.totalPage
// If not empty
LoadResult.Page(
data = listData,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (nextPage < totalPage) nextPage + 1 else null)}catch (e: Exception) {
LoadResult.Error(e)
}
}
// The exposed method of obtaining server data
abstract suspend fun apiPage(pageNumber: Int): ApiResponse<PageResult<T>>
}
Copy the code
The server needs to provide us with some basic information, such as the total number of pages of the data, the current number of pages and other information. In addition, we should pay attention to the normalization of the data, such as whether the data is null or emptyList when the data in the list is empty. **Pager<Key: Any, Value: Any> **Pager<Key: Any, Value: Any>
protected fun <T : Any> pageDataRemote(
block: suspend (pageNumber: Int) - >ApiResponse<PageResult<T>>
): Flow<PagingData<T>> = Pager(
config = PagingConfig(pageSize = 20)) {object : BasePagingSource<T>() {
override suspend fun apiPage(pageNumber: Int): ApiResponse<PageResult<T>> {
return block(pageNumber)
}
}
}.flow
Copy the code
At this point, the rest of the process is the same again. Overall encapsulation, from the Model layer to the ViewModel layer can almost achieve a few lines of code, V layer depends on the actual UI complexity, it is not too comfortable to use.
How do you implement the drop-down refresh? Swipe to Refresh dependencies in STRINGS IST can be referenced for details, please refer to the official sample and onRefresh() callback interface is provided. You can pull down the refresh function directly by calling the **refresh()** function in the LazyPagingItems class.
3, SystemBar (StatusBar, NavigationBar)
For transparent status bar and immersive status bar, we have a variety of tools in the original View system, and the official Compose provides solutions for us, and strings ist is available for us.
Accompanist is a group of libraries that aim to supplement Jetpack Compose with features that are commonly required by developers but not yet available.
First-chair IST is a collection designed to complement Jetpack Compose’s feature library. (There are some common features in development that we need but Compose does not provide, so we can check whether the STRINGS IST is provided.)
Present STRINGS IST provides such as: Insets, System UI Controller, Swipe to Refresh, Flow Layouts, Permissions, and so on. We only need Insets and System UI Controller.
OK, first talk about the color and icon of the status bar for color control, import dependence on implementation “com. Google. Accompanist: accompanist – systemuicontroller:”, we set the status bar to white color, icon color is black:
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.White,
darkIcons = true)},Copy the code
The display result is as shown in the left figure below. After changing the color of the status to black and the icon to white, the display result is as shown in the right figure below:
What if you want to control the image’s immersion into the status bar? Key here WindowCompat. SetDecorFitsSystemWindows (window, false), so we can make the content area extends to the status bar, and then we give the status bar set transparent color, and use the white icon, then display the results as shown below.
However, there is another problem, that is, the title area also extends to the status bar. Our requirement is that the background of the image extends to the status bar, but the title area should be below the status bar.
Or with the aid of Accompanist, import the Insets dependence: implementation “com. Google. Accompanist: Accompanist – Insets:”, Insets can help us to easily measure the status bar, navigation, the height of the keyboard, etc.
First we need to wrap our composite function with provideo WindowinSets, as shown below:
setContent {
MaterialTheme {
ProvideWindowInsets {
// your content}}}Copy the code
Then we use the Box layout to set up a background image and a status bar with the following pseudocode:
ProvideWindowInsets {
Box(modifier = Modifier.fillMaxSize()) {
//background image
Image()
//content
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
//app bar / title Bar
Text(text = "Compose Title")}}}Copy the code
Note two points: ProvideWindowInsets need to be at the top of the composable function. The composable function in the content area uses the statusBarsPadding() operator. This is the operator provided by Insets, which adds a status bar height to the top of the Column’s content area. The AppBar inside the Column is displayed below the status bar, as shown below:
Of course, there is another way to handle this, using the other status bar operator provided by Insets: statusBarsHeight(), to modify the content area of the pseudocode above:
//content
Column(
modifier = Modifier
.fillMaxSize()
) {
//add a placeholder
Spacer(
modifier = Modifier
.fillMaxWidth()
.statusBarsHeight()
)
//app bar / title Bar
Text(text = "Compose Title")}Copy the code
We can do the same by adding a placeholder Spacer at the top of the content and setting its height to be the height of the status bar. We’ll often need this kind of immersive UI in real development, so there’s no problem with using the operator directly in the first way or adding placeholders in the second way. I prefer the second, adding a switch parameter to control whether the Spacer is displayed or not.
Insets provide operators for navigation bars, keyboards, etc., as follows:
- Modifier.statusBarsPadding()
- Modifier.navigationBarsPadding()
- Modifier.systemBarsPadding()
- Modifier.imePadding()
- Modifier.navigationBarsWithImePadding()
- Modifier.cutoutPadding()
- Modifier.statusBarsHeight()
- Modifier.navigationBarsHeight()
- Modifier.navigationBarsWidth()
4. ComposeView And AndroidView
Although Compose is used for the development of the App’s UI, it is inevitable that the View and Compose will interact with each other during the development. For example, in Fragmen, DialogFragment, the onCreateView() function receives a View type. All we need to do is use ComposeView as follows, and then use Compose in setContent{} :
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
return ComposeView(requireContext()).apply {
setContent {
//your compose ui}}}Copy the code
For example, in Compose, we need to use some control from View system, such as SurfaceView, TextureView, etc., which is not provided by Compose. Therefore, we need to use AndroidView for this method. PlayerView is a video PlayerView that encapsulates TextureView, etc., and creates a corresponding PlayerView via factory. Then update the player and control its on, off, mute, etc. Logic:
AndroidView(
factory = {
PlayerView(it).apply {
initPlayer(player = mediaPlayer)
}
},
update = {
it.play(
url = playUrl,
mute = false
)
},
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(10.dp))
)
Copy the code
5, Preview And Theme
The next step is to Preview the Compose component. Normally, you can use the @Preview annotation on the composable function to Preview the composable function to the view. There is nothing more to say about the composable function Preview.
The first is DarkTheme and LightTheme, which Compose provides us with out-of-the-box theme switching, but we have to follow the MaterialTheme specification, which is a bit limited. So we can implement our own set of specifications in the same way, if necessary, for greater customizability (see the next section: 6, CompositionLocal for details).
ComposeShareTheme We use the ComposeShareTheme provided by Compose to preview the two different themes. The project is named ComposeShareTheme. When the project is created, Compose will help us generate ComposeShareTheme composition functions. It contains some of our theme elements, such as colors, fonts, shapes, etc. We simply use color data to display the theme:
@Composable
fun Test(a) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colors.background)
.padding(all = 16.dp)
) {
Text(
text = "This is text content",
color = MaterialTheme.colors.secondaryVariant,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
}
Copy the code
Open the theme. Kt file under the project Theme folder, which defines the related colors of the DarkTheme and LightTheme. We define background and secondaryVariant parameters in the above two topics as black and white contrasting colors respectively. Then use the Preview annotation, add the uiMode parameter, and note that your content must be wrapped with a ComposeShareTheme, otherwise the theme Preview will not work:
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun PreviewTestNight(a) {
ComposeShareTheme {
Test()
}
}
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Composable
fun PreviewTestLight(a) {
ComposeShareTheme {
Test()
}
}
Copy the code
The actual preview results are as follows:
The preview of combinatorial functions and the switch of dark mode are so simple, the difficulty lies in the standard of a set of theme of our App, without which it is difficult to do anything.
Note about exceptions that cannot be previewed: In the original View system, we may encounter some situations when we customize the View, the IDE may need to add isInEditMode() judgment, so that if it is in the AS preview page, some of the code that causes the preview will not be executed. This allows us to preview the view normally.
In Compose, however, no such feature has been found.
Let’s take a look at a very non-standard example, MMKV, which you probably all use in your projects. If I save a String value with the key name in MMKV, and I have a combinable function that simply displays the value, so I take the value directly in the combinable function and display it through MMKV, the pseudo-code is as follows (never use this in development!!). :
@Composable
fun MmkvSample(a) {
val name: String = MMKV.defaultMMKV().decodeString("name")
Text(text = name)
}
Copy the code
So at this time we went to preview this function, the preview will fail, AS the error message is given: Java. Lang. An IllegalStateException: You should Call MMKV. The initialize () first. Indeed, since MMKV must be initialized in the Application before it can be used, it is not surprising to encounter this error in the AS preview.
Another bad example is using viewModels in Compose. Some viewModels have arguments, such as Repository, and the preview can be wrong.
So the combinatorial functions should only be related to the state, and do not involve any other logic. However, if you really need to, as in the above non-canonical case, it is recommended that you extract logic and wrap it in parameters to distinguish between previews and non-previews, similar to isInEditMode() in the View. If it is a preview case, you can avoid this problem by using mock logic and returning the mock value instead of MMKV.
6, CompositionLocal
Consider the case where we need to implement a view where the red Box contains the text and the outer blue and green boxes have nothing to do with the text at all. Normally, however, we can only pass the text argument from blue, to green, to red.
The pseudocode is shown below (although the above view can be done directly in a composable function, to illustrate some of the complexity of the UI in real business development, we use the following cumbersome nesting of layers) :
@Composable
fun LocalScreen(a) {
BlueBox(text = "Hello")}@Composable
fun BlueBox(text: String) {
Box() {
GreenBox(text = text)
}
}
@Composable
fun GreenBox(text: String) {
Box() {
RedBox(text = text)
}
}
@Composable
fun RedBox(text: String) {
Box() {
Text(text = text)
}
}
Copy the code
At present, there are only three layers. If our widget is at the very bottom, the parameters it needs will have to be passed in. In this case, nodes in the whole view tree that do not need these parameters will also need to help display the definition and pass these parameters, which will be a headache in development.
Compose takes this into account, and the solution is CompositionLocal, which simply allows you to pass parameters implicitly. Look directly at the pseudocode below:
val LocalString = compositionLocalOf { "hello" }
@Composable
fun LocalScreen(a) {
CompositionLocalProvider(LocalString provides "Just Hello") {
BlueBox()
}
}
@Composable
fun BlueBox(a) {
Box() {
GreenBox()
}
}
@Composable
fun GreenBox(a) {
Box() {
RedBox()
}
}
@Composable
fun RedBox(a) {
Box() {
val text = LocalString.current
Text(text = text)
}
}
Copy the code
After the above code runs, the text field will display “Just Hello”, with a few caveats:
- val LocalString = compositionLocalOf { “hello” }
We use the compositionLocalOf API to create a CompositionLocal object and assign it to LocalString (another option is staticCompositionLocalOf);
- CompositionLocalProvider(LocalString provides “Just Hello”)
Use the CompositionLocalProvider API to provide new values for the LocalString object you create.
- LocalString.current
Use the current API to get the value provided by the most recent CompositionLocalProvider;
Using CompositionLocal, it is obvious that BlueBox and GreenBox do not need to be passive in adding text parameters. After providing the corresponding values at the top of the composable function, they can use localString.current directly in RedBox to get the required values.
Although CompositionLocal useful, but Compose don’t suggest we overuse, concrete applicable conditions: please refer to the website developer. The android, Google. Cn/jetpack/com… .
7, Recomposition
Compose is declarative, so if you need to update the content of your View, you need to update the content of your View manually by calling the relevant Setter, which is mandatory, and Compose is declarative. But this doesn’t require us to do anything, and the system will re-call the composable function to draw the view as needed with new data.
Text1 needs to be driven by the state of TIMESTAMP. Text2 directly fixes the parameter as the current timestamp, and then clicks Text3 to change the state value of TIMESTAMP. In this case, what do you think of the data display? :
@Composable
fun RecompositionSample(a) {
val timestamp = remember {
mutableStateOf(0L)
}
Column {
Text(text = "Text1: ${timestamp.value}")
Text(text = "Text2: ${System.currentTimeMillis()}")
Text(text = "Text3: Click To Update Time", modifier = Modifier.clickable {
timestamp.value = System.currentTimeMillis()
})
}
}
Copy the code
Look directly at the following results, does it seem a little strange? Why does Text2’s timestamp update when we click on it? What about Compose’s smart reorganization?
Text2 is “wrapped” in a separate layer, which looks like this:
@Composable
fun RecompositionSample(a) {
val timestamp = remember {
mutableStateOf(0L)
}
Column {
Text(text = "Text1: ${timestamp.value}")
TextWrapper()
Text(text = "Text3: Click To Update Time", modifier = Modifier.clickable {
timestamp.value = System.currentTimeMillis()
})
}
}
@Composable
fun TextWrapper(a) {
Text(text = "Text2: ${System.currentTimeMillis()}")}Copy the code
Text2 timestamp does not change:
This is where it gets confusing. Box, Column, Row, and so on use inline tags. They are all inline functions (inline functions copy the function body to the call) and share the caller scope, so all the direct components in the RecompositionSample are reorganized. When irrelevant Text2 is “encapsulated”, it is equivalent to a layer of isolation. The encapsulated Text is not affected by timestamp status and will no longer participate in the reorganization. If we add an inline tag to TextWrapper, the timestamp will still change after the result is run.
On the principle of reorganization this research is too shallow, there is not too much to share out, but also hope you forgive me. However, we should pay attention to the above example: the complex page should not be easily, according to the function, according to the business to extract the corresponding non-inline combinable functions, to achieve reuse and isolation effect.
8, the Side – effects
A View has a life cycle, such as onAttachedToWindow() and onDetachedFromWindow() for a View. There are! Consider Compose’s side effects in life cycle terms for a moment!
Suppose we have a scenario where each click of the button makes the counter add up. If the counter is 2-5, we add a text to display the current counter number, otherwise remove the text, and the code looks like this:
@Composable
fun SideEffectsSample(a) {
val tag = "SideEffectsSample"
val count = remember {
mutableStateOf(0L)
}
Column {
Button(onClick = { count.value++ }) {
Text(text = "Click To Update")}if (count.value in 2.. 5) {
// The text used to display the counter number
Text(text = "Count :${count.value}")
LaunchedEffect(key1 = true, block = {
Log.e(tag, "LaunchedEffect: ${count.value}")
})
SideEffect {
Log.e(tag, "SideEffect: ${count.value}")
}
DisposableEffect(key1 = Unit, effect = {
Log.e(tag, "DisposableEffect: ${count.value}")
onDispose {
Log.e(tag, "DisposableEffect onDispose: ${count.value}")}})}}}Copy the code
Take a look at the output of the log to see if it meets your expectations:
SideEffectsSample: DisposableEffect: 2
SideEffectsSample: SideEffect: 2
SideEffectsSample: LaunchedEffect: 2
SideEffectsSample: SideEffect: 3
SideEffectsSample: SideEffect: 4
SideEffectsSample: SideEffect: 5
SideEffectsSample: DisposableEffect onDispose: 6
When the counter reaches 2, the text displays, at which point all three effects we added will be executed. Only SideEffect is performed when the counter is accumulated to 3, 4, and 5. When the counter accumulates to 6, the text disappears, DisposableEffect callbacks to onDispose. So it can be roughly understood as:
- LaunchedEffect – Executes when a composable function first enters a composition
- SideEffect – Executes each time a composable function is reassembled
- DisposableEffect- Execute when the composable function first enters the combination, and call onDispose when the composable function exits
Note, of course, that both LaunchedEffect and DisposableEffect require a key. In the previous example we used true and Unit. When this constant is used, these side effects follow the lifecycle of the current call point. If you are using other mutable types of data, these side effects will restart depending on whether the data has changed or not. If you don’t understand, try assigning key to count.value and see what the log output is.
If you understand, go to the official website to check the life cycle and side effects of the article, I believe you will have more harvest, anyway, I feel like every time I read something more learned, what is not quite clear.
Dialog, DropdownMenu
For example, when I receive an emergency notification message, I need to display the Dialog on any page of the App. If I use the Compose state-driven function, It’s a little tricky, and it has to be used within the composable function of Compose, which is very limited.
This is what we used to do in the old View system, when we got the notification message, we got the top-level Activity, and we popover it. For Compose, I don’t think there is any need to change that. Dialog uses the same DialogFragment, but the content of the DialogFragment view is composed. Another advantage of using DialogFragment is that we can use ViewModel, which we will discuss in the following ViewModel.
The DropdownMenu is associated with a large page, so it can be used in the Compose mode.
Android Studio article
1, folder name
Had some UseCase classes are going to unified file to a folder (bag), so going to the folder named [case], as shown above, the folder icon appears inconsistent with other normal folder icon, when placed in the “case” in the folder class when use may encounter all sorts of problems, If the UseCase class involves Hilt, Hilt will also fail to generate related files.
Case is a Java keyword, kotlin when used a lot, switch case is almost forgotten. It makes you laugh.
He who does not cross the Great Wall is not a true man
When accessing the Firebase Crashlytics SDK, the debug package works normally, but the following error is reported when the Release package is connected:
What went wrong:
Execution failed for task ‘:app:uploadCrashlyticsMappingFileXXXRelease’. org.apache.http.conn.HttpHostConnectException: Connect to firebasecrashlyticssymbols.googleapis.com:443 [firebasecrashlyticssymbols.googleapis.com/172.xxx.xxx.xxx] failed: Connection timed out: connect
UploadCrashlyticsMappingFileXXXRelease this task, in the way Firebase Crashlytics SDK need to upload file such as the project after the confusion of the Mapping to Google’s servers, This step needs to deal with Google servers ah, you think, you look, you think. So you have to get AS across the Wall.
First of all, ladders, this is more, let’s talk privately. Add the following configuration to the gradle.properties file to solve the problem:
# proxy address, native127.0. 01.SystemProp. HTTPS. ProxyHost = XXX, XXX, XXX, XXX port # agent, look at you a ladder set port systemProp. HTTPS. ProxyPort = XXXXCopy the code
Another violent solution is to close the Task, which can be used temporarily to open packages or test packages, but never in production:
gradle.taskGraph.whenReady {
tasks.each { task ->
if (task.name.contains("uploadCrashlyticsMappingFile")) {
task.enabled = false}}}Copy the code
Firebase Crashlytics integration retread pits
Architecture article
1. MVVM and MVI
Speaking of the original MVP architecture, the P layer holds the V layer, so you need to pay attention to the life cycle of the P layer and manually call the View to update the data. For Compose, which is reactive and state-driven, MVVM or MVI architecture is a natural fit without the MVP imperative.
For Compose, there is not much difference between MVVM and MVI. MVI emphasizes one-way data flow, which is a responsive and streaming processing idea. At present, the overall practice can be summed up in one sentence: MVVM for simple business, MVI for complex business.
Why do you say that? In practice, the VM layer is implemented using the Jetpack ViewModel. The difference between MVVM and MVI is how the V layer interacts with the VM layer. We implement a one-way flow of data with Compose, with states (data) flowing down and events flowing up. The state is then stored in the ViewModel, from which the value of the Compose data is retrieved. The simplest case might look like this:
- MainViewModel:
class MainViewModel : ViewModel() {
val text = mutableStateOf("default")}Copy the code
- MainActivity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
val vm = ViewModelProvider(this).get(MainViewModel::class.java)
setContent {
Text(text = vm.text.value)
}
}
}
Copy the code
However, there are more methods when it comes to click events. Fortunately, we expose the corresponding methods directly in the ViewModel, and Compose brings up the event layer by calling the VM’s methods in the top-level composite function. However, when there are more and more events, this way is obviously inadequate, the overall chaos.
Therefore, in complex business, we need to group or collect related events. For example, there are several call events, such as start, calling, end, timeout, etc. What method should we use to integrate these events together? Due to the power and convenience of Kotlin, we have the following methods:
- data class
This is the simplest and most straightforward method, and can be passed directly from the Compose function’s top layer to the bottom layer (if you don’t want to raise the event layer, you can also pass the event object layer down) :
data class CallAction1(
val start: (callId: Long, callName: String) -> Unit = { _: Long, _: String -> },
val calling: () -> Unit = {},
val end: () -> Unit = {},
val timeout: () -> Unit= {},// Instantiate in ViewModel, can be directly exposed to V layer
val callAction1 = CallAction1(
start = { callId, callName ->
Log.e(tag, "callAction1 start: $callId $callName")})// Call directly from layer V
vm.callAction1.start(12."Hello")
Copy the code
- sealed class
This is a bit trickier, but you can use parameter names, but the V layer (Compose) still needs to bring up the event layer:
sealed class CallAction2 {
class Start(val callId: Long.val callName: String) : CallAction2()
object Calling : CallAction2()
object End : CallAction2()
object Timeout : CallAction2()
}
// Provide methods in ViewModel to expose layer V
fun processCallAction2(callAction2: CallAction2) {
when (callAction2) {
is CallAction2.Start -> {
Log.e(tag, "callAction2 start: ${callAction2.callId} ${callAction2.callName}")}else-> {}}}// call at layer V
vm.processCallAction2(
callAction2 = CallAction2.Start(callId = 12, callName = "Hello"))Copy the code
- enum class
This way is more suitable for the case without parameters, not as flexible as the above two ways, we choose to use it.
2, the ViewModel
This is a scenario where ViewModel is used in a DialogFragment. The following is an example of DialogViewModel:
class DialogViewModel : ViewModel() {
private val tag = DialogViewModel::class.java.simpleName
fun test(a) {
Log.e(tag, "invoke test")
viewModelScope.launch {
Log.e(tag, "invoke launch")}}}Copy the code
In DialogFragment we can use the viewModels() extension function in the fragment-ktx package to instantiate DialogViewModel and execute the test() method by pressing the button as follows:
class MyDialog : DialogFragment() {
private val vm by viewModels<DialogViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup? , savedInstanceState:Bundle?).: View? {
return ComposeView(requireContext()).apply {
setContent {
Button(onClick = { vm.test() }) {
Text(text = "Click")}}}}}Copy the code
Then in the Activity, we can display a Dialog in two ways:
- Each time a new instance is created
MyDialog().showNow(supportFragmentManager,"dialog")
Copy the code
- Using the same instance
private var myDialog: MyDialog? = null
if (myDialog == null) { myDialog = MyDialog() } myDialog? .showNow(supportFragmentManager,"dialog")
Copy the code
When using the first method, the popup window is displayed and the log is printed after clicking the button. Everything works fine. But when we use the second method, the first time the popover is displayed, everything works fine when we click the button, but when we close the popover, the second time the popover is displayed and then click the button, the printed message is only:
E/DialogViewModel: invoke test
The log in the coroutine will not be printed. How can this be? We override DialogViewModel’s onClear() method and MyDialog’s onDestroy() method, and print the log:
//DialogViewModel
override fun onCleared(a) {
super.onCleared()
Log.e("DialogViewModel"."onCleared")}//MyDialog
override fun onDestroyView(a) {
super.onDestroyView()
Log.e("MyDialog"."onDestroyView")}override fun onDestroy(a) {
super.onDestroy()
Log.e("MyDialog"."onDestroy")}Copy the code
At this time, we use the second method to test. The popover is displayed for the first time after executing the test method, the popover is closed, and the log is printed as follows:
E/MyDialog: onDestroyView E/DialogViewModel: onCleared E/MyDialog: onDestroy
Since the ViewModel created using the viewModels() extension function uses the Fragment’s ViewModelStore, DialogFragment onDestroy, The ViewModel also clears (), and the coroutine created using the viewModelScope is cancelled, causing the code inside the coroutine not to be executed.
If we need to use the second method, make sure that the ViewModelStore is the ViewModelStore of the Activity so that your coroutine follows the Activity and not the DialogFragment. So this whole detour is just to name another extension function in fragment-ktx: activityViewModels().
3, Hilt
The specific method of using Hilt is not the focus of this article, but the problems encountered and solutions are described here.
Episode: The integration of Hilt was very smooth at the time of development, but when I wrote this article, I re-integrated it again, but the operation always reported an error, the content is as follows, both inside and outside the network all search, and tried again, but still no solution. Because the website integration provide sample is 2.28 alpha, so going to change the version integration see effect, so in the warehouse “mvnrepository.com/artifact/co…” The latest version is 2.40.5. All the problems have been solved after the decisive change.
Execution failed for Task ‘:app:kaptDebugKotlin’. A failure occurred while Executing Org. Jetbrains. Kotlin. Gradle. Internal. KaptWithoutKotlincTask $KaptExecutionWorkAction” java.lang.reflect.InvocationTargetException (no error message)
1, AndroidEntryPoint
Every time you add @AndroidEntryPoint annotations to a new Activity, there will always be some strange error during compilation. Therefore, it is recommended to Clean the Project after adding annotations.
2, the ViewModel
When ViewModel has parameters, the @ViewModelInject annotation in the Hilt-common package can be used directly in the ViewModel constructor as shown in 2.28-alpha:
class MainViewModel @ViewModelInject constructor(
private val sampleBean: SampleBean
) : ViewModel() {}
Copy the code
The ViewModelInject annotation is deprecated and needs to be replaced with the @hiltViewModel annotation on the ViewModel class. The ** @inject ** annotation is also needed on the constructor, which is a bit more complicated, as shown below:
@HiltViewModel
class MainViewModel @Inject constructor(
private val sampleBean: SampleBean
) : ViewModel() {}
Copy the code
When using viewModels, you can still instantiate them with extension functions such as viewModels() and activityViewModels().
Inject in unsupported classes
Hilt supports common Android class injection, but sometimes we need to inject into non-Android classes, such as our SampleBean in object:
class SampleBean @Inject constructor() {
fun print(a) {
Log.e("SampleBean"."invoke print")}}Copy the code
If the SampleManager singleton class is directly injected with @Inject, the following:
object SampleManager {
@Inject
lateinit var sampleBean: SampleBean
}
Copy the code
The compiler will report an error:
Dagger does not support injection into Kotlin objects public final class SampleManager { ^
In this case, there are two ways to deal with it:
- Transform SampleManager to the singleton pattern of Hilt
@Singleton
class SampleManager @Inject constructor(
private val sampleBean: SampleBean
) {}
Copy the code
- Use the ** @entryPoint ** and @entryPointAccessors annotations provided by Hilt
First we need to create an EntryPoint for our SampleBean, an interface as shown below, annotated with @entryPoint, @ InstallIn (SingletonComponent: : class) annotation says it will SampleBean provided in the form of a single case, of course you also can choose other forms:
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SampleBeanEntryPoint {
fun provideSampleBean(a): SampleBean
}
Copy the code
Once we have the EntryPoint for the SampleBean instance, we need to get the pointcut from the EntryPointAccessors to get the sample SampleBean as follows:
object SampleManager {
fun print(context: Context) {
// Get the EntryPoint of SampleBean from EntryPointAccessors
val sampleBeanEntryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
SampleBeanEntryPoint::class.java
)
// Get the instance of SampleBean from EntryPoint of SampleBean
val sampleBean = sampleBeanEntryPoint.provideSampleBean()
sampleBean.print()
}
}
Copy the code
In this code, EntryPointAccessors calls fromApplication() to fetch the contents of a singleton.
- fromActivity()
- fromFragment()
- fromView()
4, Room
There are two cases that you should pay attention to:
1. When the query results use Flow, the method does not need to use the suspend flag;
Here we mark the method using suspend:
@Query("SELECT * FROM gift WHERE type = :type ORDER BY order_num DESC")
suspend fun queryGiftList(
type: Long
): Flow<List<GiftEntity>>
Copy the code
After compiling, an error is reported as follows:
Not sure how to convert a Cursor to this method’s return type (kotlinx.coroutines.flow.Flow<? extends java.util.List>).
public abstract java.lang.Object queryGiftList(long type, @org.jetbrains.annotations.NotNull() ^
2, when querying a single object, note that the return result must be null;
@Query("SELECT * FROM message WHERE (message_id = :messageId)")
fun queryMessage(messageId: Long): Flow<MessageEntity? >Copy the code
Kotlin article
1. Kotlin Android Extensions
After updating Kotlin 1.5.31, the Kotlin-Android-Extensions plugin has been deprecated. This plugin handles serialization, so all @parcelize annotations need to be replaced as follows:
Use the gradle plugin “kotlin-parcelize” instead:
apply plugin: 'kotlin-parcelize'
Copy the code
Then replace the package name used in the original annotation with:
Import kotlinx. Android. Parcel. Parcelize — — for — — — – > import kotlinx. Parcelize. Parcelize
2, LiveData, Flow【StateFlow, ShareFlow】
The main problem is the life cycle awareness of LiveData, and it is not reasonable to use LiveData as an event directly. Scenario: If a page is in the background, that is, in the Stop state, then LiveData as an event cannot be immediately notified to the Activity and executed. Consider using ShareFlow, etc., with lifecycleScope, repeatOnLifecycle, etc as events. Note that the repeatOnLifecycle API is provided in “Lifecycle – Run-time KTX :2.4.0”. Example code is as follows:
val finishEvent = MutableSharedFlow<Int>()
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(state = Lifecycle.State.CREATED) {
finishEvent.collect {
}
}
}
Copy the code
Another way is as follows:
val finishEvent = MutableSharedFlow<Int>()
lifecycleScope.launch {
finishEvent.flowWithLifecycle(
lifecycle = lifecycle,
minActiveState = Lifecycle.State.CREATED
).collect {
}
}
Copy the code
The flowWithLifecycle() extension function still uses repeatOnLifecycle(). For details about the repeatOnLifecycle API please refer to:
- The story behind designing the repeatOnLifecycle API
- Collect Android UI data streams in a more secure way
Serialization and deserialization of Json
As for Json parsing, in Java, we may mostly use Gson to parse, but after switching to Kotlin, if we still use Gson to parse, due to Kotlin’s empty security features, when using Gson slightly non-standard, then we may encounter crash problems. For details, please refer to my blog [Serialization and deserialization of Json in Kotlin — Gson, Moshi].
The three parties SDK
ARouter
After adding obfuscations to Kotlin, we cannot get the passed parameter data. In addition to the @jVMField annotation, we also need to add the @keep annotation, as shown below:
@Keep
@JvmField
@Autowired(name = "type")
var type: Int = 0
Copy the code
Billing
If you need to use Google Pay, please note the following:
- The region set in Google account cannot be China. If it is China, your account cannot be used for purchase
- The node of ladder is also very important, Japanese node is the most effective, nodes in other regions may lead to failure to purchase, it is suggested to try more
- If you still have problems, clear the GooglePlay store cache and data, then reopen it
Otherwise, you will encounter all sorts of failures during development:
- Google Play In-app Billing API version is less than 3
- An internal error occurred.
- Purchase is in an invalid state.
The third case is when we set the test account to “test card, all rejected” mode, then this error occurs. The diagram below:
Google Play Billing | |
---|---|
Key hashes need to be generated when integrating Facebook login. You are advised to directly obtain them using the following code:
private fun facebookKeyHash(a) {
try {
val info = packageManager.getPackageInfo(
application.packageName,
PackageManager.GET_SIGNATURES
)
for (signature in info.signatures) {
val md = MessageDigest.getInstance("SHA")
md.update(signature.toByteArray())
Log.d(
"KeyHash",
android.util.Base64.encodeToString(md.digest(), android.util.Base64.DEFAULT)
)
}
} catch (e: Error) {
e.printStackTrace()
}
}
Copy the code
If you need to use the openssl tool to obtain it from the command line, please be aware of the version issue. On Windows, we need to use openSSL-0.9.8e_x64 instead of OpenSSL-0.9.8k_x64. Version. Otherwise you’ll always get something like “key hash mismatch” when you log in using Facebook.
Google Play
Currently, the only way to release the new version on the Google Play Console is to use bundles. And Google will automatically re-sign our signed App. It can be seen from the following Settings -> App Integrity. The Play App Signing is automatically enabled, so if the SDK integrating other three parties requires App Signing information or key hashing information, Packages downloaded from the Google Play store will not work properly. For example, the above Facebook login, as well as such as Google login.
At this time, we need to configure Google’s signature information into the other three SDKS. For example, in Facebook, we need the secret KeyHash, directly search KeyHash in the Google Play store, and select your own app downloaded from Google Play after installation. Then you can get the key hash. The Facebook background allows multiple key hashes to be configured for a single project, so just add them to your project. To log in to Google, open Google Cloud Plateform, select your project, then go to API and Services -> Credentials, create a new credential, select OAuth 2.0 client ID, Then add the fingerprint of the certificate signed by Google from the Google Play Console.
Of course, you can also request the upgrade key, but this needs to re-send the packet processing, the process is more troublesome, you can refer to other online articles, here is no longer repeated. The above scheme is the most simple and direct, can be effective after the change.
conclusion
I’m probably a big fan of Google, and I jump right into new technologies. DataBinding was just born, I tried it, it didn’t smell good enough, maybe it didn’t suit me or I wasn’t good enough, so I just gave up. ViewBinding was born, tasted, and immediately reinvented my old Butterknife-based project and wrote a blog called It’s time to Embrace ViewBinding!! And shared their exploration experience in the project. I couldn’t wait to embrace Compose from my first encounter with it in nineteen nineteen to the official Release of Google in twenty-one. After six months of learning and nearly three months of development testing, I can’t tell whether Kotlin brings me joy or Compose brings me excitement. The development experience of these three months is very different from that of the previous three years. The combination of Jetpack and Kotlin makes me more willing to develop Android. It also gives me the ability to interact with other big front-end systems, such as two-way binding, data-driven, responsive programming, and one-way data flow. We have it, and I have it. However, this often comes at a cost, with API changes, AS upgrades, AGP upgrades, and so on. Each time, you can start all over again, or even spend a few days without making any progress. But ah, life lies in toss!! You might discover that there’s an easier way to solve this problem.
In awe, the article is writing longer and longer, really want to develop the problems and solutions are perfect description, but because of personal ability reasons many things have not yet explored the principle, also do not know whether there will be misleading you, if there are flaws in the article, please kindly give advice.
References and article recommendations
Compose architecture related
Refer to the following article on architecture, especially in games, where you can see the advantages of the MVI architecture in the code.
- How to choose Jetpack Compose architecture? MVP, MVVM, MVI @fundroid
- Ye tong back! Compose + MVI creates a classic version of Tetris @fundroid
- Rewrite wechat classic aircraft war game @annon with Android Jetpack Compose
Compose recombination correlation
You can refer to RugerMc and Fundroid for more information on how to recompose and how to work. You can also refer to RugerMc and Fundroid for more information.
- Take you through the Compose reorganization process @rugermc hand by hand
- Will Compose’s reorganization affect performance? Talk about recomposition Scope @fundroid
- Jetpack Compose Runtime: the foundation of a declarative UI @fundroid
- In-depth explanation Jetpack Compose | principle @ Android_ developers
Compose custom correlation
Custom related content can refer to the following article, the road is very long OoO big man’s custom skill is very deep.
- Jetpack-compose ink painting effect @path very long OoO
- Jetpack-compose – custom drawing @path very long OoO
Finally, I wrote my original article in Compose Alpha07. Many of the apis have been changed or deprecated, so you don’t need to study them too much. Next, you should start to update and add practical examples
- Time to learn Jetpack Compose!! Compose article aggregates articles
- Compose partner – ViewModel, LiveData
- Compose partner — Flow, Room
Oh, yeah, there’s the website, and don’t forget to check it out.