The use of the Room

Room provides an abstraction layer on top of SQLite to take full advantage of SQLite’s power while enjoying a more robust database access mechanism.

How to configure

. apply plugin: 'kotlin-kapt' ... dependencies { ... kapt "androidx.room:room-compiler:$rootProject.roomVersion" implementation "androidx.room:room-runtime:$rootProject.roomVersion" implementation "androidx.room:room-ktx:$rootProject.roomVersion" //implementation "Androidx. room: room-rxJava2 :$RootProject. roomVersion" // optional - RxJava3 support for Room //implementation "androidx.room:room-rxjava3:$rootProject.roomVersion" ... }Copy the code

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.

    Classes that use the @database annotation should meet the following criteria:

    • Is to extendRoomDatabaseAbstract class of.
    • Add a list of entities associated with the database to the comment.
    • Contains zero parameters and returns use@DaoAbstract methods of annotated classes.

    At runtime, you can call Room. DatabaseBuilder () or Room. InMemoryDatabaseBuilder () access to the Database instance.

  • Entity: indicates a table in the database.

  • DAO: Contains methods used to access the database.

The application uses the Room database to get the DATA access object (DAO) associated with the database. The application then uses each DAO to retrieve entities from the database, and then saves all changes to those entities back into the database. Finally, the application uses entities to get and set values corresponding to table columns in the database.

The relationship between different components of Room is shown in Figure 1:

Sunflower example:

The database

/** * The RoomDatabase class is generally a singleton class, because The cost of instantiating a RoomDatabase is high. Except in the case of multiple processes, we typically set up RoomDatabase as a singleton. * Add the list of entities GardenPlanting and Plant */ that are associated with the database to the annotations
@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun gardenPlantingDao(a): GardenPlantingDao  // This is a Dao class that queries tables in a database
    abstract fun plantDao(a): PlantDao  // This is a Dao class that queries tables in a database
​
    companion object {
​
        // For Singleton instantiation
        @Volatile private var instance: AppDatabase? = null
​
        // Create a singleton
        fun getInstance(context: Context): AppDatabase {
            returninstance ? : synchronized(this) { instance ? : buildDatabase(context).also { instance = it } } }// Create and pre-populate the database. See this article for more details:
        // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build()
        }
    }
}
Copy the code

Instance class

@Entity(
    tableName = "garden_plantings"ForeignKeys = [ForeignKey(entity = Plant::class, parentColumns = [)"id"], childColumns = ["plant_id"])]./ / foreign key
    indices = [Index("plant_id")] // Specifies the index list
)
data class GardenPlanting(
    // This column name is stored in the database as plant_id. We use plantId to access the column name. The column name is also case-insensitive
    @ColumnInfo(name = "plant_id") val plantId: String,
    //val plantId: String. The column name is plantId when stored in the database
​
    /** * Indicates when the [Plant] was planted. Used for showing notification when it's time * to harvest the plant. */
    @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),
​
    /** * Indicates when the [Plant] was last watered. Used for showing notification when it's * time to water the plant. */
    @ColumnInfo(name = "last_watering_date")
    val lastWateringDate: Calendar = Calendar.getInstance()
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var gardenPlantingId: Long = 0
}
​
This is the second entity class
@Entity(tableName = "plants")
data class Plant(
    @PrimaryKey @ColumnInfo(name = "id") val plantId: String,
    val name: String,
    val description: String,
    val growZoneNumber: Int.val wateringInterval: Int = 7.// how often the plant should be watered, in days
    val imageUrl: String = ""
) {
​
    /** * Determines if the plant should be watered. Returns true if [since]'s date > date of last * watering + watering Interval; false otherwise. */
    fun shouldBeWatered(since: Calendar, lastWateringDate: Calendar) =
        since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) }
​
    override fun toString(a) = name
}
​
​
Copy the code

DAO class

@Dao
interface GardenPlantingDao {
    @Query("SELECT * FROM garden_plantings")
    fun getGardenPlantings(a): Flow<List<GardenPlanting>>
​
    // Arguments passed by SQL statements are referenced directly with the: symbol.
    @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
    fun isPlanted(plantId: String): Flow<Boolean>
​
    /** * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle * the object mapping. * /
    @Transaction
    @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
    fun getPlantedGardens(a): Flow<List<PlantAndGardenPlantings>>
​
    // If we need to Insert data into a table, we can define a method and annotate it with @insert:
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long
​
    // Use the @delete annotation if you need to Delete the table data: use the primary key to find the entity to Delete.
    @Delete
    suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting)
    
    // There are also update annotations, which are not listed here
}
​
/** * The Data Access Object for the Plant class. */
@Dao
interface PlantDao {
    @Query("SELECT * FROM plants ORDER BY name")
    fun getPlants(a): Flow<List<Plant>>
​
    @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): Flow<List<Plant>>
​
    @Query("SELECT * FROM plants WHERE id = :plantId")
    fun getPlant(plantId: String): Flow<Plant>
​
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(plants: List<Plant>)
}
Copy the code

This is the whole database design. So how do you use it? Here is an example from Sunflow.

First, take a look at the architecture diagram of MVVM. It can be seen from the architecture diagram that the Repository layer directly interacts with Room. Because room provides data to the application. The Repository layer provides data to applications.

@Singleton
class PlantRepository @Inject constructor(private val plantDao: PlantDao) {
​
    fun getPlants(a) = plantDao.getPlants()   // The return value of getPlants() is Flow
      
       >, which is the return value of getPlants in PlantDao
      
​
    fun getPlant(plantId: String) = plantDao.getPlant(plantId)
​
    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int) =
        plantDao.getPlantsWithGrowZoneNumber(growZoneNumber)
}
​
Copy the code

First of all, this code may seem a little confusing to those of you who have not been exposed to the Hilt dependency injection library. But it doesn’t matter, all we need to know is that in our planet Repository, we’re going to go to the database and look up the relevant data and return it. How plantDao is instantiated, what @singleton means, what @inject means, all need not care. Now that the warehouse layer is in place, it’s time for the ViewModel to go to the warehouse and get things. So how does the ViewModel layer work

/** * The ViewModel for [PlantListFragment]. */
@HiltViewModel
class PlantListViewModel @Inject internal constructor(
    plantRepository: PlantRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    ...
    private val growZone: MutableStateFlow<Int> = MutableStateFlow(
        savedStateHandle.get(GROW_ZONE_SAVED_STATE_KEY) ? : NO_GROW_ZONE )val plants: LiveData<List<Plant>> = growZone.flatMapLatest { zone ->
        if (zone == NO_GROW_ZONE) {
            plantRepository.getPlants()
        } else {
            plantRepository.getPlantsWithGrowZoneNumber(zone)
        }
    }.asLiveData()
​
    init {
        viewModelScope.launch {
            growZone.collect { newGrowZone ->
                savedStateHandle.set(GROW_ZONE_SAVED_STATE_KEY, newGrowZone)
            }
        }
    }
    ...
}
​
Copy the code

The code in the ViewModel may seem confusing to many of you who are not familiar with Jetpack, but it doesn’t matter. We can simply analyze the plants method to see what it is used for. As for related flow or other knowledge points, you can ignore them for now.

Plants is a LivaData data type of data. The most important feature of LiveData data is that it actively notifies the observer of the LiveData if the data source changes. So plants are used for what? First plantRepository. GetPlants () and plantRepository getPlantsWithGrowZoneNumber (zone) is the inside of the plantRepository method, and the above said, The PlantRepository is just taking data from a database. Flow > LiveData > LiveData > So now I’m left with the UI layer to look at this plant and the whole link is complete.

@AndroidEntryPoint
class PlantListFragment : Fragment() {...private val viewModel: PlantListViewModel by viewModels()
​
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View {
        
        subscribeUi(adapter)
​
    }
    
    // View viewModel.plants and call Adapter.submitList (plants) when the viewModel plants change.
    private fun subscribeUi(adapter: PlantAdapter) {
        viewModel.plants.observe(viewLifecycleOwner) { plants ->
            adapter.submitList(plants)
        }
    }
    ...
}
​
Copy the code

Above is the simple use of room in the project. There is a more important piece of knowledge about room is the upgrade of the database version, this piece of knowledge online has a brother wrote very good. The link is posted below. If you don’t know anything about room, please leave a comment.

This blog post covers a lot of jetpack. After that, I will write some blogs about it, and welcome your attention. thank you

For details about how to upgrade the database version, see juejin.cn/post/684490…

Sunflower address: github.com/android/sun…