Android中构建多视图 RecyclerView的正确打开方式

简介

漂亮的UI能极大提高用户留存率,相反糟糕的UI将导致App安装率下降。

图片
漂亮的UI设计

UI体验对用户留存率有特别大的影响,较差的体验app我可能用不了2s就要卸载掉。

图片
图片

你需要学习内容如下:

  1. 1. 使用单个RecyclerView来处理多种视图类型
  2. 2. 整洁的代码 – MVVM架构
  3. 3. 显示来自外部API的数据
  4. 4. 使用Motion Layout进行动画

完整的代码: https://github.com/ibrajix/NftApp

DATA

我们使用了一个外部API,我通过https://mockapi.io/ 进行了模拟。

图片

1、2、3和4是上图显示的布局的一部分,可以是动态的。因此,我们将为每个创建一个布局项。请查看完整代码以获取各种RecyclerView布局文件。

NftData.kt

sealed class NftData {

    class Title(
        val id: Int,
        val title: String,
        val viewAll: String,
    ) : NftData()

    class Featured(
        val image: String,
        val title: String
    ) : NftData()

    class Top(
        val id: Int,
        val image: String
    ) : NftData()

    class Trending(
        val id: Int,
        val image: String,
        val name: String,
        val category: String
    ) : NftData()

}

RETROFIT

我们正在使用retrofit来向外部 API 发送请求。 ApiService.kt

interface ApiService {

    //get top nft
    @GET(EndPoints.TOP_NFT)
    suspend fun getTopNft() : List<NftData.Top>

    //get trending nft
    @GET(EndPoints.TRENDING_NFT)
    suspend fun getTrendingNft() : List<NftData.Trending>

}

ApiDataSource.kt

class ApiDataSource @Inject constructor(private val apiService: ApiService) {

    //get top nft
    suspend fun getTopNft() = apiService.getTopNft()

    //get trending nft
    suspend fun getTrendingNft() = apiService.getTrendingNft()

}

Repository

正如你所看到的,我正在使用Hilt进行依赖注入,以注入所需的类。请查看NetworkModule.kt的完整代码,了解我如何提供所需的Retrofit类和依赖项。

NftRepository.kt

class NftRepository @Inject constructor(private val apiDataSource: ApiDataSource) : SafeApiCall {

    suspend fun getTopNft() = safeApiCall { apiDataSource.getTopNft() }
    suspend fun getTrendingNft() = safeApiCall { apiDataSource.getTrendingNft() }

}

Recyclerview

一个RecyclerView需要一个ViewHolder和一个Adapter。这个应用程序由一个单独的RecyclerView组成,请检查activity_main.xml文件。

NftViewHolder.kt

  • • 这将是一个密封类,因为我们希望对继承有更多控制。
  • • 我们将使用viewBinding与每个RecyclerView布局文件进行交互。
  • • 我使用一个叫做coil的库从外部API中加载图像,同时还提供了一些转换(例如圆角等)(再见Glide)。
sealed class NftViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {

    var itemClickListener: ((view: View, item: NftData, position: Int) -> Unit)? = null

    class TitleViewHolder(private val binding: RcvLytTitleBinding) : NftViewHolder(binding){
        fun bind(title: NftData.Title) {
            binding.txtFeatured.text = title.title
            binding.txtViewAll.text = title.viewAll
            binding.txtViewAll.setOnClickListener {
                itemClickListener?.invoke(it, title, adapterPosition)
            }
        }
    }

    class FeaturedViewHolder(private val binding: RcvLytFeaturedBinding) : NftViewHolder(binding){
        fun bind(featured: NftData.Featured){
            binding.imgFeatured.load(FEATURED_IMAGE){
                crossfade(true)
                transformations(RoundedCornersTransformation(20F))
            }
            binding.imgFeatured.setOnClickListener {
                itemClickListener?.invoke(it, featured, adapterPosition)
            }
            binding.txtFeaturedTitle.text = FEATURED_IMAGE_TITLE
        }
    }

    class TopPicksViewHolder(private val binding: RcvLytTopPicksBinding) : NftViewHolder(binding){
        fun bind(topPicks: NftData.Top){
          binding.imgTopPicks.load(topPicks.image){
             crossfade(true)
             transformations(RoundedCornersTransformation(20F))
          }
          binding.imgTopPicks.setOnClickListener {
              itemClickListener?.invoke(it, topPicks, adapterPosition)
          }
        }
    }

    class TrendingViewHolder(private val binding: RcvLytTrendingBinding) : NftViewHolder(binding){
        fun bind(trending: NftData.Trending){

            binding.imgTrending.load(trending.image){
                crossfade(true)
                transformations(CircleCropTransformation())
            }

            binding.topNftContainer.setOnClickListener {
                itemClickListener?.invoke(it, trending, adapterPosition)
            }

            binding.txtNftTitle.text = trending.name
            binding.txtCategory.text = trending.category
        }
    }

}

NftAdapter.kt

  • • 我们的适配器继承ListAdapter类,这是现在推荐的方法。
  • • 我们使用DiffUtil来避免使用recyclerview的adapter的notifyDataSetChanged(),因为当可能只有几件事情发生变化时,它重新绘制整个UI是不高效的。
  • • onCreateViewHolder():检查存在的视图类型并填充相应的布局文件。
  • • onBindViewHolder():根据viewHolder与视图绑定数据。
  • • getItemViewType():如名称所示,在recyclerview中确定在特定位置显示哪种类型的视图。

NftAdapter.kt

class NftAdapter : ListAdapter<NftData, NftViewHolder>(NftDiffCallBack()) {

    var itemClickListener: ((view: View, item: NftData, position: Int) -> Unit)? = null


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NftViewHolder {
       return when(viewType){
            R.layout.rcv_lyt_title -> NftViewHolder.TitleViewHolder(
                RcvLytTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false
                )
            )
           R.layout.rcv_lyt_featured -> NftViewHolder.FeaturedViewHolder(
               RcvLytFeaturedBinding.inflate(LayoutInflater.from(parent.context), parent, false
               )
           )
           R.layout.rcv_lyt_top_picks -> NftViewHolder.TopPicksViewHolder(
               RcvLytTopPicksBinding.inflate(LayoutInflater.from(parent.context), parent, false
               )
           )
           R.layout.rcv_lyt_trending -> NftViewHolder.TrendingViewHolder(
               RcvLytTrendingBinding.inflate(LayoutInflater.from(parent.context), parent, false
               )
           )
           else -> throw IllegalArgumentException("Invalid view type")
       }
    }

    override fun onBindViewHolder(holder: NftViewHolder, position: Int) {

        holder.itemClickListener = itemClickListener

        val item = getItem(position)
        when(holder){
            is NftViewHolder.FeaturedViewHolder -> holder.bind(item as NftData.Featured)
            is NftViewHolder.TitleViewHolder -> holder.bind(item as NftData.Title)
            is NftViewHolder.TopPicksViewHolder -> holder.bind(item as NftData.Top)
            is NftViewHolder.TrendingViewHolder -> holder.bind(item as NftData.Trending)
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when(getItem(position)){
            is NftData.Title -> R.layout.rcv_lyt_title
            is NftData.Featured -> R.layout.rcv_lyt_featured
            is NftData.Top -> R.layout.rcv_lyt_top_picks
            is NftData.Trending -> R.layout.rcv_lyt_trending
        }
    }

    class NftDiffCallBack : DiffUtil.ItemCallback<NftData>(){

        override fun areItemsTheSame(oldItem: NftData, newItem: NftData): Boolean {
            return when {
                oldItem is NftData.Top && newItem is NftData.Top -> {
                    oldItem.id == newItem.id
                }
                oldItem is NftData.Trending && newItem is NftData.Trending -> {
                    oldItem.id == newItem.id
                }
                else -> {
                    false
                }
            }
        }

        override fun areContentsTheSame(oldItem: NftData, newItem: NftData): Boolean {
            return when {
                oldItem is NftData.Top && newItem is NftData.Top -> {
                    oldItem == newItem
                }
                oldItem is NftData.Trending && newItem is NftData.Trending -> {
                    oldItem == newItem
                }
                else -> {
                    false
                }
            }
        }

    }
    
}

UI

NftViewModel.kt

使用状态流(state flow)我们可以获得一个可观测的流,从数据源中发出当前和新的状态更新。

@HiltViewModel
class NftViewModel @Inject constructor(private val nftRepository: NftRepository) : ViewModel() {

    private val _nft = MutableStateFlow<Resource<List<NftData>>>(Resource.Loading)
    val nft: StateFlow<Resource<List<NftData>>> get() = _nft

    init {
        getNft()
    }

    private fun getNft() = viewModelScope.launch {

        _nft.emit(Resource.Loading)

        val topNftDeferred = async { nftRepository.getTopNft() }
        val trendingNftDeferred = async { nftRepository.getTrendingNft() }

        val topNft = topNftDeferred.await()
        val trendingNft = trendingNftDeferred.await()

        val nftList = mutableListOf<NftData>()

        if(topNft is Resource.Success && trendingNft is Resource.Success){
            nftList.add(NftData.Title(1, "Featured", ""))
            nftList.add(NftData.Featured(FEATURED_IMAGE, FEATURED_IMAGE_TITLE))
            nftList.add(NftData.Title(2, "Top Pick", "View all"))
            nftList.addAll(topNft.value)
            nftList.add(NftData.Title(2, "Trending", ""))
            nftList.addAll(trendingNft.value)
            _nft.emit(Resource.Success(nftList))
        }else{
            Resource.Failure(false, null, null)
        }

    }

}

MainActivity.kt

我们设置了我们的RecyclerView布局管理器,并在UI层使用了推荐的新方法来收集流。

 //set up recycler view
        binding.rcvNft.apply {

            val gridLayoutManager = GridLayoutManager(this@MainActivity, 6)
            gridLayoutManager.spanSizeLookup = object : SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    return when (nftAdapter.getItemViewType(position)) {
                        R.layout.rcv_lyt_title -> 6
                        R.layout.rcv_lyt_featured -> 6
                        R.layout.rcv_lyt_top_picks -> 3
                        R.layout.rcv_lyt_trending -> 6
                        else -> 1
                    }
                }
            }

            layoutManager = gridLayoutManager
            setHasFixedSize(true)
            adapter = nftAdapter

        }

        //handle clicks
        nftAdapter.itemClickListener = { view, item, position ->

            when(item) {
                is NftData.Title -> Toast.makeText(this, "View all clicked", Toast.LENGTH_LONG).show()
                is NftData.Featured -> Toast.makeText(this, "Featured nft clicked", Toast.LENGTH_LONG).show()
                is NftData.Top -> Toast.makeText(this, "Top nft clicked", Toast.LENGTH_LONG).show()
                is NftData.Trending -> Toast.makeText(this, "Trending nft clicked", Toast.LENGTH_LONG).show()
            }

        }

        //best way to collect flows in UI layer
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                nftViewModel.nft.collect{ result ->
                    when (result) {
                        Resource.Loading ->  binding.loading.changeVisibility(View.VISIBLE)
                        is Resource.Failure -> {
                            binding.loading.changeVisibility(View.GONE)
                        }
                        is Resource.Success -> {
                            binding.loading.changeVisibility(View.GONE)
                            nftAdapter.submitList(result.value)
                        }
                    }

                }
            }
        }

GitHub

https://github.com/ibrajix/NftApp

声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/186532.html

联系我们
联系我们
分享本页
返回顶部