今天要來寫關於在以Kotlin開發Android APP 中,關於協程Coroutines的實用案例

這是我之前一個案子的故事了

 之前有一個案子,對方在串接API時要求要對10組API做請求,然後獲得各自的Token與資料後來顯示畫面

剛開始我在做的時候,我就很單純地想說

反正就一個一個請求然後顯示就好吧

截圖 2023-02-04 下午5.20.50

只能說,當時太年輕了(´-ω-`),結果當然是可想而知

由於每個發送都要等到上一個請求回傳才能繼續往下,因此其載入的速度簡直慢到以為當機了(嘆

後來去查了很多關鍵字,什麼「非同步處理啊」、「共進處理啊」亂七八糟的

後來也忘了是查了哪個關鍵字,總算被我找到了那種「可以一次發送所有API並一次取得回傳」的方法(有夠混亂.....

 

其流程大概就像這樣

截圖 2023-02-04 下午5.30.21

而這樣的功能是用在什麼場景呢?沒錯,適合一次請求多個不一樣的API

而我之所以一直沒寫關於Coroutines的文章的原因,很大一部分就是因為一直找不到適合的公開API來做這個範例

不過很感謝一隻加密貨幣的網站CoinGeko提供免費API供我們使用,因此今天的範例就是要對CoinGeko來做同步API請求並顯示加密貨幣資訊的範例

這次的範例除了主角的Coroutines之外,同時也會使用

Glide、okHttp3與GSON的函式庫

 

這三個函式庫的實作可以參考這些文章喔~

->碼農日常-『Android studio』以okHttp第三方庫取得網路資料(POST、GET、WebSocket)

->碼農日常-『Android studio』撰寫Android相機及使用Glide顯示高畫質照片

->碼農日常-『Java&Kotlin』使用Gson解析JSON字串

 

範例結果

2023-02-04 16.34.25

Github

-> https://github.com/thumbb13555/AndroidBlogExamples/tree/main/CoroutineWithCrypto

 


 

1. 環境架構與介面

 

首先要來先處理好程式碼以外的所有東西(笑),首先需要加入網路權限

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.CoroutineWithCrypto"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

 

再來要來輸入需要的函式庫

build.gradle

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.noahliu.coroutinewithcrypto'
    compileSdk 33

    defaultConfig {
        applicationId "com.noahliu.coroutinewithcrypto"
        minSdk 28
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.6.0'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'

    implementation 'com.squareup.okhttp3:okhttp:4.9.0'
    implementation 'com.google.code.gson:gson:2.10.1'
    implementation 'com.github.bumptech.glide:glide:4.11.0'


}

 

最後畫一下介面,首先是主介面

activity_main.xml

截圖 2023-02-04 下午6.19.10

<?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_display"
        android:layout_width="409dp"
        android:layout_height="729dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

最後是RecyclerView的item

item.xml

截圖 2023-02-04 下午6.40.34

 

<?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="wrap_content"
    android:layout_margin="6dp">

    <TextView
        android:id="@+id/textView_Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:text="TextView"
        android:textColor="@color/black"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@+id/imageView"
        app:layout_constraintTop_toTopOf="@+id/imageView" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_margin="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_launcher_foreground" />

    <TextView
        android:id="@+id/textView_Price"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        android:gravity="end"
        android:text="TextView"
        android:textColor="@color/black"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/textView_Title"
        app:layout_constraintTop_toBottomOf="@+id/textView_Title" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

OK,程式Code以外的東西就是這些

接下來終於(?)要進入重點了┌(。Д。)┐

 


 

2. 資料架構與API

 

首先提一下這次的API

這次的API是取用加密貨幣情報網站CoinGeko的開放API

基本上幾乎是什麼資料都可以拿到,不過傳輸上有次數限制,總之就是不能短時間內一直傳資料就是了

因此,在如果你在做這個範例Code或者拿我提供的Demo在玩的話,請不要一次傳輸太多次哦!不然會暫時被鎖個幾分鐘

這裡是CoinGeko的API網站,請參考

->https://www.coingecko.com/en/api/documentation

而我用的API則是

->https://api.coingecko.com/api/v3/coins/evmos

把後面換成別的幣種的id就可以換成別的幣種的資訊了,基本上本次的實作也是用這種方式完成的

 

最後是本次的資料夾內容供參考

截圖 2023-02-04 下午7.02.02

 


 

3. 建立實體類及方法

 

首先從簡單的先下手,我建立了一個HttpModel,裡面放了兩個方法,如下

HttpModel.kt

object HttpModel {

    fun sendGet(url:String):String{
        Log.d("TAG", "sendGet $url ")
        val arrayList = ArrayList<String>()
        val client = OkHttpClient().newBuilder().build()
        val request = Request.Builder()
            .url(url)
            .build()
        val call = client.newCall(request)
        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                arrayList.add("UnknownHostException")
            }

            override fun onResponse(call: Call, response: Response) {
                arrayList.add(response.body!!.string())
            }
        })//respond
        while (arrayList.isEmpty()) {
            SystemClock.sleep(1)
            if (arrayList.isNotEmpty()) break
        }
        return try {
            arrayList[0]
        } catch (e: Exception) {
            e.toString()
        }
    }

    suspend fun getAllCoin(array: Array<String>): List<String> {
        return coroutineScope {
            return@coroutineScope (array.indices).map {
                async(Dispatchers.IO) {
                    return@async sendGet(
                        "https://api.coingecko.com/api/v3/coins/${array[it]}"
                    )
                }
            }.awaitAll()
        }
    }

}

 

其中,sendGet是我自己寫的,封裝送出Get請求的方法

而這個

 

suspend fun getAllCoin(array: Array<String>): List<String> {
    return coroutineScope {
        return@coroutineScope (array.indices).map {
            async(Dispatchers.IO) {
                return@async sendGet(
                    "https://api.coingecko.com/api/v3/coins/${array[it]}"
                )
            }
        }.awaitAll()
    }
}

基本上就是一個連續送出請求的方法

 

完成了這邊後,再來先完成一下實體類

還蠻簡單的,直接給

CoinInfo.kt

package com.noahliu.coroutinewithcrypto

class CoinInfo {
    var id: String = ""
    var name:String = ""
    var image:Image? = null
    var market_data:MarketData? = null


    class MarketData{
        var current_price:CurrentPrice? = null
    }
    class CurrentPrice{
        var usd:Double = 0.0
        var twd:Double = 0.0
        var jpy:Double = 0.0
    }

    class Image{
        var thumb:String = ""
        var small:String = ""
        var large:String = ""
    }

}

 

最後是RecyclerView的Adapter

RecyclerViewAdapter.kt

 

class RecyclerViewAdapter(var context: Context) :
    RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
    private var list: List<CoinInfo> = ArrayList()

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val tvTitle: TextView = view.findViewById(R.id.textView_Title)
        val tvPrice: TextView = view.findViewById(R.id.textView_Price)
        val igImage: ImageView = view.findViewById(R.id.imageView)
    }

    @SuppressLint("NotifyDataSetChanged")
    fun setData(list: List<CoinInfo>) {
        this.list = list
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.item, parent, false)
        )
    }


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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.tvTitle.text = list[position].name
        holder.tvPrice.text = "兌美金價格:${list[position].market_data!!.current_price!!.usd}"
        Glide.with(context).load(list[position].image!!.small).fitCenter().into(holder.igImage)
    }
}

 

OK,到此為止除了HttpModel內封裝的方法之外,大概都沒什麼特別好提的

那接著要進到重點了

 


 

4. 撰寫MainActivity

 

先看Code

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var adapter:RecyclerViewAdapter
    private val array = arrayOf("bitcoin","ethereum","dogecoin","evmos","likecoin","bitsong","tezos")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val recyclerView:RecyclerView = findViewById(R.id.recyclerview_display)
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = RecyclerViewAdapter(this)
        recyclerView.adapter = adapter


        GlobalScope.launch {
            val getCoin = runBlocking {
                return@runBlocking HttpModel.getAllCoin(array)
            }
            val list = ArrayList<CoinInfo>()
            val gson = Gson()
            getCoin.forEach {
                val info = gson.fromJson(it,CoinInfo::class.java)
                if (info.name != ""){
                    list.add(info)
                }
            }
            runOnUiThread {
                adapter.setData(list)
                Toast.makeText(this@MainActivity,"資料已完成",Toast.LENGTH_LONG).show()
            }
        }
    }
}

首先,在橘底白字的部分,我先宣告了想取得的加密貨幣資訊

同樣,也是做個參考。如果把Log放在SendGet的話,就會在畫面一起始的瞬間看到程式這裡一口氣送出了以下請求

截圖 2023-02-04 下午7.44.57

 

再來是綠底白字的部分,基本上在Coroutines裏面,舉凡出現launch的時候裡面的東西相當於就是非同步處理了(與Thread一樣的意思)

GlobalScope這部分是指說這個協程的作用範圍是Global,但我也沒想那麼多反正就用(欸

再來是runBlocking 的部分,這部分就是實際跑回圈了。同時遠端返回的資料也都會從這邊去做回傳

 

最後runOnUiThread就不用我說了,UI更新XD

到這邊,基本上按下執行後大概等個一秒左右資訊就會顯示出來了囉~

 


 

基本上,關於Coroutines我其實還有很多不能說算懂的地方

不過我個人是想藉著這篇文章,來記錄下我遇到的問題以及我的解決方案

至於關於Coroutins的詳細介紹,我當初是看這篇文章去理解的

-> https://medium.com/jastzeonic/kotlin-coroutine-%E9%82%A3%E4%B8%80%E5%85%A9%E4%BB%B6%E4%BA%8B%E6%83%85-685e02761ae0

然後那個功能當時也忘了是怎麼摸出來的,事隔一段時間回頭看以前的自己真是天才啊(欸

 

好啦,今天文章分享到這裡,最後...

TK

arrow
arrow
    創作者介紹
    創作者 碼農日常 的頭像
    碼農日常

    碼農日常大小事

    碼農日常 發表在 痞客邦 留言(1) 人氣()