# Android Programming - Lecture 2
###### tags: `Kotlin Programming`
## Android Layout
Android 的 Layout 以 `View` 和 `ViewGroup` 以階層式的架構組成。
* `View` 為使用者可見的元件,如:`Button`、`TextView`、`ImageView`等。
* `ViewGroup` 則為各種 `Layouts` 如:`LinearLayout`、`ConstrainLayout`、`ScrollView`等。
一個使用者介面中可能會是以下的架構:

來源:https://developer.android.com/guide/topics/ui/declaring-layout
### Linear Layout
* 在裡面的元件依順序一個接一個的排序。
* 有水平與垂直兩種模式,可透過`xml=android:orientation="vertical"`調整。
#### Layout Weight
可設定元件對畫面佔的程度,較大的數字可以分配填滿剩餘的空間,默認的`layout_weight`為0。
1. 都不設定
若都不設定`layout_weight`,我們會看到依照元件的順序以及預設大小排序。
```xml=
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="to" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="subject" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="message" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send" />
</LinearLayout>
```

2. 平均分配
若要讓各個元件大小平均分配並填滿,我們可以設定每個元件的`layout_weight`為1。
```xml=
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="to" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="subject" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="message" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="send" />
</LinearLayout>
```

3. 分配不均
但正常情況下我們會希望 Message 的欄位大一點,其他依照正常大小,我們可以單獨設 Message 欄位的`layout_weight=1`。
```xml=
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="to" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="subject" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="message" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send" />
</LinearLayout>
```

參考:https://developer.android.com/guide/topics/ui/layout/linear
### Constraint Layout
* 透過自己與其他元件的 Constraint 決定位置。
* 複雜的界面時可以不用多層 Layout 嵌套處理。
* 每個元件至少需要一個水平和一個垂直 Constraint。
例如圖中的`BUTTON1`與`BUTTON2`,`BUTTON1`有著水平(左右)與垂直(上)`layout_marginTop="200dp"`的 Constraint 因此沒問題,但`BUTTON2`只有水平(與`BUTTON1`水平對齊)的 Constraint,因此在實際執行時就會`BUTTON2`就會飛上去。


可藉由看 Component Tree 部分有無錯誤檢查。

接著讓我們來實際使用 Constraint Layout
1. 開啟空的 Constraint Layout
```xml=
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
```
2. 接著切換到圖形編輯模式,並放置一顆 Button,按鈕旁邊四角以及右邊色框框中可以來設定 Constraint。

3. 我們可以透過紅色框框中`+`與`x`符號新增與刪除 Constraint,新增 Constraint 後可透過旁邊下拉式選單選擇對應的`margin`大小。

4. 設定完對應的`margin`後可以調整標註上的符號來更改高度和寬度的計算方式。

詳細資料參考:https://developer.android.com/training/constraint-layout#adjust-the-view-size
5. 鍊式約束(chain)
可以把幾個元件之間連接起來,類似於 Linear Layout。
把元件們選起來,`右鍵`->`Chains`->`Create Horizontal Chain`,接著`右鍵`->`Align`->`Top Edges`建立 Chain。

Chain 有以下幾種模式:
1. Spread:各個元件會平均的分怖在 layout 上。
2. Spread inside:頭尾的元件會固定在約束範圍端,其它的則會平均分怖。
3. Packed:個元件會都聚集在一起並置中。

參考:https://developer.android.com/training/constraint-layout#constrain-chain
## Wiki Search APP
接著我們練習分以`Constrain Layout`實做一個搜尋 Wiki 頁面摘要的 APP:
https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext&titles=Google
### 建立專案
1. Create New Project。

2. Template 選擇 Empty Activity。

3. 命名為 Wiki Search,語言選擇 kotlin ,Minimum SDK 選擇 Android 11.0。

### 建立 Layout
可自行練習拉元件或在 activity_main.xml 貼上以下 XML 再至圖形編輯介面觀察與調整。

MainActivity.kt:
```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">
<EditText
android:id="@+id/edit_text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:autofillHints=""
android:ems="10"
android:hint="Title"
android:inputType="textPersonName"
app:layout_constraintEnd_toStartOf="@+id/button_search"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/edit_text_title"
app:layout_constraintTop_toTopOf="@+id/edit_text_title" />
<Button
android:id="@+id/button_clear"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="Clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/edit_text_search_result"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine"
app:layout_constraintBottom_toTopOf="@+id/button_clear"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_text_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
### 綁定元件 (View Binding)
1. 使用 View Binding 前必須先在 build.gradle 中設定 viewBinding 爲 true。
```
android {
...
buildFeatures {
viewBinding true
}
}
```
2. 在 MainActivity.kt 中對按鈕進行綁定及操作。
```kotlin=
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
```
### Wiki API
我們可以從 Wiki 的 API 獲取標題的內容,拿取的參數與結果如下:
使用 HTTP GET 方法,參數以 Query String方式,如我們這次 Call 的 API:
* https://zh.wikipedia.org/w/api.php 為 API 的 Base URL。
* 接著以`key=value`方式設定參數,如:`format=json`。
* 每筆參數以`&`符號格開,如:`format=json&action=query`。
我們可以先預覽一下 API 拿到的資料是否與一般 Wiki 一樣,這邊以 Google 主題為例:
原頁面:https://zh.wikipedia.org/zh-tw/Google
API:https://zh.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exsentences=10&exlimit=1&explaintext=1&formatversion=2&titles=google
返回資料:
```json=
{
"batchcomplete":true,
"query":{
"normalized":[
{
"fromencoded":false,
"from":"google",
"to":"Google"
}
],
"pages":[
{
"pageid":738397,
"ns":0,
"title":"Google",
"extract":"Google(中文譯名:谷歌/科高)是總部位於美國加州門洛帕克的跨國科技公司,為Alphabet Inc.的子公司,業務範圍涵蓋網際網路廣告、網際網路搜尋、雲端運算等領域,開發並提供大量基於網際網路的產品與服務,其主要利潤來自於AdWords等廣告服務。Google由在史丹佛大學攻讀理工博士的賴利·佩吉和謝爾蓋·布林共同建立,因此兩人也被稱為「Google Guys」。\n1998年9月4日,Google以私營公司的形式創立,目的是設計並管理網際網路搜尋引擎「Google搜尋」。2004年8月19日,Google公司在那斯達克上市,後來被稱為「三駕馬車」的公司兩位共同創始人與出任執行長的艾瑞克·施密特在此時承諾:共同在Google工作至少二十年,即至2024年止。Google的宗旨是「匯整全球資訊,供大眾使用,使人人受惠」(To organize the world's information and make it universally accessible and useful);而非正式的口號則為「不作惡」(Don't be evil),由工程師阿米特·帕特爾(Amit Patel)所創,並得到了保羅·布赫海特的支援。Google公司的總部稱為「Googleplex」,位於美國加州聖塔克拉拉縣的山景城。2011年4月,佩吉接替施密特擔任執行長。在2015年8月,Google宣布進行資產重組。重組後,Google劃歸新成立的Alphabet底下。同時,此舉把Google旗下的核心搜尋和廣告業務與Google無人車等新興業務分離開來。"
}
]
}
}
```
可發現確實有拿到該 Wiki 主題的摘要,接著我們開始寫程式。
在 `com.example.wikisearch` 下新增 `NetworkUtils.kt`
主要分為兩個 method:
* buildUrl(wikiSearchTitle: String)
以輸入要搜尋的主提建立並回傳完整 API 的 URL,這邊就是把輸入的`wikiSearchTitle`串進去,最後會長這樣:`https://zh.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exsentences=10&exlimit=1&explaintext=1&formatversion=2&titles=要搜尋的主提`。
```kotlin=
val WIKI_BASE_URL = "https://zh.wikipedia.org/w/api.php"
val PARAM_FORMAT = "format"
val format = "json"
val PARAM_ACTION = "action"
val action = "query"
val PARAM_PROP = "prop"
val prop = "extracts"
val PARAM_EXSENTENCES = "exsentences"
val exsentences = "10"
val PARAM_EXLIMIT = "exlimit"
val exlimit = "1"
val PARAM_EXPLAINTEXT = "explaintext"
val explaintext = "1"
val PARAM_FORMATVERSION = "formatversion"
val formatversion = "2"
val PARAM_TITLES = "titles"
fun buildUrl(wikiSearchTitle: String): URL? {
val builtUri = Uri.parse(WIKI_BASE_URL).buildUpon()
.appendQueryParameter(PARAM_FORMAT, format)
.appendQueryParameter(PARAM_ACTION, action)
.appendQueryParameter(PARAM_PROP, prop)
.appendQueryParameter(PARAM_EXSENTENCES, exsentences)
.appendQueryParameter(PARAM_EXLIMIT, exlimit)
.appendQueryParameter(PARAM_EXPLAINTEXT, explaintext)
.appendQueryParameter(PARAM_FORMATVERSION, formatversion)
.appendQueryParameter(PARAM_TITLES, wikiSearchTitle)
.build()
var url: URL? = null
try {
url = URL(builtUri.toString())
} catch (e: MalformedURLException) {
e.printStackTrace()
}
return url
}
```
* getResponseFromHttpUrl(url: URL)
輸入 URL 並實際發送 HTTP 請求。
```kotlin=
fun getResponseFromHttpUrl(url: URL): String? {
val urlConnection = url.openConnection() as HttpURLConnection
return try {
val `in` = urlConnection.inputStream
val scanner = Scanner(`in`)
scanner.useDelimiter("\\A")
val hasInput = scanner.hasNext()
if (hasInput) {
scanner.next()
} else {
null
}
} finally {
urlConnection.disconnect()
}
}
```
NetworkUtils.kt:
```kotlin=
package com.example.wikisearch
import android.net.Uri
import java.io.IOException
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
import java.util.*
object NetworkUtils {
val WIKI_BASE_URL = "https://zh.wikipedia.org/w/api.php"
val PARAM_FORMAT = "format"
val format = "json"
val PARAM_ACTION = "action"
val action = "query"
val PARAM_PROP = "prop"
val prop = "extracts"
val PARAM_EXSENTENCES = "exsentences"
val exsentences = "10"
val PARAM_EXLIMIT = "exlimit"
val exlimit = "1"
val PARAM_EXPLAINTEXT = "explaintext"
val explaintext = "1"
val PARAM_FORMATVERSION = "formatversion"
val formatversion = "2"
val PARAM_TITLES = "titles"
fun buildUrl(wikiSearchTitle: String): URL? {
val builtUri = Uri.parse(WIKI_BASE_URL).buildUpon()
.appendQueryParameter(PARAM_FORMAT, format)
.appendQueryParameter(PARAM_ACTION, action)
.appendQueryParameter(PARAM_PROP, prop)
.appendQueryParameter(PARAM_EXSENTENCES, exsentences)
.appendQueryParameter(PARAM_EXLIMIT, exlimit)
.appendQueryParameter(PARAM_EXPLAINTEXT, explaintext)
.appendQueryParameter(PARAM_FORMATVERSION, formatversion)
.appendQueryParameter(PARAM_TITLES, wikiSearchTitle)
.build()
var url: URL? = null
try {
url = URL(builtUri.toString())
} catch (e: MalformedURLException) {
e.printStackTrace()
}
return url
}
@Throws(IOException::class)
fun getResponseFromHttpUrl(url: URL): String? {
val urlConnection = url.openConnection() as HttpURLConnection
return try {
val `in` = urlConnection.inputStream
val scanner = Scanner(`in`)
scanner.useDelimiter("\\A")
val hasInput = scanner.hasNext()
if (hasInput) {
scanner.next()
} else {
null
}
} finally {
urlConnection.disconnect()
}
}
}
```
### 新增 INTERNET Permission
由於使用網路需要`INTERNET`權限,因此我們需在`AndroidManifest.xml`中新增。
```xml=
<uses-permission android:name="android.permission.INTERNET" />
```
AndroidManifest.xml:
```xml=
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wikisearch">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WikiSearch">
<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>
```
### Coroutines
接下來有需要從網路獲得資料,由於網路存取需要一些時間,而且時間不固定,在 Android 3.0 以後不能夠從主 `Thread` 去進行任何需要很久的工作(網路、檔案I/O等),因此我們需要使用 `Coroutines` 在新的 `Thread` 中執行。
使用時需在`build.gradle`中新增 dependencies。
```
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
}
```
接著只要寫在`GlobalScope.launch(Dispatchers.IO) {}`中就可以把工作丟到另一個`Thread`中執行。
### 從 API 拿資料
首先在`MainActivity.kt`的`onCreate`監聽`buttonSearch`的 Click 事件,並執行`makeWikiSearchQuery()`Function。
```kotlin=
binding.buttonSearch.setOnClickListener {
makeWikiSearchQuery()
}
```
與`buttonClear`的 Click 事件,清除`editTextSearchResult`上的文字。
```kotlin=
binding.buttonClear.setOnClickListener {
binding.editTextSearchResult.setText("");
}
```
我們先獲得原始回傳資料並顯示在`editTextSearchResult`上,首先先 call `NetworkUtils.buildUrl(wikiQuery)`產生`URL`,再在`Coroutines`中`callNetworkUtils.getResponseFromHttpUrl(wikiSearchUrl)`從網路抓取資料。
```kotlin=
private fun makeWikiSearchQuery() {
val wikiQuery = binding.editTextTitle.text.toString()
val wikiSearchUrl: URL? = NetworkUtils.buildUrl(wikiQuery)
GlobalScope.launch(Dispatchers.IO) {
Looper.prepare();
if (wikiSearchUrl != null) {
var wikiSearchResult: String? = null
try {
wikiSearchResult = NetworkUtils.getResponseFromHttpUrl(wikiSearchUrl)
} catch (e: IOException) {
e.printStackTrace()
}
binding.editTextSearchResult.setText(wikiSearchResult)
}
Looper.loop();
}
}
```
MainActivity.kt:
```kotlin=
package com.example.wikisearch
import android.os.Bundle
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.example.wikisearch.databinding.ActivityMainBinding
import kotlinx.coroutines.*
import java.io.IOException
import java.net.URL
private lateinit var binding: ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.buttonSearch.setOnClickListener {
makeWikiSearchQuery()
}
binding.buttonClear.setOnClickListener {
binding.editTextSearchResult.setText("");
}
}
private fun makeWikiSearchQuery() {
val wikiQuery = binding.editTextTitle.text.toString()
val wikiSearchUrl: URL? = NetworkUtils.buildUrl(wikiQuery)
GlobalScope.launch(Dispatchers.IO) {
Looper.prepare();
if (wikiSearchUrl != null) {
var wikiSearchResult: String? = null
try {
wikiSearchResult = NetworkUtils.getResponseFromHttpUrl(wikiSearchUrl)
} catch (e: IOException) {
e.printStackTrace()
}
binding.editTextSearchResult.setText(wikiSearchResult)
}
Looper.loop();
}
}
}
```
可看到搜尋 Google 獲得了 API (https://zh.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exsentences=10&exlimit=1&explaintext=1&formatversion=2&titles=google) 回傳的原始資料:

### 轉換 JSON 為物件
由於回傳的純文字是 JSON 形式,我們希望取出`query.pages[0].extract`內容,因此我們需要將這段 JSON 轉換為程式中的物件,這邊我們使用`GSON`。
使用時需在`build.gradle`中新增 dependencies。
```
dependencies {
implementation 'com.google.code.gson:gson:2.8.6'
}
```
接著我們需建立要讓 JSON mapping 回來的 data class。新增 WikiSearchResult.kt:
```kotlin=
package com.example.wikisearch
data class WikiSearchResult(
val batchcomplete: String,
val query: WikiQueryResutQuery
) {
data class WikiQueryResutQuery(
val normalized: List<WikiQueryResutQueryNormalized>,
val pages: List<WikiQueryResutQueryPages>
) {
data class WikiQueryResutQueryNormalized(
val fromencoded: Boolean,
val from: String,
val to: String
)
data class WikiQueryResutQueryPages(
val pageid: Int,
val ns: Int,
val title: String,
val extract: String
)
}
}
```
最後我們可以這樣使用:
```kotlin=
val gson = Gson()
val result = gson.fromJson(wikiSearchResult, WikiSearchResult::class.java)
result.query.pages[0].extract // 拿出 extract
```
MainActivity.kt:
```kotlin=
package com.example.wikisearch
import android.os.Bundle
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.example.wikisearch.databinding.ActivityMainBinding
import com.google.gson.Gson
import kotlinx.coroutines.*
import java.io.IOException
import java.net.URL
private lateinit var binding: ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.buttonSearch.setOnClickListener {
makeWikiSearchQuery()
}
binding.buttonClear.setOnClickListener {
binding.editTextSearchResult.setText("");
}
}
private fun makeWikiSearchQuery() {
val wikiQuery = binding.editTextTitle.text.toString()
val wikiSearchUrl: URL? = NetworkUtils.buildUrl(wikiQuery)
GlobalScope.launch(Dispatchers.IO) {
Looper.prepare()
if (wikiSearchUrl != null) {
var wikiSearchResult: String? = null
try {
wikiSearchResult = NetworkUtils.getResponseFromHttpUrl(wikiSearchUrl)
} catch (e: IOException) {
e.printStackTrace()
}
val gson = Gson()
val result = gson.fromJson(wikiSearchResult, WikiSearchResult::class.java)
binding.editTextSearchResult.setText(result.query.pages[0].extract)
}
Looper.loop()
}
}
}
```
最後可以只拿到文章摘要:
