项目地址: https://github.com/haikun-li/JetpackApp
(由于项目引入了jetpack compose,请使用最新的canary版本的Android studio打开项目)
目前我们使用Gradle构建语言是Groovy,但是Gradle实际上是支持Kotlin来编写Gradle构建脚本的,常见的构建脚本是.gradle结尾,而Koltin语法编写的脚本则是.gradle.kts,使用kotlin构建脚本的好处是可以有代码提示,和写一些扩展方法。
同时,我们可以使用buildSrc进行版本管理。
rootProject.buildFileName = "build.gradle.kts"
新建一个Android library,名字必须为buildSrc,创建之后会报这个名字的module已经存在,因为这个名字是保留名字,所以去setting.gradle中删掉include
buildSrc那一行即可。
buildSrc 中 build.gradle.kts内容
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}
Jetpack 是一个由多个库组成的套件,可帮助开发者遵循最佳做法,减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者精力集中编写重要的代码
navigation是一个框架,用于在 Android 应用中的“目标位置”之间导航,Android Jetpack的导航组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。导航组件还通过遵循一套既定原则来确保一致且可预测的用户体验。导航组件由以下三个关键部分组成:
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
buildscript {
repositories {
google()
}
dependencies {
def nav_version = "2.3.4"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
此外还需要在应用模块build.gradle添加
apply plugin: "androidx.navigation.safeargs.kotlin"
class NavigationActivity: AppCompatActivity(R.layout.activity_navigation)
android:name="androidx.navigation.fragment.NavHostFragment"这是依赖包中的fragment,作用是定义整个导航的起始
设置app:defaultNavHost=“true”,就会拦截系统的返回按钮,这时切换fragment会默认进行入栈
navGraph引用定义好的导航文件
findNavController().navigate(NavigationFragment1Directions.actionNavigationFragment1ToNavigationFragment22("测试"))
inner class MyLifecycleObserver(val lifecycle: Lifecycle) : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
CoroutineScope(scope).launch {
delay(3000)
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
LogUtil.e("开启定位")
}
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
LogUtil.e("关闭定位")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(MyLifecycleObserver(lifecycle))
}
class ViewModelViewModel: ViewModel() {
val userList = mutableListOf()
}
在fragment/activity中使用
val viewModel:ViewModelViewModel by viewModels()
viewModel.userList
在多个fragment中共享的ViewModel,使用by activityViewModels()创建的ViewModel依赖于Activity,在多个Fragment为同一个对象
val shareViewModel:ViewModelViewModel by activityViewModels()
或者使用koin依赖注入,koin使用说明在下文
val viewModel: ViewModelViewModel by viewModel()
val viewModel: ViewModelViewModel by sharedViewModel()
是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
private val normaLiveData1 = MutableLiveData()
normaLiveData1.value="LiveDataValue"//UI线程
normaLiveData1.postValue("LiveDataValue")//非UI主线程
normaLiveData1.observe(viewLifecycleOwner, Observer {
LogUtil.e("观察到第一个值发生了变化")
tv.text = it
})
val observer=Observer{
LogUtil.e("观察到第一个值发生了变化")
}
normaLiveData1.observeForever(observer)
//在合适的生命周期移除观察
normaLiveData1.removeObserver(observer)
private val transLiveData= Transformations.map(normaLiveData1){
"$it -----转换"
}
private val mediatorLiveData = MediatorLiveData()
mediatorLiveData.addSource(normaLiveData1){
mediatorLiveData.value="合并后的值:$it---${normaLiveData2.value}"
}
mediatorLiveData.addSource(normaLiveData2){
mediatorLiveData.value="合并后的值:${normaLiveData1.value}---$it"
}
Flow数据流以协程为基础构建,可提供多个值。从概念上来讲,数据流是可通过异步方式进行计算处理的一组数据序列,有点像RxJava。
val flow = flow {
emit("value1")
emit("value2")
delay(1000)
emit("value3")
}
val flow = flowOf("value")
val flow = listOf(1, 2, 3).asFlow()
scope.launch {
flow.collect {
LogUtil.e(it)
}
}
flowOf(1, 2, 3).map {
"第$it 个"
}.collect {
LogUtil.e(it)
}
flowOf(1, 2, 3).filter {
it > 1
}.collect {
LogUtil.e(it)
}
zip操作符会把 flow1 中的一个 item 和 flow2 中对应的一个 item 进行合并,如果 flow1 中 item 个数大于 flow2 中 item 个数,合并后新的 flow 的 item 个数 = 较小的 flow 的 item 个数
val flow1 = flowOf(1, 2, 3, 4, 5)
val flow2 = flowOf("一", "二", "三", "四", "五", "六")
flow1.zip(flow2) { a, b ->
"$a---$b"
}.collect {
LogUtil.e(it)
}
combine合并时,每次从 flow1 发出新的 item ,会将其与 flow2 的最新的 item 合并
val flow1 = flowOf(1, 2, 3, 4, 5).onEach { delay(1000) }
val flow2 = flowOf("一", "二", "三", "四", "五", "六").onEach { delay(500) }
flow1.combine(flow2) { a, b ->
"$a---$b"
}.collect {
LogUtil.e(it)
}
flow {
emit(1)
emit(1 / 0)
emit(2)
}.catch {
it.printStackTrace()
}.collect {
LogUtil.e(it)
}
withContext(Dispatchers.IO){
flowOf(1, 2, 3, 4).onEach {
//受到下面最近的flowOn控制-Main
LogUtil.e("init---${Thread.currentThread().name}")
}.filter {
//受到下面最近的flowOn控制-Main
LogUtil.e("filter---${Thread.currentThread().name}")
it > 1
}.flowOn(Dispatchers.Main).map {
//受到下面最近的flowOn控制-IO
LogUtil.e("map---${Thread.currentThread().name}")
"第$it"
}.flowOn(Dispatchers.IO).map {
//受到下面最近的flowOn控制-Main
LogUtil.e("第二次map---${Thread.currentThread().name}")
"$it 个结果"
}.flowOn(Dispatchers.Main).collect {
//collect要看整个flow处于哪个线程,此处为IO
LogUtil.e("collect---${Thread.currentThread().name}")
LogUtil.e(it)
}
}
"androidx.lifecycle:lifecycle-livedata-ktx:${LibraryVersion.LIVEDATA_KTX}"
flowOf(1, 2, 3, 4).asLiveData().observe(viewLifecycleOwner, Observer {
LogUtil.e(it)
})
更多操作符
DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
参考鸿洋的公众号内容
添加依赖
const val DATA_STORE = "1.0.0-alpha05"
const val PROTOBUF = "3.11.0"
"androidx.datastore:datastore-preferences:${LibraryVersion.DATA_STORE}"
//protobuf需下面的依赖
"androidx.datastore::datastore-core:${LibraryVersion.DATA_STORE}"
"com.google.protobuf:protobuf-java:${LibraryVersion.PROTOBUF}"
object DataStore {
private const val APP_DATA_STORE_NAME = "APP_DATA_STORE_NAME"
private lateinit var dataStore: DataStore
fun init(context: Context) {
dataStore = context.createDataStore(APP_DATA_STORE_NAME)
}
suspend fun save(key: Preferences.Key, value: T) {
dataStore.edit {
it[key] = value
}
}
suspend fun get(key: Preferences.Key): T? {
val value = dataStore.data.map {
it[key]
}
return value.first()
}
}
保存
CoroutineScope(scope).launch {
DataStore.save(preferencesKey("key1"), "aa")
}
读取
CoroutineScope(scope).launch {
val get = DataStore.get(preferencesKey("key1"))
}
protobuf相关知识不在这里展开叙述
syntax = "proto3";
option java_package = "com.haikun.jetpackapp.home.ui.demo.datastore.bean";
option java_multiple_files = true;
message MessageEvent {
int32 type = 1;
string message = 2;
}
object MessageSerializer : Serializer {
override val defaultValue: MessageEvent
get() = MessageEvent.getDefaultInstance()
override fun readFrom(input: InputStream): MessageEvent {
return MessageEvent.parseFrom(input)
}
override fun writeTo(t: MessageEvent, output: OutputStream) {
t.writeTo(output)
}
}
val createDataStore = context?.createDataStore("data", MessageSerializer)
createDataStore?.updateData {
it.toBuilder().setType(12).setMessage("消息").build()
}
CoroutineScope(scope).launch {
context?.createDataStore("data", MessageSerializer)?.data?.first()?.let {
LogUtil.e("${it.type}---${it.message}")
}
}
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源,可以使用 LiveData 对象作为数据绑定来源,自动将数据变化通知给界面 。
声明式UI 只需要把界面给「声明」出来,而不需要手动更新,只要声明的数据发生了变化,UI就跟着变化
命令式UI 需要主动让UI更新,比如setText()
android {
...
dataBinding {
enabled = true
}
}
class DataBindingViewModel : ViewModel() {
val userName = MutableLiveData()
val clickTimes = MutableLiveData()
val sexCheckId = MutableLiveData()
val love = MutableLiveData()
fun save(){
LogUtil.e("${userName.value}---${sex.value}---${love.value}")
}
}
class DataBindingFragment : Fragment() {
private val mViewModel: DataBindingViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val dataBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_data_binding,
container,
false
)
//使用liveData必须要设置lifecycleOwner,否则无法更新数据
dataBinding.lifecycleOwner = viewLifecycleOwner
dataBinding.viewModel = mViewModel
return dataBinding.root
}
}
@BindingMethods(value = [BindingMethod(type = MyButton::class, attribute = "maxTimes", method = "setMaxTimes")])
xml中使用
app:maxTimes="@{15}"
object ViewAdapter {
@BindingAdapter("minTimes")
@JvmStatic
fun setMinTimes(view: MyButton, minTimes: Int) {
view.setMin(minTimes)
}
}
xml中使用
app:minTimes="@{8}"
@InverseBindingAdapter(attribute = "clickTimes")
@JvmStatic
fun getClickTimes(view: MyButton): Int {
return view.clickTimes
}
@BindingAdapter("clickTimesAttrChanged")
@JvmStatic
fun setListener(view: MyButton, listener: InverseBindingListener?) {
view.onTimesChangeListener = {
listener?.onChange()
}
}
xml使用
app:clickTimes="@={viewModel.clickTimes}"
Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库
Room可以和kotlin协程/flow结合使用
Room可以和LiveData结合使用
Room 包含 3 个主要组件:
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
@Entity
data class Car(
@PrimaryKey(autoGenerate = true) val id: Long,
var name: String,
val color: String,
)
@PrimaryKey 主键
类名就是表名,也可以在@Entity(table=)设置表名
可以使用@Ignore忽略某个字段
@Dao
interface CarDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCar(car: Car):Long
@Delete
fun deleteCar(car: Car)
@Update
fun updateCar(car: Car)
@Query("SELECT * From Car")
fun queryCarList(): MutableList
@Query("Select * from Car where id=:id")
fun queryCarById(id: Long): Car?
}
@Database(entities = [Car::class], version = 1,exportSchema = false)
abstract class DemoDatabase : RoomDatabase() {
abstract fun carDao(): CarDao
}
private val db: DemoDatabase by lazy {
Room.databaseBuilder(
JetpackApp.getContext(),
DemoDatabase::class.java, "demo-database"
).build()
}
private val carDao: CarDao by lazy {
db.carDao()
}
carDao.insertCar(car)
carDao.delete(car)
carDao.updateCar(car)
val car = carDao.queryCarById(mUpdateId)
Room并不支持在主线程访问数据库, 除非在Builder调用allowMainThreadQueries()方法, 因为它很可能将UI锁上较长一段时间. 但是, 异步查询–返回LiveData/Flowable实例的查询–则从此规则中免除, 因为它们在需要的时候会在后台线程异步地运行查询.
使用 Flow 的响应式查询有一个重要限制:只要对表中的任何行进行更新(无论该行是否在结果集中),Flow 对象就会重新运行查询。通过将 distinctUntilChanged() 运算符应用于返回的 Flow 对象,可以确保仅在实际查询结果发生更改时通知界面:
@Query("Select * From Car where id = :id")
fun queryCarAsFlowById(id: Long): Flow
fun queryCarAsFlowByIdDistinctUntilChanged(id: Long): Flow =
queryCarAsFlowById(id).distinctUntilChanged()
使用 Kotlin 协程进行异步查询
将 suspend Kotlin 关键字添加到 DAO 方法中,以使用 Kotlin 协程功能使这些方法成为异步方法。这样可确保不会在主线程上执行这些方法。
使用 LiveData 进行可观察查询
@Query("Select * From Car where id = :id")
fun queryCarAsLiveDataById(id: Long): LiveData
以一对多为例
@Entity
data class One(@PrimaryKey(autoGenerate = true) val id: Long, val name: String)
@Entity
data class More(@PrimaryKey(autoGenerate = true) val id: Long, val oneId: Long, val name: String)
data class OneAndMore(
@Embedded val one: One,
@Relation(
parentColumn = "id",
entityColumn = "oneId"
) val moreList: MutableList
)
添加 @Transaction 注释,以确保整个操作以原子方式执行。
@Transaction
open fun insertOneAndMore(){
val one = One(0, "OneName")
val insertOneId = insertOne(one)
val more = More(0, insertOneId, "moreName1")
val more1 = More(0, insertOneId, "moreName2")
insertMore(more)
insertMore(more1)
}
@Transaction
@Query("Select * from One")
abstract fun queryOneAndMore():MutableList
有时需要使用自定义数据类型,其中包含想要存储到单个数据库列中的值。TypeConverter可以在自定义类与 Room 可以保留的已知类型之间来回转换。
例如需要把一个包含List的对象保存到数据库
@Entity
data class ComplexEntity(
@PrimaryKey val id: Long,
val list: MutableList
)
class Converters {
@TypeConverter
fun fromJson(value: String): MutableList? {
val types =
Types.newParameterizedType(MutableList::class.java, OneAndMore::class.java)
return MoshiInstance.moshi.adapter>(types).fromJson(value)
}
@TypeConverter
fun toJson(list: MutableList): String {
val types =
Types.newParameterizedType(MutableList::class.java, OneAndMore::class.java)
return MoshiInstance.moshi.adapter>(types).toJson(list)
}
}
@TypeConverters(Converters::class)
abstract class DemoDatabase : RoomDatabase()
@Insert
abstract fun insertComplexEntity(complexEntity: ComplexEntity)
@Query("Select * from ComplexEntity")
abstract fun queryComplexEntity():MutableList
参考
依赖项注入 (DI) 是一种广泛用于编程的技术,Hilt、Dagger、Koin 等等都是依赖注入库,依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点:
const val KOIN_SCOPE = "org.koin:koin-androidx-scope:${CoreVersion.KOIN}"
const val KOIN_VIEWMODEL = "org.koin:koin-androidx-viewmodel:${CoreVersion.KOIN}"
const val KOIN_FRAGMENT = "org.koin:koin-androidx-fragment:${CoreVersion.KOIN}"
object KoinModule {
val module: Module = module {
//普通创建
factory { SuperStar("王一博", "男") }
//普通创建 named为战战
factory(named("zhanZhan")) { SuperStar("肖战", "男") }
//单例创建
single { Fans("饭圈女孩", get()) }
//单例创建 named为boy
single(named("boy")) { Fans("饭圈男孩", get(named("zhanZhan"))) }
//用作用域创建
scope(named("作用域1")) {
scoped { SuperStar("蔡徐坤", "男") }
scoped { Fans("蔡徐坤的粉丝", get()) }
}
//创建viewModel
viewModel { KoinViewModel(get(named("boy"))) }
scope {
//作用域里创建viewModel
viewModel { KoinViewModel(get(named("boy"))) }
}
}
}
class JetpackApp: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@JetpackApp)
modules(KoinModule.module)
}
}
}
class KoinFragment : ScopeFragment() {
//注入普通对象
val wangYiBo: SuperStar by inject()
//注入普通对象-named
val xiaoZhan by inject(named("zhanZhan"))
//注入单例对象
val girl: Fans by inject()
//注入单例对象-named
val boy: Fans by inject(named("boy"))
//注入viewModel
val viewModel: KoinViewModel by viewModel()
//注入activity-viewModel
private val activityViewModel: KoinViewModel by sharedViewModel()
//生成作用域
private val scope1 by lazy {
getKoin().getOrCreateScope("作用域1", named("作用域1"))
}
//作用域注入
val fans by scope1.inject()
......
}
Paging是Google推出的一个应用于Android平台的分页加载库,可一次加载和显示多个小的数据块。按需载入部分数据会减少网络带宽和系统资源的使用量。
Android App Bundle 是官方 2018
年推出的动态发布方案,类似国内各种插件化方案。不过它需要 Google Play Store
支持,这导致在国内无法使用
借着 navigation 组件支持 dynamic feature module 间导航的契机,我们可以使用
dynamic feature module 来拆分功能模块以实现组件化,在开发阶段可以有需要的选择某个模块进行编译安装。
新建module,选择Dynamic Feature Module或者Instant Dynamic Feature Module
选择include module at install-time
val navOptions = navOptions {
anim {
enter = R.anim.anim_fragment_in
exit=R.anim.nav_default_exit_anim
popExit = R.anim.anim_fragment_out
}
}
findNavController().navigate(directions, navOptions)
val fragmentResultViewModel: FragmentStateViewModel by sharedViewModel()
val onDestinationChangedListener by lazy {
NavController.OnDestinationChangedListener { _, _, _ ->
onFragmentResult(fragmentResultViewModel.resultBundle)
fragmentResultViewModel.resultBundle.clear()
}
}
findNavController().addOnDestinationChangedListener(onDestinationChangedListener)
getResultBundle().putString("testKey","testValue")
override fun onFragmentResult(data: Bundle) {
data.getString("testKey")?.let {
LogUtil.e(it)
}
}
kotlin
navigation
ViewModel
LiveData
DataStore
DataBinding
Paging
Room
Koin
Coil kotlin打造的图片加载框架
Moshi json解析
Dynamic feature module
Retrofit 网络请求
MVVM
BottomNavigationView包含了A和B两个fragment,在A点击B时跳转到B,这个时候是B加到栈中,再点击A,回到A,但是B会出栈,
这不符合我们的预期,A和B应该存在两个回退栈才符合我们的预期。
官方建议这么做
这个问题很多人提出,官方也一直在跟进一拖再拖的官方
由于无法在App访问DFM中的定义,所以在app中定义的AppDatabase无法访问DFM的Dao和Entity
但是这么做会导致跨模块的关系将无法使用
每个模块生成一个database会影响性能
但是这么做会使得模块之间的持久化数据没有隔离