Android中构建多视图 RecyclerView的正确打开方式
简介
漂亮的UI能极大提高用户留存率,相反糟糕的UI将导致App安装率下降。
UI体验对用户留存率有特别大的影响,较差的体验app我可能用不了2s就要卸载掉。
你需要学习内容如下:
- 使用单个RecyclerView来处理多种视图类型
- 整洁的代码 - MVVM架构
- 显示来自外部API的数据
- 使用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 nftsuspend fun getTopNft() = apiService.getTopNft()//get trending nftsuspend 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)? = nullclass TitleViewHolder(private val binding: RcvLytTitleBinding) : NftViewHolder(binding){fun bind(title: NftData.Title) {binding.txtFeatured.text = title.titlebinding.txtViewAll.text = title.viewAllbinding.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.namebinding.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)? = nulloverride 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 = itemClickListenerval 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_titleis NftData.Featured -> R.layout.rcv_lyt_featuredis NftData.Top -> R.layout.rcv_lyt_top_picksis 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() = _nftinit {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 viewbinding.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 -> 6R.layout.rcv_lyt_featured -> 6R.layout.rcv_lyt_top_picks -> 3R.layout.rcv_lyt_trending -> 6else -> 1}}}layoutManager = gridLayoutManagersetHasFixedSize(true)adapter = nftAdapter}//handle clicksnftAdapter.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 layerlifecycleScope.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