Project address: github.com/haikun-li/J… (Please open the project with the latest Version of Canary Studio since jetpack Compose is introduced in the project.)

100% kotlin

Why kotlin

  • Expressive and concise: You can do more with less code. Express your ideas and write less boilerplate code. 67% of professional developers who use Kotlin report increased productivity.
  • Safer code: Kotlin has a number of language features that can help you avoid common programming errors like null pointer exceptions. Android apps that contain Kotlin’s code are 20% less likely to crash.
  • Interoperable: You can call Java code in Kotlin code, or Kotlin code in Java code. Kotlin is fully interoperable with the Java programming language, so you can add as much Kotlin code to your project as you need.
  • Structured concurrency: Kotlin coroutines make asynchronous code as easy to use as blocking code. Coroutines can greatly simplify the management of background tasks, such as network calls and local data access.
  • Null judgments, method/property extensions, property delegates, higher-order functions, DSL syntax sugar, coroutines, default parameters, and more
  • Although Google promises never to drop Java support, the new Compose must use Kotlin, and while Java can be implemented, it’s not easy to implement, and not easy to implement often means “not possible.”
  • How sweet!

Write the build Script using Kotlin Script+buildSrc

Gradle build language is Groovy, but Gradle actually supports Kotlin to write Gradle build scripts. Common build scripts are. Gradle end scripts, and Koltin syntax is. The advantage of using kotlin build scripts is that you can have code hints and write extension methods. In the meantime, we can use buildSrc for version management.

Use the Kotlin Script

  • Mode 1 (New project) :

With the latest Canary version of Android Studio, select Use Kotlin script(.kts) for Gradle Build Files when creating a new project

  • Method 2:

Change all. Gradle files in your project to. Gradle. KTS, and change the syntax to Kotlin Script (see demo for details), including settings.gradle, and add settings.gradle. KTS

rootProject.buildFileName = "build.gradle.kts" 
Copy the code

Using buildSrc

  • Build a new Android library with a buildSrc name. The module that reports this name already exists. Since this name is a reserved name, delete the include from setting.gradle

The buildSrc line will do.

  • The buildSrc directory files are stored in the following directories

  • BuildSrc build.gradle. KTS content
plugins {
    `kotlin-dsl`
}

repositories {
    google()
    mavenCentral()
}
Copy the code
  • It can then be referenced as follows

Android Jetpack

Jetpack is a suite of libraries that help developers follow best practices, reduce boilerplate code and write code that runs consistently across Android versions and devices, allowing developers to focus on writing important code

navigationnavigation

Navigation is a framework for navigating between “destination locations” in Android apps. Android Jetpack’s navigation component helps you navigate from simple button clicks to more complex modes like app bars and drawer navigation. Navigation components also ensure a consistent and predictable user experience by following a set of established principles. The navigation component consists of three key parts:

  • Navigation diagram: AN XML resource that contains all navigation-related information in a central location. This includes all individual content areas within the application (called targets) as well as possible paths that users can access through the application.
  • NavHost: Empty container to display the target in the navigation diagram. The navigation component contains a default NavHost implementation (NavHostFragment) that displays the Fragment target.
  • NavController: Manages the object navigated by the application in NavHost. As the user moves through the application, the NavController arranges the exchange of the target content in NavHost.

Navigation relying on

  • App rely on
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
Copy the code
  • If you want to use Safe Args to pass security parameters, include the following classpath in your project build.gradle file
Buildscript {repositories {Google ()} dependencies {def nav_version = "2.3.4" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } }Copy the code

It also needs to be added in the application module build.gradle

apply plugin: "androidx.navigation.safeargs.kotlin"
Copy the code

NavHost:

  • NavigationActivity
class NavigationActivity: AppCompatActivity(R.layout.activity_navigation)
Copy the code
  • activity_navigation
<? The XML version = "1.0" encoding = "utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <fragment android:id="@+id/navHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:layout_constraintBottom_toTopOf="@id/navView" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/navigation_demo_navigation" /> </LinearLayout>Copy the code

Android: name = “androidx. Navigation. Fragments. NavHostFragment” this is depend on the package of the fragments, role is to define the start of navigation

Setting app:defaultNavHost=”true” will block the system’s return button and the fragment switch will be pushed by default

NavGraph refers to the defined navigation file

Navigation map

  • res/navigation/navigation_demo_navigation.xml
<? The XML version = "1.0" encoding = "utf-8"? > <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation_demo_navigation" app:startDestination="@id/navigationFragment1"> <! <fragment Android :id="@+id/navigationFragment1" android:name="com.haikun.jetpackapp.home.ui.demo.navigation.NavigationFragment1" android:label="NavigationFragment1"> <action android:id="@+id/action_navigationFragment1_to_navigationFragment2" app:destination="@id/navigationFragment2" />  <action android:id="@+id/action_navigationFragment1_to_navigationFragment22" app:destination="@id/navigationFragment2" app:enterAnim="@anim/anim_fragment_in" app:popExitAnim="@anim/anim_fragment_out" /> <! --action Jump action --> <! Android :id="@+id/navigationFragment2" </fragment> <fragment android:id="@+id/navigationFragment2" android:name="com.haikun.jetpackapp.home.ui.demo.navigation.NavigationFragment2" android:label="NavigationFragment2"> <argument android:name="testKey" app:argType="string" app:nullable="true" /> <! --argument received --> <action Android :id="@+id/ action_fragment2_to_navigationFragment3" app:destination="@id/navigationFragment3" app:enterAnim="@anim/anim_fragment_in" app:popExitAnim="@anim/anim_fragment_out" /> </fragment> <fragment android:id="@+id/navigationFragment3" android:name="com.haikun.jetpackapp.home.ui.demo.navigation.NavigationFragment3" android:label="NavigationFragment3"> <action android:id="@+id/action_navigationFragment3_to_navigationFragment1" app:destination="@id/navigationFragment1" app:popUpTo="@id/navigationFragment1" app:popUpToInclusive="true" /> <! --app:popUpTo removes 2 and 3 from the stack --> <! --app:popUpToInclusive="true" will also pop the first 1 off the stack, effectively clearing it --> </fragment> </navigation>Copy the code
  • You can also switch to Design mode for editing

NavController

  • Switch from Fragment1 to Fragment2 by calling the following code directly from fragment1, passing in the parameters
FindNavController (). Navigate (NavigationFragment1Directions actionNavigationFragment1ToNavigationFragment22 (" test "))Copy the code

Other features

  • Global actions use global actions to create common actions that can be shared by multiple destinations
  • Create deep links links that direct users to specific destinations within the application
  • Use the top application bar, the drawer navigation bar, and the bottom navigation bar to manage navigation
  • Custom return navigation

Lifecycle

Disadvantages of writing logical code in the traditional lifecycle approach

  • Too many calls to manage interfaces and other components in response to the current state of the lifecycle. Managing multiple components puts a lot of code in lifecycle methods such as onStart() and onStop(), which makes them difficult to maintain
  • There is no guarantee that a component will start before an Activity or Fragment stops. This is especially true when we need to run an operation for a long time, such as some configuration check in onStart()

The advantage of the Lifecycle

  • Life-cycle-aware components perform operations in response to changes in the life-cycle state of another component, such as an Activity or Fragment. These components help you write more organized and often leaner code, and you can move component-dependent code from lifecycle methods into the components themselves, where it is easier to maintain.

Lifecycle using

  • Define LifecycleObserver
inner class MyLifecycleObserver(val lifecycle: Lifecycle) : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onStart() { CoroutineScope(scope).launch { Delay (3000) if (lifecycle. The currentState. IsAtLeast lifecycle. State. (STARTED)) {LogUtil. E (" open position ")}}} @onlifecycleEvent (Lifecycle.event.on_stop) fun onStop() {logutil.e (" close location ")}}Copy the code
  • Add LifecycleObserver
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycle.addObserver(MyLifecycleObserver(lifecycle))
}
Copy the code

ViewModel

  • Designed to store and manage interface-related data in a life-cycle oriented manner. The ViewModel class allows data to survive configuration changes such as screen rotation and is responsible for preparing the data for the interface. ViewModel objects are automatically retained during configuration changes so that the data they store is immediately available for use by the next Activity or Fragment instance.

What problems can the ViewModel solve

  • If the system destroys or recreates the interface controller, any transient interface-related data stored in it will be lost. To prevent this from happening to our program, in addition to saving data using the Activity’s savedInstanceState(only suitable for small amounts of data that can be serialized and deserialized), You can also use the ViewModel to process data (which can hold larger data)
  • Simplify resource management and avoid the risk of memory leaks. Activities and fragments often need to make asynchronous calls (such as network requests) that may take some time to return. We need to make sure that the system cleans up these calls after they are destroyed to avoid potential memory leaks. In addition, if the configuration is changed, the previous work may be repeated during object creation, resulting in a waste of resources
  • Having activities and Fragments focus on the display of the interface, and requiring the interface controller to load data from the database or network, makes the class bloated, and the ViewModel can more easily and efficiently isolate view data operations
  • Implement Fragment and Activity/Fragment to share data

Use the ViewModel

  • Implement the ViewModel, inherit the ViewModel abstract class
class ViewModelViewModel: ViewModel() {
    val userList = mutableListOf<User>()
}
Copy the code
  • Refer to the ViewModel

Use in fragment/activity

val viewModel:ViewModelViewModel by viewModels()
viewModel.userList
Copy the code

Viewmodels shared in multiple fragments. Viewmodels created using activityViewModels() depend on the Activity and are the same object in multiple fragments

val shareViewModel:ViewModelViewModel by activityViewModels()
Copy the code

Or use koIN dependency injection, which is explained below

val viewModel: ViewModelViewModel by viewModel()

val viewModel: ViewModelViewModel by sharedViewModel()
Copy the code

ViewModel lifecycle

Life cycle awareness dataLiveData

Is an observable data storage class. Unlike regular observable classes, LiveData has lifecycle awareness, meaning that it follows the lifecycle of other application components such as activities, fragments, or services. This awareness ensures that LiveData updates only application component observers that are in an active lifecycle state.

The advantage of LiveData

  • Ensure that the interface conforms to data state :LiveData follows observer mode. LiveData notifies the Observer when the underlying data changes. You can integrate code to update the interface in these observers.
  • There are no memory leaks: The observer is bound to the Lifecycle object and cleans itself up after its associated Lifecycle has been destroyed.
  • Does not crash when an Activity stops: If the observer’s life cycle is inactive (such as returning an Activity in the stack), it does not receive any LiveData events.
  • No manual handling of the lifecycle is required: interface components simply observe the relevant data and do not stop or resume the observation. LiveData automatically manages all of these operations because it can sense the associated lifecycle state changes as it observes.
  • Data is always up to date: If the life cycle becomes inactive, it receives the latest data when it becomes active again.
  • Appropriate configuration changes: If an Activity or Fragment is recreated due to a configuration change (such as a device rotation), it immediately receives the latest available data.
  • Shared resources: LiveData objects can be extended using the singleton pattern to encapsulate system services so that they can be shared across applications.

LiveData use

  • define
private val normaLiveData1 = MutableLiveData<String>()
Copy the code
  • The assignment
NormaLiveData1. Value ="LiveDataValue"//UI thread normaLiveData1. PostValue ("LiveDataValue")// Non-UI main threadCopy the code
  • Observational data
NormaLiveData1. Observe (viewLifecycleOwner, Observer {logutil. e(" observe first value change ") tv.text = it}) normaLiveData1. Observe (viewLifecycleOwner, Observer {logutil. e(" observe first value change ") tv.text = it})Copy the code
  • Observe the data without lifecycle awareness, which requires manual unobservation or memory leaks will occur
Val observer = observer < String > {LogUtil. E (" observed first value changed ")} normaLiveData1. ObserveForever (observer) / / remove observation in a suitable life cycle normaLiveData1.removeObserver(observer)Copy the code
  • Convert LiveData
Private val transLiveData= doubling. Map (normaLiveData1){"$it ----- doubling "}Copy the code
  • Merge multiple LiveData
private val mediatorLiveData = MediatorLiveData<String>() mediatorLiveData.addSource(normaLiveData1){ Mediatorlivedata. value=" Merged value: $it - ${normaLiveData2. Value}} mediatorLiveData. AddSource "(normaLiveData2) {mediatorLiveData. The combined value: value =" ${normaLiveData1.value}---$it" }Copy the code
  • LiveData combined with Room and coroutine (below)

Kotlin data flowFlow

Flow data flows are built on top of coroutines and can provide multiple values. Conceptually, a data stream is a sequence of data that can be computed asynchronously, somewhat like RxJava.

The use of a Flow

  • Create a Flow
val flow = flow<String> {
    emit("value1")
    emit("value2")
    delay(1000)
    emit("value3")
}
Copy the code
val flow = flowOf("value")
Copy the code
val flow = listOf(1, 2, 3).asFlow()
Copy the code
  • Collect the Flow – collect ()

Because collect is a hang function, it needs to be executed in a coroutine

scope.launch {
    flow.collect {
        LogUtil.e(it)
    }   
}
Copy the code
  • Conversion Flow – the map ()
FlowOf (1, 2, 3).map {" $it "}. Collect {logutil. e(it)}Copy the code
  • Filtering Flow, the filter ()
flowOf(1, 2, 3).filter {
     it > 1
}.collect {
     LogUtil.e(it)
}
Copy the code
  • Merge the Flow

The zip operator will merge one item in flow1 with one item in flow2. If the number of items in flow1 is greater than the number of items in flow2, the number of items in the new flow after the merge is equal to that of the smaller flow The item number

Val flow1 = flowOf (1, 2, 3, 4, 5) val flow2 = flowOf (" a ", "2", "three", "four", "five", "six") flow1. Zip (flow2) {a, b -> "$a---$b" }.collect { LogUtil.e(it) }Copy the code

When you combine, each time a new item is emitted from FLOW1, it is merged with the latest item from Flow2

Val flow1 = flowOf(1, 2, 3, 4, 5). OnEach {delay(1000)} val flow2 = flowOf(1, 2, 3, 4, 5). "Six"). OnEach {delay (500)} flow1.com bine (flow2) {a, b - > "$a" - $b}. Collect (it)} {LogUtil. ECopy the code
  • Catch exception —-catch()
flow {
   emit(1)
   emit(1 / 0)
   emit(2)
}.catch {
   it.printStackTrace()
}.collect {
   LogUtil.e(it)
}
Copy the code
  • Thread switching —-flowOn()
withContext(Dispatchers.IO){ flowOf(1, 2, 3, Logutil. e("init-- ${thread.currentThread ().name}")}. Filter {// Logutil. e("init-- ${thread.currentThread ().name}")} Logutil. e("filter-- ${thread.currentThread ().name}") it > 1}.flowon (dispatchers.main).map {// The following recent flowOn control -io Logutil. e("map-- ${thread.currentThread ().name}") "$it"}.flowon (dispatchers.io).map {// The following recent flowOn control -Main FlowOn (dispatchers.main).collect {flowOn(dispatchers.main).collect {flowOn(dispatchers.main).collect { IO logutil. e("collect-- ${thread.currentThread ().name}") logutil. e(it)}}Copy the code
  • To liveData – asLiveData ()

Add the dependent

"androidx.lifecycle:lifecycle-livedata-ktx:${LibraryVersion.LIVEDATA_KTX}"
Copy the code
flowOf(1, 2, 3, 4).asLiveData().observe(viewLifecycleOwner, Observer {
   LogUtil.e(it)
})
Copy the code

More operators

Local preservationDataStore

DataStore is a data storage solution that allows you to store key-value pairs or typed objects using protocol buffers. DataStore uses Kotlin coroutines and Flows to store data in an asynchronous, consistent transactional manner. Please refer to hongyang’s public account

Problems with SharedPreferences

  • Getting data through the getXXX() method may cause the main thread to block
  • SharedPreference does not guarantee type safety
  • Data loaded by SharedPreference will remain in memory forever, wasting memory
  • Although the apply() method is asynchronous, ANR may occur, with different implementations before and after 8.0
  • The apply() method cannot obtain the result of success or failure of the operation

What problems does DataStore solve

  • DataStore is implemented based on Flow, ensuring security in the main thread
  • Update data is processed in a transactional manner with four characteristics (atomicity, consistency, isolation, and persistence)
  • There are no data persistence methods like apply() and commit()
  • The SharedPreferences are automatically migrated to the DataStore to ensure data consistency and prevent data damage
  • You can listen to the success or failure of an operation
  • In addition, Jetpack DataStore provides Proto DataStore, which is used to store the typed objects of the class. It uses protocol Buffers to serialize the objects and store them locally. Protocol Buffers have been widely used by large companies such as wechat and Ali

DataStore using

Add the dependent

Const val DATA_STORE = "1.0.0-alpha05" const val PROTOBUF = "3.11.0" "Androidx. Datastore: datastore - preferences: ${LibraryVersion. DATA_STORE}" / / protobuf need to rely on below "androidx.datastore::datastore-core:${LibraryVersion.DATA_STORE}" "com.google.protobuf:protobuf-java:${LibraryVersion.PROTOBUF}"Copy the code
Save key-value pairs
object DataStore {

    private const val APP_DATA_STORE_NAME = "APP_DATA_STORE_NAME"
    private lateinit var dataStore: DataStore<Preferences>

    fun init(context: Context) {
        dataStore = context.createDataStore(APP_DATA_STORE_NAME)
    }

    suspend fun <T> save(key: Preferences.Key<T>, value: T) {
        dataStore.edit {
            it[key] = value
        }
    }

    suspend fun <T> get(key: Preferences.Key<T>): T? {
        val value = dataStore.data.map {
            it[key]
        }
        return value.first()
    }

}
Copy the code

save

CoroutineScope(scope).launch {
    DataStore.save(preferencesKey("key1"), "aa")
}
Copy the code

read

CoroutineScope(scope).launch {
    val get = DataStore.get<String>(preferencesKey("key1"))
}
Copy the code
Save the protobuf

Protobuf knowledge is not covered here

  • Define.proto files
syntax = "proto3";

option java_package = "com.haikun.jetpackapp.home.ui.demo.datastore.bean";
option java_multiple_files = true;

message MessageEvent {
  int32 type = 1;
  string message = 2;
}
Copy the code
  • Compile the file

The following three files are obtained after compilation

  • Define the Serializer
object MessageSerializer : Serializer<MessageEvent> {
    override val defaultValue: MessageEvent
        get() = MessageEvent.getDefaultInstance()

    override fun readFrom(input: InputStream): MessageEvent {
        return MessageEvent.parseFrom(input)
    }

    override fun writeTo(t: MessageEvent, output: OutputStream) {
        t.writeTo(output)
    }
}
Copy the code
  • save
val createDataStore = context? .createDataStore("data", MessageSerializer) createDataStore? .updateData {it.toBuilder().setType(12).setMessage(" message ").build()}Copy the code
  • read
CoroutineScope(scope).launch { context? .createDataStore("data", MessageSerializer)? .data? .first()? .let { LogUtil.e("${it.type}---${it.message}") } }Copy the code

Declarative UIDataBinding

A data binding library is a support library that allows you to bind interface components in a layout to data sources in an application using declarative personalities rather than programmatically, and to automatically notify the interface of data changes using LiveData objects as data binding sources.

Declarative UI VS imperative UI

  • Declarative UIs only need to “declare” the interface, rather than manually update, as the declared data changes, the UI changes

  • Imperative UIs require active UI updates, such as setText()

Use DataBinding

Open the DataBinding
android {
        ...
        dataBinding {
            enabled = true
        }
    }
Copy the code
Basic usage
  • Define data and methods in the ViewModel
class DataBindingViewModel : ViewModel() {

    val userName = MutableLiveData<String>()
    val clickTimes = MutableLiveData<Int>()
    val sexCheckId = MutableLiveData<Int>()
    val love = MutableLiveData<String>()
    
    fun save(){
        LogUtil.e("${userName.value}---${sex.value}---${love.value}")
    }
}
Copy the code
  • References and calls in XML
<data> <import type="android.view.View" /> <import type="com.haikun.jetpackapp.home.R" /> <variable name="viewModel" type="com.haikun.jetpackapp.home.ui.demo.databinding.DataBindingViewModel" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{viewModel.userName}" /> <RadioGroup android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:checkedButton="@={viewModel.sexCheckId}" android:orientation="horizontal"> <RadioButton android:id="@+id/rb1" Android :layout_width="wrap_content" Android: Layout_height ="wrap_content" Android :text="男" /> <RadioButton Android :id="@+id/rb2" Android :layout_width="wrap_content" Android :layout_height="wrap_content" Android :layout_height="wrap_content" </RadioGroup> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@={viewModel.love}" android:visibility="@{viewModel.sexCheckId==R.id.rb1?View.VISIBLE:View.GONE}" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" Android :onClick="@{v->viewModel.save()}" Android :text=" save "/> </LinearLayout>Copy the code
  • Fragment
class DataBindingFragment : Fragment() { private val mViewModel: DataBindingViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle? ) : View { val dataBinding = DataBindingUtil.inflate<FragmentDataBindingBinding>( inflater, R.layout.fragment_data_binding, Container, false) // lifecycleOwner must be set to use liveData, Otherwise unable to update the data dataBinding. LifecycleOwner = viewLifecycleOwner dataBinding. The viewModel = mViewModel return dataBinding. Root}}Copy the code
Advanced usage
  • BindingMethods Binding method name
@BindingMethods(value = [BindingMethod(type = MyButton::class, attribute = "maxTimes", method = "setMaxTimes")])
Copy the code

XML is used in

app:maxTimes="@{15}"
Copy the code
  • BindingAdapter provides custom logic

Some properties require custom binding logic. For example, the Android :paddingLeft feature does not have associated setters, but instead provides setPadding(left, top, right, bottom) methods. Statically bound adapter methods using BindingAdapter annotations support the invocation of custom property setters.

object ViewAdapter {
    @BindingAdapter("minTimes")
    @JvmStatic
    fun setMinTimes(view: MyButton, minTimes: Int) {
        view.setMin(minTimes)
    }
}
Copy the code

XML is used in

app:minTimes="@{8}"
Copy the code
  • Custom bidirectional binding
@InverseBindingAdapter(attribute = "clickTimes") @JvmStatic fun getClickTimes(view: MyButton): Int { return view.clickTimes } @BindingAdapter("clickTimesAttrChanged") @JvmStatic fun setListener(view: MyButton, listener: InverseBindingListener?) { view.onTimesChangeListener = { listener? .onChange() } }Copy the code

XML using

app:clickTimes="@={viewModel.clickTimes}"
Copy the code

Compose—- The future of Android declarative UI

  • In mid-2019, Google announced its latest UI framework for Android, Jetpack Compose, at I/O. Compose is arguably the largest library of official Android actions ever. It was announced in mid-2019, but won’t be officially released until 2021. What has the Android team been doing for two years? In the development of this library, in the development of Compose. Why spend two years building a UI framework? Because Compose doesn’t create one or more advanced UI controls like RecyclerView and ConstraintLayout, it simply does away with views and viewgroups that we’ve been writing about for years. A whole new UI framework from top to bottom. To put it bluntly, the rendering mechanics, layout mechanics, touch algorithms, and the way the UI is written are all new.
  • At first sight, the first impression of Compose is that it is surprisingly similar to Flutter. Compose requires systematic learning and a high learning cost, which is not described here
  • Compose only supports Kotlin and currently needs to be developed with the Canary version of Android Studio

The databaseRoom

Room provides an abstraction layer on TOP of SQLite to allow smooth access to the database while taking full advantage of SQLite’s power. Room can be used with Kotlin coroutines/Flows. Room can be used with LiveData

Room consists of 3 main components:

  • Database: Contains the database holder and serves as the primary access point to the underlying connection to apply retained persistent relational data.
  • Entity: indicates a table in the database.
  • DAO: Contains methods used to access the database.

Add the dependent

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"
 
  implementation "androidx.room:room-ktx:$room_version"
Copy the code

Basic usage

  • Define the Entity
@Entity
data class Car(
    @PrimaryKey(autoGenerate = true) val id: Long,
    var name: String,
    val color: String,
)
Copy the code

@primarykey the class name of the PrimaryKey is the table name. You can also set the table name in @entity (table=). You can Ignore a field using @ignore

  • Define the Dao
@Dao
interface CarDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertCar(car: Car):Long

    @Delete
    fun deleteCar(car: Car)

    @Update
    fun updateCar(car: Car)

    @Query("SELECT * From Car")
    fun queryCarList(): MutableList<Car>

    @Query("Select * from Car where id=:id")
    fun queryCarById(id: Long): Car?
}
Copy the code
  • Define the Database
@Database(entities = [Car::class], version = 1,exportSchema = false)
abstract class DemoDatabase : RoomDatabase() {

    abstract fun carDao(): CarDao
    
}
Copy the code
  • Create the Database and get the Dao
    private val db: DemoDatabase by lazy {
        Room.databaseBuilder(
            JetpackApp.getContext(),
            DemoDatabase::class.java, "demo-database"
        ).build()
    }

    private val carDao: CarDao by lazy {
        db.carDao()
    }
Copy the code
  • Add and delete
carDao.insertCar(car)
carDao.delete(car)
carDao.updateCar(car)
val car = carDao.queryCarById(mUpdateId)
Copy the code

Room does not support accessing the database on the main thread, unless the allowMainThreadQueries() method is called on Builder, which is likely to lock the UI for a longer period of time. However, asynchronous queries — queries that return instances of LiveData/Flowable — are exempted from this rule because they run the query asynchronously in background threads when needed.

  • Use the Flow Flow for reactive queries

As soon as any data in the table changes, the returned Flow object triggers the query again and reissues the entire result set.

Reactive queries using Flow have one important limitation: Whenever any row in the table is updated (whether or not it is in the result set), the Flow object rerun the query. Applying the distinctUntilChanged() operator to the returned Flow object ensures that the interface is notified only when the actual query result changes:

@Query("Select * From Car where id = :id") fun queryCarAsFlowById(id: Long): Flow<Car> fun queryCarAsFlowByIdDistinctUntilChanged(id: Long): Flow<Car? > = queryCarAsFlowById(id).distinctUntilChanged()Copy the code
  • Use Kotlin coroutines for asynchronous queries

Add the suspend Kotlin keyword to your DAO methods to make them asynchronous using the Kotlin coroutine functionality. This ensures that these methods are not executed on the main thread.

  • Use LiveData for observable queries
@Query("Select * From Car where id = :id") fun queryCarAsLiveDataById(id: Long): LiveData<Car? >Copy the code

Relationships between objects

Nested relations
One to one
More than a pair of
Many to many

Take one-to-many, for example

  • Define entities and relationships
@Entity
data class One(@PrimaryKey(autoGenerate = true) val id: Long, val name: String)

@Entity
data class More(@PrimaryKey(autoGenerate = true) val id: Long, val oneId: Long, val name: String)

data class OneAndMore(
    @Embedded val one: One,
    @Relation(
        parentColumn = "id",
        entityColumn = "oneId"
    ) val moreList: MutableList<More>
)
Copy the code
  • Define the Dao

Add the @Transaction annotation to ensure that the entire operation is performed atomically.

    @Transaction
    open fun insertOneAndMore(){
        val one = One(0, "OneName")
        val insertOneId = insertOne(one)
        val more = More(0, insertOneId, "moreName1")
        val more1 = More(0, insertOneId, "moreName2")
        insertMore(more)
        insertMore(more1)
    }

    @Transaction
    @Query("Select * from One")
    abstract fun queryOneAndMore():MutableList<OneAndMore>
Copy the code

Use type converters to process complex data

Sometimes you need to use custom data types that contain values that you want to store in a single database column. TypeConverter can be converted back and forth between custom classes and known types that Room can preserve.

For example, you need to save an object containing a List to a database

  • Define the Entity
@Entity
data class ComplexEntity(
    @PrimaryKey val id: Long,
    val list: MutableList<OneAndMore>
)
Copy the code
  • Define the Converter
class Converters {
    @TypeConverter
    fun fromJson(value: String): MutableList<OneAndMore>? {
        val types =
            Types.newParameterizedType(MutableList::class.java, OneAndMore::class.java)
        return MoshiInstance.moshi.adapter<MutableList<OneAndMore>>(types).fromJson(value)
    }

    @TypeConverter
    fun toJson(list: MutableList<OneAndMore>): String {
        val types =
            Types.newParameterizedType(MutableList::class.java, OneAndMore::class.java)
        return MoshiInstance.moshi.adapter<MutableList<OneAndMore>>(types).toJson(list)
    }
}
Copy the code
  • Add Converter to Database
@TypeConverters(Converters::class)
abstract class DemoDatabase : RoomDatabase()
Copy the code
  • Dao
    @Insert
    abstract fun insertComplexEntity(complexEntity: ComplexEntity)

    @Query("Select * from ComplexEntity")
    abstract fun queryComplexEntity():MutableList<ComplexEntity>
Copy the code

Dependency injectionKoin

Reference dependency injection (DI) is a widely used programming technique. Hilt, Dagger, Koin, etc are all dependency injection libraries. Dependency injection is one of the best architectural patterns in object-oriented design.

  • Dependency injection libraries automatically release objects that are no longer used, reducing overuse of resources.
  • Reusable dependencies and instances created within the specified scope improve code reusability and reduce a lot of template code.
  • Code becomes more readable.
  • Easy to build objects.
  • Write low-coupling code that is easier to test.

Why Koin(and not Jetpack Hilt component)

  • Pure Kotlin
  • Koin is easy to get startedHilt is much more difficult to use than Koin, with a higher threshold of entry, more Hilt code for the dependency injection part than Koin, and more and more complex code is required in a larger and more complex project
  • Koin compilation time is shortThis is very important in large projects
  • Project structure: Dependency injection for Hilt often requires more files than for Koin
  • Lines of code: Hilt generates more code than Koin, and more as the project gets more complex.
  • There are no annotations, no Kapt, no reflection, and kotlin’s powerful syntactic sugar (Inline, Reified, etc.) and functional programming
  • Hilt requires Dagger support in the dynamic module FDM, and Koin can be used in FDM

Add the dependent

    const val KOIN_SCOPE = "org.koin:koin-androidx-scope:${CoreVersion.KOIN}"
    const val KOIN_VIEWMODEL = "org.koin:koin-androidx-viewmodel:${CoreVersion.KOIN}"
    const val KOIN_FRAGMENT = "org.koin:koin-androidx-fragment:${CoreVersion.KOIN}"
Copy the code

Koin use

  • Create dependent objects in Module
object KoinModule { val module: Module = Module {// create factory {SuperStar(named("zhan ")) {SuperStar(" zhan ", named("zhan ")) { } // named single {Fans(" girl ", get())} // named single {Fans(" boy") {Fans(" boy"), Get (named("zhanZhan"))} // create scope(named(" namespace 1")) {scoped {SuperStar(" SuperStar ", "male ")} scoped {Fans(" zhanZhan"), KoinViewModel(get("boy")))} scope<KoinFragment> {// Create viewModel in scope viewModel { KoinViewModel(get(named("boy"))) } } } }Copy the code
  • startKoin
class JetpackApp: Application() {

    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger()
            androidContext(this@JetpackApp)
            modules(KoinModule.module)
        }
    }
}
Copy the code
  • Inject dependencies where needed
Class KoinFragment: ScopeFragment() {// Inject normal val wangYiBo: -- named val xiaoZhan by inject<SuperStar>(named("zhanZhan")) Named val boy: Fans by inject() // Named val boy: Fans by inject(named("boy")) KoinViewModel by viewModel() Private val scope1 by lazy {getKoin().getorcreatescope (" scope1 ", Named (" scope1 "))} // inject val fans by scope1.inject< fans >()...... }Copy the code

Paging libraryPaging

Paging is a Paging loading library developed by Google and applied to the Android platform. It can load and display multiple small data blocks at a time. Loading partial data on demand reduces network bandwidth and system resource usage.

The usage of Paging —- is complex and requires a longer lengthGuo Shen’s public number content

A kind of realization idea of componentized application

Componentization principle – using Navigation + dynamic-feature-module

  • dynamic-features-module

Android App Bundle is an official dynamic distribution scheme launched in 2018, which is similar to various domestic plug-in schemes. However, it requires Google Play Store support, which makes it unavailable in China

As the navigation component supports navigation between dynamic feature modules, we can use dynamic feature Module to split functional modules to realize componentization. In the development stage, we can select a module for compilation and installation if necessary.

  • Traditional APP dependencies:

  • Componentized APP dependencies:

New dynamicFeature module

  • Create a New Module and select Dynamic Feature Module or Instant Dynamic Feature Module

  • Select include Module at install-time

Use the navigationGlobal operationsNavigate through different dynamicFeature Modules

  • Create a new navigation folder under the RES folder in your app, and in that folder create a new app_navigation. XML file. This file uses the include-dynamic tag to introduce the dynamicFeatureModule defined navigation
<? The XML version = "1.0" encoding = "utf-8"? > <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/app_navigation" app:startDestination="@id/navigation_home"> <! <include-dynamic android:id="@+id/navigation_login" app:moduleName="module_login" app:graphResName="login_navigation" /> <include-dynamic android:id="@+id/navigation_home" app:moduleName="module_home" app:graphResName="home_navigation" /> <! --moduleName specifies the corresponding Dynamic Future module--> <! <include-dynamic Android :id="@+id/navigation_mine" app:moduleName="module_mine" app:graphResName="mine_navigation" /> <! <action android:id="@+id/action_global_navigation_login" app:destination="@id/navigation_login"/> <action android:id="@+id/action_global_navigation_home" app:destination="@id/navigation_home"/> <action android:id="@+id/action_global_navigation_mine" app:destination="@id/navigation_mine"/> </navigation>Copy the code

Single Activity, multiple Fragment applications

Fragment Universal transition animation

    val navOptions = navOptions {
        anim {
            enter = R.anim.anim_fragment_in
            exit=R.anim.nav_default_exit_anim
            popExit = R.anim.anim_fragment_out
        }
    }
    findNavController().navigate(directions, navOptions)
Copy the code

Fragment return parameter

  • Define a shareViewModel for passing parameters between fragments
    val fragmentResultViewModel: FragmentStateViewModel by sharedViewModel()
Copy the code
  • Add a destination switch listener to the desired fragment
    val onDestinationChangedListener by lazy {
        NavController.OnDestinationChangedListener { _, _, _ ->
            onFragmentResult(fragmentResultViewModel.resultBundle)
            fragmentResultViewModel.resultBundle.clear()
        }
    }
Copy the code
findNavController().addOnDestinationChangedListener(onDestinationChangedListener)
Copy the code
  • Setting return parameters
getResultBundle().putString("testKey","testValue")
Copy the code
  • Get return parameters
override fun onFragmentResult(data: Bundle) { data.getString("testKey")? .let { LogUtil.e(it) } }Copy the code

Technology stack

  • kotlin

  • navigation

  • ViewModel

  • LiveData

  • DataStore

  • DataBinding

  • Paging

  • Room

  • Koin

  • Coil Kotlin’s image loading framework

  • Moshi json parsing

  • Dynamic feature module

  • Retrofit network request

  • MVVM

Existing problems

BottomNavigationView (Navigation+BottomNavigationView

The BottomNavigationView contains two fragments A and B. When A clicks B, it jumps to B. At this time, B is added to the stack, and then clicks A to return to A, but B will be removed from the stack, which is not in line with our expectation.

The official recommendation is to do so

This is a question many have asked, and the authorities have been following up on the foot-dragging

Room Database is defined in APP module, Dao and Entity cannot be defined in Dynamic Feature Modules

Because the definition in DFM cannot be accessed in App, the AppDatabase defined in App cannot access the Dao and Entity of DFM

Solution 1
  • The official recommendation is to define a Database official recommendation in each module

But doing so would result in cross-module relationships being unusable

Generating one database per module affects performance

Solution 2
  • Define Dao and Entity in App module

But doing so leaves persistent data between modules unisolated