├── .gitignore ├── .idea ├── checkstyle-idea.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── dbnavigator.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── silencedut │ │ └── diffadapterdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── silencedut │ │ │ └── diffadapterdemo │ │ │ ├── LOLActivity.kt │ │ │ ├── LegendViewModel.kt │ │ │ ├── LinearLayoutManagerWrapper.kt │ │ │ ├── MethodStackTrace.java │ │ │ └── adapter │ │ │ ├── LegendHolder.kt │ │ │ ├── LegendViewData.kt │ │ │ ├── SkinHolder.kt │ │ │ └── SkinViewData.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── button_bg.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── holder_legend_introduce.xml │ │ └── holder_skins.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 │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── silencedut │ └── diffadapterdemo │ └── ExampleUnitTest.java ├── build.gradle ├── core ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── silencedut │ │ └── core │ │ ├── DataObjects.kt │ │ ├── Transfer.kt │ │ └── provider │ │ ├── legend │ │ ├── ILegendDateProvider.kt │ │ ├── LegendNotification.kt │ │ └── pojo │ │ │ ├── Legend.kt │ │ │ ├── LegendBaseInfo.kt │ │ │ ├── LegendPrice.kt │ │ │ ├── LegendSkin.kt │ │ │ └── Type.kt │ │ └── othermodule │ │ └── AnyApi.kt │ └── res │ └── values │ └── strings.xml ├── diffadapter ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── silencedut │ │ └── diffadapter │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── silencedut │ │ │ └── diffadapter │ │ │ ├── AsyncListUpdateDiffer.java │ │ │ ├── DiffAdapter.java │ │ │ ├── IProvideItemId.java │ │ │ ├── data │ │ │ └── BaseMutableData.java │ │ │ ├── holder │ │ │ ├── BaseDiffViewHolder.java │ │ │ └── NoDataDifferHolder.java │ │ │ ├── rvhelper │ │ │ └── RvHelper.kt │ │ │ └── utils │ │ │ ├── DiffModelProvider.java │ │ │ ├── ListChangedCallback.java │ │ │ └── UpdatePayloadFunction.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── silencedut │ └── diffadapter │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── legend ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── silencedut │ │ └── legend │ │ ├── LegendModule.kt │ │ └── impl │ │ └── LegendDataProviderImpl.kt │ └── res │ └── values │ └── strings.xml ├── media ├── demo.gif └── diffadapter.apk ├── settings.gradle └── version.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | 45 | # Keystore files 46 | # Uncomment the following line if you do not want to check your keystore files in. 47 | #*.jks 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | 52 | # Google Services (e.g. APIs or Firebase) 53 | google-services.json 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | 58 | xmlns:android 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | xmlns:.* 70 | 71 | ^$ 72 | 73 | 74 | BY_NAME 75 | 76 |
77 |
78 | 79 | 80 | 81 | .*:id 82 | 83 | http://schemas.android.com/apk/res/android 84 | 85 | 86 | 87 |
88 |
89 | 90 | 91 | 92 | .*:name 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | 98 |
99 |
100 | 101 | 102 | 103 | name 104 | 105 | ^$ 106 | 107 | 108 | 109 |
110 |
111 | 112 | 113 | 114 | style 115 | 116 | ^$ 117 | 118 | 119 | 120 |
121 |
122 | 123 | 124 | 125 | .* 126 | 127 | ^$ 128 | 129 | 130 | BY_NAME 131 | 132 |
133 |
134 | 135 | 136 | 137 | .* 138 | 139 | http://schemas.android.com/apk/res/android 140 | 141 | 142 | ANDROID_ATTRIBUTE_ORDER 143 | 144 |
145 |
146 | 147 | 148 | 149 | .* 150 | 151 | .* 152 | 153 | 154 | BY_NAME 155 | 156 |
157 |
158 |
159 |
160 |
161 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/dbnavigator.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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 46 | 47 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 88 | 89 | 90 | 110 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Diffadapter [![](https://jitpack.io/v/silencedut/diffadapter.svg)](https://jitpack.io/#silencedut/diffadapter) 2 | 一款针对RecyclerView高效刷新,多类型列表,异步数据更新,崩溃等各种复杂难处理场景的高性能易用的列表库 3 | 4 | ### Demo 5 | [diffadapter.apk](https://github.com/SilenceDut/diffadapter/blob/master/media/diffadapter.apk) 6 | 7 | 图像url,名称,价格都是异步或者通知变化的数据 8 | 9 | ![image](https://github.com/SilenceDut/diffadapter/blob/master/media/demo.gif) 10 | 11 | ## Introduce 12 | [如何实现一个高效、高性能的、异步数据实时刷新的列表](http://www.silencedut.com/2019/01/24/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E9%AB%98%E6%95%88%E3%80%81%E9%AB%98%E6%80%A7%E8%83%BD%E3%80%81%E5%BC%82%E6%AD%A5%E6%95%B0%E6%8D%AE%E5%AE%9E%E6%97%B6%E5%88%B7%E6%96%B0%E7%9A%84%E5%88%97%E8%A1%A8/) 13 | 14 | diffadapter就是根据实际项目中各种复杂的列表需求,同时为了解决DiffUtil使用不方便,容易出错而实现的一个**高效,高性能的列表库 15 | ,侵入性低,方便接入**,致力于将列表需求的开发精力用于具体的Item Holder上,而不用花时间在一些能通用的和业务无关的地方。 16 | 使用DiffUtil作为来做最小更新,屏蔽外部调用**DiffUtil**的接口。无需自行实现DiffUtil,只用实现简单的数据接口和展示数据的Holder, 17 | 不用自己去实现Adapter来管理数据和Holder之间的关系,不用考虑DiffUtil的实现细节,就能快速的开发出一个高性能的复杂列表需求。 18 | 19 | ## Feature 20 | 21 | * 无需自己实现Adapter,简单配置就可实现没有各种if-else判断类型的多Type视图列表 22 | * 使用DiffUtil来找出最小需要更新的Item集合,使用者无需做任何DiffUtil的配置即可实现高效的列表 23 | * 提供方便,稳定的更新、删、插入、查询方法,适用于各种非常频繁,复杂的场景(如因为异步或通知的原因同时出现插入,删除,全量设置的情况) 24 | * 更友好方便的异步数据更新方案 25 | 26 | ## Using 27 | 28 | ### 基本用法 29 | 30 | **Step 1:继承`BaseMutableData`,主要实现`areUISame(newData: AnyViewData)` 和 `uniqueItemFeature()`** 31 | 32 | ```kotlin 33 | class AnyViewData(var id : Long ,var any : String) : BaseMutableData() { 34 | 35 | companion object { 36 | //数据展示的layout,也是和Holder一一对应的唯一特征 37 | const val VIEW_ID = R.layout.holder_skins 38 | } 39 | 40 | override fun getItemViewId(): Int { 41 | 42 | return VIEW_ID 43 | } 44 | 45 | override fun areUISame(newData: AnyViewData): Boolean { 46 | // 判断新旧数据是否展示相同的UI,如果返回True,则表示UI不需要改变,不会updateItem,可以理解为 47 | 如果新数据newData是什么样才不需要更新UI 48 | 49 | return this.any == newData.any 50 | } 51 | 52 | override fun uniqueItemFeature(): Any { 53 | // 返回可以标识这个Item的特征,比如uid,id等,用来做动态更改单个item查找的条件 54 | return this.id 55 | } 56 | 57 | } 58 | ``` 59 | 60 | **Step 2:继承`BaseDiffViewHolder`,泛型类型传入上面定义的`AnyViewData`** 61 | 62 | ```kotlin 63 | class AnyHolder(itemView: View, recyclerAdapter: DiffAdapter): BaseDiffViewHolder( itemView, recyclerAdapter){ 64 | 65 | override fun getItemViewId(): Int { 66 | return AnyViewData.VIEW_ID 67 | } 68 | 69 | 70 | override fun updateItem(data: AnyViewData, position: Int) { 71 | 根据AnyViewData.VIEW_ID对应的layout来更新Item 72 | Log.d(TAG,"updateItem $data") 73 | } 74 | } 75 | ``` 76 | 77 | **Step 3:注册,显示到界面** 78 | 79 | ```kotlin 80 | val diffAdapter = DiffAdapter(this) 81 | 82 | //注册类型,不分先后顺序 83 | diffAdapter.registerHolder(AnyHolder::class.java, AnyViewData.VIEW_ID) 84 | diffAdapter.registerHolder(AnyHolder2::class.java, AnyViewData2.VIEW_ID) 85 | diffAdapter.registerHolder(AnyHolder3::class.java, AnyViewData3.VIEW_ID) 86 | 87 | val linearLayoutManager = LinearLayoutManager(this) 88 | recyclerView.layoutManager = linearLayoutManager 89 | recyclerView.adapter = diffAdapter 90 | 91 | //监听数据变化 92 | 93 | fun onFetchedData(datas : List>) { 94 | diffAdapter.datas = adapterListData 95 | } 96 | 97 | ``` 98 | 99 | 只需要上面几步,就可以完成如类似下图的多type列表,其中数据源里的每个BaseMutableData的getItemViewId()决定着用哪个Holder展示UI。 100 | (以上均用`kotlin`实现,`Java`使用不受任何限制) 101 | 102 | ![](https://ws3.sinaimg.cn/large/006tNc79gy1fzbiv9fcc5j30gq19ywq2.jpg) 103 | 104 | ### 增、插入、删除、修改(更新) 105 | 106 | ```java 107 | public void addData(T data) 108 | 109 | public void deleteData(BaseMutableData data) 110 | 111 | public void deleteData(int startPosition, int size) 112 | 113 | void insertData(int startPosition ,List datas) 114 | 115 | public void updateData(BaseMutableData newData) 116 | ``` 117 | 上述接口在调用的时机,频率都很复杂的场景下也不会引起崩溃 118 | 119 | 使用updateData(BaseMutableData newData)时,newData可以是新new的对象,也可以是修改后的原对象,不会出现[使用DiffUtil更新单个数据无效](http://www.silencedut.com/2019/01/24/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E9%AB%98%E6%95%88%E3%80%81%E9%AB%98%E6%80%A7%E8%83%BD%E3%80%81%E5%BC%82%E6%AD%A5%E6%95%B0%E6%8D%AE%E5%AE%9E%E6%97%B6%E5%88%B7%E6%96%B0%E7%9A%84%E5%88%97%E8%A1%A8/) 120 | 的问题 121 | 122 | 基本上就提供了上述很少的几个接口,主要是为了功能更清晰,侵入性更低,你可以根据自己的需要组合更多的功能,像下拉刷新,动画等。 123 | 124 | ### 高阶用法 125 | 126 | 基本用法中**Data和Holder绑定的模式并没什么特殊之处,早在两年前的项目[KnowWeather](https://github.com/SilenceDut/KnowWeather)就已经用上这种思想,现在只是结合DiffUtil以及其他的疑难问题解决方案将其开源,diffadapter最核心的地方在于高性能和异步获取数据或者通知数据变化时列表的更新上** 127 | 128 | 129 | #### 多数据源异步更新 130 | 131 | ![](https://ws3.sinaimg.cn/large/006tNc79gy1fz73pvxepdj30qc046ac1.jpg) 132 | 以一个类似的Item为例,这里认为服务器返回的数据列表只包含uid,也就是`List uids`,个人资料,等级,贵族等都属于不同的协议。下面展示的是异步获取个人资料展示的头像和昵称的情况,其他的可以类比。 133 | 134 | **Step 1:定义ViewData** 135 | 136 | ```kotlin 137 | data class ItemViewData(var uid:Long, var userInfo: UserInfo?, var anyOtherData: Any ...) : BaseMutableData() { 138 | 139 | companion object { 140 | const val VIEW_ID = R.layout.... 141 | } 142 | 143 | override fun getItemViewId(): Int { 144 | return VIEW_ID 145 | } 146 | 147 | override fun areUISame(newData: UserInfo): Boolean { 148 | return this.userInfo?.portrait == newData.userInfo?.portrait && this.userInfo?.nickName == newData.userInfo?.nickName && this.anyOtherData == newData.anyOtherData 149 | } 150 | 151 | override fun uniqueItemFeature(): Any { 152 | return this.uid 153 | } 154 | 155 | } 156 | ``` 157 | 数据类ItemViewData包含所有需要显示到Item上的信息,这里只处理和个人资料相关的数据,`anyOtherData: Any ...`表示Item所需的其他数据内容 158 | 159 | BaseMutableData里有个默认的方法`allMatchFeatures(@NonNull Set allMatchFeatures)`,不需要显示调用,这里当外部有异步数据变化时,提供当前BaseMutableData用来匹配变化的异步数据的对象 160 | 161 | ```java 162 | public void appendMatchFeature(@NonNull Set allMatchFeatures) { 163 | allMatchFeatures.add(uniqueItemFeature()); 164 | } 165 | ``` 166 | 167 | 默认添加了uniqueItemFeature(),allMatchFeatures是个Set,可以重写方法添加多个用来匹配的特征。 168 | 169 | 170 | **Step 2:定义View Holder,同基本用法** 171 | 172 | **Step 3:监听数据变化,更新列表** 173 | 174 | ```kotlin 175 | //用于监听请求的异步数据,userInfoData变化时与此相关的数据 176 | private val userInfoData = MutableLiveData() 177 | 178 | //在adapter里监听数据变化 179 | diffAdapter.addUpdateMediator(userInfoData, object : UpdatePayloadFunction { 180 | override fun providerMatchFeature(input: UserInfo): Any { 181 | return input.uid 182 | } 183 | 184 | override fun applyChange(input: UserInfo, originalData: ItemViewData,, payloadKeys: MutableSet): ItemViewData { 185 | 186 | return originalData.userInfo = input 187 | 188 | } 189 | }) 190 | 191 | // 任何通知数据获取到的通知 192 | fun asyncDataFetch(userInfo : UserInfo) { 193 | userInfoData.value = userInfo 194 | } 195 | 196 | ``` 197 | 这样当asyncDataFetch接收到数据变化的通知的时候,改变userInfoData的值,adapter里对应的Item就会更新。其中找到adapter中需要更新的Item是关键部分,主要由实现`UpdatePayloadFunction `来完成,实现`UpdatePayloadFunction `也很简单。 198 | 199 | ```java 200 | public abstract class UpdatePayloadFunction implements UpdateFunction { 201 | 202 | /** 203 | * 匹配所有数据,及返回类型为R的所有数据 204 | */ 205 | Object MATCH_ALL = new Object(); 206 | 207 | /** 208 | * 提供一个特征,用来查找列表数据中和此特征相同的数据 209 | * @param input 用来提供查找数据和最终改变列表的数据 ,最终匹配的是allMatchFeatures里的数据,默认情况下就是uniqueItemFeature() 210 | * @return 用来查找列表中的数据的特征项 211 | */ 212 | Object providerMatchFeature(@NonNull I input); 213 | 214 | /** 215 | * 匹配到对应的数据,如果符合条件的数据有很多个,可能会被回调多次,不需要新建对象,主需要根据Input把originalData改变相应的值就行了 216 | * @param input 是数据改变的部分数据源 217 | * @param originalData 需要改变的数据项 218 | * @param payloadKeys 用来标识改变后的数据哪些部分发生了改变,if payloadKeys is not empty , 219 | * {@link com.silencedut.diffadapter.holder.BaseDiffViewHolder#updatePartWithPayload(BaseMutableData, Bundle, int)} 220 | * will be call rather than 221 | * {@link com.silencedut.diffadapter.holder.BaseDiffViewHolder#updateItem(BaseMutableData, int)} 222 | * @return 改变后的数据项, 223 | * 224 | */ 225 | 226 | public abstract R applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set payloadKeys); 227 | 228 | } 229 | ``` 230 | `UpdatePayloadFunction`用来提供异步数据获取到后数据用来和列表中的数据匹配的规则和根据规则找到需要更改的对象后如果改变原对象,剩下的更新都由`diffadapter`来处理。如果符合条件的数据有很多个,`applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set payloadKeys)`会被回调多次。 231 | 如下时: 232 | 233 | ```java 234 | Object providerMatchFeature(@NonNull I input) { 235 | return UpdateFunction.MATCH_ALL 236 | } 237 | ``` 238 | `applyChange`回调的次数就和列表中的数据量一样多。 239 | 240 | 如果同一种匹配规则`providerMatchFeature`对应多种Holder类型,`UpdateFunction`的返回数据类型R就可以直接设为基类的`BaseMutableData`,然后再applyChange里在具体根据类型来处理不同的UI。 241 | 242 | `UpdateFunction`已废弃,`payloadKeys`可以用来解决payload方式更新item时每次需要new对象的问题。 243 | 244 | ### 最高效的Item局部更新方式 —— payload 245 | 246 | DiffUtil 能让一个列表中只更新部分数据变化的Item,payload能让同一个Item只更新需要变化的View,这种方式非常适合同一个Item有多个异步数据源的,同时又对性能有更高要求的列表。看具体更新需求来判断是否有必要。 247 | 248 | 有两种情况的局部更新 249 | 250 | **第一种是全量数据对比的情况,也就是同一个业务可能会多次调用`diffadapter.setData(List)`,可使用如下的方式** 251 | 252 | **Step 1:重写BaseMutableData的appendDiffPayload** 253 | 254 | ```kotlin 255 | data class ItemViewData(var uid:Long, var userInfo: UserInfo?, var anyOtherData: Any ...) : BaseMutableData() { 256 | 257 | companion object { 258 | const val KEY_BASE_INFO = "KEY_BASE_INFO" 259 | const val KEY_ANY = "KEY_ANY" 260 | } 261 | 262 | ... 263 | 264 | /** 265 | * 最高效的更新方式,如果不是全量频繁更新的可以不实现这个方法 266 | */ 267 | override fun appendPayloadKeys(newData: LegendViewData, payloadKeys: MutableSet) { 268 | super.appendPayloadKeys(newData, payloadKeys) 269 | if(this.userInfo!= newData.userInfo) { 270 | payloadKeys.add(KEY_BASE_INFO) 271 | } 272 | if(this.anyData != newData.anyData) { 273 | payloadKeys.add(KEY_ANY) 274 | 275 | } 276 | ... 277 | } 278 | 279 | } 280 | ``` 281 | 282 | 默认用Bundle存取变化,无需存具体的数据,只需类似设置标志位,表明Item的哪部分数据发生了变化。 283 | 284 | **第二种是异步动态更新一个Item的时候,比如个人资料获取,中途单个Item数据变化的情况,可使用如下的Step1** 285 | 286 | ```java 287 | public R applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set payloadKeys){ 288 | ... 289 | originalData.*** = input.*** 290 | payloadKeys.add("自定义String类型的Key值") 291 | ... 292 | } 293 | ``` 294 | 这两种方式不是互斥的,但也没什么关联,也可以根据自己的业务场景自行选择,如果不需要payload更新,两种方式都不需要。后续的步骤两种方式相同。 295 | 296 | **Step 2 :需要重写BaseDiffViewHolder里的`updatePartWithPayload`** 297 | 298 | ```kotlin 299 | class ItemViewHolder(itemViewRoot: View, recyclerAdapter: DiffAdapter): BaseDiffViewHolder( itemViewRoot, recyclerAdapter){ 300 | 301 | override fun updatePartWithPayload(data: ItemViewData, payloadKeys: MutableSet, position: Int) { 302 | 303 | if(payloadKeys.contains(ItemViewData.KEY_BASE_INFO)) { 304 | updateBaseInfo(data) 305 | } 306 | 307 | if(payloadKeys.contains(ItemViewData.KEY_ANY)) { 308 | updateAnyView(data) 309 | } 310 | } 311 | ``` 312 | 313 | **Step 3:监听数据变化,更新列表,这个只是异步数据更新Item需要也就是第二种场景,如果每次`diffadapter.setData(List)`的数据已经是是有所有的数据信息,不需要以下的动态更新方案** 314 | 315 | ```kotlin 316 | //用于监听请求的异步数据,userInfoData变化时与此相关的数据 317 | private val userInfoData = MutableLiveData() 318 | 319 | //在adapter里监听数据变化 320 | diffAdapter.addUpdateMediator(userInfoData, object : UpdateFunction { 321 | override fun providerMatchFeature(input: UserInfo): Any { 322 | return input.uid 323 | } 324 | 325 | override fun applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set payloadKeys): ItemViewData { 326 | //不再需要新建对象 327 | originalData.*** = input.*** 328 | payloadKeys.add("自定义String类型的Key值") 329 | return originalData 330 | 331 | } 332 | }) 333 | 334 | // 任何通知数据获取到的通知 335 | fun asyncDataFetch(userInfo : UserInfo) { 336 | userInfoData.value = userInfo 337 | } 338 | ``` 339 | 340 | 341 | ## More 342 | 343 | 一些探讨: 344 | 345 | 1. 为什么没有提供类似onItemClickLisener用来处理点击事件的接口 346 | 347 | 不是因为不好实现,其实现实起来非常简单。首先尝试去理解为什么RecyclerView.Adapter 没有提供像listview那样的点击事件的listener,我的理解是大而全的公用点击监听不是一个好的设计方式,尤其对于多类型的view来说,因为点击的是不同的holder,要在回调里根据类型来处理不同的逻辑,少不了各种`if-else`的代码块,不同holder相关的数据,逻辑耦合到一块,试想如果有四五种类型,处理统一点击回调的地方是多大的一块代码,后期的维护又是一个问题。我认为好的方式应该是在各自的holder的构造函数里来各自处理,每个holder都有自己的数据和类型,很好的隔离开不同类型数据的耦合,每个holder各司其职:显示数据,监听点击,维护方便。 348 | 349 | 2. 为什么没有下拉刷新、加载更多、动画、分割线等更多的功能 350 | 351 | 首先diffadapter主要就是为了提供高性能刷新,异步数据更新,高效的配置多类型列表的功能,这也是绝大多数列表最常见的功能,像上面说的那些功能以及onItemClickLisener都是一些额外的添加项,不想做一个为了看起来更多功能但没有任何难度,堆积代码的开源库,不想为了看起来大而全来吸引别人使用。就是职责很单一,目的很明确,diffadapter侵入性很低,不影响任何其他功能的引入,包括不限于上面提到的那些。而且上面提到的那些都有很多很好的开源库,你可以根据任何自己的需要来定制。 352 | 353 | 354 | **更详细,多样的使用方式和细节见[diffadapter demo](https://github.com/SilenceDut/diffadapter)**,有详细的demo和使用说明,demo用kotlin实现,使用了**mvvm**和**模块化**的框架方式。 355 | 356 | 这种方式也是目前能想到的比较好的异步数据更新列表的方式,非常欢迎一起探讨更多的实现方式。 357 | 358 | ## 引入 359 | 360 | **Step1.Add it in your root add build.gradle at the end of repositories:** 361 | 362 | ```java 363 | allprojects { 364 | repositories { 365 | .. 366 | maven { url 'https://jitpack.io' } 367 | } 368 | } 369 | ``` 370 | 371 | 372 | **Step2. Add the dependency:** 373 | 374 | ```java 375 | dependencies { 376 | implementation 'com.github.silencedut:diffadapter:latestVersion' 377 | } 378 | ``` 379 | 380 | **ProGuard** 381 | 382 | ```java 383 | -keep class * extends com.silencedut.diffadapter.holder.BaseDiffViewHolder {*;} 384 | -keep class * extends com.silencedut.diffadapter.data.BaseMutableData {*;} 385 | ``` 386 | 387 | ## License 388 | 389 | 390 | ``` 391 | Copyright 2017-2018 SilenceDut 392 | 393 | Licensed under the Apache License, Version 2.0 (the "License"); 394 | you may not use this file except in compliance with the License. 395 | You may obtain a copy of the License at 396 | 397 | http://www.apache.org/licenses/LICENSE-2.0 398 | 399 | Unless required by applicable law or agreed to in writing, software 400 | distributed under the License is distributed on an "AS IS" BASIS, 401 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 402 | See the License for the specific language governing permissions and 403 | limitations under the License. 404 | ``` 405 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion _compileSdkVersion 8 | 9 | defaultConfig { 10 | applicationId "com.silencedut.diffadapterdemo" 11 | minSdkVersion _minSdkVersion 12 | targetSdkVersion _targetSdkVersion 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | 23 | debug { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation project(':diffadapter') 34 | implementation project(':core') 35 | implementation project(':legend') 36 | 37 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 38 | 39 | // api "com.github.silencedut:diffadapter:2.0.0_alpha" 40 | api "android.arch.lifecycle:extensions:$lifecycle_extensions" 41 | api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 42 | 43 | } 44 | -------------------------------------------------------------------------------- /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 22 | 23 | 24 | # -------Okhttp3 --------- 25 | -dontwarn okhttp3.** 26 | -dontwarn okhttp3.logging.** 27 | -dontwarn okio.** 28 | -keep class okhttp3.internal.**{*;} 29 | -keep class okhttp3.** { *;} 30 | -keep interface okhttp3.* { *;} 31 | 32 | # -------kotlin --------- 33 | 34 | -keepclassmembernames class kotlinx.** { 35 | volatile ; 36 | } 37 | 38 | # -------hub --------- 39 | -keep class * implements com.silencedut.hub_annotation.IFindImplClz {*;} 40 | -keep class * implements com.silencedut.hub_annotation.IFindActivity {*;} 41 | 42 | 43 | -keepnames interface * extends com.silencedut.hub.IHub 44 | -keepnames interface * extends com.silencedut.hub.IHubActivity 45 | -keep interface * extends com.silencedut.hub.IHubActivity {;} 46 | 47 | -dontwarn com.alibaba.fastjson.** 48 | -keepattributes Signature 49 | -keepattributes *Annotation* 50 | # -------hub --------- 51 | 52 | # diffadapter 53 | -keep class * extends com.silencedut.diffadapter.holder.BaseDiffViewHolder {*;} 54 | -keep class * extends com.silencedut.diffadapter.data.BaseMutableData {*;} 55 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/silencedut/diffadapterdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.silencedut.diffadapterdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/LOLActivity.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo 2 | 3 | import android.arch.lifecycle.Lifecycle 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.support.v7.app.AppCompatActivity 7 | import android.support.v7.widget.LinearLayoutManager 8 | import android.support.v7.widget.RecyclerView 9 | import android.util.Log 10 | import android.view.View 11 | import android.widget.TextView 12 | import android.widget.Toast 13 | import com.silencedut.core.Transfer 14 | import com.silencedut.core.provider.legend.ILegendDateProvider 15 | import com.silencedut.diffadapter.DiffAdapter 16 | import com.silencedut.diffadapter.data.BaseMutableData 17 | import com.silencedut.diffadapter.rvhelper.RvHelper 18 | import com.silencedut.diffadapter.rvhelper.RvHelper.Companion 19 | import com.silencedut.diffadapter.utils.DiffModelProvider 20 | import com.silencedut.diffadapterdemo.adapter.LegendHolder 21 | import com.silencedut.diffadapterdemo.adapter.LegendViewData 22 | import com.silencedut.diffadapterdemo.adapter.SkinHolder 23 | import com.silencedut.diffadapterdemo.adapter.SkinViewData 24 | import com.silencedut.taskscheduler.TaskScheduler 25 | import java.util.* 26 | import kotlin.collections.ArrayList 27 | 28 | /** 29 | * @author SilenceDut 30 | * @date 2019/1/17 31 | */ 32 | private const val TAG = "LOLActivity" 33 | private const val MAX_COUNT = 5000 34 | class LOLActivity : AppCompatActivity(){ 35 | private var mRVTest : RecyclerView? = null 36 | private var mTestHandler : Handler?=null 37 | private var testStarted = false 38 | private var testCount = 0 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | setContentView(R.layout.activity_main) 43 | mRVTest = this.findViewById(R.id.rv_test) 44 | 45 | val legendViewModel = DiffModelProvider.getModel(this, LegendViewModel::class.java) 46 | val diffAdapter = DiffAdapter(this) 47 | 48 | //配置支持的Holder类型 49 | diffAdapter.registerHolder(SkinHolder::class.java, SkinViewData.VIEW_ID) 50 | diffAdapter.registerHolder(LegendHolder::class.java, LegendViewData.VIEW_ID) 51 | 52 | mRVTest!!.layoutManager = LinearLayoutManagerWrapper(this) 53 | mRVTest!!.adapter = diffAdapter 54 | 55 | //监听变化,自动刷新 56 | legendViewModel.addUpdateMediator(diffAdapter) 57 | 58 | legendViewModel.legendsData.observeForever{ 59 | 60 | if (it != null) { 61 | val list = mutableListOf>() 62 | list.addAll(it) 63 | 64 | diffAdapter.datas = list 65 | } 66 | 67 | } 68 | 69 | findViewById(R.id.fetch_data).setOnClickListener { 70 | legendViewModel.fetchLegends() 71 | } 72 | 73 | findViewById(R.id.random_update).setOnClickListener { _ -> 74 | verify(diffAdapter)?.let { 75 | legendViewModel.legendsDataChanged() 76 | } 77 | 78 | } 79 | 80 | findViewById(R.id.random_add).setOnClickListener { 81 | val oneLegend = Transfer.getImpl(ILegendDateProvider::class.java).fetchOneLegends() 82 | diffAdapter.addData(legendViewModel.convertToAdapterData(oneLegend)) 83 | } 84 | 85 | findViewById(R.id.random_insert).setOnClickListener { _ -> 86 | verify(diffAdapter)?.let { 87 | val insertIndex = (0 until diffAdapter.itemCount).random() 88 | val insertSize= (0 until diffAdapter.itemCount).random() 89 | 90 | val newList = ArrayList(diffAdapter.datas.subList(0,insertSize)) 91 | diffAdapter.insertData(insertIndex,newList) 92 | Toast.makeText(this,"现在多少条:"+diffAdapter.itemCount,Toast.LENGTH_SHORT).show() 93 | } 94 | 95 | } 96 | 97 | findViewById(R.id.random_delete_many).setOnClickListener{ _ -> 98 | verify(diffAdapter)?.let { 99 | val deleteIndex = (0 until diffAdapter.itemCount).random() 100 | val deleteSize= (0 until diffAdapter.itemCount).random() 101 | diffAdapter.deleteData(deleteIndex,deleteSize) 102 | } 103 | 104 | } 105 | 106 | findViewById(R.id.random_delete_one).setOnClickListener { _ -> 107 | verify(diffAdapter)?.let { 108 | diffAdapter.deleteData(diffAdapter.datas[(0 until diffAdapter.itemCount).random()]) 109 | } 110 | 111 | } 112 | 113 | findViewById(R.id.forcible_crash_test).setOnClickListener { _ -> 114 | 115 | if (!testStarted) { 116 | verify(diffAdapter)?.let { 117 | testStarted = true 118 | findViewById(R.id.forcible_crash_test).text = "停止" 119 | mTestHandler = Handler() 120 | 121 | mTestHandler?.let { 122 | it.post(object : Runnable { 123 | override fun run() { 124 | if(verifyCount()) { 125 | return 126 | } 127 | val oneLegend = Transfer.getImpl(ILegendDateProvider::class.java).fetchOneLegends() 128 | diffAdapter.addData(legendViewModel.convertToAdapterData(oneLegend)) 129 | val time = (100 until 130L).random() 130 | it.postDelayed(this, time) 131 | } 132 | }) 133 | 134 | it.post(object : Runnable { 135 | override fun run() { 136 | if(verifyCount()) { 137 | return 138 | } 139 | legendViewModel.fetchLegends() 140 | val time = (50 until 200L).random() 141 | it.postDelayed(this, time) 142 | } 143 | }) 144 | 145 | it.post(object : Runnable { 146 | override fun run() { 147 | if(verifyCount()) { 148 | return 149 | } 150 | if(diffAdapter.itemCount>0) { 151 | diffAdapter.deleteData(diffAdapter.datas[(0 until diffAdapter.itemCount).random()]) 152 | 153 | val insertIndex = (0 until diffAdapter.itemCount).random() 154 | val insertSize = (0 until diffAdapter.itemCount).random() 155 | 156 | val newList = ArrayList(diffAdapter.datas.subList(0, insertSize)) 157 | diffAdapter.insertData(insertIndex, newList) 158 | 159 | 160 | } 161 | val time = (60 until 150L).random() 162 | it.postDelayed(this, time) 163 | } 164 | }) 165 | 166 | it.post(object :Runnable{ 167 | override fun run() { 168 | // diffAdapter.clear() 169 | val time = (30 until 450L).random() 170 | it.postDelayed(this, time) 171 | } 172 | }) 173 | 174 | it.post(object : Runnable { 175 | override fun run() { 176 | if(verifyCount()) { 177 | return 178 | } 179 | if(diffAdapter.itemCount>0) { 180 | val deleteIndex = (0 until diffAdapter.itemCount).random() 181 | val deleteSize = (0 until diffAdapter.itemCount).random() 182 | diffAdapter.deleteData(deleteIndex, deleteSize) 183 | 184 | } 185 | 186 | val time = (100 until 300L).random() 187 | it.postDelayed(this, time) 188 | } 189 | }) 190 | 191 | it.post(object : Runnable { 192 | override fun run() { 193 | if(verifyCount()) { 194 | return 195 | } 196 | mRVTest?.let { 197 | RvHelper.scrollTo(it, (0 until diffAdapter.itemCount).random()) 198 | } 199 | 200 | val time = (300 until 500L).random() 201 | it.postDelayed(this, time) 202 | } 203 | }) 204 | } 205 | } 206 | } else { 207 | endTest() 208 | } 209 | } 210 | 211 | 212 | 213 | TaskScheduler.runOnUIThread(this,object : Runnable{ 214 | override fun run() { 215 | if(lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { 216 | Toast.makeText(this@LOLActivity,"点击Item 刷新当前Item",Toast.LENGTH_SHORT).show() 217 | } 218 | TaskScheduler.runOnUIThread(this@LOLActivity,this,10000) 219 | } 220 | },10000) 221 | 222 | //如果更新很频繁,关闭动画能极大提高UI性能 223 | RvHelper.closeDefaultAnimator(mRVTest!!) 224 | 225 | } 226 | 227 | private fun endTest() { 228 | mTestHandler?.removeCallbacksAndMessages(null) 229 | findViewById(R.id.forcible_crash_test).text = "暴力崩溃测试" 230 | mTestHandler?.removeCallbacksAndMessages(null) 231 | testStarted = false 232 | } 233 | 234 | private fun verify( diffAdapter: DiffAdapter):Any? { 235 | if(diffAdapter.itemCount == 0) { 236 | Toast.makeText(this,"先获取数据",Toast.LENGTH_LONG).show() 237 | return null 238 | } 239 | 240 | return Any() 241 | } 242 | 243 | private fun verifyCount() :Boolean{ 244 | testCount++ 245 | if(testCount > MAX_COUNT) { 246 | findViewById(R.id.forcible_crash_test).text = "暴力崩溃测试" 247 | endTest() 248 | testCount = 0 249 | return true 250 | } 251 | return false 252 | } 253 | 254 | private fun IntRange.random() : Int { 255 | try { 256 | return Random().nextInt((endInclusive + 1) - start) + start 257 | }catch (e : Exception) { 258 | Log.d(TAG,"exception on random",e) 259 | } 260 | return 0 261 | } 262 | 263 | 264 | } -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/LegendViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo 2 | 3 | import android.arch.lifecycle.MutableLiveData 4 | import android.arch.lifecycle.ViewModel 5 | import android.util.Log 6 | import com.silencedut.core.Transfer 7 | import com.silencedut.core.provider.legend.ILegendDateProvider 8 | import com.silencedut.core.provider.legend.LegendNotification 9 | import com.silencedut.core.provider.legend.pojo.* 10 | import com.silencedut.diffadapter.DiffAdapter 11 | import com.silencedut.diffadapter.data.BaseMutableData 12 | import com.silencedut.diffadapter.utils.UpdatePayloadFunction 13 | import com.silencedut.diffadapterdemo.adapter.LegendViewData 14 | import com.silencedut.diffadapterdemo.adapter.SkinViewData 15 | 16 | /** 17 | * @author SilenceDut 18 | * @date 2019/1/17 19 | */ 20 | class LegendViewModel : ViewModel(), LegendNotification.LegendInfo, LegendNotification.LegendsList { 21 | 22 | val legendsData = MutableLiveData>>() 23 | private val legendBaseInfoData = MutableLiveData() 24 | private val legendPriceData = MutableLiveData() 25 | private val legendSkinData = MutableLiveData() 26 | 27 | companion object { 28 | const val TAG = "LegendViewModel" 29 | } 30 | 31 | init { 32 | 33 | Transfer.subscribe(this) 34 | } 35 | 36 | override fun onCleared() { 37 | super.onCleared() 38 | Transfer.unSubscribe(this) 39 | } 40 | 41 | fun fetchLegends() { 42 | Transfer.getImpl(ILegendDateProvider::class.java).fetchLegends() 43 | } 44 | 45 | fun legendsDataChanged() { 46 | Transfer.getImpl(ILegendDateProvider::class.java).legendsDataChanged() 47 | } 48 | 49 | fun updateSkinHolder(legendId: Long) { 50 | Transfer.getImpl(ILegendDateProvider::class.java).updateLegendSkin(legendId) 51 | } 52 | 53 | fun updateLegendHolder(legendId: Long) { 54 | Transfer.getImpl(ILegendDateProvider::class.java).updateLegendNameAndPrice(legendId) 55 | } 56 | 57 | override fun onLegendsFetched(ids: List) { 58 | legendsData.value = ids.map { 59 | convertToAdapterData(it) 60 | } 61 | } 62 | 63 | fun convertToAdapterData(legend: Legend): BaseMutableData<*> { 64 | return when (legend.type) { 65 | Type.LEGEND -> LegendViewData(legend.id, 66 | Transfer.getImpl(ILegendDateProvider::class.java).baseLegendData(legend.id) 67 | , Transfer.getImpl(ILegendDateProvider::class.java).legendPrice(legend.id)?.price) 68 | Type.SKIN -> SkinViewData(legend.id, 69 | Transfer.getImpl(ILegendDateProvider::class.java).baseLegendData(legend.id)?.iconUrl 70 | , Transfer.getImpl(ILegendDateProvider::class.java).legendSkin(legend.id)) 71 | } 72 | } 73 | 74 | fun addUpdateMediator(diffAdapter: DiffAdapter) { 75 | //如果变化的数据可能引起多种类型的holder的刷新,UpdateFunction的类型传入基础BaseMutableData<*就行,在applyChange在根据类型进行改变 76 | diffAdapter.addUpdateMediator(legendBaseInfoData, object : UpdatePayloadFunction> { 77 | override fun providerMatchFeature(input: LegendBaseInfo): Any { 78 | return input.id 79 | } 80 | 81 | override fun applyChange(input: LegendBaseInfo, originalData: BaseMutableData<*>, payloadKeys: MutableSet): BaseMutableData<*> { 82 | Log.d(TAG, "applyChange $input") 83 | return when (originalData) { 84 | is LegendViewData -> { 85 | originalData.legendBaseInfo = input 86 | payloadKeys.add(LegendViewData.KEY_BASE_INFO) 87 | originalData 88 | } 89 | is SkinViewData -> { //不使用payload方式更新 90 | 91 | originalData.legendIcon = input.iconUrl 92 | originalData 93 | } 94 | else -> { 95 | originalData 96 | } 97 | } 98 | } 99 | }) 100 | 101 | //如果变化的数据只需要特定类型的Holder刷新,类型即可指定 102 | diffAdapter.addUpdateMediator(legendPriceData, object : UpdatePayloadFunction { 103 | override fun applyChange( 104 | input: LegendPrice, originalData: LegendViewData, payloadKeys: MutableSet 105 | ): LegendViewData { 106 | originalData.price = input.price 107 | payloadKeys.add(LegendViewData.KEY_PRICE) 108 | return originalData 109 | } 110 | 111 | override fun providerMatchFeature(input: LegendPrice): Any { 112 | Log.d(TAG, "providerMatchFeature ${input.id}") 113 | return input.id 114 | } 115 | }) 116 | 117 | //如果变化的数据只需要特定类型的Holder刷新,类型即可指定 118 | diffAdapter.addUpdateMediator(legendSkinData, object : UpdatePayloadFunction { 119 | override fun providerMatchFeature(input: LegendSkin): Any { 120 | return input.id 121 | } 122 | 123 | override fun applyChange( 124 | input: LegendSkin, originalData: SkinViewData, payloadKeys: MutableSet 125 | ): SkinViewData { 126 | originalData.legendSkin = input 127 | return originalData 128 | } 129 | 130 | }) 131 | } 132 | 133 | override fun onLegendBaseInfoFetched(legendBaseInfo: LegendBaseInfo) { 134 | Log.d(TAG, "onLegendBaseInfoFetched $legendBaseInfo") 135 | legendBaseInfoData.value = legendBaseInfo 136 | } 137 | 138 | override fun onLegendPriceFetched(legendPrice: LegendPrice) { 139 | Log.d(TAG, "onLegendPriceFetched $legendPrice") 140 | legendPriceData.value = legendPrice 141 | } 142 | 143 | override fun onLegendSkinsFetched(legendSkin: LegendSkin) { 144 | Log.d(TAG, "onLegendSkinsFetched $legendSkin") 145 | legendSkinData.value = legendSkin 146 | } 147 | } -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/LinearLayoutManagerWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo 2 | 3 | import android.content.Context 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.support.v7.widget.RecyclerView 6 | 7 | 8 | import android.util.Log 9 | 10 | /** 11 | * Author: rigarsu 12 | * Date: 2018/12/26 13 | * Description: 14 | */ 15 | class LinearLayoutManagerWrapper(context: Context): LinearLayoutManager(context) { 16 | 17 | override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { 18 | try { 19 | super.onLayoutChildren(recycler, state) 20 | } catch (e: Exception) { 21 | Log.e("onLayoutChildren error", e.toString()) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/MethodStackTrace.java: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo; 2 | 3 | 4 | import android.util.Log; 5 | 6 | /** 7 | * 8 | */ 9 | public class MethodStackTrace { 10 | 11 | private MethodStackTrace() { 12 | 13 | } 14 | 15 | public static void printMethodStack(String TAG, String msg) { 16 | 17 | Log.d("MethodStackTrace", msg+"stack"+ getStackMsg(Thread.currentThread().getStackTrace())); 18 | } 19 | 20 | private static String getStackMsg(StackTraceElement[] stackArray) { 21 | StringBuilder sb = new StringBuilder(); 22 | for (int i = 3; i < stackArray.length; i++) { 23 | StackTraceElement element = stackArray[i]; 24 | sb.append(element.toString()).append("\n"); 25 | } 26 | return sb.toString(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/adapter/LegendHolder.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo.adapter 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.View 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import com.silencedut.diffadapter.DiffAdapter 10 | import com.silencedut.diffadapter.holder.BaseDiffViewHolder 11 | import com.silencedut.diffadapterdemo.LegendViewModel 12 | import com.silencedut.diffadapterdemo.R 13 | import com.squareup.picasso.Picasso 14 | 15 | /** 16 | * @author SilenceDut 17 | * @date 2018/12/5 18 | */ 19 | class LegendHolder(itemViewRoot: View, recyclerAdapter: DiffAdapter): BaseDiffViewHolder( itemViewRoot, recyclerAdapter){ 20 | private var legendNameTv : TextView?=null 21 | private var legendIconIv : ImageView?=null 22 | private var legendPriceTv : TextView?=null 23 | companion object { 24 | const val TAG ="LegendHolder" 25 | } 26 | 27 | override fun getItemViewId(): Int { 28 | return LegendViewData.VIEW_ID 29 | } 30 | 31 | init { 32 | legendIconIv = itemViewRoot.findViewById(R.id.legendIcon_iv) 33 | legendNameTv = itemViewRoot.findViewById(R.id.legendName_tv) 34 | legendPriceTv = itemViewRoot.findViewById(R.id.legend_price) 35 | itemView.setOnClickListener { 36 | getViewModel(LegendViewModel::class.java).updateLegendHolder(data.id) 37 | } 38 | } 39 | 40 | override fun updateItem(data: LegendViewData, position: Int) { 41 | Log.d(TAG,"updateItem $data") 42 | updateBaseInfo(data) 43 | updatePrice(data) 44 | } 45 | 46 | private fun updateBaseInfo(data: LegendViewData) { 47 | data.legendBaseInfo?.let { 48 | legendNameTv?.text = it.name 49 | Picasso.get().load(it.iconUrl).into(legendIconIv) 50 | } 51 | } 52 | 53 | @SuppressLint("SetTextI18n") 54 | private fun updatePrice(data: LegendViewData) { 55 | data.price?.let { 56 | legendPriceTv?.text = "¥$it" 57 | } 58 | 59 | } 60 | 61 | /** 62 | * 最高效的更新方式,如果不是频繁更新的可以不实现这个方法 63 | */ 64 | 65 | 66 | override fun updatePartWithPayload(newData: LegendViewData?, payloadKeys: MutableSet, position: Int) { 67 | super.updatePartWithPayload(newData, payloadKeys, position) 68 | Log.d(TAG,"position :"+position+"updatePartWithPayloadKey payload :,data:"+payloadKeys) 69 | if(payloadKeys.contains(LegendViewData.KEY_BASE_INFO)) { 70 | updateBaseInfo(data) 71 | } 72 | if(payloadKeys.contains(LegendViewData.KEY_PRICE)) { 73 | updatePrice(data) 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/adapter/LegendViewData.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo.adapter 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import com.silencedut.core.provider.legend.pojo.LegendBaseInfo 6 | import com.silencedut.diffadapter.data.BaseMutableData 7 | import com.silencedut.diffadapterdemo.R 8 | 9 | /** 10 | * @author SilenceDut 11 | * @date 2018/12/5 12 | */ 13 | data class LegendViewData(var id:Long, var legendBaseInfo: LegendBaseInfo?, var price: Long?) : BaseMutableData() { 14 | 15 | companion object { 16 | const val VIEW_ID = R.layout.holder_legend_introduce 17 | const val KEY_BASE_INFO = "KEY_BASE_INFO" 18 | const val KEY_PRICE = "KEY_PRICE" 19 | } 20 | 21 | 22 | override fun getItemViewId(): Int { 23 | return VIEW_ID 24 | } 25 | 26 | 27 | override fun areUISame(newData: LegendViewData): Boolean { 28 | return this.legendBaseInfo?.id == newData.legendBaseInfo?.id && this.price == newData.price 29 | } 30 | 31 | 32 | override fun appendPayloadKeys(newData: LegendViewData, payloadKeys: MutableSet) { 33 | super.appendPayloadKeys(newData, payloadKeys) 34 | if(this.legendBaseInfo != newData.legendBaseInfo) { 35 | payloadKeys.add(KEY_BASE_INFO) 36 | Log.d("LegendViewData","appendDiffPayload"+KEY_BASE_INFO) 37 | } 38 | if(this.price != newData.price) { 39 | payloadKeys.add(KEY_PRICE) 40 | Log.d("LegendViewData","appendDiffPayload"+KEY_PRICE) 41 | } 42 | } 43 | 44 | override fun uniqueItemFeature(): Any { 45 | return this.id 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/adapter/SkinHolder.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo.adapter 2 | 3 | import android.util.Log 4 | import android.view.View 5 | import android.widget.ImageView 6 | import com.silencedut.diffadapter.DiffAdapter 7 | import com.silencedut.diffadapter.holder.BaseDiffViewHolder 8 | import com.silencedut.diffadapterdemo.LegendViewModel 9 | import com.silencedut.diffadapterdemo.R 10 | import com.squareup.picasso.Picasso 11 | 12 | /** 13 | * @author SilenceDut 14 | * @date 2018/12/5 15 | */ 16 | class SkinHolder(itemView: View, recyclerAdapter: DiffAdapter): BaseDiffViewHolder( itemView, recyclerAdapter){ 17 | private var legendIcon :ImageView?=null 18 | private var legendSkin1 :ImageView?=null 19 | private var legendSkin2 :ImageView?=null 20 | 21 | companion object { 22 | const val TAG = "SkinHolder" 23 | } 24 | 25 | override fun getItemViewId(): Int { 26 | return SkinViewData.VIEW_ID 27 | } 28 | 29 | init { 30 | legendIcon = itemView.findViewById(R.id.legendIcon_iv) 31 | legendSkin1 = itemView.findViewById(R.id.skins1_iv) 32 | legendSkin2 = itemView.findViewById(R.id.skins2_iv) 33 | itemView.setOnClickListener { 34 | getViewModel(LegendViewModel::class.java).updateSkinHolder(data.id) 35 | } 36 | } 37 | 38 | override fun updateItem(data: SkinViewData, position: Int) { 39 | Log.d(TAG,"updateItem ${data.id}") 40 | data.legendIcon?.let { 41 | Picasso.get().load(it).into(legendIcon) 42 | } 43 | data.legendSkin?.let { 44 | when(it.skins.size) { 45 | 1 -> Picasso.get().load(it.skins[0]).into(legendIcon) 46 | 2 ,3,4-> { 47 | Picasso.get().load(it.skins[0]).into(legendSkin1) 48 | Picasso.get().load(it.skins[1]).into(legendSkin2) 49 | } 50 | } 51 | 52 | } 53 | 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/silencedut/diffadapterdemo/adapter/SkinViewData.kt: -------------------------------------------------------------------------------- 1 | package com.silencedut.diffadapterdemo.adapter 2 | 3 | import android.util.Log 4 | import com.silencedut.core.provider.legend.pojo.LegendSkin 5 | import com.silencedut.diffadapter.data.BaseMutableData 6 | import com.silencedut.diffadapterdemo.R 7 | 8 | /** 9 | * @author SilenceDut 10 | * @date 2018/12/5 11 | */ 12 | data class SkinViewData(var id: Long, var legendIcon:String?,var legendSkin: LegendSkin?) : BaseMutableData() { 13 | 14 | companion object { 15 | const val VIEW_ID = R.layout.holder_skins 16 | } 17 | 18 | override fun getItemViewId(): Int { 19 | return VIEW_ID 20 | } 21 | 22 | 23 | //可以用来添加更多的匹配规则,默认以uniqueItemFeature作为匹配规则,默认是uniqueItemFeature() 24 | override fun appendMatchFeature(allMatchFeatures: MutableSet) { 25 | super.appendMatchFeature(allMatchFeatures) 26 | legendIcon?.let { allMatchFeatures.add(it) } 27 | 28 | } 29 | 30 | override fun areUISame(newData: SkinViewData): Boolean { 31 | val uiSame = this.legendIcon == newData.legendIcon && this.legendSkin?.equals(newData.legendSkin)?:false 32 | Log.d("SkinViewData","areUISame $uiSame ${newData.id}") 33 | return uiSame 34 | } 35 | 36 | override fun uniqueItemFeature(): Any { 37 | return this.id 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 22 | 23 |