本篇文章同步發表在 HKT 線上教室 部落格,線上影音教學課程已上架至 Udemy 和 Youtube 頻道。另外,想追蹤更多相關技術資訊,歡迎到 臉書粉絲專頁 按讚追蹤喔~
範例名稱:使用 TextView 和 ScrollView 將口罩資料顯示在畫面上
開發人員:HKT (侯光燦)
程式語言:Kotlin
開發環境:Android Studio 4.1.1 & Android 11 & Kotlin 1.4.21
授權範圍:使用時必須註明出處且不得為商業目的之使用
範例下載點:點我下載
範例名稱:解析 JSON 資料格式,取出藥局名稱顯示在畫面上
開發人員:HKT (侯光燦)
程式語言:Kotlin
開發環境:Android Studio 4.1.1 & Android 11 & Kotlin 1.4.21
授權範圍:使用時必須註明出處且不得為商業目的之使用
範例下載點:點我下載
昨天,我們透過「OkHttp」網路連線方式,抓到網路上口罩資料:
{
"type": "FeatureCollection",
"features": [
...
...
...,{
"type": "Feature",
"properties": {
"id": "5901024427",
"name": "博昱仁愛藥局",
"phone": "(02)87739258",
"address": "臺北市大安區仁愛路4段65號",
"mask_adult": 0,
"mask_child": 450,
"updated": "2020\/09\/13 11:32:37",
"available": "星期一上午看診、星期二上午看診、星期三上午看診、星期四上午看診、星期五上午看診、星期六上午看診、星期日上午看診、星期一下午看診、星期二下午看診、星期三下午看診、星期四下午看診、星期五下午看診、星期六下午看診、星期日下午看診、星期一晚上看診、星期二晚上看診、星期三晚上看診、星期四晚上看診、星期五晚上看診、星期六晚上看診、星期日晚上看診",
"note": "週間(週一至週五)上午9點發放號碼牌收取健保卡,下午2點領取",
"custom_note": "",
"website": "",
"county": "臺北市",
"town": "大安區",
"cunli": "仁愛里",
"service_periods": "NNNNNNNNNNNNNNNNNNNNN"
},
"geometry": {
"type": "Point",
"coordinates": [
121.546869,
25.038194
]
}
},
...
...
...
]
}
連線到 口罩資料 網址,獲取到回應資料,這個動作可以被稱爲是「呼叫 API」。APP 手機裝置端與遠端伺服器互相傳遞資料,我們通常會透過 API (Application Programming Interface:應用程式介面)來溝通。手機獲取伺服器資料,通常採用 GET 或 POST 方式。
而如果我們 UI 畫面不加以處理,直接將資料,透過TextView + ScrollView 顯示在畫面上:
...
...
...
<!-- 在 TextView 外包上一層 ScrollView ,當資料超出畫面,可滾動捲軸,看到更多資料內容-->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_pharmacies_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
</ScrollView>
...
...
...
//注意要設定 UI ,需要執行在 UiThread 裡面,否則會噴錯誤
runOnUiThread{
//將 Okhttp 獲取到的回應值,指定到畫面的 TextView 元件中
binding.tvPharmaciesData.text= pharmaciesData
}
注意:Kotlin 最新版本 v1.4.2,已經移除廢棄的 synthetic 語法,改採用 View Binding,不熟悉的同學,可以參考 KT 專門寫的這一篇文章:如何使用元件綁定 View Binding
這麼一大包資料,很難找到我們需要的資料。所以我們需要進一步篩選與過濾,取得我們要顯示的元素資料。而這次我們的 口罩資料 範例,回應的資料格式是 JSON 資料格式。
JSON 資料格式是一種輕量級的資料交換格式,程式很容易建立與解析,人類也易於閱讀與書寫。JSON 格式存放方式,採物件概念,使用大括號 {} 來包覆,裡面資料為採 key 與 value ,中間使用冒號:來分隔。例如:
{
"name" : "HKT"
}
若有第二個欄位資料,中間使用逗號來區隔是第二個欄位
{
"name" : "HKT",
"age": 18
}
常見的 JSON 資料格式,有數字、字串、布林值,而同類型資料,可以使用中括號[]來包覆,每筆資料採用逗號做分隔
{
"name" : "HKT",
"age": 18,
"class":["Java","Kotlin","Dart"]
}
可以試著將口罩資料轉貼到 Online JSON Viewer 的網頁右上方的 Text 頁籤中,完成之後可以按左邊的 Viewer 頁籤,即可透過這套線上 JSON 小工具,快速掌握整個 JSON 資料結構。
解析 JSON 資料格式,注意的是層次概念,以 口罩資料 為例,若我們要取得最外層資料,可以直接獲取,例如 「 "type": "FeatureCollection"」,我們解析方式可以寫成這樣:
//從 Okhttp 收到的回應資料 response,取出 body 的部分。
//注意這裏,response 不能二次使用,不然會噴錯誤。
//所以我們將他轉存到 pharmaciesData 。
val pharmaciesData = response.body?.string()
//將 pharmaciesData 整包字串資料,轉成 JSONObject 格式
val obj = JSONObject(pharmaciesData)
//這個時候,我們就可以透過 getString 的方式,裡面放 key (name) 值,
//即可以獲取到最外層的 type 欄位資料值。
Log.d("HKT",obj.getString("type"))
FeatureCollection
如果我們要獲取的是 features 裡面的 properties 裡面的 name。解析 JSON 資料,除了要注意層次外,還要注意結構。features 是一個陣列 [] ,中括號來包覆資料,就需要將他轉換成 JSONArray。
val pharmaciesData = response.body?.string()
//將 pharmaciesData 整包字串資料,轉成 JSONObject 格式
val obj = JSONObject(pharmaciesData)
//features 是一個陣列 [] ,需要將他轉換成 JSONArray
val featuresArray = JSONArray(obj.getString("features"))
//透過 for 迴圈,即可以取出所有的藥局名稱
for (i in 0 until featuresArray.length()) {
val properties = featuresArray.getJSONObject(i).getString("properties")
val property = JSONObject(properties)
Log.d("HKT", "name: ${property.getString("name")}")
}
name: 中美藥局
name: 新東洋藥局
name: 辰好藥局
name: 杏安藥局
name: 明皇藥局
name: 全國大藥局
name: 政德藥局
name: 嘉方藥局
name: 慶豐綜合藥局
...
...
...
如果我們 UI 畫面不加以處理,直接將資料,透過TextView + ScrollView 顯示在畫面上:
//藥局名稱變數宣告
var propertiesName: String = ""
//透過 for 迴圈,即可以取出所有的藥局名稱
for (i in 0 until featuresArray.length()) {
val properties = featuresArray.getJSONObject(i).getString("properties")
val propertieObj = JSONObject(properties)
//將每次獲取到的藥局名稱,多加跳行符號,存到變數中
propertiesName+= propertiesName + propertieObj.getString("name") +"\n"
}
//最後取得所有藥局名稱資料,指定顯示到 TextView 元件中
tv_pharmacies_data.text = propertiesName
使用 String 處理串接文字,當資料很多時很容易造成 OOM 記憶體不足,建議換成 StringBuilder
val pharmaciesData = response.body?.string()
//將 pharmaciesData 整包字串資料,轉成 JSONObject 格式
val obj = JSONObject(pharmaciesData)
//features 是一個陣列 [] ,需要將他轉換成 JSONArray
val featuresArray = JSONArray(obj.getString("features"))
//藥局名稱變數宣告
// var propertiesName: String = ""
val propertiesName = StringBuilder()
//透過 for 迴圈,即可以取出所有的藥局名稱
for (i in 0 until featuresArray.length()) {
val properties = featuresArray.getJSONObject(i).getString("properties")
val propertieObj = JSONObject(properties)
//將每次獲取到的藥局名稱,多加跳行符號,存到變數中
// propertiesName += propertiesName + propertieObj.getString("name") + "\n"
propertiesName.append(propertieObj.getString("name") + "\n")
}
runOnUiThread {
//最後取得所有藥局名稱資料,指定顯示到 TextView 元件中
binding.tvPharmaciesData.text = propertiesName
}
解析 JSON 資料,使用 getString 時,若資料中沒有對應的 key (name)值,會發生例外狀況(Exception)。
val obj = JSONObject(pharmaciesData)
//資料沒有 typeeeee 這個 key(name)值,直接獲會噴錯誤
Log.d("HKT",obj.getString("typeeeee"))
E: FATAL EXCEPTION: OkHttp Dispatcher
Process: com.thishkt.pharmacies, PID: 14914
java.lang.Error: org.json.JSONException: No value for typeeeee
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1121)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:590)
at java.lang.Thread.run(Thread.java:818)
Caused by: org.json.JSONException: No value for typeeeee
at org.json.JSONObject.get(JSONObject.java:389)
at org.json.JSONObject.getString(JSONObject.java:550)
at com.thishkt.pharmacies.MainActivity$getPharmaciesData$1.onResponse(MainActivity.kt:59)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1115)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:590)
at java.lang.Thread.run(Thread.java:818)
這個時後可以透過 has 或 isNull 方法來避免例外錯誤:
//方法一:使用 has 判斷是否存在這個資料,存在時才獲取資料
if(obj.has("typeeeee")){
Log.d("HKT",obj.getString("typeeeee"))
}else{
Log.d("HKT","has 判斷沒有這個資料")
}
//方法二:使用 isNull 判斷是為空,不為空才獲取資料
if(!obj.isNull("typeeeee")){
Log.d("HKT",obj.getString("typeeeee"))
}else{
Log.d("HKT","isNull 判斷,沒有這個資料")
}
但例外有時真的出乎意料,所以在解析資料時,為了避免不可預期錯誤造成 APP 閃退,會多加 try..catch 來防止,如:
try {
JSONObject result = new JSONObject();
...
} catch (e: JSONException) {
throw new RuntimeException(e);
}
HKT 線上教室
https://tw-hkt.blogspot.com/
Freepik
https://www.freepik.com/
JSONObject
https://developer.android.com/reference/org/json/JSONObject
JSONArray
https://developer.android.com/reference/org/json/JSONArray
JSONException
https://developer.android.com/reference/org/json/JSONException?hl=en
那今天【iThome 鐵人賽】就介紹到這邊囉~
順帶一提,KT 線上教室,臉書粉絲團,會不定期發佈相關資訊,不想錯過最新資訊,不要忘記來按讚,追蹤喔!也歡迎大家將這篇文章分享給更多人喔。
我們明天再見囉!!!掰掰~