Android UI(一)登錄注冊 - Compose

UI - 登錄注冊 - Compose

    • 一、聲明式UI
      • 1. **顛覆傳統開發模式**
      • 2. **技術優勢**
      • 3. **開發效率提升**
      • 4. **未來生態方向**
      • 5. **實際影響**
    • 二、創建項目
      • 1. Compose UI結構
      • 2. Scaffold
      • 3. 可組合函數
    • 三、創建組件頁面
      • 1. LoginPage
      • 2. RegisterPage
      • 3. MainPage
    • 四、導航
      • 1. 添加依賴
      • 2. 使用導航
      • 3. 初始化
      • 4. AndroidManifest.xml配置
    • 五、代碼分離
    • 六、源碼

??下面我們將進行Android UI的學習,我們將開始聲明式UI,Compose的學習,熟練之后你將不會再想要去使用常規的XML繪制UI了,運行效果如下圖所示:

在這里插入圖片描述

一、聲明式UI

Android Jetpack Compose 是 Google 推出的現代 聲明式 UI 框架,用于簡化 Android 應用界面開發。其核心意義體現在以下方面:


1. 顛覆傳統開發模式

  • 聲明式編程:通過描述 UI 應該是什么狀態(而非一步步指令式操作),代碼更直觀、易維護。
  • 告別 XML 布局:純 Kotlin 代碼構建 UI,減少模板代碼,提升開發效率。

2. 技術優勢

  • 響應式設計:UI 自動響應狀態變化(如數據更新),無需手動調用 setText() 等操作。
  • 高性能:基于智能重組(Recomposition)機制,僅更新變化的部分,避免全局刷新。
  • 組合優于繼承:通過可復用的 Composable 函數靈活拼裝界面,避免深層 View 嵌套問題。
  • 原生支持 Material Design:內置 Material 組件和動畫 API,快速實現現代化設計。

3. 開發效率提升

  • 實時預覽:Android Studio 的 交互式預覽Deploy Preview 功能,加速 UI 調試。
  • 簡化狀態管理:與 ViewModelFlow 等架構組件深度集成,邏輯與 UI 解耦更清晰。

4. 未來生態方向

  • 跨平臺擴展:通過 Compose Multiplatform 支持 iOS、桌面(Windows/macOS/Linux)和 Web,共享業務邏輯。
  • 社區趨勢:Google 主推的下一代 UI 方案,逐步替代傳統 View 系統,成為 Android 開發生態的核心。

5. 實際影響

  • 降低入門門檻:對新手更友好,減少對 XML 和傳統 View 系統的學習成本。
  • 提升應用性能:更高效的渲染機制和更少的代碼量,間接優化應用體積和流暢度。

二、創建項目

了解什么是Compose之后,下面創建Compose項目,記得要選擇Empty Activity

在這里插入圖片描述
命名為Android-UI-Compose,點擊Finish完成創建。
在這里插入圖片描述

創建完成之后,我們先分析里面的代碼。

1. Compose UI結構

我們打開MainActivity.kt,代碼如下所示:

package com.example.android_ui_composeimport android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.android_ui_compose.ui.theme.AndroidUIComposeThemeclass MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {AndroidUIComposeTheme {Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->Greeting(name = "Android",modifier = Modifier.padding(innerPadding))}}}}
}@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {Text(text = "Hello $name!",modifier = modifier)
}@Preview(showBackground = true)
@Composable
fun GreetingPreview() {AndroidUIComposeTheme {Greeting("Android")}
}

上述代碼很簡單,因為我們是第一次寫,所以我們講的詳細一點:

  1. MainActivity類

    • 繼承自ComponentActivity
    • 重寫了onCreate()方法
    • 使用enableEdgeToEdge()讓內容延伸到系統欄(狀態欄和導航欄)下面
    • 使用setContent設置Compose UI
  2. UI結構

    • 最外層是AndroidUIComposeTheme(自定義主題)
    • 使用Scaffold作為布局骨架(Material Design 3的腳手架組件)
    • Scaffold內部顯示Greeting組件
  3. Greeting組件

    • 是一個可組合函數,接收name和modifier參數
    • 顯示一個簡單的文本"Hello $name!"
    • 使用傳入的modifier控制布局
  4. 預覽功能

    • GreetingPreview函數提供了在Android Studio中的預覽功能
    • 使用@Preview注解標記
    • 同樣應用了主題并顯示Greeting組件

代碼特點:

  1. 完全使用聲明式UI(Compose)而非傳統XML布局
  2. 遵循Material Design 3設計規范
  3. 支持邊緣到邊緣(edge-to-edge)顯示
  4. 有預覽功能方便開發時查看效果

2. Scaffold

Scaffold是一個腳手架,說到到腳手架你能想到什么?框架,你可以把它理解成一個房間的框架,假如登錄頁面是一個房子的話,那么這個Scaffold就是房子的基本框架,在里面加什么內容取決于我們自己,MainActivity代碼中

				Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->Greeting(name = "Android",modifier = Modifier.padding(innerPadding))}

??這段代碼,就是Scaffold里面設置modifier = Modifier.fillMaxSize()占滿屏幕寬度。

3. 可組合函數

??然后在里面放了一個Greeting組件,它是一個文本,由于我們并未設置什么屬性,所以這個組件會出現在左上角,注意到我們的組件上方有一個@Composable注解,表示這是一個可組合函數,有這個注解的才能被我們的。它里面還調用了Text(),它也是一個可組合函數,只不過功能更多。

在這里插入圖片描述

??我們可以類比為常規項目里面的TextView,在Compose中這些組件會更簡單,說了這么多,我們來實際操作一下,比如我們先運行一下,看看是不是出現在左上角。

在這里插入圖片描述

三、創建組件頁面

??在Compose中使用Toast有點不一樣,我們在MainActivity中,增加一個rememberToast()函數,代碼如下所示:

@Composable
fun rememberToast(): (String) -> Unit {val context = LocalContext.currentreturn { message ->Toast.makeText(context, message, Toast.LENGTH_SHORT).show()}
}

??我們來寫一個登錄頁面,在Compose中,這個是比較簡單的。

1. LoginPage

下面我們在MainActivity中增加一個LoginPage函數,這是一個可組合函數,代碼如下所示:

@Composable
fun LoginPage(navController: NavController) {val username = remember { mutableStateOf("") }val pwd = remember { mutableStateOf("") }val showToast = rememberToast()Scaffold(modifier = Modifier.fillMaxSize() ) { innerPadding ->Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center,modifier = Modifier.padding(innerPadding).padding(16.dp).fillMaxSize() ) {Box(modifier = Modifier.size(120.dp)) {Image(painter = painterResource(id = R.drawable.ic_launcher_background),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())Image(painter = painterResource(id = R.drawable.ic_launcher_foreground),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())}OutlinedTextField(value = username.value,onValueChange = { username.value = it },label = { Text("賬號") },modifier = Modifier.padding(top = 32.dp).fillMaxWidth())OutlinedTextField(value = pwd.value,onValueChange = { pwd.value = it },label = { Text("密碼") },visualTransformation = PasswordVisualTransformation(),modifier = Modifier.padding(top = 16.dp).fillMaxWidth())Button(onClick = {},shape = RoundedCornerShape(16.dp), // 設置圓角半徑modifier = Modifier.padding(top = 32.dp).fillMaxWidth().height(48.dp)) {Text("登錄")}TextButton(onClick = {},shape = RoundedCornerShape(16.dp),modifier = Modifier.padding(top = 16.dp).fillMaxWidth().height(48.dp)) {Text("注冊")}}}
}

我們先來說一下上述的代碼,最外側我們寫了一個可觀察變量。

    val username = remember { mutableStateOf("") }val pwd = remember { mutableStateOf("") }

使用remembermutableStateOf創建可觀察的狀態變量,就類似于我們在輸入框里面輸入內容,這個值會變化,然后我們通過這個狀態變量獲取里面的值即可。

然后我們再往下,Column是一個常用的布局組件,用于垂直排列子元素,Row用于橫向排列子元素,里面設置了

	horizontalAlignment = Alignment.CenterHorizontally, // 子元素水平居中verticalArrangement = Arrangement.Center, // 子元素垂直居中

??然后放了一個Box,是一個容器,里面的組件默認會堆疊在 Box 的左上角,現在我們設置了Box的大小,而里面的圖標又設置為填充父容器,所以就是占滿的,就形成了圖標的背景和前景。

??下面就是OutlinedTextField了,輸入框,這個輸入框也是輸入時提示文字會向上浮動,這里要注意onValueChange函數,里面的值就是it,然后我們賦值給username.value,默認的值value也是username.value,那么當輸入框的內容改變時username.value的值也就變了,同時它還是一個觀察的狀態變量。

2. RegisterPage

??最后就是兩個按鈕了,這個就很好理解了,Button里面添加Text作為文本顯示,到這里未知,頁面組件就寫好了,下面我們寫注冊頁面和主頁面。

@Composable
fun RegisterPage() {val username = remember { mutableStateOf("") }val pwd = remember { mutableStateOf("") }val pwdAgain = remember { mutableStateOf("") }Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center,modifier = modifier.padding(16.dp).fillMaxSize() ) {Box(modifier = Modifier.size(120.dp)) {Image(painter = painterResource(id = R.drawable.ic_launcher_background),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())Image(painter = painterResource(id = R.drawable.ic_launcher_foreground),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())}OutlinedTextField(value = username.value,onValueChange = { username.value = it },label = { Text("賬號") },modifier = Modifier.padding(top = 32.dp).fillMaxWidth())OutlinedTextField(value = pwd.value,onValueChange = { pwd.value = it },label = { Text("密碼") },visualTransformation = PasswordVisualTransformation(),modifier = Modifier.padding(top = 16.dp).fillMaxWidth())OutlinedTextField(value = pwdAgain.value,onValueChange = { pwdAgain.value = it },label = { Text("再次輸入密碼") },visualTransformation = PasswordVisualTransformation(),modifier = Modifier.padding(top = 16.dp).fillMaxWidth())Button(onClick = {  },shape = RoundedCornerShape(16.dp), // 設置圓角半徑modifier = Modifier.padding(top = 32.dp).fillMaxWidth().height(48.dp)) {Text("注冊")}TextButton(onClick = {  },shape = RoundedCornerShape(16.dp),modifier = Modifier.padding(top = 16.dp).fillMaxWidth().height(48.dp)) {Text("已有賬號,去登錄")}}
}

3. MainPage

@Composable
fun MainPage(modifier: Modifier = Modifier) {Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center,modifier = modifier.fillMaxSize() ) {Text("用戶名:${SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME)} \n " +"密碼:${SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD)}")}
}

上面的代碼其實我都寫在MainActivity.kt中,稍后我們會分開來。

四、導航

??下面我們就要考慮頁面跳轉的事情了,這里就需要用到導航了。

1. 添加依賴

我們需要先在app模塊的build.gradle.kt文件中的dependencies{}中增加如下代碼:

	implementation("androidx.navigation:navigation-compose:2.7.7")

在這里插入圖片描述
添加后記得要點擊Sync Now

然后你會發現我們添加的依賴和上面其他的依賴不一樣,你可以把鼠標放上去,會看到一個提示彈窗,可以點擊這行藍色的文字。

在這里插入圖片描述

就會變成這樣了。

在這里插入圖片描述

然后我們在libs.version.toml文件中就能找到它。

在這里插入圖片描述

2. 使用導航

下面我們回到MainActivity.kt中,找到onCreate()函數,修改里面的代碼,如下所示:

    override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {AndroidUIComposeTheme {val navController = rememberNavController()NavHost(navController = navController, startDestination = "login") {composable("login") { LoginPage(navController) } // 登錄頁面composable("register") { RegisterPage(navController) } // 注冊頁面composable("main") { MainPage() } // 主頁面}}}}

??可以看到我們使用了rememberNavController(),這是一個導航的控制器,記得要導包,如果不清楚什么是導包可以評論區留言或者私信我,我再說明一下。然后我們使用了一個NavHost()用來裝載我們需要導航的頁面,startDestination = "login",表示第一個顯示的頁面是登錄頁面,這里我們用了一個字符串,這個其實我們可以寫一個常量的類去保存這些不變的字符串。然后我們在LoginPage和RegisterPage的函數中分別傳遞了navController,因為這兩個頁面是需要導航了,因此就傳進去,然他們導航,當然這只是推薦的方式,后續我們會使用別的更好的方式,這里就先這么用著。

在這里插入圖片描述

??上述代碼不出意外會報錯,我們可以在對應的函數中增加對應參數,這里RegisterPage也要記得增加,然后就不報錯了,下面,我們先寫一個常量類,在com.example.android_ui_compose包下新建一個utils包,包下創建一個EasyConstants類,里面的代碼如下所示:

package com.example.android_ui_compose.utilsobject EasyConstants {const val PAGE_LOGIN = "page_login"const val PAGE_REGISTER = "page_register"const val PAGE_MAIN = "page_main"const val KEY_USERNAME = "username"const val KEY_PASSWORD = "password"
}

這里我們已經知道接下來要做什么了,所以我們就提前準備好需要的常量,這里是三個頁面和賬號密碼的常量Key,下面在utils包再創建一個SPUtils類,代碼如下所示:

package com.example.android_ui_compose.utils
import android.content.Context
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KPropertyclass SPUtils private constructor(context: Context, name: String = DEFAULT_SP_NAME) {companion object {const val DEFAULT_SP_NAME = "sp_config"private var instance: SPUtils? = null@Synchronizedfun initialize(context: Context, name: String = DEFAULT_SP_NAME): SPUtils {return instance ?: SPUtils(context.applicationContext, name).also { instance = it }}fun getInstance(): SPUtils {return instance ?: throw IllegalStateException("SPUtils not initialized. Call initialize() first.")}}private val sharedPref: SharedPreferences by lazy {context.getSharedPreferences(name, Context.MODE_PRIVATE)}// 基本操作fun putString(key: String, value: String) = sharedPref.edit().putString(key, value).apply()fun getString(key: String, default: String = "") = sharedPref.getString(key, default) ?: defaultfun putInt(key: String, value: Int) = sharedPref.edit().putInt(key, value).apply()fun getInt(key: String, default: Int = 0) = sharedPref.getInt(key, default)fun putLong(key: String, value: Long) = sharedPref.edit().putLong(key, value).apply()fun getLong(key: String, default: Long = 0L) = sharedPref.getLong(key, default)fun putFloat(key: String, value: Float) = sharedPref.edit().putFloat(key, value).apply()fun getFloat(key: String, default: Float = 0f) = sharedPref.getFloat(key, default)fun putBoolean(key: String, value: Boolean) = sharedPref.edit().putBoolean(key, value).apply()fun getBoolean(key: String, default: Boolean = false) = sharedPref.getBoolean(key, default)fun putStringSet(key: String, value: Set<String>) = sharedPref.edit().putStringSet(key, value).apply()fun getStringSet(key: String, default: Set<String> = emptySet()) = sharedPref.getStringSet(key, default) ?: default// 其他操作fun contains(key: String) = sharedPref.contains(key)fun remove(key: String) = sharedPref.edit().remove(key).apply()fun clear() = sharedPref.edit().clear().apply()fun getAll() = sharedPref.all// 使用委托屬性簡化訪問fun stringPref(key: String, default: String = "") =object : ReadWriteProperty<Any, String> {override fun getValue(thisRef: Any, property: KProperty<*>) = getString(key, default)override fun setValue(thisRef: Any, property: KProperty<*>, value: String) = putString(key, value)}fun intPref(key: String, default: Int = 0) =object : ReadWriteProperty<Any, Int> {override fun getValue(thisRef: Any, property: KProperty<*>) = getInt(key, default)override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) = putInt(key, value)}fun booleanPref(key: String, default: Boolean = false) =object : ReadWriteProperty<Any, Boolean> {override fun getValue(thisRef: Any, property: KProperty<*>) = getBoolean(key, default)override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) = putBoolean(key, value)}
}// 擴展函數簡化使用
fun Context.sp(name: String = SPUtils.DEFAULT_SP_NAME) = SPUtils.initialize(this, name)

現在我們再回到MainActivity,先將onCreate中的字符串改成常量值

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {AndroidUIComposeTheme {val navController = rememberNavController()NavHost(navController = navController, startDestination = PAGE_LOGIN) {composable(PAGE_LOGIN) { LoginPage(navController) } // 登錄頁面composable(PAGE_REGISTER) { RegisterPage(navController) } // 注冊頁面composable(PAGE_MAIN) { MainPage() } // 主頁面}}}}

報錯的地方記得要導包喲~

然后修改LoginPage函數中的代碼,登錄按鈕的點擊事件:

Button(onClick = {if (username.value.isEmpty()) {showToast("請輸入用戶名")return@Button}if (pwd.value.isEmpty()) {showToast("請輸入密碼")return@Button}// 校驗賬號密碼if (username.value == SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME) &&pwd.value == SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD)) {showToast("登錄成功")// 進入主界面navController.navigate(PAGE_MAIN) {// 指定要彈出到哪個頁面(這里用當前頁面)popUpTo(navController.currentBackStackEntry?.destination?.route ?: return@navigate) {inclusive = true // 表示彈出的范圍包含當前頁面(即關閉當前頁)}}}},shape = RoundedCornerShape(16.dp), // 設置圓角半徑modifier = Modifier.padding(top = 32.dp).fillMaxWidth().height(48.dp)) {Text("登錄")}

這里的邏輯其實和我之前寫那個常規項目是一樣的,就是跳轉頁面的方式不一樣,通過navController去進行導航到目標頁面,這里登錄成功我們就導航到主頁面,注冊按鈕的點擊代碼:

            TextButton(onClick = {navController.navigate(PAGE_REGISTER)},shape = RoundedCornerShape(16.dp),modifier = Modifier.padding(top = 16.dp).fillMaxWidth().height(48.dp)) {Text("注冊")}

注冊我們就直接導航到注冊頁面。

下面再進入RegisterPage函數中的代碼,注冊按鈕的點擊事件:

Button(onClick = {if (username.value.isEmpty()) {showToast("請輸入用戶名")return@Button}if (pwd.value.isEmpty()) {showToast("請輸入密碼")return@Button}if (pwdAgain.value.isEmpty()) {showToast("請再次輸入密碼")return@Button}// 檢查兩次密碼是否一致if (pwd.value != pwdAgain.value) {showToast("兩次密碼不一致")return@Button}// 注冊賬號,保存賬號到SP,進行本地數據持久化,如果清除緩存則會消失SPUtils.getInstance().putString(EasyConstants.KEY_USERNAME, username.value)SPUtils.getInstance().putString(EasyConstants.KEY_PASSWORD, pwd.value)showToast("注冊成功,稍后進入登錄頁面進行登錄")// 一秒后返回登錄頁面CoroutineScope(Dispatchers.Main).launch {delay(1000)navController.popBackStack()}},shape = RoundedCornerShape(16.dp), // 設置圓角半徑modifier = Modifier.padding(top = 32.dp).fillMaxWidth().height(48.dp)) {Text("注冊")}

這里登錄注冊的代碼就都寫好了,其實主頁面之前就寫好了。

下面我們還需要配置一下SPUtils的初始化。

3. 初始化

??工具類方法中有一個initialize()方法,我們在使用之前要先初始化,否則會報錯,初始化我們可以在程序執行的時候進行初始化,我們在com.example.android_ui_compose下創建一個MyApplication類,里面的代碼如下:

package com.example.android_ui_composeimport android.app.Application
import com.example.android_ui_compose.utils.SPUtilsclass MyApplication : Application() {override fun onCreate() {super.onCreate()SPUtils.initialize(this)}
}

??這里需要說一下它的作用,這里繼承 Android 的 Application 類,用于全局應用級別的初始化,可以在此類中初始化全局工具(如數據庫、網絡庫、SharedPreferences 工具等),可以維護應用級別的狀態或變量,比在 Activity 中初始化更高效,因為 Application 只會創建一次。

4. AndroidManifest.xml配置

??在上面我們寫好了這個類,為了使這個類生效,我們需要在AndroidManifest.xml中配置配置它,很簡單,配置如下圖所示:

在這里插入圖片描述

雖然很簡單,但是很容易會被忘記,所以你要記得呀,然后就可以運行了。

在這里插入圖片描述

五、代碼分離

好了,下面我們在ui包下創建一個page包,page包創建LoginPage,里面代碼如下:

package com.example.android_ui_compose.ui.pageimport androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.example.android_ui_compose.R
import com.example.android_ui_compose.rememberToast
import com.example.android_ui_compose.utils.EasyConstants
import com.example.android_ui_compose.utils.EasyConstants.PAGE_MAIN
import com.example.android_ui_compose.utils.EasyConstants.PAGE_REGISTER
import com.example.android_ui_compose.utils.SPUtils@Composable
fun LoginPage(navController: NavController) {val username = remember { mutableStateOf("") }val pwd = remember { mutableStateOf("") }val showToast = rememberToast()Scaffold(modifier = Modifier.fillMaxSize() ) { innerPadding ->Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center,modifier = Modifier.padding(innerPadding).padding(16.dp).fillMaxSize() ) {Box(modifier = Modifier.size(120.dp)) {Image(painter = painterResource(id = R.drawable.ic_launcher_background),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())Image(painter = painterResource(id = R.drawable.ic_launcher_foreground),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())}OutlinedTextField(value = username.value,onValueChange = { username.value = it },label = { Text("賬號") },modifier = Modifier.padding(top = 32.dp).fillMaxWidth())OutlinedTextField(value = pwd.value,onValueChange = { pwd.value = it },label = { Text("密碼") },visualTransformation = PasswordVisualTransformation(),modifier = Modifier.padding(top = 16.dp).fillMaxWidth())Button(onClick = {if (username.value.isEmpty()) {showToast("請輸入用戶名")return@Button}if (pwd.value.isEmpty()) {showToast("請輸入密碼")return@Button}// 校驗賬號密碼if (username.value == SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME) &&pwd.value == SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD)) {showToast("登錄成功")// 進入主界面navController.navigate(PAGE_MAIN) {// 指定要彈出到哪個頁面(這里用當前頁面)popUpTo(navController.currentBackStackEntry?.destination?.route ?: return@navigate) {inclusive = true // 表示彈出的范圍包含當前頁面(即關閉當前頁)}}}},shape = RoundedCornerShape(16.dp), // 設置圓角半徑modifier = Modifier.padding(top = 32.dp).fillMaxWidth().height(48.dp)) {Text("登錄")}TextButton(onClick = {navController.navigate(PAGE_REGISTER)},shape = RoundedCornerShape(16.dp),modifier = Modifier.padding(top = 16.dp).fillMaxWidth().height(48.dp)) {Text("注冊")}}}
}

page包下創建RegisterPage類,里面代碼如下所示:

package com.example.android_ui_compose.ui.pageimport androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.example.android_ui_compose.R
import com.example.android_ui_compose.rememberToast
import com.example.android_ui_compose.utils.EasyConstants
import com.example.android_ui_compose.utils.SPUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch@Composable
fun RegisterPage(navController: NavController) {val username = remember { mutableStateOf("") }val pwd = remember { mutableStateOf("") }val pwdAgain = remember { mutableStateOf("") }val showToast = rememberToast()Scaffold(modifier = Modifier.fillMaxSize() ) { innerPadding ->Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center,modifier = Modifier.padding(innerPadding).padding(16.dp).fillMaxSize() ) {Box(modifier = Modifier.size(120.dp)) {Image(painter = painterResource(id = R.drawable.ic_launcher_background),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())Image(painter = painterResource(id = R.drawable.ic_launcher_foreground),contentDescription = "App Icon",modifier = Modifier.fillMaxSize())}OutlinedTextField(value = username.value,onValueChange = { username.value = it },label = { Text("賬號") },modifier = Modifier.padding(top = 32.dp).fillMaxWidth())OutlinedTextField(value = pwd.value,onValueChange = { pwd.value = it },label = { Text("密碼") },visualTransformation = PasswordVisualTransformation(),modifier = Modifier.padding(top = 16.dp).fillMaxWidth())OutlinedTextField(value = pwdAgain.value,onValueChange = { pwdAgain.value = it },label = { Text("再次輸入密碼") },visualTransformation = PasswordVisualTransformation(),modifier = Modifier.padding(top = 16.dp).fillMaxWidth())Button(onClick = {if (username.value.isEmpty()) {showToast("請輸入用戶名")return@Button}if (pwd.value.isEmpty()) {showToast("請輸入密碼")return@Button}if (pwdAgain.value.isEmpty()) {showToast("請再次輸入密碼")return@Button}// 檢查兩次密碼是否一致if (pwd.value != pwdAgain.value) {showToast("兩次密碼不一致")return@Button}// 注冊賬號,保存賬號到SP,進行本地數據持久化,如果清除緩存則會消失SPUtils.getInstance().putString(EasyConstants.KEY_USERNAME, username.value)SPUtils.getInstance().putString(EasyConstants.KEY_PASSWORD, pwd.value)showToast("注冊成功,稍后進入登錄頁面進行登錄")// 一秒后返回登錄頁面CoroutineScope(Dispatchers.Main).launch {delay(1000)navController.popBackStack()}},shape = RoundedCornerShape(16.dp), // 設置圓角半徑modifier = Modifier.padding(top = 32.dp).fillMaxWidth().height(48.dp)) {Text("注冊")}TextButton(onClick = { navController.popBackStack() },shape = RoundedCornerShape(16.dp),modifier = Modifier.padding(top = 16.dp).fillMaxWidth().height(48.dp)) {Text("已有賬號,去登錄")}}}
}

再創建一個MainPage類,代碼如下所示:

package com.example.android_ui_compose.ui.pageimport androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.example.android_ui_compose.utils.EasyConstants
import com.example.android_ui_compose.utils.SPUtils@Composable
fun MainPage() {Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center,modifier = Modifier.fillMaxSize() ) {Text("用戶名:${SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME)} \n " +"密碼:${SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD)}")}
}

最后我們再整理一下MainActivity中的代碼,如下所示:

package com.example.android_ui_composeimport android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.android_ui_compose.ui.page.LoginPage
import com.example.android_ui_compose.ui.page.MainPage
import com.example.android_ui_compose.ui.page.RegisterPage
import com.example.android_ui_compose.ui.theme.AndroidUIComposeTheme
import com.example.android_ui_compose.utils.EasyConstants.PAGE_LOGIN
import com.example.android_ui_compose.utils.EasyConstants.PAGE_MAIN
import com.example.android_ui_compose.utils.EasyConstants.PAGE_REGISTERclass MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {AndroidUIComposeTheme {val navController = rememberNavController()NavHost(navController = navController, startDestination = PAGE_LOGIN) {composable(PAGE_LOGIN) { LoginPage(navController) } // 登錄頁面composable(PAGE_REGISTER) { RegisterPage(navController) } // 注冊頁面composable(PAGE_MAIN) { MainPage() } // 主頁面}}}}
}@Composable
fun rememberToast(): (String) -> Unit {val context = LocalContext.currentreturn { message ->Toast.makeText(context, message, Toast.LENGTH_SHORT).show()}
}

好了,現在我們來看看項目的結構。

在這里插入圖片描述

你是否跟我一樣呢?上面代碼分離之后可以再運行一下試試看,看能夠運行起來嗎,可以就沒有問題,不可以就要再看看是不是哪里弄錯了。

六、源碼

GitHub源碼地址: Android-UI-Compose

好的,后會有期~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/93099.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/93099.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/93099.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

分享10個ai生成ppt網站(附ai生成ppt入口)

實測對比&#xff1a;15頁PPT從3小時壓縮到3分鐘的秘密武器 當ChatGPT能寫方案、Midjourney能畫圖&#xff0c;做PPT還在手動排版就OUT了&#xff01;這些AI生成PPT網站已實現「輸入文案秒出設計稿」&#xff0c;無論職場匯報、畢業答辯還是路演融資&#xff0c;零設計基礎也能…

最強開源視頻模型通義萬相wan2.1在comfyui中的安裝應用詳解

摘要&#xff1a;阿里巴巴開源通義萬相Wan2.1模型&#xff0c;支持文生視頻、圖生視頻等多種功能&#xff0c;并整合關鍵環節簡化創作流程。官方和Kiji版本需配套使用各自工作流。低顯存顯卡可使用GGUF模型解決方案&#xff0c;最低適配4G顯存。ComfyUI已原生支持該模型&#x…

機器學習:基于OpenCV和Python的智能圖像處理 實戰

機器學習&#xff1a;基于OpenCV和Python的智能圖像處理實戰——待填坑圖像處理基礎圖像的基本表示方法圖像處理的基本操作圖像運算圖像的色彩空間轉換圖像幾何變換4.1 仿射變換4.2 重映射4.3 投影變換 4.4 極坐標變換5 圖像直方圖處理7 圖像閾值處理8 圖像形態學處理github地址…

proteus實現簡易DS18B20溫度計(stm32)

一、新建proteus工程 具體看前面文章 二、搭建電路 需要配置供電網絡以及尋找元器件&#xff0c;細節看前面文章&#xff0c;下面給出電路圖 電路包含了五個部分&#xff1a; 1、DS18B20&#xff1a;數據引腳記得上拉 2、stm32電路 3、串口電路&#xff08;右下角那個器件…

Autoppt-AI驅動的演示文稿生成工具

本文轉載自&#xff1a;Autoppt-AI驅動的演示文稿生成工具 - Hello123工具導航 ** 一、 Autoppt&#xff1a;AI 驅動的智能演示文稿生成工具 Autoppt 是一款基于人工智能的在線演示文稿生成平臺&#xff0c;通過輸入主題或上傳文檔&#xff08;Word/PDF/ 圖片等&#xff09;&…

Flink on YARN啟動全流程深度解析

Flink on YARN 模式啟動流程及核心組件協作詳解整個過程分為三個主要階段&#xff1a;??JobManager 啟動??&#xff08;作業提交與 AM 初始化&#xff09;??TaskManager 資源分配與啟動????任務部署與執行??第一階段&#xff1a;作業提交與 JobManager (AM) 啟動?…

安卓開發者自學鴻蒙開發1基礎入門

1.基礎 聲明式UI&#xff1a;?? ??核心&#xff1a;?? 你??聲明??你想要UI是什么樣子&#xff08;在build()方法里描述&#xff09;&#xff0c;而不是一步步命令式地創建和操作View對象&#xff08;findViewById, setText, setOnClickListener&#xff09;。 模塊化…

彈性擴展新范式:分布式LLM計算的FastMCP解決方案

本文較長&#xff0c;建議點贊收藏&#xff0c;以免遺失。更多AI大模型應用開發學習視頻及資料&#xff0c;盡在聚客AI學院。如果你想系統學習AI大模型應用開發&#xff0c;挑戰AI高薪崗位&#xff0c;可在文章底部聯系。在現代大語言模型&#xff08;LLM&#xff09;應用架構中…

springboot項目不同平臺項目通過http接口AES加密傳輸

前言&#xff1a; 在公司協作開發的過程中&#xff0c;自己的項目是公共調用平臺&#xff0c;也可以說是中轉平臺&#xff0c;供公司其他團隊的項目進行接口調用。因為是不同團隊項目之間的相互調用&#xff0c;所以不能通過openFeign遠程調用。只能通過http遠程調用&#xff…

推薦5個網頁模板資源網

1. 企業模板官方網站&#xff1a; http://www.qimoban.com介紹&#xff1a;企業模板(qimoban.com )是一個專注于提供豐富多樣的企業模板的優質平臺&#xff0c;致力于為企業和個人打造高效、專業、個性化的模板獲取渠道。該平臺提供海量的企業模板資源&#xff0c;涵蓋企業官網…

Redis持久化機制(RDB AOF)

1. RDB RDB 持久化是把當前進程數據生成快照保存到硬盤的過程&#xff0c;觸發 RDB 持久化過程分為手動觸發和 自動觸發&#xff0c;存儲的是二進制數據。 1.1 手動觸發 使用 save 和 bgsave 命令觸發&#xff1a; save&#xff1a;Redis服務主進程阻塞式執行持久化操作&…

【css】讓瀏覽器支持小于12px的文字

【css】讓瀏覽器支持小于12px的文字.demo {display: inline-block;/** 使用Webkit引擎的變換屬性&#xff08;主要針對舊版Safari/Chrome&#xff09; **/-webkit-transform: scale(0.8); }注意&#xff1a;display: inline-block; 一定要加上&#xff01;1.transform: scale(…

機器學習-基礎入門:從概念到核心方法論

在人工智能飛速發展的今天&#xff0c;機器學習作為其核心技術&#xff0c;正深刻改變著我們的生活與工作。從 AlphaGo 戰勝圍棋世界冠軍&#xff0c;到日常的智能推薦、人臉識別&#xff0c;機器學習的應用無處不在。本文將從基礎概念出發&#xff0c;帶你系統了解機器學習的核…

《Leetcode》-面試題-hot100-動態規劃

題目列表 70. 爬樓梯 簡單難度 leetcode鏈接 118. 楊輝三角 簡單難度 leetcode鏈接 198. 打家劫舍 中等難度 leetcode鏈接 279.完全平方數 中等難度 leetcode鏈接 322.零錢兌換 中等難度 leetcode鏈接 139.單詞拆分 中等難度 leetcode鏈接 300.最長遞增子序列 中等難度 l…

數巔中標中建科技AI知識庫項目,開啟建筑業數智化新篇章

AI正以前所未有的迅猛態勢滲透進建筑業的每一處脈絡。在這場數智化轉型浪潮中&#xff0c;AI技術如何與建筑業基因深度融合&#xff1f;如何充分釋放數據價值&#xff1f;近日&#xff0c;數巔成功中標中建科技集團有限公司“企業AI知識庫研發”項目&#xff0c;這一“大語言模…

想要PDF翻譯保留格式?用對工具是關鍵

嘿&#xff0c;朋友&#xff01;最近有沒有被PDF翻譯的事兒搞得焦頭爛額呀&#xff1f;尤其是碰到韓文PDF文件的時候&#xff0c;是不是更頭疼了&#xff1f;別擔心&#xff0c;我最近也遇到了類似的問題&#xff0c;試了不少軟件&#xff0c;發現有五款軟件在處理韓文PDF翻譯時…

【MySQL?】服務器安裝 MySQL 及配置相關操作

1. 安裝 MySQL 在安裝 MySQL 時&#xff0c;如果使用官方 RPM 源&#xff0c;會遇到 GPG 密鑰驗證失敗的錯誤&#xff0c;可以按照以下步驟解決&#xff1a; 解決 GPG 密鑰驗證失敗的問題下載 MySQL 官方 GPG 密鑰 使用以下命令下載并安裝 MySQL 的官方 GPG 密鑰&#xff1a; w…

大數據量返回方案(非分頁)

一、普通方式返回100萬條數據RestController RequestMapping("/bad") public class BadController {Autowiredprivate UserRepository userRepository;/*** 危險&#xff01;一次性加載 100 萬條到內存*/GetMapping("/all-users")public List<User> …

基于Casbin的微服務細粒度權限控制方案對比與實踐

基于Casbin的微服務細粒度權限控制方案對比與實踐 隨著微服務架構在互聯網和企業級應用中的廣泛應用&#xff0c;服務間的安全邊界愈發重要。傳統的集中式權限控制方式已難以滿足微服務的高并發、動態擴展和多語言支持等需求。本文將從主流的三種微服務權限控制方案入手&#x…

5G毫米波現狀概述(截止2025 年7月)

5G毫米波現狀概述(截止2025 年7月&#xff09; 原創 modem協議筆記 2025年07月25日 06:01 廣東 聽全文 當你在體育館看球賽時&#xff0c;想發段實時視頻到朋友圈卻總卡成PPT&#xff1b;當郊區的父母抱怨“光纖拉不到家&#xff0c;網速比蝸牛慢”—這些場景背后&#xff…