git clone https://github.com/android/codelab-android-compose
Compose 中的基本布局BasicLayoutsCodelab文件夹
迁移到 Jetpack ComposeMigrationCodelab文件夹
使用 Room 持久保留数据android-basics-kotlin-inventory-app文件夹 github地址
AndroidRoom 数据库知识点
/**
* 创建表
*/
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int,
)
/**
* 格式化数据
*/
fun Item.getFormattedPrice(): String =
NumberFormat.getCurrencyInstance().format(itemPrice)
@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() = name
}
@Entity(
tableName = "garden_plantings",
foreignKeys = [
ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
],
indices = [Index("plant_id")]
)
data class GardenPlanting(
@ColumnInfo(name = "plant_id") val plantId: String,
/**
* 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
}
/**
* 数据访问对象
*/
@Dao
interface ItemDao {
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
// 查询对象
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
// 插入对象
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
// 更新对象
@Update
suspend fun update(item: Item)
// 删除对象
@Delete
suspend fun delete(item: Item)
}
@Dao
interface PlantDao {
@Query("SELECT * FROM plants ORDER BY name")
fun getPlants(): LiveData<List<Plant>>
@Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): LiveData<List<Plant>>
@Query("SELECT * FROM plants WHERE id = :plantId")
fun getPlant(plantId: String): LiveData<Plant>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(plants: List<Plant>)
}
@Dao
interface GardenPlantingDao {
@Query("SELECT * FROM garden_plantings")
fun getGardenPlantings(): LiveData<List<GardenPlanting>>
@Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
fun isPlanted(plantId: String): LiveData<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(): LiveData<List<PlantAndGardenPlantings>>
@Insert
fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long
@Delete
fun deleteGardenPlanting(gardenPlanting: GardenPlanting)
}
// 读取assets文件并存储数据
class SeedDatabaseWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = coroutineScope {
try {
applicationContext.assets.open(PLANT_DATA_FILENAME).use { inputStream ->
JsonReader(inputStream.reader()).use { jsonReader ->
val plantType = object : TypeToken<List<Plant>>() {}.type
val plantList: List<Plant> = Gson().fromJson(jsonReader, plantType)
val database = AppDatabase.getInstance(applicationContext)
database.plantDao().insertAll(plantList)
Result.success()
}
}
} catch (ex: Exception) {
Log.e(TAG, "Error seeding database", ex)
Result.failure()
}
}
companion object {
private const val TAG = "SeedDatabaseWorker"
}
}
/**
* 创刊数据库单例
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
// synchronized 块内获取数据库意味着一次只有一个执行线程可以进入此代码块
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
// Wipes and rebuilds instead of migrating if no Migration object.
// Migration is not part of this codelab.
// 将所需的迁移策略添加到构建器中
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun gardenPlantingDao(): GardenPlantingDao
abstract fun plantDao(): PlantDao
companion object {
// For Singleton instantiation
@Volatile private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return instance ?: 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)
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance(context).enqueue(request)
}
})
.build()
}
}
}
class InventoryApplication : Application() {
// Using by lazy so the database is only created when needed
// rather than when the application starts
//使用 lazy 委托,让实例 database 的创建延迟到您首次需要/访问引用时(而不是在应用启动时创建)。这样将在首次访问时创建数据库(磁盘上的物理数据库)。
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
/**
* View Model 中更新数据库
*
*/
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {
// Cache all items form the database using LiveData.
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()
/**
* Returns true if stock is available to sell, false otherwise.
*/
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
/**
* Updates an existing Item in the database.
*/
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
/**
* Launching a new coroutine to update an item in a non-blocking way
*/
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
/**
* Decreases the stock by one unit and updates the database.
*/
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
// Decrease the quantity by 1
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
/**
* Inserts the new Item into database.
*/
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
/**
* Launching a new coroutine to insert an item in a non-blocking way
*/
private fun insertItem(item: Item) {
// 如需在主线程之外与数据库交互,请启动协程并在其中调用 DAO 方法。在 insertItem() 方法内,使用 viewModelScope.launch 在 ViewModelScope 中启动协程。在 launch 函数内,对 itemDao 调用挂起函数 insert(),并传入 item。ViewModelScope 是 ViewModel 类的扩展属性,用于在 ViewModel 被销毁时自动取消其子协程。
viewModelScope.launch {
itemDao.insert(item)
}
}
/**
* Launching a new coroutine to delete an item in a non-blocking way
*/
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
/**
* Retrieve an item from the repository.
*/
fun retrieveItem(id: Int): LiveData<Item> {
return itemDao.getItem(id).asLiveData()
}
/**
* Returns true if the EditTexts are not empty
*/
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
/**
* Returns an instance of the [Item] entity class with the item info entered by the user.
* This will be used to add a new entry to the Inventory database.
*/
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
/**
* Called to update an existing entry in the Inventory database.
* Returns an instance of the [Item] entity class with the item info updated by the user.
*/
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
return Item(
id = itemId,
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
}
/**
* Factory class to instantiate the [ViewModel] instance.
*/
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
// 界面中创建viewmodel就可以使用了
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
将 LiveData 与 ViewModel 配合使用android-basics-kotlin-unscramble-app文件夹
学习采用 Kotlin Flow 和 LiveData 的高级协程advanced-coroutines-codelab文件夹