Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

The method course of Ctrip ticket Android Jetpack and Kotlin Coroutines

2025-02-23 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly explains the "Ctrip ticket Android Jetpack and Kotlin Coroutines method tutorial", the article explains the content is simple and clear, easy to learn and understand, the following please follow the editor's train of thought slowly in depth, together to study and learn "Ctrip ticket Android Jetpack and Kotlin Coroutines method tutorial"!

I. Preface

1.1 Technical background and selection

After three years of development since the 2017 Google IO conference, Kotlin has become the undisputed preferred development language for the Android platform. However, compared with the language itself, the industry adoption rate of coroutines that has entered the stable state after Kotlin 1.2 is still low.

The main advantages of Collaborative process are:

Simpler implementation of asynchronous concurrency (similar to synchronous writing)

More convenient task management

More convenient implementation of producer-consumer model

A more efficient implementation of cold stream (that is, Flow. According to official data, Flow is twice as efficient as RxJava in some benchmarks scenarios, see link 1 for details).

At the same time, the Google Android team is also vigorously promoting the Jetpack component library, in which AAC architecture components bring a new way to implement the application architecture, which can more easily achieve MVVM, which is very suitable for complex business scenarios.

1.2 Business background

Received a big demand this year, the product direction hopes to try a kind of transportation business integration platform search home page new experience. So after several rounds of technical evaluation, the business R & D team decided to jointly launch and develop this new project. Taking this opportunity, the ticket App team decided to restructure and implement it based on Android Jetpack AAC component library and Kotlin Coroutines technology.

The business logic of the front page of the ticket can be summarized and abstracted into the following two scenarios:

Multiple different View depending on the changes of the same data source.

Multiple different View will trigger the change of the same data source when the user operates.

In view of these two scenarios, the MVVM pattern based on ViewModel and LiveData fits very well, which can achieve clear business logic and low code coupling. ViewModel represents a total set of data states related to a business module, while exposing to View the interfaces that are called when many data states need to be called in response to View operations. The LiveData subordinate to the ViewModel represents each data state itself and provides it to the View subscription.

In the code implementation, we can use the same ViewModelStoreOwner (usually Fragment or Activity) in multiple View to get the same ViewModel object. As long as multiple View subscribe to the same LiveData in the same ViewModel and call the same function in ViewModel when the data status needs to be updated in response to the UI operation, we can deal with these two scenarios clearly and succinctly.

At the same time, review the code history debt on the front page of the current air ticket:

The code is lengthy, there is no reasonable encapsulation, splitting and architecture mode, and the number of lines of code in a single file is high.

Complex asynchronous operations result in layers of callback code nesting.

Inappropriate thread pool configuration.

Repeat redundant null checks and possible hidden null security issues.

Too many UI levels are nested, the code is cumbersome and the performance is not high.

Some old technologies officially eliminated by Google are still being used, and new technologies are not followed up in time.

Problem 1 can be easily solved through reasonable encapsulation, splitting, and the use of ViewModel and LiveData.

Kotlin's own empty security feature solves problem 4.

Problems 5 and 6 are mainly solved through reasonable refactoring and the use of new technologies such as ConstraintLayout, but they are beyond the scope of this article.

Then the solution of problems 2 and 3 requires Kotlin cooperation to come out.

two。 Warm-up preparation

2.1 throw a brick to attract jade

Before the specific explanation of the implementation, first through a small example to illustrate a small problem.

If we want to get a ViewModel in a Fragment or Activity, and then subscribe to its internal LiveData, if we use the official API directly, it usually looks like this:

Private lateinit var myViewModel: MyViewModel. MyViewModel = ViewModelProvider (this) [MyViewModel::class.java] myViewModel.liveData1.observer (this, Observe {doSomething1 (it)}) myViewModel.liveData2.observer (this, Observe {doSomething2 (it)}).

Due to the overloading of lambda expressions and operators in Kotlin, this code is much more concise than the corresponding Java code, but it is still not enough for Kotlin style. Let's encapsulate it a little bit and define two new functions:

/ / Top-level function version inline fun getViewModel (owner: ViewModelStoreOwner, configLiveData: t. ()-> Unit = {}): t = ViewModelProvider (owner) [T::class.java] .apply {configLiveData ()} / / extended function version inline fun ViewModelStoreOwner.getSelfViewModel (configLiveData: t. ()-> Unit = {}): t = getViewModel (this, configLiveData)

In order to use different scenarios and facilitate different people's habits, both the top-level function version and the extension function version are written here, but the function is exactly the same (the extension function version calls the top-level function version directly). Now if we want to get the ViewModel in Fragment, see what it looks like (the extension function version is used here):

Private lateinit var myViewModel: MyViewModel. MyViewModel = getSelfViewModel {liveData1.observe (this@MyFragment, Observer {doSomething1 (it)}) liveData2.observe (this@MyFragment, Observer {doSomething2 (it)}). }

The benefit of this encapsulation is not just to make the code look "DSL". First of all, the inline generic materialization function allows us to avoid writing boilerplate code like xxx::class.java, but only need to pass a generic parameter (in this case, because the lateinit property has already declared the type, we don't even have to explicitly write the generic parameter according to the type derivation), which looks much more elegant. Secondly, we use the lambda expression with recipient and the scope function apply so that we don't have to repeat myViewModel many times when we get the LiveData object in ViewModel. Boilerplate code like this.

Finally, from the point of view of the code structure, we usually subscribe to all the LiveData that need to be subscribed directly after we get the ViewModel object, and we write all the subscription logic into the scope of the lambda expression parameter of the getSelfViewModel function, so that we can have a better understanding of the subscription code.

This is just a piece of cake, and when we decide to start using Kotlin to replace Java, it's best to lay a solid foundation for Kotlin so that we can realize the maximum potential of the language. To avoid using Kotlin to write Java-style code.

2.2 Code role division

If you divide the current code by responsibility, there are probably the following categories: data classes (data class, similar to Java Bean), utility functions (such as formatting a date Convert it to a displayable string), data sources (such as pulling data from the network or reading data from a local database), core business logic (we may have to process it according to business needs after we get the original data), UI code (needless to say), state information (usually mutable objects used to represent state, etc., or the current state of the data).

We want to divide the above code into three roles, or fall into three scope, namely: View, ViewModel, Model, that is, the three major roles in the MVVM pattern. The UI code is assigned to the View; data class, the data source is regulated to Model;, and the data status or other state information is assigned to ViewModel. The utility function depends on the situation and can be placed in Model as a stand-alone component.

III. Formal realization

3.1 the basic mode of the combination of Channel and LiveData

In MVVM mode, VM, or ViewModel, represents the state of the data. In order to make business logic and code structure more reasonable. We usually split some data that depends on each other's state (usually the business it represents is also strongly related) into the same ViewModel. The LiveData (usually inside the ViewModel) represents some specific data state. For example, in the business of Ctrip's front page of air tickets, the relevant data of the departure city can be expressed by one LiveData, and the arrival city can be expressed by another LiveData, and the two LiveData are located in the same ViewModel.

If we do not use the livedata-ktx package, we create the LiveData object mainly by calling the constructor of the MutableLiveData class, and we use the MutableLiveData object directly for subscriptions, data updates, and so on. MutableLiveData is like ordinary objects, and we can use it in any asynchronous framework.

However, in order to work more perfectly with the Kotlin protocol, the livedata-ktx package provides us with another way to create LiveData, that is, the liveData {} function, whose function signature is as follows:

Fun liveData (context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT, @ BuilderInference block: suspend LiveDataScope.)-> Unit): LiveData

Let's start with the third parameter, block, which is an suspend lambda expression, that is, it runs in a co-program. The first parameter, context, is usually used to specify the scheduler for the execution of the co-program, while timeoutInMs is used to specify the timeout, which will be cancelled if the time exceeds the timeout when the LiveData does not have an active observer. Since both the first and second parameters have default values, in most cases, we only need to pass the third parameter.

The liveData {} function does not give a use case in the official documentation, so there is no so-called standard "official" usage. We took a look and found that the block block is a lambda with a receiver, and the receiver type is LiveDataScope, and LiveDataScope has a member function emit, which is very similar to RxJava's create operator and more like the flow {} function in Flow. Therefore, if our LiveData is to be used as a data source for sustainable data transmission, the collaborative process initiated by the liveData {} function needs to constantly fetch data from the outside. This scenario is an opportunity for Channel (see link 2) to give full play to its capabilities. We use the above techniques to write a simple ViewModel:

Class CityViewModel: ViewModel () {privateval departCityTextChannel = Channel (1) val departCityTextLiveData = liveData {for (result in departCityTextChannel) emit (result)} / / external UI updates data fun updateCityUI () = viewModelScope.launch (Dispatchers.IO) {val result = fetchData () / / pull data departCityTextChannel.send (result)}} by calling this method

First, we declare and initialize a Channel-- departCityTextChannel. Then we use the liveData {} function to create a LiveData object, and during the co-program started by the liveData {} function, we keep fetching data from departCityTextChannel through an infinite loop, and if not, the co-program is suspended until the data arrives (which is much more efficient than the similar producer-consumer model implemented with Java threads and BlockQueue). The for loop has first-class support for Channel.

If UI wants to update the data, the updateCityUI () function is called, and all operations within this function (usually time-consuming) are performed asynchronously within the co-program it starts. Here we start the collaborative process through the viewModelScope provided by the viewmodel-ktx package. The implementation of the scope of the collaborative program combined with the implementation of ViewModel can sense the life cycle of external UI components through ViewModel, thus helping us to cancel tasks automatically.

Finally, note that the size of the buffer size passed in to the factory function Channel (1) when initializing departCityTextChannel is 1. This is mainly because we can avoid the fact that the producer process hangs while waiting for the consumer to take the data from the Channel, thus affecting the efficiency to a certain extent. Of course, if the speed of production of producers is too fast, and the speed of consumer consumption is too slow to keep up, we can appropriately increase the value of size.

Almost every LiveData we have needs a Channel to use with it, and the liveData {} function does almost the same thing, using a for loop to get the data from the Channel and then emit it using the emit function. It can then be encapsulated as follows:

Inlineval Channel.coroutineLiveData: LiveData get () = liveData {for (entry in this@coroutineLiveData) emit (entry)}

The code to create departCityTextChannel and departCityTextLiveData objects in ViewModel looks like this:

Class CityViewModel: ViewModel () {privateval departCityTextChannel = Channel (1) val departCityTextLiveData = departCityTextChannel.coroutineLiveData. Omit other code

We encapsulate an inline extension property called coroutineLiveData, whose getter has encapsulated the creation logic of LiveData, but please note that each time this property is called, a new LiveData object is actually returned, so the right thing to do is to save its results after calling the coroutineLiveData property for reuse. Never use departCityTextChannel.coroutineLiveData every time to expect to get the same LiveData object. Of course, if you think this may be misleading, you can also change the coroutineLiveData property to an extension function.

3.2 UI Code subscription LiveData

Although the UI of the entire front page of the ticket is located in a Fragment, we can package the unrelated UI into different View separately. If we talk about city-related UI, we might do something like this:

Class CityView: LinearLayout {constructor (context: Context): super (context) constructor (context: Context, attributeSet: AttributeSet): super (context, attributeSet) constructor (context: Context, attributeSet: AttributeSet, defStyleAttr: Int): super (context, attributeSet, defStyleAttr) privateval tvCity: TextView /. Omit more View statements init {LayoutInflater.from (context). Evaluate (R.layout.flight_inquire_main_view, this). Apply {tvCIty = findViewById (R.id.tv_city) / /. Omit more View initialization}

If it's easy to get ViewModel and subscribe to LiveData in Fragment or Activity, we just need to pass them in themselves using this. However, we cannot get the Fragment object in View, so we have no choice but to define an initObserve function and pass in a reference to Fragment itself by exposing it to the Fragment call, so the code for View looks like this:

Class CityView: LinearLayout {constructor (context: Context): super (context) constructor (context: Context, attributeSet: AttributeSet): super (context, attributeSet) constructor (context: Context, attributeSet: AttributeSet, defStyleAttr: Int): super (context, attributeSet, defStyleAttr) privateval tvCity: TextView /. Omit more View statements private lateinit var cityViewModel: CityViewModel init {LayoutInflater.from (context). Evaluate (R.layout.city_view, this). Apply {tvCIty = findViewById (R.id.tv_city) / /. Omit more View initialization} tvCity.setOnClickListener {updateCityView ()} fun initObserver (owner: t) where T: ViewModelStoreOwner, T: LifecycleOwner {cityViewModel = getViewModel (owner) {cityLiveData.observe (owner, Observer {tvCity.text = it})} / /. Omit other LiveData subscriptions} private fun updateCityView () = cityVIewModel.updateCityView ()}

Owner is actually Fragment, but for decoupling, instead of using Fragment directly, it determines the responsibility of owner through generics and two upper bound constraints. once one day this View is to be ported to Activity, Activity can also pass itself directly through the initObserver function. In Fragment, as soon as we get the View object through findViewById, we should call initObserver to initialize the subscription, so the code won't go into detail.

We use a picture to summarize sections 3.1 and 3.2:

The relationship between the sample code we just wrote is clear at a glance. Both V and VM in MVVM mode already exist. Although M is not reflected in the figure, the data source from which the data is obtained, that is, the fetchData () function called in the CityViewModel.updateCityUI () function, belongs to Model, which usually encapsulates database operations or network service pull.

3.3 complex scenes

As mentioned in the first section 1.2, we have some complex business scenarios, such as multiple independent View relying on the same data source, or multiple View may trigger updates to the same data source. For example, there are two cities showing View, and users can change the city in either of them. The city information displayed in both View needs to be updated. This is a very typical case, which combines scenario 1 in Section 1.2 with scenario 2.

Based on the above code example, that is to say, in addition to the above CityView, we also need a View that shares the same data source with it, if there is a CityView2:

Class CityView2: LinearLayout {/ /. Omit other code privateval tvCity: TextView private lateinit var cityViewModel: CityViewModel init {LayoutInflater.from (context) .propagate (R.layout.city_view2, this). Apply {tvCIty = findViewById (R.id.tv_city2)} tvCity.setOnClickListener {updateCityView ()}} fun initObserver (owner: t) where T: ViewModelStoreOwner T: LifecycleOwner {cityViewModel = getViewModel (owner) {cityLiveData.observe (owner, Observer {tvCity.text = it})} private fun updateCityView () = cityVIewModel.updateCityView ()}

The rest of the code is more or less the same, except for initializing View, initObserver functions, and updating UI functions. To make sure that the cityViewModel in CityView2 and CityView is the same, just make sure that the owner passed in by the initObserver function is the same object.

Here I also draw a picture to describe this relationship:

IV. Challenges encountered by new technologies in the production environment

Any open source technology that is recognized and trusted by the industry is usually tested in a production environment with millions or even tens of millions of users. The PV level of the old home page of Ctrip ticket is in the level of 10 million, taking into account the dual platforms of iOS and Android, as well as the AB experiment, the new Android ticket platform home page of the PV level also has millions of levels. Whether it can have excellent stability performance under the number of millions of users is the test of these technologies mentioned in this article.

The Kotlin language and its standard library itself have been iterated to version 1.3.x (the latest version is 1.4.10 as of the publication of the article, while Ctrip uses 1.3.71). Coupled with several years of testing of the production environment at home and abroad, it has been relatively stable. The version of the Jetpack architecture components used this time, such as ViewModel and LiveData, is 2.2.0, and it is also very stable after months of online observation. But in the end, there are two thorny problems in kotlinx.coroutines, the Kotlin collaboration framework.

4.1the APK of Integrated Synergy reported an error in some domestic Android 5.x phones: INSTALL_FAILED_DEXOPT

Problem description: after configuring most of the kotlinx.coroutines library with version number 1.3.x, the Android app project will report an error: INSTALL_FAILED_DEXOPT on some domestic Android 5.x mobile phones, resulting in failure to install.

Under the condition of Ctrip's compilation tool chain, only version 1.3.0 of the kotlinx.coroutines library is available, while the remaining 1.3.x higher versions will steadily repeat this problem on the vivo X5Pro D (Android 5.0) model after the integration dependency. Of course, it is not the only mobile phone brand and model that can steadily reproduce this problem.

This is also discussed in the forums of the Kotlin Chinese community (see link 3). The blogger of this post also asked the official question on the issues of the official Github repository of the kotlinx.coroutines library, but the JetBrains official replied that it was a problem with the Google toolchain (see link 4). The issue was then referred to the Google side, but the Google side said it was aware of the issue, but did not fix it because the system version Android 5.x involved was too old (see link 5).

The attitude of both officials has reached this point, and we can only hope that the problem can be solved by ourselves. The options we can try include upgrading the Android SDK Build-Tools version, upgrading the Gradle version, upgrading to Kotlin 1.4, upgrading kotlinx.coroutines to 1.3.9, compiling kotlinx.coroutines 's Jar package with JDK 8 (officially using JDK 6). All of the above attempts are invalid. The final solution is that you can only use version 1.3.0 of the kotlinx.coroutines library temporarily. Since version 1.3.1 to 1.3.8 contains a large number of improvements to Flow and Bug fixes, for the sake of stability, Flow is not used in business code for the time being.

4.2 failure to get Dispatchers.Main of main thread scheduler leads to Crash

Problem description: when the main thread scheduler Dispatchers.Main is called, crash will occur in a small probability, regardless of model and system version.

This problem was discovered by online crash reporting, resulting in more than 2000 user crash.

This problem is caused by a flaw in the implementation of Dispatcher.Main. This issue has already been mentioned on the official Github issues page of kotlinx.coroutines (see link 6). The problem has been fixed by officially replacing the original ServiceLoader implementation with Class.forName in version 1.3.3 (see link 7), so the best way to avoid this problem is to upgrade the version of the kotlinx.coroutines library.

However, there is a problem with dog blood. Due to the problems described in Section 4.1, all versions of kotlinx.coroutines libraries except version 1.3.0 will have problems that 5.x phones cannot be integrated. The concurrence of these two problems has almost led to a "deadlock" of our solution, a dilemma.

At the beginning of the discovery of the online problem, we customized the main thread scheduler to replace the official Dispatchers.Main and replaced all Dispatcher.Main in the business code with a custom scheduler, but this did not completely solve the problem. Since the ktx version of the Jetpack schema component also relies on version 1.3.0 of the kotlinx.coroutines library, it will be used even if we don't use the internals of Dispatchers.Main,ViewModel and LiveData. In desperation, we have to try to copy the code that uses ViewModel and LiveData of Dispatchers.Main and replace the Dispatchers.Main with a custom main thread scheduler.

But the above solutions are temporary, in the case of unable to upgrade the kotlinx.coroutines library, we finally decided on the fork kotlinx.coroutines code. And merge the official commit that fixed the problem in 1.3.3 to version 1.3.0 code in a cherry-pick-like way, then change the version number and recompile the Jar package, and put it on the company's internal source for use.

In the long run, with the decrease in the number of 5.x phones, the minimum version of Ctrip app system support will eventually be raised to Android 6.0. only then will upgrading the kotlinx.coroutines version be considered a relatively perfect solution to this problem.

5. Conclusion

Many of the advantages of the Kotlin language and the problems it solves are pain points for Java developers. After years of technical accumulation, the Kotlin of version 1.3.x (the last version of 1.3.x is 1.3.72) has been relatively stable and mature.

Kotlin is a powerful and ambitious project that brings new concepts and new solutions to old problems to many Java developers. Although it has entered the release stage for a year and a half, but from the results of our practice, its stability still has room for improvement. With the introduction of Kotlin 1.4 and kotlinx.coroutines 1.3.9, both the Kotlin language itself and the collaboration process have entered the next stage. It is believed that their performance, stability, and functionality will really rise to a higher level in the near future.

Google officials have become increasingly close to the Android development community in recent years. They have adopted many effective suggestions put forward by Android developers and put them on the ground, and Jetpack is one of the results. As a real official product, its stability has indeed stood the test in terms of actual performance.

Jetpack contains not only architecture components, but also a series of practical libraries, such as declarative UI Framework (Compose), SQLite Database Operation Framework (Room), dependency injection (Hilt), background Task Management (WorkManager), and so on. In the future development plan, gradually trying to migrate to more Jetpack-related technologies will also be an important direction of Android technology improvement.

Thank you for your reading, the above is the content of "Ctrip ticket Android Jetpack and Kotlin Coroutines method tutorial". After the study of this article, I believe you have a deeper understanding of the method tutorial of Ctrip ticket Android Jetpack and Kotlin Coroutines, and the specific use needs to be verified in practice. Here is, the editor will push for you more related knowledge points of the article, welcome to follow!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report