TWMA重製by AndoidStudio with kotlin
TWMA重製by AndoidStudio with kotlin
主要架構
Android Studio推薦使用kotlin做為開發語言
因此在本專案中,以kotlin為主
整體框架上
- 透過Compose來描述整個app的UI(目前是把一個頁面的UI寫在一個.kt檔中)
- 並利用類別來處理資料,整體資料的操作方法都被存放在WordList中
- 在資料儲存的方面,透過SQL lite來做處理,專門去紀錄WordList的變動語資料
預計實現效果
介面相關
頁面切換
背景切換
布局切換
資料相關
單字的新增、刪除
單字的顯示
單字測試
系統相關
等級紀錄
金幣紀錄
抽獎系統
Compose系統
概述
Compose的使用就是在函數前面加上@compose標籤
每當一個Compose發生改動的時候,會自動摧毀並重建,達成刷新UI的效果
而最初的Compose會被放置在主函數下的setContent中啟用
tips 每當螢幕旋轉時,整個android app的生命週期會將UI給摧毀掉
因此若要保存Compose下的數據,需要使用by remember的方式
各種可組合項
實現UI介面效果上
在Compose中,可以利用下面列出的可組合項
Surface:
簡單來說,會將所有的元件放在這個平面上,且透過這個平面來處理modifier可以不用再單個元件上,添加太繁瑣的修飾
從字面上來理解,是一個平面,在 Material Design 設計準則中也同樣如此,我們可以將很多的元件擺放在這個平面之上,我們可以設定這個平面的邊框,圓角,顏色等等。 接下來,我們來用 Surface 組件做出一些不同的效果。
而 Surface 主要負責整個組件的形狀,陰影,背景等等,Surface 可以幫助我們更好的解耦一些程式碼,而不必在單一元件上添加很多的 Modifier 修飾符方法。
by https://jetpackcompose.cn/docs/layout/surface/
Row:
一種水平排列的組件
並且可以透過Arrangement來調整排列的方式
組件能夠將裡面的子項依照從左到右的方向水平排列。
by https://jetpackcompose.cn/docs/layout/row
Column:
一種垂直排列的組件
同樣可以透過Arrangement來調整排列的方式
example
column的模板程式,關於一個column的實現
@Composable
inline fun Column(
modifier: Modifier? = Modifier, //modifier 可以修改這個column的一些顯示上的參數
verticalArrangement: Arrangement.Vertical? = Arrangement.Top, //垂直上的對齊方式 靠上
horizontalAlignment: Alignment.Horizontal? = Alignment.Start, //水平上的對齊方式 靠右
content: (@Composable @ExtensionFunctionType ColumnScope.() -> Unit)?
): Unit
tips Lazy Column
是一種類似Column的元件,但自帶懶加載功能
對於需要滑動列表,來檢視大量資料的地方可以使用
ref https://developer.android.com/jetpack/compose/lists?hl=zh-tw
Box
布局方式為九官格的定位方法
也可依次按照順序推疊
有用的子元件
Text 用來顯示字串
Icon 用來顯示圖標
Button 用於按鍵
Compose靜態預覽
在compose中可以透過在函式前面加上@preview關鍵字,並在函數內呼叫其他可組合項,來查看實際上的呈現結果
能夠節省每次都開虛擬機確認UI布局的時間,缺點是不能夠進行互動
因此如果是用代碼操控了可組合項的顯示、動畫,就沒辦法用這招預覽
//預覽 關燈模式
/*@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark"
)*/
@Preview( //開燈模式
showBackground = true, //是否顯示背景
uiMode = Configuration.UI_MODE_NIGHT_NO, //設定當前的模式
name = "light", //該預覽的名稱
)
@Composable //就算是預覽也要記得添加可組合向的修飾符
fun PreviewWordInput() {
TWMATheme {
WordInput(goToWordPage = {}, wordList = WordList())
}
}
通用參數描述
modifier修飾符
可以添加用於修飾元件的描述,添加順序將會對效果造成影響
//語法範例
@compose
fun Show() {
//在modifier後面可以用.來銜接不同的修飾
Row(modifier = Modifier.fillMaxHeight()
.alpha(0.75f)
)
}
常見的修飾符包括
- fillMaxHeight() 填充到父元件的高度
- fillMaxWidth() 填充到父元件的寬度
- fillMaxSize() 填充到父元件的大小
- alpha(float) 調整透明度
- setSize(int, int) 設定大小
- padding(int) 留下特定寬度的邊框
針對於不同的命名空間,會有額外的修飾符選項
例如在RowSpace、ColumnSpace中
存在 weight(float) 的修飾符,可以定義該子元件在該row 或是 column下佔的空間權重
tips 使用weight後 setSize會失效
tips 各元件詳細的參數
請查詢jetpack compose的手冊
或是你好 Compose
ref1 https://developer.android.com/jetpack?hl=zh-tw
ref2 https://jetpackcompose.cn/docs/
狀態提升
將當前這個Compose存儲的狀態變數,拉到他的父組件上
並統一由最上層的父組件來管理狀態變數
當子組件需要改動父組件紀錄狀態的變數時,使用呼叫子組件時傳入的函數,以call back的方法來改動父組件的狀態變數
@compose
fun farther() {
var state by remember{
mutableStateOf(false)
}
child(()->{state = !state})
}
//將需要使用的狀態變數state提升到farther
//子組件利用changeState函數call back改變state
@compose
fun child(changeState: ()->Unit) {
changeState()
}
tips 在compose中處理變數,每當compose重組,變數的資料就會被清掉
因此我們需要用by remember的方式來讓app保留這個變數
而添加mutableStateOf則是讓compose能夠監聽這個變數
一旦變數發生變化,該compose變化重組
Navigation導航
透過navigation導航,可以實現android app中,當按下手機返回鍵,會回到上個頁面的效果
返回的感覺上類似stack(我自己覺得啦
整個navigation的設計上,也遵循著狀態提升的原則,如下
@Composable
fun AppNaviGation() {
//透過navController
//可以處理compose之間的切換
//用於指定可以在可組合項之間切換的目的地
val navController = rememberNavController()
//而切換的時候
//則會由navHost在目的地重組
NavHost(navController = navController,
startDestination = "WordInput") {
//startDestination為最初始的位置
//導航的地址註冊為一個String
composable("WordInput") {
WordInput(goToWordPage = {navController.navigate(route = "WordPage")},
wordList = wordList) //前往單字倉庫
}
composable("WordPage") {
WordPage(rebackToWordInput = {navController.navigate(route = "WordInput")}, wordList = wordList) //回到單字輸入頁
}
}
}
kotlin的簡單語法
基礎語法-變數
//變數賦值
var variable: String //var代表這個變數可以改動
val variable1: String //val代表這個變數不能改動
var variable2: String? //?代表這個變數可能為空
//切換回來的方法是
//在使用前判斷if(variable2 != Unit)
var variable2: String = "This is a String" //可以這樣初始化
var variable3 = "This is easy way" //簡化 推薦使用該方法
函數
//函數宣告
//fun functionName(paraName: paraType): returnType{}
fun Fun1(parameter1: Int, para2: Int):Int {
/*
* function body
*/
return 1
}
//可以設定預設值
fun Fun2(parameter1 = 1, para2 = 2):Int {}
//呼叫Fun1
var ans = Fun1(4, 5)
//指定parameter1參數接收的值,並讓para2用預設的值
ans = Fun2(parameter1 = 5)
OOP-類別
在kotlin中,類別有主建構子以及次建構子
執行順序是:
- 主 constructor
- 變數
- init block 們
- 最後才是第二個 constructor block 內的內容,第三個 constructor block 內的內容…依此類推
//使用主建構函數 直接在類別名稱後面加上contructor
class Word constructor(_data:String, _id:Int) {
var word:String
var id:Int
//初始化block
//可以有多個
init {
this.word = _data
this.id = _id
}
//次建構函數 這裡的回傳是this(即主建構函數)
//可以有多個存在
constructor(): this(_data:String, _id:Int) {
this.word = _data + "contrsucted by 1"
}
//利用傳入的參數defaultID 創建這個類別實體
constructor(defaultID: Int): this(_data:String, _id:Int) {
this.word = ""
this.id = defaultID
}
}
資料庫
利用SQL lite來實現輕量化的資料庫,需要
- Database:資料庫
- Dao:定義可以執行的操作
- Entity:定義資料實體
Database
資料庫,透過@Database標籤
//資料庫
//entities定義的列表 表示當前資料庫中有幾個不同的資料實體(每個用一張表去紀錄
@Database(entities = [WordE::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao //把DAO添加在這裡
}
Dao
參考SQL的語法,by https://www.fooish.com/sql/syntax.html
具體定義可以在表中進行的操作
@Dao
interface UserDao {
//插入單字
@Insert
fun insertWord(wordE: WordE?): Long? //回傳插入位置的id(類似第幾列的感覺)
//查詢特定單字
@Query("SELECT * FROM wordE WHERE id = :wordID")
fun queryWord(wordID: Long): WordE? //根據傳入的wordID在id的欄位中,找到特定的資料並取出
//取出全部資料
@Query("SELECT * FROM wordE")
fun queryAll(): List<WordE> //取出全部的資料,回傳一個資料實體形成的表格
//example沒有實際作用
@Delete
fun delete(wordE: WordE?): Int
//從id的欄位找到wordID並將其刪除
@Query("DELETE FROM wordE WHERE id = :wordId")
fun delete(wordId: Long): Int
}
Entity
資料實體,定義了存放在資料庫中的資料長相
類似於定義了一張表中,單個行存放的資料型態、名稱
必須有一個PrimaryKey,功能類似列的編號(第一列、第二列、第三…)
//每個entity相當於一張表
//類似定義一個表格中 一整列中每一行的欄位(名稱、儲存型態)
@Entity(tableName = "wordE")
data class WordE(
//主鍵 用於在資料庫中識別不同的資料列
@PrimaryKey(autoGenerate = true) //自動分配key值 初始為1
val id: Long?,
//定義該行的名稱 以及儲存的類別型態
@ColumnInfo(name = "foreignData") val foreignData: String?,
@ColumnInfo(name = "localData") val localData: String?,
@ColumnInfo(name = "extraInf") val extraInf: String?,
@ColumnInfo(name = "correctRate") val correctRate: Double?
)