Android App
真的假的啦 — 真心話大冒險App
Lecture: 一直來不及備課的鹽亞倫
今天要講什麼勒?
- Android Studio介紹
- Kotlin基本語法
- UI 初探
- 用程式碼控制 UI
- 實作 --- 建立一個真心話大冒險App
今天所有東西的連結和要下載的檔案都放在這裡:
今天最多只能讓大家對於App的開發有著初步的認識,並且完成簡單小型的App設計。
所以不太可能上完課以後你就寫得出傳說對決
如果真的有興趣的話
建議大家自行自學更多進階的技巧喔
Disclamer
最後的成果
App開發基本介紹
Android vs iOS Apps
Android
iOS
- 開發語言: Java、Kotlin
- IDE: Android Studio
- 可以在Windows、macOS、Linux上面開發
- Android系統具開放性,使用者可以「側載」,App不需要上架
- 開發語言: Swift
- IDE: Xcode
- 只能在mac上開發
(其實聽說iPad也可以) - 系統封閉,一定要經由App Store
所以今天會教Android啦
Kotlin 語言介紹
- JetBrains 公司所開發
- 使用Java虛擬機(JVM)執行
- 可以和Java互相相容運作
- Google在2017年表態要用Kotlin取代Java
-
Kotlin優勢:
- 設計簡潔,語意清楚
- 避免 NullError
- 容易入門
- 有谷歌大神背書
怎麼學習App開發?
-
靠Google大神
- iT邦幫忙或Medium等部落格文章
- Stackoverflow
- 搜尋Youtube影片
- 講義有整理一些講師覺得不錯的文章
-
靠Google大神
- 萬能的Google準備了一整套的app入門教材,超讚
- https://developer.android.com/courses/android-basics-kotlin/course
今天這堂課就是從裡面偷東西出來的
Android Studio
- Google 和 JetBrains 公司合作
- Google 官方推薦。
- 功能十分強大
- 可以直接在電腦中弄出一台虛擬的 Android 手機出來方便我們進行 app 的測試
-
常常可以從裡面發現Google新手機的資訊(Google保密真是頂呱呱的)
來安裝吧
安裝完之後長相
建立第一個專案
按 New Project
選擇 empty activity
按一下視窗底部的「Next」(下一步)。
「New Project」(新增專案) 對話方塊隨即開啟。
應用程式的名稱,幫我改成Truth_or_Dare
Android 系統用來識別應用程式的名稱
專案資料夾位置(可用預設值)
程式語言(請選Kotlin)
SDK版本,越舊支援度越高,今天選 API 19: Android 4.4 (KitKat)。
第一次開啟 Android Studio 時,會看到三個視窗:
-
「Project」(專案) 視窗會顯示專案的檔案和資料夾。
-
「Editing」(編輯) 視窗可編輯程式碼
-
「What’s New」(最新消息)(可以直接關掉)
建立手機模擬器
按這裡可以快速開啟Device Manager
按這裡可以叫出已建立的模擬器
模擬器的使用:
執行程式碼
手機的各種按鈕
Android Studio 檔案結構介紹
有Android模式和project Files模式
Android
- 整理出常用檔案,東西比較好找
- 程式碼在app/java資料夾底好
- 圖檔、UI等資源在res
Project Files
- 實際各個檔案在電腦裡的儲存位置
- 東西比較難找到
Kotlin基本語法
Kotlin Playground
- 簡單的線上寫code環境
- 練習語法時的好地方
- provided by Google
- https://developer.android.com/training/kotlinplayground
- provided by kotlinlang.org
- https://play.kotlinlang.org/
Hello World
fun main() {
println("Hello, world!!!")
}
可以看到幾個重點:
- 可以不加分號
- 用fun關鍵字宣告函數
- 和C++一樣要有main函數
-
用
println()
輸出並且換行 用print(),就不會自動換行
變數
- 整數類別:Long, Int, Short, Bite
- 浮點數型態(由大到小): Double, Float
- 字元型態:Char
- 布林值型態:Boolean
- 字串:String
注意:所有型態首字為大寫
整數
小數
變數宣告
變數宣告關鍵字:
val
var
- value
- 此變數只讀不寫
- 不能修改參考對象
- (除了初始化不能用=符號)
- 和UI整合時常用
- variable
- 就一般常見的變數
變數宣告方式
var 變數名稱: 型態 = 初始值
或是
val 變數名稱: 型態 = 初始值
val a: Int = 10
var b: String = "I am so weak."
val c: Float = 1.0 + 3.0
有初始化的變數也可以省略型態
var a = 10
輸入
今天不會用到,做個補充而已
用readLine()可以讀一行
import java.util.Scanner 之後
可以用類似cin的東西
import java.util.Scanner
fun main() {
val read = Scanner(System.`in`)
println("請輸入你的年齡:")
var age = read.nextInt()
println("你的年齡是:"+age)
}
條件
if else
val count = 10
if (count < 5) {
println("太少")
} else if (count > 5) {
println("太多")
} else {
println("剛剛好")
}
神奇的是,Kotlin中的if可以有回傳值。
每個區段的最後一行的值,會被當作若執行到該區段會讓整個語句回傳的值:
var score: Int = 85
var grade: String = if(score<=100 && score>80){
"A"
}else if(score<=80 && score>=60){
"B"
}else{
"C"
}
println("Grade: $grade")
//印出 Grade: A
When
條件
類似Switch case
when{
條件一 -> {
//如果條件一成立就執行區塊,反之繼續判斷條件二
}
條件二 -> {
//如果條件二成立就執行區塊,反之繼續判斷條件三,以此類推
}
else -> {
//如果以上的條件都不符合,就執行這個區塊
}
}
When
條件
範例
var score: Int = 85
var grade = ""
when{
score<=100 && score>80 -> grade = "A"
score<=80 && score>=60 -> grade = "B"
else -> grade = "C"
}
var score: Int = 85
var grade: String = when{
score<=100 && score>80 -> "A"
score<=80 && score>=60 -> "B"
else -> "C"
}
一樣可以有回傳值
while
迴圈
範例
// while (節錄自 Kotlin 官方教學程式碼)
// 會接續印出 0 到 9
var x = 0
while (x < 10) {
println(x)
x++
}
for
迴圈
蠻多種類的,但很簡單
// 會接續印出 0 到 10
for (x in 0..10) {
println(x)
}
// 會接續印出 0 到 9
for (x in 0 until 10) println(x)
// 會接續印出 0, 2, 4, 6, 8
for (x in 0 until 10 step 2) println(x)
// 會接續印出 10, 8, 6, 4, 2, 0
for (x in 10 downTo 0 step 2) println(x)
val names = listOf("Anne", "Peter", "Jeff")
// 會接續印出 Anne 、 Peter 、 Jeff
for (name in names) {
println(name)
}
range
就...類似python的range
(1..10)
一個1到10的range:
for i in (1..10) {
print(i)
print(" ")
}
// output: 1 2 3 4 5 6 7 8 9 10
除了for迴圈以外,range可以幹嘛
亂數
Random.nextInt() // 產生一個Int亂數
Random.nextFloat() // 產生一個Float亂數
Random.nextInt(1, 10) // 產生一個1到10的Int亂數
(1..10).random() // 產生一個1到10的亂數
函數
定義方式:
fun [函式名稱]([參數 1 名稱]: [參數 1 型態]): [回傳型態] {
[內容]
}
範例:
fun BMI(val w: Float, val h: Float): Float {
return w/(h*h);
}
// 如果函式只有一行 return 的話,可簡寫成等式
fun happyBirthday2(name: String, age: Int) =
"Happy ${age}th birthday, $name!"
類別
class
Collection
類似C++的STL
包含Array、Map、Set、List等等
名稱 | 中文名稱 | 備註 | 教學網址 |
---|---|---|---|
Array | 陣列 | 長度固定 | https://ithelp.ithome.com.tw/articles/10237401 |
List | 清單 | 長度&內容不可變 | https://ithelp.ithome.com.tw/articles/10238930 |
MutableList | 可變清單 | 長度&內容可變 | |
Set | 集合 | 內容不可變、內容不重複 | https://ithelp.ithome.com.tw/articles/10239409 |
MutableSet | 可變集合 | 內容可變、內容不重複 | |
Map | 就是Map | 鍵(Key)值(Value)對照 | https://ithelp.ithome.com.tw/articles/10240148 |
MutableMap | 就是可變Map | 內容可變 |
今天只會用到MutableSet
宣告
用mutableSetOf()函數:
val muSet = mutableSetOf("Jim", "Sue", "Sue", "Nick", "Nick")
// 只會存放不重複的 Jim, Sue, Nick
val SetOfAges: MutableSet<int> = mutableSetOf(31, 25, 10, 32, 12)
若要宣告空的:
val emptyMutableSet = mutableSetOf<String>()
今天只會用到MutableSet
取值、亂數取值
muSet.elementAt(0) // "tim"
println(muSet.random()) // 亂數輸出一個
var returnValue = muSet.randomOrNull() // set沒東西時會回傳null
今天只會用到MutableSet
取大小&修改&刪除
// 印出 set 大小
println(muSet.size)
// 在 set 最後面加入
muSet.add("Bob")
// 在 set 最後面加入一堆 data
muSet.addAll(listOf("a", "b", "c"))
// remove 某資料
muSet.remove("tim")
// 清除所有
muSet.clear()
練習時間
輸出10階金字塔
練習的答案
fun main() {
var n = 10
for (i in 1..n){
for (j in 1..i){
print('*')
}
print('\n')
}
}
UI 基礎教學
之一
基本介紹
什麼是UI
- User Interface
- 使用者介面,簡稱UI
- 指App當中使用者看到的所有東西&版面配置
- ex: 文字位置、大小、顏色、圖片及按鈕
- 一般用XML寫,語法上類似HTML和CSS的混合
- Android Studio允許直接以滑鼠拖拉的方式完成UI設計
View
- 所有東西都是一個View
- TextView、ImageView、Button等等
先回到剛剛Android Studio建立的專案當中
目前你的畫面應該是這樣子的
此時你編輯器開啟的檔案是MainActivity.kt,也就是之後要寫Kotlin程式碼的位置
點按執行,可以看到一個Hello World的App
這就是一個預設的基本UI長相
版面配置編輯器
- Android對於新手最友善的一個功能
- 利用它來修改UI
從左邊專案資料夾中選擇 res/layout/activity_main.xml
- 該檔案即為主要的UI配置檔案
如果打開activity_main.xml之後沒有看到版面配置編輯器,可在右上角找到這個地方
請選擇Design
- (1) 為「Project」(專案) 視窗
- 畫面中央會顯示 (4) 和 (5) 兩個繪圖,代表應用程式的螢幕版面配置。
- 左側標記 (4)為「Design」(設計) 檢視畫面,會顯示應用程式執行時呈現的近似效果。
- 右側標記 (5) 的視窗代表「Blueprint」(藍圖) 檢視畫面
- 標記 (2) 的「Palette」(區塊面板) 視窗有各種你可以使用的views
- 標記 (3) 的「Component Tree」(元件樹狀結構) 是另一種螢幕檢視畫面方式,列出螢幕的所有檢視畫面。
- 右邊(6)視窗是「屬性」(Attributes),顯示了 View 的各種設定,可在這裡加以變更。
UI 基礎教學
之二
View的使用
常用的 View
今天會用的 View
- TextView 文字方塊
- Button 按鈕
- ImageView 圖片
- EditText 輸入方塊(位在Text選單的plain text)
View 的使用
-
拖到畫面中
-
固定位置
- 調整設定
以TextView為例
2. 固定位置
用這些彈簧定義了物件的相對位置限制
3. 調整設定
- 點選一個物件之後,你可以在右邊的attribute視窗當中,調整這個物件的一些相關事務及設定。
常用的設定如下:
- id: 每一個物件都應有一個專屬的id,請用容易記得與分辨的名字
- text: 顯示的字
- visibility: 顯示與不顯示
- layout_width 和 layout_height: 寬度及高度,wrap_content代表根據內容大小自動調整。
- textSize: 文字大小
- textColor: 文字顏色
- fontFamily: 字型
- textStyle: 樣式(粗體之類的)
- textAlignment: 對齊方式
基本上用法和ppt差不多
亂試幾次就會了
另外,這個區域可以針對剛剛彈簧做進一步設定:
修改其數值代表最少該側我要留多少空間,為0則代表允許緊貼。
修改為60,25時的長相
試試看做出這個版面配置
ID: TitleText
大小:34sp
ID: SubtitleText
大小:20sp
其他View的使用
Button的使用
- Button基本上就是按鈕,可以按下去的那種
- 設定調整方式和TextView差不多
- 怎麼偵測被按下去之後會教
其他View的使用
ImageView的使用
- 透過ImageView使用圖片檔
- 要先建立drawable
- drawable指的是可以被畫到螢幕上的東西
- ImageView是一個View,可以透過程式決定顯示哪一個drawable
其他View的使用
ImageView的使用
1. 將圖片匯入為drawable
- 到這裡下載今天會用到的所有圖片:https://bit.ly/2023WinterApp
- 接下來,點選resource manager
其他View的使用
ImageView的使用
1. 將圖片匯入為drawable
其他View的使用
ImageView的使用
1. 將圖片匯入為drawable
- 接下來選擇你要上傳的圖片檔,然後按下next,就完成上傳了。
其他View的使用
ImageView的使用
1. 將圖片匯入為drawable
- 接下來選擇你要上傳的圖片檔,然後按下next,就完成上傳了。
其他View的使用
ImageView的使用
2. 建立imageView
- 回到版面配置編輯器,從左邊工具箱中選擇ImageView,並拖到畫面中。
- 這時候要選擇你ImageView要使用的Drawable
- 這樣就成功建立ImageView了
其他View的使用
editText的使用
- 輸入文字
-
palette中的Text標籤,除了TextView都是不同種的EditText
-
今天只會用到Plain Text
- editText基本上和textView用法差不多
- 設定多了一個hint選項
- 代表使用者尚未輸入內容時會顯示的文字。
- text屬性則為使用者輸入/編輯的內容
其他View的使用
editText的使用
editText在模擬器中長相:
實作時間
做出今天真心話大冒險的UI長相
指針圖片可以在雲端找到
紅字為id
UI 基礎教學
之三
補充:Xml檔案
xml檔案
剛剛有說過,實際上的UI是使用XML檔案紀錄的。
XML是一種標記語言,
語法上類似html,記錄的東西類似css
我們透過以上切換到code,可以看到如以下之長相。
我們可以發覺,所有我們動過的屬性設定皆會被以文字方式記錄下來。
切換到split標籤的話我們可以同時看到程式碼與UI的預覽。
還有很多東西是用xml紀錄的
res/values/strings.xml
記錄字串變數的地方
app_name 指的是手機上icon底下會顯示的名字
用Kotlin控制UI
今天最重要的一步
- 現在我們已經會使用Kotlin以及UI了
- 將兩者整合起來,以完成我們的app。
- 有點類似用js去控制網頁element
- 等等會介紹多個函數
如何在android studio裡面寫kotlin?
package com.example.helloworldtest1
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { // main函數
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 從這裡接者寫
}
// 函數可以宣告在這裡
}
// class 可以宣告在這裡
打開 mainActivity.kt
程式碼都要寫在這裡
其中 onCreate函數相當於main函數
開啟自動import功能
直接看教學
或講義
Toast.makeText()
- 出現在手機下方的彈跳通知
Toast.makeText(this, "要顯示的訊息", Toast.LENGTH_SHORT).show()
- 效果:
findViewById
- 將程式碼與UI整合過程中最重要的一步
-
使用方式如下:
val 變數名: View類別 = findViewById(R.id.物件的id)
- 這時,你會得到一個指向UI物件的一個參考變數。
findViewById
舉例,如果你的UI裡面有一個id為my_button的按鈕,那麼你可以這樣寫:
val a: Button = findViewById(R.id.my_button)
這樣一來變數a就會是那個按鈕了。如此一來,你可以透過變數a來存取my_button的屬性
使用方法為 a.屬性名
findViewById
例如如果你要輸出Button上面寫的字(text屬性),你可以這樣做:
val textString: String = a.text.toString()
Toast.makeText(this, textString, Toast.LENGTH_SHORT).show()
- 以上用法常用於editText
- 因為使用者輸入的數字就會存在editText的text屬性
- 如此就可以讀取使用者輸入的內容了
.setText()
用這個方法函數修改UI元件的text屬性
val k: TextView = findViewById(R.id.textViewid);
k.setText("hello CKEFGISC")
setOnClickListener
這個函數主要和按鈕連用,代表當一個按鈕被按下時,要執行什麼程式。
按鈕變數.setOnClickListener{
// 這裡寫被按下之後要執行的東西
}
setOnClickListener
使用例子:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myButton: Button = findViewById(R.id.ButtonId1)
myButton.setOnClickListener{
// 這裡寫被按下之後要執行的東西
Toast.makeText(this, "hahahahaha", Toast.LENGTH_SHORT).show()
}
}
}
handler
時間暫停器
先等一段時間後再執行一段程式碼
val handler = Handler() // 建立handler()
handler.postDelayed( { // 計時器
// 一段時間之後要做的事
}, delayTime) // delayTime單位是毫秒
實作時間
今天的實作目標
真心話大冒險app
兩大目標:
- 有一個旋轉的指針,當按下start按鈕之後,能夠有旋轉指針的動畫,並且停下來後會隨機出現題目
- 有一個輸入欄和一個按鈕,輸入完之後可以將使用者輸入的問題存入問題清單中,並且在之後顯示。
剛剛應該已經把UI建立完了吧
接下來直接從撰寫程式碼開始了喔
回到 mainActivity.kt
以下是這次程式碼的大致架構
package com.example.truth_or_dare
import android.os.Bundle
import android.view.animation.RotateAnimation
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import android.os.Handler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 取得各個UI元件
val rollButton: Button = findViewById(R.id.start_button)
val addQuestionButton: Button = findViewById(R.id.add_button)
val questionTextBlock: TextView = findViewById(R.id.question)
val inputTextBlock: EditText = findViewById(R.id.text_input)
val rotateArrowImage : ImageView = findViewById(R.id.spinnerImage)
val questionSet: MutableSet<String> = mutableSetOf<String>(
"測試問題一",
"測試問題二"
) // app題目清單
rollButton.setOnClickListener {
// 開始轉動按鈕被按下之後要執行的程式
}
addQuestionButton.setOnClickListener {
// 新增問題按鈕被按下之後要執行的程式
}
}
繪製流程圖
功能一
- 按鈕被按下
- 決定旋轉角度
- 開始旋轉動畫
- 等到動畫轉完之後:
- 從題單裡面隨機一題
- 將id為question的textView
的text屬性更改為題目的文字
好像沒講過?
繪製流程圖
功能二
- 按鈕被按下
- 讀取editView的text屬性
- 將讀取到的東西存到set裡面
- 清空editView text屬性的文字
旋轉動畫
轉動動畫可以使用kotlin內建的Animation完成,使用方法如下
val am = RotateAnimation(開始角度, 結束角度,
RotateAnimation.RELATIVE_TO_SELF, 旋轉x座標中心,
RotateAnimation.RELATIVE_TO_SELF, 旋轉y座標中心) // 建立動畫物件,注意,角度要用Float型態
am.duration = 旋轉時間 // 設定旋轉時間
am.setFillAfter(true) // 設定旋轉完後停在該角度
myImageView.startAnimation(am) // 將myImageView以am動畫執行
旋轉動畫
使用的範例
val am = RotateAnimation(0.0F, 720.0F,
RotateAnimation.RELATIVE_TO_SELF, 0.5F,
RotateAnimation.RELATIVE_TO_SELF, 0.5F)
// 從0度到720度,共轉兩圈
// 轉動中心在圖片的(50%, 50%)位置,即原本圖片的中心
am.duration = 2000
am.setFillAfter(true)
rotateArrowImage.startAnimation(am)
來寫完功能一吧
答案
var startDegree = 0.0f // 記錄目前指針的角度
var endDeg = 0.0f
rollButton.setOnClickListener {
// 跳出提示訊息
Toast.makeText(this, "開始旋轉!", Toast.LENGTH_SHORT).show()
// 旋轉動畫
var rotateDeg : Int = (1..360).random() // 要旋轉幾度
rotateDeg += (3..6).random() * 360 // 多加幾圈提高動畫效果
endDeg = startDegree + rotateDeg; // 結束的角度為當前角度加上要旋轉的角度
val am = RotateAnimation(startDegree, endDeg,
RotateAnimation.RELATIVE_TO_SELF, 0.5F,
RotateAnimation.RELATIVE_TO_SELF, 0.5F)
val spinTime : Long= (rotateDeg*3).toLong() // 轉多久
am.duration = spinTime
am.setFillAfter(true)
rotateArrowImage.startAnimation(am)
startDegree = endDeg % 360 // 更新起始角度數值
val handler = Handler() // 建立handler()
handler.postDelayed( { // 計時器
// 出現提問
var chosenQuestion = questionSet.random()
while (chosenQuestion == questionTextBlock.text) { // 避免問題和上一題重複
chosenQuestion = questionSet.random()
}
questionTextBlock.setText(chosenQuestion)
}, spinTime)
}
來寫完功能二吧
答案
addQuestionButton.setOnClickListener {
val stringInTextField = inputTextBlock.text.toString()
if (stringInTextField.isNotEmpty()) {
questionSet.add(stringInTextField)
Toast.makeText(this, "成功新增問題", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "請先輸入內容", Toast.LENGTH_SHORT).show()
}
inputTextBlock.setText("") // 清空文字輸入欄
}
package com.example.truth_or_dare
import android.os.Bundle
import android.view.animation.RotateAnimation
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import android.os.Handler
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rollButton: Button = findViewById(R.id.start_button)
val addQuestionButton: Button = findViewById(R.id.add_button)
val questionTextBlock: TextView = findViewById(R.id.question)
val inputTextBlock: EditText = findViewById(R.id.text_input)
val rotateArrowImage : ImageView = findViewById(R.id.spinnerImage)
val questionSet: MutableSet<String> = mutableSetOf<String>(
"測試問題一",
"測試問題二"
) // 寫在這裡的會是app預設就有的問題
var startDegree = 0.0f
var endDeg = 0.0f
rollButton.setOnClickListener {
// 跳出提示訊息
Toast.makeText(this, "開始旋轉!", Toast.LENGTH_SHORT).show()
// 旋轉動畫
var rotateDeg : Int = (1..360).random()
rotateDeg += (3..6).random() * 360
endDeg = startDegree + rotateDeg;
val am = RotateAnimation(startDegree, endDeg,
RotateAnimation.RELATIVE_TO_SELF, 0.5F,
RotateAnimation.RELATIVE_TO_SELF, 0.5F)
val spinTime : Long= (rotateDeg*3).toLong()
am.duration = spinTime
am.setFillAfter(true)
rotateArrowImage.startAnimation(am)
startDegree = endDeg % 360
val handler = Handler() // 建立handler()
handler.postDelayed( { // 計時器
// 出現提問
var chosenQuestion = questionSet.random()
while (chosenQuestion == questionTextBlock.text) {
chosenQuestion = questionSet.random()
}
questionTextBlock.setText(chosenQuestion)
}, spinTime)
}
addQuestionButton.setOnClickListener {
val stringInTextField = inputTextBlock.text.toString()
if (stringInTextField.isNotEmpty()) {
questionSet.add(stringInTextField)
Toast.makeText(this, "成功新增問題", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "請先輸入內容", Toast.LENGTH_SHORT).show()
}
inputTextBlock.setText("") // 清空文字輸入欄
}
}
}
製作安裝檔
apk檔案
- android app安裝檔
- android studio可以非常簡單的產生
- 在工具列點選Build -> Build Bundle(s) / APK(s) -> Build APK(s)
- 搞定!
- 選Locate
- 或是直接去這個位置找apk檔:
專案資料夾/app/build/outputs/apk/debug
得到apk檔案之後?
用任何你知道的方式傳到你的手機
安裝他
完成!!!
成果展示時間
下課啦~
2023建北電資聯合寒訓 --- Android App 教學
By Aaron Wu
2023建北電資聯合寒訓 --- Android App 教學
- 226