とっとこメモ

困った時のエンジニアメモ。Unity, Kotlin, Flutter などなど

【Kotlin】RecyclerView のクリックイベントを親のActivity(呼び出し元)で定義する

Adapterファイル

class CustomAdapter(private val customList: Array<String>) : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>(){
 ........
 
   //リスナー格納変数
 lateinit var listener: OnItemClickListener

    // ViewHolderに表示する画像とテキストを挿入
    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        // タップしたとき
        holder.view.setOnClickListener {
            listener.onItemClickListener(it, position, customList[position])
        }
    }

    //インターフェースの作成する
    interface OnItemClickListener{
        //呼び出し元で利用予定の引数を用意しておく
        fun onItemClickListener(view: View, position: Int, clickedText: String)
    }

    // リスナーをセットする
    fun setOnItemClickListener(listener: OnItemClickListener){
        this.listener = listener
    }
}

呼び出し元Activityファイル

        adapter.setOnItemClickListener(object : CustomAdapter.OnItemClickListener {
            override fun onItemClickListener(view: View, position: Int, clickedText: String) {
                tTap.text = "${clickedText}がタップされました。"
            }
        })

【Kotlin】Socket.io で簡易チャットアプリ

ネットワーク周りの設定

AndroidManifest.xml の修正

インターネット通信を許可するために

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

を追加する

http通信を許可するために

android:usesCleartextTraffic="true"

を追加する

以下全文

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.sample.websocket">

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

    <application
        android:allowBackup="true"
        android:usesCleartextTraffic="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ChatBoxActivity"></activity>
        <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>

ライブラリを追加

WebSocketを使用するために socket.io-client を追加

dependencies {
  ・・・・
    implementation('io.socket:socket.io-client:1.0.0') {
        exclude group: 'org.json', module: 'json'
    }
    implementation 'com.google.android.material:material:1.0.0-rc01'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

作成するファイル

ファイル名 内容
MainActivity.kt 簡易ログイン画面
activity_main.xml 簡易ログイン画面レイアウトファイル
Messsage.kt メッセージデータ用クラス
ChatBoxAdapter.kt チャット一覧用アダプターファイル
item.xml チャット一覧用レコードレイアウトファイル
ChatBoxActivity.kt チャット一覧画面
activity_chat_box.xml チャット一覧画面レイアウトファイル

簡易ログイン画面の作成

ユーザ 名を入力するためだけの簡易的な画面 チャット画面に表示させる名前を入力

MainActivity.kt

package com.example.android.sample.websocket

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    companion object{
        const val NICKNAME: String = "usernickname"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        enterchat.setOnClickListener{
            if(nickname.text.isNotEmpty()){
                var i: Intent = Intent(this, ChatBoxActivity::class.java)
                i.putExtra(NICKNAME, nickname.text.toString())
                startActivity(i)
            }
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
        <EditText
            android:id="@+id/nickname"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textSize="30dp"
            android:hint="Enter your name !"/>
        <Button
            android:layout_below="@+id/nickname"
            android:id="@+id/enterchat"
            android:text="Go to Chat"
            android:layout_height="wrap_content"
            android:layout_width="match_parent">
        </Button>
</RelativeLayout>

メッセージ用データクラスを作成

Message.kt

package com.example.android.sample.websocket
data class Message (var nickname: String, var message:String)

メッセージ一覧(RecylerView)のレコードレイアウトファイル

<LinearLayout android:orientation="horizontal"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:id="@+id/nickname"
        android:textSize="15dp"
        android:textStyle="bold"
        android:text="NickName : "
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"></TextView>
    <TextView
        android:id="@+id/message"
        android:text="message "
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </TextView>
</LinearLayout>

メッセージ一覧(RecyclerView)に使用するアダプターを作成

取得したデータをRecylerViewに適用する

ChatBoxAdapter.kt

package com.example.android.sample.websocket

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item.view.*

class ChatBoxAdapter(private val MessageList: ArrayList<Message>) : RecyclerView.Adapter<ChatBoxAdapter.MyViewHolder>() {

    inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val nickName = view.nickname
        val message = view.message
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatBoxAdapter.MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
        return MyViewHolder(view)
    }

    override fun getItemCount(): Int {
        return MessageList.size
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val m: Message = MessageList.get(position)
        holder.nickName.text = m.nickname
        holder.message.text = m.message
    }
}

チャット画面の実装

ChatBoxActivity.kt

package com.example.android.sample.websocket

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import io.socket.client.IO
import io.socket.client.Socket
import io.socket.emitter.Emitter
import kotlinx.android.synthetic.main.activity_chat_box.*
import org.json.JSONException
import org.json.JSONObject
import java.net.URISyntaxException

class ChatBoxActivity : AppCompatActivity() {

    private lateinit var socket: Socket
    public var Nickname: String = ""
    public lateinit var  MessageList: ArrayList<Message>
    public lateinit var chatBoxAdapter: ChatBoxAdapter

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

        val intent: Intent = getIntent()
        Nickname = intent.getStringExtra(MainActivity.NICKNAME)

        try {
            socket = IO.socket("http://[チャットサーバのURL]")
            socket.connect()
            socket.emit("join", Nickname)
        } catch (e: URISyntaxException){
            e.printStackTrace()
        }

        MessageList = arrayListOf()
        messagelist.adapter = ChatBoxAdapter(MessageList)
        messagelist.layoutManager = LinearLayoutManager(this)

        send.setOnClickListener{
            socket.emit("messagedetection", Nickname, message.text.toString())
            message.setText("")
        }

        socket.on("userjoinedthechat", Emitter.Listener {args ->
            runOnUiThread(Runnable {
                val data = args[0] as String
                Toast.makeText(this,data,Toast.LENGTH_SHORT).show();
            })
        })

        socket.on("userdisconnect", Emitter.Listener {args ->
            runOnUiThread(Runnable {
                val data = args[0] as String
                Toast.makeText(this,data, Toast.LENGTH_SHORT).show();
            })
        })

        socket.on("message", Emitter.Listener {args ->
            runOnUiThread(Runnable {
                val data = args[0] as JSONObject
                try{
                    var nickNameString: String = data.getString("senderNickname")
                    var messageString: String = data.getString("message")
                    val m: Message = Message(nickNameString, messageString)
                    MessageList.add(m)

                    chatBoxAdapter = ChatBoxAdapter(MessageList)
                    chatBoxAdapter.notifyDataSetChanged()
                    messagelist.adapter = chatBoxAdapter

                } catch (e : JSONException){
                    e.printStackTrace()
                }
            })
        })
    }

    override fun onDestroy() {
        super.onDestroy()
        socket.disconnect()
    }
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ChatBoxActivity">
    <LinearLayout
        android:weightSum="3"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <View
            android:layout_marginTop="5mm"
            android:id="@+id/separator"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@android:color/darker_gray"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/messagelist"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:clipToPadding="false"
            android:scrollbars="vertical" />

        <LinearLayout
            android:weightSum="3"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <EditText
                android:id="@+id/message"
                android:layout_weight="3"
                android:layout_width="wrap_content"
                android:hint="your message"
                android:layout_height="match_parent" />
            <Button
                android:id="@+id/send"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="#00000000"
                android:text="send"
                />
        </LinearLayout>
    </LinearLayout>
</RelativeLayout>

【Kotlin】Bottom Navigation + Tab Layout

元ネタ

medium.com

Gradle の修正

ナビゲーションを利用するためにbuild.gradle(:app)ファイルを修正する

dependencies {
   ・・・・
    implementation "com.google.android.material:material:1.1.0-alpha06"
    def nav_version = '2.0.0'
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

ボトムナビゲーションのメニュー用レイアウトファイルを作成

res/menu/navigation.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/navigation_call"
        android:icon="@android:drawable/ic_menu_call"
        android:title="Call"/>

    <item android:id="@+id/navigation_search"
        android:icon="@android:drawable/ic_menu_search"
        android:title="Search"/>
</menu>

activity_main.xmlファイルの修正

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/container"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
    </FrameLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/navigation"/>

</LinearLayout>

4つのフラグメントファイルを作成する

CallFragment.kt

ボトムナビゲーションの「Call」に対応するフラグメント

SearchFragment.kt

ボトムナビゲーションの「Search」に対応するフラグメント

VoiceCallFragment.kt

「Call」画面内の「VOICE CALL」タブに対応するフラグメント

VideoCallFragment.kt

「Call」画面内の「VIDEO CALL」タブに対応するフラグメント

それぞれのfragmentに対応するレイアウトファイルも作成しておく

  • fragment_call.xml
  • fragment_search.xml
  • fragment_voice_call.xml
  • fragment_video_call.xml

ここでは fragment_voice_call.xml / fragment_video_call.xml はシンプルなレイアウトファイルとする

fragment_voide_call.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".VideoCallFragment">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/hello_blank_fragment" />

</FrameLayout>

fragment_voice_call.xml も同様のレイアウトファイルを作成する

MainActivity.ktの修正

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    private var content : FrameLayout? = null

    private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
        when(item.itemId){
            R.id.navigation_call -> {
                val fragment = CallFragment()
                addFragment(fragment)
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_search -> {
                val fragment = SearchFragment()
                addFragment(fragment)
                return@OnNavigationItemSelectedListener true
            }
        }
        false
    }

    private fun addFragment(fragment: Fragment) {
        supportFragmentManager
            .beginTransaction()
            .setCustomAnimations(R.anim.nav_default_pop_enter_anim, R.anim.nav_default_exit_anim)
            .replace(R.id.content, fragment, fragment.javaClass.simpleName)
            .commit()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
        val fragment = CallFragment()
        addFragment(fragment)
    }
}

この状態でビルドすると以下のような表示になるはず

f:id:miscellaneous_engine:20200526133742j:plain:w200

タブレイアウト(View Pager)用のAdapterを作成する

MyPageAdapter.kt

class MyPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT){

    override fun getItem(position: Int): Fragment {
        when(position){
            0 -> {
                return VoiceCallFragment()
            }
            else -> {
                return VideoCallFragment()
            }
        }
    }

    override fun getCount(): Int {
        return 2
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return when(position){
            0 -> "Voice Call2"
            else -> {
                "Video Call2"
            }
        }
    }
}

fragment_call.xmlファイルの修正

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.viewpager.widget.ViewPager
            android:id="@+id/viewpager_main"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        tools:ignore="MissingConstraints">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs_main"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabMode="fixed"/>

    </androidx.viewpager.widget.ViewPager>

</androidx.constraintlayout.widget.ConstraintLayout>

CallFragment.ktの修正

package com.example.android.sample.bnavitab

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout


class CallFragment : Fragment() {
    private lateinit var viewPager: ViewPager
    private lateinit var tabs: TabLayout

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view: View = inflater.inflate(R.layout.fragment_call, container, false)
        viewPager = view.findViewById(R.id.viewpager_main)
        tabs = view.findViewById(R.id.tabs_main)

        val fragmentAdapter = MyPagerAdapter(childFragmentManager)
        viewPager.adapter = fragmentAdapter
        tabs.setupWithViewPager(viewPager)

        return view
    }
}

この状態でビルドすると以下のような表示になるはず

f:id:miscellaneous_engine:20200526133712j:plain:w200

【Kotlin】Retrofit デバッグログ出力

gradleファイルに記載を追記

dependencies {
   ・・・
   implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'
}

Retrofitインスタンス生成時に記載を追記

package com.example.android.sample.twicass

import com.example.android.sample.twicass.conf.Constant
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RetrofitInstance {
    companion object{
        fun getRetrofitInstance(): Retrofit{
            //以下追加
            val httpLogging = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
            val httpClientBuilder = OkHttpClient.Builder().addInterceptor(httpLogging)

            val retrofit = Retrofit.Builder().baseUrl(Constant.BASE_URL).addConverterFactory(
                GsonConverterFactory.create()
            ).client(httpClientBuilder.build()).build()
            return retrofit
        }
    }
}

エラー時の処理にLog出力処理を記載する

        call.enqueue(object :Callback<SearchLiveMovieResponse>{
            override fun onFailure(call: Call<SearchLiveMovieResponse>?, t: Throwable?) {
      //処理が失敗した時の内容を出力
                Log.d("fetchItems", "response fail")
                Log.d("fetchItems", "throwable :$t")
            }
            override fun onResponse(
                call: Call<SearchLiveMovieResponse>?,
                response: Response<SearchLiveMovieResponse>?
            ) {
                if(response!!.isSuccessful){
                    list.addAll(response!!.body().movieInfoList)
                    adapter.notifyDataSetChanged()
                }
                Log.d("fetchItems", "response code:" + response.code())
                Log.d("fetchItems", "response errorBody:" + response.errorBody())
            }
        })

【Kotlin】RecyclerViewで無限スクロールを実装する (APIからデータ取得編)

Koltin でRecyclerViewを使って無限スクールを実装する方法をメモります。

今回はAPIからデータを取得して表示する方法編です。

実際にはこちらの方がよく使うパターンだと思います。

gradleファイルの修正

以下のライブラリを追加します。

  • recyclerview:Recycler Viewを利用するため
  • retrofit: APIからHTTP経由でデータを取得するため
  • gson: JSONデータをパースするため
dependencies {
    ・・・
    implementation 'com.squareup.retrofit2:retrofit:2.0.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.0.2'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
}

レイアウトファイルの追加

activity_main.xmlの修正

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

レコード用レイアウトファイル

row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:background="@color/colorPrimaryDark"
    android:layout_height="44dp">

    <TextView
        android:id="@+id/textTitle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dp"
        android:textStyle="bold"
        android:textSize="17sp"
        android:textColor="@android:color/white"
        android:gravity="center_vertical"
        android:text="Lorem Ipsum" />

    <TextView
        android:id="@+id/subtitle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dp"
        android:textStyle="bold"
        android:textSize="17sp"
        android:textColor="@android:color/white"
        android:gravity="center_vertical"
        android:text="Lorem Ipsum" />
</LinearLayout>

ローディング表示用レコードのレイアウトファイル

データ読み込み中に一覧の一番下にローディング表示したレコードを表示させます。 そのレコードのレイアウトファイルです。

progressbar.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:orientation="horizontal">

        <ProgressBar
            android:id="@+id/progressbar"
            android:layout_width="24dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal" />
    </LinearLayout>

</LinearLayout>

Adapterファイルの作成

Adapterとは?

Viewとデータの橋渡しをするもの

RecyclerViewAdapter.kt

package com.example.android.sample.retrofitinfinite

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.row.view.*
import java.lang.IllegalArgumentException

class RecyclerViewAdapter(var list: ArrayList<Data>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>(){
    companion object{
        private const val VIEW_TYPE_DATA = 0
        private const val VIEW_TYPE_PROGRESS = 1
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when(viewType){
            VIEW_TYPE_DATA -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.row, parent, false)
                DataViewHolder(view)
            }
            VIEW_TYPE_PROGRESS -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.progressbar, parent, false)
                ProgressViewHolder(view)
            }
            else -> throw IllegalArgumentException("Different View type")
        }
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if(holder is DataViewHolder)
        {
            holder.textTitle.text = list.get(position).title
            holder.textSubtitle.text = list.get(position).subtitle
        }
    }

    inner class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
        var textTitle = itemView.textTitle
        var textSubtitle = itemView.subtitle
        init {
            itemView.setOnClickListener{
                Toast.makeText(itemView.context, list.get(adapterPosition).title, Toast.LENGTH_LONG).show()
            }
        }
    }
    inner class ProgressViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
    }

    override fun getItemViewType(position: Int): Int {
        var viewtype = list.get(position).category
        return when(viewtype){
            "data" -> VIEW_TYPE_DATA
            else -> VIEW_TYPE_PROGRESS
        }
    }
}

Dataクラスの作成

データクラスとは?

何もしない、データを保持するためだけのクラス。 Kotlinでは、これは データクラス と呼ばれ、 data としてマークされています。

1レコードごとのデータをここで定義する

Data.kt

package com.example.android.sample.retrofitinfinite

class Data(var category: String) {
    var title: String? = null //タイトル
    var subtitle: String? = null //サブタイトル
    init {
        this.category = category
    }
}

インターフェースの実装

Retrofit では API のリクエスト先のエンドポイントを Interface で定義します。

DataApi.kt

package com.example.android.sample.retrofitinfinite

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query

interface DataApi {
    @GET("data.php")
    fun getData(@Query("index") index: Int): Call<List<Data>>
}

APIリクエストを生成するクラス

定義したインターフェースからAPIのリクエストを生成するクラスを作成する

RetrofitInstance.kt

package com.example.android.sample.retrofitinfinite

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RetrofitInstance {
    companion object{
        fun getRetrofitInstance(): Retrofit{
            val retrofit = Retrofit.Builder().baseUrl("https://www.androidride.com/").addConverterFactory(
                GsonConverterFactory.create()
            ).build()
            return retrofit
        }
    }
}

MainActivity.tk の修正

必要なファイルは揃ったのでMainActivity.ktを修正します。

package com.example.android.sample.retrofitinfinite

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Adapter
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit

class MainActivity : AppCompatActivity() {

    lateinit var list: ArrayList<Data>
    lateinit var adapter: RecyclerViewAdapter

    var notLoading = true

    lateinit var layoutManager: LinearLayoutManager
    lateinit var api: DataApi
    lateinit var retrofit: Retrofit

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        list = ArrayList()
        adapter = RecyclerViewAdapter(list)
        recyclerview.setHasFixedSize(true)

        layoutManager = LinearLayoutManager(this)
        recyclerview.layoutManager = layoutManager
        recyclerview.adapter = adapter

        retrofit = RetrofitInstance.getRetrofitInstance()
        api = retrofit.create(DataApi::class.java)

        load(0)

        addScrolllistener()
        


    }

    private fun addScrolllistener() {
        recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener(){
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                if(notLoading && layoutManager.findLastCompletelyVisibleItemPosition() == list.size - 1){
                    list.add(Data("progress"))
                    adapter.notifyItemInserted(list.size - 1)
                    notLoading = false
                    val call: Call<List<Data>> = api.getData(list.size -1)
                    call.enqueue(object : Callback<List<Data>>{
                        override fun onResponse(
                            call: Call<List<Data>>?,
                            response: Response<List<Data>>?
                        ) {
                            list.removeAt(list.size - 1)
                            adapter.notifyItemRemoved(list.size)
                            if(response!!.body().isNotEmpty()){
                                list.addAll(response.body())
                                adapter.notifyDataSetChanged()
                                notLoading = true
                            } else {
                                Toast.makeText(applicationContext, "End of data reached", Toast.LENGTH_LONG).show()
                            }
                        }
                        override fun onFailure(call: Call<List<Data>>?, t: Throwable?) {
                        }
                    })
                }
            }
        })
    }

    private fun load(i: Int) {
        val call: Call<List<Data>> = api.getData(0)
        call.enqueue(object: Callback<List<Data>>{
            override fun onFailure(call: Call<List<Data>>?, t: Throwable?) {
            }

            override fun onResponse(call: Call<List<Data>>?, response: Response<List<Data>>?) {
                if(response!!.isSuccessful){
                    list.addAll(response!!.body())
                    adapter.notifyDataSetChanged()
                }
            }
        })
    }
}