UI - 主页面 - Compose
前言
在上一篇文章中,我们使用Compose写好了登录注册页面,在注册账号,登录后进入了主页面,本篇文章中,我们继续往下去写。
正文
在登录之后,我们进入主页面,但是当我们再运行时,发现还是进入的登录页面,按照逻辑来说我们应该保存一个值用来判断是否进入登录页面还是主页面,本篇文章中,我们先做这个逻辑,再写主页面的代码。
一、生成登录Token
我们需要一个凭证用来判断是否要进入主页面,当我们登录之后可以使用账号和密码生成一个Token字符串,这在实际开发中也是比较常用的,只不过实际开发中的Token字符串更加复杂,会加入时间戳、随机数、再进行一次不对称加密。我们在这里就简单一些,只通过账号和密码进行生成一个Token字符串即可。
这里的Token也是需要一个Key的,我们在EasyConstants中增加两个常量,表示Token和Token过期时间:
// 缓存的token
const val KEY_TOKEN = "key_token"
const val KEY_TOKEN_EXPIRE = "key_token_expire"
然后我们在utils下创建EasyUtils类,里面的代码如下:
package com.example.android_ui_compose.utils
import java.security.MessageDigest
/**
* 封装一些常用的工具方法
*/
object EasyUtils {
/**
* 存储Token信息
* @param token Token
* @param expireTime 过期时间
*/
data class TokenInfo(
val token: String,
val expireTime: Long
)
/**
* 带有过期时间的Token生成方法
* @param username 用户名
* @param password 密码
* @param expireInMinutes 过期时间(分钟)
* @return 包含过期时间的Token信息
*/
fun generateTokenWithExpire(username: String, password: String, expireInMinutes: Int = 30): TokenInfo {
val currentTime = System.currentTimeMillis()
val expireTime = currentTime + (expireInMinutes * 60 * 1000)
val rawToken = "$username:$password:$currentTime:$expireTime"
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(rawToken.toByteArray())
val token = digest.joinToString("") { "%02x".format(it) }
// 缓存Token
SPUtils.getInstance().putString(EasyConstants.KEY_TOKEN, "Bearer_$token")
SPUtils.getInstance().putLong(EasyConstants.KEY_TOKEN_EXPIRE, expireTime)
return TokenInfo("Bearer_$token", expireTime)
}
fun hasCachedUserData(): Boolean {
val token = SPUtils.getInstance().getString(EasyConstants.KEY_TOKEN)
val expireTime = SPUtils.getInstance().getLong(EasyConstants.KEY_TOKEN_EXPIRE, 0)
// 检查Token是否存在且未过期
return token.isNotEmpty() && System.currentTimeMillis() < expireTime
}
}
通过上述代码的方法注释你可以很清楚的之后具体的功能是什么,当我们生成Token之后就将Token数据进行一次保存,在hasCachedUserData方法中获取判断是否过期。
二、使用Token
下面就是使用的地方了,首先我们需要在MainActivity
中的onCreate
方法中修改进入的页面,代码如下所示:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AndroidUIComposeTheme {
val navController = rememberNavController()
val startDestination = remember {
// 检查是否有缓存数据
if (EasyUtils.hasCachedUserData()) PAGE_MAIN else PAGE_LOGIN
}
NavHost(navController = navController, startDestination = startDestination) {
composable(PAGE_LOGIN) { LoginPage(navController) } // 登录页面
composable(PAGE_REGISTER) { RegisterPage(navController) } // 注册页面
composable(PAGE_MAIN) { MainPage() } // 主页面
}
}
}
}
如果写了之后有报错,记得导包,然后进入LoginPage
,在这里我们需要在登录成功时跳转主页面之前去生成Token,就一行代码:
EasyUtils.generateTokenWithExpire(username.value, pwd.value)
同时我们再完善一下之前的代码逻辑:
if (SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME).isEmpty() || SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD).isEmpty()) {
showToast("当前账号未注册,请先注册")
return@Button
}
然后在登录账号密码输入错误的时候提示一下:
showToast("账号或密码错误")
添加位置如下图所示:
下面你就可以重新运行一下看看,输入账号登录进入主页面,然后杀死程序再打开,看看进入的是主页面还是登录页面。如果是登录页面,那就有问题。
三、Navigation
现在我们需要来写主页面,这里我们先要使用Navigation
,我们可以使用Navigation来构建页面的底部导航,首先我们定义主页面有三个功能模块,在EasyConstants中增加如下代码:
// 屏幕名称
const val SCREEN_COMMON = "Common"
const val SCREEN_RANDOM = "Random"
const val SCREEN_DIY = "DIY"
然后我们写一个类定义屏幕,在utils包下新建一个Screen类,代码如下所示:
// 定义屏幕枚举
sealed class Screen {
data object Common : Screen()
data object Random : Screen()
data object DIY : Screen()
}
这个代码就很简单,定义了三个屏幕,然后我们再写一个类用来定义底部的导航类,在utils包下仙剑一个BottomNavItem类,代码如下所示:
import androidx.compose.ui.graphics.vector.ImageVector
// 定义底部导航项数据类
data class BottomNavItem(
val title: String,
val icon: ImageVector,
val screen: Screen
)
这里定义出来屏幕之外还是导航按钮的名称和图标。
四、功能页面
我们先不急着写主页面,我们先把功能页面写出来,在page包下新建一个screen包,在screen包下创建CommonScreen类,里面的代码如下所示:
package com.example.android_ui_compose.ui.page.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun CommonScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "常用功能页面",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "这里是常用功能的内容区域",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
}
这里的代码非常简单,就是常见的纵向排列居中,简单说明一下当前页面是用来干嘛的,那么我们再依次在screen包创建RandomScreen类,代码如下所示:
package com.example.android_ui_compose.ui.page.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun RandomScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "随机功能页面",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "这里是随机功能的内容区域",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
}
最后在screen
包创建DIYScreen
类,代码如下所示:
package com.example.android_ui_compose.ui.page.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun DIYScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Build,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "自定义功能页面",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "这里是DIY功能的内容区域",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
}
三个功能页面的代码大相径庭。
五、主页面
前置的工作都做好了,现在就到了最重要的环节了,那就是主页面,修改主页面MainPage
的代码,如下所示:
package com.example.android_ui_compose.ui.page
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.example.android_ui_compose.ui.page.screen.CommonScreen
import com.example.android_ui_compose.ui.page.screen.DIYScreen
import com.example.android_ui_compose.ui.page.screen.RandomScreen
import com.example.android_ui_compose.utils.BottomNavItem
import com.example.android_ui_compose.utils.EasyConstants.SCREEN_COMMON
import com.example.android_ui_compose.utils.EasyConstants.SCREEN_DIY
import com.example.android_ui_compose.utils.EasyConstants.SCREEN_RANDOM
import com.example.android_ui_compose.utils.Screen
/**
* 主页面可组合函数
*
* 该函数实现了带有底部导航栏的应用主界面,支持在不同屏幕间切换
* 包含三个主要功能页面:通用页面、随机页面和自定义页面
*/
@Composable
fun MainPage() {
var currentScreen by remember { mutableStateOf<Screen>(Screen.Common) }
// 底部导航项列表
val bottomNavItems = listOf(
BottomNavItem(SCREEN_COMMON, Icons.Default.Menu, Screen.Common),
BottomNavItem(SCREEN_RANDOM, Icons.Default.Add, Screen.Random),
BottomNavItem(SCREEN_DIY, Icons.Default.Build, Screen.DIY)
)
Scaffold(
// 构建带底部导航栏的页面结构
bottomBar = {
NavigationBar {
bottomNavItems.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.title) },
label = { Text(item.title) },
selected = currentScreen == item.screen,
onClick = { currentScreen = item.screen }
)
}
}
}
) { paddingValues ->
// 内容显示区域,根据当前选中的屏幕显示对应页面
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when(currentScreen) {
is Screen.Common -> CommonScreen()
is Screen.Random -> RandomScreen()
is Screen.DIY -> DIYScreen()
}
}
}
}
下面简单说明一些,首先我们构建了一个底部按钮列表,三个按钮,然后通过Scaffold
的bottomBar
属性去动态生成NavigationBarItem
,通过遍历bottomNavItems
的方式。默认选中的是currentScreen
,也就是Screen.Common
,常用功能。
这里需要注意这一行代码onClick = { currentScreen = item.screen }
,就是当我们点击底部导航按钮时,就会触发,然后给currentScreen 赋值当前的屏幕Item,在底部就会根据currentScreen的变化去改变当前主页面显示的内容,currentScreen记录的是当前屏幕的状态,当状态发生改变,监听的地方就会做出相应的改变,这是Compose中很重要的一个内容,状态驱动UI改变
。
现在代码写完了,让我们来看看运行的效果如何吧。
六、源码
GitHub源码地址: Android-UI-Compose
好的,后会有期~