【Kotlin 1.3】Android程式初體驗 - Kotlin篇

話說,全世界的手機系統應該都被AndroidiOS佔光光了吧,不過最近又跑出來了一個鴻蒙系統,也許未來有可能成為第三大的手機系統吧?至於為什麼要學安卓呢?因為之前有在學一點點的Flutter,發現還是要兩個平臺都要了解一些些,才知道Flutter到底在做什麼?加上本來就是兩個不同的系統,在設定上一定有所差異,加上聽說KotlinSwift很相像,所以就來學學看了。因為本身對iOS比較熟一點,所以後面會以iOS的名詞來做不專業的說明。話不多說,這裡就用跟JetBrains合作的Android Studio,其實Kotlin也是JetBrains出品的。安裝的過程就不多做說明了,現在馬上就來試試看吧。

做一個長這樣的APP

IDE環境

新增Android專案

  • 可以選擇專案一開始的樣子,一般都是會選「Empty Activity」,其中「Activity」就有點像iOS的「ViewControler」,然後可以選擇語言,這裡我們當然是選「Kotlin」,最小API版本就選Anroid 5.0 (API 21)

編譯環境

  • 這個就不多說了,有圖有真象

新增Android手機模擬器

  • 這個就跟Xcode在新增模擬器的方法就不太一樣了,因為Android手機的廠商太多了,所以Android手機模擬器可以有很高度的客制化,這裡就不多做說明,不過在選擇x86_Image_API的時候要記得下載該image檔,才能做新增。另外值得一提的是,iOS模擬器是直接以x86-CPU去做的,但是Android模擬器是真的以Qemu去模擬手機硬體的。

執行Android程式

  • 在這裡可以選擇實機或模擬器來執行

APP的第一頁

建立資源檔

  • 在「res/values/strings.xml」的內容內,都是可以重複利用的文字,有點像iOS的plist檔,其中「app_name」就是APP的名字
<resources>
    <string name="app_name">Android_HelloWorld</string>
    <string name="select_box">請選擇其中一個</string>
    <string name="input_name">請輸入玩家姓名</string>
    <string name="select_scissors">剪刀</string>
    <string name="select_rock">石頭</string>
    <string name="select_paper"></string>
    <string name="start_game">猜拳</string>
    <string name="label_name">姓名</string>
    <string name="label_winner">勝利者</string>
    <string name="label_me">我方出拳</string>
    <string name="label_pc">電腦出拳</string>
    <string name="title_activity_sub">SubActivity</string>
    <string name="nextPage">下一頁</string>
    <string name="nextPageTitle">第二頁</string>
    <string name="goBackButton">回上一頁</string>
</resources>

畫面程式組合

  • 一般檔案的長相基本上是「OOxx.kt + xx_oo.xml」為一組頁面,在iOS中就是像「OO.swift + OO.xib」的組合,在目前的Android寫法,還沒有像Storyboard的方式出現。在這裡使用的是「ConstraintLayout」,Layout產生的方式跟iOS的不太一樣,其中主要說明的是「android:id="@+id/?????"」的這個屬性,它就是這個元件的代號,可以把它想成是HTML的id,是個唯一值識別碼,另外「android:text="@string/?????"」的"@string"就是引用「res/values/strings.xml」內的文字mapping,只要改strings.xml,這裡的內容也會跟著改變。

第一頁的長相

  • 在這裡雖然跟可以用拉的去放元件,但總覺得沒有Xcode的好用,也許直接key比較快?先求有再求好吧。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">

    <TextView
            android:id="@+id/select_box"
            android:text="@string/select_box"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/input_name"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"/>
    <EditText
            android:id="@+id/input_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:ems="10"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:hint="@string/input_name"/>
    <RadioGroup
            android:id="@+id/radioGroup"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/select_box"
            android:layout_marginTop="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:orientation="horizontal">
        <RadioButton
                android:id="@+id/select_scissors"
                android:text="@string/select_scissors"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:checked="true"/>
        <RadioButton
                android:id="@+id/select_rock"
                android:text="@string/select_rock"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"/>
        <RadioButton
                android:id="@+id/select_paper"
                android:text="@string/select_paper"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"/>
    </RadioGroup>
    <Button
            android:id="@+id/start_game"
            android:text="@string/start_game"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/radioGroup"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"/>
    <TextView
            android:id="@+id/label_name"
            android:text="@string/label_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/start_game"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"/>
    <TextView
            android:id="@+id/label_winner"
            android:text="@string/label_winner"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/start_game"
            android:layout_marginStart="16dp"
            app:layout_constraintStart_toEndOf="@+id/label_name"/>
    <TextView
            android:id="@+id/label_me"
            android:text="@string/label_me"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/start_game"
            android:layout_marginStart="16dp"
            app:layout_constraintStart_toEndOf="@+id/label_winner"/>
    <TextView
            android:id="@+id/label_pc"
            android:text="@string/label_pc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:layout_marginStart="16dp"
            app:layout_constraintTop_toBottomOf="@+id/start_game"
            app:layout_constraintStart_toEndOf="@+id/label_me"/>
    <Button
            android:id="@+id/nextPage"
            android:text="@string/nextPage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="24dp"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
</android.support.constraint.ConstraintLayout>

onCreate()

  • onCreate()可以類比成iOS的viewDidLoad(),是程式的進入點,只執行一次,細節可以參考一下Android的生命週期,其中比較重要的是「setContentView(R.layout.main_activity)」就是載入「main_activity.xml」畫面檔到這個「Activity」內使用。另外EditText?這個「?」就表示它是「可以」是null值的意思,跟Swift的表示法相當。
class MainActivity : AppCompatActivity() {

    private var inputName: EditText? = null
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity) 
    }
}

R.java

  • R.java是一個很重要的檔,在「res」底下的檔案全部都會「自動編譯」在裡面,可以看看裡面都是些int值,代表的是各個資源檔的唯一ID值,利用「R.OO.XX」之類的程式,就可以讀取這些資源了,這個真的滿好用的,點點點就寫完了,但沒事別亂動它。在iOS也有學它的framework,叫做「R.swift」
/* AUTO-GENERATED FILE. DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found. It
 * should not be modified by hand.
 */

package com.example.android_helloworld;

public final class R {
  public static final class anim {
    public static final int abc_fade_in=0x7f010000;
    public static final int abc_fade_out=0x7f010001;
    public static final int abc_grow_fade_in_from_bottom=0x7f010002;
    ...
    }
}

findViewById()

  • 這個動作好像是一定會有的?利用findViewById()去讓該元件初始化,然後進行接下來的操作。不過要一個一個寫好麻煩啊,為什麼不用類似R.java的寫法呢?或者像@IBOutlet用拉的呢?XD
class MainActivity : AppCompatActivity() {

    /// 初始化設定元件
    private fun initElements() {

        inputName = findViewById(R.id.input_name)
        radioGroup = findViewById(R.id.radioGroup)
        startGame = findViewById(R.id.start_game)
        nextPage = findViewById(R.id.nextPage)

        labelName = findViewById<TextView>(R.id.label_name)
        labelMe = findViewById<TextView>(R.id.label_me)
        labelWinner = findViewById<TextView>(R.id.label_winner)
        labelPC = findViewById<TextView>(R.id.label_pc)
    }
}

setOnClickListener()

  • 利用setOnClickListener()設定按鍵按下的反應,在iOS應該類似叫.addTarget()。另外要說明的是「?:」這個運算子,它有點像Swift的 「guard let else」。
class MainActivity : AppCompatActivity() {

    /// 設定猜拳按鈕按下的動作 (沒有寫名字就跳提示)
    private fun setButtonListener() {

        val startGame = startGame ?: return
        val nextPage = nextPage ?: return

        startGame.setOnClickListener { moraResult() }
        nextPage.setOnClickListener { nextPage() }
    }
}

intent

  • intent在這裡的功能就是「跳到下一頁」,順便把「把包Bundle內的東西」一起帶過去,它比較像iOS的Segue,startActivityForResult()在iOS比較像present()。然後有回傳值的函式是長這樣,「fun bundleMaker(): Bundle」
class MainActivity : AppCompatActivity() {

    /// 跳到下一頁 (帶值)
    private fun nextPage() {

        val bundle = bundleMaker()
        val intent = Intent(this, SubActivity::class.java)

        bundle.putString("Winner", labelWinner?.text.toString())
        bundle.putString("UserName", inputName?.text.toString())
        intent.putExtras(bundle)

        startActivityForResult(intent, 1)
    }

    /// 產生要帶去下一頁的值
    private fun bundleMaker(): Bundle {

        val bundle = Bundle()

        bundle.putString("Winner", labelWinner?.text.toString())
        bundle.putString("UserName", inputName?.text.toString())

        return bundle
    }
}

Kotlin的Switch case

  • Kotlin的Switch case叫做when,功能很強大,除了有「Switch case」的功能之外,還可以有「if-else if」的能力,真是太厲害了啊。
class MainActivity : AppCompatActivity() {

   /// 我方出拳的結果 (勾選值)
    private fun moraResultForMe(): Int {

        val selectScissors = findViewById<RadioButton>(R.id.select_scissors)
        val selectRock = findViewById<RadioButton>(R.id.select_rock)
        val selectPaper = findViewById<RadioButton>(R.id.select_paper)

        var answer = 0

        when {
            selectScissors.isChecked -> answer = 0
            selectRock.isChecked -> answer = 1
            selectPaper.isChecked -> answer = 2
        }

        return answer
    }
}

onActivityResult()

class MainActivity : AppCompatActivity() {

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        getReturnValueForNextPage(requestCode, resultCode, data)
    }
}

APP的第二頁

新增第二頁

  • 新增第二頁,也就是加上一個新的Activity,在iOS中就是加上一個新的ViewController,比較要注意的是,除了會產生「OOxx.kt + xx_oo.xml」的組合之外,還會在「AndroidManifest.xml」中加上「<activity android:name=".OOXXActivity"></activity>」,用來表示這一頁是有被Build的,如果沒有加的話,就會讀不到這頁了喲。AndroidManifest.xml這個檔非常的重要,有空大家可以去查一查喲。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android_helloworld">
	<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/AppTheme">
	    <activity android:name=".SubActivity">
	    </activity>
	    <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>

取得上一頁傳來的值

  • 這裡呢,也是利用intent (iOS: Segue)來取得數值包(extras),另外「let」的功能就自己看一下囉。
class SubActivity : AppCompatActivity() {

    /// 取得上一頁傳過來的數值
    private fun getValueForPreviousPage(): String? {

        var title: String? = null

        intent?.extras.let {
            title = it?.getString("Winner", "什麼都沒有")
        }

        return title
    }
}

setResult()

  • 把值傳到上一頁也是利用intent帶值回去,然後利用setResult()把值傳回去,然後在上一頁的「onActivityResult()」就會收到了喲。
class SubActivity : AppCompatActivity() {

    /// 回傳數值到上一頁
    private fun returnValueForPreviousPage(subValue: String) {

        val bundle = Bundle()
        val intent = Intent()

        bundle.putString("SubValue", subValue)
        intent.putExtras(bundle)

        setResult(Activity.RESULT_OK, intent)
    }
}

範例程式碼下載

後記

  • 其實Kotlin的語法雖然跟Swift有點像,應該說Swift有去參考Kotlin吧?但是還是有滿大的差別的,還是要先看看kotlin語法才是,它是另一種JVM程式語言,也許將來它會越來越強大呢?其實我也是一知半解的,騙吃騙吃啦。