本文系 Jetpack Compose Beginner Series 一文的翻译,润色期间增加了个人的一些理解。它是我目前读到最适合习惯多年使用xml布局开发者的Jetpack Compose入门文章。
Android 应用程序开发中的声明式编程方法
从2013年甚至更早时候,我们 Android 开发人员多年来一直习惯使用基于 XML
的布局来设计用户界面。现在是时候换个角度,探索这个声明式 UI
工具包了,它有望让我们作为开发人员的生活变得轻松很多。
对于我们这些根深蒂固的XML
使用者来说,Jetpack Compose
的想法可能看起来太复杂 了。但相信我,从代码清晰度、开发速度和维护方面的好处来说是值得的。
ॐ श्री गणेशाय नमः 🙏 // 这是一段梵语(Sanskrit),意为"Om(宇宙之音),向神圣的象头神甘尼许(Ganesha)致以敬礼!"这是对印度教象头神甘尼许的祈祷文,常用于祈求智慧、成功或清除障碍。
今天我们将学习 Jetpack Compose App
的核心概念,并从头开始构建一个应用程序。这将是您开始 Jetpack Compose
之旅的快速指南。
本次要实现的APP效果图,包含登录页面和信息流主页。
入门基础知识
Jetpack Compose 中有很多东西,例如 UI 组件、布局、状态管理、StateFlow、SideEffects。但是当我们刚开始时,我们不必一下子了解所有内容。随着我们习惯使用 Compose 进行设计,传递数据和管理状态等事情将变得更加有意义。所以,我们不要紧张------从基础开始,我们会一步一步弄清楚。
1. 可组合函数
在 Jetpack Compose 中,构建 UI 就像定义函数一样简单。您只需在函数上添加@Composable
注释,就可以了!您已进入 UI 创建的世界。
kotlin
@Composable
fun FancyButton (label: String) {
Button(onClick = { /* 处理按钮点击 */ }) {
Text(text = label)
}
}
为了预览它,就像我们过去编写 XML
并在 Design
选项卡中预览它一样,我们需要添加以下代码来定义在 Preview
选项卡中显示的内容。
kotlin
@Preview (showBackground = true,name = "文本预览")
@Composable
fun YourPreviewName() {
YourAppTheme {
FancyButton(label = "Android")
}
}
2. 撰写布局
在 Jetpack Compose
中,界面元素形成一个层次结构,通过调用彼此内部的composable
函数进行排列。可以将它们视为容纳视图或其他布局的不可见容器。
2.1 Column竖向布局
如果您喜欢垂直排列,Column
是一个不错的选择。它将您的视图堆叠在一起,形成垂直层叠。
类似于 LinearLayout --- orientation = "vertical"
。
kotlin
@Composable
fun UserCard(user: User) {
Column {
Text(text = user.firstName)
Text(text = user.email)
}
}
2.2 Row横向布局
此布局适合水平排列视图。将元素并排排列。
类似于 LinearLayout --- orientation = "horizontal"
。
kotlin
@Composable
fun UserCard(user: User) {
Row {
Text(text = user.firstName)
Text(text = user.lastName)
}
}
2.3 Scaffold脚手架
Scaffold
是一个 Compose 函数
,用于根据 Material Design 构建应用布局。它采用topBar、bottomBar和floatingActionButton等参数来高效地构建应用的关键组件。可以理解成预先内置了一个通用布局,包含顶部、底部导航,以及FAB。
kotlin
Scaffold(
topBar = {
TopAppBar(
title = {
// Placeholder for top app bar content
}
)
},
bottomBar = {
BottomAppBar {
// Placeholder for bottom app bar content
}
},
floatingActionButton = {
FloatingActionButton(onClick = { /* Handle click */ }) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
) { innerPadding ->
Column() {
// Main content
}
}
2.4 LazyColumn懒加载
在项目中大量使用Column
可能会导致性能问题,因为所有项目的布局都与可见性无关。Compose 提供了LazyColumn
和 LazyRow
等高效替代方案,它们仅处理视口中的可见项目。类似于 Recyclerview
,能自动执行复用和回收,需要注意,如果复用不当,很容易产生bug。
kotlin
LazyColumn {
items(messages) { message ->
ProductItemRow(message)
}
}
此外,我们可以使用 items()
扩展函数(称为 itemsIndexed()
)来获取索引号 。此外,LazyVerticalGrid
和 LazyHorizontalGrid
可组合项可用于网格布局。
2.5 Modifiers修饰符
Modifiers修饰符
通过提供装饰或添加行为 来增强 Compose 界面元素。示例包括背景、填充以及行、文本或按钮的点击事件监听器。
Compose 中的修饰符的作用与 XML
属性(如 id
、padding
、margin
、color
、alpha
、ratio
和 elevation
等)类似。
可以将修饰符理解为
XML
中对于View
的通用属性设置。
kotlin
@Composable
private fun UserProfile(fullName: String) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = "Name")
Text(text = fullName
}
}
2.6 TextField输入框
TextField
是一个专为用户输入文本或数字而设计的 UI 组件,方便收集用户提供的数据。
类似于 XML
中的EditText
。
kotlin
@Composable
fun UserInputField() {
// to save user input value
var userInput by remember { mutableStateOf(TextFieldValue("")) }
TextField(
value = userInput,
onValueChange = {
userInput = it
},
label = { Text(text = "Enter Your Text") },
placeholder = { Text(text = "Type something here...") },
)
}
这些是您开始所需的基本内容,我们将在本系列的后续部分中进一步了解它们。
第一个使用 Material3 的 Compose 项目
依次通过 Android Studio 菜单栏的New Project → Jetpack Compose Empty Activity
来创建新项目。
让我们看看Android Studio创建的项目结构和文件。
1. Color.kt --- 包含应用程序的颜色
颜色文件包含与我们的应用相关的所有颜色。我们可以在此处添加我们的应用颜色。但是,根据Material3 指南的建议,我们应该从Material3ThemeBuilder自动生成我们的应用颜色主题,以遵循每种风格的" Material You "个性化。
2. Theme.kt --- 包含应用程序的样式/主题
我们的应用有两种主要状态:浅色 和 深色 。此外,为了符合 Material You
原则,调色板现在会根据所选壁纸动态调整。为了与此功能协调,我们需要根据所选调色板配置主题颜色。
有超过 26 种颜色角色映射到 Material Components。我们选定的应用颜色将映射到 Material Theme Color Roles。查看此文档,了解全面的颜色选项。
kotlin
private val DarkColorScheme = darkColorScheme(
primary = YourDarkPrimaryColor,
secondary = YourAppDarktSecondaryColor,
tertiary = YourAppDarkTertiaryColor
)
private val LightColorScheme = lightColorScheme(
primary = YourAppLightPrimaryColor,
secondary = YourAppLightSecondaryColor,
tertiary = YourAppLightTertiaryColor
/* 要覆盖的其他默认颜色角色
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
Material Theme
将根据壁纸样式或选择调整您的配色方案。
kotlin
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
3. Type.kt --- 应用程序的字体格式
就像改变颜色一样,调整Type意味着替换Material3主题的常用样式。
kotlin
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
4. MainActivity.kt --- 应用程序主界面
在 Compose 中,Activity
仍充当 Android 应用的起点。与传统 View 系统中使用 XML 文件不同,您可以使用 setContent
来概述布局。在其中,将调用 Composable 函数。
让我们快速观察一下这里发生了什么。MainActivity的初始化逻辑仍然写在onCreate()
函数里,以下用不同颜色对应相应代码块。
- 🔵 →该
setContent
块设置 Compose UI。它使用Surface composable
作为容器,应用主题中的背景颜色。 - 🟡 →
Greeting
可组合函数。它接受一个name
参数和一个可选modifier
参数,其默认值为Modifier
。 - 🔴 →
GreetingPreview
可组合项是一种预览功能,可用于在开发过程中可视化 UI 组件的外观。
设计一个简单的卡片式UI
让我们创建一个TravelCard.kt
文件并开始编写代码。我们将了解一些 UI 元素,并最终完成如下这个 在尼泊尔旅行 卡片样式。
使用 Jetpack Compose 实现简单的尼泊尔旅行卡 UI
在我们开始之前,为Compose Material 3和Coil图像加载库添加这些依赖项。
用 Compose 的方式思考
可以使用以下元素制作上述 UI。
travel_card.xml == CardView > LinearLayout > ImageView > TextView123...
类似地,在撰写中我们可以将它们分为以下部分。
kotlin
@Composable
fun TravelCard() {
Card() {
Column() {
Image()
Column() {
Text(text = Your Category".uppercase())
Text( text = "Your Title")
Text(text = "Your Description")
}
}
}
}
现在,让我们通过添加一些修饰符来制作第一张卡片,
kotlin
@Composable
fun TravelCard() {
Card(
modifier = Modifier
.padding(10.dp)
.shadow(
elevation = 5.dp,
spotColor = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.medium
),
shape = MaterialTheme.shapes.medium
) {
//... card contianer
}
}
然后,添加列、图像和文本。
kotlin
@Composable
fun TravelCard() {
Card(
modifier = Modifier
.padding(10.dp)
.shadow(
elevation = 5.dp,
spotColor = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.medium
),
shape = MaterialTheme.shapes.medium
) {
Column(
Modifier
.fillMaxWidth(),
) {
Image(
painter = painterResource(id = R.drawable.ic_travel_dummy),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(8.dp)
.height(150.dp)
.size(84.dp)
.clip(MaterialTheme.shapes.medium)
)
Column(
Modifier
.padding(10.dp),
) {
Text(
text = "yourText".uppercase(),
style = appTypography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(8.dp)
)
Text(
text = "Your Title",
style = appTypography.titleLarge,
maxLines = 2,
color = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.padding(8.dp)
)
Text(
text = "Your Description",
style = appTypography.bodySmall,
maxLines = 3,
color = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.padding(8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Preview(showBackground = true)
@Composable
fun TravelCardPreview() {
TestArticleTheme {
TravelCard()
}
}
您可以用AsyncImage
替换Image组件来从 URL 加载图像:
kotlin
AsyncImage(
model = travel.thumbnail,
contentDescription = productEntity.title,
modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth()
.height(150.dp),
contentScale = ContentScale.Crop,
)
最后,为了在我们的应用中显示TravelCard
,请在MainActivity.kt
中添加以下内容
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ArticleTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TravelCard()
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun Preview() {
TestArticleTheme {
TravelCard()
}
}
现在,让我们创建一个简单的登录页面。
复杂场景设计------登录
创建新文件LoginScreen.kt
和LoginComponents.kt
用于登录页面设计及其组件。
登录屏幕 --- Firefly App 的主屏幕设计
从上图可以看出,有电子邮件文本框、密码文本框、登录按钮和注册文本等组件。现在,让我们将电子邮件文本框组件整合起来,使其不仅可以在注册部分使用,还可以在应用程序的各个页面上使用。
1. 电子邮件输入 & 密码输入
kotlin
@Composable
fun EmailInput(
label: String,
icon: ImageVector,
currentValue: String,
focusRequester: FocusRequester? = null,
keyboardActions: KeyboardActions,
onValueChange: (String) -> Unit
) {
TextField(
value = currentValue,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester ?: FocusRequester()),
leadingIcon = { Icon(imageVector = icon, contentDescription = label) },
label = { Text(text = label) },
shape = Shapes.medium,
singleLine = true,
keyboardActions = keyboardActions,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = true,
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
)
}
@Composable
fun PasswordInput(
label: String,
icon: ImageVector,
currentValue: String,
focusRequester: FocusRequester? = null,
keyboardActions: KeyboardActions,
onValueChange: (String) -> Unit
) {
var passwordVisible by remember { mutableStateOf(false) }
TextField(
value = currentValue,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester ?: FocusRequester()),
leadingIcon = { Icon(imageVector = icon, contentDescription = label) },
trailingIcon = {
val passwordIcon = if (passwordVisible) {
AppIcons.PasswordEyeVisible
} else {
AppIcons.PasswordEyeInvisible
}
val description = if (passwordVisible) {
"Hide Password"
} else {
"Show Password"
}
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = passwordIcon, contentDescription = description)
}
},
label = { Text(text = label) },
shape = Shapes.medium,
singleLine = true,
keyboardActions = keyboardActions,
visualTransformation = if (passwordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = true,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
)
}
2. 参数说明
label
: String,它是一个字符串,为用户提供有关预期输入的上下文或指导。icon
: ImageVector,它可以是提供与电子邮件输入相关的视觉提示的图像或矢量图形。
或者对于图标,您可以使用此官方库。
kotlin
// build.gradle
implementation 'androidx.compose.material:material-icons-extended:1.5.4'
// in Kotlin file
object AppIcons {
val Email = Icons.Default.Email
val Password = Icons.Default.Lock
val PasswordEyeVisible = Icons.Default.Visibility
val PasswordEyeInvisible = Icons.Default.VisibilityOff
}
currentValue
: String,此参数保存电子邮件输入字段的当前值。它表示当前在输入字段中输入或选择的文本。onValueChange
: (String) -> Unit,这是一个高阶函数参数。它以 lambda 函数作为参数,其中 lambda 函数接收一个String参数。此函数是一个回调函数,当电子邮件输入的值发生变化时调用。(String) -> Unit
语法指定 lambda 函数应接受一个String参数并返回void。
创建个性化登录用户界面的过程非常简单。只需在 Column
中排列元素并根据需要应用所需的修饰符即可。
kotlin
// LoginScreen.kt
@Composable
fun LoginScreen() {
// Sperate this function as we have to addd viewmodel, declear variables here
// Identify keys actions and listener we may require for login screens.
LoginContent(
email = "[email protected]",
password = "password",
onEmailChange = {
// listen changes of email field
},
onPasswordChange = {
// listen changes of password field
},
onLoginClick = {
// when onLogin Button is Clicked
},
onSignUpClick = navigateToSignUp, // signUp Click
)
}
@Composable
fun LoginContent(
email: String,
password: String,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLoginClick: () -> Unit,
onSignUpClick: () -> Unit
) {
val passwordFocusRequester = FocusRequester()
val focusManager: FocusManager = LocalFocusManager.current
Column(
Modifier
.padding(MaterialTheme.dimens.extraLarge)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.weight(2f)
.padding(MaterialTheme.dimens.medium), contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_app_logo),
contentDescription = "logo",
Modifier.padding(10.dp)
)
}
Box(
modifier = Modifier.weight(3f),
) {
Spacer(modifier = Modifier.height(20.dp))
Column(verticalArrangement = Arrangement.Center) {
EmailInput(
currentValue = email,
keyboardActions = KeyboardActions(onNext = { passwordFocusRequester.requestFocus() }),
onValueChange = onEmailChange,
icon = AppIcons.Email,
label = stringResource(id = R.string.label_email),
)
Spacer(modifier = Modifier.height(20.dp))
PasswordInput(
currentValue = password,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
focusRequester = passwordFocusRequester,
onValueChange = onPasswordChange,
icon = AppIcons.Password,
label = stringResource(id = R.string.label_password),
)
Spacer(modifier = Modifier.height(30.dp))
Button(
onClick = {
onLoginClick()
},
Modifier
.fillMaxWidth()
.disableMutipleTouchEvents()
) {
Box {
Text(text = "Sign In", Modifier.padding(8.dp))
}
}
}
}
Box(
modifier = Modifier.weight(0.5f)
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Don't have an account?", color = Color.Black)
TextButton(onClick = {
onSignUpClick()
}) {
Text(text = "Sign Up")
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
FireflyComposeTheme {
LoginScreen(
navigateToHome = {},
navigateToSignUp = {})
}
}
这就是我们创建一个简单的登录页面所需的全部内容。这很令人印象深刻,对吧。