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 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