神奇宝贝 眼前一亮的 Jetpack + MVVM 极简实战
前言
Jetpack 实战项目 PokemonGo(神奇宝贝)基于 MVVM 架构和 Repository 设计模式,PokemonGo 项目中用到的技术,都是之前写过的一系列文章里面涉及到的知识点:Paging3(network + db),Dagger-Hilt,App Startup,DataBinding,Room,Motionlayout,Kotlin Flow,Coil,JProgressView 等等。
项目 PokemonGo 已经上传到 GitHub: https://github.com/hi-dhl/PokemonGo,欢迎前去查看,动态效果图如下所示,如果动图无法查看,请点击这里查看 动态效果图 | 静态图
Jetpack 实战项目 PokemonGo 包含了以下功能:
- 自定义 RemoteMediator 实现 network + db 的混合使用 ( RemoteMediator 是 Paging3 当中重要成员 )
- 使用 Data Mapper 分离数据源 和 UI
- Kotlin Flow 结合 Retrofit2 + Room 的混合使用
- Kotlin Flow 与 LiveData 的使用
- 使用 Coil 加载图片
- 使用 ViewModel、LiveData、DataBinding 协同工作
- 使用 Motionlayout 做动画
- App Startup 与 Hilt 的使用
- 在 Flow 基础上封装成功或者失败处理
PokemonGo 涉及的技术:
- Gradle Versions Plugin:检查依赖库是否存在最新版本
- Kotlin + Coroutines + Flow:flow 是对 Kotlin 协程的扩展,让我们可以像运行同步代码一样运行异步代码
- JetPack
- Paging3(network + db):用到了 Paging3 中的
RemoteMediator
用来实现 network + db - Dagger-Hilt (2.28-alpha):依赖注入框架
- App Startup:设置组件初始化顺序
- DataBinding:以声明方式将可观察数据绑定到界面上
- Room:在 SQLite 上提供了一个抽象层,流畅地访问 SQLite 数据库
- LiveData:在底层数据库更改时通知视图
- ViewModel:以注重生命周期的方式管理界面相关的数据
- Andriod KTX:编写更简洁、惯用的 Kotlin 代码
- Paging3(network + db):用到了 Paging3 中的
- 项目架构
- MVVM 架构
- Repository 设计模式
- Data Mapper 数据映射
- Retrofit2 & OkHttp3:用于请求网路数据
- Coil:基于 Kotlin 开发的首个图片加载库
- material-components-android:模块化和可定制的材料设计 UI 组件
- Motionlayout :MotionLayout 是一种布局类型,可帮助您管理应用中的动画
- Timber: 日志打印
- JProgressView :一个小巧灵活可定制的进度条,支持图形:圆形、圆角矩形、矩形等等
以上技术栈对应之前写的技术文章:
- Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
- Jetpack 成员 Paging3 实践以及源码分析(一)
- Jetpack 新成员 Paging3 网络实践及原理分析(二)
- Jetpack 新成员 Hilt 实践(一)启程过坑记
- Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
- Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
- 全方面分析 Hilt 和 Koin 性能
- [译][2.4K Star] 放弃 Dagger 拥抱 Koin
- 项目中封装 Kotlin + Android Databinding
- 为数不多的人知道的 Kotlin 技巧以及 原理解析(一)
- 为数不多的人知道的 Kotlin 技巧以及 原理解析(二)
如果之前对这些技术没有接触过,或者只是听说,对阅读本文没有什么影响,本文会对这些技术结合着项目 PokemonGo 来分析,为了文章的简洁性,本文不会细究技术细节,因为每个技术都需要花好几篇文章才能分析清楚,我会在后续的文章去详细分析。
如何检查依赖库最新版本
在之前的文章 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度 分析过,到目前为止大概管理 Gradle 依赖提供了 4 种不同方法:
- 手动管理 :在每个 module 中定义插件依赖库,每次升级依赖库时都需要手动更改(不建议使用)
- 使用 ext 的方式管理插件依赖库 :这是 Google 推荐管理依赖的方法 Android官方文档
- Kotlin + buildSrc:自动补全和单击跳转,依赖更新时 将重新 构建整个项目
- Composing builds:自动补全和单击跳转,依赖更新时 不会重新 构建整个项目
新版的 AndroidStudio 只支持 ext 的方式 和 手动方式管理 检查依赖库是否存在最新版本,不支持 buildSrc、gradle-wrapper 版本的检查。
满足不了 PokemonGo 项目的需求,在 PokemonGo 项目中采用 buildSrc 方式去管理所有依赖库,因为 PokemonGo 项目采用单模块结构,而且支持 自动补全 和 单击跳转 很方便,所这里用到了 Gradle Versions Plugin 插件去检查依赖库的最新版本,检查结果如下所示:
The following dependencies have later release versions:- androidx.swiperefreshlayout:swiperefreshlayout [1.0.0 -> 1.1.0]https://developer.android.com/jetpack/androidx- com.squareup.okhttp3:logging-interceptor [3.9.0 -> 4.7.2]https://square.github.io/okhttp/- junit:junit [4.12 -> 4.13]http://junit.org- org.koin:koin-android [2.1.5 -> 2.1.6]- org.koin:koin-androidx-viewmodel [2.1.5 -> 2.1.6]- org.koin:koin-core [2.1.5 -> 2.1.6]Gradle release-candidate updates:- Gradle: [6.1.1 -> 6.5.1]
会列出所有需要更新的依赖库的最新版本,并且 Gradle Versions Plugin 比 AndroidStudio 所支持的更加全面:
- 支持手动方式管理依赖库最新版本检查
- 支持 ext 的方式管理依赖库最新版本检查
- 支持 buildSrc 方式管理依赖库最新版本检查
- 支持 gradle-wrapper 最新版本检查
- 支持多模块的依赖库最新版本检查
那么如何使用呢?只需要三步
1.将 PokemonGo 项目根目录 checkVersions.gradle 文件拷贝到你的项目根目录下面
2.在项目的根目录 build.gradle 文件夹内添加以下代码
apply from: './checkVersions.gradle' buildscript {repositories {google()jcenter()}dependencies {classpath "com.github.ben-manes:gradle-versions-plugin:0.28.0"} }
3.添加完成之后,在根目录下执行以下命令。
./gradlew dependencyUpdates
会在当前目录下生成 build/dependencyUpdates/report.txt 文件。
MVVM 架构
Jetpack 实战项目 PokemonGo 基于 MVVM 架构和 Repository 设计模式,如今几乎所有的 Android 开发者至少都听过 MVVM 架构,在谷歌 Android 团队宣布了 Jetpack 的视图模型之后,它已经成为了现代 Android 开发模式最流行的架构之一,如下图所示:
MVVM 有助于将应用程序的业务逻辑与 UI 完全分开。 如果业务逻辑与 UI 逻辑之间的联系非常紧密,那么维护将很困难,由于很难重用业务逻辑,因此编写单元测试代码非常困难,一堆重复的代码和复杂的逻辑。
Jetpack 的视图模型的 MVVM 架构由 View + DataBinding + ViewModel + Model 组成。
DataBinding
DataBinding(数据绑定)实际上是 XML 布局中的另一个视图结构层次,视图 (XML) 通过数据绑定层不断地与 ViewModel 交互。
我们来看一个例子,首页上有个 RecyclerView 用来展示神奇宝贝数据(名字、图片、点击事件等等),每一个 item 对应一个 ViewHolder,来看一下 ViewHolder 的实现。
class PokemonViewModel(view: View) : DataBindingViewHolder<PokemonListModel>(view) {private val mBinding: RecycleItemPokemonBinding by viewHolderBinding(view)override fun bindData(data: PokemonListModel, position: Int) {mBinding.apply {pokemon = dataexecutePendingBindings()}}}
正如你所看到的,由于使用了数据绑定,ViewHolder 里面的代码变的非常简单,可能这个例子不够明显,我们来看一个劲爆的,点击首页每一个 item 会跳转到详情页面,详情页面如下图所示:
详情页面(DetailActivity)展示了神奇宝贝的详细数据,先查询数据库,如果没有找到,读取网路数据然后保存到数据库,由于使用了数据绑定,代码变得非常简单,如下所示:
class DetailActivity : DataBindingAppCompatActivity() {private val mBindingActivity: ActivityDetailsBinding by binding(R.layout.activity_details)private val mViewModel: DetailViewModel by viewModels()lateinit var mPokemonModel: PokemonListModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)mBindingActivity.apply {mPokemonModel = requireNotNull(intent.getParcelableExtra(KEY_LIST_MODEL))pokemonListModel = mPokemonModellifecycleOwner = this@DetailActivityviewModel = mViewModel.apply {fectchPokemonInfo(mPokemonModel.name).observe(this@DetailActivity, Observer {})}}}
}
正如你所见 DetailActivity 代码变得非常简单,如果以后我们想要改变网络的 URL、Model、获取或保存数据的方式等等,我们不需要改变 DetailActivity 中的任何代码。
更多关于 DataBinding 的使用请参考我另外一个仓库 JDataBinding:目前已经封装了一系列的组件包含 DataBindingActivity、DataBindingAppCompatActivity、DataBindingFragmentActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter、DataBindingViewHolder 等等。
ViewModel
ViewModel 是 MVVM 架构中非常重要的设计,它在 activities 或 fragments 和业务逻辑中起到了非常重要的作用,它不依赖于 UI 组件,使得单元测试更加容易,ViewModel 以生命周期的方式管理界面相关的数据,直到 Activity 被销毁。
LiveData 与 ViewModel 具有很好的协同作用,LiveData 持有从数据源获取到的数据,并且它可以被 DataBinding 组件观察,当 Activity 被销毁时,它将被取消订阅。
而详情页面(DetailActivity) 代码之所以能这么简单得益于 ViewModel、LiveData、DataBinding 协同工作, 我们来看一下 ViewModel 代码。
class DetailViewModel @ViewModelInject constructor(val polemonRepository: Repository
) : ViewModel() {private val _pokemon = MutableLiveData<PokemonInfoModel>()val pokemon: LiveData<PokemonInfoModel> = _pokemon@OptIn(ExperimentalCoroutinesApi::class)fun fectchPokemonInfo(name: String) = liveData<PokemonInfoModel> {polemonRepository.featchPokemonInfo(name).collectLatest {_pokemon.postValue(it)emit(it)}.......// 省略部分代码,}}
activity_details.xml 代码
<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><variablename="viewModel"type="com.hi.dhl.pokemon.ui.detail.DetailViewModel" /></data>......<androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/weight"android:text="@{viewModel.pokemon.getWeightString}"/>......</layout>
这是获取神奇宝贝的详细信息,通过 DataBinding 以声明方式将数据(神奇宝贝的体重)绑定到界面上,更多使用参考项目中的代码。
Repository
Repository 设计模式是最流行、应用最广泛的设计模式之一,在 Repository 层获取网络数据,并将数据存储到数据库中,在这一层中有两个非常重要的成员 Paging3 库中的 RemoteMediator
和 Data Mappers。
RemoteMediator
在之前的文章 Jetpack 成员 Paging3 实践以及源码分析(一) 和 Jetpack 新成员 Paging3 网络实践及原理分析(二) 分别分析了使用 Paging3 访问 数据库 和 网络,但是遗漏了 RemoteMediator
类的使用,RemoteMediator 是 Paging3 当中一个非常重要的成员,用于实现 数据库 和 网络 访问,所以这里是对之前的文章一个补充。
RemoteMediator
很重要,需要单独花一篇文章去分析,为了节省篇幅,在这里不会详细的去分析它,如果对RemoteMediator
不太理解没有关系,我会在后续的文章里面详细的分析它。
项目中网络访问用的是 Retrofit2 & OkHttp3 用来请求网络数据,使用 Room 作为数据库存储,将获得的数据保存到数据库中,Room 在 SQLite 上提供了一个抽象层,流畅地访问 SQLite 数据库,同时拥有了 SQLite 全部功能,在编译的时候进行错误检查。
@OptIn(ExperimentalPagingApi::class)
class PokemonRemoteMediator(val api: PokemonService,val db: AppDataBase
) : RemoteMediator<Int, PokemonEntity>() {val mPageKey = 0override suspend fun load(loadType: LoadType,state: PagingState<Int, PokemonEntity>): MediatorResult {try {......val pageKey = when (loadType) {// 首次访问 或者调用 PagingDataAdapter.refresh()LoadType.REFRESH -> null// 在当前加载的数据集的开头加载数据时LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)// 在当前数据集末尾添加数据LoadType.APPEND -> {......if (remoteKey == null || remoteKey.nextKey == null) {return MediatorResult.Success(endOfPaginationReached = true)}remoteKey.nextKey}}......// 使用 Retrofit2 获取网络数据val page = pageKey ?: 0val result = api.fetchPokemonList(state.config.pageSize,page * state.config.pageSize).results.......db.withTransaction {if (loadType == LoadType.REFRESH) { // 当首次加载,或者下拉刷新的时候,清空当前数据 }......// 存储获取到的数据remoteKeysDao.insertAll(entity)pokemonDao.insertPokemon(item)}return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)} catch (e: IOException) {return MediatorResult.Error(e)} catch (e: HttpException) {return MediatorResult.Error(e)}}
}
注意:使用了 @OptIn(ExperimentalPagingApi::class)
需要在 App 模块 build.gradle 文件内添加以下代码。
android {kotlinOptions {freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]}
}
在 RemoteMediator
的实现类 PokemonRemoteMediator
中的核心部分是关于参数 LoadType 的判断。
LoadType.REFRESH
:首次访问 或者调用 PagingDataAdapter.refresh() 触发,这里不需要做任何操作,返回 null 就可以LoadType.PREPEND
:在当前列表头部添加数据的时候时触发,实际在项目中基本很少会用到直接返回MediatorResult.Success(endOfPaginationReached = true)
,参数 endOfPaginationReached 表示没有数据了不在加载LoadType.APPEND
:下拉加载更多时触发,这里获取下一页的 key, 如果 key 不存在,表示已经没有更多数据,直接返回MediatorResult.Success(endOfPaginationReached = true)
不会在进行网络和数据库的访问
接下来的逻辑和之前请求网络数据的逻辑没有什么区别了,使用 Retrofit2 获取网络数据,然后使用 Room 将数据保存到数据库中。
接下来聊一下 Repository 中另外一个重要的成员 Data Mapper,在项目中起到了非常的重要,在一个快速开发的项目中,为了越快完成第一个版本交付,下意识的将数据源和 UI 绑定到一起,当业务逐渐增多,数据源变化了,上层也要一起变化,导致后期的重构工作量很大,核心的原因耦合性太强了。
Data Mapper(个人建议)
Data Mapper 的意识非常重要,在项目中起到了非常的重要,关于 Data Mappers 在 Repository 中的重要性可以看一下国外大神写的这篇文章 The “Real” Repository Pattern in Android 在 Medium 上获得了 4.9K 的赞。
使用 Data Mapper 分离数据源的 Model 和 页面显示的 Model,不要因为数据源的增加、修改或者删除,导致上层页面也要跟着一起修改,换句话说使用 Data Mapper 做一个中间转换,如下图所示,来源于网络:
使用 Data Mapper(数据映射)优点如下:
- 数据源的更改不会影响上层的业务
- 糟糕的后端实现不会影响上层的业务 ( 想象一下,如果你被迫执行2个网络请求,因为后端不能在一个请求中提供你需要的所有信息,你会让这个问题影响你的整个代码吗? )
- Data Mapper 便于做单元测试,确保不会因为数据源的变化,而影响上层的业务
如果在一个大型项目中直接使用 Data Mapper 会有适得其反的效果,所以需要结合设计模式来完善,这不在本文讨论范围之内,其实在这里我想表达是,不要因为快速实现某个功能,下意识的将数据源的 model 和 UI 绑定在一起。
Data Mappe 实现方式有很多种,可以手动实现,也可以通过引入第三方框架,其中有名框架 modelmapper,在 PokemonGo 项目中是手动实现的。
Koltin Flow
停止使用 RxJava,尝试一下 Flow,不仅简单而且功能很强大,Retrofit2 和 Room 也都提供了对应的支持。
Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable,在 PokemonGo 项目中也用到了 Flow。
override suspend fun featchPokemonInfo(name: String): Flow<PokemonInfoModel> {return flow {val pokemonDao = db.pokemonInfoDao()var infoModel = pokemonDao.getPokemon(name)// 查询数据库是否存在,如果不存在请求网络if (infoModel == null) {// 网络请求val netWorkPokemonInfo = api.fetchPokemonInfo(name)......pokemonDao.insertPokemon(infoModel) // 插入更新数据库}val model = mapper2InfoModel.map(infoModel) // 数据转换emit(model)}.flowOn(Dispatchers.IO)
}
在这里做了三件事:
- 查询数据库是否存在,如果不存在请求网络
- 请求网络获取数据,更新数据库
- 将数据源的 Model 转换为页面显示的 Model
依赖注入
Hilt、Dagger、Koin 等等都是依赖注入库,使用依赖注入库有以下优点:
- 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
- 在配置 scopes 范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。
- 代码变得更具可读性。
- 易于构建对象。
- 编写低耦合代码,更容易测试。
在 PokemonGo 项目中使用的是 Hilt,Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持,来看一下 Hilt 与 Room 在一起使用的例子。
@Module
@InstallIn(ApplicationComponent::class)
object RoomModule {/*** @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。* @Singleton 提供单例*/@Provides@Singletonfun provideAppDataBase(application: Application): AppDataBase {return Room.databaseBuilder(application, AppDataBase::class.java, "dhl.db").fallbackToDestructiveMigration().allowMainThreadQueries().build()}@Singleton@Providesfun provideTasksRepository(db: AppDataBase): Repository {return PokemonFactory.makePokemonRepository(db)}
}
这里需要用到 @Module 注解,使用 @Module 注解的普通类,在其内部提供 Room 的实例,更多使用可以查看 PokemonGo 项目。
小巧灵活的进度条
神奇宝贝详情页的进度条使用的是 JProgressView :一个小巧灵活可定制的进度条,支持图形:圆形、圆角矩形、矩形等等,效果如下图所示:
起源于当时想用一个现成的库,但是在网上找了很多,没有一个合适自己的,要不大而全,要不作者好久没更新了,要不不兼容 DataBinding,于是乎就自己封装了一个小巧灵活的进度条,项目长期维护并持续更新,如果有更好的建议欢迎告知我,JProgressView 使用非常的简单,根据自己的需求去配置即可。
<com.hi.dhl.jprogressview.JProgressViewandroid:layout_width="match_parent"android:layout_height="18dp"android:layout_below="@+id/exp"android:translationZ="100dp"app:maxProgressValue="@{viewModel.pokemon.maxExp}"app:progressValue="@{viewModel.pokemon.exp}"app:progress_animate_duration="@integer/progress_animate_duration"app:progress_color="@color/color_progress_4"app:progress_color_background="@color/color_progress_bg"app:progress_paint_bg_width="@dimen/circle_stroke_width"app:progress_paint_value_width="@dimen/circle_stroke_width"app:progress_text_color="@android:color/black"app:progress_text_size="@dimen/text_size_12sp"app:progress_type="@integer/porgress_tpye_round_rect" />
名称 | 值类型 | 默认值 | 备注 |
---|---|---|---|
progress_type | integer | 圆形:1 | 矩形:0;矩形:0;矩形:0 |
progress_animate_duration | integer | 2000 | 动画运行时间 |
progress_color | color | Color.GRAY | 当前进度颜色 |
progress_color_background | color | Color.GRAY | 进度条背景颜色 |
progress_paint_bg_width | dimen | 10 | 进度条背景画笔的宽度 |
progress_paint_value_width | dimen | 10 | 当前进度画笔的宽度 |
progress_text_color | color | Color.BLUE | 进度条上的文字的颜色 |
progress_text_size | dimen |
sp2Px(20f)
|
进度条上的文字的大小 |
progress_text_visible | boolean | 默认不显示:false | 是否显示文字 |
progress_value | integer | 0 | 当前进度 |
progress_value_max | integer | 100 | 当前进度条的最大值 |
更多关于进度条的使用,查看 JProgressView 仓库,全文到这里就结束了,为了节省篇幅,很多在之前系列文章里面分析过的,这里不在详细分析了,更多技术细节会在后续的系列文章中分析。
正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。
结语
致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。
Android10-Source-Analysis
正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis。
Leetcode-Solutions-with-Java-And-Kotlin
由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。
- 数据结构: 数组、栈、队列、字符串、链表、树……
- 算法: 查找算法、搜索算法、位运算、排序、数学、……
每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin。
Technical-Article-Translation
目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation。
神奇宝贝 眼前一亮的 Jetpack + MVVM 极简实战相关推荐
- Django Web 开发极简实战
课程介绍 本课程是一个系列基础教程,目标是带领读者上手实战 Django Web 开发,课程以 Django 1.10 为基础,通过一个在线视频网站的构建,实战化的介绍 Django Web 开发中涉 ...
- TDengine极简实战:从采集到入库,从前端到后端,体验物联网设备数据流转
作者:牛晓青 背景 我们的项目涉及物联网相关业务,由于一开始的年少无知,传感器数据采用了 MySQL 进行存储,经过近两年的数据累积,目前几个核心表单表数据已过亿,虽然通过索引优化. SQL 优化以及 ...
- .NET Core实战项目之CMS 第八章 设计篇-内容管理极简设计全过程
写在前面 上一篇文章.NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程中我带着大家进行了权限部分的极简设计,也仅仅是一个基本的权限设计.不过你完全可以基于这套权限系统设计你的更 ...
- .NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程
写在前面 这篇我们对用户权限进行极简设计并保留其扩展性.首先很感谢大家的阅读,前面六章我带着大家快速入门了ASP.NET Core.ASP.NET Core的启动过程源码解析及配置文件的加载过程源码解 ...
- MVVM架构之自动增删改的极简RecycleView的实现
先上个源代码的链接:github.com/whenSunSet/- RecycleView是Google替代ListView的一种方案,其有着很高的解耦度,让许多开发者抛弃了以往的ListView,那 ...
- 【2022·深度强化学习课程】深度强化学习极简入门与Pytorch实战
课程名称:深度强化学习极简入门与Pytorch实战 课程内容:强化学习基础理论,Python和深度学习编程基础.深度强化学习理论与编程实战 课程地址:https://edu.csdn.net/cour ...
- Serverless 实战 —— 基于 Serverless 的 VuePress 极简静态网站
基于 Serverless 的 VuePress 极简静态网站 作者: Aceyclee 之前用过 Docsify + Serverless Framework 快速创建个人博客系统,虽然 docsi ...
- 30个Python常用极简代码,拿走就用
点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 作者丨Fatos Morina 来源丨Python 技术 编辑丨极市 ...
- 30 段极简 Python 代码:这些小技巧你都 Get 了么?
选自 | towardsdatascienc 编译 | 机器之心 学 Python 怎样才最快,当然是实战各种小项目,只有自己去想与写,才记得住规则.本文是 30 个极简任务,初学者可以尝试着自己实现 ...
最新文章
- Appium使用のhelloworld
- vetur插件_6款让开发效率“起飞的”VS code插件,哪个才是你的最爱
- 四十九、IQ 与测试评分案例
- Dubbo(六)使用SpringBoot搭建dubbo服务提供者工程
- Laravel测试驱动开发--功能测试
- python编译exe运行慢_Python运行速度慢你知道这是为什么吗?
- eclipse和idea开发servlet的区别
- Entity Framework第三篇IQueryable和list本地集合
- 报错ValueError: check_hostname requires server_hostname
- 一句“哭什么哭”,说得好
- 计算机绘图cad期末考试试题,20年广东理工学院成人高考期末考试 计算机绘图(AutoCAD) 复习资料.pdf...
- 【Power Automate】在power automate中使用SharePoint rest api(Send an http request to SharePoint)获取列表数据
- QCustomplot 实现鼠标追踪定位线以及坐标
- 山西最新五大姓氏排名发布,排名第一的是王,第二的竟是……
- Android Studio插件GsonFormat快速实现JavaBean
- VBE开源插件Rubberduck
- html标签 lt heavy gt,HTML Purifier:转换&lt; body&gt;到&lt; div&gt;
- 企业管理 史玉柱:公司只有三个人可以谈战略,其他人抓好执行
- Base64编解码原理并用Java手工实现Base64编解码
- Eclipse安装、激活、配置最新版JRebel