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外键
childColumnsThe list of column names in the current Entity.
parentColumnsThe 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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值