Kotlin Programming 2022 Spring - 用 Kotlin 開發 Android 程式 === ###### tags: `Kotlin 2022 Spring` `Kotlin` `2022 Spring`  ## Android Studio > Orz [name=tutorial 做到一半才突然想起來這堂課預設是用 IntelliJ 的助教] 要開發跨平臺的程式,往往很困難;因此在絕大多數的情況下,我們都會在想對應的系統從事該平臺的程式開發。但在手機上開發,似乎又顯得太麻煩了。 好在 Java 的特性,使得他編譯出來的程式可以在所有安裝 JVM 的機器上運行,而基於 Java 的 Kotlin 完美的繼承了這些優點,我們可以在電腦上從事 Kotlin 開發,並且在手機上執行。 但我們要在電腦上進行手機 App 的測試,必然需要有虛擬機或是一些更輕量的模擬器;在從事跨平臺的開發的時候,最麻煩的事情便是進行環境的配置。 而使用 Android Studio 這款 IDE,不止可以讓你非常方便的從事 Java、Kotlin 開發,也可以在開發完之後非常方便的開啓 Android 模擬器,進行測試、除錯。 :::warning Android Studio 頗吃效能,因此最好能在性能足夠強勁的機器上面進行開發。 當開啓模擬器時的資源用量:  考慮到我這臺筆電是 8 代 i7,都會感覺到明顯的等待時間,使用性能更低下開發機的同學可能要更有耐心一些! ::: 其實同學們也可以使用 Intellij 來進行 Android 程式開發,用起來也差不多,請參考 [Tutorial: Create your first Android application](https://www.jetbrains.com/help/idea/create-your-first-android-application.html)。 ### 安裝 Android Studio 1. 上他的官網 https://developer.android.com/studio 下載安裝檔  2. 根據提示步驟開始安裝  3. 需要安裝 Android Virtual Device 的 Component  ### 第一個 Android App #### 新增 Project 1. 在安裝完 Android Studio 後,我們將之打開:  2. 選擇 New Project -> Phone And Tablet -> Empty Activity 來新增一個新的空白視窗應用程式:  3. 選擇名稱、路徑,並設置語言爲 Kotlin 而非 Java;其他留爲預設就好。完成後按下 Finish:  之後需要等一段時間,來讓他 Importing 以及 Indexing 整個 project。 #### 簡單的 IDE 介紹  我們可以看到他有兩個分頁,分別是 `activity_main.xml` 與 `MainActivity.kt`,前者是 GUI 的配置,而後者是這個 GUI Windows 後面的控制程式。 當我們點擊 `activity_main.xml`,我們能看到這樣子的視窗:  我們可以在這邊進行 GUI 的設計以及調整,例如在此將預設在中心的 `Hello World!` 字樣移動到另一個地方:  注意到這個分頁的右上角有 `Code`、`Split` 與 `Design`,目前是選擇 `Design`,如果我們將他切換成 Code,則會看到 GUI 的 Code: , GUI 視窗是以 XML 的格式儲存,會存放每個部件(widget)的屬性(attribute),我們可以在其中直接進行值的修改:  例如在這邊我將原本的 `Hello World!` 字樣改掉:  我們回去 `MainActivity.kt` 來修改程式碼:  我們在上面新增一行 `import androidx.appcompat.app.AlertDialog`,再在 `onCreate` 函式內新增一行 `AlertDialog.Builder(this).setMessage("This is my first Android App").show()`,如圖:  #### 編譯並執行程式 我們必須要先新增一個 Android Emulator,我們開啓 Device Manager 來新增 Virtual Device:   之後選擇模擬的硬體機器、Android 系統版本:  我這邊是使用 Pixel XL 與 S(Android 12.0),之後我們可以爲 device 取名,選擇預設是橫放或是直放,這邊選擇直放:  完成後按 Finish,就可以新增 Virtual Device,之後按下 Actions 下面的三角形,就可以新增一個 Emulator Instance:  我們能看到我們打開的虛擬設備:  最後再在右上角選擇我們新增加的 Device,之後按下綠色三角形,就可以執行我們剛剛創建的 App 了!  等待幾秒之後,我們就會看到 `BUILD SUCCESSFUL`,接着他就會開始執行這隻程式;或是如果有編譯錯誤訊息的話,可能代表環境設置不對,或是專案程式碼、設定沒有正確。  ### Demo 剛進入程式,就可以看到一個提示框說 `This is my first Android App`:  我們前面在`onCreate` 寫上的程式碼,就意味着我希望在這個視窗被創造出來的時候(`onCreate`)跳出提示框。 ``` AlertDialog.Builder(this).setMessage("This is my first Android App").show() ``` 之後隨便點一下畫面,就可以看到我們的主畫面,正是我們設計好的 GUI  ## Android 程式開發 ### Activity 如果有開發過 Visual Basic,那大家大概會很容易瞭解 Activity,他其實很類似於 .NET 當中的 Form。不似傳統的 C/C++,我們在開發的時候會宣告一個 `main()` 函數來當我們的程式進入點(entrypoint);在 Android App 被打開的時候,他會去起一個 Activity 的 Instance,然後根據不同的情況去呼叫該 Activity 的各個 methods。 基本上來說,activity 就是與使用者互動的一整個視窗單位。每個有 UI,會和使用者互動的 App 都至少會有一個 interface,我們剛剛寫的 firstandroidapp,就只有一個 activity 叫做 `MainActivity`:  當你的 App 被開啓的時候,OS 會將創造某個特定的 Activity Instance,每個 Activity 都有幾個特定的 methods,會在該 instance 的不同生命階段的時候會被觸發。 例如我們剛剛看到的: - onCreate - 字面含義上就是當 instance 被 created 時候會觸發的,在這個 method 裏面,你需要去初始化這個 instance 所需要的一切東西 - 最常見的情況是,你會在 onCreate 裏面去呼叫 setContentView(),來指定某個 UI,例如我們初始程式碼就有一行 `setContentView(R.layout.activity_main)`,可以試試看如果把這行註解掉會發生什麼事 實際上還有很多不同的 methods,會在不同的時候被觸發,來實現 Activity Instance 的創建、互動、回收。 要能詳細瞭解每個 method 在做的事情,我們需要瞭解 activity 的 lifecycle:  基本上 activitiy instance 分爲四個狀態:active、paused、stopped、dead - active 就是使用者正在使用的 activity,同一時間同個系統只會有一個 active 的 activity - paused 就是 active 的 activity 正在被搶走控制權的時候會觸發的,例如電話來了,此時已經不再和使用者互動了 - stopped 就是 activity 已經退出 foreground,不再進行任何動作 - dead 就是沒有被啓用的 activity,他可能是沒被開啓過,或是被關閉、回收了 我們先看前半部分,基本上這是一個 activity 從 dead 到 active 的過程:  - Activity 會先被 create,會觸發 onCreate - 接着初始化 UI,會觸發 onStart - 在能開始互動的時候,會觸發 onResume 再看看中間這邊,基本上這是一個 activity 從 active 到 stopped 的過程:  接着你的 App 可能會被打斷,例如應用程式切換,或是有人來電,你的程式「即將」被放入後臺執行 - 會進入 onPause,例如你是影片播放器,當應用程式切換的時候,你會暫停影片 - 等 onPause 結束,你的 activity 將不再在 foreground,也就是說使用者將不會再看到你的畫面 - 此時有可能使用者切回你的應用程式,則會回到 onResume 階段,此時你可能會繼續播放影片 - 也有可能你的 activity 被放入了 background,此時 onStop 會被呼叫,讓你可以將你使用的資源回收 或是我們可以再舉幾個例子幫助理解: 1. 我們原本在 activity A,然後打開另一個 activity B: - A 的 onPause 會被觸發,此時 A 會進入 paused - B 的 onCreate、onStart、onResume 會被觸發,接着進入 Active - A 的 onStop 會被觸發,此時 A 進入 stopped 2. 從 activity B 按下 Back 鍵退出程式並回到前一個 activity A - B 的 onPause 會被觸發,此時 B 進入 paused - A 的 onRestart、onStart、onResume 會分別被觸發,此時進入 start - B 的 onStop 被觸發,進入 stopped - B 的 onDestroy 被觸發,進入 dead :::warning 在 Android 12,按下 Back 鍵不會再將 Activity Destroy。 原本你可能會在 `onBackPressed()` 中幫你的 Activity 收屍,但現在你可以改用 `super.onBackPressed()`,並且結束後 Activity 將被放到後臺。 [[REF: Behavior changes]](https://developer.android.com/about/versions/12/behavior-changes-all#back-press) ::: ### EventListener 實際上空有 UI 沒有用,還要讓 UI 可以和使用者互動,例如按下一個按鈕可以把地球炸了,這就是一種互動;如果空有 UI 沒有互動,那麼這其實不太能稱之爲一隻應用程式。 如果我們要能做到:按下按鈕 -> 把地球炸了,那我們的流程大概是這樣子的: 1. 寫一個 function `blowupEarth`,當你呼叫這個 function 的時候,地球會被炸掉 2. 當 button 被點擊的時候,會去呼叫 `blowupEarth` 這個 function 我們現在延續剛剛 firstandroidapp 的這個專案,新增一個 button,你可以從左邊的 palette 把 button 拉出來:  接着按執行,我們會發現和剛剛拉的按鈕位置差很多,或是多換幾個位置了,實際出現的 Button 始終在最左上角:  心細的同學可能會發現噴出了一個 Error:`MissingConstraints`  要能完全理解這個原因,需要先瞭解 android app 的 GUI 是怎麼定位的,但我們這邊先不涉及那麼多,先對這個 button 按右鍵,接着選擇 Constrain -> parent top;接着再按一次右鍵,選擇 Constrain -> parent bottom;重複此步驟,分別按過 top、bottom、start、end,如圖:  此時再拖拽 button,就會發現他旁邊長出了類似彈簧的線:  此時再執行,就會發現他的確在正確的地方出現了。 我們先不深究我們剛剛做了什麼,反正我們先拉出一個 button,接着試圖達成「按下按鈕 -> 炸掉地球」的目標。 我們想要將 button3(我們剛剛拉出的 button,你可以在 Component Tree 上或是 XML code 看到它的 id)的 OnClick Method 給 Override 掉,那我們的程式必須要先能找到 button3,並對他進行操作。 我們需要先 `import android.widget.Button`,這樣我們才能宣告 Button 類別的變數,並將它賦值爲 button3。 我們可以運用 `findViewById` 這個函式來幫助我們從 id 找到那個物件,在 Android 當中,View 就是一塊顯示在畫面上的東西,然後使用者可以與那個東西互動,因此 button 是一種 View(用更精確的術語:Button 是 View 的子類別)。 因此我們可以透過 `findViewById` 這個函式,讓我們用 `button3` 這個 id 找到對應的物件,並對他進行操作。 直接看 code:  我們找到 button3 並用 mybutton 這個變數儲存它(其實是 reference),之後再呼叫 mybutton 的 setOnClickListener,這樣就可以 Override 掉舊的 onClick,他就會做我們希望他做的事情。  甚至我們也可以利用這個函式,去改其他 widget 的,進而創造更強的互動性,例如: ``` mybutton.setOnClickListener { var text: TextView = findViewById(R.id.textView) text.setText("NYCU is best") } ``` 當點擊 Button,上面的字樣就會改變。 ## ConstraintLayout layout(佈局)其實就是在講說你每個 widget 應該要放在哪裏、大小多大、該怎麼擺,而 ConstraintLayout 就是 android studio 與 intellij 採用的預設 layout,設定起來比較簡單,效果也還不錯。 Constraint 的中文叫做約束,基本的想法就是如果我想要讓某個 widget 出現在畫面上的某個位置,那我們就爲他加上一些 constraints,強迫他們要待在某些地方。 在 ConstraintLayout 當中所有沒有 constraint 的元件都會被放在畫面最左上角,這可以解釋爲何我們剛才的 button 無論我們把它拖拽到哪裏,程式執行起來他都會飛到左上角去。 而我們剛剛讓他「正常」運作的方式是爲他加上四個 constraint,分別是「parent top」、「parent bottom」、「parent start」、「parent end」,在 ConstraintLayout 當中,每個元素都至少需要擁有一個水平的 Constraint 與一個垂直的 Constraint,否則就會跳出錯誤訊息:`MissingConstraints` 如果我們要對某個物件增加一個 constraint,先點一下該物件,然後會看到白色大顆圓圓的東西,拖拽這顆圓圓的東西:   你可以把它拖拽到畫面的邊緣或是其他 widget 上:  例如這顆 button 此時就有 2 個 constraints,分別是「button 的上緣在 textView 上緣下面」以及「button的左邊在畫面左界的右邊」。 藉由這種方法,來讓你的介面不管在怎樣長寬比、怎樣大小的螢幕上面都有一些基本的形狀,你可以簡單的設計畫面,又不用擔心會跑版的太嚴重。 ## 外部文件 ### 儲存外部文件 我們有時候會需要在程式當中使用一些素材,我們可能會想在程式當中顯示一張圖片,或是一些龐大資料庫,這些東西不適合放在程式碼中。 Android Project 下有一個叫做 `res` 的目錄便是讓我們拿來放置一些額外的檔案供程式運行時使用的。  我們如果想要放置一些自定義的檔案進去,就需要先在 `res` 目錄下創建一個 `raw` 型別的子目錄,如下圖,我創建了一個名爲 `raw` 的 `raw` 型別資料夾以供我放入資料:   成功創建之後我們便能將檔案放入 `raw`,我們一樣右鍵 `res`,點擊 `New`:  如果要放圖片,可以選 `Image Asset`;而如果要放純文字檔,可以選擇 `File`。 新增成功後,點擊我們剛創建的文件,就可以編輯:  就可以將自定義的文件放入到 Project 當中。 ### 讀取外部文件 我們想要打開一個檔案,必須先知道該檔案的 `Identifier`,我們可以用 以下程式碼來找到我們剛剛創建的 `dict.txt`: ``` resources.getIdentifier("dict", "raw", packageName) ``` 或 ``` R.raw.dict ``` 之後可以使用 `resources.openRawResource(id)` 來開啓某個文件,他會回傳一個 `InputStream`,基本上我們把它當作一個`資料來源`。 ``` resources.openRawResource(resources.getIdentifier("dict", "raw", packageName)) ``` 或 ``` resources.openRawResource(R.raw.dict) ``` 有了資料來源之後,我們還需要從它身上讀取資料,所以我們要創建一個 Scanner 物件,它可以從 InputStream 讀取資料並且進行解碼: ``` Scanner(resources.openRawResource(R.raw.dict)) ``` 有了 Scanner 之後,後續的事情就簡單的多了,各位可以自行參悟: ```kotlin= class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val scanner = Scanner(resources.openRawResource(R.raw.dict)) val dict = HashSet<String>() while (scanner.hasNext()) dict.add(scanner.next()) // 隨機挑一個字印出來 AlertDialog.Builder(this) .setMessage(dict.random()) .show() } } ``` 執行效果如下:  ## 一個簡單的範例:1A2B    `activity_main.xml`: ```ml= <?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"> <Button android:id="@+id/submit" android:layout_width="120dp" android:layout_height="60dp" android:layout_marginStart="256dp" android:layout_marginTop="412dp" android:text="Submit" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button6" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="176dp" android:layout_marginTop="412dp" android:text="6" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button4" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="16dp" android:layout_marginTop="412dp" android:text="4" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button9" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="176dp" android:layout_marginTop="492dp" android:text="9" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button8" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="96dp" android:layout_marginTop="492dp" android:text="8" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button3" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="176dp" android:layout_marginTop="332dp" android:text="3" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/delete" android:layout_width="120dp" android:layout_height="60dp" android:layout_marginStart="256dp" android:layout_marginTop="332dp" android:text="Delete" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button5" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="96dp" android:layout_marginTop="412dp" android:text="5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button7" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="16dp" android:layout_marginTop="492dp" android:text="7" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button1" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="16dp" android:layout_marginTop="332dp" android:text="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button2" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="96dp" android:layout_marginTop="332dp" android:text="2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/input" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="28dp" android:layout_marginTop="55dp" android:fontFamily="monospace" android:text="" android:textSize="32pt" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.077" /> <TextView android:id="@+id/msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.064" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.535" /> </androidx.constraintlayout.widget.ConstraintLayout> ``` `MainActivity.kt`: ```kotlin= package com.example.kotlon2022springfirst import android.graphics.Color import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.appcompat.app.AlertDialog import android.widget.Button import android.widget.TextView class MainActivity : AppCompatActivity() { private val guess = ArrayList<Int>() // private lateinit var input: TextView private val input by lazy { findViewById<TextView>(R.id.input) } // private lateinit var msg: TextView private val msg by lazy { findViewById<TextView>(R.id.msg) } private val answer = (1..9).shuffled().take(4).toList() private var win = false private var times = 0 private fun push(x: Int){ if(!(win || x in guess || guess.size == 4)) guess.add(x) input.text = guess.joinToString("") } private fun pop() { if(!win && guess.isNotEmpty()) guess.removeLast() input.text = guess.joinToString("") } private fun judge() { if(guess.size != 4) return if(answer == guess){ input.text = "Congrat!" msg.text = "You tried ${times} times" win = true return } var A = 0 var B = 0 for (i in 0..3) A += if (answer[i] == guess[i]) 1 else 0 B = answer.intersect(guess).size - A; times += 1 msg.text = "${guess.joinToString("")} -> ${A}A${B}B, you tried ${times} times" input.text = "" guess.clear() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) AlertDialog.Builder(this).setMessage(answer.joinToString("")).show() // un-comment the following two lines if input and msg are vars // input = findViewById<TextView>(R.id.input) // msg = findViewById<TextView>(R.id.msg) input.setBackgroundColor(Color.parseColor("#CCCCCC")) input.setTextColor(Color.parseColor("#113355")) val button1: Button = findViewById(R.id.button1) button1.setBackgroundColor(Color.CYAN) button1.setTextColor(Color.RED) findViewById<Button>(R.id.button1).setOnClickListener { push(1) } findViewById<Button>(R.id.button2).setOnClickListener { push(2) } findViewById<Button>(R.id.button3).setOnClickListener { push(3) } findViewById<Button>(R.id.button4).setOnClickListener { push(4) } findViewById<Button>(R.id.button5).setOnClickListener { push(5) } findViewById<Button>(R.id.button6).setOnClickListener { push(6) } findViewById<Button>(R.id.button7).setOnClickListener { push(7) } findViewById<Button>(R.id.button8).setOnClickListener { push(8) } findViewById<Button>(R.id.button9).setOnClickListener { push(9) } findViewById<Button>(R.id.submit).setOnClickListener { judge() } findViewById<Button>(R.id.delete).setOnClickListener{ pop() } } } ``` 更換 TextView 或是 Button 顏色的方法,請參考上方程式碼 lines 67--72 。 ## References [Android Developers](https://developer.android.com/guide)
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up