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
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.