前语
Jetpack Compose想必各位安卓开发者现已不太陌生了,由于最近JetBrains-Compose for iOS开始Alpha**,对运用Compose进行跨平台的渴望又进了一步。
事实上之前我也触摸过Compose,做过点小游戏,但一直没有真实去做一个项目来学习和实践它,所以我想要做一个项目来一边学习一边实践。
但为什么没有挑选jb的Compose做跨平台,而是挑选了安卓的Jetpack Compose?事实上我认为自己对Compose的了解还比较浅,包括语法什么的,都不怎样了解。并且跨平台的Compose目前有许多组件没有完结,而我想要把这个项目的效果共享给咱们运用,因而我就不得不挑选Jetpack Compose。
当然这不是妥协,由于都是Compose,在界面上无非是API和特点上的差异,逻辑基本上能够复用,当然除了一些jetpack的组件比方viewmodel,可是咱们随时能够测验把此项目移植到跨平台上,当然,这需求时刻,以后有机会我会和咱们共享怎样迁移跨平台。
文末有开源地址
项目来历
这次我做的项目叫食选,由于上学期在学校食堂吃到现已不知道吃什么了,其时由于玩Compose,就做了个只要一个页面的APP,功用便是录入食物名称,然后点击能够抽取一份现已录入的食物,接下来我就知道吃什么了,哈哈,相当于随机抽取一个标签的程序。
上一次仅仅做了一个简略的界面,功用也比较单调,因而这次计划做个完善的。只不过这次并不计划只做它一个功用。
第一个,我最近在网上看到了一个程序员在家做饭办法指南和阻隔运用手册,简略来讲便是做菜的,可是后者有一个小功用,便是你能够挑选你现已有的食材和厨具,依据这些挑选的烹饪材料来引荐有哪些菜品能够做。当成果呈现后,点击成果能够跳转到B站的视频播放页,内容便是这道菜的制造教程。
第二个便是我刚刚说到抽取食物的功用,当然后边或许还会扩大一些功用,可是现在一点一点来吧。
项目计划
我对整个项目的技能挑选和结构规划,有一部分参考了Google的这个项目:nowinandroid: A fully functional Android app built entirely with Kotlin and Jetpack Compose
事务架构规划
最近谷歌在推一种由数据驱动UI,且UI和数据层之间只要单向流动关系的分层架构。想必图或许有许多读者现已看过许屡次了,可是我这块还是想放出来一下。
如图,UI仅仅收集Data和网络层的数据,经过不同的数据来改变UI规划。
为了完结这种规划,这儿咱们选用MVI的架构规划。
MVI
这个图或许咱们也看过了,我简略来说一下吧。
Model
这个便是咱们了解的Model了,这儿放着的是界面的逻辑,UI经过绑定Model后就能够获取相应的东西。
View
展现的界面便是View了,当然这儿还包括各种自界说组件,View绑定Model后就能够获取其间的数据并且展现了。
Intent
界面各种操作的目的,简略来讲,咱们点击一个按钮,假定需求进行登录验证,那么这便是一个目的。
State
同目的一样State存在于Model中,View绑定Model实际上是绑定了其间的State,当State改变UI就要发送变化,而UI发送目的,Model的逻辑处理终究也要转换到State上,这样才能驱动UI发生改变。
现在咱们看看上面的流程图应该会了解许多。
项目架构规划
前面说到了我学习的是Google的nowinandroid项目,因而此次也是选用了类似nowinandroid的模块化。与之前做过的组件化项目不同,模块化后几乎不能被独立拆解,依靠于多个模块运用。
简略来讲,整个程序目前被分为了3大层,同时咱们考虑运用Gradle的统一依靠办理来对各个模块的库进行办理,这也代表咱们这次将运用KTS。
APP层
该层担任将其他的模块引进进来,合并为一个完好的APP。 事实上APP层便是担任导航处理,完结一个主页和首要的页面,其他页面都在对应的模块里编写。
core层
该层首要是一些通用的事务,比方数据耐久化,数据模型,网络恳求,自界说UI组件,以及公共依靠模块。其他模块需求时就引进这些模块。
feature层
该层首要是一些通用的事务,比方数据耐久化,数据模型,网络恳求,自界说UI组件,以及公共依靠模块,其他模块需求时就引进这些模块。
挑选基础库
事实上咱们还需求预备一些事情,比方挑选一些常用的库,帮咱们加快开发。
程序导航
此次选用了一个activity多Composable的组合,这就意味着我不能运用activity的栈来办理界面的载入和退出。
因而我挑选了Google的一个导航库运用 Compose 进行导航 | Jetpack Compose | Android Developers
navigation的话它能够帮咱们办理界面栈就像是咱们之前在avtivity里那样。
依靠注入
咱们将许多内容分层,就导致了在各个模块引进时比较费事,你或许需求New很屡次,这样也造成了耦合度的上升,为此我也挑选了一款安卓的依靠注入库。 运用 Hilt 完结依靠项注入 | Android 开发者 | Android Developers (google.cn)
剩余的便是咱们常见的数据库耐久化库Room和网络恳求库Retrofit2
OK,现在咱们现已预备好了相关的库,接下来让咱们完结基础建设。
基建开发
下面的部分建议跟着项目看 1250422131/FoodChoice: 食选,解决日子中每天吃饭,吃什么,做什么,怎样做的问题,此项目也是我对JetpackCompose的MVI架构学习的一次实践。 (github.com)
网络恳求封装
咱们打开network模块,之前我运用过Ktor的网络恳求库,可是由于Retrofit现在对协程也支持了,因而我这次就挑选了Retrofit,可是这样还不行,咱们需求为Retrofit进行一次封装。
尽管说是封装,可是Retrofit事实上现已封装的很完善了,咱们只需求进行一点点的装备。
object RetrofitNiaNetwork {
val networkApi = Retrofit.Builder()
.baseUrl("https://api.xxxxx.com/app/cook/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitNiaNetworkApi::class.java)
}
上面需求留意的便是addConverterFactory,咱们的接口成果基本上都是Json,因而需求装备一个数据解析,这儿咱们用了Gson。
下面是所有APP接口的一个调集,当然后边要扩展的话,比方切换根路径,那就持续新增一个这样的接口类。
interface RetrofitNiaNetworkApi {
@GET("cookInfo.php")
suspend fun getCookFoodData(): CookFoodInfo
@GET("cookingIngredients.php")
suspend fun getCookingIngredients(): CookingIngredientsInfo
}
留意这块咱们用了suspend关键字,代表需求协程进行挂起。
数据库+依靠注入封装
在database模块中,咱们需求封装一下Room,使得Room能够被直接注入。
RoomDatabase
以往咱们继承RoomDatabase后应该是给它一个工厂模式或许单例,总归让大局保证只要一个RoomDatabase目标,但现在咱们有依靠注入就不需求这么做了,咱们这儿只写获取Dao的办法。
@Database(
entities = [CookingIngredientEntity::class, CookFoodEntity::class],
version = 1,
exportSchema = false,
)
abstract class AppDatabase : RoomDatabase() {
abstract fun cookingIngredientDao(): CookingIngredientDao
abstract fun cookFoodDao(): CookFoodDao
}
RoomDatabase目标获取
前面咱们说到有用Hilt完结依靠注入,现在咱们就来完结它,
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
private const val DB_NAME = "db_food_choice"
@Provides
@Singleton
fun providesFdDatabase(
@ApplicationContext context: Context,
): AppDatabase = Room.databaseBuilder(
context,
AppDatabase::class.java,
DB_NAME,
) // 是否答应在主线程进行查询
.allowMainThreadQueries()
// 数据库晋级异常之后的回滚
.fallbackToDestructiveMigration().build()
}
这儿运用@Module告知Hilt这是个能够注入的模块,@InstallIn(SingletonComponent::class) 则是指它能够在大局注入。
而providesFdDatabase
办规律指DatabaseModule被怎么创建,其间内部的代码便是创建了一个AppDatabase目标。而AppDatabase被注入时就会调用这个办法来发生AppDatabase,别的这是单例的哦,由于这儿选用了 @Singleton。
依靠注入Dao
但并不是这样就结束了的,咱们更希望咱们能够直接拿到Dao目标,还记得咱们在AppDatabase中界说的办法吗?正常情况下咱们是用一个初始化好的AppDatabase目标调用办法后来拿Dao,可是现在咱们用的依靠注入就能够换另一种办法。
@Module
@InstallIn(SingletonComponent::class)
object DaosModule {
@Provides
fun providesCookingIngredientDao(
database: AppDatabase,
): CookingIngredientDao = database.cookingIngredientDao()
@Provides
fun providesCookFoodDao(
database: AppDatabase,
): CookFoodDao = database.cookFoodDao()
}
就像是刚刚,咱们界说了DaosModule 是个Model,并且能够在大局注入,而下面便是分别写了CookFoodDao 和 CookingIngredientDao 被注入时怎么发生这个目标。
@Dao
interface CookFoodDao {
@Query("SELECT * from fc_cook_food where stuff = :stuff ORDER BY id DESC")
suspend fun selectByStuffList(stuff: String): MutableList<CookFoodEntity>
@Query("SELECT * from fc_cook_food where name = :name ORDER BY id DESC")
suspend fun selectByNameList(name: String): CookFoodEntity?
@Query("SELECT * from fc_cook_food ORDER BY id DESC")
suspend fun selectList(): MutableList<CookFoodEntity>
@Insert
suspend fun inserts(vararg cookFoodEntity: CookFoodEntity)
@Update
suspend fun update(cookFoodEntity: CookFoodEntity)
}
终究咱们看一个简略的Dao,现在Room也支持协程和KSP,因而咱们能够直接挑选运用KSP方式导入Room,以及在协程中来履行SQL。
数据源装备
让咱们这次进入data模块,这儿首要是对网络和本地数据进行整合/更新然后回来的操作,咱们需求引进common,database,network,model模块,其间model模块寄存的是网络数据的模型。
下面我拿一个来讲,CookFoodInfoRepository是对CookFoodInfo进行数据整合的类,事实上咱们这个项目功用需求完结几个基础的才能。
- 对Dao层接口进行进一步封装和处理
- 对网络数据进行获取后同步到本地耐久化储存
- 回来同步成功与失利的成果
class CookFoodInfoRepository @Inject constructor(
private val cookFoodDao: CookFoodDao,
) {
suspend fun getCookingFoods(stuff: String) =
run { cookFoodDao.selectByStuffList(stuff) }
suspend fun getCookingFoods() =
run { cookFoodDao.selectList() }
suspend fun syncWith(): Boolean {
val cookingFoodInfoResult = runCatching {
RetrofitNiaNetwork.networkApi.getCookFoodData()
}
// 成功的前提下进行
if (cookingFoodInfoResult.isSuccess) {
// 较为复杂的但写法清新语法糖
cookingFoodInfoResult.getOrNull()?.data?.forEach {
cookFoodDao.selectByName(it.name)?.apply {
cookFoodDao.update(it.asCookFoodEntity().copy(id = id))
} ?: apply {
cookFoodDao.inserts(it.asCookFoodEntity())
}
}
}
return cookingFoodInfoResult.isSuccess
}
}
咱们运用@Inject对该类的构造办法进行了注解,这代表它能够被注入,其间咱们在构造办法里承接了CookFoodDao的目标,回想一下,前面咱们运用了依靠注入告知了CookFoodDao能够被注入,因而这儿咱们就不需求办理了,运用时直接注入即可,无需传参。
上面的代码看起来有一些抽象,事实上getCookingFoods
仅仅承接了下dao层的成果,并没有进行进一步封装,而要点首要是同步这块。
数据同步
syncWith
办法是用来同步网络数据到本地的,咱们先来看看,首先咱们拿到了一个cookingFoodInfoResult
变量,向后看变得知这是runCatching的成果,这个是kotlin的内置函数,用来简化trycatch
的。而在runCatching中是一段网络恳求,怎样样?在协程中网络恳求便是这么简略,特别是咱们进行了一次封装之后。
在下方咱们进行了一次判别查看getCookFoodData
是否被恳求成功了,那么失利的话就抛异常被,就或许是网络有问题或许服务器处理有问题。
当恳求成功时,咱们经过getOrNull
办法获取一下成果目标,还记得咱们前面写的API接口类吗?这儿咱们就直接拿到回来目标了,剩余的retrofit现已帮咱们完结啦。
接下来咱们遍历回来成果,selectByName
办规律看看有没有查询成果,有的话则就update
反之则inserts
,这就完结了对数据库内容的同步和更新。
文末
至此,咱们现已对项目所需求的基本内容完结了编写,后边便是对功用的UI和逻辑绑定进行编写,这块咱们将谈及一些compose的组件,以及路由终究用法。
感谢咱们看到这儿,后边我还会更新这个项目的其他解释,如果咱们发现有错误的当地欢迎纠正,随时乐意听取咱们的意见。
项目开源
觉得项目不错的话就来个star? 1250422131/FoodChoice: 食选,解决日子中每天吃饭,吃什么,做什么,怎样做的问题,此项目也是我对JetpackCompose的MVI架构学习的一次实践。 (github.com)