【Android 6.0】RecyclerView - 高效能的ListView

現在要介紹的是RecyclerView,這應該是手機上最常用的元件之一,就是列表。RecyclerView可以說是ListView的改良版,和ListView相同的是,RecyclerView一樣是由配置器(Adapter)來處理ItemView呈現的內容,不同的是,RecyclerView有實做Recycle機制,就像iOS的UITableView的Reuse機制。話不多說,現在就來一步步做一個長得像這樣的APP吧,以下的說明還是會以iOS做類比。

Toolbar

關閉ActionBar

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.recyclerview">
    <application
        ...
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

設定Toolbar

  • 接下來呢,就跟一般使用UI元件一樣,利用findViewById()之類的方式去處理,把Toolbar元件拉一拉,利用setSupportActionBar()加到畫面上。
class MainActivity : AppCompatActivity() {

    private lateinit var toolbar: Toolbar
    private lateinit var recyclerView: RecyclerView
    private lateinit var alertDialog: AlertDialog

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initIdentity()

        toolbarSetting("我是Toolbar")
    }

    /// 初始化UI的ID
    private fun initIdentity() {
        toolbar = findViewById(R.id.toolbar)
    }

    /// 設定Toolbar (加在畫面上)
    private fun toolbarSetting(title: String) {

        val backgroundColor = resources.getColor(R.color.colorPrimaryDark, null)
        val titleTextColor = resources.getColor(R.color.colorPrimary, null)

        toolbar.title = title

        toolbar.setBackgroundColor(backgroundColor)
        toolbar.setTitleTextColor(titleTextColor)
        toolbar.setNavigationIcon(R.drawable.ic_add_box_black_24dp)

        setSupportActionBar(toolbar)
    }
}

讓Toolbar的按鍵按下有反應

class MainActivity : AppCompatActivity() {

    /// 設定ToolbarIcon按下的Listener
    private fun toolbarIconOnClickSetting() {

        toolbar.setNavigationOnClickListener {
            Toast.makeText(this, "我是Toolbar Icon", Toast.LENGTH_SHORT).show()
        }
    }
}

RecyclerView

設定RecyclerView

  • 接下來就是加上主角RecyclerView,如果沒有的話,也在Paleete點擊下載。另外Android出了一個[ConstraintLayout],這個就有iOS AutoLayout的影子,不得不說在畫面的設定上,Xcode的確比較人性化一點。

使用RecyclerView

class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {

    /// cell的數量 (tableView(_:numberOfRowsInSection:))
    override fun getItemCount(): Int {}

    /// cell的長相 (tableView(_:cellForRowAt:))
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {}

    /// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
}

實做interface

  • 接下來我們就把假資料放到RecyclerView內,讓它有真實資料的產生。這裡要說明的是,在設定item畫面的時候,有一個叫做layout_weight - 權重的設定,在iOS也有這樣的東西去設定大小比例。在這裡就可以試試RecyclerView的基本功能是否正常。
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private var context: Context
    private var modelArray: ArrayList<CellModel>

    /// 初始化資料 (UITableViewDelegate)
    constructor(context: Context, modelArray: ArrayList<CellModel>) : super() {
        this.context = context
        this.modelArray = modelArray
    }

    /// cell的數量 (tableView(_:numberOfRowsInSection:))
    override fun getItemCount(): Int {
        return modelArray.count()
    }

    /// cell的長相 (tableView(_:cellForRowAt:))
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {
        val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_cell, parent, false)
        return CellHolder(cellView)
    }

    /// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

        val cellHolder = holder as? CellHolder ?: return
        val modelData = modelArray[position]

        holder.nameTextView.text = modelData.name
        holder.regionTextView.text = modelData.region
    }
}
object Utility {

    /// 測試用假資料 (Cell)
    fun demoModelData(count: Int) : ArrayList<CellModel> {

        var modelArray = ArrayList<CellModel>()

        for (index in 0..count) {

            var model = CellModel()
            model.name = "William - $index"
            model.region = "Earth - $index"

            modelArray.add(model)
        }

        return modelArray
    }
}
// MARK: - 基本的Model長相
interface BaseModel {}

// MARK: - CellModel (內容的Model)
class CellModel : BaseModel {
    var name: String? = null
    var region: String? = null
}
class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        recyclerViewSetting()
        recyclerView.adapter = modelAdapterMaker(20)
    }

    /// 初始化UI的ID
    private fun initIdentity() {
        ...
        recyclerView = findViewById(R.id.recyclerView)
    }

    /// [假的] RecyclerView上的資料
    private fun modelAdapterMaker(count: Int) : ModelAdapter {
        return ModelAdapter(this, Utility.demoModelData(20))
    }
}

RecyclerView Holder

  • 接下來我們可以看到RecyclerView實際上是在Toolbar的下方,所以第一個Cell是看不到的,所以要手動加上一個空白的Header,讓RecyclerView的資料能完整的顯示出來。為了讓Header跟Toolbar一樣大,高度是要設定成「?attr/actionBarSize」,然後利用「BaseModel」來做基本類型的使用,用getItemViewType(position: Int)來區分類別。
// MARK: - Cell的類型 (Header / Cell)
enum class CellType(val value: Int) {
    Header(0),
    Cell(1),
}

// MARK: - 基本的Model長相
interface BaseModel {
    val cellType: CellType
}

// MARK: - HeaderModel (內容的Model)
class HeaderModel : BaseModel {

    override val cellType: CellType
        get() = CellType.Header
}

// MARK: - CellModel (內容的Model)
class CellModel : BaseModel {

    override val cellType: CellType
        get() { return CellType.Cell }

    var name: String? = null
    var region: String? = null
}
object Utility {

    /// 空白的Header
    fun headerModelData() : HeaderModel {
        return HeaderModel()
    }
}
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private var context: Context
    private var modelArray: ArrayList<BaseModel>

    /// 初始化資料 (UITableViewDelegate)
    constructor(context: Context, modelArray: ArrayList<BaseModel>) : super() {
        this.context = context
        this.modelArray = modelArray
    }

    /// cell的數量 (tableView(_:numberOfRowsInSection:))
    override fun getItemCount(): Int {
        return modelArray.count()
    }

    /// cell的長相 (tableView(_:cellForRowAt:))
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {

        val viewTypeEnum = CellType.values()[viewType]
        var viewHolder: RecyclerView.ViewHolder

        when(viewTypeEnum) {
            CellType.Header -> { viewHolder = headerViewMaker(parent) }
            CellType.Cell -> { viewHolder = cellViewMaker(parent) }
        }

        return viewHolder
    }

    /// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

        when(holder) {
            is HeaderHolder -> { headerHolderSetting(holder, position) }
            is CellHolder -> { cellHolderSetting(holder, position) }
        }
    }

    /// item的類型 (cell or header)
    override fun getItemViewType(position: Int): Int {
        val cellType = modelArray[position].cellType
        return cellType.value
    }

    /// XML -> HeaderView -> Holder
    private fun headerViewMaker(parent: ViewGroup): HeaderHolder {
        val headerView = LayoutInflater.from(context).inflate(R.layout.recyclerview_header, parent, false)
        return HeaderHolder(headerView)
    }

    /// XML -> CellView -> Holder
    private fun cellViewMaker(parent: ViewGroup): CellHolder {
        val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_cell, parent, false)
        return CellHolder(cellView)
    }

    /// Header的內容細部設定
    private fun headerHolderSetting(holder: HeaderHolder, position: Int) {
        holder.itemView.setBackgroundColor(0x00ff00)
    }

    /// Cell的內容細部設定 [假資料]
    private fun cellHolderSetting(holder: CellHolder, position: Int) {

        val modelData = modelArray[position] as? CellModel ?: return

        holder.nameTextView.text = modelData.name
        holder.regionTextView.text = modelData.region
    }
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        recyclerView.adapter = modelAdapterMaker(20)
    }

    /// [假的] RecyclerView上的資料 (Header +Cells)
    private fun modelAdapterMaker(count: Int) : ModelAdapter {

        var modelArrayList = ArrayList<BaseModel>()
        val demoModelArray = Utility.demoModelData(count)

        modelArrayList.add(headerModelData())
        modelArrayList.all(demoModelArray)

        return ModelAdapter(this, modelArrayList)
    }
}

使用第三方套件

安裝第三方套件

  • Android Studio在安裝第三方套件的方面就比Xcode來的好多了。直接在build.gradle上implementation套件的全名,然後按下右上角的「Sync Now」就可以了,這裡要安裝的是okhttp - 網路連線套件 + Gson - 解json套件

WebAPI

  • 這裡要真正從網路上取得資料,利用okhttp來讀取API - uinames的資料,當然跟iOS一樣,要先打開網路的權限才可以喲。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.recyclerview">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

</manifest>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {

        getUINamesBody(10) { responseBody ->

            responseBody?.let {
                Log.e("[ResponseBody]", "${responseBody.toString()}")
            }
        }
    }

    /// 取得API的資料 (非同步)
    private fun getUINamesBody(count: Int, callback: (ResponseBody?) -> Unit) {

        val urlString = "https://uinames.com/api/?amount=$count"
        val request = Request.Builder().get().url(urlString).build()
        val newCall = OkHttpClient().newCall(request)

        newCall.enqueue(object : Callback {

            override fun onFailure(call: Call, e: IOException) {
                callback(null)
            }

            override fun onResponse(call: Call, response: Response) {
                callback(response.body)
            }
        })
    }

    /// [真的] RecyclerView上的資料 (Header + Sections + Cells)
    private fun webModelAdapterMaker(models: ArrayList<UINameModel>) : ModelAdapter {

        val demoSectionData = Utility.demoSectionData(5)

        var modelArrayList = ArrayList<BaseModel>()

        modelArrayList.add(headerModelData())

        for (sectionData in demoSectionData) {
            modelArrayList.add(sectionData)
            modelArrayList.addAll(models)
        }

        return ModelAdapter(this, modelArrayList)
    }
}

JSON ==> Model

  • 利用Gson,將JSON字串轉成model
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {

        getUINamesBody(10) { responseBody ->

            responseBody?.let {

                val jsonString = responseBody.string()
                val arrayType = object : TypeToken<ArrayList<UINameModel>>(){}.type
                val uiNameModelArray = Gson().fromJson<ArrayList<UINameModel>>(jsonString, arrayType)

                Log.e("[GSON]", "$jsonString")

                for (model in uiNameModelArray) {
                    Log.e("[GSON]", ${model.name})
                }
            }
        }
    }
}
// MARK: - UINameModel (內容的Model)
class UINameModel : BaseModel {

    override val cellType: CellType
        get() { return CellType.Cell }

    @SerializedName("name")
    var name: String? = null

    @SerializedName("region")
    var region: String? = null
}

讀取真實資料

  • 這裡將從網路讀來的資料加入,然後再加上Section資料,完成一個簡單的RecyclerView功能。比較要注意的是,如果要更新主畫面的話,也是一樣要在runOnUiThread - main queue中執行。
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {

       getUINamesBody(20) { responseBody ->

            responseBody?.let {

                val uiNameModelArray = nameModelArrayMaker(responseBody)

                runOnUiThread {
                    recyclerView.adapter = webModelAdapterMaker(uiNameModelArray)
                }
            }
        }
    }

    /// [GSON] 將 ResponseBody => JSON String => ArrayList<UINameModel>
    private fun nameModelArrayMaker(responseBody: ResponseBody) : ArrayList<UINameModel> {
        val jsonString = responseBody.string()
        val arrayType = object : TypeToken<ArrayList<UINameModel>>(){}.type
        return Gson().fromJson<ArrayList<UINameModel>>(jsonString, arrayType)
    }
}
// MARK: - Cell的類型 (Header / Cell)
enum class CellType(val value: Int) {
    Header(0),
    Cell(1),
    Section(2),
}

// MARK: - 基本的Model長相
interface BaseModel {
    val cellType: CellType
}

// MARK: - HeaderModel (Header的Model)
class HeaderModel : BaseModel {

    override val cellType: CellType
        get() = CellType.Header
}

// MARK: - SectionModel (Section的Model)
class SectionModel : BaseModel {

    override val cellType: CellType
        get() = CellType.Section

    var title: String? = null
}

// MARK: - CellModel (內容的Model)
class CellModel : BaseModel {

    override val cellType: CellType
        get() { return CellType.Cell }

    var name: String? = null
    var region: String? = null
}

// MARK: - UINameModel (內容的Model)
class UINameModel : BaseModel {

    override val cellType: CellType
        get() { return CellType.Cell }

    @SerializedName("name")
    var name: String? = null

    @SerializedName("region")
    var region: String? = null
}
// MARK: - 類似UITableView的Delegate / DataSource
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private var context: Context
    private var modelArray: ArrayList<BaseModel>

    /// 初始化資料 (UITableViewDelegate)
    constructor(context: Context, modelArray: ArrayList<BaseModel>) : super() {
        this.context = context
        this.modelArray = modelArray
    }

    /// cell的數量 (tableView(_:numberOfRowsInSection:))
    override fun getItemCount(): Int {
        return modelArray.count()
    }

    /// cell的長相 (tableView(_:cellForRowAt:))
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {

        val viewTypeEnum = CellType.values()[viewType]

        return when(viewTypeEnum) {
            CellType.Header -> {  headerViewMaker(parent) }
            CellType.Section -> { sectionViewMaker(parent) }
            CellType.Cell -> { cellViewMaker(parent) }
        }
    }

    /// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

        when(holder) {
            is HeaderHolder -> { headerHolderSetting(holder, position) }
            is SectionHolder -> { sectionHolderSetting(holder, position) }
            is CellHolder -> { nameHolderSetting(holder, position) }
        }
    }

    /// item的類型 (cell or header)
    override fun getItemViewType(position: Int): Int {
        val cellType = modelArray[position].cellType
        return cellType.value
    }

    /// XML -> HeaderView -> Holder
    private fun headerViewMaker(parent: ViewGroup): HeaderHolder {
        val headerView = LayoutInflater.from(context).inflate(R.layout.recyclerview_header, parent, false)
        return HeaderHolder(headerView)
    }

    /// XML -> SectionView -> Holder
    private fun sectionViewMaker(parent: ViewGroup): SectionHolder {
        val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_section, parent, false)
        return SectionHolder(cellView)
    }

    /// XML -> CellView -> Holder
    private fun cellViewMaker(parent: ViewGroup): CellHolder {
        val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_cell, parent, false)
        return CellHolder(cellView)
    }

    /// Header的內容細部設定
    private fun headerHolderSetting(holder: HeaderHolder, position: Int) {
        holder.itemView.setBackgroundColor(0x00ff00)
    }

    /// Section的內容細部設定
    private fun sectionHolderSetting(holder: SectionHolder, position: Int) {
        val modelData = modelArray[position] as? SectionModel ?: return
        holder.titleTextView.text = modelData.title
    }

    /// Cell的內容細部設定 [假資料]
    private fun cellHolderSetting(holder: CellHolder, position: Int) {

        val modelData = modelArray[position] as? CellModel ?: return

        holder.nameTextView.text = modelData.name
        holder.regionTextView.text = modelData.region
    }

    /// Cell的內容細部設定 [Web]
    private fun nameHolderSetting(holder: CellHolder, position: Int) {

        val modelData = modelArray[position] as? UINameModel ?: return

        holder.nameTextView.text = modelData.name
        holder.regionTextView.text = modelData.region
    }
}

AlertDailog

  • 如果再加上一個「讀取轉圈圈」就更完美了,這裡使用「AlertDailog」來做ProgressBar。
<resources>

    <!-- 透明的AlertDailog -->
    <style name="TransparentAlertDialog">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowTitleStyle">@null</item>
        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
        <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:background">@android:color/transparent</item>
    </style>

</resources>
class MainActivity : AppCompatActivity() {

    /// 初始化AlertDialog
    private fun progressAlertDialogSetting() {

        val progressView = layoutInflater.inflate(R.layout.progressbar_dialog, null)
        val builder = AlertDialog.Builder(this, R.style.TransparentAlertDialog)

        builder.setView(progressView)
        builder.setCancelable(false)

        alertDialog = builder.create()
    }
}

更換ICON圖示

  • 利用免費的MakeAppIcon來製做APP ICON,然後放入「res」下的相關位置就可以了喲。

範例程式碼下載

後記

  • 果然是隔行如隔山啊,寫起來超沒效率的,不過為了更上一層樓是值得的,加油。