This is the 22nd day of my participation in the August Wen Challenge.More challenges in August

In this chapter, we will learn about the database. Here we will learn about the Room library. Of course, there are other libraries that can operate on the database.

In general, non-UI-related data is either stored locally (a local file system, or a database to be created later for CriminalIntent) or on a Web server.

Official Room introduce address: developer.android.com/training/da…

Room architecture building library

Room is a library of Jetpack architecture components that provides an abstraction layer on TOP of SQLite for smooth database access while taking full advantage of SQLite’s power.

The Room API contains classes for defining databases and creating database instances. Annotation classes are used to determine which classes need to be stored in the database, which classes represent the database, and which classes specify database table access functions, among other things. The compiler handles annotation classes and generates database implementation code.

Start by adding dependencies to build. Gradle:

Plugins {id' com.android. Application 'id' kotlin-android' id' kotlin-kapt'} def room_version = "2.3.0" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version"Copy the code

Creating a database

  • Annotate the model class to make it a database entity

    Room builds a database table for the application based on the Entity class, annotates the Entity column with @Entity, and then hands it over to Room to create a database table.

    /** ** Crime Entity class ** @property ID * @Property title * @Property date Date * @Property isSolved @constructor Create empty Crime */ @entity data class Crime(@primaryKey val ID: UUID = UUID.randomUUID(), var title: String = "", var date: Date = Date(), var isSolved: Boolean = false, var requiresPolice: Boolean = false )Copy the code

    Entity is a class-level annotation that indicates that the annotated class defines one or more database table structures, where each record in the database table represents a Crime object. The attribute name corresponds to the table field name.

    The @primaryKey annotation specifies which field in the database is the primary key, which is unique and can be used to look up a single record.

  • Create a database class

    @Database(entities = [Crime::class], version = 1)
    abstract class CrimeDatabase : RoomDatabase() {
    }
    Copy the code

    The @database annotation tells Room that the CrimeDatabase class is the Database in the application.

    Parameter “1” represents a collection of entity classes that tells Room which entity class to use when creating and managing database tables. Parameter 2 indicates the database version.

  • 3. Create type converters to enable the database to process model data

    The background database engine of Room is SQLite (Structured Query Language)

    SQLite user manual: www.sqlite.org

    The type converter tells Room how to convert the specific type of data to be saved.

    Define the data type conversion class crimeTypeconverters.kt:

    class CrimeTypeConverters { @TypeConverter fun fromDate(date: Date?) : Long? { return date? .time } @TypeConverter fun toDate(millisSinceEpoch: Long?) : Date? { return millisSinceEpoch? .let { Date(it) } } @TypeConverter fun toUUID(uuid: String?) : UUID { return UUID.fromString(uuid) } @TypeConverter fun fromUUID(uuid: UUID?): String? { return uuid?.toString() } }Copy the code

    Add the @Typeconverters annotation to CrimeDatabase and pass in the CrimeTypeConverters class. This tells the database to use the CrimeTypeConverters function when converting data types.

    @Database(entities = [Crime::class], version = 1)
    @TypeConverters(CrimeTypeConverters::class)
    abstract class CrimeDatabase : RoomDatabase() {
    }
    Copy the code

Ok, database and database table definitions are complete!

Define database access objects

Add the CrimeDao. Kt interface class to define functions that operate on the database. The @DAO annotation tells Room that CrimeDao is a data access object, and Room will automatically generate implementation code for the functions in the CrimeDao interface.

@Dao
interface CrimeDao {

   @Query("SELECT * FROM crime")
   fun getCrimes(): List<Crime>

   @Query("SELECT * FROM crime WHERE id = (:id)")
   fun getCrime(id: UUID): Crime?
}
Copy the code
@Database(entities = [Crime::class], version = 1)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
    abstract fun crimeDao():CrimeDao
}
Copy the code

Use warehouse mode to access the database

A warehouse class is simply a set of logic that encapsulates a layer to access data from a single or multiple data sources. It determines how data is read and saved, whether from a local database or a remote server. The UI code gets the data to use directly from the repository, regardless of how it interacts with the database.

private const val DATABASE_NAME = "crime-database" class CrimeRepository private constructor(context: Context) { private val database: CrimeDatabase = Room.databaseBuilder( context.applicationContext, CrimeDatabase::class.java, DATABASE_NAME ).build() private val crimeDao = database.crimeDao() fun getCrimes():List<Crime> = crimeDao.getCrimes() fun getCrime(id:UUID):Crime? = crimeDao.getCrime(id) companion object { private var INSTANCE: CrimeRepository? = null fun initialize(context: Context) { if (INSTANCE == null) { INSTANCE = CrimeRepository(context) } } fun get(): CrimeRepository { return INSTANCE ? : throw IllegalStateException("CrimeRepository must be initialized") } } }Copy the code

The CrimeRepository is created using the singleton pattern and will have only one instance object throughout the application process.

class CriminalIntentApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        CrimeRepository.initialize(this)
    }

}
Copy the code

In CriminalIntentApplication for CrimeRepository initialization tasks. Remember in AndroidManifest. Put CriminalIntentApplication registered in the XML tags, replace the system default Application.

Test database access

This tells us that the database data is stored in the application, and then it is accessed directly. The crash occurs. Since this leads to the next section, time-consuming tasks such as the database should be put into child threads.

In the demo, we’re going to insert some data. The data source is not provided by loading a database file.

Application of thread

Room does not allow any database operations to be performed on the main thread. As strong behavior, the Room will be thrown. Java lang. An IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time. The exception.

A thread is a single sequence of executions. Code in a single thread executes incrementally. All Android applications start from the main thread. The main thread is not a predetermined execution sequence like a thread. Instead, it runs in an infinite loop, waiting for the user or the system to trigger events.

The main thread is sometimes called the UI thread because the events that are responded to are basically UI-related.

  • A background thread

    For example, database access is time-consuming. If the operation is performed on the main thread, waiting too long will cause the application not responding (ANR).

    All time-consuming tasks should be done on background threads. The UI can only be updated on the main thread.

Using LiveData

LiveData is an observable data store class.

Profile LiveData official address: developer.android.com/topic/libra…

Room native support works with LiveData.

Google developed LiveData to make it easier to transfer data between application modules, and it also supports data transfer between threads.

Configure query in Room DAO to return LiveData, Room will automatically perform query operation on background thread, and publish the result data to LiveData object after completion. Then configure the activity or fragment to observe the target LiveData object. This way, as soon as the LiveData being observed is ready, the activity or fragment is notified of the result on the main thread.

fun getCrimes():LiveData<List<Crime>> = crimeDao.getCrimes() fun getCrime(id:UUID):LiveData<Crime? > = crimeDao.getCrime(id)Copy the code
class CrimeListViewModel : ViewModel() { private val crimeRepository = CrimeRepository.get() val crimesLiseLiveData = crimeRepository.getCrimes() init { GlobalScope.launch { for (i in 0 until 100) { val crime :Crime = Crime() crime.title = "Crime #$i" crime.isSolved  = i % 2 ! = 0 crime.requiresPolice = i % 2 == 0 crimeRepository.insertCrimes(crime) } } } }Copy the code

The liveData.observe (LifecycleOwner, Observer) function is used to register an Observer for a LiveData instance, allowing the Observer to breathe and live with other components such as activities or fragments.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) crimeListViewModel.crimesLiseLiveData.observe( viewLifecycleOwner, Observer { crimes -> crimes? .let{ updateUI(crimes) } } ) }Copy the code

Fragment on AndroidX is a lifecycle owner. It implements the LifecycleOwner interface, which has a Lifecycle object that represents the fragment instance’s Lifecycle state.

Refer to the Github address at the end of the code.

Challenge exercise: Solve Schema warnings

If you look closely at the project’s build log, you’ll see a warning that the application does not provide a Schema export directory.

A database schema is a database structure that contains major elements: what tables are in the database, what columns are in these tables, and what relationships and constraints are among the tables.

Room supports exporting database schemas to a file. This is useful because you can save it in version control for version history control.

There are two ways to eliminate Schema warnings in the build log:

  • 1. Provide a schema file location for the @database annotation

    Add the following kapt{} code block to your app/build.gradle file:

    android { ... kapt{ arguments{ arg("room.schemaLocation", "... Address location..." )}}}Copy the code
  • 2. Disable schema export

    Set exportSchema to false

    @Database(entities = [Crime::class], version = 1, exportSchema = false)
    @TypeConverters(CrimeTypeConverters::class)
    abstract class CrimeDatabase : RoomDatabase() {
        abstract fun crimeDao():CrimeDao
    }
    Copy the code

Further study: singletons

Singletons mean that there is only one instance object in the entire application. This class is responsible for creating its own objects and ensuring that only a single object is created.

It can control the number of instances, save system resources, very convenient to use.

Because of its long life cycle, it is not suitable for persistent storage.

The emphasis here is not to abuse the singleton pattern.

Five ways to write singletons in Kotlin refer to the blog: juejin.cn/post/684490…

other

CriminalIntent project Demo address: github.com/visiongem/A…


🌈 follow me ac~ ❤️

Public account: Ni K Ni K