Android是一种可视化的技术, 在终端市场有70%占有率, 广泛应用在手机, 车载平板, 智能电视等领域, 其和web前端, ios开发一同属于大前端技术领域
通过本文, 您可以了解Android技术全貌, 并用kotlin语言逐个实现个模块, 每个部分都有示例代码和效果图
最核心的就是这4个概念:Activity、Service、BroadcaseReceiver 和 ContentProvider

kotlin和java, 其中前者更高效简介, 且是更面向未来的技术activity对应了js, 负责写交互相关的业务逻辑layout对应了html+css, 负责页面骨架和样式jetpack做数据视图绑定, 最终用jetpack compose实现更复杂的交互Android第一行代码书籍, 更现代的先进概念可在https://developer.android.com学习IDE都用安卓官网推荐的Android Studio, 也是Jetbranis系列的
我们可在Android官网入门demo下载源代码

选择安卓SDK的版本为Android10, 语言为kotlin后, 项目结构如下

其中各目录含义如下
我们可以用USB线连接到电脑上看到APP, 也可以在IDE里用安卓模拟器看到APP可视化的效果
注意, 电脑一定要预留足够空间(例如10GB), The emulator process for AVD has terminated报错, 可在~/Library/Logs/Google/AndroidStudio找到AndroidStudio的日志
run起来效果如下, 这就是一个模拟的安卓手机, 和新买的手机一样, 预装了很多APP(如chrome浏览器, 日历, 电话, 短信, 应用商店等)

页面的布局, 比如按钮放在哪里, 间距多少, 颜色字体等样式, 可在res/layout/activity_main.xml中修改, 有2种方式
design标签页通过拖拽(类似低代码平台)code标签页通过直接编写xml实现split则左右分隔了code和design两者
放一个文本和按钮, 位置对齐, 设置文字, 效果如下图, 这样第一个app就run起来
每个页面在Android中称为Activity, 我们希望实现: 当点第一个页面的send按钮时, 跳转到第二个页面. 下图是第二个页面

先在MainActivity增加sendMessage函数, 并绑定到send按钮
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
/** Called when the user taps the Send button */
fun sendMessage(view: View) {
// Do something in response to button
}
}
Intent是相互独立的组件(如两个activity)之间, 提供运行时绑定的对象, 表示应用执行某操作的意图.
在Main中定义一个常量, 并通过Intent发送给第二个activity
const val EXTRA_MESSAGE = "com.example.myfirstapp.MESSAGE"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
/** Called when the user taps the Send button */
fun sendMessage(view: View) {
val editText = findViewById<EditText>(R.id.editTextTextPersonName)
val message = editText.text.toString()
val intent = Intent(this, DisplayMessageActivity::class.java).apply {
putExtra(EXTRA_MESSAGE, message)
}
startActivity(intent)
}
}
在 Project 窗口中,右键点击 app 文件夹,然后依次选择 New > Activity > Empty Activity 添加ViewText, 并显示第一个页面带回来的字符串
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_display_message)
// Get the Intent that started this activity and extract the string
val message = intent.getStringExtra(EXTRA_MESSAGE)
// Capture the layout's TextView and set the string as its text
val textView = findViewById<TextView>(R.id.textView).apply {
text = message
}
}
改AndroidManifest.xml
<activity android:name=".DisplayMessageActivity"
android:parentActivityName=".MainActivity">
<!-- The meta-data tag is required if you support API level 15 and lower -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
1.下载地址:https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-compiler-embeddable/1.4.32/kotlin-compiler-embeddable-1.7.20-Beta.jar
2.下载完成之后找到~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable/1.4.32/4e982732a609a692421652cfbee3a5c885c48674。
Kotlin - 省略 findViewById()失败 apply plugin: ‘kotlin-android-extensions’

先创建一个menu文件(res/menu/main.xml)
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add" />
<item android:id="@+id/remove_item"
android:title="Remove"/>
menu>
再在Activity中指定点击menu的反应(通知消息)
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add_item -> Toast.makeText(this, "You clicked Add",Toast.LENGTH_SHORT).show()
R.id.remove_item->Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show()
}
return true
}

按Back键, 或通过finish()函数
button1.setOnClickListener {
finish()
}
android用task管理activity, 一个task就是一组存放在栈里的activity集合
当启动activity则入栈, 当activity销毁(按back键或调用finish()销毁)则出栈

另一个activity时调用, 通常在此释放消耗CPU的操作其中又可分为一下3种生存期

我们建立一个新的项目ActivityTest, 其中有两个按钮, 分别启动NormalActivity和DialogActivity
package com.example.activitylifecycletest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val tag = "MainActivity"
@SuppressLint("WrongViewCast")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
setContentView(R.layout.activity_main)
startNormalActivity.setOnClickListener {
val intent = Intent(this, NormalActivity::class.java)
startActivity(intent)
}
startDialogActivity.setOnClickListener {
val intent = Intent(this, DialogActivity::class.java)
startActivity(intent)
}
}
override fun onStart() {
super.onStart()
Log.d(tag, "onStart")
}
override fun onResume() {
super.onResume()
Log.d(tag, "onResume")
}
override fun onPause() {
super.onPause()
Log.d(tag, "onPause")
}
override fun onStop() {
super.onStop()
Log.d(tag, "onStop")
}
override fun onDestroy() {
super.onDestroy()
Log.d(tag, "onDestroy")
}
override fun onRestart() {
super.onRestart()
Log.d(tag, "onRestart")
}
}
如果从Activity A跳转到Activity B, 若Activity A被回收了则临时数据也会被回收, 此时os仍会通过onCreate创建新的Activity B, 而不是通过onRestart重启Activity B, 虽有界面, 但数据丢了还是会影响用户体验
为了解决此问题, 可用onSaveInstanceState()保存数据, 当onCreate()再加载数据
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
Log.d(tag, "tempData is $tempData")
}
}
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putString("data_key", "Something you just typed")
}
有4种, 可在AndroidManifest.xml中通过给标签指定android:launchMode来选择启动模式
是默认行为: os不在乎栈内是否已存在, 而是每次启动都创建一个该activity的新实例
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("FirstActivity", this.toString())
setContentView(R.layout.first_layout)
button1.setOnClickListener {
val intent = Intent(this, FirstActivity::class.java)
startActivityForResult(intent, 1)
}
}
可看到每点击按钮就创建一个实例, 也需要连按3次back键才能退出程序
2022-08-07 19:12:30.231 30429-30429/com.example.activitytest D/FirstActivity: com.example.activitytest.FirstActivity@570edff
2022-08-07 19:12:42.595 30429-30429/com.example.activitytest D/FirstActivity: com.example.activitytest.FirstActivity@78f70b5
2022-08-07 19:12:44.177 30429-30429/com.example.activitytest D/FirstActivity: com.example.activitytest.FirstActivity@cc75a0a

启动activity时, 若栈顶已经是该activity了, 则直接使用而不再创建新的activity实例.
在AndroidManifest.xml中设置
<activity
android:name=".FirstActivity"
android:exported="true"
android:launchMode="singleTop"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>

启动该activity时, 检查栈中是否存在该activity实例
示例操作如下: 现在重新运行程序,在FirstActivity界面点击按钮进入SecondActivity,然后在
SecondActivity界面点击按钮,又会重新进入FirstActivity。
在second中启动first时, 因为发现栈顶为second且first不是栈顶, 在栈顶之下,则使second出栈并复用栈中已有的first.
2022-08-07 19:37:01.576 32502-32502/com.example.activitytest D/FirstActivity: com.example.activitytest.FirstActivity@1af950
2022-08-07 19:37:05.515 32502-32502/com.example.activitytest D/SecondActivity: com.example.activitytest.SecondActivity@afa6507
2022-08-07 19:37:10.241 32502-32502/com.example.activitytest D/FirstActivity: onRestart
2022-08-07 19:37:10.802 32502-32502/com.example.activitytest D/SecondActivity: onDestroy

为了解决其他各应用程序均可共用此activity, 单独创建一个新的栈来管理这个activity, 其他各应用程序均共用此栈
通过javaClass.simpleName获取当前实例的类名
通过一个专门的集合ActivityCollector可实现对所有Activities的管理
添加BaseActivity的class
package com.example.activitytest
import android.app.Activity
import android.os.Bundle
import android.service.voice.VoiceInteractionSession
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
object ActivityCollector {
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivity(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}
使FirstActivity, SecondActivity, ThirdActivity均继承自BaseActivity
业务逻辑是点FirstActivity会调用SecondActivity, 点SecondActivity会调用ThirdActivity, 当点ThirdActivity时销毁所有activities
class ThirdActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ThirdActivity", "Task id is $taskId")
setContentView(R.layout.third_activity)
button3.setOnClickListener {
ActivityCollector.finishAll()
android.os.Process.killProcess(android.os.Process.myPid())
}
}
}
通过actionStart包装startActivity, 这样可以暴露出自己需要的参数, 方便别人看文档
被调用方定义如下
class SecondActivity : BaseActivity() {
companion object {
fun actionStart(context: Context, data1: String, data2: String) {
val intent = Intent(context, SecondActivity::class.java)
intent.putExtra("param1", data1)
intent.putExtra("param2", data2)
context.startActivity(intent)
}
}
}
调用方如下
button1.setOnClickListener {
secondActivity.actionStart(this, "data1", "data2")
}
Intent是Android程序, 各组件之间, 交互的方式, 不仅可指明当前组件想要执行的动作, 还可在不同组件间传递数据.
一般可用于启动Activity, 启动Service, 发送广播
button1.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java) // 第二个参数是希望操作的activity
startActivity(intent) // 启动
}
不明确指出要启动哪个Activity, 而是制定一系列更抽象的action和category,然后由系统分析此intent并找到合适的activity区启动.
可在AndroidManifest.xml中添加如下代码, 表示SecondActivity可响应此action和category
<activity
android:name=".SecondActivity"
android:exported="false">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
intent-filter>
activity>
再在FirstActivity中添加响应事件
其中android.intent.category.DEFAULT是默认的category
button1.setOnClickListener {
// val intent = Intent(this, SecondActivity::class.java) // 第二个参数是希望操作的activity
val intent = Intent("com.example.activitytest.ACTION_START") // 第二个参数是希望操作的activity
startActivity(intent) // 启动
}
如果再加上未定义的category, 即intent.addCategory("com.example.activitytest.MY_CATEGORY"), 如下图就会报错, 这是因为我们并未在AndroidManifest.xml中声明可响应此category的Activity, 报错如预期是正常的
button1.setOnClickListener {
// val intent = Intent(this, SecondActivity::class.java) // 第二个参数是希望操作的activity
val intent = Intent("com.example.activitytest.ACTION_START") // 第二个参数是希望操作的activity
intent.addCategory("com.example.activitytest.MY_CATEGORY")
startActivity(intent) // 启动
}
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.example.activitytest.ACTION_START cat=[com.example.activitytest.MY_CATEGORY] }
Intent的目的就是让各应用互相调用, 下例便可调安卓原生的浏览器打开百度跳转, 而不用自己实现一个浏览器, 那岂不是太麻烦了
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.baidu.com")
startActivity(intent)
}
也可以自己实现一个可跳转的activity, 其中指定可相应https开头的url
<activity android:name=".ThirdActivity"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
intent-filter>
activity>

button1.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
firstActivity跳转到secondActivity
button1.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivityForResult(intent, 1)
}
secondActivity添加如下
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val button2: Button = findViewById(R.id.button2)
button2.setOnClickListener {
val intent = Intent()
intent.putExtra("data_return", "hello FirstActivity")
setResult(RESULT_OK, intent)
finish()
}
}
override fun onBackPressed() {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity 123")
setResult(RESULT_OK, intent)
finish()
}
}
因为在secondActivity被销毁之前会回调上一个activity的onActivityResult()方法, 所以我们需要重写上一个activity的此方法
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityReenter(resultCode, data)
when (requestCode) {
1 -> if (resultCode == RESULT_OK) {
val returnedData = data?.getStringExtra("data_return")
Log.d("FirstActivity", "returned data is $returnedData")
}
}
}
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#00ff00"
android:textSize="24sp"
android:text="@string/this_is_textview"
tools:ignore="MissingConstraints" />

<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
使MainActivity实现View.OnClickListener接口(通过实现onClick()函数), 通过button.setOnClickListener(this)来把MainActivity传入, 当点击按钮时即可执行onClick()中的代码
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
// 在此处添加逻辑
Log.d("MainActivity", "clicked a button")
}
}
}
}
点击button时会打印日志2022-08-08 10:57:43.339 5996-5996/com.example.uiwidgettest D/MainActivity: clicked a button
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
获取editText的内容, 并通知
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
val inputText = editText.text.toString()
Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
}
}
}
}

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img_1"/>
drawable文件夹里放两个图片

点击按钮则替换imageView的图片

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
可通过按钮控制进度条的可见性
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
if (progressBar.visibility == View.VISIBLE) {
progressBar.visibility = View.GONE
} else {
progressBar.visibility = View.VISIBLE
}
}
}
}
也可设置成水平进度条
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
progressBar.progress = progressBar.progress + 10
}
}
}


弹窗, 屏蔽其他空间的交互, 一般显示警告信息或重要内容
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
AlertDialog.Builder(this).apply {
setTitle("This is Dialog")
setMessage("something important")
setPositiveButton("OK") { dialog, which -> }
setNegativeButton("Cancel") { dialog, which -> }
show()
}
}
}
}


默认android:orientation属性指定了排列方向是vertical
下例android:orientation=horizontal, 三个按钮的垂直布局分别为上中下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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>
<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" />
因为指定了android:layout_weight属性,此时控件的宽度就不应该再由android:layout_width来决定了, layout_weight分别为1表示两者均分

<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"
/>
Button的宽度仍然按照wrap_content来计算,而EditText则会占满屏幕所有的剩余空间

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="Button 4" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Button 5" />

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 2" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 4" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 5" />

所有控件都默认放在左上角, 设置layout_gravity可布局
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="This is TextView"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Button"
/>

我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器
例如我们实现一个自定义顶部标题栏, 使可返回上一级, 新建一个layout/title.xml
铺满一个ListView, 用ArrayAdapter填充数据
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
LinearLayout>
class MainActivity : AppCompatActivity() {
private val data = listOf(
"Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)
listView.adapter = adapter
}
}

class Fruit(val name: String, val imageId: Int)
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruit = getItem(position) // 获取当前项的Fruit实例
if (fruit != null) {
view.fruitImage.setImageResource(fruit.imageId)
view.fruitName.text = fruit.name
}
return view
}
}
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits() // 初始化水果数据
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
listView.adapter = adapter
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear", R.drawable.pear_pic))
fruitList.add(Fruit("Grape", R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
}
}

这里添加了一个initFruits()方法,用于初始化所有的水果数据。在Fruit类的构造函数中将水果的名字和对应的图片id传入,然后把创建好的对象添加到水果列表中。
另外,我们使用了一个repeat函数将所有的水果数据添加了两遍,这是因为如果只添加一遍的话,数据量还不足以充满整个屏幕。
接着在onCreate()方法中创建了FruitAdapter对象,并将它作为适配器传递给ListView,这样定制ListView界面的任务就完成了
Adapter内部会通过getItem(position)拿到某行数据,并通过setImageResource()和setText()设置图片和文本数据
广播用于在Android系统内实现通知,概念较为简单

为了实现上述效果, 代码如下
receiver, 当收到消息时, 触发receiver逻辑(弹窗, 关闭所有activities, 跳转到loginActivity)BroadcastReceiver, 是写在onResume和onPause内, 而不是onCreate和onDestroy, 是因为我们希望只有栈顶的activity才可收到强制下线的广播, 其他非栈顶的activity不应该且没必要收此广播, 即当一个activity失去栈顶位置时自动取消BraodcastReceiver的注册package com.example.broadcastbestpractice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
open class BaseActivity : AppCompatActivity() {
lateinit var receiver: ForceOfflineReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityCollector.addActivity(this)
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter()
intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE")
receiver = ForceOfflineReceiver()
registerReceiver(receiver, intentFilter)
}
override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
inner class ForceOfflineReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
AlertDialog.Builder(context).apply {
setTitle("Warning")
setMessage("You are forced to be offline. Please try to login again.")
setCancelable(false)
setPositiveButton("OK") { _, _ ->
ActivityCollector.finishAll() // 销毁所有Activity
val i = Intent(context, LoginActivity::class.java)
context.startActivity(i) // 重新启动LoginActivity
}
show()
}
}
}
}
MainActivity类, 继承BaseActivity类, 当点击button时发出broadcast消息, 触发后续逻辑package com.example.broadcastbestpractice
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
object ActivityCollector {
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivity(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
forceOffline.setOnClickListener {
val intent = Intent("com.example.broadcastbestpractice.FORCE_OFFLINE")
sendBroadcast(intent)
}
}
}
loginActivity, 当用户名密码输入正确是跳转到MainActivity, 否则弹出Toastpackage com.example.broadcastbestpractice
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_login.*
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
login.setOnClickListener {
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
// 如果账号是admin且密码是123456,就认为登录成功
if (account == "admin" && password == "123456") {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(
this, "account or password is invalid",
Toast.LENGTH_SHORT
).show()
}
}
}
}
AndroidManifest.xml中指定主activity, 可以从登录页开始
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.broadcastbestpractice">
<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.BroadcastBestPractice"
tools:targetApi="31">
<activity
android:name=".BaseActivity"
android:exported="false" />
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
<activity
android:name=".MainActivity"
android:exported="true" />
application>
manifest>
Android的数据持久化3种方式: 文件存储, SharedPreferences, 数据库存储
默认写在/data/data/com.example.FilePersistenceTest/files/data中, 当按back键时即写入该文件
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onDestroy() {
super.onDestroy()
val inputText = editText.text.toString()
save(inputText)
}
private fun save(inputText: String) {
try {
val output = openFileOutput("data", Context.MODE_PRIVATE)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.use {
it.write(inputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
程序加载时, 读取到editText中
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inputText = load()
if (inputText.isNotEmpty()) {
editText.setText(inputText)
editText.setSelection(inputText.length)
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_SHORT).show()
}
}
private fun load(): String {
val content = StringBuilder()
try {
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}
}
是键值对的形式, 且保持数据结构(如整型, 如字符串等), 更易用
有2种获取SharedPreferences的方式
布局如下
<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/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data" />
<Button
android:id="@+id/restoreButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restore Data" />
LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
}
restoreButton.setOnClickListener {
val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
val name = prefs.getString("name", "")
val age = prefs.getInt("age", 0)
val married = prefs.getBoolean("married", false)
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "age is $age")
Log.d("MainActivity", "married is $married")
}
}
}
通过点击按钮, 会在/data/data/com.example.sharedpreferencestest/shared_prefs/data.xml中存放如下数据
<map>
<string name="name">Tomstring>
<boolean name="married" value="false" />
<int name="age" value="28" />
map>
点击load按钮, 即可加载数据, 在控制台打印如下
// 刚开始点击load按钮后的打印, 为空值
2022-08-12 09:56:59.452 3376-3376/com.example.sharedpreferencestest D/MainActivity: name is
2022-08-12 09:56:59.452 3376-3376/com.example.sharedpreferencestest D/MainActivity: age is 0
2022-08-12 09:56:59.452 3376-3376/com.example.sharedpreferencestest D/MainActivity: married is false
// 点击save按钮后, 再点击load按钮后的打印, 为save的值
2022-08-12 10:04:05.721 5566-5566/com.example.sharedpreferencestest D/MainActivity: name is Tom
2022-08-12 10:04:05.721 5566-5566/com.example.sharedpreferencestest D/MainActivity: age is 28
2022-08-12 10:04:05.721 5566-5566/com.example.sharedpreferencestest D/MainActivity: married is false

新建一个LoginActivity, 布局如下
<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="60dp"
android:orientation="horizontal">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Account:"
android:textSize="18sp" />
<EditText
android:id="@+id/accountEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1" />
LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Password:"
android:textSize="18sp" />
<EditText
android:id="@+id/passwordEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:inputType="textPassword" />
LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/rememberPass"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Remember password"
android:textSize="18sp" />
LinearLayout>
<Button
android:id="@+id/login"
android:layout_width="200dp"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:text="Login" />
LinearLayout>
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val prefs = getPreferences(Context.MODE_PRIVATE)
val isRemember = prefs.getBoolean("remember_password", false)
if (isRemember) {
// 将账号和密码都设置到文本框中
val account = prefs.getString("account", "")
val password = prefs.getString("password", "")
accountEdit.setText(account)
passwordEdit.setText(password)
rememberPass.isChecked = true
}
login.setOnClickListener {
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
// 如果账号是admin且密码是123456,就认为登录成功
if (account == "admin" && password == "123456") {
val editor = prefs.edit()
if (rememberPass.isChecked) { // 检查复选框是否被选中
editor.putBoolean("remember_password", true)
editor.putString("account", account)
editor.putString("password", password)
} else {
editor.clear()
}
editor.apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(
this, "account or password is invalid",
Toast.LENGTH_SHORT
).show()
}
}
}
}

SQLiteOpenHelper类, 有onCreate()和onUpgrade()方法, 实现数据库创建和升级
getReadableDatabase()和getWriteableDatabase()获取数据库实例
点击按钮则创建database, 其中databaseHelper专门负责调用sqliteOpenHelper来调用数据库
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 1)
createDatabase.setOnClickListener {
dbHelper.writableDatabase
}
}
}
class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
private val createBook = "create table Book (" +
" id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
}
在/data/data/com.example.databasetest/databases/BookStore.db有sqlite文件, 可导出并用DBBrowser插件查看
目前有了Book表,若希望增加Category表, 则可添加如下代码, 多次调用onCreate(), 但只有第一次才会生效, 因为只有第一次才会建库BookStore.db, 后续若此库存在则不会再建库BookStore.db, 除非卸载app或手动删除BookStore.db文件
class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
private val createBook = "create table Book (" +
" id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
private val createCategory = "create table Category (" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
// 为了保证升级兼容性, 一般会在onUpgrade()方法内根据version判断执行不同版本的升级逻辑
// 从低version开始, 保证任何版本都能成功执行
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
if (oldVersion <= 2) {
db.execSQL("alter table Book add column category_id integer")
}
}
}
若传入的version大于旧值则调用onUpgrade, 否则若version相等则调onUpdate
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
// 传入的version>1时, 即会调用onUpgrade()
通过如下, 可将数据插入数据库中
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
addData.setOnClickListener {
val db = dbHelper.writableDatabase
val values1 = ContentValues().apply {
// 开始组装第一条数据
put("name", "The Da Vinci Code")
put("author", "Dan Brown")
put("pages", 454)
put("price", 16.96)
}
db.insert("Book", null, values1) // 插入第一条数据
val values2 = ContentValues().apply {
// 开始组装第二条数据
put("name", "The Lost Symbol")
put("author", "Dan Brown")
put("pages", 510)
put("price", 19.95)
}
db.insert("Book", null, values2) // 插入第二条数据
}
}
}
最终会发现落库成功
sqlite3 BookStore.db
SQLite version 3.35.4 2021-04-02 15:20:15
Enter ".help" for usage hints.
sqlite> select * from Book;
1|Dan Brown|16.96|454|The Da Vinci Code
2|Dan Brown|19.95|510|The Lost Symbol
3|Dan Brown|16.96|454|The Da Vinci Code
4|Dan Brown|19.95|510|The Lost Symbol
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
updateData.setOnClickListener {
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("price", 10.99)
db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
}
}
}
更新后如下
sqlite3 BookStore.db
SQLite version 3.35.4 2021-04-02 15:20:15
Enter ".help" for usage hints.
sqlite> select * from Book;
1|Dan Brown|10.99|454|The Da Vinci Code
2|Dan Brown|19.95|510|The Lost Symbol
3|Dan Brown|10.99|454|The Da Vinci Code
4|Dan Brown|19.95|510|The Lost Symbol
deleteData.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book", "pages > ?", arrayOf("500"))
}

下例为获取表中所有行
queryData.setOnClickListener {
val db = dbHelper.writableDatabase
// 查询Book表中所有的数据
val cursor = db.query("Book", null, null, null, null, null, null)
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
} while (cursor.moveToNext())
}
cursor.close()
}
查出后打印日志如下
2022-08-12 15:10:34.264 7666-7666/com.example.databasetest D/MainActivity: book name is The Da Vinci Code
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book author is Dan Brown
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book pages is 454
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book price is 10.99
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book name is The Lost Symbol
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book author is Dan Brown
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book pages is 510
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book price is 19.95
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book name is The Da Vinci Code
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book author is Dan Brown
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book pages is 454
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book price is 10.99
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book name is The Lost Symbol
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book author is Dan Brown
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book pages is 510
2022-08-12 15:10:34.265 7666-7666/com.example.databasetest D/MainActivity: book price is 19.95
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Lost Symbol", "Dan Brown", "510", "19.95")
)
db.execSQL("update Book set price = ? where name = ?", arrayOf("10.99", "The Da Vinci Code"))
db.execSQL("delete from Book where pages > ?", arrayOf("500"))
val cursor = db.rawQuery("select * from Book", null)
先开启事务, 然后在try catch 和 finally中捕获exception, 做对应处理
replaceData.setOnClickListener {
val db = dbHelper.writableDatabase
db.beginTransaction() // 开启事务
try {
db.delete("Book", null, null)
if (true) {
// 手动抛出一个异常,让事务失败
throw NullPointerException()
}
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
db.insert("Book", null, values)
db.setTransactionSuccessful() // 事务已经执行成功
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction() // 结束事务
}
}
日志就会打印printStackTrace对应的方法
2022-08-12 15:30:39.075 8005-8005/com.example.databasetest W/System.err: java.lang.NullPointerException
2022-08-12 15:30:39.080 8005-8005/com.example.databasetest W/System.err: at com.example.databasetest.MainActivity.onCreate$lambda-7(MainActivity.kt:71)
2022-08-12 15:30:39.080 8005-8005/com.example.databasetest W/System.err: at com.example.databasetest.MainActivity.$r8$lambda$Ff3cxcjnoRHI0B8DD9nQh8YQ2cY(Unknown Source:0)
通常我们若需向SharedPreferences存储数据, 需3个步骤
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
然而可通过高阶函数简化, 具体方式如下
通过新建一个SharedPreferences.kt文件, 加入如下代码, 其实是为SharedPreferences类添加了open函数, 其中open函数是高阶函数, 其参数为block函数
其中block是我们添加数据的业务逻辑
这个open函数做了一些模板工作, 它会帮我们拿到editor对象, 调我们的block()业务逻辑, 最终通过apply提交数据
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}
定义之后, 我们可按如下使用, 其中向open函数内传的block()函数就是如下3行业务逻辑
getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
其实AndroidStudioIDE在初始化项目时即自动引入了ktx库, 此库就会为我们做这些工作, 只不过将open函数名换成了edit函数名, 调用方式如下
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
通常的用法是如下
val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)
为了简化, 我们可定义一个ContentValues.kt文件, 在其中定义cvOf()方法
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}
调用的时候, 就很简化了, 通过cvOf一次性就可以输入一堆数据
val values = cvOf(
"name" to "Game of Thrones", "author" to "George Martin",
"pages" to 720, "price" to 20.85
)
db.insert("Book", null, values)
其实AndroidStudioIDE自动引入的ktx库, 也有contentValuesOf方法, 我们可以直接使用, 它的调用方法如下
val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin",
"pages" to 720, "price" to 20.85)
db.insert("Book", null, values)