优雅使用Windows 10
Kotlin
简介
Kotlin是一种现代但已经成熟的编程语言,它简洁、安全、可与Java和其他语言互操作,并提供了许多在多个平台之间重用代码的方法。它由JetBrains公司于2011年设计和开发,并在2016年正式发布。Kotlin旨在解决Java语言在编码效率和代码质量方面存在的问题,并且与Java语言完全兼容。Kotlin通过简化语法、提供更强大的功能以及减少样板代码的编写,使得开发者能够更高效地编写清晰、简洁而又安全的代码。
Android Studio中创建 Kotlin File文件
- Kotlin Class 文件:用于定义一个类,适合面向对象编程的场景,如定义
Activity
、Fragment
等。- Kotlin File 文件:用于定义顶级函数、变量等,适合函数式编程的场景,如 Jetpack Compose 的 UI 组件定义。
这两者的选择主要取决于你要定义的内容和你所使用的编程范式。在 Jetpack Compose 中,由于其函数式编程的特点,使用
Kotlin File
是更自然的选择。而在传统的 Android 开发中,定义Activity
类时,Kotlin Class
是更合适的。
在目标目录下,右键New
- Kotlin Class/File
,选择Class或者File都可以,Android Studio会根据文件内容自动变更。
Kotlin文件的运行入口是main()
方法:
1 | fun main() { |
标准函数和静态方法
标准函数with、run和apply
Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用。
with函数接收两个参数:任意类型的对象,lambda表达式。在lambda表达式中提供第一个参数对象的上下文,并使用lambda表达式中的最后一行代码作为返回值返回。
1 | val result = with(obj) { |
with函数可以在连续调用同一个对象的多个方法时,让代码更精简。
1 | val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") |
run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。首先run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。
1 | val result = obj.run { |
apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。
1 | // result == obj |
定义静态方法
和绝大多数主流编程语言不同的是,Kotlin却极度弱化了静态方法这个概念,想要在Kotlin中定义一个静态方法反倒不是一件容易的事。
那么Kotlin为什么要这样设计呢?因为Kotlin提供了比静态方法更好用的语法特性,单例类。
1 | object Util { |
虽然这里的doAction()
方法并不是静态方法,但是我们仍然可以使用Util.doAction()
的方式来调用,这就是单例类所带来的便利性。
不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果只是希望让类中的某一个方法变成静态方法的调用方式,可以使用companion object。
1 | class Util { |
companion object这个关键字实际上会在Util类的内部创建一个伴生类,而doAction2()
方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证Util类始终只会存在一个伴生类对象,因此调用Util.doAction2()
方法实际上就是调用了Util类中伴生对象的doAction2()
方法。
Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。
如果我们给单例类或companion object中的方法加上@JvmStatic
注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法,如下所示:
1 | class Util { |
@JvmStatic
注解只能加在单例类或companion object中的方法上。由于doAction2()
方法已经成为了真正的静态方法,那么现在不管是在Kotlin中还是在Java中,都可以使用Util.doAction2()
的写法来调用了。
顶层方法指的是没有定义在任何类中的方法,Kotlin编译器会将所有的顶层方法全部编译成静态方法。想要定义一个顶层方法,首先需要创建一个Kotlin文件。对着任意包名右击 → New → Kotlin File/Class,在弹出的对话框中输入文件名即可,创建类型要选择File。
例如创建一个Helper.kt
文件:
1 | fun doSomething() { |
在Kotlin代码中,所有的顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例,直接输入方法名即可。
但如果是在Java代码中调用,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。Kotlin编译器会自动创建对应的的Java类,doSomething()
方法以静态方法的形式定义在HelperKt
类里。
扩展函数
扩展函数表示,即使不修改某个类的源码,仍然可以打开这个类,向该类添加新的函数。
1 | fun ClassName.methodName(param1: Int, param2: Int): Int { |
定义扩展函数只需要在函数名的前面加上一个ClassName.
的语法结构,就表示将该函数添加到指定类当中。
文件名虽然并没有固定的要求,但是建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于以后查找。当然,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。
例如定义一个函数,用于统计字符串中字母的数量。可以创建一个String.kt
文件:
1 | fun String.lettersCnt(): Int { |
运算符重载
Kotlin允许我们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。运算符重载使用的是operator关键字。
例如让两个Money对象相加,Money.kt
文件:
1 | class Money(val value: Int) { |
这样,就可以将两个Money对象相加,也能直接和数字相加。
1 | val money1 = Money(5) |
高阶函数
如果一个函数接收另一个函数作为参数,或者返回值类型是另一个函数,该函数就称为高阶函数。
函数类型的基本规则:
1 | (String, Int) -> Unit |
->
的左边用来声明函数接收的参数,多个参数之间逗号分隔,如果不接收任何参数,写一对空括号->
的右边用于声明函数的返回值类型,如果没有返回值就使用Unit。
将上述函数类型添加到某个函数的参数声明或者返回值声明上,这个函数就是一个高阶函数:
1 | fun example(func: (String, Int) -> Unit) { |
高阶函数允许让函数类型的参数来决定函数的执行逻辑。Lambda表达式是最常见也是最普遍的高阶函数调用方式。
1 | fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { |
apply函数可以用于给Lambda表达式提供一个指定的上下文,当需要连续调用一个对象的多个方法时,apply函数可以让代码更精简。
1 | fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder { |
这里给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是StringBuilder。
StringBuilder.
:在函数类型的前面加上ClassName.
,表示这个函数类型是定义在哪个类当中。
现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。这里仍然用吃水果这个功能来举例:
1 | fun main() { |
可以看到,build函数的用法和apply函数基本上是一模一样的,只不过我们编写的build函数目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。如果想实现apply函数的这个功能,需要借助于Kotlin的泛型才行。
Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行开销完全消除。内联函数的用法非常简单,只需要在定义高阶函数时加上inline
关键字声明即可。
为什么Kotlin要提供一个noinline
关键字来排除内联功能呢?内联的函数类型参数只允许传递给另一个内联函数,这是它最大的局限性。而非内联的函数类型参数可以自由地传递给其他任何函数。另外,内联函数和非内联函数有一个重要的区别,内联函数引用的lambda表达式可以使用return关键字进行函数返回,非内联函数只能进行局部返回。
Andriod开发
Android Studio
Google将JDK、Android SDK都集成了,Android官方网就可以下载最新的开发工具:下载 Android Studio
创建Android项目
New Project
-Phone and Tablet
-Empty Activity
Minimum SDK
:设置项目最低能兼容的Android版本
Android模拟器
Device Manager
-Create Virtual Device
- 选择目标参数的设备
选择操作系统版本。其余配置全部默认。
点击
Start
按钮,就会启动模拟器。Run 'app'
将项目运行到模拟器上项目成功运行,显示了
Hollo Android
。打开模拟器中的启动器列表,可以看到已经安装了对应的应用。
项目结构
任何一个新建的项目都会默认使用Android模式的项目结构,但这并不是项目真实的目录结构,而是被Android Studio转换过的。这种项目结构简洁明了,适合进行快速开发,但是对于新手来说可能不易于理解。可以切换成项目模式。
.gradle
和.idea
放置Android Studio自动生成的文件,无需关心。
app
项目中的代码、资源等内容。
build
编译时自动生成的文件,无需过多关心。
src
androidTest
:编写Android Test测试用例,可以对项目进行一些自动化测试。main
java
:防止Java和Kotlin代码,默认会自动生成一个MainActivity.kt
文件res
:项目中使用到的图片、布局、字符串等资源。drawable
:图片虽然Android Studio没有帮我们自动生成,但是应该自行创建
drawable-xxhdpi
等目录。最好能够给同一张图片提供几个不同分辨率的版本,分别放在这些目录下,然后程序运行的时候,会自动根据当前运行设备分辨率的高低选择加载哪个目录下的图片。但更多的时候美工只会提供一份图片,这时把所有图片都放在drawable-xxhdpi
目录下就好了,这是最主流的设备分辨率目录。mipmap
:图标values
:字符串、颜色、样式、尺寸xml
:布局、菜单等
test
:编写单元测试用力,对项目进行自动化测试的另一种方式。.gitignore
:将app模块内指定的目录或文件排除在版本控制之外。build.gradle.kts
app模块的gradle构建脚本,通常这个文件中的内容无需修改。
proguard-rules.pro
:指定项目代码的混淆规则。当代码开发完成后打包成安装包文件,如果不希望代码被哦破解,通常会将代码进行混淆。
gradle
包含gradle wrapper的配置文件,使用gradle wrapper的方式,不需要提前把gradle下载好,而是根据本地的缓存情况,自动决定是否联网下载。
libs.versions.toml
:定义各个依赖库的版本号,使得依赖管理更加清晰和一致。
.gitignore
记录git版本控制之外的文件、目录。
build.gradle.kts
项目全局的gradle构建脚本,通常这个文件中的内容无需修改。
gradle.properties
项目全局的gradle配置文件。
gradlew
和gradlew.bat
用于在命令行中执行gradle命令。
local.properties
指定本机中的Andriod SDK路径,自动生成。
settings.gradle.kts
指定项目中所有引入的模块。通常情况下都是自动引入。
AndroidManifest.xml
app\src\main\AndroidManifest.xml
中有如下代码:
1 | <!-- 注册MainActivity,没有注册的Activity是不能使用的 --> |
MainActivity
继承自ComponentActivity
,ComponentActivity
继承自Activity
,**Activity
是一个提供用户界面的组件**。
1 | class MainActivity : ComponentActivity() { |
项目中的资源使用
app\src\main\res\values\strings.xml
1
2
3<resources>
<string name="app_name">HelloWorld</string>
</resources>这里定义了一个应用程序名的字符串,有以下两种方式可以用来引用:
- 在代码中使用通过
R.string.app_name
。 - 在XML中使用
@string/app_name
。
其中
string
可以被替换,如果是引用的图片资源就可以替换成drawable
,如果是引用的应用图标就可以替换成mipmap
,如果是引用的布局文件就可以替换成xml
,以此类推。- 在代码中使用通过
app目录下的 build.gradle.kts
Android Studio采用Gradle构建项目。它使用了一种领域特定语言DSL进行项目布置,摒弃了如Maven等基于XML文件的各种繁琐配置。
1 | // 该项目使用的Gradle插件,通过 alias 方法引入,这意味着它们的确切 ID 和版本号在 libs.versions.toml 文件中定义。 |
settings.gradle.kts
1 | // gradle插件管理 |
日志工具的使用
Android中的日志工具类是android.util.Log
,它提供了5个打印日志的方法:
Log.v()
:verbose,打印那些最琐碎、意义最小的日志信息。是Android中最低的日志级别。Log.d()
:debug,打印一些调试信息。Log.i()
:info,打印一些比较重要的数据,可以帮助分析用户行为。Log.w()
:warn。Log.e()
:error。
例如在MainActivity.kt
的onCreate()
方法中添加一行日志:
1 | class MainActivity : ComponentActivity() { |
运行程序,在Android Studio底部工具栏的Logcat
中就能看到打印信息。
Activity
Activity是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个Activity,但不包含任何Activity的应用程序很少见。
基本用法
新建一个Android项目,选择No Activity
,其余选项和之前章节中创建Android项目-Empty Activity保持一致。等待Gradle构建完成后,项目就创建成功了。
手动创建Activity
Compose 下的 Empty Activity: 使用 Jetpack Compose 的声明式 UI 编程,UI 是通过 Kotlin 代码直接编写的,没有 XML 文件。
Activity 下的 Empty Views Activity: 使用传统的视图系统,UI 是通过 XML 文件定义的,然后在 Activity 中引用这些 XML 布局。
适用场景:
- Compose 下的 Activity: 适合希望使用现代 UI 工具包 Jetpack Compose 开发的项目,特别适合新项目或者希望采用最新技术的项目。
- 传统 Views Activity: 适合已有的项目或需要兼容传统视图系统的项目,或者开发者更熟悉传统的视图系统。
学习曲线:
- Jetpack Compose: 更现代,但需要学习新的编程范式(声明式 UI)。
- 传统视图系统: 更传统,许多开发者已经熟悉。
项目创建成功后,仍然会默认使用Android模式的项目结构,手动改成Project模式。
传统的视图系统:Activity 下的 Empty Views Activity
右键选择要创建Activity的包,New
- Activity
- Empty Views Activity
,会弹出一个创建Activity的对话框。
不勾选Generate a Layout File
,创建完成后,可以看到生成的是一个Kotlin文件:
1 | class FirstActivity : AppCompatActivity() { |
声明式 UI 编程:Compose 下的 Empty Activity
右键选择要创建Activity的包,New
- Compose
- Empty Activity
,会弹出一个创建Activity的对话框。
不勾选Launcher Activity
,否则会将创建的Activity设置为当前项目的主Activity。
创建完成后,可以看到生成的是一个Kotlin文件:
1 | class TestActivity : ComponentActivity() { |
创建和加载布局
传统的视图系统
Android程序的设计讲究逻辑和视图分离,最好每个Activity都能对应一个布局,布局用来显示界面内容。
右击app/src/main/res
目录,New
-> Directory
,会弹出一个新建目录的窗口,创建一个layout目录。再对着layout
目录右键,New
-> Layout Resource File
,将布局文件命名为first_layout
,根元素默认选择为ConstraintLayout
,改用LinearLayout
。
创建出来的xml文件,默认代码为:
1 | <?xml version="1.0" encoding="utf-8"?> |
通过<Button>
标签,添加一个按钮:
1 | <?xml version="1.0" encoding="utf-8"?> |
android:id
:给当前元素定义一个唯一的标识符,之后可以在代码中对其进行操作。如果需要在XML中引用一个id,使用
@id/id_name
语法,定义id的时候,需要使用@+id/id_name
。android:layout_width
指定当前宽度。match_parent
表示和父元素一样宽。android:layout_height="wrap_content"
指定当前高度,
wrap_content
表示当前元素的高度能刚好包含里面的内容就行。android:text
指定元素中显示的文字内容。
一个简单的布局就编写完成了,然后在Activity中加载该布局:
1 | class FirstActivity : AppCompatActivity() { |
项目中添加的任何资源,都会在R文件中生成一个相应的资源id。
声明式 UI 编程
当你使用 Jetpack Compose 时,布局不再通过传统的 XML 文件来定义,而是使用 Kotlin 代码进行声明式编程。Jetpack Compose 是一种现代的 Android UI 工具包,允许你以更直观和可组合的方式构建用户界面。
在 Compose 中,UI 通过 @Composable
注释的函数来定义。每个 @Composable
函数都是一个 UI 组件。
1 | import androidx.compose.material.Text |
可以使用 @Preview
注解来预览 Composable 函数的效果,而无需运行整个应用。
1 | @Preview(showBackground = true) |
AndroidManifest.xml
所有的Activity都要在AndroidManifest.xml
中进行注册才能生效。一般情况下,创建的Activity都会被自动注册进来。
1 | <?xml version="1.0" encoding="utf-8"?> |
android:name
用于指定具体注册哪一个Activity。.FirstActivity
是相对引用。
不过,仅仅是这样注册了Activity,我们的程序仍然不能运行,因为还没有为程序配置主Activity。也就是说,程序运行起来的时候,不知道要首先启动哪个Activity。
1 | <activity |
添加<intent-filter>
标签并在其中添加<action android:name="android.intent.action.MAIN" />
和<category android:name="android.intent.category.LAUNCHER" />
声明即可。
此外,可以使用android:label
指定Activity中标题栏的内容。主Activity指定的label不仅会称为标题栏中的内容,还会成为Launcher中应用程序的名称。
使用Toast
Toast是Android提供的一种很好的提醒方式,可以将一些短小的信息通知给用户,并在一段时间后自动消失,不占用屏幕空间。
1 | class FirstActivity : AppCompatActivity() { |
如果布局文件中的组件很多,使用findViewById
就很繁琐,可以通过View Binding来代替,View Binding可以生成代码来直接访问视图,无需使用findViewById
。
要使用View Binding,首先需要在build.gradle.kts
中添加如下内容:
1 | android { |
然后在代码中使用View Binding代替findViewById
。
1 | override fun onCreate(savedInstanceState: Bundle?) { |
使用Menu
首先在res目录下新建一个menu文件夹,右击res目录→New→Directory,输入文件夹名“menu”,点击“OK”。接着在这个文件夹下新建一个名叫“main”的菜单文件,右击menu文件夹→New→Menu resource file。
在main.xml中添加如下代码:
1 | <?xml version="1.0" encoding="utf-8"?> |
回到FirstActivity中来重写onCreateOptionsMenu()
方法,重写方法可以使用Ctrl + O
快捷键:
1 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { |
销毁一个Activity
只要按一下Back键就可以销毁当前的Activity。不过,如果你不想通过
按键的方式,而是希望在程序中通过代码来销毁Activity,当然也可以,Activity类提供了一个finish()
方法,我们只需要调用一下这个方法就可以销毁当前的Activity。
1 | // 为按钮注册点击监听器 |
使用Intent在Activity之间穿梭
Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可用于启动Activity、启动Service以及发送广播等场景。
使用显式Intent
右键选择要创建Activity的包,New
- Activity
- Empty Views Activity
,会弹出一个创建Activity的对话框,不勾选Generate a Layout File
,创建SecondActivity
。
对着layout
目录右键,New
-> Layout Resource File
,将布局文件命名为second_layout
:
1 | <?xml version="1.0" encoding="utf-8"?> |
相应地,重写SecondActivity
,应用该布局:
1 | override fun onCreate(savedInstanceState: Bundle?) { |
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)
:
- Context要求提供一个启动Activity的上下文。
- Class用于指定想要启动的目标Activity
Activity类提供了一个startActivity()
方法,专门用于启动Activity。
重写FirstActivity中的按钮监听代码:
1 | // 为按钮注册点击监听器 |
就能实现在FirstActivity的基础上打开SecondActivity,按一下Back键就可以销毁当前Activity,从而回到上一个Activity。
使用隐式Intent
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出想要启动哪一个Activity,而是指定一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的Activity去启动。
通过在AndroidManifest.xml
中的<activity>
标签下配置<intent-filter>
内容,可以指定当前Activity能够响应的action和category:
1 | <activity |
相应地,修改FirstActivity.kt
中的按钮点击事件:
1 | // 为按钮注册点击监听器 |
隐式Intent的更多用法
使用隐式Intent,不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity,这就使多个应用程序之间的功能共享成为了可能。
调用浏览器
比如你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),只需要调用系统的浏览器来打开这个网页就行了。
1 | // 为按钮注册点击监听器 |
同时,可以在AndroidManifest.xml
中的activity标签下的<intent-filter>
标签中配置<data>
标签:
android:scheme
:指定数据的协议部分,如httpsandroid:host
:指定数据的主机名部分,如www.bing.com
android:port
:指定数据的端口部分android:path
:指定主机名和端口之后的部分android:mimeType
:指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有<data>
标签中指定的内容和Intent中携带的Data完全一致时,当前Activity才能够响应该Intent。
调用拨号界面
1 | // 为按钮注册点击监听器 |
向下一个Activity传递数据
在启动Activity时传递数据的思路很简单,Intent中提供了一系列putExtra()
方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了。
例如在FirstActivity中:
1 | // 为按钮注册点击监听器 |
在SecondActivity中获取传递的数据:
1 | val stringExtraData = intent.getStringExtra("extra_data") |
返回数据给上一个Activity
返回上一个Activity只需要按一下Back键就可以了,没有一个用于启动Activity的Intent来传递数据。可以通过registerForActivityResult()
注册对Activity结果的处理。
在FirstActivity做如下改动:
1 | class FirstActivity : AppCompatActivity() { |
在SecondActivity中:
1 | class SecondActivity : AppCompatActivity() { |
Activity的生命周期
返回栈
Android中的Activity是可以层叠的。我们每启动一个新的Activity,就会覆盖在原Activity之上,然后点击Back键会销毁最上面的Activity,下面的一个Activity就会重新显示出来。
Android使用任务(task)来管理Activity,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。
Activity状态
每个Activity在生命周期中,最多可能有4种状态。
运行状态
暂停状态
当一个Activity不再处于栈顶,但仍然可见时(并不是每个Activity都会占满整个屏幕,比如对话框),就进入了暂停状态。只有在内存极低的情况下,系统才会考虑回收这种Activity。
停止状态
当一个Activity不再处于栈顶,并且完全不可见,就进入了停止状态。其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
销毁状态
Activity从栈中移除后,就变成了销毁状态。系统最倾向于回收这种状态的Activity,以保证手机的内存充足。
Activity的生存期
Activity类中定义了7个回调方法,覆盖了Activity生命周期的每个环节。
onCreate()
:在Activity被创建的时候调用。应该在该方法中完成初始化操作,比如加载布局、绑定事件等。onStart()
:在Activity由不可见变为可见时调用。onResume()
:在Activity准备好和用户交互时调用。此时Activity一定位于返回栈的栈顶,并处于运行状态。onPause()
:在系统准备去启动或恢复另一个Activity时调用。通常在这个方法中,将一些消耗CPU的资源放掉、保存一些关键数据。但执行速度一定要快,避免影响新的栈顶Activity使用。onStop()
:在Activity完全不可见时调用。和onPause()
方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()
方法会得到执行,onStop()
方法并不会执行。onDestroy()
:在Activity被销毁之前调用,之后Activity的状态变为销毁状态。onRestart()
:在Activity由停止变为运行状态之前调用。
上述7个方法,除了onRestart()
之外,都是两两相对,从而可以将Activity分为以下3种生存期:
- 完整生存期。Activity在
onCreate()
和onDestroy()
方法之间所经历的就是完整生存期。 - 可见生存期。Activity在
onStart()
和onStop()
之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即使可能无法和用户进行交互。可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()
方法中对资源进行加载,而在onStop()
方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。 - 前台生存期。Activity在
onResume()
方法和onPause()
方法之间所经历的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的。
新建一个项目ActivityLifeCycleTest
为例子,New Project -> Empty Views Activity
。
会默认生成MainActivity
和activity_main.xml
。继续创建两个子Activity:NormalActivity
和DialogActivity
。同样地,以Empty Views Activity
形式创建,不勾选Generate a Layout File
。之后在res/layout
目录下,手动创建normal_layout.xml
和dialog_layout.xml
两个布局文件:
1 | <?xml version="1.0" encoding="utf-8"?> |
1 | <?xml version="1.0" encoding="utf-8"?> |
相应地,NormalActivity
和DialogActivity
中添加对应的布局:
1 | class NormalActivity : AppCompatActivity() { |
为了将DialogActivity设置成对话框的样式,需要在AndroidManifest.xml
中配置<activity>
标签:
1 | <!-- android:theme属性用于指定主题 --> |
接着,修改activity_main.xml
:
1 | <?xml version="1.0" encoding="utf-8"?> |
最后,修改MainActivity
中的代码:
1 | class MainActivity : AppCompatActivity() { |
在Activity的7个回调方法中分别打印了一句话,这样就可以通过观察日志来更直观地理解Activity的生命周期。
Activity被回收了怎么办
当一个Activity进入了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次。
但是Activity A中是可能存在临时数据和状态的。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。
Activity中还提供了一个onSaveInstanceState()
回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。onSaveInstanceState()
方法会携带一个Bundle
类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()
方法保存字符串,使用putInt()
方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取
值,第二个参数是真正要保存的内容。
1 | override fun onSaveInstanceState(outState: Bundle) { |
我们一直使用的onCreate()
方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在Activity被系统回收之前,通过onSaveInstanceState()
方法保存数据,这个参数就会带有保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可,比如将文本内容重新赋值到文本输入框上。
1 | override fun onCreate(savedInstanceState: Bundle?) { |
Intent还可以结合Bundle一起用于传递数据。首先我们可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标Activity之后,先从Intent中取出Bundle,再从Bundle中一一取出数据。
另外,当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程,虽然这个问题同样可以通过onSaveInstanceState()方法来解决,但是一般不太建议这么做,因为对于横竖屏旋转的情况,有更加优雅的解决方
案,后续介绍。
Activity的启动模式
启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>
标签指定android:launchMode
属性来选择启动模式。
standard
standard是Activity默认的启动模式。在standard模式下,每当启动一个新的Activity,就会在返回栈中入栈,并处于栈顶的位置。系统不在乎Activity是否已经在返回栈中,每次启动都会创建一个Activity的新实例。
singleTop
当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例。当返回栈的栈顶并不是该Activity时,再次启动该Activity还是会创建新实例。
singleTask
使用singleTop模式,如果Activity没有处于栈顶位置,还是可能创建多个Activity实例。而使用singleTask,每次启用该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果存在则直接使用,并把该Activity之上的所有其他Activity都出栈,如果没有发现就创建新的Activity实例。
singleInstance
不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。
假设程序中有一个Activity允许其他程序调用,如果想实现其他程序和当前程序共享这个Activity的实例,就可以使用singleInstance模式解决这个问题,不管哪个应用程序来访问这个Activity,都共用一个返回栈,解决了共享Activity实例的问题。
Activity的实践技巧
知晓当前是在哪一个Activity
创建一个BaseActivity类,由于不需要让BaseActivity在AndroidManifest.xml中注册,所以创建一个普通的Kotlin类即可。然后让BaseActivity继承AppCompatActivity,并重写onCreate()
方法:
1 | open class BaseActivity : AppCompatActivity() { |
再将FirstActivity、SecondActivity和ThirdActivity由继承AppCompatActivity改为继承BaseActivity。这样,每当进入一个Activity界面时,其类名就会被打印出来,就能知道当前所在的Activity。
随时退出程序
只需要一个专门的集合对所有的Activity进行管理。
新建一个单例类ActivityCollector作为Activity的集合:
1 | /** |
在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的Activity添加到集合里。然后在BaseActivity中重写onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明从集合里移除一个马上要销毁的Activity。
1 | open class BaseActivity : AppCompatActivity() { |
不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()
方法就可以了。例如在ThirdActivity界面想通过点击按钮直接退出程序,只需将代码改成如下形式:
1 | class ThirdActivity : BaseActivity() { |
启动Activity的最佳写法
启动Activity的方法相信你已经非常熟悉了,首先通过Intent构建出当前的“意图”,然后调用startActivity()
或startActivityForResult()
方法将Activity启动起来,如果有数据需要在Activity之间传递,也可以借助Intent来完成。
假设SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必须传递过来,那么我们很容易会写出如下代码:
1 | val intent = Intent(this, SecondActivity::class.java) |
虽然这样写是完全正确的,但是在真正的项目开发中经常会出现对接的问题。比如SecondActivity并不是由你开发的,但现在你负责开发的部分需要启动SecondActivity,而你却不清楚启动SecondActivity需要传递哪些数据。这时无非就有两个办法:一个是你自己去阅读SecondActivity中的代码,另一个是询问负责编写SecondActivity的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决上面的窘境。
1 | class SecondActivity : BaseActivity() { |
这样写,可以非常清晰地知道启动SecondActivity需要传递哪些数据。同时,还能简化启动Activity的代码:
1 | button1.setOnClickListener { |
UI开发
常用控件的使用
创建一个名为UIWidgetTest
的Empty Views Activity项目。
TextView
TextView用于在界面显示文本信息。修改默认创建的activity_main.xml
:
1 | <?xml version="1.0" encoding="utf-8"?> |
android:gravity
:TextView中的文字默认居左上角对齐,通过android:gravity
显式指定对齐方式:top、bottom、start、end、center等。可以用“|”来同时指定多个值,这里我们指定的是”center”,效果等同于”center_vertical|center_horizontal”,表示文字在垂直和水平方向都居中对齐。android:textColor="#00ff00"
:指定文字颜色android:textSize="24sp"
:指定文字大小,要使用sp作为单位(这样,当用户在系统中修改了文字显式尺寸时,应用程序中的文字大小才会跟着变化)
Button
1 | <Button |
Android系统默认会将按钮上的英文字母全部转换成大写。可以在XML中添加android:textAllCaps="false"
这个属性,这样系统就会保留原始文字内容。
接着可以为Button的点击事件注册一个监听器:
1 | override fun onCreate(savedInstanceState: Bundle?) { |
这里调用button的setOnClickListener()
方法时利用了Java单抽象方法接口的特性,从而可以使用函数式API的写法来监听按钮的点击事件。这样每当点击按钮时,就会执行Lambda表达式中的代码,只需要在Lambda表达式中添加待实现的逻辑。
除了使用函数式API的方式来注册监听器,也可以使用接口的方式注册监听器:
1 | class MainActivity : AppCompatActivity(), View.OnClickListener { |
让MainActivity实现View.OnClickListener
接口,并重写了onClick()
方法,然后在调用button的setOnClickListener()方法时将MainActivity的实例传进去。这样每当点击按钮时,就会执行onClick()方法中的代码了。
EditText
EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。
1 | <EditText |
android:hint
属性用于设定提示性的文本。当我们输入任何内容时,这段文本就会自动消失。
随着输入的内容不断增多,EditText会被不断地拉长。这是由于EditText的高度指定的是wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得非常难看。可以使用android:maxLines
属性来解决这个问题。这里通过android:maxLines指定了EditText的最大行数为两行,这样当输入的内容超过2行时,文本就会向上滚动,EditText则不会再继续拉伸。
还可以结合使用EditText与Button来完成一些功能,比如通过点击按钮获取EditText中输入的内容:
1 | class MainActivity : AppCompatActivity(), View.OnClickListener { |
ImageView
ImageView是用于在界面上展示图片的一个控件,它可以让程序界面更丰富多彩。
图片通常放在以drawable开头的目录下,并且要带上具体的分辨率。现在最主流的手机屏幕分辨率大多是xxhdpi的,所以我们在res目录下再新建一个drawable-xxhdpi
目录:右键 -> New -> Android Resource Directory。
再准备好两张图片,放入该目录下,注意图片的命名要符合Android规范:只能包含小写字母、数字、下划线和点号,并且不能以数字开头。
如果放入图片后,该图片被Android Studio标红,尝试重启Android Studio: File -> Invalidate Caches / Restart
,然后选择 Invalidate and Restart
。之后,就能通过<ImageView>
标签往应用中插入图片。
1 | <ImageView |
还可以在程序中通过代码动态地更改ImageView中的图片:
1 | class MainActivity : AppCompatActivity(), View.OnClickListener { |
ProgressBar
ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。
1 | <ProgressBar |
如何才能让进度条在数据加载完成时消失呢?所有的Android控件都具有可见属性,可以通过android:visibility
进行指定,可选值有3种:visible、invisible和gone。
- visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。
- invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
- gone则表示控件不仅不可见,而且不再占用任何屏幕空间。
也可以通过代码来设置控件的可见性,setVisibility()
方法,允许传入View.VISIBLE
、View.INVISIBLE
和View.GONE
这三种值。
1 | class MainActivity : AppCompatActivity(), View.OnClickListener { |
还可以通过style
指定不同的进度条样式、通过android:max
设置进度条的最大值:
1 | <ProgressBar |
在代码中可以动态地更改进度条的进度:
1 | class MainActivity : AppCompatActivity(), View.OnClickListener { |
AlertDialog
AlertDialog可以在当前界面弹出一个对话框,这个对话框置顶于所有界面元素之上,能够屏蔽其他控件的交互能力,因此AlertDialog一般用于提示一些非常重要的内容或者警告信息。
比如为了防止用户误删重要内容,在删除前弹出一个确认对话框:
1 | class MainActivity : AppCompatActivity(), View.OnClickListener { |
基本布局
一个丰富的界面是由很多个控件组成的,借助布局能让各个控件都有条不紊地摆放在界面上。布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,就能完成一些比较复杂的界面实现。
新建一个Empty Views Activity的UILayoutTest项目,并让Android Studio自动帮我们创建好Activity,Activity名和布局名都使用默认值。
LinearLayout
LinearLayout,线性布局,这个布局会将它所包含的控件在线性方向上依次排列。通过android:orientation
属性指定排列方向为vertical
则垂直排列,指定的是horizontal
,控件就在水平方向上排列。
android:layout_gravity
:指定控件在布局中的对齐方式。当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度不固定,每添加一个控件,水平方向上的长度都会改变,因此无法指定该方向上的对齐方式,vertical时同理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<?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="horizontal">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button 2" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button 3" />
</LinearLayout>android:layout_weight
:允许使用比例的方式指定控件大小,在手机屏幕的适配性方面可以起到非常重要的作用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something" />
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send" />
</LinearLayout>由于使用了
android:layout_weight
属性,此时控件的宽度
不再由android:layout_width
来决定,指定成0 dp
是一种比较规范的写法。在EditText和Button里将android:layout_weight属性的值都指定为1,表示EditText和Button将在水平方向平分宽度(系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值)。还可以通过指定部分控件的layout_weight值来实现更好的效果:
1
2
3
4
5
6
7
8
9
10
11
12<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something" />
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send" />仅指定了EditText的android:layout_weight属性,并将Button的宽度改回了wrap_content。这表示Button的宽度仍然按照wrap_content来计算,而EditText则会占满屏幕所有的剩余空间。使用这种方式编写的界面,不仅可以适配各种屏幕,而且看起来也更加舒服。
RelativeLayout
RelativeLayout,相对布局,它可以通过相对定位的方式让控件出现在布局的任何位置。也正因为如此,RelativeLayout中的属性非常多。
1 | <?xml version="1.0" encoding="utf-8"?> |
效果如下:
上面例子中的每个控件都是相对于父布局进行定位,如果要相对于控件进行定位,则如下所示:
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
android:layout_above
属性可以让一个控件位于另一个控件的上方,需要为这个属性指定相对控件id的引用,其他属性也是相似。当一个控件去引用另一个控件的id时,该控件一定要定义在被引用控件的后面,不然会出现找不到id的情况。
效果如下:
RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout_alignStart
表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignEnd
表示让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有android:layout_alignTop
和android:layout_alignBottom
,道理都是一样的。
FrameLayout
FrameLayout,帧布局,它相比于前面两种布局简单很多,因此它的应用场景少了很多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角。
1 | <?xml version="1.0" encoding="utf-8"?> |
可以看到,文字和按钮都位于布局的左上角。由于Button是在TextView之后添加的,因此按钮压在了文字的上面。除了这种默认效果之外,还可以使用layout_gravity
属性来指定控件在布局中的对齐方式,这和LinearLayout中的用法是相似的。
1 | <?xml version="1.0" encoding="utf-8"?> |
TextView在FrameLayout中居左对齐,Button在FrameLayout中居右对齐:
总体来讲,由于定位方式的欠缺,FrameLayout的应用场景相对偏少一些。
自定义控件
常用控件和布局的继承结构:
可以看到,所有控件都是直接或间接继承自View,所有布局都是直接或间接继承自ViewGroup。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件。因此,我们使用的各种控件其实就是在View的基础上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。
当系统自带的控件并不能满足我们的需求时,可以利用上面的继承结构来创建自定义控件。新创建一个Empty Views Activity的UICustomViews项目。
引入布局
创建一个包含左右两个按钮用于返回、编辑操作的标题栏布局。一般我们的程序中可能有很多个Activity需要这样的标题栏,如果在每个Activity的布局中都编写一遍同样的标题栏代码,会导致代码冗余。可以通过引入布局的方式提取共同代码。
先去src/main/res/values/themes.xml
,将默认的主题Theme.Material3.DayNight.NoActionBar
改为Theme.AppCompat.Light.NoActionBar
,否则后续的布局会有部分内容被主题的配置所覆盖:
1 | <resources xmlns:tools="http://schemas.android.com/tools"> |
在res目录下,新建一个drawable-xxhdpi
目录,将title_bg.png
、back_bg.png
和edit_bg.png
(资源下载地址见
前言)(第一行代码——Android(第3版) (ituring.com.cn)界面的随书下载中的随书资源.zip下载后,就能得到相应的资源)放入,分别用于作为标题栏、返回按钮和编辑按钮的背景。
在layout目录下,新建一个title.xml
布局:
1 | <?xml version="1.0" encoding="utf-8"?> |
android:layout_margin
这个属性,它可以指定控件在上下左右方向上的间距。当然也可以使用android:layout_marginStart
或android:layout_marginTop
等属性来单独指定控件在某个方向上的间距。
然后,通过在activity_main.xml
中使用<include>
标签引入布局,就能使用上该标题栏:
1 | <?xml version="1.0" encoding="utf-8"?> |
创建自定义控件
如果布局中有一些控件要求能够响应事件,还是需要在每个Activity中为这些控件单独编写一次事件注册的代码。比如标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,也会导致代码冗余,这种情况最好是使用自定义控件的方式来解决。
创建TitileLayout继承LinearLayout,成为自定义的标题栏控件。
1 | class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { |
在TitleLayout的主构造函数中声明了Context
和AttributeSet
这两个参数,在布局中引入TitleLayout
控件时就会调用这个构造函数。然后在init结构体中需要对标题栏布局进行动态加载,这就要借助LayoutInflater
来实现。
现在自定义控件已经创建好了,接下来我们需要在布局文件中添加这个自定义控件,修改activity_main.xml
中的代码:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
重新运行程序,你会发现此时的效果和使用引入布局方式的效果是一样的。
最常用且最难用的控件:ListView
由于手机屏幕空间比较有限,能够一次性在屏幕上显示的内容并不多,当程序中有大量的数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕。
ListView的简单用法
新建一个名为ListViewTest的Empty Views Activity项目,修改activity_main.xml,添加ListView控件:
1 | <?xml version="1.0" encoding="utf-8"?> |
再修改MainActivity:
1 | class MainActivity : AppCompatActivity() { |
即可通过滚动的方式查看屏幕外的数据。
定制ListView界面
Jetpack Compose
Jetpack Compose 是一种声明式 UI 工具包,与传统的基于 XML 的布局系统不同,它允许开发者使用 Kotlin 代码直接构建 UI。
Android UI Toolkit
在代码中,布局由ViewGroup
对象表示,它是控制子节点在屏幕上的位置和行为的容器。小部件由View
对象表示,它显示单个UI组件,比如按钮和文本框。
View
表示 UI 组件的基本构建块。它在屏幕上占据一个矩形区域,在其中绘制特定的 UI 组件,如按钮或文本字段。**View
也支持交互和事件。专用视图通常会公开一组用于管理交互式事件的特定事件监听器**。
Composable 函数
使用XML构建UI的问题:
- UI不具备可扩展性
- 难以创建自定义视图
- 状态的所有权通常分散在多个所有者之间。
所有这些问题的根本原因在于 Android View 构建其状态和绘制自身及其子类的方式。为了避免这些问题,你需要重新开始,使用不同的基本构建块。在 Jetpack Compose 中,这个构建块被称为composable 函数。
具有@Composable
注解的函数,几乎就是创建新的小部件所需的全部内容。在Compose的世界里,这些小部件称为Composables
。
1 | @Composable |
注解类通过向代码附加元数据来简化代码。通过使用注解,可以向类添加行为,并生成有用的代码,无需编写大量样板文件。
在Compose中,调用在屏幕上显示某些内容的函数称为发出UI。要发出消息,就需要调用Text
函数。Text
也是一个Composable函数,它是构成 Jetpack Compose 的默认Composable函数之一。需要注意的一点是,Composable函数只能从其他Composable函数中调用——如果你尝试删除@Composable,你会收到一个错误,阻止你使用 Text()
。
Composable
注解类有三个自己的注解:
1 | @MustBeDocumented |
@MustBeDocumented
:表示注解是公共API的一部分,应该包含在生成的文档中。Retention
:告诉编译器注解应该存在多长事件。使用AnnotationRetention.BINARY
,处理器将在编译过程中,将代码存储在二进制文件中。@Target
:描述应用类型的上下文。@Composable
可以应用于类型、参数、函数和属性。
显示Composables
可以使用Activity或Fragment作为起点。
1 | class MainActivity : AppCompatActivity() { |
可以使用**content
块**将composable
和activity
连接。无需使用XML文件定义布局内容,只需要调用composable
函数即可。
1 | public fun ComponentActivity.setContent( |
可以看到,setContent()
是ComponentActivity
的扩展函数。扩展函数能在不更改源代码的情况下向类添加其他功能。这意味着可以在任何ComponentActivity
及其子类上使用setContent()
。
调用setContent()
会将名为content
的composable 函数设置为根视图,可以看作一个容器,可以在其中添加任意数量的元素,在此容器内调用其余的composable函数。
基本的Composable函数
Text
1 | package androidx.compose.material |
例如创建一个如下的Composable函数:
1 | @Composable |
预览
在Composable函数上使用@Preview
注解即可,不通过运行应用程序就能看到修改效果。
使用预览,Composable函数需要满足如下任一条件:
- 没有参数。
- 所有参数都有默认参数。
- 提供一个
@PreviewParameter
以及一个特殊工厂,提供想要在UI上绘制的参数。
TextField
1 | package androidx.compose.material |
TextField是用于输入字段的组件。TextField允许用户输入文本,并且如果TextField重新组合,输入的文本也不能消失或变更。要使TextField正常工作,必须提供一个重构期间不会更改的值,即状态值。使用mutableStateOf()
函数,可以将一个空String包装到一个状态容器中,将使用它来存储和显示输入字段中的文本。
例如:
1 | @Composable |
remember()
能在改变状态时,保留设置的内容。如果不使用remember()
,在改变状态(每次用户点击键盘上的键,内部状态都会发生变化)时,会丢失设置,并使用默认值(空字符串)。
使用 OutlinedTextField 添加Email字段
OutlinedTextField 是一个样式化的 TextField,它使用一个特殊的内部函数来绘制和动画化字段周围的边框和描述文本。
要添加提示或 Compose 中已知的标签,使用 label 属性并传入另一个composable函数。
1 | @Composable |
效果:
Button
1 | package androidx.compose.material |
1 | @Composable |
RadioButton
目前,Jetpack Compose 没有单选组的实现,必须自己创建自定义组。
1 | @Composable |
1 | @Composable |
FloatingActionButton
浮动操作按钮之所以被称为“浮动”,是因为它们具有更高的高度,使其位于所有内容之上。它们被用来将应用程序的主要操作放在用户触手可及的地方。
1 | @OptIn(ExperimentalMaterialApi::class) |
1 | @Composable |
大多数情况下,需要对content
添加Icon()
。
1 | @Composable |
Icon的主要特点是允许设置ImageVector
类型的向量用作图标。
其他Button
- IconButton:类似FloatingActionButton,但它没有浮动。通常用于导航。
- OutlinedButton:与OutlinedTextField类似,提供额外的功能,如边框。
- IconToggleButton:具有两种图标状态,可以打开和关闭它们。
- TextButton:最常见于卡片和对话框中,可以执行不太明显的操作。
Progress Bars
当执行长时间的操作(例如从服务器或数据库获取数据)时,最好显示进度条。进度条通过显示动画来减少等待时间过长的感觉,并让用户感觉到正在发生某些事情。
CircularProgressIndicator
1 | @Composable |
如果您不设置进度progress,进度条将运行无限旋转动画。
LinearProgressIndicator
1 | @Composable |
示例:
1 | @Composable |
AlertDialog
对话框用于提醒用户有关操作的信息,或请求确认。例如,您可以使用对话框来确认用户是否要删除某个项目、请求他们对应用程序进行评分等。使用对话框最重要的部分是处理确定何时显示或关闭该对话框的状态。
1 | @Composable |
onDismissRequest
:回调函数,当对话框被请求关闭时执行(点击对话框外部、按下返回键)confirmButton
:确认操作dismissButton
:关闭操作
Layout 布局
Linear Layouts
LinearLayout 的特征是将其子项定位在线性流中。此流称为方向,可以是水平或垂直的。在 Jetpack Compose 中,有两个不同的可组合函数可以替换 LinearLayout,每个方向一个。
Row 行
1 | @Composable |
- horizontal Arrangement包括:
SpaceBetween
:为每个子对象放置相等的间距,不计算第一个子对象之前或最后一个子对象之后的间距。SpaceEvenly
:为每个子对象放置相等的间距,包括起始和结束间距。SpaceAround
:和SpaceEvenly
一样放置子项,但连续子项之间的间距减少一半。Center
、Start
、End
:将子项置于中心、开头、末尾。子项之间没有空格。
- vertical Alignment包括:
Top
:将子项与父项的顶部对齐。CenterVertically
:将子项垂直对齐父项的中心。Bottom
:将子项与父项的底部对齐。
在 Row 中定位子项的最后一种方法是使用权重。要添加权重,需要使用特殊方式从 Compose 访问 weight()
修饰符。
1 | @Composable |
Column 列
1 | @Composable |
Box
Box用于相对于父组件的边缘显示子组件,并允许叠放子组件。当需要在特定位置显示元素或希望显示重叠元素(如对话框)时,非常有用。
1 | @Composable |
contentAlignment
:用于修改子组件的外观和行为。如果想在每个子项之间有不同的Alignment,需要在子项上使用Modifier.align()
设置Alignment。- TopStart
- TopCenter
- TopEnd
- CenterStart
- Center
- CenterEnd
- BottomStart
- BottomCenter
- BottomEnd
propagateMinConstraints
:是否传递最小约束并将其用于内容。
1 | @Composable |
当 Box 中有多个子项时,它们的渲染顺序与你将它们放置在 Box 中的顺序相同。
Surface
Surface 的独特之处在于它一次只能容纳一个子对象,但它为子对象的内容提供了许多样式处理,例如高度、边框等。
1 | @Composable |
使用 Surface 的最常见方式是作为组件的根布局。由于它只能容纳一个 child,因此该 child 通常是定位其余元素的另一个布局。**Surface()
不处理定位,它的子项处理**。
1 | @Composable |
有一种名为 Card 的常用自定义 Surface 实现。一个Card有完全相同的五个用途,只能容纳一个子组件。Card 和 Surface 之间的唯一区别是其默认参数。Card 具有预定义的标高,并使用带圆角的 Material 主题形状。
Scaffold
可以使用Scaffold来实现一个可视化布局,该布局遵循标准 Material Design 结构。它结合了几种不同的组件来构建整个屏幕。
1 | @Composable |
构建Lists
Scrolling Modifier
在Jetpack Compose中,可以将Column和支持滚动的modifier一起使用。
实现一个简单的滚动Column:
1 | @Composable |
垂直滚动:
1
2
3
4Column(
// 根据滚动方向创建滚动状态,并保留滚动位置
modifier = modifier.verticalScroll(rememberScrollState())
)水平滚动:
1
2
3
4Row(
// 根据滚动方向创建滚动状态,并保留滚动位置
modifier = modifier.horizontalScroll(rememberScrollState())
)
当拥有静态内容时,可滚动的列和行非常有用。但是,对于动态的数据收集,它们不是一个好主意。这是因为可滚动可组合项会急切地组合和渲染其中的所有元素,当您要显示大量元素时,这可能是一个繁重的操作。
LazyColumn 和 LazyRow
当使用 LazyColumn 或 LazyRow 时,框架仅会组合能够在屏幕上显示的元素。当你滚动时,会组合新的元素,并且旧的元素会被销毁。当你向回滚动时,旧的元素会重新组合。
1 | @Composable |
其中最重要的参数是 content
,它表示列表中的内容。此内容属于 LazyListScope
类型,而不是通常的 Composable 类型。
1 | @LazyScopeMarker |
item()
允许向列表中添加新的组合项。每次都可以使用不同的组合项类型。items()
允许设置一个 包含希望在每个列表项中使用的数据 的列表。一旦设置了数据,还需要提供一个itemContent
,它是一个用于显示列表中每个项的可组合项。stickyHeader()
允许设置一个头部可组合项,它会一直显示在列表的顶部,即使你向下滚动查看新项目时也是如此。
示例:
1 | private val bookInfo = listOf( |
Compose中的Grid
如图,该网格包含10个元素,分为3列。最后一行的两个元素被标记为不可见,从而达到视觉上10个元素的效果。
1 | private val filledItems = listOf( |
搭建应用Jet Notes
自下而上的构建方法
使用 Jetpack Compose 构建应用时,明智的做法是从较小的可组合项开始,然后逐步构建到设计中。从最小的组件构建应用程序可以从一开始就解耦和复用代码。
Note
先构建Note组件,方便后续复用。
C:\Users\Hunter\AndroidStudioProjects\jet-materials\08-applying-material-design-to-compose\projects\starter
是这个笔记App最终的代码。
Anaconda
简介
Anaconda是一个免费开源的Python和R语言的发行版本,用于计算科学(数据科学、机器学习、大数据处理和预测分析),Anaconda致力于简化包管理和部署。Anaconda的包使用软件包管理系统Conda进行管理。
Anaconda3默认包含Python 3.7,但是用户可以创建虚拟环境来使用任意版本的Python包。
常用命令
- 查看conda版本:
conda --version
- 查看conda的环境配置:
conda config --show
- 更新conda:
conda update conda
- 更新Anaconda整体:
conda update Anaconda
管理环境
Conda允许创建相互隔离的虚拟环境(Virtual Environment),这些环境各自包含属于自己的文件、包以及他们的依存关系,并且不会相互干扰。
创建虚拟环境
conda create -n env_name python=3.8
不指定python版本时,自动创建基于最新python版本的虚拟环境。
创建虚拟环境时,安装必要的包
conda create -n env_name numpy matplotlib python=3.8
列举虚拟环境
conda env list
所显示的列表中,前面带星号
*
的表示当前活动环境。激活虚拟环境
conda activate env_name
退出虚拟环境
conda deactivate
,回到base删除虚拟环境、虚拟环境中的包
conda remove --name env_name --all
conda remove --name env_name package_name
导出环境
conda env export --name myenv > myenv.yml
还原环境
conda env create -f myenv.yml
管理Package
查询包的安装情况。
conda list
conda list pkgname*
查询当前Anaconda repository中是否有想要安装的包
conda search package_name
安装、更新、卸载包
conda install package_name
conda update package_name
conda uninstall package_name
清理缓存
conda clean -p
删除没有用的包 –packagesconda clean -t
删除tar打包 –tarballsconda clean -y -all
删除所有的安装包及cache(索引缓存、锁定文件、未使用过的包和tar包)
conda install vs pip install
- conda可以管理非python包,pip只能管理python包。
- conda自己可以用来创建环境,pip不能,需要依赖virtualenv之类的。
conda安装的包是编译好的二进制文件,安装包文件过程中会自动安装依赖包;pip安装的包是wheel或源码,装过程中不会去支持python语言之外的依赖项。 - conda安装的包会统一下载到一个目录文件中,当环境B需要下载的包,之前其他环境安装过,就只需要把之间下载的文件复制到环境B中,下载一次多次安装。pip是直接下载到对应环境中。
- conda只能在conda管理的环境中使用,例如比如conda所创建的虚环境中使用。pip可以在任何环境中使用,在conda创建的环境 中使用pip命令,需要先安装pip(conda install pip ),然后可以 环境A 中使用pip 。
- conda 安装的包,pip可以卸载,但不能卸载依赖包,pip安装的包,只能用pip卸载。
由于conda的库不如pip的库丰富,有时候可能迫不得已要使用pip安装。只有在conda install搞不定时才使用pip intall。
Windows 通过WSL2 Ubuntu和Docker搭建深度学习环境
参考:
- Windows11 + WSL Ubuntu + Pycharm + Conda for deeplearning | 公孙启 (gongsunqi.xyz)
- Docker官方文档:Install Docker Engine on Ubuntu | Docker Docs
WSL2 Ubuntu安装Docker Engine
设置 Docker的
apt
仓库1
2
3
4
5
6
7
8
9
10
11
12
13# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update下载安装Docker的安装包
1
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
查看docker版本,确认是否成功安装。
1
2❯ docker -v
Docker version 27.1.1, build 6312585
WSL2 下安装Anaconda的Docker镜像
参考Anaconda官网文档Docker — Anaconda documentation
1 | # 拉取镜像 |
在miniconda3容器中安装cudatoolkit和cudnn
wsl2和windows11共用显卡驱动,因此我们只需安装cudatoolkit和cudnn。以后windows显卡驱动正常更新即可。
windows下nvidia-smi
命令查看显卡驱动,以及支持的 CUDA 的最高版本,CUDA Version
指的是可驱动的最高版本。
1 | nvidia-smi |
根据可驱动的最高版本直接去官网下载对应版本:https://developer.nvidia.com/cuda-toolkit-archive
执行官网提供的安装命令:
1 | wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.0-1_all.deb |
创建环境,安装pytorch
1 | conda create -n pytorch python=3.10 |
根据官网确定环境配置,得到相应的安装命令:
1 | conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia |
plan2
基于 WSL2 和 Docker 的深度学习环境指北 - duanyll
安装Docker Engine
安装 NVIDIA Container Toolkit
安装带 CUDA 的 PyTorch 镜像
pytorch/pytorch:2.4.0-cuda12.1-cudnn9-devel
若想要使用pycharm连接docker容器,容器端口号必须指定为22,因为SFTP默认使用22端口。
docker run -it –gpus all -p 8077:22 pytorch/pytorch:2.4.0-cuda12.1-cudnn9-devel
进入容器,用which python
查看,为/opt/conda/bin/python
,可知默认使用conda来管理环境和package。
容器中安装SSH
sudo apt purge openssh-server # wsl2 自带的好像 sshd 不完整,先删除掉
apt update
apt -y upgrade
apt install -y vim openssh-server
service ssh start
service ssh status
设置root密码和配置文件
passwd root
vim /etc/ssh/sshd_config
添加如下内容
1 | #启用公钥私钥配对认证方式 |
重启ssh
service ssh restart
【详细教程】pycharm使用docker容器开发_pycharm docker-CSDN博客
不知道为什么连不上,只能正常连接子系统和docker。
修复 WSL2 镜像网络模式下无法连接 Docker 的问题 - sulinehk blog - 专注于计算机科学与软件工程的技术博客
可能是添加了这个配置才好的。
直接运行项目中的jupyter notebook内容,会报错:
Running as root is not recommended. Use --allow-root to bypass.
在容器中执行:
1 | jupyter server --generate-config |
安装matplotlib
Matplotlib 是 Python 的绘图库。
conda install -y matplotlib
Matplotlib Pyplot
Pyplot 是 Matplotlib 的子库,提供了和 MATLAB 类似的绘图 API。
Pyplot 是常用的绘图模块,能很方便让用户绘制 2D 图表。
Pyplot 包含一系列绘图函数的相关函数,每个函数会对当前的图像进行一些修改,例如:给图像加上标记,生新的图像,在图像中产生新的绘图区域等等。
使用的时候,我们可以使用 import 导入 pyplot 库,并设置一个别名plt:
1 | import matplotlib.pyplot as plt |
一些常用的 pyplot 函数:
plot()
:用于绘制线图和散点图scatter()
:用于绘制散点图bar()
:用于绘制垂直条形图和水平条形图hist()
:用于绘制直方图pie()
:用于绘制饼图imshow()
:用于绘制图像subplots()
:用于创建子图
SpringMVC
数据库事务机制
ACID原则
事务机制遵循ACID原则:
Atomicity
原子性:事务是一个原子操作,由一系列操作组成。事务的原子性确保所有操作要么完成,要么完全不起作用(完整性)。Consistency
一致性:事务执行前后,系统必须确保它所建模的业务处于一致的状态。例如转账,无论事务是否成功,转账者和收款人的总额应该不变。Isolation
隔离性:并发操作相同的数据时,各事务之间相互独立。(但难免会存在冲突)Durability
持久性:一旦事务完成,它对数据的改变是持久的,即使数据库发生故障也不影响持久性。
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障(A、I、D是手段,C是目的)。
Java虚拟机 JVM
JVM启动入口:JLI_Launch
函数
以OPENJDK8源码GitHub - openjdk/jdk at jdk8-b120为例,虚拟机的启动入口在jdk/src/share/bin/java.c
的JLI_Launch
函数,整个流程分为:
- 配置JVM装载环境
- 解析虚拟机参数
- 设置线程栈大小
- 执行
JavaMain
方法
HTTP请求出现HttpMediaTypeNotAcceptableException报错
现象
发起HTTP请求调用,控制台报错:
org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:322) ~[spring-webmvc-6.1.10.jar:6.1.10]