列表視圖 為實現各種排列組合類的視圖(包括但不限於Spinner、ListView、GridView等等),Android提供了五花八門的適配器用於組裝某個規格的數據,常見的適配器有:數組適配器ArrayAdapter、簡單適配器SimpleAdapter、基本適配器BaseAdapter、翻頁適配 ...
列表視圖
為實現各種排列組合類的視圖(包括但不限於Spinner、ListView、GridView等等),Android提供了五花八門的適配器用於組裝某個規格的數據,常見的適配器有:數組適配器ArrayAdapter、簡單適配器SimpleAdapter、基本適配器BaseAdapter、翻頁適配器PagerAdapter。適配器的種類雖多,卻個個都不好用,以數組適配器為例,它與Spinner配合實現下拉框效果,其實現代碼紛復繁雜,一直為人所詬病。故而在下拉框一小節之中,乾脆把ArrayAdapter連同Spinner一股腦都摒棄了,取而代之的是Kotlin擴展函數selector。
到了列表視圖ListView這裡,與之搭檔的一般是基本適配器BaseAdapter,這個BaseAdapter更不簡單,基於它的列表適配器得重寫好幾個方法,還有那個想讓初學者撞牆的ViewHolder。總之,每當要實現類似新聞列表、商品列表之類的頁面,一想到這個難纏的BaseAdapter,心裡便發怵。譬如下圖所示的六大行星的說明列表,左側是圖標,右邊為文字說明,很普通的一個頁面。
可是這個行星列表頁面,倘若使用Java編碼,就得書寫下麵一大段長長的代碼:
public class PlanetJavaAdapter extends BaseAdapter { private Context mContext; private ArrayList<Planet> mPlanetList; private int mBackground; public PlanetJavaAdapter(Context context, ArrayList<Planet> planet_list, int background) { mContext = context; mPlanetList = planet_list; mBackground = background; } @Override public int getCount() { return mPlanetList.size(); } @Override public Object getItem(int arg0) { return mPlanetList.get(arg0); } @Override public long getItemId(int arg0) { return arg0; } @Override public View getView(final int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { holder = new ViewHolder(); convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list_view, null); holder.ll_item = (LinearLayout) convertView.findViewById(R.id.ll_item); holder.iv_icon = (ImageView) convertView.findViewById(R.id.iv_icon); holder.tv_name = (TextView) convertView.findViewById(R.id.tv_name); holder.tv_desc = (TextView) convertView.findViewById(R.id.tv_desc); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } Planet planet = mPlanetList.get(position); holder.ll_item.setBackgroundColor(mBackground); holder.iv_icon.setImageResource(planet.image); holder.tv_name.setText(planet.name); holder.tv_desc.setText(planet.desc); return convertView; } public final class ViewHolder { public LinearLayout ll_item; public ImageView iv_icon; public TextView tv_name; public TextView tv_desc; } }
上面Java實現的適配器類PlanetJavaAdapter,果真又冗長又晦澀,然而這段代碼模版基本上是列表視圖的標配,只要用Java編碼,就必須依樣畫瓢。如果用Kotlin實現這個適配器類會是怎樣的呢?馬上利用Android Studio把上述Java代碼轉換為Kotlin編碼,轉換後的Kotlin代碼類似以下片段:
class PlanetKotlinAdapter(private val mContext: Context, private val mPlanetList: ArrayList<Planet>, private val mBackground: Int) : BaseAdapter() { override fun getCount(): Int { return mPlanetList.size } override fun getItem(arg0: Int): Any { return mPlanetList[arg0] } override fun getItemId(arg0: Int): Long { return arg0.toLong() } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView var holder: ViewHolder? if (view == null) { holder = ViewHolder() view = LayoutInflater.from(mContext).inflate(R.layout.item_list_view, null) holder.ll_item = view.findViewById(R.id.ll_item) as LinearLayout holder.iv_icon = view.findViewById(R.id.iv_icon) as ImageView holder.tv_name = view.findViewById(R.id.tv_name) as TextView holder.tv_desc = view.findViewById(R.id.tv_desc) as TextView view.tag = holder } else { holder = view.tag as ViewHolder } val planet = mPlanetList[position] holder.ll_item!!.setBackgroundColor(mBackground) holder.iv_icon!!.setImageResource(planet.image) holder.tv_name!!.text = planet.name holder.tv_desc!!.text = planet.desc return view!! } inner class ViewHolder { var ll_item: LinearLayout? = null var iv_icon: ImageView? = null var tv_name: TextView? = null var tv_desc: TextView? = null } }
相比之下,直接轉換得來的Kotlin代碼,最大的改進是把構造函數及初始化參數放到了第一行,其它地方未有明顯優化。眼瞅著沒多大改善,反而因為Kotlin的空安全機制,平白無故多了好些問號和雙感嘆號,可謂得不償失。問題出在Kotlin要求每個變數都要初始化上面,視圖持有者ViewHolder作為一個內部類,目前雖然無法直接對控制項對象賦值,但是從代碼邏輯可以看出先從佈局文件獲取控制項,然後才會調用各種設置方法。這意味著,上面的控制項對象必定是先獲得實例,在它們被使用的時候肯定是非空的,因此完全可以告訴編譯器,這些控制項對象一定會在使用前賦值,編譯器您老就高抬貴手,睜一隻眼閉一隻眼放行好了。
毋庸置疑,該想法合情合理,Kotlin正好提供了這種後門,它便是關鍵字lateinit。lateinit的意思是延遲初始化,它放在var或者val前面,表示被修飾的變數屬於延遲初始化屬性,即使沒有初始化也仍然是非空的。如此一來,這些控制項在聲明之時無需賦空值,在使用的時候也不必畫蛇添足加上兩個感嘆號了。根據新來的lateinit修改前面的Kotlin適配器,改寫後的Kotlin代碼如下所示:
class PlanetListAdapter(private val context: Context, private val planetList: MutableList<Planet>, private val background: Int) : BaseAdapter() { override fun getCount(): Int = planetList.size override fun getItem(position: Int): Any = planetList[position] override fun getItemId(position: Int): Long = position.toLong() override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView val holder: ViewHolder if (convertView == null) { view = LayoutInflater.from(context).inflate(R.layout.item_list_view, null) holder = ViewHolder() //先聲明視圖持有者的實例,再依次獲取內部的各個控制項對象 holder.ll_item = view.findViewById(R.id.ll_item) as LinearLayout holder.iv_icon = view.findViewById(R.id.iv_icon) as ImageView holder.tv_name = view.findViewById(R.id.tv_name) as TextView holder.tv_desc = view.findViewById(R.id.tv_desc) as TextView view.tag = holder } else { holder = view.tag as ViewHolder } val planet = planetList[position] holder.ll_item.setBackgroundColor(background) holder.iv_icon.setImageResource(planet.image) holder.tv_name.text = planet.name holder.tv_desc.text = planet.desc return view!! } //ViewHolder中的屬性使用關鍵字lateinit延遲初始化 inner class ViewHolder { lateinit var ll_item: LinearLayout lateinit var iv_icon: ImageView lateinit var tv_name: TextView lateinit var tv_desc: TextView } }
以上的Kotlin代碼總算有點模樣了,雖然總體代碼還不夠精簡,但是至少清晰明瞭,其中主要運用了Kotlin的以下三項技術:
1、構造函數和初始化參數放在類定義的首行,無需單獨構造,也無需手工初始化;
2、像getCount、getItem、getItemId這三個函數,僅僅返回簡單運算的數值,可以直接用等號取代大括弧;
3、對於視圖持有者的內部控制項,在變數名稱前面添加lateinit,表示該屬性為延遲初始化屬性;
網格視圖
在前面的列表視圖一小節中,給出了Kotlin改寫後的適配器類,通過關鍵字lateinit固然避免了麻煩的空校驗,可是控制項對象遲早要初始化的呀,晚賦值不如早賦值。翻到前面PlanetListAdapter的實現代碼,認真觀察發現控制項對象的獲取其實依賴於佈局文件的視圖對象view,既然如此,不妨把該視圖對象作為ViewHolder的構造參數傳過去,使得視圖持有者在構造之時便能一塊初始化內部控制項。據此改寫後的Kotlin適配器代碼如下所示:
class PlanetGridAdapter(private val context: Context, private val planetList: MutableList<Planet>, private val background: Int) : BaseAdapter() { override fun getCount(): Int = planetList.size override fun getItem(position: Int): Any = planetList[position] override fun getItemId(position: Int): Long = position.toLong() override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView val holder: ViewHolder if (view == null) { view = LayoutInflater.from(context).inflate(R.layout.item_grid_view, null) holder = ViewHolder(view) //視圖持有者的內部控制項對象已經在構造時一併初始化了,故這裡無需再做賦值 view.tag = holder } else { holder = view.tag as ViewHolder } val planet = planetList[position] holder.ll_item.setBackgroundColor(background) holder.iv_icon.setImageResource(planet.image) holder.tv_name.text = planet.name holder.tv_desc.text = planet.desc return view!! } //ViewHolder中的屬性在構造時初始化 inner class ViewHolder(val view: View) { val ll_item: LinearLayout = view.findViewById(R.id.ll_item) as LinearLayout val iv_icon: ImageView = view.findViewById(R.id.iv_icon) as ImageView val tv_name: TextView = view.findViewById(R.id.tv_name) as TextView val tv_desc: TextView = view.findViewById(R.id.tv_desc) as TextView } }
利用該適配器運行測試應用,得到的網格效果如下圖所示,可見與Java代碼的運行結果完全一致。
至此基於BaseAdapter的Kotlin列表適配器告一段落,上述的適配器代碼模版,同時適用於列表視圖ListView與網格視圖GridView。