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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…