开始前我们先回答几个问题
1.Jetpack是什么/怎么用?
2.android-sunflower-0.1.6是什么?
问题一:
问题二:
长征第一步
地址:https://github.com/googlesamples/android-sunflower
--------------------------------------进入正题---------------------------------------
衔接上篇
JetPack控件LifeCycles(基于android-sunflower-0.1.6)
从植物列表查询开始吧
val factory = InjectorUtils.providePlantListViewModelFactory(context)
fun providePlantListViewModelFactory(context: Context): PlantListViewModelFactory {
val repository = getPlantRepository(context)
return PlantListViewModelFactory(repository)
}
private fun getPlantRepository(context: Context): PlantRepository {
return PlantRepository.getInstance(AppDatabase.getInstance(context).plantDao())
}
class PlantListViewModelFactory(
private val repository: PlantRepository
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>) = PlantListViewModel(repository) as T
}
上面的操作就是想把PlantRepository集成到PlantListViewModel中去.
这里我们只关心代码
PlantRepository.getInstance(AppDatabase.getInstance(context).plantDao())
//类似于Retrofit注解式编程
//exportschema表示是否支持保留历史版本
//https://developer.android.com/reference/android/arch/persistence/room/Database#exportschema
@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 Kotlin的单例
@Volatile private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
//枷锁
return instance ?: synchronized(this) {
//类似于apply 不过有it实例
instance ?: buildDatabase(context).also { instance = it }
}
}
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)
//WorkManager 放在后面讨论 我们只需要知道是线程调度即可
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance().enqueue(request)
}
})
.build()
}
}
}
思考两个问题
问题1:google一下发现是固定写法https://developer.android.com/reference/android/arch/persistence/room/Database
问题2:之前学Databinding也有这个类,用法类似,而这里是给Room附加一个或多个工具类
https://developer.android.com/reference/android/arch/persistence/room/TypeConverters
在GardenPlanting中你会发现Calendar是不能存入数据库的,一定是Converters做到的,具体实现可以看源码,哎让人又爱又恨的注解式编程。
@ColumnInfo(name = "last_watering_date")
val lastWateringDate: Calendar = Calendar.getInstance()
class Converters {
@TypeConverter fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis
@TypeConverter fun datestampToCalendar(value: Long): Calendar =
Calendar.getInstance().apply { timeInMillis = value }
}
坐标 :GardenPlantingDao
可以用SpringMVC的注解去理解
注解 | 意思 |
---|---|
@Dao | 标志Dao |
@Query | 查 |
@Insert | 增 |
@Delete | 删 |
@Transaction | 事务 发生错误会回滚 |
@Entity | 申明表 |
@ColumnInfo | 列 |
@PrimaryKey | 主键 |
标志 | 意思 |
---|---|
name | 列名 |
tableName | 表名 |
foreignKeys | 外键 |
childColumns | The list of column names in the current Entity. |
parentColumns | The list of column names in the parent Entity. |
autoGenerate | 用于主键 自动生成 |
onConflict = OnConflictStrategy.REPLACE | 插入冲突替换 |
//外键为Plant的id 主键为自己(GardenPlanting)的plant_id
foreignKeys = [ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])],
理解就好了
获取 AppDatabase.getInstance(context).plantDao()之后
PlantRepository也只不过是写了一个单例而加逻辑。
//获取植物列表
fun getPlants() = plantDao.getPlants()
//作者花了很大的功夫将Repository集成到ViewModel中,
//就是为了初始化调用获取最终的LiveData>
private val plantList = MediatorLiveData<List<Plant>>()
init {
growZoneNumber.value = NO_GROW_ZONE
//和map的不同在于会随着growZoneNumber的改变而返回不同的livePlantList
//要知道初始化后livePlantList是不能变化的
val livePlantList = Transformations.switchMap(growZoneNumber) {
if (it == NO_GROW_ZONE) {
plantRepository.getPlants()
} else {
plantRepository.getPlantsWithGrowZoneNumber(it)
}
}
plantList.addSource(livePlantList, plantList::setValue)
}
//监听plantList变化动态更新adapter
viewModel.getPlants().observe(viewLifecycleOwner, Observer { plants ->
if (plants != null) adapter.submitList(plants)
})
这里就算livePlantList 改变也不应该布局跟着变化啊,更何况setLifecycleOwner也没有调用啊,问题就出在
MediatorLiveData上面
解答:不是不想设置setLifecycleOwner,而是当前页面除了RecyclerView没有其他布局,所以就算是设置了Databinding动态设置也体现不出来。并且RecyclerView更新需要手动调用setAdapter.
遇到一个错误,修改版本号 version
Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
又遇到一个问题
解决:addMigrations
https://www.jianshu.com/p/df48fb35a1fe
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().enqueue(request)
}
}).allowMainThreadQueries()
.addMigrations(MIGRATION_1_2)
.build()
}
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// database.execSQL("ALTER TABLE department " + " ADD COLUMN phone_num TEXT")
}
}
迁移异常
A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
处理:纯MySql语法貌似不好用,请参考官方版本迁移教程
https://developer.android.com/training/data-storage/room/migrating-db-versions.html
代码如下
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
"PRIMARY KEY(`id`))")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
}
}
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
错误 ( notNull=false )
Migration didn't properly handle about
解决:加入 NOT NULL
https://stackoverflow.com/questions/46372036/room-database-migration-doesnt-properly-handle-alter-table-migration
例如:我的错误
睁大眼睛看看 原来是 id没有申明非空
java.lang.IllegalStateException: Migration didn't properly handle about(com.google.samples.apps.sunflower.data.About).
Expected:
TableInfo{name='about', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}, imageUrl=Column{name='imageUrl', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}, description=Column{name='description', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, year=Column{name='year', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
Found:
TableInfo{name='about', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}, imageUrl=Column{name='imageUrl', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}, description=Column{name='description', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1}, year=Column{name='year', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
我的SQL
database.apply {
execSQL("CREATE TABLE `about` (" +
" `id` INTEGER NOT NULL, " +
" `name` TEXT NOT NULL ," +
" `year` INTEGER NOT NULL ," +
" `description` TEXT NOT NULL ," +
" `imageUrl` TEXT NOT NULL ," +
"PRIMARY KEY(`id`))"
)
}
About
@Entity(tableName = "about")
data class About(
// @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "year") val year: Int,
@ColumnInfo(name = "description") var description: String,
@ColumnInfo(name = "imageUrl") var imageUrl: String = ""
) {
override fun toString() = name
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0
}
可以发现Bean对象和SQL有严格对照关系,
有位大神做了个对比脚本,感兴趣的去试试
https://hrankit.github.io/RoomSQLiteDifferenceFinder/
注意:注解编程通常是在编译期报错,很难定位错误地点,从而增加了代码的审查难度,所以爱建议避免大量的复制粘贴,保持随时编译的习惯。
参考资料
https://developer.android.com/training/data-storage/room/accessing-data#query-collection-args
源代码:
http://yun.xiaomakj.cn/owncloud/index.php/s/Ib7Rt6StWKS1s4U
ZJSD2
aboutViewModel.all.observe(viewLifecycleOwner, Observer { result ->
if (result != null && result.isNotEmpty()) {
aboutArrayList = result as ArrayList<About>
aboutArrayList.reverse()
adapter.notifyDataSetChanged()
}
})
//AboutViewModel中
var all: LiveData<List<About>> = aboutRepository.getAlls()
//AboutRepository中
fun getAlls() = aboutDao.getAllInfos()
// AboutDao中
@Transaction
@Query("SELECT * FROM about")
fun getAllInfos(): LiveData<List<About>>
执行顺序:是由下而上的,也就是说observe函数会被自动执行,这是因为LiveData的执行机制造成的,满足以下三个条件即执行
JetPack控件WorkManager(基于Mixin Messenger)