├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
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里的 extends BaseViewHolder> 就是可以兼容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