当前位置:   article > 正文

解锁Android高阶技能,探秘实战Jetpack<十二>-------全面用LiveData+ViewModel+savedState重构之前实现的页面&架构商品详情模块1...

hitabbottomlayout

实战:全面用LiveData+ViewModel+savedState重构之前实现的页面

在上一次https://www.cnblogs.com/webor2006/p/14006380.html已经对于JetPack的核心组件进行了全面细致的学习,并且也将它们应用到了咱们的主APP里面了,这里继续来巩固实操一把,将首页相关数据也进行一个全面改造,为啥要用JetPack的组件进行改造呢?这里一定要明白为啥要使用它们,下面简单的再来回忆一下:

 

也就是利用LiveData可以完全代替EventBus,并且它还比EventBus要强,不用咱们反注册,干嘛不用?

而SavedState它是ViewModel的升级,在内存不足APP被杀时也能保证数据能够复用,所以需要考虑APP被杀数据需要保存的场景用它进行数据恢复是非常合适的。

对于我自己的理解,用它们的原因最重要的是因为是Google力推的,而且都集成到了androidx标准库了,还有啥理由不去拥抱它们呢?

HomePageFragment:基于LiveData+ViewModel+SavedState改造数据请求

1、编写HomeViewModel,把首页Tab相关的所有接口请求挪进去:

目前先来看一下首页Tab数据的请求还是普通的方式:

接下来改成ViewModel,怎么改造呢,其实是一个套路,由于这是第一次改,所以从0开始慢慢来,之后就不会这么详细了,先来移动一下包:

1、然后新建一个HomeViewModel,将首页相关的ViewModel的逻辑都封装于此:

2、让它继承着ViewModel:

根据之前https://www.cnblogs.com/webor2006/p/13956989.htmlViewModel的使用方式来:

3、增加SavedState:

由于咱们想在内存不足app被杀时数据也能够快速得到恢复,所以单凭ViewModel是办不到的,它只能是在配置发生变更时有用,而此时需要使用ViewModel的进阶用法了,回忆一下之前https://www.cnblogs.com/webor2006/p/13993984.html所介绍的这块东东。

所以咱们定义一个对应的构造方法:

而在创建ViewModel的底层实现就会通过这个构造参数传递进去,回忆一下:

 

所以此时咱们就可以使用这个savedState对象进行数据的保存与恢复啦。

4、将请求挪到此类中:

  1. package org.devio.`as`.proj.main.fragment.home
  2. import androidx.lifecycle.SavedStateHandle
  3. import androidx.lifecycle.ViewModel
  4. import org.devio.`as`.proj.main.http.ApiFactory
  5. import org.devio.`as`.proj.main.http.api.HomeApi
  6. import org.devio.`as`.proj.main.model.TabCategory
  7. import org.devio.hi.library.restful.HiCallback
  8. import org.devio.hi.library.restful.HiResponse
  9. /**
  10. * 利用Jetpack中的LiveData+ViewModel+savedState组件进行数据的请求复用
  11. */
  12. class HomeViewModel(private val savedState: SavedStateHandle) : ViewModel() {
  13. fun queryCategoryTabs() {
  14. ApiFactory.create(HomeApi::class.java)
  15. .queryTabList().enqueue(object : HiCallback<List<TabCategory>> {
  16. override fun onSuccess(response: HiResponse<List<TabCategory>>) {
  17. val data = response.data
  18. if (response.successful() && data != null) {
  19. //todo
  20. }
  21. }
  22. override fun onFailed(throwable: Throwable) {
  23. }
  24. })
  25. }

2、使用LiveData发送数据:

而对于结果的监听咱们利用LiveData来进行改造,这个在之前的账户中心信息获取就已经使用过了:

具体如下:

  1. package org.devio.`as`.proj.main.fragment.home
  2. import androidx.lifecycle.LiveData
  3. import androidx.lifecycle.MutableLiveData
  4. import androidx.lifecycle.SavedStateHandle
  5. import androidx.lifecycle.ViewModel
  6. import org.devio.`as`.proj.main.http.ApiFactory
  7. import org.devio.`as`.proj.main.http.api.HomeApi
  8. import org.devio.`as`.proj.main.model.TabCategory
  9. import org.devio.hi.library.restful.HiCallback
  10. import org.devio.hi.library.restful.HiResponse
  11. /**
  12. * 利用Jetpack中的LiveData+ViewModel+savedState组件进行数据的请求复用
  13. */
  14. class HomeViewModel(private val savedState: SavedStateHandle) : ViewModel() {
  15. fun queryCategoryTabs(): LiveData<List<TabCategory>?> {
  16. val liveData = MutableLiveData<List<TabCategory>?>()
  17. //先从savedState中进行获取,如果能获取则直接返回
  18. val memCache = savedState.get<List<TabCategory>?>("categoryTabs")
  19. if (memCache != null) {
  20. liveData.postValue(memCache)
  21. return liveData
  22. }
  23. ApiFactory.create(HomeApi::class.java)
  24. .queryTabList().enqueue(object : HiCallback<List<TabCategory>> {
  25. override fun onSuccess(response: HiResponse<List<TabCategory>>) {
  26. val data = response.data
  27. if (response.successful() && data != null) {
  28. liveData.value = data
  29. savedState.set("categoryTabs", data)
  30. }
  31. }
  32. override fun onFailed(throwable: Throwable) {
  33. //ignore
  34. }
  35. })
  36. return liveData
  37. }

3、调用一下:

另外在updateUI时需要做一下小修改:

所以提取一下:

另外还有一个小细节,就是每次更新UI的这句判断就可以去掉了:

这也是使用Jetpack组件的好处,所以改一下:

至此整个类就已经改造成了,还是比较简单的,下面就依葫芦画瓢将剩下的界面快速改造一把。

HomeTabFragment:基于ViewModel+savedState实现首页Tab初始化数据的内存存储&复用:

将这个请求进行改造,这块就不过多说明了,基本都是套路,将请求的逻辑抽到ViewModel类中:

  1. package org.devio.`as`.proj.main.fragment.home
  2. import androidx.lifecycle.LiveData
  3. import androidx.lifecycle.MutableLiveData
  4. import androidx.lifecycle.SavedStateHandle
  5. import androidx.lifecycle.ViewModel
  6. import org.devio.`as`.proj.main.http.ApiFactory
  7. import org.devio.`as`.proj.main.http.api.HomeApi
  8. import org.devio.`as`.proj.main.model.HomeModel
  9. import org.devio.`as`.proj.main.model.TabCategory
  10. import org.devio.hi.library.restful.HiCallback
  11. import org.devio.hi.library.restful.HiResponse
  12. import org.devio.hi.library.restful.annotation.CacheStrategy
  13. /**
  14. * 利用Jetpack中的LiveData+ViewModel+savedState组件进行数据的请求复用
  15. */
  16. class HomeViewModel(private val savedState: SavedStateHandle) : ViewModel() {
  17. fun queryCategoryTabs(): LiveData<List<TabCategory>?> {
  18. val liveData = MutableLiveData<List<TabCategory>?>()
  19. //先从savedState中进行获取,如果能获取则直接返回
  20. val memCache = savedState.get<List<TabCategory>?>("categoryTabs")
  21. if (memCache != null) {
  22. liveData.postValue(memCache)
  23. return liveData
  24. }
  25. ApiFactory.create(HomeApi::class.java)
  26. .queryTabList().enqueue(object : HiCallback<List<TabCategory>> {
  27. override fun onSuccess(response: HiResponse<List<TabCategory>>) {
  28. val data = response.data
  29. if (response.successful() && data != null) {
  30. liveData.value = data
  31. savedState.set("categoryTabs", data)
  32. }
  33. }
  34. override fun onFailed(throwable: Throwable) {
  35. //ignore
  36. }
  37. })
  38. return liveData
  39. }
  40. fun queryTabCategoryList(
  41. categoryId: String?,
  42. pageIndex: Int,
  43. cacheStrategy: Int
  44. ): LiveData<HomeModel?> {
  45. val liveData = MutableLiveData<HomeModel?>()
  46. val memCache = savedState.get<HomeModel>("categoryList")
  47. //只有是第一次加载时 才需要从内存中取
  48. if (memCache != null && pageIndex == 1 && cacheStrategy == CacheStrategy.CACHE_FIRST) {
  49. liveData.postValue(memCache)
  50. return liveData
  51. }
  52. ApiFactory.create(HomeApi::class.java)
  53. .queryTabCategoryList(cacheStrategy, categoryId!!, pageIndex, 10)
  54. .enqueue(object : HiCallback<HomeModel> {
  55. override fun onSuccess(response: HiResponse<HomeModel>) {
  56. val data = response.data;
  57. if (response.successful() && data != null) {
  58. //一次缓存数据,一次接口数据
  59. liveData.value = data
  60. //只有在刷新的时候,且不是本地缓存的数据 才存储到内容中
  61. if (cacheStrategy != CacheStrategy.NET_ONLY && response.code == HiResponse.SUCCESS) {
  62. savedState.set("categoryList", data)
  63. }
  64. } else {
  65. liveData.postValue(null)
  66. }
  67. }
  68. override fun onFailed(throwable: Throwable) {
  69. liveData.postValue(null)
  70. }
  71. })
  72. return liveData
  73. }
  74. }

然后调用改一下:

 

另外判断生命周期的代码也可以去掉了:

熟悉了套路之后改造起来也非常之快。

CategoryFragment:分类数据请求用LiveData+ViewModel重构

这个页面由于不需要考虑app被杀数据恢复的问题,所以此时就不需要用savedState了,下面快速改一把,这界面涉及到两个请求:

将其抽取到ViewModel中:

  1. package org.devio.`as`.proj.main.fragment.category
  2. import androidx.lifecycle.LiveData
  3. import androidx.lifecycle.MutableLiveData
  4. import androidx.lifecycle.ViewModel
  5. import org.devio.`as`.proj.main.http.ApiFactory
  6. import org.devio.`as`.proj.main.http.api.CategoryApi
  7. import org.devio.`as`.proj.main.model.Subcategory
  8. import org.devio.`as`.proj.main.model.TabCategory
  9. import org.devio.hi.library.restful.HiCallback
  10. import org.devio.hi.library.restful.HiResponse
  11. class CategoryViewModel : ViewModel() {
  12. fun querySubcategoryList(categoryId: String): LiveData<List<Subcategory>?> {
  13. val subcategoryListData = MutableLiveData<List<Subcategory>?>()
  14. ApiFactory.create(CategoryApi::class.java).querySubcategoryList(categoryId)
  15. .enqueue(simpleCallback(subcategoryListData))
  16. return subcategoryListData
  17. }
  18. fun queryCategoryList(): LiveData<List<TabCategory>?> {
  19. val tabCategoryData = MutableLiveData<List<TabCategory>?>()
  20. ApiFactory.create(CategoryApi::class.java).queryCategoryList()
  21. .enqueue(simpleCallback<List<TabCategory>>(tabCategoryData))
  22. return tabCategoryData
  23. }
  24. //回调抽取一下
  25. private fun <T> simpleCallback(liveData: MutableLiveData<T?>): HiCallback<T> {
  26. return object : HiCallback<T> {
  27. override fun onSuccess(response: HiResponse<T>) {
  28. if (response.successful() && response.data != null) {
  29. liveData.postValue(response.data)
  30. } else {
  31. liveData.postValue(null)
  32. }
  33. }
  34. override fun onFailed(throwable: Throwable) {
  35. liveData.postValue(null)
  36. }
  37. }
  38. }
  39. }

调用一下:

  1. package org.devio.`as`.proj.main.fragment.category
  2. import android.graphics.Color
  3. import android.os.Bundle
  4. import android.text.TextUtils
  5. import android.util.SparseIntArray
  6. import android.view.View
  7. import android.widget.ImageView
  8. import android.widget.LinearLayout
  9. import android.widget.TextView
  10. import androidx.lifecycle.Observer
  11. import androidx.lifecycle.ViewModelProvider
  12. import androidx.recyclerview.widget.GridLayoutManager
  13. import kotlinx.android.synthetic.main.fragment_category.*
  14. import org.devio.`as`.proj.common.ui.component.HiBaseFragment
  15. import org.devio.`as`.proj.common.ui.view.EmptyView
  16. import org.devio.`as`.proj.common.ui.view.loadUrl
  17. import org.devio.`as`.proj.main.R
  18. import org.devio.`as`.proj.main.model.Subcategory
  19. import org.devio.`as`.proj.main.model.TabCategory
  20. import org.devio.`as`.proj.main.route.HiRoute
  21. import org.devio.hi.ui.tab.bottom.HiTabBottomLayout
  22. /**
  23. * 商品分类
  24. */
  25. class CategoryFragment : HiBaseFragment() {
  26. private var viewModel: CategoryViewModel? = null
  27. private var emptyView: EmptyView? = null
  28. private val SPAN_COUNT = 3
  29. private val layoutManager = GridLayoutManager(context, SPAN_COUNT)
  30. private val subcategoryList = mutableListOf<Subcategory>()
  31. private val groupSpanSizeOffset = SparseIntArray()
  32. private val decoration = CategoryItemDecoration({ position ->
  33. subcategoryList[position].groupName
  34. }, SPAN_COUNT)
  35. private val subcategoryListCache = mutableMapOf<String, List<Subcategory>>()
  36. override fun getLayoutId(): Int {
  37. return R.layout.fragment_category
  38. }
  39. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  40. super.onViewCreated(view, savedInstanceState)
  41. HiTabBottomLayout.clipBottomPadding(root_container)
  42. content_loading.visibility = View.VISIBLE
  43. viewModel = ViewModelProvider(this).get(CategoryViewModel::class.java)
  44. queryCategoryList()
  45. }
  46. private fun queryCategoryList() {
  47. viewModel?.queryCategoryList()?.observe(viewLifecycleOwner, Observer {
  48. if (it == null) {
  49. showEmptyView()
  50. } else {
  51. onQueryCategoryListSuccess(it)
  52. }
  53. })
  54. }
  55. private fun onQueryCategoryListSuccess(data: List<TabCategory>) {
  56. if (!isAlive) return
  57. emptyView?.visibility = View.GONE
  58. content_loading.visibility = View.GONE
  59. slider_view.visibility = View.VISIBLE
  60. slider_view.bindMenuView(itemCount = data.size,
  61. onBindView = { holder, position ->
  62. val category = data[position]
  63. // holder.menu_item_tilte 无法直接访问
  64. // holder.itemView.menu_item_title. findviewbyid
  65. holder.findViewById<TextView>(R.id.menu_item_title)?.text = category.categoryName
  66. }, onItemClick = { holder, position ->
  67. val category = data[position]
  68. val categoryId = category.categoryId
  69. if (subcategoryListCache.containsKey(categoryId)) {
  70. onQuerySubcategoryListSuccess(subcategoryListCache[categoryId]!!)
  71. } else {
  72. querySubcategoryList(categoryId)
  73. }
  74. })
  75. }
  76. private fun querySubcategoryList(categoryId: String) {
  77. viewModel?.querySubcategoryList(categoryId)?.observe(viewLifecycleOwner, Observer {
  78. if (it != null) {
  79. onQuerySubcategoryListSuccess(it)
  80. if (!subcategoryListCache.containsKey(categoryId)) {
  81. subcategoryListCache.put(categoryId, it)
  82. }
  83. }
  84. })
  85. }
  86. private val spanSizeLookUp = object : GridLayoutManager.SpanSizeLookup() {
  87. override fun getSpanSize(position: Int): Int {
  88. var spanSize = 1
  89. val groupName: String = subcategoryList[position].groupName
  90. val nextGroupName: String? =
  91. if (position + 1 < subcategoryList.size) subcategoryList[position + 1].groupName else null
  92. if (TextUtils.equals(groupName, nextGroupName)) {
  93. spanSize = 1
  94. } else {
  95. //当前位置和 下一个位置 不再同一个分组
  96. //1 .要拿到当前组 position (所在组)在 groupSpanSizeOffset 的索引下标
  97. //2 .拿到 当前组前面一组 存储的 spansizeoffset 偏移量
  98. //3 .给当前组最后一个item 分配 spansize count
  99. val indexOfKey = groupSpanSizeOffset.indexOfKey(position)
  100. val size = groupSpanSizeOffset.size()
  101. val lastGroupOffset = if (size <= 0) 0
  102. else if (indexOfKey >= 0) {
  103. //说明当前组的偏移量记录,已经存在了 groupSpanSizeOffset ,这个情况发生在上下滑动,
  104. if (indexOfKey == 0) 0 else groupSpanSizeOffset.valueAt(indexOfKey - 1)
  105. } else {
  106. //说明当前组的偏移量记录,还没有存在于 groupSpanSizeOffset ,这个情况发生在 第一次布局的时候
  107. //得到前面所有组的偏移量之和
  108. groupSpanSizeOffset.valueAt(size - 1)
  109. }
  110. // 3 - (6 + 5 % 3 )第几列=012
  111. spanSize = SPAN_COUNT - (position + lastGroupOffset) % SPAN_COUNT
  112. if (indexOfKey < 0) {
  113. //得到当前组 和前面所有组的spansize 偏移量之和
  114. val groupOffset = lastGroupOffset + spanSize - 1
  115. groupSpanSizeOffset.put(position, groupOffset)
  116. }
  117. }
  118. return spanSize
  119. }
  120. }
  121. private fun onQuerySubcategoryListSuccess(data: List<Subcategory>) {
  122. if (!isAlive) return
  123. decoration.clear()
  124. groupSpanSizeOffset.clear()
  125. subcategoryList.clear()
  126. subcategoryList.addAll(data)
  127. if (layoutManager.spanSizeLookup != spanSizeLookUp) {
  128. //设置一下sapnSizeLookup
  129. layoutManager.spanSizeLookup = spanSizeLookUp
  130. }
  131. slider_view.bindContentView(
  132. itemCount = data.size,
  133. itemDecoration = decoration,
  134. layoutManager = layoutManager,
  135. onBindView = { holder, position ->
  136. val subcategory = data[position]
  137. holder.findViewById<ImageView>(R.id.content_item_image)
  138. ?.loadUrl(subcategory.subcategoryIcon)
  139. holder.findViewById<TextView>(R.id.content_item_title)?.text =
  140. subcategory.subcategoryName
  141. },
  142. onItemClick = { holder, position ->
  143. //是应该跳转到类目的商品列表页的
  144. val subcategory = data[position]
  145. val bundle = Bundle()
  146. bundle.putString("categoryId", subcategory.categoryId)
  147. bundle.putString("subcategoryId", subcategory.subcategoryId)
  148. bundle.putString("categoryTitle", subcategory.subcategoryName)
  149. HiRoute.startActivity(context!!, bundle, HiRoute.Destination.GOODS_LIST)
  150. }
  151. )
  152. }
  153. private fun showEmptyView() {
  154. if (!isAlive) return
  155. if (emptyView == null) {
  156. emptyView = EmptyView(context!!)
  157. emptyView?.setIcon(R.string.if_empty3)
  158. emptyView?.setDesc(getString(R.string.list_empty_desc))
  159. emptyView?.setButton(getString(R.string.list_empty_action), View.OnClickListener {
  160. queryCategoryList()
  161. })
  162. emptyView?.setBackgroundColor(Color.WHITE)
  163. emptyView?.layoutParams = LinearLayout.LayoutParams(
  164. LinearLayout.LayoutParams.MATCH_PARENT,
  165. LinearLayout.LayoutParams.MATCH_PARENT
  166. )
  167. root_container.addView(emptyView)
  168. }
  169. content_loading.visibility = View.GONE
  170. slider_view.visibility = View.GONE
  171. emptyView?.visibility = View.VISIBLE
  172. }
  173. }

另外对于更新UI的生命周期的判断可以去掉了,涉及到三处:

 

 

整个代码如下:

  1. package org.devio.`as`.proj.main.fragment.category
  2. import android.graphics.Color
  3. import android.os.Bundle
  4. import android.text.TextUtils
  5. import android.util.SparseIntArray
  6. import android.view.View
  7. import android.widget.ImageView
  8. import android.widget.LinearLayout
  9. import android.widget.TextView
  10. import androidx.lifecycle.Observer
  11. import androidx.lifecycle.ViewModelProvider
  12. import androidx.recyclerview.widget.GridLayoutManager
  13. import kotlinx.android.synthetic.main.fragment_category.*
  14. import org.devio.`as`.proj.common.ui.component.HiBaseFragment
  15. import org.devio.`as`.proj.common.ui.view.EmptyView
  16. import org.devio.`as`.proj.common.ui.view.loadUrl
  17. import org.devio.`as`.proj.main.R
  18. import org.devio.`as`.proj.main.model.Subcategory
  19. import org.devio.`as`.proj.main.model.TabCategory
  20. import org.devio.`as`.proj.main.route.HiRoute
  21. import org.devio.hi.ui.tab.bottom.HiTabBottomLayout
  22. /**
  23. * 商品分类
  24. */
  25. class CategoryFragment : HiBaseFragment() {
  26. private var viewModel: CategoryViewModel? = null
  27. private var emptyView: EmptyView? = null
  28. private val SPAN_COUNT = 3
  29. private val layoutManager = GridLayoutManager(context, SPAN_COUNT)
  30. private val subcategoryList = mutableListOf<Subcategory>()
  31. private val groupSpanSizeOffset = SparseIntArray()
  32. private val decoration = CategoryItemDecoration({ position ->
  33. subcategoryList[position].groupName
  34. }, SPAN_COUNT)
  35. private val subcategoryListCache = mutableMapOf<String, List<Subcategory>>()
  36. override fun getLayoutId(): Int {
  37. return R.layout.fragment_category
  38. }
  39. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  40. super.onViewCreated(view, savedInstanceState)
  41. HiTabBottomLayout.clipBottomPadding(root_container)
  42. content_loading.visibility = View.VISIBLE
  43. viewModel = ViewModelProvider(this).get(CategoryViewModel::class.java)
  44. queryCategoryList()
  45. }
  46. private fun queryCategoryList() {
  47. viewModel?.queryCategoryList()?.observe(viewLifecycleOwner, Observer {
  48. if (it == null) {
  49. showEmptyView()
  50. } else {
  51. onQueryCategoryListSuccess(it)
  52. }
  53. })
  54. }
  55. private fun onQueryCategoryListSuccess(data: List<TabCategory>) {
  56. emptyView?.visibility = View.GONE
  57. content_loading.visibility = View.GONE
  58. slider_view.visibility = View.VISIBLE
  59. slider_view.bindMenuView(itemCount = data.size,
  60. onBindView = { holder, position ->
  61. val category = data[position]
  62. // holder.menu_item_tilte 无法直接访问
  63. // holder.itemView.menu_item_title. findviewbyid
  64. holder.findViewById<TextView>(R.id.menu_item_title)?.text = category.categoryName
  65. }, onItemClick = { holder, position ->
  66. val category = data[position]
  67. val categoryId = category.categoryId
  68. if (subcategoryListCache.containsKey(categoryId)) {
  69. onQuerySubcategoryListSuccess(subcategoryListCache[categoryId]!!)
  70. } else {
  71. querySubcategoryList(categoryId)
  72. }
  73. })
  74. }
  75. private fun querySubcategoryList(categoryId: String) {
  76. viewModel?.querySubcategoryList(categoryId)?.observe(viewLifecycleOwner, Observer {
  77. if (it != null) {
  78. onQuerySubcategoryListSuccess(it)
  79. if (!subcategoryListCache.containsKey(categoryId)) {
  80. subcategoryListCache.put(categoryId, it)
  81. }
  82. }
  83. })
  84. }
  85. private val spanSizeLookUp = object : GridLayoutManager.SpanSizeLookup() {
  86. override fun getSpanSize(position: Int): Int {
  87. var spanSize = 1
  88. val groupName: String = subcategoryList[position].groupName
  89. val nextGroupName: String? =
  90. if (position + 1 < subcategoryList.size) subcategoryList[position + 1].groupName else null
  91. if (TextUtils.equals(groupName, nextGroupName)) {
  92. spanSize = 1
  93. } else {
  94. //当前位置和 下一个位置 不再同一个分组
  95. //1 .要拿到当前组 position (所在组)在 groupSpanSizeOffset 的索引下标
  96. //2 .拿到 当前组前面一组 存储的 spansizeoffset 偏移量
  97. //3 .给当前组最后一个item 分配 spansize count
  98. val indexOfKey = groupSpanSizeOffset.indexOfKey(position)
  99. val size = groupSpanSizeOffset.size()
  100. val lastGroupOffset = if (size <= 0) 0
  101. else if (indexOfKey >= 0) {
  102. //说明当前组的偏移量记录,已经存在了 groupSpanSizeOffset ,这个情况发生在上下滑动,
  103. if (indexOfKey == 0) 0 else groupSpanSizeOffset.valueAt(indexOfKey - 1)
  104. } else {
  105. //说明当前组的偏移量记录,还没有存在于 groupSpanSizeOffset ,这个情况发生在 第一次布局的时候
  106. //得到前面所有组的偏移量之和
  107. groupSpanSizeOffset.valueAt(size - 1)
  108. }
  109. // 3 - (6 + 5 % 3 )第几列=012
  110. spanSize = SPAN_COUNT - (position + lastGroupOffset) % SPAN_COUNT
  111. if (indexOfKey < 0) {
  112. //得到当前组 和前面所有组的spansize 偏移量之和
  113. val groupOffset = lastGroupOffset + spanSize - 1
  114. groupSpanSizeOffset.put(position, groupOffset)
  115. }
  116. }
  117. return spanSize
  118. }
  119. }
  120. private fun onQuerySubcategoryListSuccess(data: List<Subcategory>) {
  121. decoration.clear()
  122. groupSpanSizeOffset.clear()
  123. subcategoryList.clear()
  124. subcategoryList.addAll(data)
  125. if (layoutManager.spanSizeLookup != spanSizeLookUp) {
  126. //设置一下sapnSizeLookup
  127. layoutManager.spanSizeLookup = spanSizeLookUp
  128. }
  129. slider_view.bindContentView(
  130. itemCount = data.size,
  131. itemDecoration = decoration,
  132. layoutManager = layoutManager,
  133. onBindView = { holder, position ->
  134. val subcategory = data[position]
  135. holder.findViewById<ImageView>(R.id.content_item_image)
  136. ?.loadUrl(subcategory.subcategoryIcon)
  137. holder.findViewById<TextView>(R.id.content_item_title)?.text =
  138. subcategory.subcategoryName
  139. },
  140. onItemClick = { holder, position ->
  141. //是应该跳转到类目的商品列表页的
  142. val subcategory = data[position]
  143. val bundle = Bundle()
  144. bundle.putString("categoryId", subcategory.categoryId)
  145. bundle.putString("subcategoryId", subcategory.subcategoryId)
  146. bundle.putString("categoryTitle", subcategory.subcategoryName)
  147. HiRoute.startActivity(context!!, bundle, HiRoute.Destination.GOODS_LIST)
  148. }
  149. )
  150. }
  151. private fun showEmptyView() {
  152. if (emptyView == null) {
  153. emptyView = EmptyView(context!!)
  154. emptyView?.setIcon(R.string.if_empty3)
  155. emptyView?.setDesc(getString(R.string.list_empty_desc))
  156. emptyView?.setButton(getString(R.string.list_empty_action), View.OnClickListener {
  157. queryCategoryList()
  158. })
  159. emptyView?.setBackgroundColor(Color.WHITE)
  160. emptyView?.layoutParams = LinearLayout.LayoutParams(
  161. LinearLayout.LayoutParams.MATCH_PARENT,
  162. LinearLayout.LayoutParams.MATCH_PARENT
  163. )
  164. root_container.addView(emptyView)
  165. }
  166. content_loading.visibility = View.GONE
  167. slider_view.visibility = View.GONE
  168. emptyView?.visibility = View.VISIBLE
  169. }
  170. }

ProfileFragment:基于LiveData+ViewModel改造数据请求

还有最后一个页面:

抽到ViewModel当中:

  1. package org.devio.`as`.proj.main.fragment.profile
  2. import androidx.lifecycle.LiveData
  3. import androidx.lifecycle.MutableLiveData
  4. import androidx.lifecycle.ViewModel
  5. import org.devio.`as`.proj.common.BuildConfig
  6. import org.devio.`as`.proj.main.http.ApiFactory
  7. import org.devio.`as`.proj.main.http.api.AccountApi
  8. import org.devio.`as`.proj.main.model.CourseNotice
  9. import org.devio.hi.library.restful.HiCallback
  10. import org.devio.hi.library.restful.HiResponse
  11. class ProfileViewModel : ViewModel() {
  12. fun queryCourseNotice(): LiveData<CourseNotice> {
  13. val noticeData = MutableLiveData<CourseNotice>();
  14. ApiFactory.create(AccountApi::class.java).notice()
  15. .enqueue(object : HiCallback<CourseNotice> {
  16. override fun onSuccess(response: HiResponse<CourseNotice>) {
  17. if (response.data != null && response.data!!.total > 0) {
  18. noticeData.postValue(response.data)
  19. }
  20. }
  21. override fun onFailed(throwable: Throwable) {
  22. //ignore
  23. if (BuildConfig.DEBUG) {
  24. throwable.printStackTrace()
  25. }
  26. }
  27. })
  28. return noticeData
  29. }
  30. }

调用一下:

基于ViewModel+LiveData架构商品详情模块:

目标:

接下来则来构建商品的详情模块,它是电商中至为重要的一个页面,该页面如果做得不好直接导致用户的流失公司收益的减少,所以要做好这么复杂的模块还是巨有一定的挑战的,下面先来看一下整体的大纲:

架构分析:

化整为零:

对于复杂模块得将其进行细分,一点点进行攻破, 先来看一下整个详情的效果:

大致可以看到有如下功能:

1、商品顶部Banner轮播:

 

2、标题滑动渐变效果:

3、商品评价:

4、店铺模块:

5、商品属性展示:

6、商品图片广告长图展示,这个是由多张图展示的,就不多说了,人人皆知的效果。

7、相似商品推荐列表:

8、底部操作区域:

针对这么多列表类型拆分成多个独立的HiDataItem,需求拆分按照数据流入的规则,看一下Item拆解如下:

而这里涉及到的用户交互:

疑难解惑:

  • 评论标签流式布局-ChipGroup,注意滑动复用问题
    通常的写法是会用RecyclerView+FlowLayoutManager来实现,但是咱们这种个数不多,也不存在滑动复用的问题,所有用它来实现有点小题大作了,所以这里会以一种全新的思路进行开发。
  • 商品相册浏览避免滑动抖动,需要提前预设宽高,图片载加成功后再等比计算实际尺寸。
  • 滑动标题栏渐变,需要动态计算白色---透明色的中间颜色值。
  • 页面布局样式:GridLayoutManager(spanCount=2)。
    其实就是指这两个Grid:

    对于这块会再嵌一个RecyclerView,然后将它的spansize设置为3列,而

    而对于它则对整个RecyclerView的GridLayoutManager设置为2列既可。

大厂经验分享【涨姿势】:

这里来看一下像大厂对于这种重量级的详情页可能会用到哪些手段呢,这里纵观一下:

骨架屏:

使用页面加载更加真实,减少等待感。也就是请设计切一张跟详情类似的图片用来先前展示。

predraw:

也就是在接口请求之前使用列表页的数据进行预渲染头部信息,这样在页面一打开时就能看到商品轮播,名称,价格等基础信息, 而当数据成功请求之后再做页面刷新既可,如果使用这种方式那就没必要使用骨架屏了。

Http接口耗时优化:

接口合并。多个Http请求合并到一个总的接口。由服务端做商品详情信息的组装,减少http时延。对于大厂像详情页基本上都是一个接口,不会弄非常多的接口的。

图片加载滑动优化:

由于图片在加载之前是无法知道它的宽高值的,那么在列表滑动时就会出现列表闪动现象,为了解决此类问题,通常会有如下几种解决方案:

  • 产品约定好图片的宽高比(1:1,3:4,9:16),从而根据宽度计算出高度进行展示。
  • 根据Url携带的图片实际尺寸等比计算出视图的宽高,这里需要借助于CDN的能力(比如:https://img.xxx.com/tfs/android-logo-large-720-720.png)。
  • cdn裁剪,根据ImageView的宽高size,往图片Url上拼接尺寸信息(100-100),根据当前设备评分,网络环境拼接quanlity=70图片质量参数。
  • 如果不具备以上条件怎么办?视图添加到列表上之前,设置宽高相等的尺寸,图片下载完成后,等比缩放视图的尺寸。可以有效防止图片加载成功页面闪跳的问题。【咱们要采用的方案】

搭建详情页整体结构:

1、定义详情API:

其数据格式比较复杂:

下面来定义一下:

  1. package org.devio.`as`.proj.main.http.api
  2. import org.devio.`as`.proj.main.model.DetailModel
  3. import org.devio.hi.library.restful.HiCall
  4. import org.devio.hi.library.restful.annotation.GET
  5. import org.devio.hi.library.restful.annotation.Path
  6. interface DetailApi {
  7. @GET("goods/detail/{id}")
  8. fun queryDetail(@Path("id") goodsId: String): HiCall<DetailModel>
  9. }

其中需要定义一下DetailModel数据模型:

  1. package org.devio.`as`.proj.main.model
  2. data class DetailModel(
  3. val categoryId: String,
  4. val commentCountTitle: String,
  5. val commentModels: List<CommentModel>?,
  6. val commentTags: String,
  7. val completedNumText: String,
  8. val createTime: String,
  9. val flowGoods: List<GoodsModel>?,
  10. val gallery: List<SliderImage>?,
  11. val goodAttr: List<MutableMap<String, String>>?,
  12. val goodDescription: String,
  13. val goodsId: String,
  14. val goodsName: String,
  15. val isFavorite: Boolean,
  16. val groupPrice: String,
  17. val hot: Boolean,
  18. val marketPrice: String,
  19. val shop: Shop,
  20. val similarGoods: List<GoodsModel>?,
  21. val sliderImage: String,
  22. val sliderImages: List<SliderImage>?,
  23. val tags: String
  24. )
  25. data class CommentModel(
  26. val avatar: String,
  27. val content: String,
  28. val nickName: String
  29. )
  30. data class Shop(
  31. val completedNum: String,
  32. val evaluation: String,
  33. val goodsNum: String,
  34. val logo: String,
  35. val name: String
  36. )
  37. data class Favorite(val goodsId: String, var isFavorite: Boolean)

2、新建Activity:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import android.os.Bundle
  3. import com.alibaba.android.arouter.facade.annotation.Route
  4. import org.devio.`as`.proj.common.ui.component.HiBaseActivity
  5. import org.devio.`as`.proj.main.R
  6. /**
  7. * 商品详情页
  8. */
  9. @Route(path = "/detail/main")
  10. class DetailActivity : HiBaseActivity() {
  11. override fun onCreate(savedInstanceState: Bundle?) {
  12. super.onCreate(savedInstanceState)
  13. setContentView(R.layout.activity_detail)
  14. }
  15. }

3、准备布局:

这里采用约束布局,重要的页面能尽量减少布局的嵌套尽量减少, 关于布局这块只对关键处进行说明,因为这块基本上都比较熟了,

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:id="@+id/root_container"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent"
  8. android:background="@color/color_eee">
  9. <androidx.recyclerview.widget.RecyclerView
  10. android:id="@+id/recycler_view"
  11. android:layout_width="match_parent"
  12. android:layout_height="0dp"
  13. android:overScrollMode="never"
  14. app:layout_constraintBottom_toTopOf="@+id/bottom_layout"
  15. app:layout_constraintLeft_toLeftOf="parent"
  16. app:layout_constraintTop_toTopOf="parent" />
  17. <!-- 底部操作栏 -->
  18. <LinearLayout
  19. android:id="@+id/bottom_layout"
  20. android:layout_width="match_parent"
  21. android:layout_height="58dp"
  22. android:background="@color/color_white"
  23. android:orientation="horizontal"
  24. app:layout_constraintBottom_toBottomOf="parent"
  25. app:layout_constraintLeft_toLeftOf="parent">
  26. <org.devio.as.proj.common.ui.view.IconFontTextView
  27. android:id="@+id/action_favorite"
  28. android:layout_width="0dp"
  29. android:layout_height="match_parent"
  30. android:layout_weight="1"
  31. android:gravity="center"
  32. android:text="&#xe60e;\n收藏"
  33. android:textColor="@color/color_999"
  34. android:textSize="@dimen/sp_14" />
  35. <TextView
  36. android:id="@+id/action_order"
  37. android:layout_width="0dp"
  38. android:layout_height="match_parent"
  39. android:layout_weight="1"
  40. android:background="@color/color_de3"
  41. android:gravity="center"
  42. android:textColor="@color/color_white"
  43. android:textSize="@dimen/sp_14"
  44. tools:text="¥29元\n现在购买" />
  45. </LinearLayout>
  46. </androidx.constraintlayout.widget.ConstraintLayout>

其中这块标红处需要说明一下,它需要设置成0dp:

注意:它不能是match_parent,因为它会充满整个高度:

接下来准备标题:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:id="@+id/root_container"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent"
  8. android:background="@color/color_eee">
  9. <!-- 标题栏 -->
  10. <FrameLayout
  11. android:id="@+id/title_bar"
  12. android:layout_width="match_parent"
  13. android:layout_height="70dp"
  14. android:fitsSystemWindows="true"
  15. app:layout_constraintRight_toLeftOf="parent"
  16. app:layout_constraintTop_toTopOf="parent">
  17. <org.devio.as.proj.common.ui.view.IconFontTextView
  18. android:id="@+id/action_back"
  19. android:layout_width="wrap_content"
  20. android:layout_height="wrap_content"
  21. android:gravity="center"
  22. android:paddingLeft="12dp"
  23. android:paddingTop="@dimen/dp_5"
  24. android:paddingRight="12dp"
  25. android:paddingBottom="@dimen/dp_5"
  26. android:text="@string/if_back"
  27. android:textSize="18sp" />
  28. <org.devio.as.proj.common.ui.view.IconFontTextView
  29. android:id="@+id/action_share"
  30. android:layout_width="wrap_content"
  31. android:layout_height="wrap_content"
  32. android:layout_gravity="right"
  33. android:gravity="center"
  34. android:paddingLeft="12dp"
  35. android:paddingTop="@dimen/dp_5"
  36. android:paddingRight="12dp"
  37. android:paddingBottom="@dimen/dp_5"
  38. android:text="@string/if_share"
  39. android:textSize="18sp" />
  40. </FrameLayout>
  41. <androidx.recyclerview.widget.RecyclerView
  42. android:id="@+id/recycler_view"
  43. android:layout_width="match_parent"
  44. android:layout_height="match_parent"
  45. android:overScrollMode="never"
  46. app:layout_constraintBottom_toTopOf="@+id/bottom_layout"
  47. app:layout_constraintLeft_toLeftOf="parent"
  48. app:layout_constraintTop_toTopOf="parent" />
  49. <!-- 底部操作栏 -->
  50. <LinearLayout
  51. android:id="@+id/bottom_layout"
  52. android:layout_width="match_parent"
  53. android:layout_height="58dp"
  54. android:background="@color/color_white"
  55. android:orientation="horizontal"
  56. app:layout_constraintBottom_toBottomOf="parent"
  57. app:layout_constraintLeft_toLeftOf="parent">
  58. <org.devio.as.proj.common.ui.view.IconFontTextView
  59. android:id="@+id/action_favorite"
  60. android:layout_width="0dp"
  61. android:layout_height="match_parent"
  62. android:layout_weight="1"
  63. android:gravity="center"
  64. android:text="&#xe60e;\n收藏"
  65. android:textColor="@color/color_999"
  66. android:textSize="@dimen/sp_14" />
  67. <TextView
  68. android:id="@+id/action_order"
  69. android:layout_width="0dp"
  70. android:layout_height="match_parent"
  71. android:layout_weight="1"
  72. android:background="@color/color_de3"
  73. android:gravity="center"
  74. android:textColor="@color/color_white"
  75. android:textSize="@dimen/sp_14"
  76. tools:text="¥29元\n现在购买" />
  77. </LinearLayout>
  78. </androidx.constraintlayout.widget.ConstraintLayout>

其中由于需要沉浸式的效果,这里给标题View增加了一个这个属性:

4、设置状态栏风格:

5、注入Arouter:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import android.graphics.Color
  3. import android.os.Bundle
  4. import android.text.TextUtils
  5. import com.alibaba.android.arouter.facade.annotation.Autowired
  6. import com.alibaba.android.arouter.facade.annotation.Route
  7. import org.devio.`as`.proj.common.ui.component.HiBaseActivity
  8. import org.devio.`as`.proj.main.R
  9. import org.devio.`as`.proj.main.model.GoodsModel
  10. import org.devio.`as`.proj.main.route.HiRoute
  11. import org.devio.hi.library.util.HiStatusBar
  12. /**
  13. * 商品详情页
  14. */
  15. @Route(path = "/detail/main")
  16. class DetailActivity : HiBaseActivity() {
  17. @JvmField
  18. @Autowired
  19. var goodsId: String? = null
  20. /*此字段是用来提前加载商品轮播及基础信息用的*/
  21. @JvmField
  22. @Autowired
  23. var goodsModel: GoodsModel? = null
  24. override fun onCreate(savedInstanceState: Bundle?) {
  25. super.onCreate(savedInstanceState)
  26. HiStatusBar.setStatusBar(this, true, statusBarColor = Color.TRANSPARENT, translucent = true)
  27. HiRoute.inject(this)
  28. assert(!TextUtils.isEmpty(goodsId)) { " goodsId must bot be null" }
  29. setContentView(R.layout.activity_detail)
  30. }
  31. }

6、initView():

 

7、数据请求API准备:

此时则需要使用ViewModel了,新建一个类:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import androidx.lifecycle.ViewModel
  3. class DetailViewModel() : ViewModel() {
  4. }

对于ViewModel的使用目前也已经比较熟悉了,不过这里用一种自定义参数的方式演练一把,对于ViewModel带参数不是正常只支持这两种嘛:

用的都是系统的参数,但是!!!有些情况可以需要携带一些自定义的参数,就比如此时此刻,对于详情的请求需要有一个goodsId,所以看一下这时该怎么来定义ViewModel:

此时需要指定一下创建工厂,因为我们在获得ViewModel时可以指定一个factory:

而工厂的编写方法可以参数系统创建的思路:

所以咱们可以这样定义:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import androidx.lifecycle.ViewModel
  3. import androidx.lifecycle.ViewModelProvider
  4. import androidx.lifecycle.ViewModelStoreOwner
  5. class DetailViewModel(val goodsId: String?) : ViewModel() {
  6. companion object {
  7. private class DetailViewModelFactory(val goodsId: String?) :
  8. ViewModelProvider.NewInstanceFactory() {
  9. override fun <T : ViewModel?> create(modelClass: Class<T>): T {
  10. try {
  11. val constructor = modelClass.getConstructor(String::class.java)
  12. if (constructor != null) {
  13. return constructor.newInstance(goodsId)
  14. }
  15. } catch (exception: Exception) {
  16. //ignore
  17. }
  18. //如果发生异常,则直接用降级方案由父类进行创建
  19. return super.create(modelClass)
  20. }
  21. }
  22. fun get(goodsId: String?, viewModelStoreOwner: ViewModelStoreOwner): DetailViewModel {
  23. return ViewModelProvider(viewModelStoreOwner, DetailViewModelFactory(goodsId)).get(
  24. DetailViewModel::class.java
  25. )
  26. }
  27. }
  28. }

其中标红的可以看一下父类的创建行为:

接下来定义请求方法:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import android.text.TextUtils
  3. import androidx.lifecycle.*
  4. import com.alibaba.android.arouter.BuildConfig
  5. import org.devio.`as`.proj.main.http.ApiFactory
  6. import org.devio.`as`.proj.main.http.api.DetailApi
  7. import org.devio.`as`.proj.main.model.DetailModel
  8. import org.devio.hi.library.restful.HiCallback
  9. import org.devio.hi.library.restful.HiResponse
  10. class DetailViewModel(val goodsId: String?) : ViewModel() {
  11. companion object {
  12. private class DetailViewModelFactory(val goodsId: String?) :
  13. ViewModelProvider.NewInstanceFactory() {
  14. override fun <T : ViewModel?> create(modelClass: Class<T>): T {
  15. try {
  16. val constructor = modelClass.getConstructor(String::class.java)
  17. if (constructor != null) {
  18. return constructor.newInstance(goodsId)
  19. }
  20. } catch (exception: Exception) {
  21. //ignore
  22. }
  23. //如果发生异常,则直接用降级方案由父类进行创建
  24. return super.create(modelClass)
  25. }
  26. }
  27. fun get(goodsId: String?, viewModelStoreOwner: ViewModelStoreOwner): DetailViewModel {
  28. return ViewModelProvider(viewModelStoreOwner, DetailViewModelFactory(goodsId)).get(
  29. DetailViewModel::class.java
  30. )
  31. }
  32. }
  33. fun queryDetailData(): LiveData<DetailModel?> {
  34. val pageData = MutableLiveData<DetailModel?>()
  35. if (!TextUtils.isEmpty(goodsId)) {
  36. ApiFactory.create(DetailApi::class.java).queryDetail(goodsId!!)
  37. .enqueue(object : HiCallback<DetailModel> {
  38. override fun onSuccess(response: HiResponse<DetailModel>) {
  39. if (response.successful() && response.data != null) {
  40. pageData.postValue(response.data)
  41. } else {
  42. pageData.postValue(null)
  43. }
  44. }
  45. override fun onFailed(throwable: Throwable) {
  46. pageData.postValue(null)
  47. if (BuildConfig.DEBUG) {
  48. throwable.printStackTrace()
  49. }
  50. }
  51. })
  52. }
  53. return pageData
  54. }
  55. }

8、发起请求:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import android.graphics.Color
  3. import android.os.Bundle
  4. import android.text.TextUtils
  5. import android.view.View
  6. import androidx.constraintlayout.widget.ConstraintLayout
  7. import androidx.lifecycle.Observer
  8. import androidx.recyclerview.widget.GridLayoutManager
  9. import com.alibaba.android.arouter.facade.annotation.Autowired
  10. import com.alibaba.android.arouter.facade.annotation.Route
  11. import kotlinx.android.synthetic.main.activity_detail.*
  12. import org.devio.`as`.proj.common.ui.component.HiBaseActivity
  13. import org.devio.`as`.proj.common.ui.view.EmptyView
  14. import org.devio.`as`.proj.main.R
  15. import org.devio.`as`.proj.main.model.DetailModel
  16. import org.devio.`as`.proj.main.model.GoodsModel
  17. import org.devio.`as`.proj.main.route.HiRoute
  18. import org.devio.hi.library.util.HiStatusBar
  19. import org.devio.hi.ui.item.HiAdapter
  20. /**
  21. * 商品详情页
  22. */
  23. @Route(path = "/detail/main")
  24. class DetailActivity : HiBaseActivity() {
  25. private lateinit var viewModel: DetailViewModel
  26. private var emptyView: EmptyView? = null
  27. @JvmField
  28. @Autowired
  29. var goodsId: String? = null
  30. /*此字段是用来提前加载商品轮播及基础信息用的*/
  31. @JvmField
  32. @Autowired
  33. var goodsModel: GoodsModel? = null
  34. override fun onCreate(savedInstanceState: Bundle?) {
  35. super.onCreate(savedInstanceState)
  36. HiStatusBar.setStatusBar(this, true, statusBarColor = Color.TRANSPARENT, translucent = true)
  37. HiRoute.inject(this)
  38. assert(!TextUtils.isEmpty(goodsId)) { " goodsId must bot be null" }
  39. setContentView(R.layout.activity_detail)
  40. initView()
  41. queryDetailData()
  42. }
  43. private fun initView() {
  44. action_back.setOnClickListener { onBackPressed() }
  45. action_share.setOnClickListener {
  46. showToast("share,not support for now.")
  47. }
  48. /* 这里的Grid全局设置成2列 */
  49. recycler_view.layoutManager = GridLayoutManager(this, 2)
  50. recycler_view.adapter = HiAdapter(this)
  51. }
  52. private fun queryDetailData() {
  53. viewModel = DetailViewModel.get(goodsId, this)
  54. viewModel.queryDetailData().observe(this, Observer {
  55. if (it == null) {
  56. showEmptyView()
  57. } else {
  58. bindData(it)
  59. }
  60. })
  61. }
  62. private fun bindData(detailModel: DetailModel) {
  63. TODO("Not yet implemented")
  64. }
  65. //还是采用动态添加的方式来实现空View,因为大多数情况下是用不到的,为了提高布局性能
  66. private fun showEmptyView() {
  67. if (emptyView == null) {
  68. emptyView = EmptyView(this)
  69. emptyView!!.setIcon(R.string.if_empty3)
  70. emptyView!!.setDesc(getString(R.string.list_empty_desc))
  71. emptyView!!.layoutParams = ConstraintLayout.LayoutParams(-1, -1)
  72. emptyView!!.setBackgroundColor(Color.WHITE)
  73. emptyView!!.setButton(getString(R.string.list_empty_action), View.OnClickListener {
  74. viewModel.queryDetailData()
  75. })
  76. root_container.addView(emptyView)
  77. }
  78. recycler_view.visibility = View.GONE
  79. emptyView!!.visibility = View.VISIBLE
  80. }
  81. }

基于HiBanner+HiDataItem实现列表主图轮播:

接下来则来绑定数据到RecyclerView上面了。

1、准备商品轮播头部Item布局:

由于商品头部又有相对嵌套的关系,为了减少布局层级还是使用约束根布局来搭建,具体布局细节就不过多描述了,只针对关键点进行说明:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:layout_width="match_parent"
  6. android:layout_height="wrap_content"
  7. android:layout_marginBottom="@dimen/dp_10"
  8. android:background="@color/color_white"
  9. android:paddingBottom="@dimen/dp_10">
  10. <org.devio.hi.ui.banner.HiBanner
  11. android:id="@+id/hi_banner"
  12. android:layout_width="match_parent"
  13. android:layout_height="360dp"
  14. app:autoPlay="true"
  15. app:layout_constraintLeft_toLeftOf="parent"
  16. app:layout_constraintTop_toTopOf="parent"
  17. app:loop="true"
  18. tools:background="@color/colorAccent" />
  19. <TextView
  20. android:id="@+id/price"
  21. android:layout_width="wrap_content"
  22. android:layout_height="wrap_content"
  23. android:layout_marginLeft="@dimen/dp_10"
  24. android:layout_marginTop="@dimen/dp_20"
  25. android:textColor="@color/color_d43"
  26. android:textSize="@dimen/sp_14"
  27. android:textStyle="bold"
  28. app:layout_constraintLeft_toLeftOf="parent"
  29. app:layout_constraintTop_toBottomOf="@+id/hi_banner"
  30. tools:text="¥100" />
  31. <TextView
  32. android:id="@+id/sale_desc"
  33. android:layout_width="wrap_content"
  34. android:layout_height="wrap_content"
  35. android:layout_marginTop="@dimen/dp_20"
  36. android:layout_marginRight="@dimen/dp_10"
  37. android:textColor="@color/color_9b9"
  38. android:textSize="@dimen/sp_12"
  39. app:layout_constraintRight_toRightOf="parent"
  40. app:layout_constraintTop_toBottomOf="@+id/hi_banner"
  41. tools:text="已拼100件" />
  42. <TextView
  43. android:id="@+id/title"
  44. android:layout_width="match_parent"
  45. android:layout_height="wrap_content"
  46. android:layout_marginLeft="@dimen/dp_10"
  47. android:layout_marginTop="6dp"
  48. android:layout_marginRight="@dimen/dp_10"
  49. android:ellipsize="end"
  50. android:maxLines="2"
  51. android:textColor="@color/color_000"
  52. android:textSize="14sp"
  53. android:textStyle="bold"
  54. app:layout_constraintLeft_toLeftOf="parent"
  55. app:layout_constraintTop_toBottomOf="@+id/price"
  56. tools:text="移动端架构师成长体系课谁学谁知道\n\n移动端架构师成长体系课谁学谁知道" />
  57. </androidx.constraintlayout.widget.ConstraintLayout>

预览如下:

2、绑定Item数据:

根据咱们之前https://www.cnblogs.com/webor2006/p/13607431.html封装的HiDataItem,新建一个对应的Item:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import android.widget.ImageView
  3. import kotlinx.android.synthetic.main.layout_detail_item_header.*
  4. import org.devio.`as`.proj.common.ui.view.loadUrl
  5. import org.devio.`as`.proj.main.R
  6. import org.devio.`as`.proj.main.model.DetailModel
  7. import org.devio.`as`.proj.main.model.SliderImage
  8. import org.devio.hi.ui.banner.core.HiBannerAdapter
  9. import org.devio.hi.ui.banner.core.HiBannerModel
  10. import org.devio.hi.ui.banner.indicator.HiNumIndicator
  11. import org.devio.hi.ui.item.HiDataItem
  12. import org.devio.hi.ui.item.HiViewHolder
  13. class HeaderItem(
  14. val sliderImages: List<SliderImage>?,
  15. val price: String?,
  16. val completedNumText: String?,
  17. val goodsName: String?
  18. ) : HiDataItem<DetailModel, HiViewHolder>() {
  19. override fun onBindData(holder: HiViewHolder, position: Int) {
  20. val context = holder.itemView.context ?: return
  21. val bannerItems = arrayListOf<HiBannerModel>()
  22. sliderImages?.forEach {//将其转换成Banner对应的Model
  23. val bannerMo = object : HiBannerModel() {}
  24. bannerMo.url = it.url
  25. bannerItems.add(bannerMo)
  26. }
  27. holder.hi_banner.setHiIndicator(HiNumIndicator(context))
  28. holder.hi_banner.setBannerData(bannerItems)
  29. holder.hi_banner.setBindAdapter { viewHolder: HiBannerAdapter.HiBannerViewHolder?, mo: HiBannerModel?, position: Int ->
  30. val imageView = viewHolder?.rootView as? ImageView
  31. mo?.let { imageView?.loadUrl(it.url) }
  32. }
  33. }
  34. override fun getItemLayoutRes(): Int {
  35. return R.layout.layout_detail_item_header
  36. }
  37. }

其中目前报错了:

因为HiDataItem的构造中有一个参数需要赋值,看一下:

其它这个data参数木有用到,所以将其给一个默认的值既可

所以下而来搞一下:

然后再将剩一下的View进行数据绑定,没啥好说的:

  1. package org.devio.`as`.proj.main.biz.detail
  2. import android.text.SpannableString
  3. import android.text.Spanned
  4. import android.text.TextUtils
  5. import android.text.style.AbsoluteSizeSpan
  6. import android.widget.ImageView
  7. import kotlinx.android.synthetic.main.layout_detail_item_header.*
  8. import org.devio.`as`.proj.common.ui.view.loadUrl
  9. import org.devio.`as`.proj.main.R
  10. import org.devio.`as`.proj.main.model.DetailModel
  11. import org.devio.`as`.proj.main.model.SliderImage
  12. import org.devio.hi.ui.banner.core.HiBannerAdapter
  13. import org.devio.hi.ui.banner.core.HiBannerModel
  14. import org.devio.hi.ui.banner.indicator.HiNumIndicator
  15. import org.devio.hi.ui.item.HiDataItem
  16. import org.devio.hi.ui.item.HiViewHolder
  17. class HeaderItem(
  18. val sliderImages: List<SliderImage>?,
  19. val price: String?,
  20. val completedNumText: String?,
  21. val goodsName: String?
  22. ) : HiDataItem<DetailModel, HiViewHolder>() {
  23. override fun onBindData(holder: HiViewHolder, position: Int) {
  24. val context = holder.itemView.context ?: return
  25. val bannerItems = arrayListOf<HiBannerModel>()
  26. sliderImages?.forEach {
  27. val bannerMo = object : HiBannerModel() {}
  28. bannerMo.url = it.url
  29. bannerItems.add(bannerMo)
  30. }
  31. holder.hi_banner.setHiIndicator(HiNumIndicator(context))
  32. holder.hi_banner.setBannerData(bannerItems)
  33. holder.hi_banner.setBindAdapter { viewHolder: HiBannerAdapter.HiBannerViewHolder?, mo: HiBannerModel?, position: Int ->
  34. val imageView = viewHolder?.rootView as? ImageView
  35. mo?.let { imageView?.loadUrl(it.url) }
  36. }
  37. holder.price.text = spanPrice(price)
  38. holder.sale_desc.text = completedNumText
  39. holder.title.text = goodsName
  40. }
  41. /**
  42. * 设置价格的富文本
  43. */
  44. private fun spanPrice(price: String?): CharSequence {
  45. if (TextUtils.isEmpty(price)) return ""
  46. val ss = SpannableString(price)
  47. ss.setSpan(AbsoluteSizeSpan(18, true), 1, ss.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  48. return ss
  49. }
  50. override fun getItemLayoutRes(): Int {
  51. return R.layout.layout_detail_item_header
  52. }
  53. }

3、DetailActivity.bindData()添加到HiAdapter中:

其中标红的商品价格有情况需要说明一下,先看一下它们的数据形态:

marketPrice金额前面有¥符号,而groupPrice木有,而且marketPrice有可能为空,此时就应该显示成groupPrice,所以。。咱们需要封装一下价格的显示:

另外还需要预加载一下,对于有传goodsModel的情况下,这样可以一进界面就可以看到商品的头倍信息,增加用户体验:

这里有一个细节需要提一下:

因为在preBindData()时其布局都还没有完成,此时调用它recyclerView的notify()肯定就会抛异常的,看一下调用的方法就知道了:

 

4、增加跳转事件:

给首页Banner和商品列表增加一下跳转事件:

其中在ARouter中增加一个页面映射:

另外对于商品列表的点击也得加一下事件:

不过此时报错了,是因为GoodsModel木有实现Parcelable接口,不是往Bundle可以传序列号对象么?是的,但是!!这里是要学习一下在Koltin中使用Parcelable跟在Java中使用有啥不一样,感受一下:

其实在Kotlin中使用Parcelable非常之简单,只要再加一个注解既可:

然后就可以了,是不是超赞,回想一下Java中的写法,不要太清爽哦~~

5、运行:

报错了。。

其实是咱们字段定义没有允许为空造成,如下:

然后GoodsItem这块得判空了:

再运行:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/259927
推荐阅读
相关标签
  

闽ICP备14008679号