在移动应用开发领域,Android 开发一直是技术演进的前沿阵地,而 UI 开发作为用户与应用交互的核心环节,其技术体系的变革更是备受瞩目。
技术演进背景
Android UI 开发体系发展脉络
原生 View 体系阶段
在早期的 Android 开发中,原生 View 体系占据了主导地位。开发者通过继承 View 类并重写其方法来自定义控件,这种基于面向对象的开发模式要求开发者对 View 的绘制、事件处理等机制有深入理解。每个自定义控件都成为一个独立的类,开发者需要处理大量的细节,如测量、布局和绘制过程,这使得开发过程较为复杂且容易出错。
XML 布局文件与 Java/Kotlin 代码的分离是这一阶段的典型特征。开发者在 XML 文件中定义界面布局,包括控件的类型、属性和层次结构。然后在 Java 或 Kotlin 代码中通过 findViewById 方法获取控件实例,进而设置事件监听器、更新控件状态等。这种 XML+Java/Kotlin 混合开发模式虽然在一定程度上实现了界面与逻辑的分离,但在实际开发中也存在诸多问题。例如,当布局变得复杂时,XML 文件容易变得冗长且难以维护;代码与布局之间的关联依赖于控件的 ID,一旦 ID 发生冲突或更改,就可能导致运行时错误。
传统开发代码示例 :
布局文件(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btnClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"/>
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Count: 0"/>
</LinearLayout>
代码文件(MainActivity.java)
public class MainActivity extends AppCompatActivity {
private int count = 0;
private TextView tvCount;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvCount = findViewById(R.id.tvCount);
Button btnClick = findViewById(R.id.btnClick);
btnClick.setOnClickListener(v -> {
count++;
tvCount.setText("Count: " + count);
});
}
}
现代声明式 UI 框架的兴起背景
随着移动应用的不断发展,用户对应用的界面交互要求越来越高,传统的 UI 开发模式在应对复杂的界面和频繁的状态变化时逐渐暴露出效率低下的问题。开发一个复杂的界面,需要在 XML 中定义复杂的布局层次,然后在代码中手动更新 UI 组件的状态,这不仅增加了开发的工作量,也使得代码变得难以维护。
在这种背景下,声明式 UI 框架逐渐兴起。声明式 UI 的核心思想是让开发者直接描述界面的最终状态,而不是手动控制界面的变化过程。这种开发模式使得 UI 的构建更加直观和高效,能够显著减少代码量并降低开发难度。同时,声明式 UI 框架通常具备更好的性能优化机制,能够自动处理界面的更新和重绘,从而提高应用的运行效率。
Compose 技术定位
Kotlin Compose 是 Google 推出的一种现代声明式 UI 框架,它基于 Kotlin 语言,旨在彻底改变 Android UI 开发的方式。Compose 被定位为 Google 官方的 Modern Toolkit,是 Android UI 开发的未来方向。它不仅仅是一个简单的 UI 框架,而是一个集成了众多先进技术和理念的开发工具包,能够为开发者提供更高效、更灵活、更强大的 UI 开发体验。
Compose 的出现,使得 Android 开发者可以摆脱传统 XML 布局和复杂的 View 操作,直接使用 Kotlin 代码以声明式的方式构建 UI。它将界面、逻辑和数据紧密地结合在一起,通过函数式编程的方式实现界面的动态更新,极大地简化了开发流程并提高了开发效率。同时,Compose 还具备跨平台的潜力,能够在多个平台上运行,为开发者提供了更广阔的应用场景。
Compose 开发代码示例 :
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.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 androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ContentView()
}
}
}
@Composable
fun ContentView() {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp)
) {
Button(onClick = { count++ }) {
Text(text = "Click Me")
}
Text(text = "Count: $count")
}
}
Compose 核心技术特征
声明式编程范式
UI=ƒ(State) 数学表达
在 Kotlin Compose 中,UI 被视为一个函数的输出,而这个函数的输入是应用的状态。即 UI = f(state),这一数学表达式直观地体现了声明式 UI 的核心思想。开发者只需已关注如何根据当前的状态生成对应的 UI,而无需手动控制 UI 的更新过程。当应用的状态发生变化时,Compose 会自动重新执行相关的函数,生成新的 UI 并将其渲染到屏幕上。
例如,一个简单的计数器应用,其状态是一个整数计数值。在 Compose 中,开发者可以定义一个函数,该函数根据计数值的状态构建一个显示计数值和两个按钮(增加和减少计数)的界面。当用户点击按钮导致计数值改变时,Compose 会自动检测到状态的变化,并重新执行界面构建函数,更新显示的计数值。
Compose 中 UI=ƒ(State) 代码示例 :
@Composable
fun CounterView(count: Int, onIncrement: () -> Unit, onDecrement: () -> Unit) {
Column {
Text(text = "Count: $count")
Row {
Button(onClick = onDecrement) {
Text(text = "-")
}
Button(onClick = onIncrement) {
Text(text = "+")
}
}
}
}
// 使用示例
var count by remember { mutableStateOf(0) }
CounterView(
count = count,
onIncrement = { count++ },
onDecrement = { count-- }
)
状态驱动更新机制
Compose 的状态驱动更新机制是其声明式编程范式的核心实现之一。在 Compose 中,状态是通过特殊的变量或对象来表示的,这些状态可以是简单的基本数据类型,也可以是复杂的对象。当状态发生变化时,Compose 会自动跟踪这些变化,并重新执行与该状态相关的 UI 构建函数,从而实现界面的更新。
Compose 提供了多种方式来定义和管理状态。例如,可以使用 mutableStateOf 函数来创建一个可变的状态对象,该对象可以存储任意类型的值,并且当其值发生变化时会通知 Compose 进行界面更新。此外,Compose 还支持将状态作为参数传递给可组合函数,使得状态可以在不同的界面组件之间共享和传递。
Compose 状态驱动更新代码示例 :
@Composable
fun Greeting(name: String) {
var isSelected by remember { mutableStateOf(false) }
Surface(
color = if (isSelected) Blue else White,
modifier = Modifier.padding(8.dp)
) {
Text(
text = "Hello, $name!",
modifier = Modifier
.padding(16.dp)
.clickable { isSelected = !isSelected }
)
}
}
布局系统差异
XML 布局的优缺点分析
XML 布局作为传统 Android UI 开发的主要布局方式,具有一定的优缺点。
优点方面,XML 布局实现了界面与逻辑的分离,开发者可以在 XML 文件中专注于界面的设计,而在 Java/Kotlin 代码中处理业务逻辑。这有助于团队中的不同成员(如 UI 设计师和开发人员)并行工作。XML 布局还提供了丰富的布局类型和属性,能够满足各种复杂的界面布局需求,如线性布局、相对布局、约束布局等。此外,Android Studio 提供了强大的 XML 布局设计工具,如拖拽式布局编辑器、预览功能等,方便开发者进行可视化开发。
然而,XML 布局也存在一些缺点。随着界面复杂度的增加,XML 文件容易变得臃肿和难以维护。开发者需要在 XML 文件中手动调整控件的层次结构和属性,这可能导致布局效率低下。而且,XML 布局与代码的交互相对繁琐,需要通过 findViewById 等方法进行关联,容易出现控件 ID 冲突或找不到控件等问题。此外,XML 布局的热重载能力有限,在开发过程中修改布局文件后需要重新运行应用才能看到效果,这影响了开发效率。
传统 XML 布局代码示例 :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome to My App"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/loginButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Compose 布局模型:布局修饰符与测量逻辑
Compose 的布局模型与传统的 XML 布局有显著不同。在 Compose 中,布局是通过可组合函数和布局修饰符来实现的。可组合函数定义了界面的结构和内容,而布局修饰符则用于控制界面组件的布局属性,如大小、位置、对齐方式等。
Compose 的布局修饰符提供了一种灵活且直观的方式来定制界面组件的布局。开发者可以将多个修饰符组合在一起,以实现复杂多样的布局效果。例如,可以通过 Modifier.padding() 修饰符为组件添加内边距,通过 Modifier.size() 修饰符设置组件的大小,通过 Modifier.align() 修饰符指定组件的对齐方式等。
Compose 的测量逻辑遵循一个清晰的流程:测量子项 → 确定自身尺寸 → 放置子项。每个可组合函数在布局过程中都会经历这三个步骤。首先,它会测量其子组件所需的尺寸,然后根据自身的布局规则和修饰符确定自己的尺寸,最后将子组件放置在合适的位置。这种测量逻辑使得 Compose 能够精确地计算每个界面组件的布局,确保界面的正确性和性能。
Compose 布局代码示例 :
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColors
} else {
LightColors
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
@Composable
fun MyScreen() {
MyAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Welcome to My App",
style = Typography.h5,
modifier = Modifier.padding(bottom = 16.dp)
)
Button(
onClick = { /* Handle login */ },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text(text = "Login")
}
}
}
}
}
核心差异深度解析
状态管理机制对比
传统模式的 findViewById 与数据绑定
在传统的 Android UI 开发中,状态管理主要依赖于 findViewById 方法和数据绑定技术。开发者通过 findViewById 获取 UI 组件的实例,然后在代码中手动更新组件的状态,如设置文本、图片、可见性等。这种方式在简单的界面中尚可接受,但在复杂的界面中,随着状态的增多和交互的复杂化,手动更新 UI 组件的状态变得越来越困难且容易出错。
数据绑定技术的出现一定程度上改善了这种情况。通过数据绑定,开发者可以在 XML 布局文件中直接引用数据模型中的字段,从而实现界面与数据的双向绑定。当数据模型中的数据发生变化时,界面会自动更新;反之,当用户通过界面交互修改了数据时,数据模型也会自动更新。然而,数据绑定的配置相对复杂,需要定义数据模型、绑定适配器等,并且在运行时会增加一定的性能开销。
传统数据绑定代码示例 :
布局文件(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.example.data.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.email}"
android:textSize="14sp"/>
</LinearLayout>
</layout>
代码文件(MainActivity.java)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_main);
User user = new User("John Doe", "john@example.com");
binding.setUser(user);
}
}
Compose 状态提升与单向数据流
Compose 采用了一种不同的状态管理方式,即状态提升与单向数据流。在 Compose 中,状态通常被定义在尽可能高的层级,然后通过参数传递的方式将状态和事件处理函数传递给需要的子组件。这种状态提升的做法使得状态的管理和更新更加集中和可控。
单向数据流是 Compose 状态管理的核心原则之一。状态的变化只能由特定的事件触发,这些事件通常发生在 UI 组件中,如用户点击按钮、输入文本等。当事件发生时,事件处理函数会更新状态,然后 Compose 会根据新的状态重新渲染界面。数据的流动是单向的,从状态源出发,经过事件处理和状态更新,最终影响界面的显示。这种单向数据流机制使得数据的流向清晰明确,减少了因数据循环依赖或状态不一致而导致的问题。
Compose 状态管理代码示例 :
@Composable
fun UserProfile(
initialName: String,
initialEmail: String
) {
var name by remember { mutableStateOf(initialName) }
var email by remember { mutableStateOf(initialEmail) }
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Name: $name")
Text(text = "Email: $email")
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth()
)
}
}
性能优化对比
传统 View 的布局层级优化
在传统的 Android 开发中,布局层级的优化是提高 UI 性能的关键之一。复杂的布局层级会导致过多的测量和绘制操作,从而增加 CPU 和内存的消耗,影响应用的流畅性。为了优化布局层级,开发者通常会采用合并布局、减少嵌套等方法。例如,使用 ConstraintLayout 替代嵌套的 LinearLayout 或 RelativeLayout,以减少布局的嵌套深度,从而降低测量和布局的开销。
此外,还可以通过移除不必要的 View 组件、使用更高效的布局类型等方式来优化布局性能。例如,避免使用过多的 FrameLayout 来实现简单的叠加效果,而是使用更合适的布局方式;对于不需要响应用户交互的 View,可以设置 android:layout_width 和 android:layout_height 为 wrap_content 或 match_parent,并去掉不必要的背景等,以减少绘制的复杂度。
传统布局层级优化前代码示例 :
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/user_icon"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="John Doe"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="john@example.com"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Phone:"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="123-456-7890"/>
</LinearLayout>
</LinearLayout>
Compose 重组作用域控制
Compose 提供了重组作用域控制机制来优化性能。通过合理地组织可组合函数的结构,开发者可以控制界面更新的范围,避免不必要的重组和重绘。在 Compose 中,当状态发生变化时,只有那些依赖于该状态的可组合函数及其子函数会被重新执行,而其他未受影响的函数不会被重组。这使得 Compose 能够在界面更新时仅处理必要的部分,从而提高性能。
Compose 重组作用域代码示例 :
@Composable
fun ParentComponent() {
// 状态在父组件中定义
var count by remember { mutableStateOf(0) }
Column {
// 子组件 1 依赖于 count 状态
ChildComponent1(count = count)
// 子组件 2 不依赖于 count 状态
ChildComponent2()
// 子组件 3 依赖于 count 状态
ChildComponent3(count = count)
}
}
@Composable
fun ChildComponent1(count: Int) {
Text(text = "Count in Child 1: $count")
}
@Composable
fun ChildComponent2() {
Text(text = "Static text in Child 2")
}
@Composable
fun ChildComponent3(count: Int) {
Text(text = "Count in Child 3: $count")
Button(onClick = { /* Some action */ }) {
Text(text = "Click Me")
}
}
传统 View 重组作用域代码示例 :
public class ParentView extends LinearLayout {
private ChildView1 childView1;
private ChildView2 childView2;
private ChildView3 childView3;
private int count = 0;
public ParentView(Context context) {
super(context);
initializeViews();
}
private void initializeViews() {
setOrientation(VERTICAL);
childView1 = new ChildView1(getContext(), count);
childView2 = new ChildView2(getContext());
childView3 = new ChildView3(getContext(), count);
addView(childView1);
addView(childView2);
addView(childView3);
}
public void incrementCount() {
count++;
childView1.updateCount(count);
childView3.updateCount(count);
}
}
public class ChildView1 extends LinearLayout {
private TextView countTextView;
public ChildView1(Context context, int initialCount) {
super(context);
initializeViews(initialCount);
}
private void initializeViews(int initialCount) {
setOrientation(HORIZONTAL);
countTextView = new TextView(getContext());
countTextView.setText("Count in Child 1: " + initialCount);
addView(countTextView);
}
public void updateCount(int newCount) {
countTextView.setText("Count in Child 1: " + newCount);
invalidate(); // 触发重绘
}
}
public class ChildView2 extends LinearLayout {
public ChildView2(Context context) {
super(context);
initializeViews();
}
private void initializeViews() {
setOrientation(HORIZONTAL);
TextView staticTextView = new TextView(getContext());
staticTextView.setText("Static text in Child 2");
addView(staticTextView);
}
}
public class ChildView3 extends LinearLayout {
private TextView countTextView;
private Button actionButton;
public ChildView3(Context context, int initialCount) {
super(context);
initializeViews(initialCount);
}
private void initializeViews(int initialCount) {
setOrientation(VERTICAL);
countTextView = new TextView(getContext());
countTextView.setText("Count in Child 3: " + initialCount);
addView(countTextView);
actionButton = new Button(getContext());
actionButton.setText("Click Me");
actionButton.setOnClickListener(v -> {
// 按钮点击事件处理
});
addView(actionButton);
}
public void updateCount(int newCount) {
countTextView.setText("Count in Child 3: " + newCount);
invalidate(); // 触发重绘
}
}
列表渲染效率对比(RecyclerView vs LazyColumn)
列表渲染是移动应用中常见的场景,也是性能优化的重点之一。在传统开发中,RecyclerView 是常用的列表渲染组件。RecyclerView 通过 recycling 机制复用列表项的 View,从而减少 View 的创建和销毁开销,提高列表滚动的流畅性。然而,RecyclerView 的使用相对复杂,需要配置 Adapter、ViewHolder 等组件,并且在处理复杂的列表项布局和交互时,代码容易变得繁琐。
Compose 中的 LazyColumn(用于垂直列表)和 LazyRow(用于水平列表)提供了更简洁高效的列表渲染方式。LazyList 在渲染时只会生成可见区域内的列表项,以及部分即将进入可见区域的列表项,从而减少了内存占用和初始化开销。当列表滚动时,LazyList 会动态地创建和销毁列表项,确保内存的高效利用。与 RecyclerView 相比,LazyList 的代码更加简洁,开发者无需手动管理 View 的复用和列表项的绑定,只需专注于列表项的 UI 构建和数据提供。
Compose LazyColumn 代码示例 :
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(messages) { message ->
MessageItem(message = message)
Divider(color = Color.LightGray)
}
}
}
@Composable
fun MessageItem(message: Message) {
Row(modifier = Modifier.padding(16.dp)) {
Image(
painter = rememberImagePainter(data = message.sender.profilePictureUrl),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = message.sender.name,
style = TextStyle(fontWeight = FontWeight.Bold)
)
Spacer(modifier = Modifier.height(4.dp))
Text(text = message.text)
}
Spacer(modifier = Modifier.width(16.dp))
Text(text = formatDateTime(message.timestamp))
}
}
传统 RecyclerView 代码示例 :
布局文件(activity_main.xml)
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
布局文件(item_message.xml)
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/avatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_default_avatar"
android:layout_gravity="center_vertical"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/senderNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"/>
<TextView
android:id="@+id/messageTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/timeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"/>
</LinearLayout>
开发体验对比
代码可维护性
组合式架构的模块化优势
Compose 的组合式架构为代码的模块化提供了天然的支持。开发者可以将界面的不同部分分解为独立的可组合函数,每个函数负责一个特定的功能或界面元素。这些可组合函数可以在不同的界面中复用,也可以通过参数化的方式进行定制。这种模块化的开发方式使得代码结构更加清晰,易于理解和维护。
例如,一个按钮组件可以定义为一个可组合函数,其中包含按钮的样式、点击事件处理等逻辑。该按钮组件可以被多个界面调用,并且可以通过传递不同的参数来改变按钮的外观和行为,如按钮的文本、颜色、大小等。当需要修改按钮的逻辑或样式时,只需修改该可组合函数即可,而无需在多个地方查找和修改代码。
Compose 模块化代码示例 :
@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Primary,
contentColor = Color.White,
disabledBackgroundColor = Color.Gray,
disabledContentColor = Color.LightGray
)
) {
Text(text = text)
}
}
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
onLoginClick: (String, String) -> Unit
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Welcome Back",
style = Typography.h4,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Email
)
)
Spacer(modifier = Modifier.height(8.dp))
PasswordTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
text = "Login",
onClick = { onLoginClick(email, password) },
modifier = Modifier.align(Alignment.End)
)
}
}
@Composable
fun RegisterScreen(
modifier: Modifier = Modifier,
onRegisterClick: (String, String, String) -> Unit
) {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Create Account",
style = Typography.h4,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Email
)
)
Spacer(modifier = Modifier.height(8.dp))
PasswordTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
text = "Register",
onClick = { onRegisterClick(name, email, password) },
modifier = Modifier.align(Alignment.End)
)
}
}
传统 View 模块化代码示例 :
public class PrimaryButton extends AppCompatButton {
public PrimaryButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setTextColor(ContextCompat.getColor(getContext(), R.color.white));
setBackgroundResource(R.drawable.bg_primary_button);
setPadding(16, 12, 16, 12);
setTypeface(Typeface.DEFAULT_BOLD);
}
}
public class LoginScreen extends FrameLayout {
private EditText emailEditText;
private EditText passwordEditText;
private PrimaryButton loginButton;
public LoginScreen(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews();
}
private void initializeViews() {
LayoutInflater.from(getContext()).inflate(R.layout.screen_login, this, true);
emailEditText = findViewById(R.id.emailEditText);
passwordEditText = findViewById(R.id.passwordEditText);
loginButton = findViewById(R.id.loginButton);
loginButton.setOnClickListener(v -> {
String email = emailEditText.getText().toString();
String password = passwordEditText.getText().toString();
if (onLoginClickListener != null) {
onLoginClickListener.onLogin(email, password);
}
});
}
public interface OnLoginClickListener {
void onLogin(String email, String password);
}
private OnLoginClickListener onLoginClickListener;
public void setOnLoginClickListener(OnLoginClickListener listener) {
this.onLoginClickListener = listener;
}
}
public class RegisterScreen extends FrameLayout {
private EditText nameEditText;
private EditText emailEditText;
private EditText passwordEditText;
private PrimaryButton registerButton;
public RegisterScreen(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews();
}
private void initializeViews() {
LayoutInflater.from(getContext()).inflate(R.layout.screen_register, this, true);
nameEditText = findViewById(R.id.nameEditText);
emailEditText = findViewById(R.id.emailEditText);
passwordEditText = findViewById(R.id.passwordEditText);
registerButton = findViewById(R.id.registerButton);
registerButton.setOnClickListener(v -> {
String name = nameEditText.getText().toString();
String email = emailEditText.getText().toString();
String password = passwordEditText.getText().toString();
if (onRegisterClickListener != null) {
onRegisterClickListener.onRegister(name, email, password);
}
});
}
public interface OnRegisterClickListener {
void onRegister(String name, String email, String password);
}
private OnRegisterClickListener onRegisterClickListener;
public void setOnRegisterClickListener(OnRegisterClickListener listener) {
this.onRegisterClickListener = listener;
}
}
// 使用示例
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LoginScreen loginScreen = findViewById(R.id.loginScreen);
loginScreen.setOnLoginClickListener((email, password) -> {
// 处理登录逻辑
});
RegisterScreen registerScreen = findViewById(R.id.registerScreen);
registerScreen.setOnRegisterClickListener((name, email, password) -> {
// 处理注册逻辑
});
}
}
开发调试效率
实时预览功能实测对比
Compose 的实时预览功能为开发调试带来了极大的便利。在 Android Studio 中,开发者只需编写可组合函数,并添加 @Preview 注解,即可在预览窗口中实时查看界面的渲染效果。当修改代码时,预览窗口会立即更新,无需启动模拟器或真实设备,大大加快了开发迭代的速度。
例如,开发者在编写一个登录界面的可组合函数时,可以在代码中添加 @Preview 注解:
Compose 实时预览代码示例 :
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyAppTheme {
LoginScreen(
onLoginClick = { email, password ->
// 预览时不需要实际处理登录逻辑
}
)
}
}
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
onLoginClick: (String, String) -> Unit
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Welcome Back",
style = Typography.h4,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Email
)
)
Spacer(modifier = Modifier.height(8.dp))
PasswordTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onLoginClick(email, password) },
modifier = Modifier.align(Alignment.End)
) {
Text(text = "Login")
}
}
}
传统 XML 布局实时预览代码示例 :
布局文件(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome Back"
android:textSize="24sp"
android:textStyle="bold"/>
<EditText
android:id="@+id/emailEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress"
android:layout_marginTop="8dp"/>
<EditText
android:id="@+id/passwordEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword"
android:layout_marginTop="8dp"/>
<Button
android:id="@+id/loginButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
android:layout_gravity="end"
android:layout_marginTop="16dp"/>
</LinearLayout>
代码文件(MainActivity.java)
public class MainActivity extends AppCompatActivity {
private EditText emailEditText;
private EditText passwordEditText;
private Button loginButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
emailEditText = findViewById(R.id.emailEditText);
passwordEditText = findViewById(R.id.passwordEditText);
loginButton = findViewById(R.id.loginButton);
loginButton.setOnClickListener(v -> {
String email = emailEditText.getText().toString();
String password = passwordEditText.getText().toString();
// 处理登录逻辑
});
}
}
在 Android Studio 的设计视图中,可以预览传统 XML 布局的界面效果。但与 Compose 的实时预览相比,传统 XML 布局的预览有时可能不够准确,特别是在涉及到复杂的动态数据或自定义 View 时。而且,当修改布局文件后,需要重新运行应用才能看到最终的界面效果,这增加了开发的时间成本。
热重载速度量化数据
Compose 的热重载能力使得开发者在开发过程中能够即时看到代码更改后的界面效果,而无需重新启动应用。这在处理界面状态和交互逻辑时非常有用,能够显著提高开发效率。根据实际测试,Compose 的热重载速度通常比传统开发模式快数倍甚至数十倍。
例如,在一个包含多个状态的复杂界面中,当开发者修改状态相关的代码时,Compose 能够在几秒钟内完成热重载并更新界面显示。而在传统开发模式下,开发者需要重新运行应用,等待应用启动并进入相应的界面,这个过程可能需要数十秒甚至更长时间,尤其是在大型项目中。
多平台支持
Compose Multiplatform 现状
Compose Multiplatform 是 Jetpack Compose 的一个分支项目,旨在为多平台开发提供支持。目前,Compose Multiplatform 已经支持在 Android、iOS、桌面应用(如 Windows、macOS、Linux)和 Web 等平台上构建用户界面。这使得开发者能够使用一套 Kotlin 代码构建跨平台的应用界面,实现代码的复用和共享。
Compose Multiplatform 提供了与平台无关的 UI 组件和 API,同时也允许开发者根据需要调用平台特定的代码和资源。例如,在 Android 和 iOS 平台上,开发者可以使用相同的 Compose UI 代码来构建界面,但在需要访问平台特定功能(如 Android 的通知或 iOS 的相册)时,可以通过期望/实现模式编写平台特定的代码。
尽管 Compose Multiplatform 仍处于发展阶段,但它已经展现出巨大的潜力和前景。随着越来越多的开发者和企业开始已关注跨平台开发,Compose Multiplatform 有望成为一种重要的多平台 UI 开发解决方案。
Compose Multiplatform 代码示例(跨平台按钮组件) :
// 共享模块代码
expect fun getPlatformName(): String
@Composable
fun MultiplatformButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier
) {
Text(text = "$text - ${getPlatformName()}")
}
}
// Android 实现模块代码
actual fun getPlatformName(): String = "Android"
// iOS 实现模块代码
actual fun getPlatformName(): String = "iOS"
// 桌面应用实现模块代码
actual fun getPlatformName(): String = "Desktop"
// Web 实现模块代码
actual fun getPlatformName(): String = "Web"
传统跨平台方案代码示例(React Native) :
JavaScript 组件代码(Button.js)
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
const PlatformButton = ({ text, onPress }) => {
return (
<TouchableOpacity style={styles.button} onPress={onPress}>
<Text style={styles.buttonText}>{text}</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
backgroundColor: '#0000FF',
padding: 12,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold'
}
});
export default PlatformButton;
Android 原生代码( MainActivity.java )
public class MainActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "MultiplatformApp";
}
}
iOS 原生代码(AppDelegate.m)
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:[RCTBundleURLProvider sharedSettings].jsBundleURL
moduleProvider:nil
launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"MultiplatformApp"
initialProperties:nil];
rootView.backgroundColor = [UIColor whiteColor];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
实践应用场景
推荐使用 Compose 的场景
高频交互界面(如社交应用)
对于社交应用等具有高频交互界面的应用场景,Compose 的声明式编程范式和高效的状态管理机制能够显著提高开发效率和用户体验。在社交应用中,界面通常需要频繁地更新以反映用户的操作和数据的变化,如点赞、评论、消息推送等。
Compose 的智能重组机制能够确保只有受状态变化影响的部分界面被重新渲染,从而保证界面更新的流畅性。同时,Compose 的可组合函数可以方便地构建各种复杂的交互组件,如可折叠的评论列表、动态的表情选择器等。开发者可以更快速地实现这些复杂的交互效果,并且代码的可维护性更高。
Compose 社交应用界面代码示例 :
@Composable
fun PostCard(post: Post) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clickable { /* Handle post click */ },
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberImagePainter(post.user.profilePictureUrl),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = post.user.name,
style = Typography.subtitle1,
fontWeight = FontWeight.Bold
)
Text(
text = formatDateTime(post.timestamp),
style = Typography.caption,
color = Color.Gray
)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = { /* Handle more options click */ }
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "More options"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = post.content,
style = Typography.body1
)
Spacer(modifier = Modifier.height(8.dp))
postimageUrl?.let { imageUrl ->
Image(
painter = rememberImagePainter(imageUrl),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 200.dp)
.clip(RoundedCornerShape(8.dp))
)
}
Spacer(modifier = Modifier.height(16.dp))
Divider(color = Color.LightGray)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
PostAction(
icon = Icons.Default.ThumbUp,
text = "${post.likes}",
onClick = { /* Handle like click */ }
)
PostAction(
icon = Icons.Default.Comment,
text = "${post.comments}",
onClick = { /* Handle comment click */ }
)
PostAction(
icon = Icons.Default.Share,
text = "Share",
onClick = { /* Handle share click */ }
)
PostAction(
icon = Icons.Default.Bookmark,
text = "Save",
onClick = { /* Handle save click */ }
)
}
}
}
}
@Composable
fun PostAction(
icon: ImageVector,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.clickable { onClick() }
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.Gray
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = text,
style = Typography.caption,
color = Color.Gray
)
}
}
设计系统迭代项目
在设计系统迭代项目中,Compose 的组合式架构和实时预览功能能够为团队带来极大的便利。设计系统通常包含一系列的 UI 组件和设计规范,这些组件需要在多个项目和界面中复用,并且随着设计规范的更新而不断迭代。
Compose 的可组合函数使得 UI 组件的定义和复用变得简单直观。开发者可以将设计系统中的每个组件定义为一个独立的可组合函数,并按照设计规范进行实现。当设计规范发生变化时,只需修改相应的可组合函数即可,所有使用该组件的地方都会自动更新。同时,实时预览功能使得设计和开发人员能够即时看到组件在不同状态下的显示效果,确保组件的实现符合设计要求。
Compose 设计系统组件代码示例 :
// 颜色主题
val LightColors = lightColors(
primary = Purple500,
primaryVariant = Purple700,
onPrimary = White,
secondary = Teal200,
secondaryVariant = Teal300,
onSecondary = Black,
background = White,
surface = White,
error = Red400,
onBackground = Black,
onSurface = Black,
onError = White
)
val DarkColors = darkColors(
primary = Purple300,
primaryVariant = Purple500,
onPrimary = Black,
secondary = Teal200,
secondaryVariant = Teal300,
onSecondary = Black,
background = Black,
surface = Black,
error = Red400,
onBackground = White,
onSurface = White,
onError = Black
)
// 字体主题
val Typography = Typography(
h1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Light,
fontSize = 96.sp,
lineHeight = 112.sp,
letterSpacing = (-1.5).sp
),
h2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Light,
fontSize = 60.sp,
lineHeight = 72.sp,
letterSpacing = (-0.5).sp
),
h3 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 48.sp,
lineHeight = 56.sp,
letterSpacing = 0.sp
),
h4 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 34.sp,
lineHeight = 40.sp,
letterSpacing = 0.25.sp
),
h5 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 24.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
h6 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
subtitle1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
subtitle2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
body2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 1.25.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
overline = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Regular,
fontSize = 10.sp,
lineHeight = 12.sp,
letterSpacing = 1.5.sp
)
)
// 自定义按钮组件
@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Primary,
contentColor = Color.White,
disabledBackgroundColor = Color.Gray,
disabledContentColor = Color.LightGray
)
) {
Text(text = text)
}
}
// 自定义文本字段组件
@Composable
fun OutlinedTextFieldWithLabel(
value: String,
onValueChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
errorText: String? = null
) {
Column(modifier = modifier) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = { Text(text = label) },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
isError = isError,
modifier = Modifier.fillMaxWidth()
)
if (isError && !errorText.isNullOrEmpty()) {
Text(
text = errorText,
color = MaterialTheme.colors.error,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(start = 12.dp)
)
}
}
}
// 使用示例
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
onLoginClick: (String, String) -> Unit
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var emailError by remember { mutableStateOf(false) }
var passwordError by remember { mutableStateOf(false) }
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Welcome Back",
style = Typography.h4,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextFieldWithLabel(
value = email,
onValueChange = { email = it; emailError = false },
label = "Email",
modifier = Modifier.fillMaxWidth(),
isError = emailError,
errorText = "Please enter a valid email address"
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextFieldWithLabel(
value = password,
onValueChange = { password = it; passwordError = false },
label = "Password",
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = "Toggle password visibility"
)
},
isError = passwordError,
errorText = "Password must be at least 8 characters long"
)
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
text = "Login",
onClick = {
if (email.isEmpty() || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
emailError = true
}
if (password.length < 8) {
passwordError = true
}
if (!emailError && !passwordError) {
onLoginClick(email, password)
}
},
modifier = Modifier.align(Alignment.End)
)
}
}
传统 View 设计系统组件代码示例 :
颜色资源文件(res/values/colors.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Light theme colors -->
<color name="light_primary">@color/purple_500</color>
<color name="light_primary_variant">@color/purple_700</color>
<color name="light_on_primary">@color/white</color>
<color name="light_secondary">@color/teal_200</color>
<color name="light_secondary_variant">@color/teal_300</color>
<color name="light_on_secondary">@color/black</color>
<color name="light_background">@color/white</color>
<color name="light_surface">@color/white</color>
<color name="light_error">@color/red_400</color>
<color name="light_on_background">@color/black</color>
<color name="light_on_surface">@color/black</color>
<color name="light_on_error">@color/white</color>
<!-- Dark theme colors -->
<color name="dark_primary">@color/purple_300</color>
<color name="dark_primary_variant">@color/purple_500</color>
<color name="dark_on_primary">@color/black</color>
<color name="dark_secondary">@color/teal_200</color>
<color name="dark_secondary_variant">@color/teal_300</color>
<color name="dark_on_secondary">@color/black</color>
<color name="dark_background">@color/black</color>
<color name="dark_surface">@color/black</color>
<color name="dark_error">@color/red_400</color>
<color name="dark_on_background">@color/white</color>
<color name="dark_on_surface">@color/white</color>
<color name="dark_on_error">@color/black</color>
<!-- Other colors -->
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC6</color>
<color name="teal_300">#FF01D2CD</color>
<color name="red_400">#FFFF0000</color>
</resources>
字体资源文件(res/values/styles.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Light theme -->
<item name="colorPrimary">@color/light_primary</item>
<item name="colorPrimaryVariant">@color/light_primary_variant</item>
<item name="colorOnPrimary">@color/light_on_primary</item>
<item name="colorSecondary">@color/light_secondary</item>
<item name="colorSecondaryVariant">@color/light_secondary_variant</item>
<item name="colorOnSecondary">@color/light_on_secondary</item>
<item name="android:colorBackground">@color/light_background</item>
<item name="colorSurface">@color/light_surface</item>
<item name="colorError">@color/light_error</item>
<item name="colorOnBackground">@color/light_on_background</item>
<item name="colorOnSurface">@color/light_on_surface</item>
<item name="colorOnError">@color/light_on_error</item>
<!-- Font styles -->
<item name="fontFamily">@font/roboto</item>
</style>
<style name="AppTheme.Dark" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Dark theme -->
<item name="colorPrimary">@color/dark_primary</item>
<item name="colorPrimaryVariant">@color/dark_primary_variant</item>
<item name="colorOnPrimary">@color/dark_on_primary</item>
<item name="colorSecondary">@color/dark_secondary</item>
<item name="colorSecondaryVariant">@color/dark_secondary_variant</item>
<item name="colorOnSecondary">@color/dark_on_secondary</item>
<item name="android:colorBackground">@color/dark_background</item>
<item name="colorSurface">@color/dark_surface</item>
<item name="colorError">@color/dark_error</item>
<item name="colorOnBackground">@color/dark_on_background</item>
<item name="colorOnSurface">@color/dark_on_surface</item>
<item name="colorOnError">@color/dark_on_error</item>
</style>
<style name="ButtonStyle" parent="Widget.MaterialComponents.Button">
<item name="android:backgroundTint">@color/light_primary</item>
<item name="android:textColor">@color/light_on_primary</item>
<item name="cornerRadius">8dp</item>
</style>
<style name="TextInputStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxStrokeColor">@color/teal_200</item>
<item name="android:textColorHint">@color/black</item>
</style>
</resources>
布局文件(layout/item_text_input.xml)
<com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_text"
app:errorEnabled="true"
>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
布局文件(layout/login_screen.xml)
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome Back"
android:textSize="24sp"
android:textStyle="bold"/>
<include
layout="@layout/item_text_input"
android:id="@+id/emailTextInput"/>
<include
layout="@layout/item_text_input"
android:id="@+id/passwordTextInput"/>
<Button
android:id="@+id/loginButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
android:layout_gravity="end"
/>
</LinearLayout>
代码文件(LoginScreen.java)
public class LoginScreen extends FrameLayout {
private TextInputLayout emailTextInputLayout;
private TextInputEditText emailEditText;
private TextInputLayout passwordTextInputLayout;
private TextInputEditText passwordEditText;
private Button loginButton;
public LoginScreen(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews();
}
private void initializeViews() {
LayoutInflater.from(getContext()).inflate(R.layout.login_screen, this, true);
emailTextInputLayout = findViewById(R.id.emailTextInput);
emailEditText = emailTextInputLayout.getEditText();
passwordTextInputLayout = findViewById(R.id.passwordTextInput);
passwordEditText = passwordTextInputLayout.getEditText();
loginButton = findViewById(R.id.loginButton);
loginButton.setOnClickListener(v -> handleLoginClick());
}
private void handleLoginClick() {
String email = emailEditText.getText().toString();
String password = passwordEditText.getText().toString();
boolean emailError = false;
boolean passwordError = false;
if (email.isEmpty() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
emailError = true;
emailTextInputLayout.setError("Please enter a valid email address");
} else {
emailTextInputLayout.setError(null);
}
if (password.length() < 8) {
passwordError = true;
passwordTextInputLayout.setError("Password must be at least 8 characters long");
} else {
passwordTextInputLayout.setError(null);
}
if (!emailError && !passwordError) {
if (onLoginClickListener != null) {
onLoginClickListener.onLogin(email, password);
}
}
}
public interface OnLoginClickListener {
void onLogin(String email, String password);
}
private OnLoginClickListener onLoginClickListener;
public void setOnLoginClickListener(OnLoginClickListener listener) {
this.onLoginClickListener = listener;
}
}
跨平台需求项目
对于需要在多个平台上发布应用的项目,Compose Multiplatform 提供了一个高效且统一的开发解决方案。开发者可以使用一套 Kotlin 代码构建跨平台的 UI,同时根据各平台的特点进行适当的定制。
例如,一个企业级应用需要同时在 Android、iOS 和桌面平台上运行,使用 Compose Multiplatform 可以大大减少开发和维护的工作量。开发者可以在共享模块中实现业务逻辑和通用的 UI 组件,然后在各平台的特定模块中添加平台相关的代码和资源。这样,不仅提高了代码的复用率,还能确保应用在不同平台上的功能和界面的一致性。
Compose Multiplatform 跨平台应用代码示例 :
共享模块代码(commonMain/kotlin/com/example/MyApp.kt)
expect fun getPlatformName(): String
@Composable
expect fun PlatformSpecificGreeting(): @Composable () -> Unit
@Composable
fun MyApp() {
MyAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Welcome to MyApp on ${getPlatformName()}!")
PlatformSpecificGreeting()
OutlinedButton(onClick = { /* Handle button click */ }) {
Text(text = "Click Me")
}
}
}
}
}
Android 模块代码(androidMain/kotlin/com/example/MainActivity.kt)
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.MyApp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
actual fun getPlatformName(): String = "Android"
@Composable
actual fun PlatformSpecificGreeting(): @Composable () -> Unit = {
Text(
text = "Hello from Android!",
style = Typography.body1,
modifier = Modifier.padding(16.dp)
)
}
iOS 模块代码(iosMain/kotlin/com/example/MainViewModel.swift)
import SwiftUI
import shared
struct ContentView: View {
var body: some View {
MyAppKt.MyApp()
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
actual fun getPlatformName(): String = "iOS"
@Composable
actual fun PlatformSpecificGreeting(): @Composable () -> Unit = {
Text(
text = "Hello from iOS!",
style = Typography.body1,
modifier = Modifier.padding(16.dp)
)
}
桌面应用模块代码(desktopMain/kotlin/com/example/Main.kt)
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
MyApp()
}
}
actual fun getPlatformName(): String = "Desktop"
@Composable
actual fun PlatformSpecificGreeting(): @Composable () -> Unit = {
Text(
text = "Hello from Desktop!",
style = Typography.body1,
modifier = Modifier.padding(16.dp)
)
}
@Preview
@Composable
fun MyAppPreview() {
MyApp()
}
Web 模块代码(webMain/kotlin/com/example/Main.kt)
import org.jetbrains.compose.web.render.render
import com.example.MyApp
fun main() {
render {
MyApp()
}
}
actual fun getPlatformName(): String = "Web"
@Composable
actual fun PlatformSpecificGreeting(): @Composable () -> Unit = {
Text(
text = "Hello from Web!",
style = Typography.body1,
modifier = Modifier.padding(16.dp)
)
}
Flutter 登录界面代码(lib/screens/login_screen.dart)
import 'package:flutter/material.dart';
class LoginScreen extends StatelessWidget {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
border: OutlineInputBorder(),
),
obscureText: true,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () {
final email = _emailController.text;
final password = _passwordController.text;
_handleLogin(context, email, password);
},
child: Text('Login'),
),
SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.pushNamed(context, '/register');
},
child: Text('Don't have an account? Register'),
),
],
),
),
);
}
void _handleLogin(BuildContext context, String email, String password) {
if (email.isEmpty || !RegExp(r'^[^@]+@[^@]+.[^@]+').hasMatch(email)) {
_showErrorDialog(context, 'Please enter a valid email address');
return;
}
if (password.length < 8) {
_showErrorDialog(context, 'Password must be at least 8 characters long');
return;
}
// 处理登录逻辑,例如调用 API 或导航到主屏幕
Navigator.pushReplacementNamed(context, '/home');
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
}
Flutter 注册界面代码(lib/screens/register_screen.dart)
import 'package:flutter/material.dart';
class RegisterScreen extends StatelessWidget {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Register'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
border: OutlineInputBorder(),
),
obscureText: true,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () {
final name = _nameController.text;
final email = _emailController.text;
final password = _passwordController.text;
_handleRegister(context, name, email, password);
},
child: Text('Register'),
),
SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Already have an account? Login'),
),
],
),
),
);
}
void _handleRegister(BuildContext context, String name, String email, String password) {
if (name.isEmpty) {
_showErrorDialog(context, 'Please enter your name');
return;
}
if (email.isEmpty || !RegExp(r'^[^@]+@[^@]+.[^@]+').hasMatch(email)) {
_showErrorDialog(context, 'Please enter a valid email address');
return;
}
if (password.length < 8) {
_showErrorDialog(context, 'Password must be at least 8 characters long');
return;
}
// 处理注册逻辑,例如调用 API 或导航到主屏幕
Navigator.pushReplacementNamed(context, '/home');
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
}
















暂无评论内容