今天要來寫關於在以Kotlin開發Android APP 中,關於協程Coroutines的實用案例
這是我之前一個案子的故事了
之前有一個案子,對方在串接API時要求要對10組API做請求,然後獲得各自的Token與資料後來顯示畫面
剛開始我在做的時候,我就很單純地想說
反正就一個一個請求然後顯示就好吧
只能說,當時太年輕了(´-ω-`),結果當然是可想而知
由於每個發送都要等到上一個請求回傳才能繼續往下,因此其載入的速度簡直慢到以為當機了(嘆
後來去查了很多關鍵字,什麼「非同步處理啊」、「共進處理啊」亂七八糟的
後來也忘了是查了哪個關鍵字,總算被我找到了那種「可以一次發送所有API並一次取得回傳」的方法(有夠混亂.....
其流程大概就像這樣
而這樣的功能是用在什麼場景呢?沒錯,適合一次請求多個不一樣的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字串
範例結果
Github
-> https://github.com/thumbb13555/AndroidBlogExamples/tree/main/CoroutineWithCrypto
1. 環境架構與介面
首先要來先處理好程式碼以外的所有東西(笑),首先需要加入網路權限
<?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>
再來要來輸入需要的函式庫
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' }
最後畫一下介面,首先是主介面
<?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
<?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就可以換成別的幣種的資訊了,基本上本次的實作也是用這種方式完成的
最後是本次的資料夾內容供參考
3. 建立實體類及方法
首先從簡單的先下手,我建立了一個HttpModel,裡面放了兩個方法,如下
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() } }
基本上就是一個連續送出請求的方法
完成了這邊後,再來先完成一下實體類
還蠻簡單的,直接給
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
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
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的話,就會在畫面一起始的瞬間看到程式這裡一口氣送出了以下請求
再來是綠底白字的部分,基本上在Coroutines裏面,舉凡出現launch的時候裡面的東西相當於就是非同步處理了(與Thread一樣的意思)
而GlobalScope這部分是指說這個協程的作用範圍是Global,但我也沒想那麼多反正就用(欸
再來是runBlocking 的部分,這部分就是實際跑回圈了。同時遠端返回的資料也都會從這邊去做回傳
最後runOnUiThread就不用我說了,UI更新XD
到這邊,基本上按下執行後大概等個一秒左右資訊就會顯示出來了囉~
基本上,關於Coroutines我其實還有很多不能說算懂的地方
不過我個人是想藉著這篇文章,來記錄下我遇到的問題以及我的解決方案
至於關於Coroutins的詳細介紹,我當初是看這篇文章去理解的
然後那個功能當時也忘了是怎麼摸出來的,事隔一段時間回頭看以前的自己真是天才啊(欸
好啦,今天文章分享到這裡,最後...
留言列表