APK知识框架
项目结构
基本结构
说明:
-
manifests\AndroidManifest.xml 文件,这是APP的核心配置文件,定义了包名、组件、权限、等。
-
com.example.demo_study:源代码
-
om.example.demo_study (androidTest):依赖设备测试,依赖 Android 框架,适合进行 UI、数据库操作等涉及设备的测试。
-
com.example.demo_study (test):本地测试,不依赖设备,速度快主要用于逻辑验证。
-
res:资源文件,
-
drawable:存放图像资源(如 png、jpg 和xml)。比如ic_launcher_background.xml 和 ic_launcher_foreground.xml,用于定义启动图标的背景和前景。
-
mipmap:存放启动图标的多分辨率版本。比如ic_launcher/ 和 ic_launcher_round/,分别是矩形和圆形启动图标,引用drawable中的背景和前景。
-
layout:布局文件。比如activity_main.xml,
-
values:
- colors.xml
- strings.xml
- themes.xml:定义应用的主题(普通主题)。
- themes.xml (night) 是夜间模式主题。
-
xml:存放通用配置文件。比如backup_rules.xml:定义数据备份的规则。data_extraction_rules.xml:定义数据提取的规则。
-
res (generated):自动生成的资源文件,通常是 R 文件,它将资源文件映射为 Java 常量。
-
Gradle Scripts
- build.gradle.kts (Project: demo_study):项目级别,定义插件等
- build.gradle.kts (Module: :app):模块级别的 Gradle 配置文件,依赖库、编译 SDK 等。
- proguard-rules.pro:混淆规则文件,用于优化代码并保护代码安全。
- gradle.properties:定义全局的 Gradle 属性。
- gradle-wrapper.properties:Gradle Wrapper 的配置文件 、Gradle路径和版本。
- local.properties:本地配置文件,通常包含 Android SDK 的路径。
- settings.gradle.kts:定义项目包含的模块。
更详细的内容可以在资源管理器查看:
- demo/app/src/main/java/包名:源码
- demo/app/src/main/res:资源
- demo/app/src/test/
- demo/build/outputs/apk/debug/demo.apk
内容解释
AndroidManifest.xml
<!-- xml版本和编码格式 -->
<?xml version="1.0" encoding="utf-8"?>
<!-- 命名空间,Android/tools 特定的属性,比如android:label、tools:targetApi -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- <application> 节点 -->
<!--supportsRtl:从右到左的文本布局(Right to Left)-->
<!--targetApi:表示此应用的目标 API 级别是 31(Android 12)-->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo_study"
tools:targetApi="31">
<!--activity节点:指明类、是否是否可以由外部应用组件启动-->
<activity
android:name=".MainActivity"
android:exported="true">
<!--intent-filter定义了活动可以响应的意图-->
<intent-filter>
<!--表明此activity是应用的入口点-->
<action android:name="android.intent.action.MAIN" />
<!--显示icon在设备的抽屉中-->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
intent-filter标签:
- action标签:这个组件能响应的操作。android:name为比如MAIN(应用入口)、VIEW(查看)、SEND(发送)
- category标签:区分不同的启动场景。android:name为比如LAUNCHER(启动器中启动的活动)、DEFAULT(默认处理某种 Intent 的组件)、BROWSABLE(通过浏览器或 Web 启动)、
- data标签:处理的指定数据类型或 URI。android:scheme:指定协议(如 http、https);android:host:指定主机名(如 www.example.com);android:path:指定路径(如 /home);android:mimeType:指定数据类型(如 image/*、text/plain)
<!-- 最常用:默认的 -->
<intent-filter>
<!-- 最表明app入口 -->
<action android:name="android.intent.action.MAIN" />
<!-- 在桌面应用中 -->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 分享图片时会打开此组件 -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!-- 自定义特定组件 -->
<intent-filter>
<!-- am start -a com.example.demo.CUSTOM_ACTION调用 -->
<!-- context.startActivity(new Intent("com.example.demo.CUSTOM_ACTION")) -->
<action android:name="com.example.demo.CUSTOM_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
源码
class MainActivity : AppCompatActivity() {
// Bundle 是一个键值对集合,可以通过此参数恢复之前的状态。
override fun onCreate(savedInstanceState: Bundle?) {
// 确保父类逻辑也被执行(例如,支持库的一些内部初始化操作)。
super.onCreate(savedInstanceState)
// 标准做法:打印日志
Log.d("MainActivity", "MainActivity has been created!");
setContentView(R.layout.activity_main)
}
}
test
package com.example.demo_study;
import org.junit.Test; // 引入Test注解
import static org.junit.Assert.*;
public class ExampleUnitTest {
@Test // 表明这个是一个测试类,直接可以运行
public void addition_isCorrect() {
// 测试一个简单的加法函数
assertEquals(4, 2 + 2);
}
}
androidTest
@RunWith(AndroidJUnit4::class) // 等效于java中@RunWith(AndroidJUnit4.class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// 获取设备 -> 获取context
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
println("sout ${appContext.packageName}") // 在android中推荐使用Log进行打印
// 只能打印在android设备,不能打印在控制台
Log.d("TestLog", "这是一个测试日志 ${appContext.packageName}");
assertEquals("com.example.demo_study", appContext.packageName)
}
}
drawable
矢量图:用于图标的前景和背景。
ic_launcher_background.xml、 ic_launcher_foreground.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<此处省略很多path>
</vector>
layout
activity_main.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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
mipmap
自适应图标
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
values
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 使用@color/black-->
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
strings.xml
<resources>
<string name="app_name">demo_study</string>
</resources>
themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Demo_study" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<!-- 引用@style/Theme.Demo_study -->
<style name="Theme.Demo_study" parent="Base.Theme.Demo_study" />
</resources>
xml
规则文件
backup_rules.xml
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
data_extraction_rules.xml
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
Gradle构建
build.gradle.kts (Project: demo_study)
plugins {
// 提供了构建、打包、资源管理等功能。数字表示AGP(Android Gradle Plugin) 版本
// false: 表示插件在当前模块中未被应用,仅在子模块中使用,比如app
id("com.android.application") version "8.2.1" apply false
// 为 Android 项目添加 Kotlin 语言支持。
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
/*
com.android.application vs com.android.library
application用于apk
library用于aar(Android Archive),供其他模块使用。类似于jar
*/
}
build.gradle.kts (Module: :app)
// 已经解释过
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
// Android 构建配置
android {
// R 文件资源的命名空间以namespace
namespace = "com.example.demo_study"
// SDK的API是34
compileSdk = 34
defaultConfig {
// 应用的唯一标识符,作为包名在设备中安装。可以和namespace不同
applicationId = "com.example.demo_study"
// 最低版本,也就是我实现的版本。
minSdk = 31
/* 应用针对的 Android API 版本。
如果运行在 API 34 的设备上,系统会启用 API 34 的所有行为和限制。
如果运行在 API 33(或更低版本)设备上,系统会以兼容模式运行你的应用。
*/
targetSdk = 34
// apk的版本 代码和名称
versionCode = 1
versionName = "1.0"
// Android 仪器化测试。
// 配合androidTestImplementation 引入的依赖,如 androidx.test.ext:junit 和 androidx.test.espresso。
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// 构建类型
buildTypes {
release {
// 是否启用代码压缩
isMinifyEnabled = false
// 配置混淆文件
proguardFiles(
// 使用 Android 提供的默认混淆规则文件。
getDefaultProguardFile("proguard-android-optimize.txt"),
// 使用自定义混淆规则文件。
"proguard-rules.pro"
)
}
}
// 编译配置
compileOptions {
// 源代码兼容性,开发时用的Java语言版本
sourceCompatibility = JavaVersion.VERSION_1_8
// 编译时兼容性,编译后可以运行的Java环境版本
targetCompatibility = JavaVersion.VERSION_1_8
}
// kotlin配置
kotlinOptions {
// kotlin的JVM目标版本
jvmTarget = "1.8"
}
}
// 三种依赖:源码、test、androidTest
dependencies {
// 提供 Kotlin 扩展函数,简化 Android 开发
implementation("androidx.core:core-ktx:1.10.1")
// 支持旧版本 Android 的现代 UI 元件。
implementation("androidx.appcompat:appcompat:1.6.1")
// 提供 Material Design UI 组件。
implementation("com.google.android.material:material:1.9.0")
// 支持 ConstraintLayout 的库。
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
// 用于注解Test
testImplementation("junit:junit:4.13.2")
// 提供扩展的 JUnit 测试框架,测试组件
androidTestImplementation("androidx.test.ext:junit:1.1.5")
// 提供 UI 测试框架,用于模拟用户交互和验证界面行为。
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
proguard-rules.pro:需要再学习。
gradle.properties
# 多看看英文,有好处
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# jvm参数,统一编码
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# 是否启用并行模式,可以加快速度
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
# 指定项目使用 AndroidX 库,而不是旧的 Android 支持库(Support Library)。
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
# 指定 Kotlin 代码的风格标准,默认值为 official。
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
# 每个库的 R 类仅包含自身的资源,而不包含其依赖库的资源。减少每个库 R 类的大小,降低耦合
android.nonTransitiveRClass=true
gradle-wrapper.properties:
Gradle Wrapper 的配置文件,主要用于定义项目构建过程中使用的 Gradle 版本以及下载路径等信息。
#Wed Dec 11 17:04:56 CST 2024
# 定义 Gradle Wrapper 下载的 Gradle 版本存储的基础路径。
distributionBase=GRADLE_USER_HOME
# 在本地存储下载的 Gradle 发行包(distribution)的相对路径。
distributionPath=wrapper/dists
# url/版本,bin.zip:只有二进制压缩,常用。all.zip:二进制文件及 Gradle 文档和源代码。
# 团队合作,使用统一的 Gradle 版本,避免因版本差异导致构建失败。
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
# Gradle ZIP 文件存储的基础路径和相对路径
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
settings.gradle.kts
配置项目结构、依赖解析规则及插件管理。
// 配置 Gradle 插件的管理方式。
pluginManagement {
// 指定插件的下载源。
repositories {
// Google Maven 仓库,用于获取 Android 相关插件
google()
// Maven Central 仓库,用于获取常规插件
mavenCentral()
// Gradle 官方插件门户,用于获取各种 Gradle 插件
gradlePluginPortal()
}
}
// 设置依赖解析规则。
dependencyResolutionManagement {
// 强制所有依赖库从全局的 repositories 中解析,保证统一管理。
// 如果子模块中单独声明了 repositories(例如在 build.gradle(app) 中 声明repositories { jcenter()}),构建时会报错。
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
// 用于解析 Android 依赖库
google()
// 用于解析通用 Java/Kotlin 依赖库
mavenCentral()
}
}
// 定义项目的根名称,
rootProject.name = "demo_study"
// 指定项目包含的模块。表示位于项目根目录下的 app/ 文件夹中。
include(":app")
ContentProvider
通过 URI来访问和管理数据。
package com.example.demo2_study
import android.content.ContentProvider
import android.content.ContentValues
import android.database.MatrixCursor
import android.net.Uri
class MyContentProvider : ContentProvider() {
private val data = mutableListOf("Item1", "Item2", "Item3")
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): MatrixCursor {
// 定义返回的列
val cursor = MatrixCursor(arrayOf("item"))
data.forEach { item ->
cursor.addRow(arrayOf(item))
}
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val newItem = values?.getAsString("item") ?: return null
data.add(newItem)
return uri
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
val itemToRemove = selectionArgs?.get(0) ?: return 0
return if (data.remove(itemToRemove)) 1 else 0
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}
override fun getType(uri: Uri): String? {
return "vnd.android.cursor.item/vnd.com.example.demo_study.provider"
}
}
1
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Intent
import android.location.LocationManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val uri = Uri.parse("content://com.example.demo_study.provider")
// 插入数据
val newItemValues = ContentValues().apply {
put("item", "NewItem")
}
contentResolver.insert(uri, newItemValues)
// 删除数据
val deleteArgs = arrayOf("Item1")
contentResolver.delete(uri, null, deleteArgs)
// 查询剩余数据
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.let {
while (it.moveToNext()) {
val item = it.getString(it.getColumnIndexOrThrow("item"))
Log.d("ContentProviderExample", "Remaining item: $item")
}
it.close()
}
}
}
1
<provider
android:name=".MyContentProvider"
android:authorities="com.example.contentproviderexample.provider"
android:exported="true" />
1
四大组件
Activity
表示应用中的一个界面,用户与应用交互时的主要方式。
/*
新建SecondActivity.kt,用于跳转目的地
*/
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
class MainActivity : AppCompatActivity() {
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button = findViewById<Button>(R.id.mybutton)
button.setOnClickListener(){
// 这里的this表示MainActivity( Activity 是 Context 的子类) ,
// 如果有歧义可以使用,this@MainActivity
// SecondActivity::class获取 Kotlin KClass 对象。::class.java获取KClass 对象对应的 Java Class 对象。
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("name", "Tom")
intent.putExtra("age", 18)
intent.putExtra("isStudent", true)
startActivity(intent)
}
/*
// 等效,View.OnClickListener接口
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
val intent = Intent(this@MainActivity, SecondActivity::class.java)
startActivity(intent)
}
})
*/
}
}
ADK在建立新的Activity时,会自动创建布局文件和在Manifest中增加相应的activity组件。
SecondActivity.kt:
class SecondActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val name = findViewById<TextView>(R.id.textViewname)
val age = findViewById<TextView>(R.id.textViewage)
val isStudent = findViewById<TextView>(R.id.textViewisStudent)
name.text = "name: ${intent.getStringExtra("name")}"
age.text = "name: ${intent.getIntExtra("age", 0)}"
isStudent.text = "name: ${intent.getBooleanExtra("isStudent", false)}"
}
}
activity_second.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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondActivity">
<TextView
android:id="@+id/textViewname"
android:text="name: "
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:paddingTop="8dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/textViewage"
android:text="age: "
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewname"
android:paddingTop="8dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/textViewisStudent"
android:text="isStudent: "
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewage"
android:paddingTop="8dp"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
Service
不需要与用户直接交互,用于处理后台任务,比如下载文件。
/*
在MainActivity.kt中startActivity(Intent(this, SecondActivity::class.java))即可调用服务
*/
import android.app.Service
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.util.Log
class MyService : Service() {
private val handler = Handler()
private val runnable = object : Runnable {
override fun run() {
Log.d("MyService", "Service is running") // logcat | grep MyService可以看到
// 延迟 5 秒后重新放入消息队列,不会阻塞任何线程。
handler.postDelayed(this, 5000)
}
}
// 开始执行服务
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
handler.post(runnable)
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(runnable)
}
override fun onBind(intent: Intent?): IBinder? = null
}
BroadcastReceiver
接收并响应应用或系统广播的事件
动态注册:
MainActivity:
// 创建广播
val myReceiver = MyReceiver()
// 注册广播
val filter = IntentFilter("com.example.demo2_study.MY_BROADCAST")
registerReceiver(myReceiver, filter)
val button = findViewById<Button>(mybutton)
button.setOnClickListener {
val intent = Intent("com.example.demo2_study.MY_BROADCAST")
intent.putExtra("message", "this is a message")
// 发送广播
sendBroadcast(intent)
}
MyReceiver.kt
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val message = intent?.getStringExtra("message")
Toast.makeText(context, "message: $message", Toast.LENGTH_LONG).show()
}
}
静态注册:
去掉动态注册代码中的注册。在AndroidManifest.xml中注册,其他相同。
<receiver android:name=".MyReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.example.demo2_study.MY_BROADCAST"/>
</intent-filter>
</receiver>
tip:从 Android 8.0 开始,系统更注重优化电池和性能,因此很多后台广播接收被限制,静态无法生效。
ContentProvider
应用间的数据共享,提供数据操作接口(如 CRUD)。存数数据的方式:数据库、文件系统、共享内存
MainActivity.kt
class MainActivity : AppCompatActivity() {
@SuppressLint("Range")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val insert = findViewById<Button>(R.id.insertbutton)
insert.setOnClickListener {
// 使用ContentValues存储,键值对
val values = ContentValues().apply {
put("message", " hello, Contentprovider")
}
// 调用contentResolver.insert,插入指定位置的ContentValues
contentResolver.insert(Uri.parse("content://com.example.demo_contentprovider/simple"), values)
}
val query = findViewById<Button>(R.id.querybutton)
query.setOnClickListener{
val cursor = contentResolver.query(
// 使用uri找到SimpleContentProvider而不是通过对象
Uri.parse("content://com.example.demo_contentprovider/simple"),
null, null, null, null
)
cursor?.let {
while (it.moveToNext()){
val id = it.getInt(it.getColumnIndex("id"))
val value = it.getString(it.getColumnIndex("value"))
Log.d("MainActivity", " " + id + " == " + value)
}
it.close()
}
}
}
}
SimpleContentProvider.kt
class SimpleContentProvider : ContentProvider() {
// 通过list存储,也可以通过数据库
private val data = mutableListOf<String>()
override fun onCreate(): Boolean {
// 如需,这里可以初始化,比如数据库
return true;
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
// 创建MatrixCursor, 用于存储查询结果。每一行数据都包含两列:id 和 value。
val cursor = MatrixCursor(arrayOf("id", "value"))
for ((index, item) in data.withIndex())
// addRow表示向MatrixCursor添加数据
cursor.addRow(arrayOf(index, item))
return cursor
}
override fun getType(uri: Uri): String? {
return "vnd.android.cursor.dir/com.example.demo_contentprovider.simple"
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
// 表示从values中键值为message的值,并插入到data
values?.getAsString("message")?.let {
data.add(it)
}
// ".../${data.size-1}"表示插入的位置,并返回uri用于判定null是否成功
return Uri.parse("content://com.example.demo_contentprovider/simple/${data.size-1}")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}
}
布局
常用布局:LinearLayout、RelativeLayout、TableLayout、FrameLayout、GridLayout、ConstraintLayout(重要)
安卓布局其实和HTML里的盒子模型类型,也存在padding,margin等。
常用属性:
属性 | 描述 |
---|---|
layout_width/layout_height | 宽/高 |
padding | 内边距 |
margin | 外边距 |
visibility | visible:显示; invisible:不现实但是占用空间; gone:不显示也不占用空间 |
focusable | 焦点 |
enabled | 是否启用该控件 |
background | 背景色 |
id | 唯一标识符,用于获取空间 |
LinearLayout
是指按照线性顺序排
demo1
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" // horizontal
android:padding="16dp"
android:gravity="center_horizontal"> // 有top、center、left、bottom | center_horizontal等属性
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, LinearLayout!"
android:layout_gravity="right" // layout_gravity属性可以修改在父gravity基础上叠加
android:textSize="20sp"
android:layout_marginBottom="50dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1"
android:layout_marginBottom="10dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2" />
</LinearLayout>
demo2
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="3" // 也可以不定义,控制会自动计算和
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Button" />
</LinearLayout>
/* 由于layout_width为0dp,所以layout_weight且为所占的比例,比如Button占 2/3
*/
RelativeLayout
相对布局是指相对于父亲或者兄弟部件进行位置
demo1
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- TextView 位于顶部并居中 -->
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a TextView"
android:textSize="18sp"
android:layout_centerHorizontal="true" // 水平且居中
android:layout_alignParentTop="true" // 与父视图的顶部对齐
android:layout_marginTop="20dp" />
<!-- Button 1 在 TextView 下方并居中 -->
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1"
android:layout_below="@id/textView1" // 在属性控件的下面
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp" />
<!-- Button 2 在 Button 1 下方并居中 -->
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2"
android:layout_below="@id/button1"
android:layout_centerHorizontal="true"
android:layout_marginTop="-10dp" /> // 可以为负值
</RelativeLayout>
demo2
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- TextView 居中 -->
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a centered TextView"
android:textSize="18sp"
android:layout_centerInParent="true" />
<!-- Button 1 居中并放置在 TextView 的下方 -->
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1"
android:layout_below="@id/textView"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp" />
<!-- Button 2 放置在 Button 1 的右侧 -->
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2"
android:layout_toRightOf="@id/button1" // button2放在button1的右侧
android:layout_marginLeft="20dp" // button2左边margin为20dp
android:layout_alignTop="@id/button1" /> // button2顶部和button1对齐
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 3"
android:layout_alignParentRight="true" // Parent有top/bottom/left/right
android:layout_centerHorizontal="true" // 还有属性值layout_centerVertical
android:layout_marginTop="20dp" />
</RelativeLayout>
demo3
ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
/*
ConstraintLayout 允许你通过约束来放置视图
android、app、tools表示 XML 命名空间,用于引用 Android 系统和工具属性。
layout_width/layout_height: 区分match_parent和wrap_content
tools:context:表示为MainActivity 活动设计的
*/
<TextView
android:id="@+id/hellotextvie"
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
/*略*/
<Button
android:id="@+id/insertbutton"
android:text="insert"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hellotextvie"
android:padding="8dp"
tools:ignore="MissingConstraints" />
/*
表明padding之间为8dp
tools:ignore="MissingConstraints"表明开发者已经明确忽略警告。有无都行
*/
</androidx.constraintlayout.widget.ConstraintLayout>
部分问题
androidTest:最终如何使用?
Bundle
没成功
<!-- 浏览器输入指定网址,跳转此组件 -->
<intent-filter android:priority="1000">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="www.example.com" android:path="/home" />
</intent-filter>
// 验证方法
adb shell am start -a android.intent.action.VIEW -d "http://www.example.com"
// 指定报名才成功
adb shell am start -n com.example.demo_study/.MainActivity -a android.intent.action.VIEW -d "http://www.example.com"
. androidx.test.ext:junit
和 androidx.test.espresso
的功能
5.1 androidx.test.ext:junit
-
功能
:
- 提供扩展的 JUnit 测试框架,支持 Android 特性。
- 允许测试 Android 组件(如 Activity)的行为。
-
常见特性
:
-
ActivityScenario
:
- 用于在测试中启动并控制 Activity 的生命周期。
-
提供与 Android 生命周期集成的断言方法。
-
-
例子
:
kotlin复制代码@Test fun testActivityLaunch() { val scenario = ActivityScenario.launch(MainActivity::class.java) assertThat(scenario.state, equalTo(Lifecycle.State.RESUMED)) }
5.2 androidx.test.espresso
-
功能
:
- 提供 UI 测试框架,用于模拟用户交互和验证界面行为。
-
主要特点
:
- 提供简单的 API 来定位视图并执行操作(如点击、输入文本等)。
- 支持同步操作,避免多线程问题。
-
例子
:
kotlin复制代码@Test fun testButtonClick() { onView(withId(R.id.my_button)) // 定位按钮 .perform(click()) // 模拟点击 onView(withId(R.id.result_text)) // 检查结果视图 .check(matches(withText("Clicked!"))) // 验证文本 }
Kotlin简介
Kotlin由JetBrains公司开发。谷歌宣布其成为安卓第一开发语言。
兼容Java,可以和Java混编。
语言类型
- 编译型
编译器直接将源代码一次性编译成与CPU相配的二进制文件,计算机可直接执行,例如C,C++。
特点:一次编译。不同操作系统(编译的后二进制文件需要调用此OS的api)和CPU(指令集)需要重新编译。
tip:编译后的文件,如果想要在另一个机器上跑,需要相同的操作系统(需要调用此系统的API),还需要相同的CPU(指令集)
- 解释型
程序运行时,解释器会将源码一行一行实时解析成二进制再执行。例如JS,Python。
特点:安装对应的VM即可运行。效率低。
Java的语言类型:
java准确来说属于混合型语言,但更偏向于解释型。
编译:java存在JIT和AOT,JIT将可将热点代码直接编译成机器码,AOT可在安装时把代码编译成机器码
解释:java运行时需编译成class文件,JVM在解释class文件。
基本
变量、函数
fun main(args: Array<String>){
val a = 1 // 定义变量。推导为Int(包装类)。
var b = 2 // 定义常量
println(myFun(4, 5))
}
fun test() {} // 无参无返
// 下面等效
fun myFun(a: Int, b: Int): Int { // 有参有返
return a + b
}
fun myFun(a: Int, b: Int): Int = a + b
fun myFun(a: Int, b: Int) = a + b // 推导类型
val:定义只读变量,可以是任何类型
const:常量,值必须在编译时已知,并且只能是基本数据类型或 String
多个变量
fun main(){
printNums(1, 2, 3, 4) // 打印1 2 3 4
}
fun printNums(vararg numbers : Int){
for (number in numbers)
println(number)
}
条件
fun getMax(a: Int, b: Int) = if (a > b) a else b // 此处必须有else,否则无返报错
// Kotlin中==等价于Java的equals比较的时是对象里的内容
fun myIf(name: String): String{
if (name == "D") return "不及格"
else return "Others" // 此处必须有else,否则无返报错
}
fun getScore(name: String) = when (name) {
"A" -> "best"
"B" -> "better"
else -> "bad" // 此处必须有else,否则无返报错
}
// when 参数检查
fun checkNumber(num: Number) {
when(num){
is Int -> println("Integer")
is Double -> println("Double")
else -> println("others") // 可以省略,因为无返
}
}
循环
// 输出 0 ≤ .. ≤ 10
val range = 0..10 // [0, 10]
val range = 0 until 10 // [0, 10)
val range = 0 until 10 step 2 // 同上,步长为2
val range = 10 downTo 0 // [10, 0]
for (i in range) {
println(i)
}
类
// Demo1: 拥有主构造器
// 如果不带open,decompile则为final,不可继承
open class Person(val name: String, val age: Int) {
init {
println("name = " + name + ", age = " + age)
}
}
// 主构造。如果父类有主构造则子类必须调用。无参的主构造也是
class Student(name: String, age: Int, val number: String, val grade: Int) : Person(name, age) {
// 次构造调用主构造
constructor(name: String, age: Int, number: String) : this(name,age,number,0)
constructor() : this("", 0, "", 0)
}
fun main(args: Array<String>){
// 每次创建类都会调用Person类中的init代码块
val s1 = Student("Tom", 18, "123456", 6) // 打印信息。
val s2 = Student("Tommy", 19, "123457")
val s3 = Student()
}
// Demo2: 没有主构造器
open class Person(name: String, age: Int) {
init {
println("name = " + name + ", age = " + age)
}
}
class Student : Person{
// num不可以在其他地方使用,报错。
constructor(name: String, age: Int, num: String) : super(name, age) // 调用父类的构造器
}
fun main(args: Array<String>){
val s1 = Student("Tom", 18, "123456")
}
接口
interface Study {
fun read()
// 和java(default,static除外)不同,可以在接口中直接实现
fun eat() {
println("eat...")
}
}
// 类继承父类和接口
class Student : Person, Study{
override fun read() {
println("read...")
}
}
数据类data
Idea中创建Data类
// 在java中
public class UserBean {
private String id;
private String name;
private String pwd;
// 拥有get/set方法,空构造器和所有属性的构造器
// 重写equals、hashCode、toString方法
}
// 在kotlin中
// 拥有有get/set方法
class UserBean(var id: String, var name: String, var pwd: String)
// data会自动重写equals、hashCode、toString方法
data class UserBean(var id: String, var name: String, var pwd: String)
fun main(args: Array<String>){
val user = UserBean("123", "Tom", "123456")
user.name = "Bob" // 本质在调用set方法
println(user.name) // 本质在调用get方法
}
单例object
Idea中创建object类
object Singleton {
fun test() {
println("test")
}
}
// 使用
fun main(args: Array<String>){
Singleton.test() // 相等于java代码为Singleton.INSTANCE.test();
}
decomplie的java文件
public final class Singleton {
@NotNull
public static final Singleton INSTANCE;
public final void test() {
String var1 = "test";
System.out.println(var1);
}
private Singleton() {
}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
数组
fun main(){
// 不可变
val array1 = arrayOf(1, 2, 3) // 返回Array<Int>
val array2 = arrayOf(1, "hello", 3) // 返回Array<Any>
val ints = array1 + 4 // 新的数组
val toMutableList = array1.toMutableList() // 转为MutableList也可以
ArrayList<Int>(listOf(1, 2, 3)) // 或者使用ArrayList就可变
}
集合
// Demo1: List
val list = ArrayList<Int>();
list.add(1);
list.add(2);
val listOf = listOf<Int>(1, 2, 3) // 不可变
println(listOf.get(1))
val mutableListOf = mutableListOf<Int>(1, 2, 3) // 可变
mutableListOf.add(4)
println(mutableListOf.get(2))
for (i in mutableListOf) {
println(i)
}
// Demo1 : list- 遍历
fun main(){
val list = mutableListOf<Int>(1, 2, 3, 4)
for ((index, item) in list.withIndex()){\
// 输出:index = 0, item = 1 ...
println("index = " + index + ", item = " + item)
}
}
// Demo2: Set 相似与List
// Demo3: Map
val map = HashMap<String, String>()
map.put("1", "Tom")
map.put("2", "Alice")
map["3"] = "Bob" // 等效上面。hashmap指向类似下标的操作
map["4"] = "Jerry"
// 等效println(map.get("3"))
println(map["3"])
val mapOf = mapOf<String, String>("1" to "Tom", "2" to "Bob") // 不可变
val mutableMapOf = mutableMapOf<String, String>("1" to "Tom", "2" to "Bob") // 可变
mutableMapOf.put("3", "alice")
// Hashmap遍历
for (mutableEntry in mutableMapOf) {
println(mutableEntry.toString())
}
for ((key, value) in mutableMapOf) {
println(key + " " + value)
}
Lambda
List
val listOf = listOf<String>("a", "bb", "ccc", "dddd")
var maxLengthString = "" // 常规方法
for (s in listOf) {
if (s.length > maxLengthString.length) maxLengthString = s;
}
println(maxLengthString)
// 等效
var lambda = {str: String -> str.length}
println(listOf.maxByOrNull(lambda)) // maxByOrNull是一个普通方法,需要一个Lambda参数
// 等效
listOf.maxByOrNull(){str: String -> str.length} // 若Lambda为方法的最后一个参数,则可将{}提到外面
listOf.maxByOrNull {str: String -> str.length} // 若有且仅有一个参数且是Lambda,则可去掉()
listOf.maxByOrNull {str -> str.length} // kotlin有推导机制
listOf.maxByOrNull {it.length} // 若Lambda只有一个参数,则可用it(iterator)替代参数名
// 类型的方法
listOf.filter { it.length > 2 } // 过滤长度大于2的字符串
listOf.map { it.toUpperCase() } // 字符全部转为大写
listOf.any {it.length > 3} // 是否有长度大于3的字符串
listOf.all { it.length > 1 } // 是否有长度均大于1的字符串
Thread
// object用于实现接口,即声明匿名内部类
Thread(object : Runnable {
override fun run() {
println("test")
}
}).start()
// Runnable是Java单抽象方法接口,可对代码进行简化
Thread( Runnable {
println("test")
}).start()
// Runnable接口只用一个方法,使用Lambda
Thread({
println("test")
}).start()
// Thread只需一个参数Runnable参数,则可省略()
Thread { println("test") }.start()
只要是只接受一个函数式接口的,都可以这样写。比如button.setOnClickListener { println("test") }
空指针检查机制
在java中处理空指针
public void doStudy(Study study) {
study.doHomework();
study.readBooks();
}
// 上述代码时存在空指针风险的,传入null,则程序崩溃
public void doStudy(Study study) {
if (study != null) {
study.doHomework();
study.readBooks();
}
}
对于kotlin来说
// kotlin会在编译期自动检查
fun study(study: Study) {
study.doHomework()
study.readBooks()
}
fun main() {
study(null) //报错
study(Student()) //正确
}
// `?`表示可以传入null
fun study(study: Study?) {
if (study != null) { // 程序员必须要保证不会空
study.doHomework()
study.readBooks()
}
}
// `?.`表示非空才执行
fun study(study: Study?) {
study?.doHomework() // study不空才执行方法
study?.readBooks()
}
//此时靠?.则保证了study肯定不为空,才会执行let函数
fun study(study: Study?) {
study?.let {
//it为study
it.doHomework()
it.readBooks()
}
}
// `?:` 表示a不空才为b
val c = a ?: b
// Demo: 例子
fun getTextLength(text: String?) = text?.length ?: 0
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
// 强行通过编译,就需要依靠`!!`,这时就是程序员来保证安全
fun study(study: Study?) {
//假设此时为空抛出异常,则和java一样
study!!.doHomework()
study!!.readBooks()
}
// study可以为Study也可为空
var study: Study? = null
内嵌表达式
fun main(args: Array<String>){
val name = "World"
println("Hello $name") // $name等效于${name}
println("Win ${if (2 > 1) 2 else "1"}") // ${if...}
println("fun ${say("World")}") // // ${fun()}
}
fun say(content: String): String = "say $content"
函数参数默认值
// Demo1:
fun main(args: Array<String>){
say(100)
say(100, "World")
}
fun say(num: Int, str: String = "default") {
println("num = $num, str = $str")
}
// Demo2: 指定形参传实参
fun main(){
say(2, "Tom")
say(str = "Bob") // 不传具有默认值的
}
fun say(num: Int = 100, str: String) { // 默认值在前面
println("num = $num, str = $str")
}
构造器中的默认值
class Student(name: String, age: Int, val number: String, val grade: Int) : Person(name, age){
constructor(name: String, age: Int, number: String) : this(name, age, number, 0) {
}
...
}
// 等效于上面
class Student(name: String, age: Int, val number: String, val grade: Int = 0) : Person(name, age){
...
}
TODO
TODO()
是一个有效的表达式,且返回类型是 Nothing
,表示该代码永远不会正常返回。
public fun TODO(reason: String? = null): Nothing = throw NotImplementedError(reason)
为什么不会报错?
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
因为 Nothing
是所有类型的子类型,可以暂时替所有类的返回值。编译正确,运行抛出相应异常。
block
作为高阶参数
// 高阶函数参数,允许将代码块(函数或 lambda 表达式)作为参数供函数使用,实现更灵活的代码逻辑和复用。
inline fun <T> myFun(condition : Boolean, block: () -> T): T? {
return if (condition) block() else null
}
fun main() {
val myFun = myFun(true) {
println("hello world")
"abc"
}
println(myFun)
}
T.()
的形式就是定义扩展函数的方式。
// kotlin的Unit( = Java中的void)
// 定义一个扩展函数类型 T.() -> Unit
fun <T> myFun(obj: T, block: T.() -> Unit) {
// 在 T 对象上调用扩展函数
obj.block()
}
// 为 String 类型定义一个扩展函数
fun String.printToupper() {
println(this.uppercase())
}
fun main() {
val str = "hello"
myFun(str){
printToupper() // 调用扩展函数
}
}
//Demo2 : apply 函数本质上就是一个 T.() -> Unit 类型的函数
val mutableList = mutableListOf<Int>(1, 2, 3).apply {
add(4)
add(5)
}
apply源码
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
// 等效this.block(),this就是调用apply的对象
block()
return this
}
委托模式
fun main(){
val consolePrint = ConsolePrinter()
val document = Document(consolePrint)
document.printer("hello") // ConsolePrint : hello
document.say("hello") // say : hello
}
interface Printer{
fun printer(content : String)
fun say(content : String)
}
class ConsolePrinter : Printer{
override fun printer(content : String) {
println("ConsolePrint : $content")
}
override fun say(content: String) {
println("say : $content")
}
}
// Document类通过by printer委托了Printer接口的所有方法给了printer对象
class Document(printer : Printer) : Printer by printer {}
型是 Nothing
,表示该代码永远不会正常返回。
public fun TODO(reason: String? = null): Nothing = throw NotImplementedError(reason)
为什么不会报错?
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
因为 Nothing
是所有类型的子类型,可以暂时替所有类的返回值。编译正确,运行抛出相应异常。
block
作为高阶参数
// 高阶函数参数,允许将代码块(函数或 lambda 表达式)作为参数供函数使用,实现更灵活的代码逻辑和复用。
inline fun <T> myFun(condition : Boolean, block: () -> T): T? {
return if (condition) block() else null
}
fun main() {
val myFun = myFun(true) {
println("hello world")
"abc"
}
println(myFun)
}
T.()
的形式就是定义扩展函数的方式。
// kotlin的Unit( = Java中的void)
// 定义一个扩展函数类型 T.() -> Unit
fun <T> myFun(obj: T, block: T.() -> Unit) {
// 在 T 对象上调用扩展函数
obj.block()
}
// 为 String 类型定义一个扩展函数
fun String.printToupper() {
println(this.uppercase())
}
fun main() {
val str = "hello"
myFun(str){
printToupper() // 调用扩展函数
}
}
//Demo2 : apply 函数本质上就是一个 T.() -> Unit 类型的函数
val mutableList = mutableListOf<Int>(1, 2, 3).apply {
add(4)
add(5)
}
apply源码
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
// 等效this.block(),this就是调用apply的对象
block()
return this
}
委托模式
fun main(){
val consolePrint = ConsolePrinter()
val document = Document(consolePrint)
document.printer("hello") // ConsolePrint : hello
document.say("hello") // say : hello
}
interface Printer{
fun printer(content : String)
fun say(content : String)
}
class ConsolePrinter : Printer{
override fun printer(content : String) {
println("ConsolePrint : $content")
}
override fun say(content: String) {
println("say : $content")
}
}
// Document类通过by printer委托了Printer接口的所有方法给了printer对象
class Document(printer : Printer) : Printer by printer {}