【NowInAndroid架构拆解】番外篇3之给xml布局者最佳的Jetpack Compose介绍文章

本文系 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 提供了LazyColumnLazyRow 等高效替代方案,它们仅处理视口中的可见项目。类似于 Recyclerview,能自动执行复用和回收,需要注意,如果复用不当,很容易产生bug。

kotlin 复制代码
LazyColumn {
    items(messages) { message ->
        ProductItemRow(message)
    }
}

此外,我们可以使用 items() 扩展函数(称为 itemsIndexed() )来获取索引号 。此外,LazyVerticalGridLazyHorizontalGrid 可组合项可用于网格布局。

2.5 Modifiers修饰符

Modifiers修饰符 通过提供装饰或添加行为 来增强 Compose 界面元素。示例包括背景、填充以及行、文本或按钮的点击事件监听器

Compose 中的修饰符的作用与 XML 属性(如 idpaddingmargincoloralpharatioelevation 等)类似。

可以将修饰符理解为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 3Coil图像加载库添加这些依赖项。

用 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.ktLoginComponents.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 = {})
    }
}

这就是我们创建一个简单的登录页面所需的全部内容。这很令人印象深刻,对吧。

参考资料

相关推荐
道友老李2 小时前
【微服务架构】SpringCloud(二):Eureka原理、服务注册、Euraka单独使用
spring cloud·微服务·架构
哦豁灬2 小时前
CUDA 学习(1)——GPU 架构
学习·架构·gpu
Faith_xzc9 小时前
存算分离是否真的有必要?从架构之争到 Doris 实战解析
大数据·数据库·数据仓库·架构·开源
云上的阿七12 小时前
无服务器架构将淘汰运维?2025年云计算形态预测
运维·架构·serverless
JAVA开发区12 小时前
微服务架构中的API网关:Spring Cloud与Kong/Traefik等方案对比
spring cloud·微服务·架构·api 网关
森焱森18 小时前
星型组网和路由器组网的区别
网络·架构·智能路由器
StarRocks_labs18 小时前
vivo 湖仓架构的性能提升之旅
数据仓库·架构·数据分析·云计算·湖仓一体
云祺vinchin19 小时前
Q&A:备份产品的存储架构采用集中式和分布式的优劣?
大数据·运维·网络·分布式·架构
Teecertlabs19 小时前
【机密计算顶会解读】11:ACAI——使用 Arm 机密计算架构保护加速器执行
arm开发·架构·机密计算·arm cca·可信执行环境