JetPack控件ROOM(基于android-sunflower-0.1.6)

前言:

开始前我们先回答几个问题

1.Jetpack是什么/怎么用?
2.android-sunflower-0.1.6是什么?

问题一:

  1. Jetpack是什么?
    *给出下图:*简单的概括为四大组件库,可以看到他提供的功能还是蛮实用的,没有多余的部分。
    是什么
  2. Jetpack怎么用?
    这个问题比较大一下说不清楚,我们从接下来的源码分析中,一步一步理解和掌握,有人会说既然做这么多年开发直接看文档不就能行了吗,你说的有道理,但是阅读外文文档学习确实不符合中国国情,这里就涉及到个人以及政治方面的原因了。也可以看到上面的模块之多,不是一日而语的,纸上读来终觉浅,绝知此事要躬行,我们需要慢慢来,我也是第一次在技术迁移上感受到这种焦虑感,Kotlin在Android开发当中的比重越来越大,还在使用JAVA的伙伴赶紧跟上脚步。

问题二:
长征第一步
地址:https://github.com/googlesamples/android-sunflower
--------------------------------------进入正题---------------------------------------
衔接上篇
JetPack控件LifeCycles(基于android-sunflower-0.1.6)

ROOM

从植物列表查询开始吧

坐标:PlantListFragment

val factory = InjectorUtils.providePlantListViewModelFactory(context)

坐标:InjectorUtils

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())
}

坐标:PlantListViewModelFactory

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())

坐标:AppDatabase

//类似于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. abstract fun gardenPlantingDao(): GardenPlantingDao 凭什么这样就可以得到Dao?
  2. TypeConverters是啥?

问题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 主键

坐标:GardenPlanting

标志 意思
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"])],

理解就好了

坐标:PlantRepository

获取 AppDatabase.getInstance(context).plantDao()之后
PlantRepository也只不过是写了一个单例而加逻辑。

//获取植物列表
fun getPlants() = plantDao.getPlants()

坐标:PlantListViewModel

//作者花了很大的功夫将Repository集成到ViewModel中,
//就是为了初始化调用获取最终的LiveData<List<Plant>>
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
效果

坐标:AboutFragment

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的执行机制造成的,满足以下三个条件即执行
执行条件

WorkManager

JetPack控件WorkManager(基于Mixin Messenger)

猜你喜欢

转载自blog.csdn.net/qq_20330595/article/details/87797828