├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dq │ │ └── qkotlin │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── dq │ │ │ └── qkotlin │ │ │ ├── MainActivity.kt │ │ │ ├── bean │ │ │ ├── BaseResponseEntity.kt │ │ │ ├── ResponseEntity.kt │ │ │ ├── ResponseException.kt │ │ │ ├── ResponsePageEntity.kt │ │ │ └── UserBaseBean.kt │ │ │ ├── net │ │ │ ├── CoroutineUtil.kt │ │ │ ├── LoadState.kt │ │ │ ├── NetworkResponseCallback.kt │ │ │ ├── RetrofitInstance.kt │ │ │ └── UserApiService.kt │ │ │ ├── tool │ │ │ ├── QApplication.kt │ │ │ ├── QUtil.kt │ │ │ ├── UIExtend.kt │ │ │ └── ViewBindingAdapter.kt │ │ │ ├── ui │ │ │ ├── base │ │ │ │ ├── BaseLVPagerViewModel.kt │ │ │ │ ├── BaseRVActivity.kt │ │ │ │ ├── BaseRVPagerViewModel.kt │ │ │ │ ├── BaseViewModel.kt │ │ │ │ └── INavBar.kt │ │ │ ├── common │ │ │ │ └── WelcomeActivity.kt │ │ │ ├── home │ │ │ │ ├── DetailActivity.kt │ │ │ │ ├── HomeFragment.kt │ │ │ │ ├── HomeListViewAdapter.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ └── detail │ │ │ │ │ ├── UserQuickAdapter.kt │ │ │ │ │ ├── UserRVActivity.kt │ │ │ │ │ └── UserRVViewModel.kt │ │ │ ├── mvc │ │ │ │ ├── FriendActivity.kt │ │ │ │ ├── FriendRecyclerAdapter.kt │ │ │ │ └── MvcRVFragment.kt │ │ │ └── notifications │ │ │ │ ├── ProfileFragment.kt │ │ │ │ └── ProfileViewModel.kt │ │ │ └── view │ │ │ ├── FooterListView.kt │ │ │ ├── ShakeAnimation.java │ │ │ └── SpacesItemDecoration.java │ └── res │ │ ├── color │ │ └── white_black_selector.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── empty_topic.png │ │ ├── drawable-xxhdpi │ │ ├── tab_1_0.png │ │ ├── tab_1_1.png │ │ ├── tab_2_0.png │ │ ├── tab_2_1.png │ │ ├── tab_5_0.png │ │ ├── tab_5_1.png │ │ ├── tabbar_video_origin.png │ │ ├── tabbar_video_selected.png │ │ ├── user_photo.png │ │ └── video_info_like_tiny.png │ │ ├── drawable │ │ ├── ic_dashboard_black_24dp.xml │ │ ├── ic_home_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_notifications_black_24dp.xml │ │ ├── ic_white_black_24dp.xml │ │ ├── ic_white_black_45dp.xml │ │ ├── red_circle_disable.xml │ │ ├── red_circle_normal.xml │ │ ├── red_circle_press.xml │ │ ├── red_circle_selector.xml │ │ ├── splash.xml │ │ ├── white_background.xml │ │ └── white_selector.xml │ │ ├── layout │ │ ├── activity_listview.xml │ │ ├── activity_main.xml │ │ ├── activity_recyclerview.xml │ │ ├── fragment_dashboard.xml │ │ ├── fragment_home.xml │ │ ├── fragment_profile.xml │ │ ├── listitem_follower.xml │ │ ├── listview_empty.xml │ │ ├── marge_refresh_recyclerview.xml │ │ ├── toolbar_common.xml │ │ └── toolbar_draw.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── welcome_logo.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── dq │ └── qkotlin │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QKotlin 2 | 3 | Kotlin+MVVM框架,最符合实际接口情况、最接地气的封装 4 | 5 | 大家都已经看过很多MVVM的开发框架了,各式各样的都有,高star的几个项目我也基本都消化一遍,但是都感觉差了点什么。 6 | 要么封装的太过复杂,别人很难上手,实际也用不上那么复杂的封装; 7 | 要么就是为了封装而封装,实际情况很难变通; 8 | 要么就是光顾着搭建架子,实际的restful api接口根本对接不上 9 | 10 | 1、本框架主要技术关键词: 11 | 协程suspend、retrofit、smart下拉刷新、BaseRecyclerViewAdapterHelper、ViewBinding、ViewModel 12 | 13 | 2、本框架优点: 14 | 非常贴合实际项目需求,用的个别第3方的也都是最前沿的技术。不用sleep,wait模拟服务器接口,本框架直接拿实际网络接口演示 15 | 16 | 本框架针对下拉刷新、底部加载更多、判断是否有更多页、判断空布局、内存重启时候Fragment处理、、等等问题重点封装,其他无所谓的东西能不封装的就不封装,更方便接入你的项目 17 | 18 | ViewModel里监听的接口返回情况,封装的明明白白: 19 | 20 | //具体的网络接口返回情况 21 | ```kotlin 22 | enum class LoadState { 23 | None, 24 | Loading, //下拉刷新开始请求接口 or 普通开始请求接口 25 | SuccessHasMore, //下拉刷新请求成功且服务器告诉我还有下一页 or 普通请求成功 26 | SuccessNoMore, //下拉刷新请求成功且服务器告诉我已经没有下一页了 27 | CodeError, //下拉刷新请求成功但是服务器给我返回了错误的code码 or 普通请求成功但是服务器给我返回了错误的code码 28 | NetworkFail, //下拉刷新请求失败 or 普通请求失败,原因是压根就没访问到服务器 29 | PageLoading, //底部翻页开始请求接口 30 | PageSuccessHasMore, //底部翻页请求成功且服务器告诉我还有下一页 31 | PageSuccessNoMore, //底部翻页请求成功且服务器告诉我已经没有下一页了 32 | PageCodeError, //底部翻页请求成功但是服务器给我返回了错误的code码 33 | PageNetworkFail, //底部翻页请求失败,原因是压根就没访问到服务器 34 | } 35 | ``` 36 | 37 | 38 | 服务器返回的接口往往是这样的: 39 | ``` 40 | "code":1 41 | "message":成功 42 | "data":{ 43 | "total":1000 //一共有多少条数据 44 | "totalpage":50 //一共多少页 45 | "currentpage":1 //当前请求的是第几页 46 | "items": [{ //具体的T对象 47 | "name":"小涨" 48 | "age":20 49 | } 50 | {...} 51 | ] 52 | } 53 | ``` 54 | 下面看一下代码: 55 | 56 | 基于RecyclerView的界面对应的 BaseRVPagerViewModel: 57 | ```kotlin 58 | 59 | /** 60 | * 场景:如果你的列表界面用的是RecyclerView,那么Activity或Fragment里的 MyViewModel 继承这个VM,(T是列表的实体类) 61 | * 62 | * 特点:不监听list,只监听网络访问状态loadStatus,然后根据不同的loadStatus来直接用list;轻便简单容易理解 63 | * 为什么还有tempList:因为recyclerview有notifyItemRangeInserted,所以翻页的时候要用到这一页的templist,然后用templist做局部刷新 64 | */ 65 | open class BaseRVPagerViewModel: ViewModel() { 66 | 67 | //内部使用可变的Mutable 68 | protected val _loadStatus = MutableLiveData() 69 | 70 | //对外开放的是final,这是谷歌官方的写法 71 | open val loadStatus: LiveData = _loadStatus 72 | 73 | //下拉刷新的错误信息,服务器给我返回的 也可以自定义 74 | var errorMessage:String? = null 75 | 76 | //最核心的数据列表,我的做法是:不监听他,直接get他 77 | //当然也有人的做法是 LiveData> 然后onChange里无脑notityDataChanged,个人觉得那样做反而限制很多 78 | //特别注明:如果使用的是BaseRecyclerViewAdapterHelper,他的adapter里有会有个list的指针,我们这里也有个指针,但是内存共用一个 79 | open val list: MutableList = arrayListOf() 80 | 81 | //下拉刷新请求返回的临时templist: 82 | var tempRefreshlist: List? = null 83 | 84 | //翻页请求返回的临时templist: 85 | //为什么分别定义两个temp:因为极端情况下,下拉刷新和底部翻页同时请求网络,只用一个temp的话就不知道应该setList还是addList 86 | //注意:这样做分成两个也不会造成占用内存增加,因为我addList(tempList)之后, 立即templist = null 87 | var tempPagelist: List? = null 88 | 89 | //下次请求需要带上的页码参数 90 | private var page = 1 91 | 92 | /** 93 | * 功能:万能的列表请求接口 94 | * @params get请求参数,无需page字段 95 | * @loadmore true = 是底部翻页,false = 下拉刷新 96 | * @block 具体的那两行suspend协程请求网络的代码块,其返回值是网络接口返回值 97 | */ 98 | open fun requestList(params : HashMap, loadmore : Boolean , block:suspend() -> BasePageEntity){ 99 | 100 | 101 | _loadStatus.value = (if (loadmore) LoadState.PageLoading else LoadState.Loading) 102 | 103 | //如果是加载更多,就加上参数page;否则(下拉刷新)就强制设为1,如果服务器要求是0,就改成"0" 104 | params["page"] = if (loadmore) page.toString() else "1" 105 | 106 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 107 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 108 | //这里是主线程; 109 | errorMessage = "Emm..服务器出小差了"; 110 | _loadStatus.setValue( 111 | if (loadmore) LoadState.PageNetworkFail else LoadState.NetworkFail 112 | ) 113 | } 114 | 115 | /*viewModelScope是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 116 | viewModelScope.launch(coroutineExceptionHandler) { 117 | 118 | //具体的那两行suspend协程请求网络的代码 由VM子类来实现 119 | val response: BasePageEntity = block(); 120 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 121 | 122 | if (loadmore) { 123 | //加载更多 124 | if (response.isSuccess) {//加载更多服务器返回成功 125 | page++ 126 | 127 | //这次底部翻页接口返回的具体List 128 | tempPagelist = response.data?.items 129 | 130 | //触发activity的onChanged,让activity处理界面 131 | _loadStatus.setValue( 132 | if (response.data!!.hasMore()) LoadState.PageSuccessHasMore else LoadState.PageSuccessNoMore 133 | ) 134 | 135 | //代码走到这里,tempPagelist已经用完了(把他addAll了),就立即释放掉temp的内存 136 | tempPagelist = null; 137 | 138 | } else { 139 | _loadStatus.setValue(LoadState.PageCodeError) 140 | } 141 | } else { //下拉刷新请求完毕 142 | if (response.isSuccess) { 143 | page = 2 //页面强制设置为下次请求第2页 144 | 145 | //这次下拉刷新接口返回的具体List 146 | tempRefreshlist = response.data?.items 147 | 148 | //触发activity的onChanged,让activity处理界面 149 | _loadStatus.setValue( 150 | if (response.data!!.hasMore()) LoadState.SuccessHasMore else LoadState.SuccessNoMore 151 | ) 152 | 153 | //代码走到这里,界面已经用过了tempRefreshlist(把他addAll了),就立即释放掉temp的内存 154 | tempRefreshlist = null; 155 | 156 | } else { 157 | //服务器告诉我参数错误 158 | _loadStatus.setValue(LoadState.CodeError) 159 | errorMessage = response.message 160 | } 161 | } 162 | } 163 | } 164 | } 165 | ``` 166 | 167 | 以上代码是BaseRVPagerViewModel,其中T是列表的每一行的具体实体类;下面代码是列表界面Activity需要继承自 BaseRVActivity: 168 | ```kotlin 169 | 170 | /** 171 | * 场景:如果Activity里有RecyclerView,那么就继承BaseRVActivity,T是列表数据的每条的Bean,VM 是BaseRVPagerViewModel子类 172 | */ 173 | open abstract class BaseRVActivity> : BaseAppCompatActivity() { 174 | 175 | protected val viewModel: VM by lazy { ViewModelProvider(this).get(onBindViewModel()) } 176 | 177 | override fun initView() { 178 | super.initView() 179 | initRVObservable() 180 | } 181 | 182 | //子类自己写获取adapter的方法(比如new ) 然后通过这个方法返回就行了 183 | //out 就是java里的 就是可以兼容BaseViewHolder的子类 184 | abstract fun adapter(): BaseQuickAdapter 185 | 186 | //子类自己写获取refreshLayout的方法(比如findViewById或者binding.) 然后通过这个方法返回就行了 187 | abstract fun refreshLayout(): SmartRefreshLayout 188 | 189 | //子类重写 190 | abstract fun onBindViewModel(): Class 191 | 192 | protected open fun initRVObservable() { 193 | //监听网络返回值 194 | viewModel.loadStatus 195 | .observe(this, Observer { loadState -> 196 | when (loadState) { 197 | LoadState.None -> { 198 | } 199 | LoadState.Loading -> { 200 | } 201 | LoadState.SuccessNoMore, LoadState.SuccessHasMore -> { 202 | refreshLayout().finishRefresh(0) 203 | 204 | adapter().setList(viewModel.tempRefreshlist!!) 205 | 206 | if (loadState === LoadState.SuccessHasMore) 207 | refreshLayout().finishLoadMore() 208 | else refreshLayout().finishLoadMoreWithNoMoreData() 209 | 210 | if (viewModel.list.isNullOrEmpty()) { 211 | emptyLayout.findViewById(R.id.empty_tv).setText("空空如也~") 212 | adapter().setEmptyView(emptyLayout) 213 | } 214 | } 215 | LoadState.CodeError, LoadState.NetworkFail -> { 216 | refreshLayout().finishRefresh(0) 217 | refreshLayout().finishLoadMoreWithNoMoreData() 218 | 219 | if (viewModel.list.isNullOrEmpty()) { 220 | emptyLayout.findViewById(R.id.empty_tv).setText(viewModel.errorMessage) 221 | adapter().setEmptyView(emptyLayout) 222 | } 223 | } 224 | LoadState.PageLoading -> { 225 | } 226 | LoadState.PageSuccessHasMore , LoadState.PageSuccessNoMore-> { 227 | adapter().addData(viewModel.tempPagelist!!) 228 | 229 | if (loadState === LoadState.PageSuccessHasMore) 230 | refreshLayout().finishLoadMore() 231 | else refreshLayout().finishLoadMoreWithNoMoreData() 232 | } 233 | LoadState.PageCodeError, LoadState.PageNetworkFail -> 234 | refreshLayout().finishLoadMoreWithNoMoreData() 235 | } 236 | }) 237 | } 238 | 239 | //空布局 240 | private val emptyLayout: View by lazy { 241 | LayoutInflater.from(this).inflate(R.layout.listview_empty, null) 242 | } 243 | } 244 | ``` 245 | 以上是BaseRVActivity,下面就是具体的Activity的实现方式,我想了很久,到底Adapter实体类 和 ViewModel实体类 和 RefreshLayout实体类 到底是放到BaseRVActivity类里合适,还是放到具体的子类Activity里,最后决定是: 246 | ViewModel实体类 放在Base里,因为毕竟是要封装框架,ViewModel是框架级的东西,Base里经常会用到他; 247 | 而RefreshLayout 和 Adapter 放到具体的子类Activity,因为他们往往会因为界面的个性化,做出具体的调整 248 | 249 | 以下是具体的子类 UserListActivity 实现方式 250 | 251 | ```kotlin 252 | 253 | /** 254 | * RecyclerView的Demo,具体每一条的bean是UserBaseBean,VM是UserArrayViewModel 255 | */ 256 | class UserListActivity : BaseRVActivity() { 257 | 258 | private lateinit var adapter: UserQuickAdapter 259 | 260 | private lateinit var binding: ActivityRecycleviewBinding 261 | 262 | override fun initView() { 263 | super.initView() 264 | 265 | adapter = UserQuickAdapter(viewModel.list) 266 | 267 | binding.recyclerView.layoutManager = LinearLayoutManager(this) 268 | binding.recyclerView.adapter = adapter 269 | } 270 | 271 | override fun onCreate(savedInstanceState: Bundle?) { 272 | super.onCreate(savedInstanceState) 273 | binding = DataBindingUtil.setContentView(this, R.layout.activity_recyclerview) 274 | initView(); 275 | 276 | binding.refreshLayout.setOnRefreshListener { 277 | val params = HashMap() 278 | params["keyword"] = "小" 279 | viewModel.requestUserList(params, false) 280 | } 281 | 282 | binding.refreshLayout.setOnLoadMoreListener { 283 | val params = HashMap() 284 | params["keyword"] = "小" 285 | viewModel.requestUserList(params, true) 286 | } 287 | 288 | 289 | //demo 添加的 Header 290 | //Header 是自行添加进去的 View,所以 Adapter 不管理 Header 的 DataBinding。 291 | //请在外部自行完成数据的绑定 292 | // val view: View = layoutInflater.inflate(R.layout.listitem_follower, null, false) 293 | // view.findViewById(R.id.iv).setVisibility(View.GONE) 294 | // adapter.addHeaderView(view) 295 | 296 | binding.refreshLayout.autoRefresh(100,200,1f,false);//延迟100毫秒后自动刷新 297 | 298 | //item 点击事件 299 | // adapter.setOnItemClickListener(object : OnItemClickListener() { 300 | // fun onItemClick(adapter: BaseQuickAdapter<*, *>?, view: View?, position: Int) { 301 | // } 302 | // }) 303 | } 304 | 305 | override fun getTootBarTitle(): String { 306 | return "RecyclerView列表" 307 | } 308 | 309 | //本界面对应的VM类,如果VM复杂的话,也可以独立成一个外部文件 310 | class UserArrayViewModel: BaseRVPagerViewModel() { 311 | 312 | //按MVVM设计原则,请求网络应该放到更下一层的"仓库类"里,但是我感觉如果你只做网络不做本地取数据,没必要 313 | //请求用户列表接口 314 | fun requestUserList(params : HashMap , loadmore : Boolean){ 315 | 316 | //调用"万能列表接口封装" 317 | super.requestList(params, loadmore){ 318 | 319 | //用kotlin高阶函数,传入本Activity的"请求用户列表接口的代码块" 就是这3行代码 320 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 321 | val response: BasePageEntity = apiService.userList(params) 322 | response 323 | } 324 | } 325 | } 326 | 327 | override fun adapter(): UserQuickAdapter = adapter 328 | 329 | override fun refreshLayout(): SmartRefreshLayout = binding.refreshLayout 330 | 331 | override fun onBindViewModel(): Class = UserArrayViewModel::class.java 332 | } 333 | ``` 334 | 335 | 此外,本框架还做了对网络请求的封装,这个并不是本框架最大亮点,就不再贴代码了 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | 9 | defaultConfig { 10 | applicationId "com.dq.qkotlin" 11 | minSdkVersion 19 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildFeatures{ 20 | dataBinding = true 21 | viewBinding = true 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | } 38 | 39 | apply plugin: 'kotlin-kapt' 40 | 41 | kapt { 42 | generateStubs = true 43 | } 44 | 45 | dependencies { 46 | 47 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 48 | implementation 'androidx.core:core-ktx:1.2.0' 49 | implementation 'androidx.appcompat:appcompat:1.1.0' 50 | implementation 'com.google.android.material:material:1.1.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 52 | implementation 'androidx.vectordrawable:vectordrawable:1.1.0' 53 | implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2' 54 | implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' 55 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' 56 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' 57 | implementation 'androidx.fragment:fragment-ktx:1.2.0' 58 | 59 | //协程 60 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3' 61 | 62 | // Retrofit 63 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 64 | implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'//json解析 65 | implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') {//打印网络请求日志 66 | exclude group: 'org.json', module: 'json' 67 | } 68 | 69 | //glide 70 | implementation 'com.github.bumptech.glide:glide:4.11.0' 71 | 72 | // 下拉刷新 73 | implementation 'com.scwang.smart:refresh-layout-kernel:2.0.3' //核心必须依赖 74 | implementation 'com.scwang.smart:refresh-header-classics:2.0.3' //经典刷新头 75 | implementation 'com.scwang.smart:refresh-footer-classics:2.0.3' //经典加载 76 | 77 | // 知名的RecyclerViewAdapter封装 78 | implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4' 79 | 80 | //dialog,toast,如果你有自己的更好的状态栏库,可替换成你的 81 | implementation "com.github.kongzue.DialogX:DialogX:0.0.43.beta13" 82 | 83 | // 状态栏,如果你有自己的更好的状态栏库,可替换成你的 84 | implementation 'com.gyf.immersionbar:immersionbar:3.0.0' 85 | implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0' 86 | 87 | //内存泄漏 88 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' 89 | 90 | testImplementation 'junit:junit:4.+' 91 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 92 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 93 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dq/qkotlin/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.dq.qkotlin", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 33 | 34 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.fragment.app.FragmentTransaction 6 | import com.dq.qkotlin.ui.mvc.MvcRVFragment 7 | import com.dq.qkotlin.ui.home.HomeFragment 8 | import com.dq.qkotlin.ui.notifications.ProfileFragment 9 | import com.google.android.material.bottomnavigation.BottomNavigationView 10 | 11 | class MainActivity : AppCompatActivity() { 12 | 13 | private lateinit var navView: BottomNavigationView 14 | 15 | private lateinit var homeFragment: HomeFragment 16 | private lateinit var mvcRVFragment: MvcRVFragment 17 | private lateinit var profileFragment: ProfileFragment 18 | 19 | private var currentTabId = 0//当前选中的tab id 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContentView(R.layout.activity_main) 24 | createBottomNavigationView(savedInstanceState); 25 | } 26 | 27 | private fun createBottomNavigationView(savedInstanceState: Bundle?){ 28 | navView = findViewById(R.id.nav_view) 29 | 30 | //设置导航栏菜单项Item选中监听 31 | navView.setOnNavigationItemSelectedListener { item -> 32 | val transaction = supportFragmentManager.beginTransaction() 33 | hideCurrentFragment(transaction) 34 | showFragment(item.itemId,transaction) 35 | currentTabId = item.itemId 36 | true 37 | } 38 | 39 | //通过TAG查找得到该Fragment,第一次还没添加的时候得到的fragment为null 40 | homeFragment = createFragment(HomeFragment::class.java, HomeFragment.TAG) 41 | mvcRVFragment = createFragment(MvcRVFragment::class.java, MvcRVFragment.TAG) 42 | profileFragment = createFragment(ProfileFragment::class.java, ProfileFragment.TAG) 43 | 44 | val transaction = supportFragmentManager.beginTransaction() 45 | // 因为在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠 46 | if (savedInstanceState == null) { 47 | // 正常时候 48 | transaction.add(R.id.container, homeFragment, HomeFragment.TAG) 49 | transaction.commit() 50 | currentTabId = R.id.navigation_home 51 | } else { 52 | 53 | transaction.hide(homeFragment).hide(profileFragment).hide(mvcRVFragment) 54 | // “内存重启”时调用 解决重叠问题 55 | currentTabId = savedInstanceState.getInt("LAST_SELECT_TABID", R.id.navigation_home) 56 | showFragment(currentTabId,transaction) 57 | } 58 | } 59 | 60 | override fun onSaveInstanceState(outState: Bundle) { 61 | super.onSaveInstanceState(outState) 62 | // 保存当前Fragment的下标 63 | outState.putInt("LAST_SELECT_TABID",navView.selectedItemId) 64 | } 65 | 66 | private fun createFragment(cls: Class ,tag: String) : T { 67 | if (supportFragmentManager.findFragmentByTag(tag) != null){ 68 | return supportFragmentManager.findFragmentByTag(tag) as T 69 | } else { 70 | return cls.newInstance(); 71 | } 72 | } 73 | 74 | //显示Fragment 75 | private fun showFragment(willTabId: Int, transaction: FragmentTransaction) { 76 | when (willTabId) { 77 | R.id.navigation_home -> { 78 | if (homeFragment.isAdded){ 79 | transaction.show(homeFragment) 80 | } else { 81 | transaction.add(R.id.container, homeFragment, HomeFragment.TAG) 82 | } 83 | } 84 | R.id.navigation_dashboard -> { 85 | if (mvcRVFragment.isAdded){ 86 | transaction.show(mvcRVFragment) 87 | } else { 88 | transaction.add(R.id.container, mvcRVFragment, MvcRVFragment.TAG) 89 | } 90 | } 91 | R.id.navigation_notifications -> { 92 | if (profileFragment.isAdded){ 93 | transaction.show(profileFragment) 94 | } else { 95 | transaction.add(R.id.container, profileFragment, ProfileFragment.TAG) 96 | } 97 | } 98 | } 99 | transaction.commit() 100 | } 101 | 102 | //隐藏当前的Fragment,切换tab时候使用 103 | private fun hideCurrentFragment(transaction: FragmentTransaction) { 104 | when (currentTabId) { 105 | R.id.navigation_home -> transaction.hide(homeFragment) 106 | R.id.navigation_dashboard -> transaction.hide(mvcRVFragment) 107 | R.id.navigation_notifications -> transaction.hide(profileFragment) 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/bean/BaseResponseEntity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.bean 2 | 3 | open class BaseResponseEntity { 4 | 5 | // 判断标示 6 | var code = 0 7 | 8 | // 提示信息 9 | var message: String? = null 10 | 11 | val isSuccess: Boolean 12 | get() = code == 1 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/bean/ResponseEntity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.bean 2 | 3 | /** 4 | * 回调信息统一封装类 5 | * 服务器返回的列表json的标准格式为: 6 | * "code":1 7 | * "message":成功 8 | * "data":{ 9 | * "total":1000 10 | * "totalpage":50 11 | * "currentpage":1 12 | * "items": [{ 13 | * "name":"小涨" 14 | * "age":20 15 | * } 16 | * {...} 17 | * ] 18 | * } 19 | */ 20 | class ResponseEntity : BaseResponseEntity() { 21 | 22 | //显示数据(用户需要关心的数据) 23 | var data: T? = null 24 | private set 25 | 26 | fun setData(data: T) { 27 | this.data = data 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/bean/ResponseException.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.bean 2 | 3 | //为了统一封装 404 和 服务器返回code错误。统一用这个类来处理 4 | class ResponseException(val errorMessage: String?, val errorCode: Int) : Throwable() 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/bean/ResponsePageEntity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.bean 2 | 3 | class ResponsePageEntity : BaseResponseEntity(){ 4 | 5 | //显示数据(用户需要关心的数据) 6 | var data: PageData? = null 7 | } 8 | 9 | class PageData { 10 | var total = 0 11 | var totalpage = 0 12 | var currentpage = 0 13 | var nextpage = 0 14 | 15 | //显示数据 16 | var items: MutableList? = null 17 | 18 | //返回true == 服务器告诉我,当前不是最后一页 还有下一页; false == 当前是最后一页了 19 | fun hasMore(): Boolean { 20 | return totalpage > currentpage 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/bean/UserBaseBean.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.bean 2 | 3 | import java.io.Serializable 4 | 5 | class UserBaseBean : Serializable { 6 | var userid: Int = 0 7 | var name: String? = null 8 | var avatar: String? = null 9 | var gender = 0 10 | var age = 0 11 | var create_time = 0 12 | var follow = 0 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/net/CoroutineUtil.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.tool 2 | 3 | import com.dq.qkotlin.bean.BaseResponseEntity 4 | import com.dq.qkotlin.net.NetworkFailCallback 5 | import com.dq.qkotlin.bean.ResponseException 6 | import kotlinx.coroutines.CoroutineExceptionHandler 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.launch 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | //通用的接口调用 12 | fun requestCommon(scope: CoroutineScope, requestBlock: suspend () -> Unit, failCallback: NetworkFailCallback) { 13 | 14 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext: CoroutineContext, throwable: Throwable -> 15 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 16 | //这里是主线程; 17 | if (throwable is ResponseException) { 18 | //进入这里,说明是服务器返回code错误 19 | val commonException: ResponseException = throwable 20 | 21 | failCallback?.onResponseFail(commonException.errorCode, commonException.errorMessage) 22 | } else { 23 | //进入这里,说明是服务器404 24 | failCallback?.onResponseFail(NET_ERROR_CODE, NET_ERROR) 25 | } 26 | } 27 | 28 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 29 | scope.launch(coroutineExceptionHandler) { 30 | 31 | requestBlock() 32 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 33 | //这里是主线程 34 | } 35 | } 36 | 37 | //检查服务器返回的数据的code是否是success,如果是失败,throw异常到CoroutineExceptionHandler里 38 | //这个方法你可用也可以不用 39 | fun checkResponseCodeAndThrow(responseEntity : BaseResponseEntity){ 40 | if (responseEntity == null){ 41 | throw Throwable() 42 | } 43 | if (!responseEntity.isSuccess){ 44 | throw ResponseException(responseEntity.message, responseEntity.code) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/net/LoadState.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.net 2 | 3 | //具体的网络接口返回情况 4 | enum class LoadState { 5 | None, 6 | Loading, //下拉刷新开始请求接口 or 普通开始请求接口 7 | SuccessHasMore, //下拉刷新请求成功且服务器告诉我还有下一页 or 普通请求成功 8 | SuccessNoMore, //下拉刷新请求成功且服务器告诉我已经没有下一页了 9 | CodeError, //下拉刷新请求成功但是服务器给我返回了错误的code码 or 普通请求成功但是服务器给我返回了错误的code码 10 | NetworkFail, //下拉刷新请求失败 or 普通请求失败,原因是压根就没访问到服务器 11 | PageLoading, //底部翻页开始请求接口 12 | PageSuccessHasMore, //底部翻页请求成功且服务器告诉我还有下一页 13 | PageSuccessNoMore, //底部翻页请求成功且服务器告诉我已经没有下一页了 14 | PageCodeError, //底部翻页请求成功但是服务器给我返回了错误的code码 15 | PageNetworkFail, //底部翻页请求失败,原因是压根就没访问到服务器 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/net/NetworkResponseCallback.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.net 2 | 3 | //这是下面两个的二合一 4 | interface NetworkResponseCallback { 5 | 6 | /** 7 | * 网络+解析json之后的回调( 成功 or 失败都可以用这个) 8 | * @param responseEntry是 最大的那层\我们自己定义的\json解析出来的Model。如果网络请求失败,那么是null 9 | * @param errorMessage是网络请求失败的时候的错误信息 10 | */ 11 | fun onResponse(responseEntry: R?, errorMessage: String?) 12 | } 13 | 14 | interface NetworkFailCallback { 15 | 16 | /** 17 | * 网络请求<失败>回调,原因可能是404,也可能是服务器返回code错误 18 | */ 19 | fun onResponseFail(code: Int, errorMessage: String?) 20 | } 21 | 22 | interface NetworkSuccessCallback { 23 | 24 | /** 25 | * 网络请求<成功>回调,这个成功是网络请求必定有返回值,但是code是否为success不一定 26 | * R 可能是具体的Model,也可以是ResponseEntity, 看你具体的写法。 27 | */ 28 | fun onResponseSuccess(responseEntry: R?) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/net/RetrofitInstance.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.net 2 | 3 | import android.util.Log 4 | import com.dq.qkotlin.BuildConfig 5 | import com.dq.qkotlin.tool.BASE_URL 6 | import com.ihsanbal.logging.LoggingInterceptor 7 | import okhttp3.ConnectionPool 8 | import okhttp3.OkHttpClient 9 | import okhttp3.internal.platform.Platform 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.moshi.MoshiConverterFactory 12 | import java.util.concurrent.TimeUnit 13 | 14 | 15 | class RetrofitInstance { 16 | 17 | private constructor() { 18 | 19 | //RetrofitInstance创建单例, 生命周期比Application还长,即使内存重启也不会重新进入这里 20 | 21 | val builder = OkHttpClient.Builder().connectTimeout(DEFAULT_TIMEOUT.toLong(),TimeUnit.SECONDS) 22 | .writeTimeout(DEFAULT_TIMEOUT.toLong(),TimeUnit.SECONDS) 23 | .readTimeout(5, TimeUnit.SECONDS) 24 | .callTimeout(5, TimeUnit.SECONDS) 25 | .connectionPool(ConnectionPool(8, 15, TimeUnit.SECONDS)) 26 | 27 | if (BuildConfig.DEBUG) { 28 | //打印网络请求日志 29 | builder.addInterceptor( 30 | LoggingInterceptor.Builder() 31 | .setLevel(com.ihsanbal.logging.Level.BASIC) 32 | .log(Platform.INFO) 33 | .request("Request") 34 | .response("Response") 35 | .build() 36 | ) 37 | } 38 | 39 | okHttpClient = builder.build() 40 | 41 | retrofit = Retrofit.Builder() 42 | .client(okHttpClient) 43 | .addConverterFactory(MoshiConverterFactory.create()) 44 | .baseUrl(BASE_URL) 45 | .build() 46 | 47 | } 48 | 49 | companion object { 50 | 51 | private val DEFAULT_TIMEOUT:Int = 5 52 | 53 | private var okHttpClient: OkHttpClient? = null 54 | private var retrofit: Retrofit? = null 55 | 56 | val instance: RetrofitInstance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { 57 | RetrofitInstance() 58 | } 59 | } 60 | 61 | //这样调用,可以把retrofit设置为private 62 | fun create(service: Class): T { 63 | return retrofit!!.create(service) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/net/UserApiService.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.net 2 | 3 | import com.dq.qkotlin.bean.ResponseEntity 4 | import com.dq.qkotlin.bean.ResponsePageEntity 5 | import com.dq.qkotlin.bean.UserBaseBean 6 | import retrofit2.http.* 7 | 8 | interface UserApiService { 9 | 10 | //获取用户列表 11 | @GET("user/getlist") 12 | suspend fun userList(@QueryMap map: HashMap) : ResponsePageEntity 13 | 14 | //关注、点赞、领取 15 | @FormUrlEncoded 16 | @POST("follow/follow") 17 | suspend fun userFollow(@FieldMap map: HashMap) : ResponseEntity 18 | 19 | //删除 20 | @FormUrlEncoded 21 | @POST("user/block") 22 | suspend fun userDelete(@FieldMap map: HashMap) : ResponseEntity 23 | 24 | //获取详情 25 | @GET("user/profile") 26 | suspend fun userProfile(@QueryMap map: HashMap) : ResponseEntity 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/tool/QApplication.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.tool 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | 6 | class QApplication : Application(){ 7 | 8 | companion object { 9 | open lateinit var instance: QApplication 10 | } 11 | 12 | //Tips: 将手机开发者选项里设置为后台进程为2,然后进入后台,点微信,再回来app,结果: 13 | // 这时候重新走了这里的Application onCreate,但是MainVC的onCreate方法中,是从tag启动FM(saveBundle!=null),且tabRootFM里的子FM也是如此 14 | override fun onCreate() { 15 | super.onCreate() 16 | instance = this 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/tool/QUtil.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.tool 2 | 3 | //如果请求服务器404了,那么给Activity回调的code和message就是这两个 4 | const val NET_ERROR_CODE = -1 5 | const val NET_ERROR = "Emm...服务器出问题了" 6 | 7 | const val WX_APPID = "wxef0fccfe0b197" 8 | 9 | //服务端根路径 10 | const val BASE_URL: String = "https://api.itopic.com.cn/api/" 11 | 12 | //图片路径 13 | const val QINIU_URL = "https://qiniu.itopic.com.cn/" 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/tool/UIExtend.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.tool 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | 6 | 7 | fun dp2px(context : Context, dp: Int): Int { 8 | return TypedValue.applyDimension( 9 | TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), 10 | context.getResources().getDisplayMetrics()).toInt() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/tool/ViewBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.tool 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.text.TextUtils 5 | import android.widget.ImageView 6 | import androidx.databinding.BindingAdapter 7 | import com.bumptech.glide.Glide 8 | import com.bumptech.glide.request.RequestOptions 9 | 10 | object ViewBindingAdapter { 11 | 12 | @JvmStatic //kotlin 必须加这句Static,java可以不加 13 | @BindingAdapter(value = ["key", "holder"], requireAll = false) 14 | fun setImageUri( 15 | imageView: ImageView, 16 | key: String, 17 | holder: Drawable 18 | ) { 19 | if (!TextUtils.isEmpty(key)){ 20 | Glide.with(imageView.context) 21 | .load(QINIU_URL + key) 22 | .placeholder(holder) 23 | .into(imageView) 24 | } else { 25 | imageView.setImageDrawable(holder) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/base/BaseLVPagerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.base 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.dq.qkotlin.net.LoadState 5 | import com.dq.qkotlin.bean.ResponsePageEntity 6 | import com.dq.qkotlin.tool.NET_ERROR 7 | import kotlinx.coroutines.CoroutineExceptionHandler 8 | import kotlinx.coroutines.launch 9 | import kotlin.collections.HashMap 10 | 11 | /** 12 | * 如果你的列表界面用的是listview,那么Activity或Fragment里的 MyViewModel 继承自用这个VM,(T是列表的实体类) 13 | * 14 | * 特点:不监听list,只监听网络访问状态loadStatus,然后根据不同的loadStatus来直接用list;轻便简单容易理解 15 | * 为什么:因为listview没有recyclerview的notifyItemRangeInserted,只有notity,所以干脆直接在这里就全部addAll处理好,然后界面直接notity 16 | */ 17 | open class BaseLVPagerViewModel: BaseViewModel() { 18 | 19 | //下拉刷新的错误信息是服务器给我返回的,如果网络请求失败,我就本地自定义 20 | var errorMessage:String? = null 21 | 22 | //最核心的数据列表,我的做法是:不监听他,直接get他 23 | //当然也有人的做法是 LiveData> 监听onChange,然后无脑notityDataChanged,个人觉得这样做反而限制很多,比如如果需要修改某个item的某个状态,你就要全部重新刷新 24 | var list: MutableList? = null 25 | 26 | //下次请求需要带上的页码参数 27 | private var page = 1 28 | 29 | //万能的列表请求的封装,params:接口参数(不包括page字段)。loadmore:true表示底部加载更多,false表示下拉刷新 30 | //block是Retrofit的网络请求代码块 31 | open fun requestList(params : HashMap, loadmore : Boolean , block:suspend() -> ResponsePageEntity){ 32 | 33 | _loadStatus.value = (if (loadmore) LoadState.PageLoading else LoadState.Loading) 34 | 35 | //如果是加载更多,就加上参数page;否则(下拉刷新)就强制设为1,如果服务器要求是0,就改成"0" 36 | params["page"] = if (loadmore) page.toString() else "1" 37 | 38 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 39 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 40 | //这里是主线程; 41 | errorMessage = NET_ERROR; 42 | _loadStatus.setValue( 43 | if (loadmore) LoadState.PageNetworkFail else LoadState.NetworkFail 44 | ) 45 | } 46 | 47 | /*viewModelScope是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 48 | viewModelScope.launch(coroutineExceptionHandler) { 49 | 50 | //用高阶函数,直接让具体的子 51 | val response: ResponsePageEntity = block(); 52 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 53 | 54 | //触发activity的onChanged,让activity处理界面 55 | if (loadmore) { 56 | //加载更多 57 | if (response.isSuccess) {//加载更多服务器返回成功 58 | page++ 59 | response.data?.items?.let { list?.addAll(it) } 60 | _loadStatus.setValue( 61 | if (response.data!!.hasMore()) LoadState.PageSuccessHasMore else LoadState.PageSuccessNoMore 62 | ) 63 | } else { 64 | //加载更多服务器返回code是失败 65 | errorMessage = response.message 66 | _loadStatus.setValue(LoadState.PageCodeError) 67 | } 68 | } else { //下拉刷新的 69 | if (response.isSuccess) { 70 | page = 2 //页面强制设置为下次请求第2页 71 | 72 | //初始化 or 重置list数据 73 | if (list == null) 74 | list = response.data?.items?.toMutableList() 75 | else { 76 | list?.clear() 77 | response.data?.items?.let { list?.addAll(it) } 78 | } 79 | 80 | _loadStatus.setValue( 81 | if (response.data!!.hasMore()) LoadState.SuccessHasMore else LoadState.SuccessNoMore 82 | ) 83 | } else { 84 | //下拉刷新的接口 服务器返回code是失败 85 | errorMessage = response.message 86 | _loadStatus.setValue(LoadState.CodeError) 87 | } 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/base/BaseRVActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.base 2 | 3 | import android.util.Log 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.Observer 9 | import androidx.lifecycle.ViewModelProvider 10 | import com.chad.library.adapter.base.BaseQuickAdapter 11 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 12 | import com.dq.qkotlin.R 13 | import com.dq.qkotlin.net.LoadState 14 | import com.dq.qkotlin.tool.QApplication 15 | import com.scwang.smart.refresh.layout.SmartRefreshLayout 16 | 17 | /** 18 | * 场景:如果Activity里有RecyclerView,那么就继承BaseRVActivity,T是列表数据的每条的Bean,VM 是BaseRVPagerViewModel子类 19 | */ 20 | open abstract class BaseRVActivity> : AppCompatActivity() { 21 | 22 | protected val viewModel: VM by lazy { ViewModelProvider(this).get(onBindViewModel()) } 23 | 24 | //子类自己写获取adapter的方法(比如new ) 然后通过这个方法返回就行了 25 | //out 就是java里的 就是可以兼容BaseViewHolder的子类 26 | abstract fun adapter(): BaseQuickAdapter 27 | 28 | //子类自己写获取refreshLayout的方法(比如findViewById或者binding.) 然后通过这个方法返回就行了 29 | abstract fun refreshLayout(): SmartRefreshLayout 30 | 31 | //子类重写 32 | abstract fun onBindViewModel(): Class 33 | 34 | protected fun initRVObservable() { 35 | //监听网络返回值 36 | viewModel.loadStatus 37 | .observe(this, Observer { loadState: LoadState -> 38 | 39 | Log.e("dq","BaseRVActivity 收到通知"+loadState) 40 | 41 | when (loadState) { 42 | LoadState.None -> { 43 | } 44 | LoadState.Loading -> { 45 | } 46 | LoadState.SuccessNoMore, LoadState.SuccessHasMore -> { 47 | //下拉刷新成功 48 | refreshLayout().finishRefresh(0) 49 | 50 | //这是BaseRecyclerViewAdapterHelper 这个第3方库的写法。如果不用这个库,请换成自己的 addData 和 notifyDataSetChanged 51 | adapter().setList(viewModel.tempRefreshlist!!) 52 | 53 | if (loadState === LoadState.SuccessHasMore) //下拉刷新成功 且 还有更多数据 54 | refreshLayout().finishLoadMore() 55 | else refreshLayout().finishLoadMoreWithNoMoreData() //下拉刷新成功 且 没有更多数据 56 | 57 | //检查是否是空布局 58 | if (viewModel.list.isNullOrEmpty()) { 59 | emptyLayout.findViewById(R.id.empty_tv).setText("空空如也~") 60 | //这是BaseRecyclerViewAdapterHelper 这个第3方库的写法 61 | adapter().setEmptyView(emptyLayout) 62 | } 63 | } 64 | LoadState.CodeError, LoadState.NetworkFail -> { 65 | //下拉刷新失败 66 | refreshLayout().finishRefresh(0) 67 | refreshLayout().finishLoadMoreWithNoMoreData() 68 | 69 | if (viewModel.list.isNullOrEmpty()) { 70 | emptyLayout.findViewById(R.id.empty_tv).setText(viewModel.errorMessage) 71 | adapter().setEmptyView(emptyLayout)//这一句会报警?! 72 | } 73 | } 74 | LoadState.PageLoading -> { 75 | } 76 | LoadState.PageSuccessHasMore , LoadState.PageSuccessNoMore-> { 77 | //加载更多成功 78 | adapter().addData(viewModel.tempPagelist!!) 79 | 80 | if (loadState === LoadState.PageSuccessHasMore) 81 | refreshLayout().finishLoadMore() 82 | else refreshLayout().finishLoadMoreWithNoMoreData() 83 | } 84 | LoadState.PageCodeError, LoadState.PageNetworkFail -> 85 | //加载更多失败 86 | refreshLayout().finishLoadMore() 87 | } 88 | }) 89 | } 90 | 91 | //空布局 92 | private val emptyLayout: View by lazy { 93 | LayoutInflater.from(this).inflate(R.layout.listview_empty, null) 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/base/BaseRVPagerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.base 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.dq.qkotlin.net.LoadState 5 | import com.dq.qkotlin.bean.ResponsePageEntity 6 | import com.dq.qkotlin.tool.NET_ERROR 7 | import kotlinx.coroutines.CoroutineExceptionHandler 8 | import kotlinx.coroutines.launch 9 | import kotlin.collections.HashMap 10 | 11 | /** 12 | * 场景:如果你的列表界面用的是RecyclerView,那么Activity或Fragment里的 MyViewModel 继承这个VM,(T是列表的实体类) 13 | * 14 | * 特点:不监听list,只监听网络访问状态loadStatus,然后根据不同的loadStatus来直接用list;轻便简单容易理解 15 | * 为什么还有tempList这东西?:因为recyclerview有notifyItemRangeInserted,所以翻页的时候要用到这一页的templist,然后用templist做局部刷新 16 | */ 17 | open class BaseRVPagerViewModel: BaseViewModel() { 18 | 19 | //下拉刷新的错误信息是服务器给我返回的,如果网络请求失败,我就本地自定义 20 | var errorMessage:String? = null 21 | 22 | //最核心的数据列表,我的做法是:不监听他,直接get他 23 | //当然也有人的做法是 LiveData> 然后onChange里无脑notityDataChanged,个人觉得那样做反而限制很多 24 | //特别注明:如果使用的是BaseRecyclerViewAdapterHelper,他的adapter里有会有个list的指针,我们这里也有个指针,但是内存共用一个 25 | open val list: MutableList = arrayListOf() 26 | 27 | //下拉刷新请求返回的临时templist: 28 | var tempRefreshlist: List? = null 29 | 30 | //翻页请求返回的临时templist: 31 | //为什么分别定义两个temp:因为极端情况下,下拉刷新和底部翻页同时请求网络,只用一个temp的话就不知道应该setList还是addList 32 | //注意:这样做分成两个也不会造成占用内存增加,因为我addList(tempList)之后, 立即templist = null 33 | var tempPagelist: List? = null 34 | 35 | //下次请求需要带上的页码参数 36 | private var page = 1 37 | 38 | /** 39 | * 功能:万能的列表请求接口 40 | * @params get请求参数,无需page字段 41 | * @loadmore true = 是底部翻页,false = 下拉刷新 42 | * @block 具体的那两行suspend协程请求网络的代码块,其返回值是网络接口返回值 43 | */ 44 | open fun requestList(params : HashMap, loadmore : Boolean , block:suspend() -> ResponsePageEntity){ 45 | 46 | _loadStatus.value = (if (loadmore) LoadState.PageLoading else LoadState.Loading) 47 | 48 | //如果是加载更多,就加上参数page;否则(下拉刷新)就强制设为1,如果服务器要求是0,就改成"0" 49 | params["page"] = if (loadmore) page.toString() else "1" 50 | 51 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 52 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 53 | //这里是主线程; 54 | errorMessage = NET_ERROR 55 | _loadStatus.setValue( 56 | if (loadmore) LoadState.PageNetworkFail else LoadState.NetworkFail 57 | ) 58 | } 59 | 60 | /*viewModelScope是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 61 | viewModelScope.launch(coroutineExceptionHandler) { 62 | 63 | //具体的那两行suspend协程请求网络的代码 由VM子类来实现 64 | val response: ResponsePageEntity = block(); 65 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 66 | 67 | if (loadmore) { 68 | //加载更多 69 | if (response.isSuccess) {//加载更多服务器返回成功 70 | page++ 71 | 72 | //这次底部翻页接口返回的具体List 73 | tempPagelist = response.data?.items 74 | 75 | //触发activity的onChanged,让activity处理界面 76 | _loadStatus.setValue( 77 | if (response.data!!.hasMore()) LoadState.PageSuccessHasMore else LoadState.PageSuccessNoMore 78 | ) 79 | 80 | //代码走到这里,tempPagelist已经用完了(把他addAll了),就立即释放掉temp的内存 81 | tempPagelist = null; 82 | 83 | } else { 84 | _loadStatus.setValue(LoadState.PageCodeError) 85 | } 86 | } else { //下拉刷新请求完毕 87 | if (response.isSuccess) { 88 | page = 2 //页面强制设置为下次请求第2页 89 | 90 | //这次下拉刷新接口返回的具体List 91 | tempRefreshlist = response.data?.items 92 | 93 | //触发activity的onChanged,让activity处理界面 94 | _loadStatus.setValue( 95 | if (response.data!!.hasMore()) LoadState.SuccessHasMore else LoadState.SuccessNoMore 96 | ) 97 | 98 | //代码走到这里,界面已经用过了tempRefreshlist(把他addAll了),就立即释放掉temp的内存 99 | tempRefreshlist = null; 100 | 101 | } else { 102 | //服务器告诉我参数错误 103 | _loadStatus.setValue(LoadState.CodeError) 104 | errorMessage = response.message 105 | } 106 | } 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.base 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.dq.qkotlin.bean.UserBaseBean 7 | import com.dq.qkotlin.net.* 8 | 9 | open class BaseViewModel: ViewModel() { 10 | //内部使用可变的Mutable 11 | protected val _loadStatus = MutableLiveData() 12 | 13 | //对外开放的是final,这是谷歌官方的写法 14 | open val loadStatus: LiveData = _loadStatus 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/base/INavBar.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.base 2 | 3 | import android.view.Gravity 4 | import android.view.View 5 | import android.view.ViewStub 6 | import android.widget.TextView 7 | import androidx.appcompat.widget.Toolbar 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.FragmentActivity 10 | import com.dq.qkotlin.R 11 | import com.gyf.immersionbar.ImmersionBar 12 | 13 | interface INavBar { 14 | 15 | //传入xml里的ViewStub,返回Toolbar 16 | fun initToolbarView(toolbarViewStub: ViewStub) : Toolbar? { 17 | toolbarViewStub?.let { 18 | if (enableToolbar()) { 19 | it.layoutResource = getBindToolbarLayout() 20 | val mToolbar: Toolbar = it.inflate().findViewById(R.id.toolbar_root) 21 | 22 | //左边返回按钮 23 | val toolBarLeftIcon = getToolBarLeftIcon() 24 | if (toolBarLeftIcon == 0) { 25 | mToolbar.navigationIcon = null 26 | } else { 27 | mToolbar.setNavigationIcon(toolBarLeftIcon) 28 | } 29 | 30 | mToolbar.setNavigationOnClickListener { view -> 31 | onNavigationOnClick(view) 32 | } 33 | 34 | mToolbar.title = getTootBarTitle() 35 | // setTitleCenter(mToolbar) 36 | 37 | return mToolbar 38 | } 39 | } 40 | return null 41 | } 42 | 43 | //设置状态栏 44 | fun initStatusBar(activity: FragmentActivity) { 45 | //设置共同沉浸式样式 46 | ImmersionBar.with(activity) 47 | .fitsSystemWindows(true) 48 | .statusBarColor(R.color.toolbar_color) 49 | // .statusBarDarkFont(true, 0.2f) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度 50 | .statusBarDarkFont(true) //状态栏字体是深色,不写默认为亮色 51 | .navigationBarColor(R.color.white) 52 | .navigationBarDarkIcon(true).init() 53 | } 54 | 55 | fun initStatusBar(fragment: Fragment) { 56 | //设置共同沉浸式样式 57 | ImmersionBar.with(fragment) 58 | .fitsSystemWindows(true) 59 | .statusBarColor(R.color.toolbar_color) 60 | .statusBarDarkFont(true) //状态栏字体是深色,不写默认为亮色 61 | .navigationBarColor(R.color.white) 62 | .navigationBarDarkIcon(true).init() 63 | } 64 | 65 | 66 | fun getTootBarTitle(): String { 67 | return "" 68 | } 69 | 70 | /** 71 | * 设置返回按钮的图样,可以是Drawable ,也可以是ResId 72 | * 注:仅在 enableToolBarLeft 返回为 true 时候有效 73 | */ 74 | fun getToolBarLeftIcon(): Int { 75 | return R.drawable.ic_white_black_24dp 76 | } 77 | 78 | fun enableToolbar(): Boolean { 79 | return true 80 | } 81 | 82 | //toolbar用什么布局 83 | fun getBindToolbarLayout(): Int { 84 | return R.layout.toolbar_common 85 | } 86 | 87 | fun onNavigationOnClick(view: View) {} 88 | 89 | //把状态栏的标题居中 90 | private fun setTitleCenter(mToolbar: Toolbar) { 91 | for (i in 0 until mToolbar.childCount) { 92 | val view: View = mToolbar.getChildAt(i) 93 | if (view is TextView) { 94 | view.gravity = Gravity.CENTER 95 | val params = Toolbar.LayoutParams(Toolbar.LayoutParams.WRAP_CONTENT,Toolbar.LayoutParams.MATCH_PARENT) 96 | params.gravity = Gravity.CENTER 97 | view.layoutParams = params 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/common/WelcomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.common 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.dq.qkotlin.MainActivity 9 | import com.dq.qkotlin.ui.base.INavBar 10 | import java.util.* 11 | 12 | class WelcomeActivity : AppCompatActivity() ,INavBar { 13 | 14 | private val handler = Handler(Looper.getMainLooper()) 15 | 16 | public override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | initStatusBar(this) 19 | 20 | //延时是为了让onCreate先走完,耗时代码放到Runnable里,这样可以防止耗时代码导致的白屏 21 | handler.postDelayed(initRun, 150) 22 | } 23 | 24 | private val initRun = Runnable { 25 | //这里可以写耗时操作,比如读取数据,初始化表情,读取缓存,设置全局变量等 26 | 27 | //1.3秒后跳转到主页 或者 登录页,看具体需求 28 | handler.postDelayed(launchHome, 1300) 29 | } 30 | 31 | private val launchHome = Runnable { 32 | val intent = Intent() 33 | intent.setClass(this@WelcomeActivity, MainActivity::class.java) 34 | // if (AccountManager.instance.isLogin){ 35 | // intent.putExtra("relink", intent.getIntExtra("relink", -1)) 36 | // intent.setClass(this@WelcomeActivity, MainActivity::class.java) 37 | // } else { 38 | // intent.setClass(this@WelcomeActivity, UserRegWechatActivity::class.java) 39 | // } 40 | startActivity(intent) 41 | finish() 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/DetailActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import com.dq.qkotlin.ui.base.INavBar 5 | 6 | class DetailActivity : AppCompatActivity(), INavBar { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.AdapterView 10 | import android.widget.TextView 11 | import android.widget.Toast 12 | import androidx.databinding.DataBindingUtil 13 | import androidx.fragment.app.Fragment 14 | import androidx.lifecycle.MutableLiveData 15 | import androidx.lifecycle.Observer 16 | import androidx.lifecycle.ViewModelProvider 17 | import com.dq.qkotlin.R 18 | import com.dq.qkotlin.bean.UserBaseBean 19 | import com.dq.qkotlin.databinding.ActivityListviewBinding 20 | import com.dq.qkotlin.bean.ResponseEntity 21 | import com.dq.qkotlin.net.LoadState 22 | import com.dq.qkotlin.net.NetworkResponseCallback 23 | import com.dq.qkotlin.tool.QApplication 24 | import com.dq.qkotlin.ui.home.detail.UserRVActivity 25 | import com.dq.qkotlin.ui.base.INavBar 26 | import com.kongzue.dialogx.dialogs.WaitDialog 27 | 28 | //基于MVVM + ListView + ViewBinding 29 | class HomeFragment : Fragment(), INavBar { 30 | 31 | //为了展示ErrorView 下拉刷新后变成正常列表 32 | private var debugError = 1; 33 | 34 | companion object { 35 | val TAG = "HomeFragment" 36 | } 37 | 38 | private lateinit var homeViewModel: HomeViewModel 39 | 40 | private lateinit var binding: ActivityListviewBinding 41 | 42 | //本类的做法是:只有下拉刷新的时候才new adapter 并 setAdapter。你也可以修改成onCreate里就初始化,也可以尝试一下直接用binding.adapter,不用这个全局变量了 43 | private var adapter: HomeListViewAdapter? = null 44 | 45 | //空布局 46 | private val emptyLayout: View by lazy { 47 | LayoutInflater.from(activity).inflate(R.layout.listview_empty, null) 48 | } 49 | 50 | //AC.onCreate -> FM.onAttach -> FM.onCreate -> FM.onCreateView -> FM.onActivityCreated 51 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 52 | homeViewModel = ViewModelProvider(this).get(HomeViewModel::class.java) 53 | binding = DataBindingUtil.inflate(inflater, R.layout.activity_listview, container,false) 54 | val root: View = binding.getRoot() 55 | return root 56 | } 57 | 58 | //... -> FM.onCreateView -> FM.onActivityCreated -> FM.onStart -> AC.onStart -> AC.onResume -> FM.onResume 59 | override fun onActivityCreated(savedInstanceState: Bundle?) { 60 | super.onActivityCreated(savedInstanceState) 61 | initView() 62 | 63 | homeViewModel.loadStatus 64 | .observe(viewLifecycleOwner, Observer { loadState: LoadState -> 65 | Log.e("dq","HomeFragment 收到通知"+loadState) 66 | when (loadState) { 67 | LoadState.None -> { 68 | } 69 | LoadState.Loading -> { 70 | } 71 | LoadState.SuccessNoMore, LoadState.SuccessHasMore -> { 72 | //下拉刷新成功 73 | binding.refreshLayout.finishRefresh(0) 74 | 75 | if (adapter == null) { 76 | //说明是第一次下拉刷新成功 77 | adapter = HomeListViewAdapter(requireContext() ,homeViewModel.list!! , R.layout.listitem_follower) 78 | adapter!!.setOnItemClickListener(object : HomeListViewAdapter.OnItemClickListener { 79 | 80 | override fun onItemClick(view: View, position: Int) { 81 | //Item的view的点击事件 82 | val itemBean: UserBaseBean = homeViewModel.list!!.get(position) 83 | if (view.id == R.id.follow_button){ 84 | //关注用户 85 | if (position % 2 == 0) { 86 | //MVC方式去关注 87 | executeFollowUserByMVC(itemBean) 88 | } else { 89 | //MVVM方式去关注 90 | lazyCreateFollowLiveData() 91 | 92 | val params = HashMap() 93 | params["userid"] = "1" 94 | params["to_userid"] = itemBean.userid.toString() 95 | homeViewModel.requestFollowByMVVM(itemBean.userid , itemBean.follow, if (itemBean.follow == 1) 0 else 1 , params) 96 | } 97 | 98 | } else if (view.id == R.id.delete_button){ 99 | //删除用户 100 | if (position % 2 == 0) { 101 | //MVC方式去删除 102 | executeDeleteUserByMVC(itemBean) 103 | } else { 104 | //MVVM方式去删除 105 | lazyCreateDeleteLiveData() 106 | 107 | val params = HashMap() 108 | params["userid"] = "1" 109 | params["to_userid"] = itemBean.userid.toString() 110 | homeViewModel.requestDeleteByMVVM(itemBean.userid ,params) 111 | } 112 | 113 | } 114 | } 115 | }) 116 | //这一步是利用DataBinding触发数据刷新,相当于setAdapter 117 | binding.adapter = adapter 118 | } else { 119 | //说明是第N次下拉刷新成功 120 | adapter!!.notifyDataSetChanged() 121 | } 122 | 123 | if (loadState === LoadState.SuccessHasMore) { 124 | //下拉刷新成功 且 还有更多数据 125 | binding.refreshLayout.finishLoadMore() 126 | } else { 127 | //下拉刷新成功 且 没有更多数据 128 | binding.refreshLayout.finishLoadMoreWithNoMoreData() 129 | } 130 | 131 | //检查是否显示\隐藏EmptyView 132 | if (homeViewModel.list!!.isEmpty()) { 133 | //这个setEmptyView是我自己封装的,如果有数据了系统底层自己会帮我们隐藏emptyLayout 134 | binding.listView.setEmptyView(emptyLayout) 135 | } 136 | } 137 | LoadState.CodeError, LoadState.NetworkFail -> { 138 | //下拉刷新失败 139 | binding.refreshLayout.finishRefresh(0) 140 | binding.refreshLayout.finishLoadMoreWithNoMoreData() 141 | 142 | //设置ErrorView 143 | emptyLayout.findViewById(R.id.empty_tv).setText(homeViewModel.errorMessage) 144 | binding.listView.setEmptyView(emptyLayout) 145 | } 146 | LoadState.PageLoading -> { 147 | } 148 | LoadState.PageSuccessHasMore -> { 149 | //底部加载更多成功 且 还有更多数据 150 | adapter?.notifyDataSetChanged() 151 | binding.refreshLayout.finishLoadMore() 152 | } 153 | LoadState.PageSuccessNoMore -> { 154 | //底部加载更多成功 且 没有更多数据了 155 | adapter?.notifyDataSetChanged() 156 | binding.refreshLayout.finishLoadMoreWithNoMoreData() 157 | } 158 | LoadState.PageCodeError, LoadState.PageNetworkFail -> 159 | //底部加载失败 160 | binding.refreshLayout.finishLoadMore() 161 | } 162 | }) 163 | 164 | 165 | binding.refreshLayout.setOnRefreshListener { 166 | //触发下拉刷新 167 | val params = HashMap() 168 | params["keyword"] = "小" 169 | params["userid"] = "1" 170 | homeViewModel.requestUserList(params, false) 171 | } 172 | 173 | binding.refreshLayout.setOnLoadMoreListener { 174 | val params = HashMap() 175 | params["keyword"] = "小" 176 | params["userid"] = "1" 177 | homeViewModel.requestUserList(params, true) 178 | } 179 | 180 | binding.listView.setOnItemClickListener{ adapterView: AdapterView<*>?, view: View?, position: Int, l: Long -> 181 | 182 | val i = Intent(context, UserRVActivity::class.java) 183 | i.putExtra("param1",homeViewModel.list!!.get(position)); 184 | startActivity(i) 185 | } 186 | 187 | binding.refreshLayout.autoRefresh(100,200,1f,false);//延迟100毫秒后自动刷新 188 | } 189 | 190 | private fun initView(){ 191 | initStatusBar(this) 192 | initToolbarView( requireView().findViewById(R.id.view_stub_toolbar)) 193 | } 194 | 195 | //去关注某用户 ---- 采用MVC的写法 196 | private fun executeFollowUserByMVC(itemBean: UserBaseBean){ 197 | 198 | //逻辑:先本地设置data数据,然后再请求服务器,如果服务器访问失败,再把本地data设置回来。这样用户体验更好 199 | itemBean.follow = if (itemBean.follow == 1) 0 else 1 200 | adapter!!.notifyDataSetChanged() 201 | 202 | val params = HashMap() 203 | params["userid"] = "1" 204 | params["to_userid"] = itemBean.userid.toString() 205 | homeViewModel.requestFollowByMVC(params, object : 206 | NetworkResponseCallback> { 207 | //网络请求返回 208 | override fun onResponse(responseEntry: ResponseEntity?, errorMessage: String?) { 209 | 210 | if (responseEntry == null){ 211 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 212 | Toast.makeText(QApplication.instance, errorMessage , Toast.LENGTH_SHORT).show() 213 | 214 | itemBean.follow = if (itemBean.follow == 1) 0 else 1 215 | adapter!!.notifyDataSetChanged() 216 | 217 | return 218 | } else if (!responseEntry.isSuccess){ 219 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 220 | Toast.makeText(QApplication.instance, responseEntry.message , Toast.LENGTH_SHORT).show() 221 | 222 | itemBean.follow = if (itemBean.follow == 1) 0 else 1 223 | adapter!!.notifyDataSetChanged() 224 | 225 | return 226 | } 227 | } 228 | }) 229 | } 230 | 231 | //因为不是每次进入Activity都会调用这个接口,但是如果你每次都直接就初始化这个livedata,会额外多初始化一些系统livedata源码里的全局变量,浪费内存。所以这里做成懒加载 232 | private fun lazyCreateFollowLiveData(){ 233 | if (homeViewModel.followRequestStateMap == null){ 234 | homeViewModel.followRequestStateMap = MutableLiveData>() 235 | 236 | //MVVM 关注某用户,key是userid(int) value是:请求中、请求成功、请求失败。 237 | homeViewModel.followRequestStateMap!!.observe(viewLifecycleOwner, Observer { loadStateMap: Pair -> 238 | 239 | val to_userid: Int = loadStateMap.first 240 | val idealState: Int = loadStateMap.second // 0 :最新的状态是"未关注" ;1:最新的状态是"已关注" 241 | 242 | Log.e("dq","HomeFragment 关注收到通知"+idealState) 243 | 244 | for (itemBean in homeViewModel.list!!) { 245 | if (itemBean.userid == to_userid){ 246 | itemBean.follow = idealState 247 | adapter!!.notifyDataSetChanged() 248 | break 249 | } 250 | } 251 | }) 252 | } 253 | } 254 | 255 | 256 | //删除Item ---- 采用MVC的写法 257 | private fun executeDeleteUserByMVC(itemBean: UserBaseBean){ 258 | WaitDialog.show("MVC写法加载中..") 259 | val params = HashMap() 260 | params["userid"] = "1" 261 | params["to_userid"] = itemBean.userid.toString() 262 | homeViewModel.requestDeleteByMVC(params, object : 263 | NetworkResponseCallback> { 264 | //网络请求返回 265 | override fun onResponse(responseEntry: ResponseEntity?, errorMessage: String?) { 266 | //关闭弹框 267 | WaitDialog.dismiss() 268 | 269 | if (responseEntry == null){ 270 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 271 | Toast.makeText(QApplication.instance, errorMessage , Toast.LENGTH_SHORT).show() 272 | return 273 | } else if (!responseEntry.isSuccess){ 274 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 275 | Toast.makeText(QApplication.instance, responseEntry.message , Toast.LENGTH_SHORT).show() 276 | return 277 | } 278 | 279 | homeViewModel.list!!.remove(itemBean) 280 | adapter!!.notifyDataSetChanged() 281 | 282 | } 283 | }) 284 | } 285 | 286 | //因为不是每次进入Activity都会调用这个接口,但是如果你每次都直接就初始化这个livedata,会额外多初始化一些系统livedata源码里的全局变量,浪费内存。所以这里做成懒加载 287 | private fun lazyCreateDeleteLiveData(){ 288 | if (homeViewModel.deleteRequestStatePair == null){ 289 | homeViewModel.deleteRequestStatePair = MutableLiveData>() 290 | 291 | //MVVM 关注某用户,key是userid(int) value是:请求中、请求成功、请求失败。 292 | homeViewModel.deleteRequestStatePair!!.observe(viewLifecycleOwner, Observer { loadStateMap: Pair -> 293 | 294 | val to_userid: Int = loadStateMap.first 295 | val loadState: LoadState = loadStateMap.second 296 | 297 | when (loadState) { 298 | LoadState.Loading -> { 299 | WaitDialog.show("MVVM写法加载中..") 300 | } 301 | LoadState.SuccessNoMore -> { 302 | //删除成功 303 | WaitDialog.dismiss() 304 | for (itemBean in homeViewModel.list!!) { 305 | if (itemBean.userid == to_userid){ 306 | homeViewModel.list!!.remove(itemBean) 307 | adapter!!.notifyDataSetChanged() 308 | break 309 | } 310 | } 311 | } 312 | LoadState.CodeError, LoadState.NetworkFail -> { 313 | //删除失败 314 | WaitDialog.dismiss() 315 | Toast.makeText(QApplication.instance, homeViewModel.deleteRequestErrorMessage , Toast.LENGTH_SHORT).show() 316 | } 317 | } 318 | }) 319 | } 320 | } 321 | 322 | override fun getTootBarTitle(): String { 323 | return "MVVM+ListView+viewBindng" 324 | } 325 | 326 | override fun getToolBarLeftIcon(): Int { 327 | return 0 328 | } 329 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/HomeListViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.BaseAdapter 8 | import androidx.annotation.LayoutRes 9 | import androidx.databinding.DataBindingUtil 10 | import com.dq.qkotlin.BR 11 | import com.dq.qkotlin.bean.UserBaseBean 12 | import com.dq.qkotlin.databinding.ListitemFollowerBinding 13 | 14 | //基于DataBindingUtil的 ListView Adapter,但是下半段展示的是基于传统的方式 15 | class HomeListViewAdapter(val context: Context, var list: MutableList 16 | , @LayoutRes private val layoutResId: Int) : BaseAdapter() { 17 | 18 | private var onItemClickListener: OnItemClickListener? = null 19 | 20 | override fun getCount(): Int = list.size 21 | 22 | override fun getItem(position: Int): Any = list[position] 23 | 24 | override fun getItemId(position: Int): Long { 25 | return position.toLong() 26 | } 27 | 28 | override fun getView(position: Int,convertView: View?, parent: ViewGroup?): View { 29 | //前半段是基于ViewBinding绑定数据,虽然简化很多 但是有一些弊端:比如自定义显示条件的时候需要新增很多类且别人不容易找到对应的类,且xml不方便复用 30 | val binding: ListitemFollowerBinding 31 | if (convertView == null){ 32 | binding = DataBindingUtil.inflate(LayoutInflater.from(context), 33 | layoutResId ,parent,false) 34 | binding.followButton.setOnClickListener { v -> 35 | onItemClickListener?.onItemClick(v ,v.tag as Int); 36 | } 37 | binding.deleteButton.setOnClickListener { v -> 38 | onItemClickListener?.onItemClick(v ,v.tag as Int); 39 | } 40 | } else { 41 | binding = DataBindingUtil.getBinding(convertView)!! 42 | } 43 | binding.setVariable(BR.bean, list[position]) 44 | 45 | //后半段是基于传统的,我建议还是用传统的方式去做 46 | binding.followButton.tag = position 47 | binding.followButton.setText( if (list[position].follow == 1) "已经关注" else "未关注") 48 | 49 | binding.deleteButton.tag = position 50 | 51 | return binding.root 52 | } 53 | 54 | fun setOnItemClickListener(onItemClickListener: OnItemClickListener) { 55 | this.onItemClickListener = onItemClickListener 56 | } 57 | 58 | interface OnItemClickListener { 59 | fun onItemClick(view: View ,position: Int) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.viewModelScope 5 | import com.dq.qkotlin.bean.ResponseEntity 6 | import com.dq.qkotlin.bean.ResponsePageEntity 7 | import com.dq.qkotlin.bean.UserBaseBean 8 | import com.dq.qkotlin.net.* 9 | import com.dq.qkotlin.tool.NET_ERROR 10 | import com.dq.qkotlin.ui.base.BaseLVPagerViewModel 11 | import kotlinx.coroutines.CoroutineExceptionHandler 12 | import kotlinx.coroutines.launch 13 | 14 | class HomeViewModel: BaseLVPagerViewModel() { 15 | 16 | //请求列表,params:接口参数(不包括page字段)。loadmore:true表示底部加载更多,false表示下拉刷新 17 | fun requestUserList(params : HashMap , loadmore : Boolean){ 18 | 19 | //调用《万能》列表请求的封装 20 | super.requestList(params, loadmore){ 21 | 22 | //这里只需要写"本Activity的Retrofit请求列表接口"的代码闭包,返回值是 23 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 24 | val response: ResponsePageEntity = apiService.userList(params) 25 | response 26 | } 27 | } 28 | 29 | /******* 关注 ********/ 30 | 31 | //请求关注接口(MVC的调用方式) 32 | fun requestFollowByMVC(params : HashMap, responseCallback: NetworkResponseCallback>){ 33 | 34 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 35 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 36 | //这里是主线程; 37 | responseCallback?.onResponse(null, NET_ERROR) 38 | } 39 | 40 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 41 | viewModelScope.launch(coroutineExceptionHandler) { 42 | 43 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 44 | //suspend 是这一步 45 | val response: ResponseEntity = apiService.userFollow(params) 46 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 47 | //这里是主线程 48 | responseCallback?.onResponse(response, null) 49 | } 50 | } 51 | 52 | //关注接口请求状态:key是userid(int) value是:关注状态,0未关注,1已经关注,这里用int以免以后出现 互相关注3 。如果是点赞,那用bool就行了 53 | //因为不是每次进入Activity都会调用这个接口,所以用lazy节省内存 54 | var followRequestStateMap: MutableLiveData>? = null 55 | 56 | //关注接口请求 - 错误message,要显示给用户看。你也可以统一用一个toastMessage,但是用统一message的弊端是无法针对某个接口的错误做特殊UI定制 57 | var followRequestErrorMessage: String? = null 58 | 59 | //请求关注接口(MVVM的调用方式) 60 | fun requestFollowByMVVM(to_userid: Int, currentValue: Int, idealValue: Int, params : HashMap) { 61 | 62 | if (followRequestStateMap == null){ 63 | followRequestStateMap = MutableLiveData>() 64 | } 65 | 66 | val loadingMap = Pair(to_userid, idealValue) 67 | followRequestStateMap!!.value = loadingMap 68 | 69 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 70 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 71 | //这里是主线程; 72 | followRequestErrorMessage = NET_ERROR 73 | //给Activity回调网络请求失败 74 | val errorMap = Pair(to_userid, currentValue) 75 | followRequestStateMap!!.value = errorMap 76 | } 77 | 78 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 79 | viewModelScope.launch(coroutineExceptionHandler) { 80 | 81 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 82 | //suspend 是这一步 83 | val responseEntry: ResponseEntity = apiService.userFollow(params) 84 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 85 | //这里是主线程 86 | if (responseEntry == null){ 87 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 88 | followRequestErrorMessage = NET_ERROR 89 | //给Activity回调网络请求失败 90 | val errorMap = Pair(to_userid, currentValue) 91 | followRequestStateMap!!.value = errorMap 92 | 93 | return@launch 94 | 95 | } else if (!responseEntry.isSuccess){ 96 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 97 | followRequestErrorMessage = responseEntry.message 98 | //给Activity回调code失败 99 | val errorMap = Pair(to_userid , currentValue) 100 | followRequestStateMap!!.value = errorMap 101 | 102 | return@launch 103 | } 104 | 105 | //请求成功了,因为在请求前已经提前发出成功通知了,真的成功就不用发了 106 | } 107 | } 108 | 109 | 110 | /******* 删除 ********/ 111 | 112 | //请求删除接口(MVC的调用方式) 113 | fun requestDeleteByMVC(params : HashMap, responseCallback: NetworkResponseCallback>){ 114 | 115 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 116 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 117 | //这里是主线程; 118 | responseCallback?.onResponse(null, NET_ERROR) 119 | } 120 | 121 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 122 | viewModelScope.launch(coroutineExceptionHandler) { 123 | 124 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 125 | //suspend 是这一步 126 | val response: ResponseEntity = apiService.userDelete(params) 127 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 128 | //这里是主线程 129 | responseCallback?.onResponse(response, null) 130 | } 131 | } 132 | 133 | 134 | //删除接口请求状态:key是userid(int) value是:请求中、请求成功、请求失败。 135 | //因为不是每次进入Activity都会调用这个接口,所以用lazy节省内存 136 | var deleteRequestStatePair: MutableLiveData>? = null 137 | 138 | //删除接口请求 - 错误message,要显示给用户看。你也可以统一用一个toastMessage,但是用统一message的弊端是无法针对某个接口的错误做特殊UI定制 139 | var deleteRequestErrorMessage: String? = null 140 | 141 | //请求删除接口(MVVM的调用方式) 142 | fun requestDeleteByMVVM(to_userid: Int, params : HashMap){ 143 | 144 | if (deleteRequestStatePair == null){ 145 | deleteRequestStatePair = MutableLiveData>() 146 | } 147 | 148 | val loadingPair = Pair(to_userid, LoadState.Loading) 149 | deleteRequestStatePair!!.value = loadingPair 150 | 151 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 152 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 153 | //这里是主线程; 154 | deleteRequestErrorMessage = NET_ERROR 155 | //给Activity回调网络请求失败 156 | val errorPair = Pair(to_userid, LoadState.NetworkFail) 157 | deleteRequestStatePair!!.value = errorPair 158 | } 159 | 160 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 161 | viewModelScope.launch(coroutineExceptionHandler) { 162 | 163 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 164 | //suspend 是这一步 165 | val responseEntry: ResponseEntity = apiService.userDelete(params) 166 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 167 | //这里是主线程 168 | if (responseEntry == null){ 169 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 170 | deleteRequestErrorMessage = NET_ERROR 171 | //给Activity回调网络请求失败 172 | val errorPair = Pair(to_userid, LoadState.NetworkFail) 173 | deleteRequestStatePair!!.value = errorPair 174 | 175 | return@launch 176 | 177 | } else if (!responseEntry.isSuccess){ 178 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 179 | deleteRequestErrorMessage = responseEntry.message 180 | //给Activity回调code失败 181 | val errorPair = Pair(to_userid , LoadState.NetworkFail) 182 | deleteRequestStatePair!!.value = errorPair 183 | 184 | return@launch 185 | } 186 | 187 | //给Activity回调成功 188 | val successPair = Pair(to_userid , LoadState.SuccessNoMore) 189 | deleteRequestStatePair!!.value = successPair 190 | } 191 | } 192 | 193 | 194 | 195 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/detail/UserQuickAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home.detail 2 | 3 | import com.chad.library.adapter.base.BaseQuickAdapter 4 | import com.chad.library.adapter.base.viewholder.BaseDataBindingHolder 5 | import com.dq.qkotlin.R 6 | import com.dq.qkotlin.bean.UserBaseBean 7 | import com.dq.qkotlin.databinding.ListitemFollowerBinding 8 | 9 | 10 | class UserQuickAdapter(list: MutableList) 11 | : BaseQuickAdapter>(R.layout.listitem_follower,list) { 12 | 13 | // BaseRecyclerViewAdapterHelper官方的代码里有这么一句mPresenter,发现没什么用,就去掉了 14 | // private val mPresenter: UserBaseBeanPresenter = UserBaseBeanPresenter() 15 | 16 | override fun convert(holder: BaseDataBindingHolder, item: UserBaseBean) { 17 | 18 | //设置子View的点击事件 19 | // 获取 Binding 20 | val binding: ListitemFollowerBinding? = holder.dataBinding 21 | if (binding != null) { 22 | binding.setBean(item) 23 | // binding.setPresenter(mPresenter) 24 | binding.executePendingBindings() 25 | 26 | //后半段是基于传统的,我建议还是用传统的方式去做 27 | binding.followButton.setText( if (item.follow == 1) "已经关注" else "未关注") 28 | 29 | } else { 30 | // BaseRecyclerViewAdapterHelper官方的代码里做了 != null 判断,但是我测试发现不可能为null 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/detail/UserRVActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home.detail 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.View 6 | import android.widget.Toast 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.lifecycle.MutableLiveData 9 | import androidx.lifecycle.Observer 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import com.chad.library.adapter.base.BaseQuickAdapter 12 | import com.chad.library.adapter.base.listener.OnItemChildClickListener 13 | import com.chad.library.adapter.base.listener.OnItemClickListener 14 | import com.dq.qkotlin.R 15 | import com.dq.qkotlin.bean.PageData 16 | import com.dq.qkotlin.bean.ResponseEntity 17 | import com.dq.qkotlin.bean.UserBaseBean 18 | import com.dq.qkotlin.databinding.ActivityRecyclerviewBinding 19 | import com.dq.qkotlin.net.* 20 | import com.dq.qkotlin.tool.QApplication 21 | import com.dq.qkotlin.ui.base.BaseRVActivity 22 | import com.dq.qkotlin.ui.base.INavBar 23 | import com.dq.qkotlin.view.SpacesItemDecoration 24 | import com.kongzue.dialogx.dialogs.WaitDialog 25 | import com.scwang.smart.refresh.layout.SmartRefreshLayout 26 | 27 | /** 28 | * Mvvm + RecyclerView的Demo,具体每一条的bean是UserBaseBean,VM是UserArrayViewModel 29 | */ 30 | class UserRVActivity : BaseRVActivity() , INavBar { 31 | 32 | //dqerror 33 | private var debugerror = 1 34 | 35 | private lateinit var adapter: UserQuickAdapter 36 | 37 | private lateinit var binding: ActivityRecyclerviewBinding 38 | 39 | private fun initView() { 40 | 41 | //设置状态栏 42 | initStatusBar(this) 43 | //设置toolbar 44 | initToolbarView(findViewById(R.id.view_stub_toolbar)) 45 | 46 | adapter = UserQuickAdapter(viewModel.list) 47 | 48 | // 先注册需要点击的子控件id(注意,请不要写在convert方法里) 49 | adapter.addChildClickViewIds(R.id.follow_button, R.id.delete_button); 50 | 51 | adapter.setOnItemChildClickListener(object : OnItemChildClickListener { 52 | override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) { 53 | 54 | val itemBean: UserBaseBean = viewModel.list!!.get(position) 55 | when(view?.id) { 56 | R.id.follow_button -> { 57 | //关注用户 58 | if (position % 2 == 0) { 59 | //MVC方式去关注 60 | executeFollowUserByMVC(position, itemBean) 61 | } else { 62 | //MVVM方式去关注 63 | lazyCreateFollowLiveData() 64 | 65 | val params = HashMap() 66 | params["userid"] = "1" 67 | params["to_userid"] = itemBean.userid.toString() 68 | viewModel.requestFollowByMVVM(itemBean.userid , itemBean.follow, if (itemBean.follow == 1) 0 else 1 , params) 69 | } 70 | } 71 | R.id.delete_button -> { 72 | //删除用户 73 | if (position % 2 == 0) { 74 | //MVC方式去删除 75 | executeDeleteUserByMVC(position, itemBean) 76 | } else { 77 | //MVVM方式去删除 78 | lazyCreateDeleteLiveData() 79 | 80 | val params = HashMap() 81 | params["userid"] = "1" 82 | params["to_userid"] = itemBean.userid.toString() 83 | viewModel.requestDeleteByMVVM(itemBean.userid ,params) 84 | } 85 | } 86 | } 87 | } 88 | }) 89 | 90 | // 设置颜色、高度、间距等 91 | val itemDecoration = SpacesItemDecoration(this, SpacesItemDecoration.VERTICAL) 92 | .setParam(R.color.divider_color, 1, 30f, 0f) 93 | binding.recyclerView.addItemDecoration(itemDecoration) 94 | 95 | binding.recyclerView.layoutManager = LinearLayoutManager(this) 96 | binding.recyclerView.adapter = adapter 97 | 98 | } 99 | 100 | override fun onCreate(savedInstanceState: Bundle?) { 101 | super.onCreate(savedInstanceState) 102 | binding = DataBindingUtil.setContentView(this, R.layout.activity_recyclerview) 103 | initView() 104 | initRVObservable() 105 | 106 | binding.refreshLayout.setOnRefreshListener { 107 | val params = HashMap() 108 | params["userid"] = "1" 109 | params["keyword"] = "大" 110 | params["error"] = debugerror.toString() 111 | 112 | viewModel.requestUserList(params, false) 113 | 114 | debugerror = 0 115 | } 116 | 117 | binding.refreshLayout.setOnLoadMoreListener { 118 | val params = HashMap() 119 | params["userid"] = "1" 120 | params["keyword"] = "大" 121 | viewModel.requestUserList(params, true) 122 | } 123 | 124 | 125 | //demo 添加的 Header 126 | //Header 是自行添加进去的 View,所以 Adapter 不管理 Header 的 DataBinding。 127 | //请在外部自行完成数据的绑定 128 | // val view: View = layoutInflater.inflate(R.layout.listitem_follower, null, false) 129 | // view.findViewById(R.id.iv).setVisibility(View.GONE) 130 | // adapter.addHeaderView(view) 131 | 132 | binding.refreshLayout.autoRefresh(100,200,1f,false);//延迟100毫秒后自动刷新 133 | 134 | //item 点击事件 135 | adapter.setOnItemClickListener(object : OnItemClickListener { 136 | override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) { 137 | val params = HashMap() 138 | params["userid"] = "1" 139 | params["keyword"] = "大" 140 | params["error"] = "1" 141 | viewModel.requestListByMVC(params, object : NetworkSuccessCallback>{ 142 | 143 | override fun onResponseSuccess(responseEntry: PageData?) { 144 | Log.e("dq","成功 onResponse = "+ responseEntry!!.items!!.size) 145 | } 146 | 147 | }, object : NetworkFailCallback { 148 | 149 | override fun onResponseFail(code: Int, errorMessage: String?) { 150 | Log.e("dq","失败 onResponse = "+ errorMessage) 151 | } 152 | }) 153 | } 154 | }) 155 | } 156 | 157 | 158 | //去关注某用户 ---- 采用MVC的写法 159 | private fun executeFollowUserByMVC(position: Int, itemBean: UserBaseBean){ 160 | 161 | //逻辑:先本地设置data数据,然后再请求服务器,如果服务器访问失败,再把本地data设置回来。这样用户体验更好 162 | itemBean.follow = if (itemBean.follow == 1) 0 else 1 163 | adapter!!.notifyItemChanged(position) 164 | 165 | val params = HashMap() 166 | params["userid"] = "1" 167 | params["to_userid"] = itemBean.userid.toString() 168 | viewModel.requestFollowByMVC(params, object : 169 | NetworkResponseCallback> { 170 | //网络请求返回 171 | override fun onResponse(responseEntry: ResponseEntity?, errorMessage: String?) { 172 | 173 | if (responseEntry == null){ 174 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 175 | Toast.makeText(QApplication.instance, errorMessage , Toast.LENGTH_SHORT).show() 176 | 177 | itemBean.follow = if (itemBean.follow == 1) 0 else 1 178 | adapter!!.notifyItemChanged(position) 179 | 180 | return 181 | } else if (!responseEntry.isSuccess){ 182 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 183 | Toast.makeText(QApplication.instance, responseEntry.message , Toast.LENGTH_SHORT).show() 184 | 185 | itemBean.follow = if (itemBean.follow == 1) 0 else 1 186 | adapter!!.notifyItemChanged(position) 187 | 188 | return 189 | } 190 | } 191 | }) 192 | } 193 | 194 | //因为不是每次进入Activity都会调用这个接口,但是如果你每次都直接就初始化这个livedata,会额外多初始化一些系统livedata源码里的全局变量,浪费内存。所以这里做成懒加载 195 | private fun lazyCreateFollowLiveData(){ 196 | if (viewModel.followRequestStateMap == null){ 197 | viewModel.followRequestStateMap = MutableLiveData>() 198 | 199 | //MVVM 关注某用户,key是userid(int) value是:请求中、请求成功、请求失败。 200 | viewModel.followRequestStateMap!!.observe(this, Observer { loadStateMap: Pair -> 201 | 202 | val to_userid: Int = loadStateMap.first 203 | val idealState: Int = loadStateMap.second // 0 :最新的状态是"未关注" ;1:最新的状态是"已关注" 204 | 205 | Log.e("dq","HomeFragment 关注收到通知"+idealState) 206 | 207 | for ((index, itemBean) in viewModel.list!!.withIndex()) { 208 | if (itemBean.userid == to_userid){ 209 | itemBean.follow = idealState 210 | adapter!!.notifyItemChanged(index) 211 | break 212 | } 213 | } 214 | }) 215 | } 216 | } 217 | 218 | 219 | //删除Item ---- 采用MVC的写法 220 | private fun executeDeleteUserByMVC(position: Int, itemBean: UserBaseBean){ 221 | WaitDialog.show("MVC写法加载中..") 222 | val params = HashMap() 223 | params["userid"] = "1" 224 | params["to_userid"] = itemBean.userid.toString() 225 | 226 | 227 | viewModel.requestDeleteByMVC(params, object : 228 | NetworkResponseCallback> { 229 | //网络请求返回 230 | override fun onResponse(responseEntry: ResponseEntity?, errorMessage: String?) { 231 | //关闭弹框 232 | WaitDialog.dismiss() 233 | 234 | if (responseEntry == null){ 235 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 236 | Toast.makeText(QApplication.instance, errorMessage , Toast.LENGTH_SHORT).show() 237 | return 238 | } else if (!responseEntry.isSuccess){ 239 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 240 | Toast.makeText(QApplication.instance, responseEntry.message , Toast.LENGTH_SHORT).show() 241 | return 242 | } 243 | 244 | viewModel.list!!.remove(itemBean) 245 | adapter!!.notifyItemRemoved(position) 246 | 247 | } 248 | }) 249 | } 250 | 251 | //因为不是每次进入Activity都会调用这个接口,但是如果你每次都直接就初始化这个livedata,会额外多初始化一些系统livedata源码里的全局变量,浪费内存。所以这里做成懒加载 252 | private fun lazyCreateDeleteLiveData(){ 253 | if (viewModel.deleteRequestStatePair == null){ 254 | viewModel.deleteRequestStatePair = MutableLiveData>() 255 | 256 | //MVVM 关注某用户,key是userid(int) value是:请求中、请求成功、请求失败。 257 | viewModel.deleteRequestStatePair!!.observe(this, Observer { loadStateMap: Pair -> 258 | 259 | val to_userid: Int = loadStateMap.first 260 | val loadState: LoadState = loadStateMap.second 261 | 262 | when (loadState) { 263 | LoadState.Loading -> { 264 | WaitDialog.show("MVVM写法加载中..") 265 | } 266 | LoadState.SuccessNoMore -> { 267 | //删除成功 268 | WaitDialog.dismiss() 269 | for ((index, itemBean) in viewModel.list!!.withIndex()) { 270 | if (itemBean.userid == to_userid){ 271 | viewModel.list!!.remove(itemBean) 272 | adapter!!.notifyItemChanged(index) 273 | break 274 | } 275 | } 276 | } 277 | LoadState.CodeError, LoadState.NetworkFail -> { 278 | //删除失败 279 | WaitDialog.dismiss() 280 | Toast.makeText(QApplication.instance, viewModel.deleteRequestErrorMessage , Toast.LENGTH_SHORT).show() 281 | } 282 | } 283 | }) 284 | } 285 | } 286 | 287 | override fun adapter(): UserQuickAdapter = adapter 288 | 289 | override fun refreshLayout(): SmartRefreshLayout = binding.refreshLayout 290 | 291 | override fun onBindViewModel(): Class = UserRVViewModel::class.java 292 | 293 | // Implements - INavBar 294 | override fun getTootBarTitle(): String { 295 | return "Mvvm+BaseRecyclerViewAdapterHelper" 296 | } 297 | 298 | override fun onNavigationOnClick(view: View) { 299 | finish() 300 | } 301 | 302 | //toolbar用什么布局 303 | // override fun getBindToolbarLayout(): Int { 304 | // return R.layout.toolbar_draw 305 | // } 306 | 307 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/home/detail/UserRVViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.home.detail 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.viewModelScope 5 | import com.dq.qkotlin.bean.PageData 6 | import com.dq.qkotlin.bean.ResponseEntity 7 | import com.dq.qkotlin.bean.ResponsePageEntity 8 | import com.dq.qkotlin.bean.UserBaseBean 9 | import com.dq.qkotlin.net.* 10 | import com.dq.qkotlin.tool.NET_ERROR 11 | import com.dq.qkotlin.tool.checkResponseCodeAndThrow 12 | import com.dq.qkotlin.tool.requestCommon 13 | import com.dq.qkotlin.ui.base.BaseRVPagerViewModel 14 | import kotlinx.coroutines.CoroutineExceptionHandler 15 | import kotlinx.coroutines.launch 16 | 17 | class UserRVViewModel: BaseRVPagerViewModel() { 18 | 19 | //按MVVM设计原则,请求网络应该放到更下一层的"仓库类"里,但是我感觉如果你只做网络不做本地取数据,没必要 20 | //请求用户列表接口 21 | fun requestUserList(params : HashMap , loadmore : Boolean){ 22 | 23 | //调用"万能列表接口封装" 24 | super.requestList(params, loadmore){ 25 | 26 | //用kotlin高阶函数,传入本Activity的"请求用户列表接口的代码块" 就是这3行代码 27 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 28 | val response: ResponsePageEntity = apiService.userList(params) 29 | response 30 | } 31 | } 32 | 33 | /******* 关注 ********/ 34 | 35 | //请求关注接口(MVC的调用方式) 36 | fun requestFollowByMVC(params : HashMap, responseCallback: NetworkResponseCallback>){ 37 | 38 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 39 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 40 | //这里是主线程; 41 | responseCallback?.onResponse(null, NET_ERROR) 42 | } 43 | 44 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 45 | viewModelScope.launch(coroutineExceptionHandler) { 46 | 47 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 48 | //suspend 是这一步 49 | val response: ResponseEntity = apiService.userFollow(params) 50 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 51 | //这里是主线程 52 | responseCallback?.onResponse(response, null) 53 | } 54 | } 55 | 56 | //关注接口请求状态:key是userid(int) value是:关注状态,0未关注,1已经关注,这里用int以免以后出现 互相关注3 。如果是点赞,那用bool就行了 57 | //因为不是每次进入Activity都会调用这个接口,所以用lazy节省内存 58 | var followRequestStateMap: MutableLiveData>? = null 59 | 60 | //关注接口请求 - 错误message,要显示给用户看。你也可以统一用一个toastMessage,但是用统一message的弊端是无法针对某个接口的错误做特殊UI定制 61 | var followRequestErrorMessage: String? = null 62 | 63 | //请求关注接口(MVVM的调用方式) 64 | fun requestFollowByMVVM(to_userid: Int, currentValue: Int, idealValue: Int, params : HashMap) { 65 | 66 | if (followRequestStateMap == null){ 67 | followRequestStateMap = MutableLiveData>() 68 | } 69 | 70 | //请求服务器之前,我就发出广播 71 | val loadingMap = Pair(to_userid, idealValue) 72 | followRequestStateMap!!.value = loadingMap 73 | 74 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 75 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 76 | //这里是主线程; 77 | followRequestErrorMessage = NET_ERROR 78 | //给Activity回调网络请求失败 79 | val errorMap = Pair(to_userid, currentValue) 80 | followRequestStateMap!!.value = errorMap 81 | } 82 | 83 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 84 | viewModelScope.launch(coroutineExceptionHandler) { 85 | 86 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 87 | //suspend 是这一步 88 | val responseEntry: ResponseEntity = apiService.userFollow(params) 89 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 90 | //这里是主线程 91 | if (responseEntry == null){ 92 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 93 | followRequestErrorMessage = NET_ERROR 94 | //给Activity回调网络请求失败 95 | val errorMap = Pair(to_userid, currentValue) 96 | followRequestStateMap!!.value = errorMap 97 | 98 | return@launch 99 | 100 | } else if (!responseEntry.isSuccess){ 101 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 102 | followRequestErrorMessage = responseEntry.message 103 | //给Activity回调code失败 104 | val errorMap = Pair(to_userid , currentValue) 105 | followRequestStateMap!!.value = errorMap 106 | 107 | return@launch 108 | } 109 | 110 | //请求成功了,因为在请求前已经提前发出成功通知了,真的成功就不用发了 111 | } 112 | } 113 | 114 | 115 | /******* 删除 ********/ 116 | 117 | //请求删除接口(MVC的调用方式) 118 | fun requestDeleteByMVC(params : HashMap, responseCallback: NetworkResponseCallback>){ 119 | 120 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 121 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 122 | //这里是主线程; 123 | 124 | //如果404,代码会进入这里 125 | responseCallback?.onResponse(null, NET_ERROR) 126 | } 127 | 128 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 129 | viewModelScope.launch(coroutineExceptionHandler) { 130 | 131 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 132 | //suspend 是这一步 133 | val response: ResponseEntity = apiService.userDelete(params) 134 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 135 | //这里是主线程 136 | responseCallback?.onResponse(response, null) 137 | } 138 | } 139 | 140 | 141 | //删除接口请求状态:key是userid(int) value是:请求中、请求成功、请求失败。 142 | //因为不是每次进入Activity都会调用这个接口,所以用lazy节省内存 143 | var deleteRequestStatePair: MutableLiveData>? = null 144 | 145 | //删除接口请求 - 错误message,要显示给用户看。你也可以统一用一个toastMessage,但是用统一message的弊端是无法针对某个接口的错误做特殊UI定制 146 | var deleteRequestErrorMessage: String? = null 147 | 148 | //请求删除接口(MVVM的调用方式) 149 | fun requestDeleteByMVVM(to_userid: Int, params : HashMap){ 150 | 151 | if (deleteRequestStatePair == null){ 152 | deleteRequestStatePair = MutableLiveData>() 153 | } 154 | 155 | val loadingPair = Pair(to_userid, LoadState.Loading) 156 | deleteRequestStatePair!!.value = loadingPair 157 | 158 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 159 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 160 | //这里是主线程; 161 | deleteRequestErrorMessage = NET_ERROR 162 | //给Activity回调网络请求失败 163 | val errorPair = Pair(to_userid, LoadState.NetworkFail) 164 | deleteRequestStatePair!!.value = errorPair 165 | } 166 | 167 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 168 | viewModelScope.launch(coroutineExceptionHandler) { 169 | 170 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 171 | //suspend 是这一步 172 | val responseEntry: ResponseEntity = apiService.userDelete(params) 173 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 174 | //这里是主线程 175 | if (responseEntry == null){ 176 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 177 | deleteRequestErrorMessage = NET_ERROR 178 | //给Activity回调网络请求失败 179 | val errorPair = Pair(to_userid, LoadState.NetworkFail) 180 | deleteRequestStatePair!!.value = errorPair 181 | 182 | return@launch 183 | 184 | } else if (!responseEntry.isSuccess){ 185 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 186 | deleteRequestErrorMessage = responseEntry.message 187 | //给Activity回调code失败 188 | val errorPair = Pair(to_userid , LoadState.NetworkFail) 189 | deleteRequestStatePair!!.value = errorPair 190 | 191 | return@launch 192 | } 193 | 194 | //给Activity回调成功 195 | val successPair = Pair(to_userid , LoadState.SuccessNoMore) 196 | deleteRequestStatePair!!.value = successPair 197 | } 198 | } 199 | 200 | 201 | //用BaseViewModel里的requireCommon的Demo展示 202 | fun requestListByMVC(params : HashMap, successCallback: NetworkSuccessCallback>, failCallback: NetworkFailCallback){ 203 | 204 | requestCommon(viewModelScope, { 205 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 206 | //suspend 是这一步 207 | val response: ResponsePageEntity = apiService.userList(params) 208 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 209 | //这里是主线程 210 | 211 | //检查服务器返回的数据的code是否是success。如果是失败,代码会走到failCallback 212 | checkResponseCodeAndThrow(response) 213 | 214 | //给Activity回调成功 215 | successCallback?.onResponseSuccess(response.data) 216 | } 217 | 218 | ,failCallback 219 | ) 220 | } 221 | 222 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/mvc/FriendActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.mvc 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.View 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.databinding.DataBindingUtil 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import com.dq.qkotlin.R 11 | import com.dq.qkotlin.bean.ResponsePageEntity 12 | import com.dq.qkotlin.bean.UserBaseBean 13 | import com.dq.qkotlin.databinding.ActivityRecyclerviewBinding 14 | import com.dq.qkotlin.net.* 15 | import com.dq.qkotlin.tool.NET_ERROR 16 | import com.dq.qkotlin.tool.QApplication 17 | import com.dq.qkotlin.ui.base.INavBar 18 | import com.dq.qkotlin.view.SpacesItemDecoration 19 | import com.scwang.smart.refresh.layout.SmartRefreshLayout 20 | import com.scwang.smart.refresh.layout.api.RefreshLayout 21 | import kotlinx.coroutines.* 22 | 23 | /** 24 | * 全部基于最传统的MVC,只涉及refresh这一个第3方 25 | */ 26 | class FriendActivity : AppCompatActivity(), INavBar { 27 | 28 | private val title: String by lazy { intent.getStringExtra("title")!! } 29 | 30 | //View 31 | private lateinit var mAdapter: FriendRecyclerAdapter 32 | 33 | private lateinit var binding: ActivityRecyclerviewBinding 34 | 35 | //Data 36 | open val list: MutableList = arrayListOf() 37 | //下次请求需要带上的页码参数 38 | private var page = 1 39 | 40 | //协程 41 | private val scope = MainScope() 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | binding = DataBindingUtil.setContentView(this, R.layout.activity_recyclerview) 46 | 47 | //设置状态栏 48 | initStatusBar(this) 49 | //设置toolbar 50 | initToolbarView(findViewById(R.id.view_stub_toolbar)) 51 | 52 | initView() 53 | 54 | initListener() 55 | } 56 | 57 | private fun initView() { 58 | 59 | //设置RecyclerView 60 | val layoutManager = LinearLayoutManager(this) 61 | layoutManager.orientation = LinearLayoutManager.VERTICAL 62 | binding.recyclerView.layoutManager = layoutManager 63 | 64 | // 选择2:设置颜色、高度、间距等 65 | val itemDecoration = SpacesItemDecoration(this, SpacesItemDecoration.VERTICAL) 66 | .setParam(R.color.divider_color, 1, 30f, 0f) 67 | binding.recyclerView.addItemDecoration(itemDecoration) 68 | 69 | //RecyclerView创建适配器,并且设置 70 | mAdapter = FriendRecyclerAdapter(this, list) 71 | binding.recyclerView.adapter = mAdapter 72 | } 73 | 74 | private fun initListener() { 75 | //下拉刷新底部加载控件 76 | val refreshLayout: SmartRefreshLayout = findViewById(R.id.refresh_layout); 77 | 78 | refreshLayout.setOnRefreshListener { 79 | //触发了下拉刷新 80 | val params = HashMap() 81 | params["keyword"] = "小" 82 | params["page"] = "1" 83 | 84 | requestFriendList(params, object : 85 | NetworkResponseCallback> { 86 | //网络请求返回 87 | override fun onResponse(responseEntry: ResponsePageEntity?, errorMessage: String?) { 88 | //处理 89 | handleListResponse(it, true , responseEntry, errorMessage) 90 | } 91 | }) 92 | } 93 | 94 | refreshLayout.setOnLoadMoreListener { 95 | //触发了底部加载更多 96 | val params = HashMap() 97 | params["keyword"] = "小" 98 | params["page"] = page.toString() 99 | 100 | requestFriendList(params, object : 101 | NetworkResponseCallback> { 102 | //网络请求返回 103 | override fun onResponse(responseEntry: ResponsePageEntity?, errorMessage: String?) { 104 | handleListResponse(it, false , responseEntry, errorMessage) 105 | } 106 | }) 107 | } 108 | 109 | //先设置为没更多了 110 | // refreshLayout.finishLoadMoreWithNoMoreData() 111 | 112 | //立即开始刷新 113 | refreshLayout.autoRefresh(100,200,1f,false);//延迟100毫秒后自动刷新 114 | } 115 | 116 | //请求列表,这个方法进行了初步的解耦 117 | private fun requestFriendList(params : HashMap, responseCallback: NetworkResponseCallback>){ 118 | 119 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 120 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 121 | //这里是主线程; 122 | Log.e("dq",throwable.toString()) 123 | responseCallback?.onResponse(null, NET_ERROR) 124 | } 125 | 126 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 127 | scope.launch(coroutineExceptionHandler) { 128 | 129 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 130 | //suspend 是这一步 131 | val response: ResponsePageEntity = apiService.userList(params) 132 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 133 | //这里是主线程 134 | responseCallback?.onResponse(response, null) 135 | } 136 | } 137 | 138 | private fun handleListResponse(refreshLayout: RefreshLayout, isRefresh: Boolean = false, responseEntry: ResponsePageEntity?, errorMessage: String?){ 139 | 140 | if (isRefresh){ 141 | //取消下拉刷新状态 142 | refreshLayout.finishRefresh(0) 143 | } 144 | 145 | if (responseEntry == null){ 146 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 147 | Toast.makeText(QApplication.instance, errorMessage , Toast.LENGTH_SHORT).show() 148 | return 149 | } else if (!responseEntry.isSuccess){ 150 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 151 | Toast.makeText(QApplication.instance, responseEntry.message , Toast.LENGTH_SHORT).show() 152 | return 153 | } 154 | 155 | //检查底部是否还有更多数据 156 | if (responseEntry.data!!.hasMore()) { 157 | //还有更多数据 158 | refreshLayout.finishLoadMore() 159 | } else { 160 | //没有更多数据 161 | refreshLayout.finishLoadMoreWithNoMoreData() 162 | } 163 | 164 | if (isRefresh) { 165 | //是下拉刷新的返回 166 | page = 2 //页面强制设置为下次请求第2页 167 | responseEntry.data?.let { 168 | //处理列表数据 169 | list.clear() 170 | list?.addAll(it.items!!) 171 | 172 | //检查显示\隐藏空布局。不同RV库对EmptyView有不同的实现,这是基于原生Adapter的方式。这一句需要写在notifyDataSetChanged前 173 | mAdapter.showEmptyViewEnable = true 174 | mAdapter.notifyDataSetChanged() 175 | } 176 | } else { 177 | //是底部加载更多 178 | page++ 179 | 180 | responseEntry.data?.let { 181 | list?.addAll(it.items!!) 182 | //这里也可以改成:notifyItemRangeInserted 183 | mAdapter.notifyDataSetChanged() 184 | } 185 | } 186 | } 187 | 188 | //FM.onPause -> AC.onPause -> FM.onStop -> AC.onStop -> FM.onDestroyView -> FM.onDestroy -> FM.onDetach -> AC.onDestroy 189 | override fun onDestroy() { 190 | super.onDestroy() 191 | scope.cancel(null) 192 | } 193 | 194 | // Implements - INavBar 195 | override fun getTootBarTitle(): String { 196 | return "MVC+普通RecyclerView" 197 | } 198 | 199 | override fun onNavigationOnClick(view: View) { 200 | finish() 201 | } 202 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/mvc/FriendRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.mvc 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.bumptech.glide.Glide 11 | import com.bumptech.glide.load.resource.bitmap.RoundedCorners 12 | import com.bumptech.glide.request.RequestOptions 13 | import com.dq.qkotlin.R 14 | import com.dq.qkotlin.bean.UserBaseBean 15 | import com.dq.qkotlin.tool.dp2px 16 | 17 | 18 | class FriendRecyclerAdapter(val context: Context, val list: List) : RecyclerView.Adapter(){ 19 | 20 | private val mInflater: LayoutInflater = LayoutInflater.from(context) 21 | 22 | //是否可以显示emptyView。因为刚进来第一次加载中的时候,虽然list是empty,但我不希望显示emptyView 23 | var showEmptyViewEnable = false 24 | 25 | //viewType分别为item以及空view 26 | private val VIEW_TYPE_ITEM: Int = 0 27 | private val VIEW_TYPE_EMPTY: Int = 1 28 | 29 | //item上的控件的点击事件 30 | private var onItemClickListener: OnItemClickListener? = null 31 | 32 | //itemview数量 33 | override fun getItemCount(): Int { 34 | //这里也需要添加判断,如果list.size()为0的话,只引入一个布局,就是emptyView,此时 这个recyclerView的itemCount为1 35 | if (showEmptyViewEnable && list.isNullOrEmpty()) { 36 | return 1; 37 | } 38 | //如果不为0,按正常的流程跑 39 | return list.size 40 | } 41 | 42 | override fun getItemViewType(position: Int): Int { 43 | //在这里进行判断,如果我们的集合的长度为0时,我们就使用emptyView的布局 44 | return if (showEmptyViewEnable && list.isNullOrEmpty()) { 45 | VIEW_TYPE_EMPTY 46 | } else VIEW_TYPE_ITEM 47 | //如果有数据,则使用ITEM的布局 48 | } 49 | 50 | //创建ViewHolder并绑定上itemview 51 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 52 | //在这里根据不同的viewType进行引入不同的布局 53 | if (viewType == VIEW_TYPE_EMPTY) { 54 | //空布局 55 | val emptyView: View = mInflater.inflate(R.layout.listview_empty, parent, false) 56 | return object : RecyclerView.ViewHolder(emptyView) {} 57 | } else { 58 | //正常item 59 | val view: View = mInflater.inflate(R.layout.listitem_follower, parent, false) 60 | val viewHolder = TopicViewHolder(view) 61 | viewHolder.itemView.setOnClickListener { 62 | onItemClickListener?.onItemClick(viewHolder.itemView.tag as Int); 63 | } 64 | return viewHolder 65 | } 66 | } 67 | 68 | //ViewHolder的view控件设置数据 69 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 70 | if (holder is TopicViewHolder) { 71 | holder.name_tv.text = list.get(position).name 72 | holder.itemView.tag = position 73 | 74 | Glide.with(holder.avatar_iv.context)//.context是MainActivity 75 | .load(list.get(position).avatar) 76 | .placeholder(R.drawable.user_photo) 77 | .apply(RequestOptions.bitmapTransform(RoundedCorners(dp2px(holder.avatar_iv.context ,4)))) 78 | .into(holder.avatar_iv) 79 | } 80 | } 81 | 82 | 83 | //kotlin 内部类默认是static ,前面加上inner为非静态 84 | //自定义的RecyclerView.ViewHolder,构造函数需要传入View参数。相当于java的构造函数第一句的super(view); 85 | class TopicViewHolder(view: View) : RecyclerView.ViewHolder(view) { 86 | val name_tv: TextView = view.findViewById(R.id.name_tv) 87 | val avatar_iv: ImageView = view.findViewById(R.id.avatar_iv) 88 | 89 | // init { 90 | // name_tv.setTextColor(view.resources.getColor(R.color.sky_color)) 91 | // } 92 | } 93 | 94 | fun setOnItemClickListener(onItemClickListener: OnItemClickListener) { 95 | this.onItemClickListener = onItemClickListener 96 | } 97 | 98 | interface OnItemClickListener { 99 | fun onItemClick(position: Int) 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/mvc/MvcRVFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.mvc 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.Toast 10 | import androidx.fragment.app.Fragment 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.dq.qkotlin.R 14 | import com.dq.qkotlin.bean.UserBaseBean 15 | import com.dq.qkotlin.bean.ResponsePageEntity 16 | import com.dq.qkotlin.net.NetworkResponseCallback 17 | import com.dq.qkotlin.net.RetrofitInstance 18 | import com.dq.qkotlin.net.UserApiService 19 | import com.dq.qkotlin.tool.NET_ERROR 20 | import com.dq.qkotlin.tool.QApplication 21 | import com.dq.qkotlin.ui.base.INavBar 22 | import com.dq.qkotlin.view.SpacesItemDecoration 23 | import com.scwang.smart.refresh.layout.SmartRefreshLayout 24 | import com.scwang.smart.refresh.layout.api.RefreshLayout 25 | import kotlinx.coroutines.CoroutineExceptionHandler 26 | import kotlinx.coroutines.MainScope 27 | import kotlinx.coroutines.cancel 28 | import kotlinx.coroutines.launch 29 | 30 | class MvcRVFragment : Fragment(), INavBar { 31 | 32 | companion object { 33 | val TAG = "MvcRVFragment" 34 | } 35 | 36 | //View 37 | private lateinit var mAdapter: FriendRecyclerAdapter 38 | 39 | private lateinit var recyclerView: RecyclerView 40 | 41 | //Data 42 | open val list: MutableList = arrayListOf() 43 | //下次请求需要带上的页码参数 44 | private var page = 1 45 | 46 | //协程 47 | private val scope = MainScope() 48 | 49 | //AC.onCreate -> FM.onAttach -> FM.onCreate -> FM.onCreateView -> FM.onActivityCreated 50 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 51 | return inflater.inflate(R.layout.activity_recyclerview, container, false) 52 | } 53 | 54 | //... -> FM.onCreateView -> FM.onActivityCreated -> FM.onStart -> AC.onStart -> AC.onResume -> FM.onResume 55 | override fun onActivityCreated(savedInstanceState: Bundle?) { 56 | super.onActivityCreated(savedInstanceState) 57 | 58 | initView() 59 | initListener() 60 | } 61 | 62 | private fun initView() { 63 | 64 | initStatusBar(this) 65 | initToolbarView(requireView().findViewById(R.id.view_stub_toolbar)) 66 | 67 | //设置RecyclerView 68 | recyclerView = requireView().findViewById(R.id.recycler_view) 69 | val layoutManager = LinearLayoutManager(requireContext()) 70 | layoutManager.orientation = LinearLayoutManager.VERTICAL 71 | recyclerView.layoutManager = layoutManager 72 | 73 | // 设置颜色、高度、间距等 74 | val itemDecoration = SpacesItemDecoration(requireContext(), SpacesItemDecoration.VERTICAL) 75 | .setParam(R.color.divider_color, 1, 30f, 0f) 76 | recyclerView.addItemDecoration(itemDecoration) 77 | 78 | //RecyclerView创建适配器,并且设置 79 | mAdapter = FriendRecyclerAdapter(requireContext(), list) 80 | recyclerView.adapter = mAdapter 81 | 82 | mAdapter.setOnItemClickListener(object : FriendRecyclerAdapter.OnItemClickListener { 83 | override fun onItemClick(position: Int) { 84 | //Item点击事件 85 | val itemBean: UserBaseBean = list.get(position) 86 | val i = Intent(context, FriendActivity::class.java) 87 | i.putExtra("title",itemBean.name); 88 | requireActivity().startActivity(i) 89 | } 90 | }) 91 | } 92 | 93 | private fun initListener() { 94 | //下拉刷新底部加载控件 95 | val refreshLayout: SmartRefreshLayout = requireView().findViewById(R.id.refresh_layout); 96 | 97 | refreshLayout.setOnRefreshListener { 98 | //触发了下拉刷新 99 | val params = HashMap() 100 | params["keyword"] = "大" 101 | params["page"] = "1" 102 | 103 | requestFriendList(params, object : 104 | NetworkResponseCallback> { 105 | //网络请求返回 106 | override fun onResponse(responseEntry: ResponsePageEntity?, errorMessage: String?) { 107 | //处理 108 | handleListResponse(it, true , responseEntry, errorMessage) 109 | } 110 | }) 111 | } 112 | 113 | refreshLayout.setOnLoadMoreListener { 114 | //触发了底部加载更多 115 | val params = HashMap() 116 | params["keyword"] = "大" 117 | params["page"] = page.toString() 118 | 119 | requestFriendList(params, object : 120 | NetworkResponseCallback> { 121 | //网络请求返回 122 | override fun onResponse(responseEntry: ResponsePageEntity?, errorMessage: String?) { 123 | handleListResponse(it, false , responseEntry, errorMessage) 124 | } 125 | }) 126 | } 127 | 128 | //立即开始刷新 129 | refreshLayout.autoRefresh(100,200,1f,false);//延迟100毫秒后自动刷新 130 | } 131 | 132 | //请求列表,这个方法进行了初步的解耦 133 | private fun requestFriendList(params : HashMap, responseCallback: NetworkResponseCallback>){ 134 | 135 | val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 136 | //访问网络异常的回调用, 这种方法可以省去try catch, 但不适用于async启动的协程 137 | //这里是主线程; 138 | Log.e("dq",throwable.toString()) 139 | responseCallback?.onResponse(null, NET_ERROR) 140 | } 141 | 142 | /*MainScope()是一个绑定到当前viewModel的作用域 当ViewModel被清除时会自动取消该作用域,所以不用担心内存泄漏为问题*/ 143 | scope.launch(coroutineExceptionHandler) { 144 | 145 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 146 | //suspend 是这一步 147 | val response: ResponsePageEntity = apiService.userList(params) 148 | //如果网络访问异常,代码会直接进入CoroutineExceptionHandler,不会走这里了 149 | //这里是主线程 150 | responseCallback?.onResponse(response, null) 151 | } 152 | } 153 | 154 | private fun handleListResponse(refreshLayout: RefreshLayout, isRefresh: Boolean = false, responseEntry: ResponsePageEntity?, errorMessage: String?){ 155 | 156 | if (isRefresh){ 157 | //取消下拉刷新状态 158 | refreshLayout.finishRefresh(0) 159 | } 160 | 161 | if (responseEntry == null){ 162 | //进入这里,说明是服务器崩溃,errorMessage是我们本地自定义的 163 | Toast.makeText(QApplication.instance, errorMessage , Toast.LENGTH_SHORT).show() 164 | return 165 | } else if (!responseEntry.isSuccess){ 166 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 167 | Toast.makeText(QApplication.instance, responseEntry.message , Toast.LENGTH_SHORT).show() 168 | return 169 | } 170 | 171 | //检查底部是否还有更多数据 172 | if (responseEntry.data!!.hasMore()) { 173 | //还有更多数据 174 | refreshLayout.finishLoadMore() 175 | } else { 176 | //没有更多数据 177 | refreshLayout.finishLoadMoreWithNoMoreData() 178 | } 179 | 180 | if (isRefresh) { 181 | //是下拉刷新的返回 182 | page = 2 //页面强制设置为下次请求第2页 183 | responseEntry.data?.let { 184 | //处理列表数据 185 | list.clear() 186 | list?.addAll(it.items!!) 187 | 188 | //检查显示\隐藏空布局。不同RV库对EmptyView有不同的实现,这是基于原生Adapter的方式。这一句需要写在notifyDataSetChanged前 189 | mAdapter.showEmptyViewEnable = true 190 | 191 | mAdapter.notifyDataSetChanged() 192 | } 193 | } else { 194 | //是底部加载更多 195 | page++ 196 | 197 | responseEntry.data?.let { 198 | list?.addAll(it.items!!) 199 | //这里也可以改成:notifyItemRangeInserted 200 | mAdapter.notifyDataSetChanged() 201 | } 202 | } 203 | } 204 | 205 | //FM.onPause -> AC.onPause -> FM.onStop -> AC.onStop -> FM.onDestroyView -> FM.onDestroy -> FM.onDetach -> AC.onDestroy 206 | override fun onDestroy() { 207 | super.onDestroy() 208 | scope.cancel(null) 209 | } 210 | 211 | // Implements - INavBar 212 | override fun getTootBarTitle(): String { 213 | return "MVC+普通Recycle" 214 | } 215 | 216 | override fun getToolBarLeftIcon(): Int { 217 | return 0 218 | } 219 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/notifications/ProfileFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.notifications 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import android.widget.Toast 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.Observer 11 | import androidx.lifecycle.ViewModelProvider 12 | import com.dq.qkotlin.R 13 | import com.dq.qkotlin.bean.UserBaseBean 14 | import com.dq.qkotlin.tool.QApplication 15 | import com.dq.qkotlin.ui.base.INavBar 16 | import com.dq.qkotlin.ui.home.HomeViewModel 17 | import com.kongzue.dialogx.dialogs.WaitDialog 18 | 19 | class ProfileFragment : Fragment(), INavBar { 20 | 21 | companion object { 22 | val TAG = "ProfileFragment" 23 | } 24 | 25 | private lateinit var titleTextView: TextView 26 | 27 | private lateinit var profileViewModel: ProfileViewModel 28 | 29 | //AC.onCreate -> FM.onAttach -> FM.onCreate -> FM.onCreateView -> FM.onActivityCreated 30 | override fun onCreateView( 31 | inflater: LayoutInflater, 32 | container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View? { 35 | return inflater.inflate(R.layout.fragment_profile, container, false) 36 | } 37 | 38 | //... -> FM.onCreateView -> FM.onActivityCreated -> FM.onStart -> AC.onStart -> AC.onResume -> FM.onResume 39 | override fun onActivityCreated(savedInstanceState: Bundle?) { 40 | super.onActivityCreated(savedInstanceState) 41 | profileViewModel = ViewModelProvider(this).get(ProfileViewModel::class.java) 42 | initView() 43 | initObservable() 44 | } 45 | 46 | private fun initView() { 47 | initStatusBar(this) 48 | initToolbarView(requireView().findViewById(R.id.view_stub_toolbar)) 49 | 50 | titleTextView = requireView().findViewById(R.id.home_tv) 51 | requireView().findViewById(R.id.detail_button).setOnClickListener { v -> 52 | val params = HashMap() 53 | params["to_userid"] = "1" 54 | profileViewModel.requestUserProfile(params) 55 | } 56 | } 57 | 58 | private fun initObservable() { 59 | //Dialog 60 | profileViewModel.loadingProfileLiveData 61 | .observe(viewLifecycleOwner, Observer { loading: Boolean -> 62 | if (loading) { 63 | WaitDialog.show("MVVM写法加载中..") 64 | } else { 65 | //关闭弹框 66 | WaitDialog.dismiss() 67 | } 68 | }) 69 | 70 | //Toast 71 | profileViewModel.toastLiveData 72 | .observe(viewLifecycleOwner, Observer { toastMessage: String -> 73 | Toast.makeText(QApplication.instance, toastMessage, Toast.LENGTH_SHORT).show() 74 | }) 75 | 76 | //数据 77 | profileViewModel.profileLiveData 78 | .observe(viewLifecycleOwner, Observer { userBaseBean: UserBaseBean -> 79 | titleTextView.text = userBaseBean.name 80 | }) 81 | } 82 | 83 | override fun getTootBarTitle(): String { 84 | return "MVVM+请求详情Model" 85 | } 86 | 87 | override fun getToolBarLeftIcon(): Int { 88 | return 0 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/ui/notifications/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.ui.notifications 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.dq.qkotlin.bean.ResponseEntity 7 | import com.dq.qkotlin.bean.UserBaseBean 8 | import com.dq.qkotlin.net.* 9 | import com.dq.qkotlin.tool.requestCommon 10 | 11 | class ProfileViewModel : ViewModel() { 12 | 13 | //监听整个Model 14 | private val _profileLiveData = MutableLiveData() 15 | val profileLiveData = _profileLiveData 16 | 17 | //监听整个Model请求状态 18 | private val _loadingProfileLiveData = MutableLiveData() 19 | val loadingProfileLiveData = _loadingProfileLiveData 20 | 21 | //监听整个Model请求错误message 22 | private val _toastLiveData = MutableLiveData() 23 | val toastLiveData = _toastLiveData 24 | 25 | fun requestUserProfile(params: HashMap) { 26 | 27 | _loadingProfileLiveData.value = true 28 | 29 | requestCommon(viewModelScope, { 30 | 31 | var apiService : UserApiService = RetrofitInstance.instance.create(UserApiService::class.java) 32 | //suspend 是这一步 33 | val responseEntry: ResponseEntity = apiService.userProfile(params) 34 | 35 | //给Activity回调加载完成 36 | _loadingProfileLiveData.value = false 37 | 38 | if (responseEntry.isSuccess){ 39 | //给Activity回调成功 40 | _profileLiveData.value = responseEntry.data 41 | } else { 42 | //进入这里,说明是服务器验证参数错误,message是服务器返回的 43 | _toastLiveData.value = responseEntry.message 44 | } 45 | 46 | }, object : NetworkFailCallback { 47 | 48 | //说明是404、500 49 | override fun onResponseFail(code: Int, errorMessage: String?) { 50 | //这里是主线程; 51 | _loadingProfileLiveData.value = false 52 | _toastLiveData.value = errorMessage 53 | } 54 | }) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/view/FooterListView.kt: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ListView 8 | import android.widget.TextView 9 | import com.dq.qkotlin.R 10 | 11 | /** 12 | * 使用PagedListView 要保证PAGE_SIZE个item 一定可以充满屏幕 13 | */ 14 | class FooterListView : ListView { 15 | private var mEmptyView // 内容为空显示的emptyview。当无headerview时候使用,占满全屏 16 | : View? = null 17 | private var mEmptyFooterView // 有headerview时候使用,只是在底部 18 | : View? = null 19 | 20 | constructor(context: Context) : super(context) { 21 | initView(context) 22 | } 23 | 24 | constructor( 25 | context: Context, 26 | attrs: AttributeSet? 27 | ) : super(context, attrs) { 28 | initView(context) 29 | } 30 | 31 | constructor( 32 | context: Context, 33 | attrs: AttributeSet?, 34 | defStyle: Int 35 | ) : super(context, attrs, defStyle) { 36 | initView(context) 37 | } 38 | 39 | private fun initView(context: Context) { 40 | setFooterDividersEnabled(false) 41 | } 42 | 43 | fun resetFooterMessage(message: String?) { 44 | resetFooterMessageAndIcon(message, 0, 0) 45 | } 46 | 47 | @JvmOverloads 48 | fun resetFooterMessageAndIcon( 49 | message: String?, 50 | drawableIcon: Int, 51 | paddingBottomPx: Int = 0 52 | ) { 53 | if (mEmptyFooterView == null) { 54 | return 55 | } 56 | val tv = 57 | mEmptyFooterView!!.findViewById(R.id.empty_tv) as TextView 58 | tv.text = message ?: "" 59 | if (drawableIcon != 0) { 60 | val drawable = 61 | resources.getDrawable(drawableIcon) 62 | drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) 63 | tv.setCompoundDrawables(null, drawable, null, null) 64 | } 65 | if (paddingBottomPx != 0) { 66 | tv.setPadding(0, tv.paddingTop, 0, paddingBottomPx) 67 | } 68 | } 69 | 70 | fun removeEmptyFooter() { 71 | if (mEmptyFooterView != null) { 72 | removeFooterView(mEmptyFooterView) 73 | mEmptyFooterView = null 74 | } 75 | } 76 | 77 | fun setFooterViewBackgroundColor(color: Int) { 78 | mEmptyFooterView!!.setBackgroundColor(color) 79 | } 80 | 81 | override fun setEmptyView(newEmptyView: View) { 82 | // If we already have an Empty View, remove it 83 | if (mEmptyView != null) { 84 | if (mEmptyView === newEmptyView) { 85 | return 86 | } 87 | val currentEmptyViewParent = mEmptyView!!.parent 88 | if (null != currentEmptyViewParent 89 | && currentEmptyViewParent is ViewGroup 90 | ) { 91 | currentEmptyViewParent.removeView(mEmptyView) 92 | } 93 | } 94 | newEmptyView.let{ 95 | newEmptyView.isClickable = true 96 | val newEmptyViewParent = newEmptyView.parent 97 | if (null != newEmptyViewParent 98 | && newEmptyViewParent is ViewGroup 99 | ) { 100 | newEmptyViewParent.removeView(newEmptyView) 101 | } 102 | (parent as ViewGroup).addView( 103 | newEmptyView, 104 | ViewGroup.LayoutParams.MATCH_PARENT, 105 | ViewGroup.LayoutParams.MATCH_PARENT 106 | ) 107 | super.setEmptyView(newEmptyView) 108 | } 109 | mEmptyView = newEmptyView 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/view/ShakeAnimation.java: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.view; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.animation.ValueAnimator; 5 | import android.view.View; 6 | import android.view.animation.LinearInterpolator; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | /** 11 | * Created by dongjin on 2017/11/23. 12 | */ 13 | public class ShakeAnimation { 14 | 15 | public ShakeAnimation with(@NonNull View view){ 16 | this.view = view; 17 | return this; 18 | } 19 | 20 | public static ShakeAnimation create(){ 21 | return new ShakeAnimation(); 22 | } 23 | 24 | public void start(){ 25 | 26 | if (view == null) throw new NullPointerException("View cant be null!"); 27 | 28 | final ObjectAnimator imageViewObjectAnimator = ObjectAnimator.ofFloat(view, "translationX", 0, 25, -25, 25, -25,15, -15, 6, -6, 0); 29 | 30 | imageViewObjectAnimator.setDuration(duration); 31 | imageViewObjectAnimator.setRepeatMode(repeatMode); 32 | imageViewObjectAnimator.setRepeatCount(0); 33 | imageViewObjectAnimator.setInterpolator(new LinearInterpolator()); 34 | imageViewObjectAnimator.start(); 35 | } 36 | 37 | private int duration = 700; 38 | private int repeatMode = ValueAnimator.RESTART; 39 | private int repeatCount = RESTART; 40 | private View view; 41 | 42 | public static final int RESTART = 1; 43 | public static final int REVERSE = 2; 44 | public static final int INFINITE = -1; 45 | 46 | public ShakeAnimation setDuration(int duration){ 47 | this.duration = duration; 48 | return this; 49 | } 50 | 51 | public ShakeAnimation setRepeatMode(int repeatMode){ 52 | this.repeatMode = repeatMode; 53 | return this; 54 | } 55 | 56 | public ShakeAnimation setRepeatCount(int repeatCount){ 57 | this.repeatCount = repeatCount; 58 | return this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/dq/qkotlin/view/SpacesItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.dq.qkotlin.view; 2 | 3 | /* 4 | * Copyright 2019. Bin Jing (https://github.com/youlookwhat) 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Canvas; 22 | import android.graphics.Paint; 23 | import android.graphics.Rect; 24 | import android.graphics.drawable.Drawable; 25 | import android.view.View; 26 | import android.widget.LinearLayout; 27 | 28 | import androidx.annotation.DrawableRes; 29 | import androidx.core.content.ContextCompat; 30 | import androidx.recyclerview.widget.RecyclerView; 31 | 32 | 33 | /** 34 | * 给 LinearLayoutManager 增加分割线,可设置去除首尾分割线个数 35 | * 36 | * @author jingbin 37 | * https://github.com/youlookwhat/ByRecyclerView 38 | */ 39 | public class SpacesItemDecoration extends RecyclerView.ItemDecoration { 40 | 41 | public static final int HORIZONTAL = LinearLayout.HORIZONTAL; 42 | public static final int VERTICAL = LinearLayout.VERTICAL; 43 | private static final String TAG = "itemDivider"; 44 | private Context mContext; 45 | private Drawable mDivider; 46 | private Rect mBounds = new Rect(); 47 | /** 48 | * 在AppTheme里配置 android:listDivider 49 | */ 50 | private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; 51 | /** 52 | * 头部 不显示分割线的item个数 这里应该包含刷新头, 53 | * 比如有一个headerView和有下拉刷新,则这里传 2 54 | */ 55 | private int mHeaderNoShowSize = 0; 56 | /** 57 | * 尾部 不显示分割线的item个数 默认不显示最后一个item的分割线 58 | */ 59 | private int mFooterNoShowSize = 1; 60 | /** 61 | * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. 62 | */ 63 | private int mOrientation; 64 | private Paint mPaint; 65 | /** 66 | * 如果是横向 - 宽度 67 | * 如果是纵向 - 高度 68 | */ 69 | private int mDividerSpacing; 70 | /** 71 | * 如果是横向 - 左边距 72 | * 如果是纵向 - 上边距 73 | */ 74 | private int mLeftTopPadding; 75 | /** 76 | * 如果是横向 - 右边距 77 | * 如果是纵向 - 下边距 78 | */ 79 | private int mRightBottomPadding; 80 | 81 | public SpacesItemDecoration(Context context) { 82 | this(context, VERTICAL, 0, 1); 83 | } 84 | 85 | public SpacesItemDecoration(Context context, int orientation) { 86 | this(context, orientation, 0, 1); 87 | } 88 | 89 | public SpacesItemDecoration(Context context, int orientation, int headerNoShowSize) { 90 | this(context, orientation, headerNoShowSize, 1); 91 | } 92 | 93 | /** 94 | * Creates a divider {@link RecyclerView.ItemDecoration} 95 | * 96 | * @param context Current context, it will be used to access resources. 97 | * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. 98 | * @param headerNoShowSize headerViewSize + RefreshViewSize 99 | * @param footerNoShowSize footerViewSize 100 | */ 101 | public SpacesItemDecoration(Context context, int orientation, int headerNoShowSize, int footerNoShowSize) { 102 | mContext = context; 103 | mHeaderNoShowSize = headerNoShowSize; 104 | mFooterNoShowSize = footerNoShowSize; 105 | setOrientation(orientation); 106 | final TypedArray a = context.obtainStyledAttributes(ATTRS); 107 | mDivider = a.getDrawable(0); 108 | a.recycle(); 109 | } 110 | 111 | /** 112 | * Sets the orientation for this divider. This should be called if 113 | * {@link RecyclerView.LayoutManager} changes orientation. 114 | * 115 | * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} 116 | */ 117 | public SpacesItemDecoration setOrientation(int orientation) { 118 | if (orientation != HORIZONTAL && orientation != VERTICAL) { 119 | throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL"); 120 | } 121 | mOrientation = orientation; 122 | return this; 123 | } 124 | 125 | /** 126 | * Sets the {@link Drawable} for this divider. 127 | * 128 | * @param drawable Drawable that should be used as a divider. 129 | */ 130 | public SpacesItemDecoration setDrawable(Drawable drawable) { 131 | if (drawable == null) { 132 | throw new IllegalArgumentException("drawable cannot be null."); 133 | } 134 | mDivider = drawable; 135 | return this; 136 | } 137 | 138 | public SpacesItemDecoration setDrawable(@DrawableRes int id) { 139 | setDrawable(ContextCompat.getDrawable(mContext, id)); 140 | return this; 141 | } 142 | 143 | @Override 144 | public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) { 145 | if (parent.getLayoutManager() == null || (mDivider == null && mPaint == null)) { 146 | return; 147 | } 148 | if (mOrientation == VERTICAL) { 149 | drawVertical(canvas, parent, state); 150 | } else { 151 | drawHorizontal(canvas, parent, state); 152 | } 153 | } 154 | 155 | private void drawVertical(Canvas canvas, RecyclerView parent, RecyclerView.State state) { 156 | canvas.save(); 157 | final int left; 158 | final int right; 159 | //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. 160 | if (parent.getClipToPadding()) { 161 | left = parent.getPaddingLeft(); 162 | right = parent.getWidth() - parent.getPaddingRight(); 163 | canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom()); 164 | } else { 165 | left = 0; 166 | right = parent.getWidth(); 167 | } 168 | 169 | final int childCount = parent.getChildCount(); 170 | final int lastPosition = state.getItemCount() - 1; 171 | for (int i = 0; i < childCount; i++) { 172 | final View child = parent.getChildAt(i); 173 | final int childRealPosition = parent.getChildAdapterPosition(child); 174 | 175 | // 过滤到头部不显示的分割线 176 | if (childRealPosition < mHeaderNoShowSize) { 177 | continue; 178 | } 179 | // 过滤到尾部不显示的分割线 180 | if (childRealPosition <= lastPosition - mFooterNoShowSize) { 181 | if (mDivider != null) { 182 | parent.getDecoratedBoundsWithMargins(child, mBounds); 183 | final int bottom = mBounds.bottom + Math.round(child.getTranslationY()); 184 | final int top = bottom - mDivider.getIntrinsicHeight(); 185 | mDivider.setBounds(left, top, right, bottom); 186 | mDivider.draw(canvas); 187 | } 188 | 189 | if (mPaint != null) { 190 | RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); 191 | int left1 = left + mLeftTopPadding; 192 | int right1 = right - mRightBottomPadding; 193 | int top1 = child.getBottom() + params.bottomMargin; 194 | int bottom1 = top1 + mDividerSpacing; 195 | canvas.drawRect(left1, top1, right1, bottom1, mPaint); 196 | } 197 | } 198 | } 199 | canvas.restore(); 200 | } 201 | 202 | private void drawHorizontal(Canvas canvas, RecyclerView parent, RecyclerView.State state) { 203 | canvas.save(); 204 | final int top; 205 | final int bottom; 206 | //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. 207 | if (parent.getClipToPadding()) { 208 | top = parent.getPaddingTop(); 209 | bottom = parent.getHeight() - parent.getPaddingBottom(); 210 | canvas.clipRect(parent.getPaddingLeft(), top, 211 | parent.getWidth() - parent.getPaddingRight(), bottom); 212 | } else { 213 | top = 0; 214 | bottom = parent.getHeight(); 215 | } 216 | 217 | final int childCount = parent.getChildCount(); 218 | final int lastPosition = state.getItemCount() - 1; 219 | for (int i = 0; i < childCount; i++) { 220 | final View child = parent.getChildAt(i); 221 | final int childRealPosition = parent.getChildAdapterPosition(child); 222 | 223 | // 过滤到头部不显示的分割线 224 | if (childRealPosition < mHeaderNoShowSize) { 225 | continue; 226 | } 227 | // 过滤到尾部不显示的分割线 228 | if (childRealPosition <= lastPosition - mFooterNoShowSize) { 229 | if (mDivider != null) { 230 | parent.getDecoratedBoundsWithMargins(child, mBounds); 231 | final int right = mBounds.right + Math.round(child.getTranslationX()); 232 | final int left = right - mDivider.getIntrinsicWidth(); 233 | mDivider.setBounds(left, top, right, bottom); 234 | mDivider.draw(canvas); 235 | } 236 | 237 | if (mPaint != null) { 238 | RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); 239 | int left1 = child.getRight() + params.rightMargin; 240 | int right1 = left1 + mDividerSpacing; 241 | int top1 = top + mLeftTopPadding; 242 | int bottom1 = bottom - mRightBottomPadding; 243 | canvas.drawRect(left1, top1, right1, bottom1, mPaint); 244 | } 245 | } 246 | } 247 | canvas.restore(); 248 | } 249 | 250 | @Override 251 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 252 | if (mDivider == null && mPaint == null) { 253 | outRect.set(0, 0, 0, 0); 254 | return; 255 | } 256 | //parent.getChildCount() 不能拿到item的总数 257 | int lastPosition = state.getItemCount() - 1; 258 | int position = parent.getChildAdapterPosition(view); 259 | 260 | 261 | // 滚动条置顶 262 | boolean isFixScrollTop = false; 263 | boolean isShowDivider = mHeaderNoShowSize <= position && position <= lastPosition - mFooterNoShowSize; 264 | 265 | if (mOrientation == VERTICAL) { 266 | if (isFixScrollTop) { 267 | outRect.set(0, 0, 0, 1); 268 | } else if (isShowDivider) { 269 | outRect.set(0, 0, 0, mDivider != null ? mDivider.getIntrinsicHeight() : mDividerSpacing); 270 | } else { 271 | outRect.set(0, 0, 0, 0); 272 | } 273 | } else { 274 | if (isFixScrollTop) { 275 | outRect.set(0, 0, 1, 0); 276 | } else if (isShowDivider) { 277 | outRect.set(0, 0, mDivider != null ? mDivider.getIntrinsicWidth() : mDividerSpacing, 0); 278 | } else { 279 | outRect.set(0, 0, 0, 0); 280 | } 281 | } 282 | } 283 | 284 | /** 285 | * 设置不显示分割线的item位置与个数 286 | * 287 | * @param headerNoShowSize 头部 不显示分割线的item个数 288 | * @param footerNoShowSize 尾部 不显示分割线的item个数,默认1,不显示最后一个,最后一个一般为加载更多view 289 | */ 290 | public SpacesItemDecoration setNoShowDivider(int headerNoShowSize, int footerNoShowSize) { 291 | this.mHeaderNoShowSize = headerNoShowSize; 292 | this.mFooterNoShowSize = footerNoShowSize; 293 | return this; 294 | } 295 | 296 | /** 297 | * 设置不显示头部分割线的item个数 298 | * 299 | * @param headerNoShowSize 头部 不显示分割线的item个数 300 | */ 301 | public SpacesItemDecoration setHeaderNoShowDivider(int headerNoShowSize) { 302 | this.mHeaderNoShowSize = headerNoShowSize; 303 | return this; 304 | } 305 | 306 | public SpacesItemDecoration setParam(int dividerColor, int dividerSpacing) { 307 | return setParam(dividerColor, dividerSpacing, 0, 0); 308 | } 309 | 310 | /** 311 | * 直接设置分割线颜色等,不设置drawable 312 | * 313 | * @param dividerColor 分割线颜色 314 | * @param dividerSpacing 分割线间距 315 | * @param leftTopPaddingDp 如果是横向 - 左边距 316 | * 如果是纵向 - 上边距 317 | * @param rightBottomPaddingDp 如果是横向 - 右边距 318 | * 如果是纵向 - 下边距 319 | */ 320 | public SpacesItemDecoration setParam(int dividerColor, int dividerSpacing, float leftTopPaddingDp, float rightBottomPaddingDp) { 321 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 322 | mPaint.setStyle(Paint.Style.FILL); 323 | mPaint.setColor(ContextCompat.getColor(mContext, dividerColor)); 324 | mDividerSpacing = dividerSpacing; 325 | mLeftTopPadding = dip2px(leftTopPaddingDp); 326 | mRightBottomPadding = dip2px(rightBottomPaddingDp); 327 | mDivider = null; 328 | return this; 329 | } 330 | 331 | /** 332 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 333 | */ 334 | public int dip2px(float dpValue) { 335 | final float scale = mContext.getResources().getDisplayMetrics().density; 336 | return (int) (dpValue * scale + 0.5f); 337 | } 338 | 339 | 340 | } 341 | -------------------------------------------------------------------------------- /app/src/main/res/color/white_black_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/empty_topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xhdpi/empty_topic.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_1_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tab_1_0.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tab_1_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_2_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tab_2_0.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tab_2_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_5_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tab_5_0.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tab_5_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tab_5_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tabbar_video_origin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tabbar_video_origin.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/tabbar_video_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/tabbar_video_selected.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/user_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/user_photo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/video_info_like_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QDong415/QKotlin/62f7eba42af60cdb35f1028dc40fc26af4beeac3/app/src/main/res/drawable-xxhdpi/video_info_like_tiny.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dashboard_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_white_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_white_black_45dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/red_circle_disable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/red_circle_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/red_circle_press.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/red_circle_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/white_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/white_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_listview.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 24 | 25 | 33 | 34 | 39 | 40 | 41 | 44 | 45 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_recyclerview.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 33 | 34 | 39 | 40 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 |