玩安卓从 0 到 1 之适配器思考

2,953 阅读6分钟

前言

这篇文章是这个系列的第五篇文章了,下面是前三篇文章:

1、玩安卓从 0 到 1 之总体概览

2、玩安卓从 0 到 1 之项目首页

3、玩安卓从 0 到 1 之首页框架搭建

4、玩安卓从 0 到 1 之架构思考

按照惯例,放一下 Github 地址和 apk 下载地址吧!

apk 下载地址:www.pgyer.com/llj2

Github地址:github.com/zhujiang521…

前因后果

之前不管是 ListView 、GridView 还是 RecyclerView ,都使用的是泓洋大神的开源库 baseAdapter ,以前代码全都是 Java 编写的,觉得这个库很方便,省了很多事,所以之前公司的项目中也都使用的是这个库,一直没觉得有什么不对,但是之后都换成 Kotlin 编写项目之后就觉得有点不太对了,为什么这样说呢?

大家先来看下使用这个库的时候写的代码:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
    @Override
    public void convert(ViewHolder holder, String s)
    {
        holder.setText(R.id.id_item_list_title, s);
    }
});

看着很简单,也很方便,还有就是在 ViewHolder 中添加了许多常用的辅助方法,比如上面使用到的 setText 方法,只需要传入 TextView 的 id 和字符就可以完成设置,无需进行 findViewById ,如果你的控件是自定义的或者是辅助方法中没有你需要的方法,这个时候你还可以使用 getView 方法来获取到你所需要的控件来进行操作。

其实这个库是很好的,但 Kotlin 横空出世了,Kotlin 的 extensions 也是使用 Kotlin 的一大原因,为什么?因为不用写那些乱七八糟的 findViewById 了啊!之前用 Java 编写的时候没有办法才写的(这里不提黄油刀等工具,只是单纯地谈论 findViewById 。),现在能不写肯定不想写了,但是再看看我用 Kotlin 时使用这个库的时候写的代码:

class ProfileAdapter(context: Context,
    profileItemList: ArrayList<ProfileItem>,
    layoutId: Int = R.layout.adapter_profile):
    CommonAdapter<ProfileItem>(context, layoutId, profileItemList) {

    override fun convert(holder: ViewHolder, t: ProfileItem, position: Int) {
        val profileAdLlItem = holder.getView<LinearLayout>(R.id.profileAdLlItem)
        val profileAdIv = holder.getView<ImageView>(R.id.profileAdIv)
        val profileAdTvTitle = holder.getView<TextView>(R.id.profileAdTvTitle)
        profileAdTvTitle.text = t.title
        profileAdIv.setImageResource(t.imgId)
        profileAdLlItem.setOnClickListener {
        holder.itemView.profileAdTvTitle.text = t.title
        holder.itemView.profileAdIv.setImageResource(t.imgId)
    }
      
}

没有什么意义啊!我使用三方库的原因是什么?肯定是为了简化代码,为了省事,但这样显然没有省事。

再来看一下泓洋大神对这个库的描述:

只需要简单的将Adapter继承CommonAdapter,复写convert方法即可。省去了自己编写ViewHolder等大量的重复的代码。

“省去了自己编写ViewHolder等大量的重复的代码”,但是回过头来想一想,“ViewHolder等大量的重复的代码”又究竟是什么呢?随便举个例子吧:

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public View rootView;
        public TextView mTvTimeFrame;
        public TextView mTvTimeFrameDelete;
        public TextView mTvStartDate;

        public ViewHolder(View rootView) {
            super(rootView);
            this.rootView = rootView;
            this.mTvTimeFrame = (TextView) rootView.findViewById(R.id.tv_time_frame);
            this.mTvTimeFrameDelete = (TextView) rootView.findViewById(R.id.tv_time_frame_delete);
            this.mTvStartDate = (TextView) rootView.findViewById(R.id.tv_start_date);
        }
    }

平时写 ViewHolder 的作用也只是对控件进行初始化而已,现在有了 Kotlin 的 extensions ,其实并没有这个必要了,咱们完全可以自己来写个基类解决,不需要使用泓洋大神这个库了,况且这个库也停更了四年了。。。

开始解决

上面说的其实不完全对,其实这个库还有其他的作用,比如 Item 的 多 Type 等等,在这里由于玩安卓这个应用比较简单,没有用到多 Type 的地方,所以在这里先不考虑多 Item 的情况,只考虑单 Item 的情况。

接下来的任务就是自己写一个基类了,并把使用到这个库的 Adapter 给修改成继承咱们的基类。

其实这个基类很简单,平时咱们怎样写 RecyclerView 的 Adapter 现在就怎样写就行:

abstract class BaseListAdapter<T : Any>(
    protected val mContext: Context,
    private val layoutId: Int,
    private val dataList: List<T>
) : RecyclerView.Adapter<BaseListAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        convert(holder.itemView, dataList[position], position)
    }

    abstract fun convert(view: View, data: T, position: Int)

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemCount() = dataList.size

    class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
        LayoutContainer

}

是不是很简单!如果其他地方需要使用的时候只需要继承此类并实现 convert 方法即可,为啥也叫 convert 呢?当然是因为懒了,这样就不需要改太多的代码啊!

还是再简单说下上面的基类吧,构造方法中的三个参数:Context 就不说了,Adapter 中很多地方也会用到,为什么叫 mContext ,也是因为泓洋大神的库中也是这个名称。。。。layoutId 则是布局,dataList 就是数据的集合了。这里有一点需要注意,我在 convert 方法中传入的并不是 ViewHolder ,而是 ViewHolder 中的 itemView,这是为了在使用的时候可以直接使用 Kotlin 的“魔法”!其他的代码就不多说了,因为平时大家写 Adapter 的时候都是这么干的。

来看下上面的例子改成继承咱们的 BaseListAdapter 该怎样写吧:

class ProfileAdapter(
    context: Context,
    profileItemList: ArrayList<ProfileItem>,
    layoutId: Int = R.layout.adapter_profile
) : BaseListAdapter<ProfileItem>(context, layoutId, profileItemList) {

    override fun convert(view: View, data: ProfileItem, position: Int) {
        view.profileAdTvTitle.text = data.title
        view.profileAdIv.setImageResource(data.imgId)
        view.profileAdLlItem.setOnClickListener {
            toJump(data.title)
        }
    }
    
}

看到刚才所说的魔法了吗?不需要进行 findViewById了,但是还需要通过 view 来获取一下,不过也比之前要好多了是不!

文末总结

这篇文章其实写的有点水,自己其实这块搞的并不是很清楚,看官方给出的实例完全可以直接获取到的,不需要再通过 view 来获取,但是我那样进行尝试的时候始终不行,来看下官方的代码:

class ForecastListAdapter(private val weekForecast: ForecastList,
        private val itemClick: (Forecast) -> Unit) :
        RecyclerView.Adapter<ForecastListAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.ctx).inflate(R.layout.item_forecast, parent, false)
        return ViewHolder(view, itemClick)
    }

    @SuppressLint("SetTextI18n")
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bindForecast(weekForecast[position])
    }

    override fun getItemCount() = weekForecast.size

    class ViewHolder(override val containerView: View, private val itemClick: (Forecast) -> Unit)
        : RecyclerView.ViewHolder(containerView), LayoutContainer {

        fun bindForecast(forecast: Forecast) {
            with(forecast) {
                Picasso.with(itemView.ctx).load(iconUrl).into(icon)
                dateText.text = date.toDateString()
                descriptionText.text = description
                maxTemperature.text = "${high}º"
                minTemperature.text = "${low}º"
                itemView.setOnClickListener { itemClick(this) }
            }
        }
    }
}

之前以为是在 ViewHolder 中的问题,但我试过将方法放到 ViewHolder 中,还是不行,我又以为是什么库没有引用,又把实例中的所有依赖引用了下,还是不可以,如果有知道的欢迎去我的 Github 上帮我提个 issues,或者直接在评论区告诉我,感激不尽。

接上文——解魔法

上面写了官方的就可以直接获取到控件,感觉像是用了魔法一样,但是开发没有魔法,肯定是有原因的。。。

于是乎我开始找不同,终于发现了罪魁祸首:

androidExtensions {
    experimental = true
}

刚开始的时候Extensions是不支持在ViewHolder中使用视图绑定的,因此还是需要些findViewById,但是从Kotlin 1.1.4起,Extensions加入了增强功能,由于这项功能还未正式发布,因此需要开启实验标志。

使用的方法上面其实写的也有,在Activity,Fragment,View中我们知道import对应的layout就可以了,但是 ViewHolder 需要实现 LayoutContainer ,接口返回一个containerView,按照字面意思理解就是内容视图,这个 containerView 就包含了 ViewHolder 里面的所有子View,因此就可以直接使用控件了。

修改代码

既然找到原因了,那就开始吧!

首先修改下 BaseListAdapter 的抽象方法中的函数,不直接通过 holder 的 itemView 来获取控件了,直接通过 ViewHolder 来获取就行了,还有一个原因是这篇文章的一个评论:

掘金评论

来看下修改后的 BaseListAdapter :

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    convert(holder, dataList[position], position)
}

abstract fun convert(holder: ViewHolder, data: T, position: Int)

基本没改,只是把参数又变回 ViewHolder 了,来看下使用吧:

override fun convert(holder: ViewHolder, data: ProfileItem, position: Int) {
    with(holder) {
        profileAdTvTitle.text = data.title
        profileAdIv.setImageResource(data.imgId)
        profileAdLlItem.setOnClickListener {
            toJump(data.title)
        }
    }
}

优雅了些许,而且也不担心 ViewHolder 不起作用。

OK 了,先到这里吧!

如果喜欢文章别忘了点赞关注啊,咱们下回见!