Android UI(一)登录注册 - Compose

发布于:2025-08-16 ⋅ 阅读:(17) ⋅ 点赞:(0)

  下面我们将进行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_compose

import 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.AndroidUIComposeTheme

class 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.current
    return { 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.utils

object 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.KProperty

class SPUtils private constructor(context: Context, name: String = DEFAULT_SP_NAME) {

    companion object {
        const val DEFAULT_SP_NAME = "sp_config"
        private var instance: SPUtils? = null

        @Synchronized
        fun 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) ?: default

    fun 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_compose

import android.app.Application
import com.example.android_ui_compose.utils.SPUtils

class 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.page

import 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.page

import 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.page

import 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_compose

import 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_REGISTER

class 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.current
    return { message ->
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}

好了,现在我们来看看项目的结构。

在这里插入图片描述

你是否跟我一样呢?上面代码分离之后可以再运行一下试试看,看能够运行起来吗,可以就没有问题,不可以就要再看看是不是哪里弄错了。

六、源码

GitHub源码地址: Android-UI-Compose

好的,后会有期~


网站公告

今日签到

点亮在社区的每一天
去签到