Android ListViewに変更を通知する

一度、表示されたListViewに対して変更された内容を通知する方法。

よくあるケースとして

  • セルデータの編集・削除を行った
  • ネットワークを通じて新しいデータを取得

などが想定されます。

例としてListViewのitemを長押しして選択対象を削除するというユースケースの処理を記述します。

基本のListView出力

class MainActivity : AppCompatActivity() {

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

        val listData = arrayListOf<String>("a", "b", "c")
        listView = findViewById<ListView>(R.id.list_view)
        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listData)
        listView.adapter = adapter
}

長押しイベントの取得

長押しイベントはOnItemLongClickListener()で取得します。kotlinで記述すると以下の通り。Booleanの戻り値が必要。

listView.setOnItemLongClickListener { parent, view, position, id ->
    true
}

長押しイベントの実装

adapterに渡したArrayListの指定indexを削除。データ変更の通知はadapterのnotifyDataSetChanged()メソッドで通知します。

listView.setOnItemLongClickListener { parent, view, position, id ->
    listData.removeAt(position)
    adapter.notifyDataSetChanged()
    true
}

配列差替え時の注意

全リフレッシュをするような時に

var listData = arrayListOf<String>("a", "b", "c")
listView = findViewById<ListView>(R.id.list_view)
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listData)
listView.adapter = adapter
//ArrayListをリフレッシュ
listData = arrayListOf<String>("e", "f", "g")
adapter.notifyDataSetChanged()

としても、val listData = arrayListOf<String>("e", "f", "g")で別のarrayListOfオブジェクトが生成される(adapterに渡したArrayListとは別オブジェクトになる)ので、更新されません。clear()後にadd()することで解決。または新しいadapterを作成します。

clear()を利用の例

listData.clear()
listData.add("e")
listData.add("f")
listData.add("g")
adapter.notifyDataSetChanged()

新しいadapter作成の例 この場合はnotifyDataSetChanged()は必要ありません。

listData = arrayListOf<String>("e", "f", "g")
listView.adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listData)


Android ActionBar/toolBarにmenuItemを配置

アコーディオンメニューとなるアイテムを設置

よくある以下のようなアコーディオンメニューを設置します。

以下の例はActionBarに設置したものです。

f:id:letitride:20190611135846p:plain:h160

f:id:letitride:20190611135859p:plain:h160

リソースファイルの作成

res/menu/にmenu resource fileを作成します。

main_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android">
    <item
            android:id="@+id/menu1"
            android:title="メニュー1"
            app:showAsAction="never"/>
    <item
            android:id="@+id/menu2"
            android:title="メニュー2"
            app:showAsAction="never"/>
</menu>

app:showAsActionがActionBarに配置される定義となります。

ActionBarに配置せずにオーバーフローメニューとするので、neverを指定します。

リソースファイルの設置

作成したmenu resource fileはActivityで配置します。

アイテムの設置
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    getMenuInflater().inflate(R.menu.main_menu, menu);
    return super.onCreateOptionsMenu(menu)
}
リスナー
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
    if( item?.itemId == R.id.menu1 ){ }
    else if( item?.itemId == R.id.menu2 ){ }
    return super.onOptionsItemSelected(item)
}

アイコンアイテムを設置

toolBarにアイコンアイテムを設置します。

f:id:letitride:20190611142151p:plain:h160

リソースファイルの作成

item_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:title="アイテム1" android:id="@+id/menu_item1" android:icon="@android:drawable/btn_star_big_on"
          app:showAsAction="always"/>
    <item android:title="アイテム2" android:id="@+id/menu_item2" android:icon="@android:drawable/btn_star_big_off"
          app:showAsAction="always"/>
</menu>

app:showAsActionは常にアプリバーに配置するalwaysを指定します。但し、アイテムと他の項目が重なる可能性があります。スペースに空きがある場合のみアプリバーに配置するにはifRoomを指定します(空きがない場合はオーバーフローメニューに入ります)。

リソースファイルの設置 / リスナー

ActionBarと同様に作成したmenu resource fileはActivityで配置します。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    //アイテムの設置
    toolbar.inflateMenu(R.menu.item_menu)
    //リスナー
    toolbar.setOnMenuItemClickListener {
        if(it.itemId == R.id.menu_item1){ }
        else if(it.itemId == R.id.menu_item1){ }
        true
    }
}
コードからアイコンの動的変更

例えば押下時にアイコン画像をoff -> onに切り替えるようなことをするには以下のように記述します。

toolbar.setOnMenuItemClickListener {
    if(it.itemId == R.id.menu_item1){
        it.setIcon(android.R.drawable.btn_star_big_on)
    }
    true
}

リスナーをかまさない場合は、

toolbar.menu.findItem(R.id.menu1).setIcon(android.R.drawable.btn_star_big_on)

のようにfindItem()を使用してアクセスします。


Android ActionBarとToolBarについてのメモ

この2つの違いが曖昧なので、調べたことをメモしておきます。

ActionBar

どこで定義されている?

まずActionBarについて。ActionBarはアプリのテーマとして自動で定義されているもの。定義は、AndroidManifest

<application
    <!-- 中略 -->
    android:theme="@style/AppTheme >

として記載されていて、変更したい場合は、style.xmlname="AppTheme"の要素に以下のように定義を追記したり、属性値を変更すればよい。

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- 中略 -->
    <item name="actionBarStyle">
    </item>
<style>

また、特別なことをしない限り自動でlayoutされ表示されます。

ActionBarの動作の記述

前述の通り、自動で生成されるViewですのでActivity内のコールバックメソッドで動作を記述するものがあります。以下は一例です。R.menu.~~~のリソースファイルについての説明は省略します。

class MainActivity : AppCompatActivity() {

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

        //タイトル名の変更
        supportActionBar?.title = "ActionBar"
        //タイトルラベルの左側のナビゲーションアイテムの設置
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        //ナビゲーションアイテムを変更
        supportActionBar?.setHomeAsUpIndicator(android.R.drawable.sym_def_app_icon)

        //タイトルラベル右側のメニューアイテムの設置
        override fun onCreateOptionsMenu(menu: Menu?): Boolean {
            getMenuInflater().inflate(R.menu.menu_item, menu);
        }

        //設置したアイテムのリスナーコールバック
        override fun onOptionsItemSelected(item: MenuItem?): Boolean {
    }    

onOptionsItemSelected()はすべてのアイテムからのコールバックとなるので、渡された引数MenuItem?からどのアイテムが選択されたかを調べて処理する必要があります。

ActionBarを表示しない

ActionBarを表示しないようにするには

アプリ全体

アプリ全体で非表示にするにはstyle.xmlAppTheme

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="windowNoTitle">true</item>
    <!-- 中略 -->
</style>

とします。

Activity個別

個別のActivityで制御する場合は、2種類の方法があります。

AndroidManifestで制御する

AndroidManifestで制御するには以下のようにandroid:themeを記述してActionBarを無効にします。

<activity
    android:name=".MainActivity"
    android:theme="@style/AppTheme.NoActionBar">
Actiityコードで制御する

Activityの上のコード制御するにはhide()メソッドを使用します。

class MainActivity : AppCompatActivity() {

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

但し、supportActionBar?.hide()をしたあとでsetSupportActionBar(toolbar)とするとクラッシュするので、toolbarを使用する時には注意。

ToolBar

ToolBarは各layoutに記述するViewとなります。ほぼActionBarと同等な機能を有しますが、CoordinatorLayoutと組み合わせて動的にlayout制御することができたり、カスタマイズしやすいコンポーネント(らしい)です。

定義

定義はlayoutにて以下のように記述します。

<android.support.v7.widget.Toolbar
           android:id="@+id/toolbar"
           android:layout_width="match_parent"
           android:layout_height="?attr/actionBarSize"
           android:background="?attr/colorPrimary"
           android:theme="@style/ThemeOverlay.AppCompat.ActionBar" />

ToolBar定義時のActionBar

また記述後、以下のようにActionBarとToolBarが重複して表示されるケースがあります。

f:id:letitride:20190610140611p:plain:h300

これは上記のActionBarの非表示化を行っていない為、ToolBarの上にActionBarが表示されてしまっている状態です。

また、画像のようにAppBarLayoutを使用した場合、ConstraintLayoutがToolBarの下に潜り込んでいる状態になる時があります。この場合、ConstraintLayoutにてapp:layout_behaviorを設定して問題を修正しました。

app:layout_behavior="@string/appbar_scrolling_view_behavior"

ToolBarの動作の記述

ToolBarはViewですので、他Viewと同様にメソッドにアクセスして利用します。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_post_detail)
  
        //タイトルの変更
        toolbar.setTitle(post.title)
        //タイトルラベルの左側のナビゲーションアイテムの設置
        toolbar.setNavigationIcon(android.R.drawable.ic_delete)
        //ナビゲーションアイテムのリスナー
        toolbar.setNavigationOnClickListener {  }

        //タイトルラベルの右側のメニューアイテムの追加
        toolbar.inflateMenu(R.menu.menu_item)
        //メニューアイテムのリスナー
        toolbar.setOnMenuItemClickListener { menuItem ->
        }

ナビゲーションアイテムのリスナーは個別にありますが、メニューアイテムsetOnMenuItemClickListener()のリスナーは各アイテム共通ですので、引数menuItemからどのアイテムがClickされたかを判断して処理を記述します。

setSupportActionBar(toolbar)

setSupportActionBar()はToolBarをActionBarのメソッドを利用して操作できるAdapterのようです。既存で実装していたActionBarからToolBarに変更する時のサポートメソッドのように見受けられます?

このメソッドを実行したあとは、toolbar自体が持つメンバメソッドが機能しなくなるので注意します。


Android SQLiteの使用とmigration

schemaの作成

スキーマはSQLiteOpenHelperを実装したクラスで作成する。 DB_VERSIONはmigrationを行う際にインクリメントすることでonUpgrade()に渡される。

val DB_NAME = "MyApplication"
val DB_VERSION = 1
class MyApplicationDBOpenHelper(context: Context): SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL( "create table foo ( _id INTEGER PRIMARY KEY AUTOINCREMENT, titleId integer, title text, created_at timestamp  )" )
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
}

onCreate

onCreate()はDB_NAMEのデータベースがアプリ上にまだ作成されていない場合に呼び出される。また実行時に現在のDB_VERSIONを実行済みバージョンとして記録する。

Primary Keyは_idとするのがお作法らしい。違っていると思わぬところでハマることがあるとか。

尚、onCreate()onUpgrade()は同時に呼び出されることはないのでmigration時には考慮する必要がある。

onUpgrade / migration

onUpgrade()は実行済みバージョンとソース上のDB_VERSIONに差異があった場合に実行される。但し、過去に渡ってのすべてのmigrationは管理しておらず、あくまで、そのアプリが以前に利用していたDB_VERSION:oldVersionが渡されるのみである。よって、実装上で過去に行ったすべてのmigrationを管理しておく必要がある。

例えば、DB_VERSION 1 -> 2 -> 3 と3度のmigrationを行った、以下のパターンを考慮する必要がある。

  • DB_VERSION:1 から 2を介さずに3のアプリをインストール
  • DB_VERSION:3からの新規ユーザ
DB_VERSION:1 から 2を介さずに3のアプリをインストール
private fun version2(db: SQLiteDatabase?){
    db?.execSQL("alter table foo add t1 text" )
}

private fun version3(db: SQLiteDatabase?){
    db?.execSQL("alter table foo add t2 text" )
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    if( oldVersion < 2 ){
        version2(db)
    }
    if( oldVersion < 3 ){
        version3(db)
    }
}

こうすることでDB_VERSION:1からのユーザは履歴を追ってmigrationを完了させることができる。

DB_VERSION:3からの新規ユーザ

また、DB_VERSION:3からの新規ユーザはononUpgrade()はトリガされないので、onCreate()```も同様で最新のスキーマに更新する必要がある。

override fun onCreate(db: SQLiteDatabase?) {
    db?.execSQL( "create table foo ( _id INTEGER PRIMARY KEY AUTOINCREMENT, titleId integer, title text, created_at timestamp  )" )
    version2(db)
    version3(db)
}

SQLクエリの実行

INSERT

try {
    //thisはContext
    val db = MyApplicationDBOpenHelper(this).writableDatabase
    db.execSQL(
        "INSERT INTO foo ( titleId, title, created_at ) values (?, ?)", 
        arrayOf( 1, "タイトル名", SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(Date()) )
     )
}catch (e:IllegalArgumentException){
     Log.d("LOG_ERROR", e.stackTrace.toString() )
}

SQLiteのTIMESTAMP型は内部的には文字列で扱っている?よう。

SELECT

rawQuery()のバインドするパラメータはすべてStringである必要があるようで、例えintegerであっても文字列として渡す。 TIMESTAMP型はgetString()で取り出せる。

val db = MyApplicationDBOpenHelper(this).readableDatabase
val cursor = db.rawQuery("select titleId, created_at from foo where titleId = ?", arrayOf(1.toString()))
while (cursor.moveToNext()) {
    Log.d( "FOO_RECORD", cursor.getInt(cursor.getColumnIndex("titleId")).toString() )
    Log.d( "FOO_RECORD", cursor.getString(cursor.getColumnIndex("created_at")).toString() )
}
cursor.close()

尚、SELECT時に

Failed to read row 0, column -1 from a CursorWindow which has 1 rows, 1 columns.

のようなエラーが出た場合、getColumnIndex("String")に渡すカラム名はDDLに記載したカラム名と大文字小文字も含めて一致してない場合がある。上記の例でいうと以下のようなケース。

cursor.getInt(cursor.getColumnIndex("titleID") )
/*
正しくは
cursor.getInt(cursor.getColumnIndex("titleId") )
*/

また、Primary keyが_idでない場合も出るとか出ないとか。


Android toolbarと配置するitemについてのメモ

f:id:letitride:20190608083720p:plain:h160

所謂この部分、ツールバー?(iOSではナビゲーションバーかな?)と呼ぶらしいんだけど、これについての取り扱いについて色々ハマったのでメモ。

タイトル名を変更
toolbar.setTitle("ツールバー")

または、

setSupportActionBar(toolbar)
supportActionBar?.setTitle("ツールバー")

ただし、toolbar.setTitle()の場合、先にsetSupportActionBar(toolbar)を読んでいるとダメ。

ダメな例

setSupportActionBar(toolbar)
toolbar.setTitle("ツールバー")

OKな例

toolbar.setTitle("ツールバー")
setSupportActionBar(toolbar)
ナビゲーションアイコンを設置

画像でいうところのツールバー左の❌アイコン。

//アイコンの設置
toolbar.setNavigationIcon(android.R.drawable.ic_delete)
//リスナー定義
toolbar.setNavigationOnClickListener {
}

ただし、setNavigationOnClickListener()リスナー定義後に、setSupportActionBar(toolbar)を呼ぶとリスナーが効かなくなる。

ダメな例

toolbar.setNavigationOnClickListener {
}
setSupportActionBar(toolbar)
オブションメニュー / オプションアイコン

右側の⭐️マークアイコン。これはmenu resourceを先に作っておく。res/menu/(無い場合はres/からresource directoryを作成)にこういったファイルを作成

menu_item.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
             xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:title="sample"
               android:id="@+id/star"
               android:icon="@android:drawable/btn_star"
               app:showAsAction="always"/>
</menu>

override fun onCreateOptionsMenu()で設置するが、setSupportActionBar(toolbar)を呼び出しておかないとコールバックされない

class MyActivity : AppCompatActivity() {

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

        setSupportActionBar(toolbar)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_item, menu)
        return super.onCreateOptionsMenu(menu)
    }

   //リスナー定義 複数設置できるのでitemIdごとに定義できる
    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        if(item?.itemId == R.id.star){
        }
        return super.onOptionsItemSelected(item)
    }
}

または、setSupportActionBar(toolbar)を呼ばずに、toolbar.inflateMenu()で設置する。

class MyActivity : AppCompatActivity() {

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

        toolbar.inflateMenu(R.menu.menu_item)
        toolbar.setOnMenuItemClickListener {
            if(it.itemId == R.id.star){
            }
            true
        }
    }
}


Android kotlin Activity間のオブジェクトの受け渡し

"intent object 渡す"とかでググると色々ヒットするが、渡されたオブジェクトは呼び出し先で新しく生成されたオブジェクトとして扱われることを備忘として書いておく。

例えば、以下のような定義のオブジェクトを渡す場合

class DataState(
    var data:String = ""
):Serializable{
    fun funcA(){}
}

呼び出し元

val intent = Intent( this, toActivity::class.java)
//this.dataStateはメンバに宣言
this.dataState = DataState("origin data")
Log.d( "sendObject::", dataState.toString() )
intent.putExtra("dataState", dataState)
startActivity(intent)

呼び出し先

val dataState = intent.getSerializableExtra("dataState") as DataState
Log.d("receiveObject::", dataState.toString())

のように記述するのだけれど、この時、呼び出し元と呼び出し先はJVM上では別のオブジェクトとして管理される。

D/sendObject::: dataState@a711d48
D/receiveObject::: dataState@225adb2

なので、呼び出し先で渡されたオブジェクトの状態を変更しても呼び出し元のオブジェクトには干渉しない。

呼び出し先で以下の操作をしても

val dataState = intent.getSerializableExtra("dataState") as DataState
dataState.data = "updated data"
finish()

呼び出し元のオブジェクトは変更されない。

this.dataState.data // "origin data"

呼び出し元に変更を伝えるには、呼び出し先でstartActivityForResult()して、呼び出し元で

dataState.data = "updated data"
val intent = Intent()
intent.putExtra("dataState", dataState)
setResult(RESULT_OK, intent)
finish()

とするしかないのかな?この時、onPause()とかライフサイクルイベントではsetResult()されない?受け取れない?みたいなので、キーイベントに応じてfinish()してあげると良さげ。以下はバックキーで呼び出し元に戻る時にオブジェクトを送信している。

override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    if(keyCode == KeyEvent.KEYCODE_BACK){
        val intent = Intent()
        intent.putExtra("dataState", this.dataState)
        setResult(RESULT_CANCELED, intent)
        finish()
    }
    return super.onKeyDown(keyCode, event)
}


Android Kotlin ListViewの次のN件の読み込み

よくあるListViewの最終セルに次のN件の読み込み的なやつ。ページネーションと読んでいいのかどうか。

Activity

class MainActivity : AppCompatActivity() {

    lateinit var myListView:ListView
    lateinit var myAdapter:MyAdapter

    val listItems = ArrayList<String>()
    var columnIndex = 0

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

        //listItemの追加
        addItem()
        myListView = findViewById<ListView>(R.id.postsListView)
        myAdapter = MyAdapter(this, listItems)
        myListView.adapter = myAdapter
    }

    private fun addItem(){
        listItems.add( columnIndex.toString() )
        columnIndex++
    }

    public fun loadNextItem(){
        addItem()
        //itemの変更をadapterに通知
        myAdapter.notifyDataSetChanged()
    }
}

Adapter

ViewHolder
private class ViewHolder(view: View) {
    val name = view.findViewById<TextView>(R.id.name)
}
各セルになるViewの作成メソッド

itemセルはlist_item_cell.xml、次へのセルはlist_next_cell.xmlにlayout定義されているものとする。

次へのセルとなるViewにもタグをつけておくとgetView()時に再利用されるconvertViewの識別に利用できる。

mainActivity.loadNextItem()notifyDataSetChanged()this.notifyDataSetChanged()としてもこのケースでは動作するが、ほとんどのケースでは別スレッドから取得したデータをUIスレッドに渡すと思うので、Activity側でadapterに通知するほうがハマりどころが少ない。

class MyAdapter(
    val context:Context,
    val items: ArrayList<String>
) :BaseAdapter() {

    private val inFlater = LayoutInflater.from(context)

    private fun makeNextView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_next_cell, parent, false)
        view.tag = "nextView"
        view.setOnClickListener {
            val mainActivity = context as MainActivity
            mainActivity.loadNextItem()
        }
        return view
    }

    private fun createView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_item_cell, parent, false)
        view.tag = ViewHolder(view)
        return view
    }
}
getCount()

getCount()は次へセルの分を足してreturnする

override fun getCount(): Int {
    return items.size + 1
}
getView()

positionがitems配列の終端に達した時はページネーションセルを返す。

またconvertViewが次へになるセルのリサイクルになる時がある為、セットされたタグで再利用可能であるViewか確認を行う。

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

    //ページネーションセル
    if(items.size <= position){
        return makeNextView(parent)
    }
    //コンテンツセル
    var view = convertView ?: createView(parent)
    if(view.tag == "nextView"){
        view = createView(parent)
    }
    val viewHolder = view.tag as ViewHolder
    viewHolder.name.text = items[position].title
    return view
}

MyAdapter

上記を記述したAdapterクラス

class MyAdapter(
    val context:Context,
    val items: ArrayList<String>
) :BaseAdapter() {

    private val inFlater = LayoutInflater.from(context)

    private class ViewHolder(view: View) {
        val name = view.findViewById<TextView>(R.id.name)
    }

    private fun makeNextView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_next_cell, parent, false)
        view.tag = "nextView"
        view.setOnClickListener {
            val mainActivity = context as MainActivity
            mainActivity.loadNextItem()
        }
        return view
    }

    private fun createView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_item_cell, parent, false)
        view.tag = ViewHolder(view)
        return view
    }

    override fun getCount(): Int {
        return items.size + 1
    }


    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        //ページネーションセル
        if(items.size <= position){
            return makeNextView(parent)
        }
        //コンテンツセル
        var view = convertView ?: createView(parent)
        if(view.tag == "nextView"){
            view = createView(parent)
        }
        val viewHolder = view.tag as ViewHolder
        viewHolder.name.text = items[position].title
        return view
    }
}

実際にIDEで記述して実行したわけではないので、ところどころTypoだったり、足りないところがあるかもしれません。