零蚀
[🔗 Jetpack官方文档]
[🔗 Jetpack教程基础篇]
这类基础知识学习,只要有正确官方文档,那就是照猫画虎的学习方式,建议自己遵照文档内容学习
目录
🔗 大纲
🔗 Android 知识栈
🔗 Android Jetpack 系列
🔗 NO.2 Jetpeck 基础<Fra导航 & ListAdapter>
🔗 NO.3 Jetpeck 基础<Retrofit & WorkManager>
前言
-
jetpack
-
项目的构建总是趋向于简化,在Android开发的道路上,开发人员也不断的将代码进行防呆处理和易修易改的调整,所以在各个程序员各显神通后,google也意识到其实代码的构建中开发者可能会无法避免的要考虑很多方面,而且这些细枝末节很可能导致应用出现故障,所以开始了有了自己一套框架体系,它将目前很多原本要注意的方面集合成一个个组件,而这些组件有一个共同的名称Jetpack。
-
文档的内容是根据网上的教程顺序,可能会省略一些Android中的常识。(抄文档有利于加深印象和思考),之前的文档的学习逻辑是有问题的,这里我们重新开始学习jetpack。
-
ViewModel 和 LiveData
-
ViewModel
-
ViewModel 和 LiveData的设计理念是数据驱动界面,通过数据的变更来更新Android界面的变化,这样可以有效避免,生命周期的变化对数据的影响,比如横竖屏,关闭打开应用导致应用数据的丢失。虽然我们可以通过
onSaveInstanceState()
来将数据进行保存,但是这种保存数据的量是非常少的。 -
这里的ViewModel是持有所有准备展示的数据(LiveData),ViewModel是数据展示的决策者,LiveData是需要展示的数据。
-
一个ViewModel需要和对应的UI控制器产生关联,这一步这个UI控制器需要持有这个ViewModel的引用,这里我设置为handleTestViewModle,当关联失效时ViewModel会触发
onCleared()
来清理资源。
open class TestViewModel : ViewModel(){ private lateinit var handlerTestViewModel: TestViewModel init { Log.e("zeroJp", "GameViewModel created!") } override fun onCleared() { super.onCleared() Log.e("zeroJp", "TestViewModel onCleared: " ) } }
-
viewModel是如何保存我们的数据的,它如何在旋转或者关闭打开应用之后还能做到数据不会被丢失,其实这个是通过ViewModelProvider。
-
在配置更改的期间,我们的ViewModel不是直接进行new 来创建一个实例,而是会先判断是否有已经存在的ViewModel。如果有,则复用。
-
ViewModelProvider创建的实例是有指定范围的,比如隶属哪个fragment,或者activity。只要作用域还存活,那么ViewModel就会被保留,例如,如果fragment,在没有发生detach前ViewModel不会被清理。
-
ViewModel的初始化是通过
ViewModelProvider.get()
来创建一个ViewModelProvider,它
// 创建一个的ViewModel关联到main activity的范围 ViewModelProvider(this).get(TestViewModel::class.java)
- 举个🌰,还是之前的横竖屏的问题,如果我们在activity中创建一个变量,每一秒自增,那该怎么做让他在切换时不会被重建。
// main activity val testModel = ViewModelProvider(this).get(TestViewModel::class.java) handler = Handler(mainLooper) { if(testModel.getValue()<100) { testModel.increase(text) handler.sendEmptyMessageDelayed(1,1000) } false } ...... // viewModel open class TestViewModel : ViewModel(){ private var i =0 open fun getValue() = i open fun increase(textView: TextView){ i++ textView.text = i.toString() } override fun onCleared() { super.onCleared() Log.e("zero", "onCleared: " ) } }
- 如果在初始化时需要处理ViewModel时候,一般会用到ViewModelFactory,当然一般可以在构建factory的时候传入对应的参数,用来初始化操作。
class TestViewModelFactory:ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(TestViewModel::class.java)) { return TestViewModel() as T }else{ throw IllegalArgumentException("Unknown ViewModel class") } } } // MainActivity val viewModelFactory = TestViewModelFactory() ViewModelProvider(this,viewModelFactory).get(TestViewModel::class.java)
-
-
LiveData
-
之前说了ViewModel它的作用就是在某些范围里面保存数据,而如果我们希望数据对生命周期具有感知,那么我们就会用到LiveData,如果学过埋点的相关知识,你会知道对生命周期感知,会用到LifecycleOwner,而LiveData正是如此。
-
当一个fragment被销毁时候(用户切换掉),即使fragment本身并没有被销毁,而我们使用fragment的生命周期,而不是视图的生命周期很可能发生问题,我们来看一下liveData的用法。
open class TestViewModel : ViewModel(){ open val data = MutableLiveData<Int>() init { data.value =0; } open fun getValue():Int?{ return data.value } open fun increase(textView: TextView,viewLifecycleOwner:LifecycleOwner){ data.value = data.value?.plus(1) // 递增1,minus()递减 data.observe(viewLifecycleOwner,{ textView.text = data.value.toString() }) } override fun onCleared() { super.onCleared() } }
-
这里我们可以看到,这里用到了观察者,如果观察到的话才改变UI渲染,如果感兴趣可以点进去,我们会发现,里面会做一个判断,如果持有者owner的状态变成Destory直接return了etc。
-
最后我们将dataBinding和ViewModel、LiveData联合在一起做一个案例。将所有的点击事件丢给ViewModel处理。
open class TestViewModel : ViewModel(){ open val data = MutableLiveData<Int>() init { data.value =0; } open fun getValue():Int?{ return data.value } open fun increase(textView: TextView,viewLifecycleOwner:LifecycleOwner){ data.value = data.value?.plus(1) // 递增1,minus()递减 data.observe(viewLifecycleOwner,{ // 观察到变化之间进行赋值 textView.text = data.value.toString() }) } open fun click(view:View){ "文本${(view as TextView).text}".toast } override fun onCleared() { super.onCleared() } }
- 在build中开启databinding之后我们在xml中写入我们的viewModel(这种侵入式看着真变扭)。然后添加点击事件。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="testData" type="com.tg.jetpack.model.TestViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/MainActivity" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".activity.MainActivity" tools:viewBindingIgnore="true"> <TextView android:id="@+id/text" android:text="@{testData.dataValue}" android:onClick="@{testData.click}" android:layout_width="100dp" android:layout_height="100dp" android:textColor="#000000" android:textSize="30sp" android:gravity="center" android:textStyle="bold" android:clickable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" android:focusable="true" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
- 将我们的viewModel和DataBinding进行绑定。
val model = ViewModelProvider(this).get(TestViewModel::class.java) val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) binding.testData = model //将xml中的testData定义为model
-
Room
-
Room 使用
- 上面简单的介绍了以下viewModel 和 UI的关系之后,文档介绍了数据库的操作,大体的结构如下。
- 在Android开发久了,可能就会觉得,为什么我们要使用sql语法来编写数据库,如果不是很常用到数据库的内容,学了又很容易忘记那些复杂的操作,而且sql语句如果打错了,也不会编译前就通知我们,所以jetpack就出了一个Room。
- 首先我们看一下如何创建表和字段,如下所示:
@Entity(tableName = "table_name_test") // 表名 data class TestData( @PrimaryKey(autoGenerate = true) // 自动生成的id主键 var id:Long = 0L, @ColumnInfo(name = "name_info") // 列名称 var name:String="", @ColumnInfo(name = "age_info") var age:Int = 0 )
- 然后我们要创建dao(Data Access Object数据存取对象),也就是数据操作的对象。
@Dao interface TestDataDao { @Insert // 这里插入和更新的内容对象必须是@Entity描述的对象 fun insert(data:TestData) @Update // 默认根据主键更新 fun update(data: TestData) @Query("SELECT * from table_name_test WHERE name_info = :name") fun query(name:String):TestData // 根据名字返回数据 @Query("DELETE FROM table_name_test") // 这样的查询不会删除表本身 fun clear() @Query("SELECT * FROM table_name_test ORDER BY id DESC") fun getAllNights(): LiveData<List<TestData>> //递减查询 }
- 然后有了表,有了操作对象,我们需要创建数据库。这里我们构建了一个
test_database
数据库。并关联上了TestData的表。这里如果你更改表字段内容时候,必须更新数据库的version字段,exportSchema是否保留版本备份。
@Database(entities = [TestData::class], version = 1, exportSchema = false) abstract class TestDataBase :RoomDatabase() { abstract val dao: TestDataDao companion object { @Volatile private var INSTANCE: TestDataBase? = null fun getInstance(context: Context): TestDataBase { synchronized(this) { var instance = INSTANCE if (instance == null) { instance = Room.databaseBuilder( context.applicationContext, TestDataBase::class.java, "test_database" ) .fallbackToDestructiveMigration() .build() INSTANCE = instance } return instance } } } }
- 数据库,表,dao都构建完毕之后,rebuild一下,这个时候在build中会生成XXX_impl,我这里使用也是很简单(注意开启权限,还有dao的操作要放在线程里,为了防止阻塞主线程,会报错)
val dataBase = TestDataBase.getInstance(this) val dao = dataBase.dao Thread { dao.insert(TestData(name = "李一", age = 19)) dao.insert(TestData(name = "王二", age = 20)) dao.insert(TestData(name = "薛三", age = 21)) dao.insert(TestData(name = "邱四", age = 22)) val query = dao.query(name = "王二") Log.e("zero", "这个人的名字是${query.name},id是${query.id},年龄是${query.age}") }.start() // print : 这个人的名字是王二,id是2,年龄是20
-
协程和Room
-
协程相关内容见**[🔗 Android kotlin 专题]** ,协程有两个特点非阻塞和步代码有序性,第一个特点就不多说了,原理就是开线程,切换线程,它的主要的功能是有序性,这个主要应用于一些复杂的逻辑上,比如需要在主线程和线程间来回切换,等上一个线程的结果才有下一个线程结果的话,那么就会产生所谓的死亡嵌套。
-
协程的大体思路是这样的,官网给的内容如下。
- 如此我们设计一个案例,因为Room的查询需要在IO线程中,查询结束再在主线程中进行打印。这里不再介绍
CoroutineScope
开启协程的方式。根据上面的案例,我们再添加进协程。
// Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
open class TestViewModel: ViewModel(){ open val data = MutableLiveData<Int>() private lateinit var context: Context private lateinit var dao:TestDataDao open fun addContext(context: Context) { this.context = context val dataBase = TestDataBase.getInstance(context) dao = dataBase.dao viewModelScope.launch(Dispatchers.IO) { // 在IO线程中插入 dao.insert(TestData(name = "李一", age = 19)) dao.insert(TestData(name = "王二", age = 20)) dao.insert(TestData(name = "薛三", age = 21)) dao.insert(TestData(name = "邱四", age = 22)) } } init { data.value =0 } val dataValue:String get() = data.value.toString() open fun increase(textView: TextView,viewLifecycleOwner:LifecycleOwner){ data.value = data.value?.plus(1) // 递增1,minus()递减 data.observe(viewLifecycleOwner,{ if(textView.text.length>4)return@observe textView.text = data.value.toString() }) } @SuppressLint("SetTextI18n") fun queryDao(name: String, text:TextView){ // launch默认是在主线程中的 viewModelScope.launch{ // 在IO线程中查询 val query = withContext(Dispatchers.IO){ dao.query(name) } // 在主线程中打印 text.text="查询结果,姓名:${query.name},年龄:${query.age},id:${query.id}" text.textSize = 10f } } open fun click(view:View){ queryDao("薛三",view as TextView) } override fun onCleared() { super.onCleared() } }
-