跳过正文

读书笔记:第一行代码Android(第三版)

·5427 字·26 分钟
Gordon Mark
作者
Gordon Mark
书名第一行代码:Android(第二版/第三版)
作者郭霖
状态已读完
简介Android + Java 开发

第一行代码:Android(第2版)

安卓系统架构

  • Linux 内核层:硬件驱动
  • 系统运行库层:特性支持,如 SQLite 库
  • 应用框架层:构建应用程序需要的 API ,如四大组件
  • 应用层:APP

安卓四大组件:活动(Activity)、服务(Service)、广播(Broadcast Receiver)和内容提供器(Content Provider)

项目结构
#

java 文件夹: 项目代码

res 文件夹: 项目图片、布局、字符串等资源。

- Drawable 文件夹:用于存放各种类型的图形资源,包括但不限于 PNG、JPEG、GIF 图片,XML 定义的可绘制资源(如形状、选择器等),以及九宫格图(9-patch)。这些资源可以被用来作为背景、图标、分割线等。
- Mipmap 文件夹:主要用于存放应用启动器图标。使用 mipmap 文件夹有助于确保应用程序图标在不同分辨率和屏幕密度的设备上都能获得最佳显示效果。
- Layout 文件夹:页面布局文件
- values 文件夹:字符串、样式、颜色等配置

build.gradle(最外层的) app 模块的 gradle 构建脚本

build.gradle(app 内的) 编译版本,依赖

AndroidManifest.xml 项目的配置文件,四大组件都要在此注册。

assets: 大体积资源文件,静态文件等

活动(Activity)
#

  • 设置主活动: 在 activity 标签中加入 intent-filter 标签等声明。
  • **添加菜单:**构建 menu 资源文件并在活动中引用

Intent
#

通过使用 Intent 进行活动的跳转、启动服务、发送广播等。

  • 显示 Intent: 显式的指出要启动的活,
    • 启动活动时传参: putExtra -> getStringExtra 在。
    • 返回数据给上个活动: startActivityForResult 启动新活动,使用 onActivityResult 接受新活动销毁时返回的参数
  • 隐式 Intent: 不指定具体目标组件(Activity、Service等),而是通过描述操作意图让系统匹配合适组件来响应的机制。
    • Action:要执行的操作(如ACTION_VIEWACTION_SEND
    • Data:操作的数据URI和类型(如tel:123456http://...
    • Category:组件的附加信息(如CATEGORY_BROWSABLE

每个 Intent 只能指定一个 action,但能指定多个 category。标签中指明了当前Activity可以响应的 action, 而标签则包含了 一些附加信息,更精确地指明了当前Activity能够响应的Intent中还可能带有的category。

// 隐式Intent - 通过Action和数据类型描述意图
val implicitIntent = Intent().apply {
    action = Intent.ACTION_VIEW
    data = Uri.parse("https://www.example.com")
}
startActivity(implicitIntent)

隐式 Intent 的响应

<activity android:name=".MyShareActivity">
  <!-- 声明Intent过滤器 -->
  <intent-filter>
    <!-- 定义Action -->
    <action android:name="android.intent.action.SEND" />

    <!-- 定义数据类型 -->
    <data android:mimeType="text/plain" />

    <!-- 定义类别 -->
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

在Activity中处理隐式Intent

class MyShareActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_share)

        // 检查Intent的Action
        when (intent?.action) {
            Intent.ACTION_SEND -> {
                if (intent.type == "text/plain") {
                    handleSendText(intent)
                } else if (intent.type?.startsWith("image/") == true) {
                    handleSendImage(intent)
                }
            }
        }
    }

    private fun handleSendText(intent: Intent) {
        val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
        // 处理文本分享
        textView.text = sharedText
    }

    private fun handleSendImage(intent: Intent) {
        val imageUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
        // 处理图片分享
        imageView.setImageURI(imageUri)
    }
}

返回数据给上一个Activity
#

使用 startActivityForResult方法

// 第一个Activity
class FirstActivity : AppCompatActivity() {
    
    companion object {
        const val REQUEST_CODE = 1001
        const val EXTRA_KEY = "result_data"
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        
        val button = findViewById<Button>(R.id.button)
        val resultTextView = findViewById<TextView>(R.id.result_text_view)
        
        button.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivityForResult(intent, REQUEST_CODE)
        }
    }
    
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        
        if (requestCode == REQUEST_CODE) {
            val result = data?.getStringExtra(EXTRA_KEY) ?: "没有数据返回"
            
            when (resultCode) {
                RESULT_OK -> {
                    // 用户主动提交
                    Toast.makeText(this, "提交成功: $result", Toast.LENGTH_SHORT).show()
                }
                RESULT_CANCELED -> {
                    // 用户按返回键
                    Toast.makeText(this, "已取消: $result", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

// 第二个Activity  按钮返回
class SecondActivity : AppCompatActivity() {
    
    private var isSubmitted = false
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        
        val submitButton = findViewById<Button>(R.id.submit_button)
        val editText = findViewById<EditText>(R.id.input_edit_text)
        
        submitButton.setOnClickListener {
            isSubmitted = true
            val input = editText.text.toString()
            val data = Intent().apply {
                putExtra(FirstActivity.EXTRA_KEY, input)
            }
            setResult(RESULT_OK, data)
            finish()
        }
        
        // 处理返回键
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                returnWithCancel("用户取消了操作")
            }
        })
    }
    
    private fun returnWithCancel(message: String) {
        val data = Intent().apply {
            putExtra(FirstActivity.EXTRA_KEY, message)
        }
        setResult(RESULT_CANCELED, data)
        finish()
    }
    
    // 或者重写onBackPressed(已弃用,但还能用)
    @Deprecated("Deprecated in Java")
    override fun onBackPressed() {
        returnWithCancel("通过onBackPressed返回")
    }
}

使用 Activity Result API

// 第一个Activity
class FirstActivity : AppCompatActivity() {
    
    // 注册Activity Result合约
    private val startForResult = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        when (result.resultCode) {
            RESULT_OK -> {
                val data = result.data?.getStringExtra("result_data")
                val actionType = result.data?.getStringExtra("action_type")
                
                when (actionType) {
                    "submit" -> {
                        Toast.makeText(this, "提交成功: $data", Toast.LENGTH_SHORT).show()
                    }
                    "cancel" -> {
                        Toast.makeText(this, "用户取消: $data", Toast.LENGTH_SHORT).show()
                    }
                    "back" -> {
                        Toast.makeText(this, "用户按返回键: $data", Toast.LENGTH_SHORT).show()
                    }
                }
            }
            RESULT_CANCELED -> {
                Toast.makeText(this, "操作被取消", Toast.LENGTH_SHORT).show()
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startForResult.launch(intent)
        }
    }
}

// 第二个Activity  按钮返回
class SecondActivity : AppCompatActivity() {
    
    private var isSubmitted = false
    private var inputData: String = ""
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        
        val submitButton = findViewById<Button>(R.id.submit_button)
        val cancelButton = findViewById<Button>(R.id.cancel_button)
        val editText = findViewById<EditText>(R.id.input_edit_text)
        
        submitButton.setOnClickListener {
            inputData = editText.text.toString()
            isSubmitted = true
            returnResult("submit", inputData)
        }
        
        cancelButton.setOnClickListener {
            returnResult("cancel", "用户手动取消")
        }
        
        // 处理返回键 - 使用 onBackPressedDispatcher(推荐)
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                returnResult("back", "用户按返回键")
            }
        })
    }
    
    private fun returnResult(actionType: String, data: String) {
        val resultData = Intent().apply {
            putExtra("result_data", data)
            putExtra("action_type", actionType)
        }
        setResult(RESULT_OK, resultData)
        finish()
    }
}

生命周期
#

使用任务(task)来管理Activity , 一个任务就是一组存放在栈里的Activity 的集合,这个栈也被称作返回栈(back stack)。

根据活动的可见性、是否位于栈顶,活动生命周期的四个状态:运行状态、暂停状态、停止状态、销毁状态。

  • _ _onCreate() :第一次创建时调用,Activity 初始化,包括加载布局,绑定事件等。
  • onStart(): 由不可见变为可见
  • onResume(): 准备好与用户进行交互式调用,一般位于返回栈栈顶,此时 Activity 位于前台
  • onPause(): 系统准备启动或恢复另一个 Activity 时调用。释放资源,可在此保留关键数据此时 Activity 不再位于前台(比如在 Activity 上展示了弹窗)
  • onStop(): 完全不可见时调用, 如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执 行,而onStop()方法并不会执行。 (该状态下很有可能被回收)
  • onDestory(): 在Activity被销毁之前调用,之后Activity的状态将变为销毁状 态。
  • onRestart()。这个方法在Activity由停止状态 (onStop()) 变为运行状态之前调用,也就是Activity 被重新启动了。

OnSaveInstance: 活动被回收之前调用,可保留临时数据。使用携带的 Bundle类型的参数 用以保存数据。可以在活动重新创建的onCreate()方法中通过 Bundle 获取。 因此, Intent也可以结合Bundle一起用于传递 数据。

活动的启动模式
#

AndroidManifest.xml 通过 launchMode 属性指定

standard
#

默认模式, 每当启 动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的 Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该 Activity的新实例。

SingleTop(栈顶复用模式)
#

如果实例已经在栈顶,则不会创建新实例,而是调用其 onNewIntent()方法,否则才创建新的实例。

SingleTask(栈内复用模式)
#

整个任务栈中只存在一个实例,如果实例已存在,会清除它上面的所有活动,调用 onNewIntent(),可以设置 taskAffinity(任务相关性)指定任务栈。

SingleInstance(单实例模式)
#

  • 单独在一个新的任务栈中
  • 该任务栈只有这个活动
  • 系统内只存在一个实例
  • 通常用于需要独立运行的活动

页面控件及布局
#

  • 控件可见性: visible、invisible、gone
  • 布局

线性布局 LinearLayout: 在线性方向(水平 horizontal,垂直 vertical)上依次排列

相对布局 RelativeLayout: 相对定位方式(相对父布局、相对页面)让控件出现在任何位置

帧布局 FrameLayout: 所有控件默认在左上角

百分比布局:PrecentFrameLayout 和 PercentRelativeLayout

自定义布局: 定义好后,通过 include 引入;

  • 控件的继承结构

ListView
#

使用 adapter 来设置其中每部分要展示的内容

ArrayAdapter 通过getView()方法convertView参数缓存布局文件以及要操作的控件,以此在提升加载速度

通过 OnItemClickListener 点击事件

  • 自定义控件:

创建自定义布局,逻辑代码中通过 inflate 加载布局文件

引用时通过包名+类名来引用,对于逻辑层加入事件

只能实现纵向滚动的效果

RecyclerView
#

适配器构建

class FruitAdapter(val fruitList: List<Fruit>) : 
RecyclerView.Adapter<FruitAdapter.ViewHolder>() { 

    // view 子项最外层布局
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage) 
        val fruitName: TextView = view.findViewById(R.id.fruitName) 
    } 

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 
        val view = LayoutInflater.from(parent.context) 
            .inflate(R.layout.fruit_item, parent, false) 
        // 此处添加点击事件
        return ViewHolder(view) 
    } 

    // 对子项数据复制
    override fun onBindViewHolder(holder: ViewHolder, position: Int) { 
        val fruit = fruitList[position] 
        holder.fruitImage.setImageResource(fruit.imageId) 
        holder.fruitName.text = fruit.name 
    } 

    override fun getItemCount() = fruitList.size 

} 

ListView的布局排列是由自身去管理的,而RecyclerView 则将这个工作交给了LayoutManagerLayoutManager制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局。

GridLayoutManager可以用于实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。

Fragment
#

  • 可以嵌入在活动中的 UI 片段,每个碎片都有其对于的布局文件、代码文件。
  • 一个活动中可以包括多个碎片,通过 FrameLayout 碎片的切换
  • 在碎片中获取活动实现,碎片与活动,碎片与碎片之间的通讯。

在 Activity 中添加 Fragment

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" > 
 
    <fragment 
        android:id="@+id/leftFrag" 
        android:name="com.example.fragmenttest.LeftFragment" 
        android:layout_width="0dp" 
        android:layout_height="match_parent" 
        android:layout_weight="1" /> 
 
    <fragment 
        android:id="@+id/rightFrag" 
        android:name="com.example.fragmenttest.RightFragment" 
        android:layout_width="0dp" 
        android:layout_height="match_parent" 
        android:layout_weight="1" /> 
 
</LinearLayout> 

通过 FrameLayout 可以实现 Fragment 的动态添加

    private fun replaceFragment(fragment: Fragment) { 
        val fragmentManager = supportFragmentManager 
        val transaction = fragmentManager.beginTransaction() 
        transaction.replace(R.id.rightLayout, fragment) 
        transaction.commit() 
    } 

Fragment 与 Activity 之间的交互
#

Activity 调用 Fragment

调用FragmentManager的findFragmentById()方法,可以在Activity中得到相应 Fragment的实例,可以实现调用特定 Fragment 中方法。

类似于findViewById()方法,kotlin-android-extensions插件也对 findFragmentById()方法进行了扩展,允许我们直接使用布局文件中定义的Fragment id名 称来自动获取相应的Fragment实例

val fragment = leftFrag as LeftFragment 

Fragment 调用 Activity

在每个Fragment中都可以通过调用getActivity()方法来得到 和当前Fragment相关联的Activity实例,代码如下所示:

if (activity != null) { 
    // 将 activity 强制转换为 MainActivity 类型
    val mainActivity = activity as MainActivity 
} 

不同 Fragment 之间的通信

首先在一个Fragment中 可以得到与它相关联的Activity,然后再通过这个Activity去获取另外一个Fragment的实例, 这样就实现了不同Fragment之间的通信功能。

// Activity中定义接口
class MainActivity : AppCompatActivity(), FragmentA.OnDataListener {
    
    fun sendDataToFragmentB(data: String) {
        val fragmentB = supportFragmentManager.findFragmentById(R.id.fragment_b) as? FragmentB
        fragmentB?.updateData(data)
    }
}

// FragmentA
class FragmentA : Fragment() {
    // 定义通信接口
    interface OnDataListener {
        fun onDataSent(data: String)
    }
    
    private var listener: OnDataListener? = null
    
    override fun onAttach(context: Context) {
        // 将Activity转换为OnDataListener接口
        super.onAttach(context)
        listener = context as? OnDataListener
    }

    // 调用Activity方法实现与另一个Fragment通信
    fun sendData() {
        listener?.onDataSent("Hello from FragmentA")
    }
} 

碎片的生命周期
#

Fragment 大多数状态与 Activity 相似。

停止状态: 当一个Activity进入停止状态时,与它相关联的Fragment就会进入停止状态 。通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移 除,但在事务提交之前调用了addToBackStack()方法,这时的Fragment也会进入停止 状态。

销毁状态 : Fragment总是依附于Activity而存在,因当Activity被销毁时,与它相关联的 Fragment就会进入销毁状态。或者通过调用FragmentTransaction的remove()、 replace()方法将Fragment从Activity中移除,但在事务提交之前并没有调用 addToBackStack()方法,这时的Fragment也会进入销毁状态。

动态加载布局
#

比如:根据设备类型(平板 、手机)使用不同的布局

  • 使用限定符 对应不同的文件夹名 (可以利用限定符区分 大小、分辨率、方向)如 layout-large
  • 在逻辑层判断使用的是哪个布局
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 检查是否是双面板布局
        isDualPane = findViewById<View>(R.id.fragment_container_right) != null
        
        if (savedInstanceState == null) {
            if (isDualPane) {
                // 平板设备 - 显示两个Fragment
                setupTabletLayout()
            } else {
                // 手机设备 - 显示一个Fragment
                setupPhoneLayout()
            }
        }
    }

广播机制( BroadcastReceiver )
#

可以跨进程通信

  • 标准广播: 异步,同时受到,无法被打断
  • 有序广播:同步执行 同一时刻只有一个收到

接收系统广播
#

系统广播包括系统电量变化,开机,网路变化等

注册广播
#

动态注册
#

代码中注册,使用完成后需要及时关闭

class MainActivity : AppCompatActivity() { 
    lateinit var timeChangeReceiver: TimeChangeReceiver 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        val intentFilter = IntentFilter() 
        // 设置监听广播类型
        intentFilter.addAction("android.intent.action.TIME_TICK") 
        timeChangeReceiver = TimeChangeReceiver() 
        registerReceiver(timeChangeReceiver, intentFilter) 
    } 
    override fun onDestroy() { 
        super.onDestroy() 
        // 注销接收器
        unregisterReceiver(timeChangeReceiver) 
    } 
    
    inner class TimeChangeReceiver : BroadcastReceiver() { 
        override fun onReceive(context: Context, intent: Intent) { 
            Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
        }
    }
}

静态注册
#

在 AndroidManifest 中注册

对于系统的广播接收,部分需要声明权限。 在Android 8.0系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。**隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。 **

声明权限

 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />  

不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为** BroadcastReceiver中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会出现错误**。

发送广播
#

标准广播
#

自定义 intent + sendBroadcast 方法

val intent = Intent("com.example.broadcasttest.MY_BROADCAST") 
// 指明要发送的程序
intent.setPackage(packageName) 
sendBroadcast(intent) 

有序广播
#

自定义intent + sendOrderedBroadcast 方法

 val intent = Intent("com.example.broadcasttest.MY_BROADCAST") 
intent.setPackage(packageName) 
sendOrderedBroadcast(intent, null)   //  intent,权限相关字符 

高优先级的 Receiver 先接收

<receiver 
  android:name=".MyBroadcastReceiver" 
  android:enabled="true" 
  android:exported="true"> 
  <intent-filter android:priority="100">   
    <action android:name="com.example.broadcasttest.MY_BROADCAST"/> 
  </intent-filter> 
</receiver> 

如果接收器在 onReceive 中逻辑中执行 abortBroadcast() ,将这条广播截断,后面 的BroadcastReceiver将无法再接收到这条广播。

本地广播
#

使用 LocalBroadCast 对广播进行广利,使用其对于的发送和接收方法。

数据持久化技术
#

Android 中的三种数据持久化方法

  • 文件
  • SharedPreferences
  • 数据库

文件
#

  • 存储在应用目录下的 files 文件夹下( 所有的文件都默认存储到/data/data//files/ )
  • 文件流写入 (openFileOutput、openFileInput)
// 写入文件
fun save(inputText: String) { 
    try { 
        val output = openFileOutput("data", Context.MODE_PRIVATE) 
        val writer = BufferedWriter(OutputStreamWriter(output)) 
        writer.use {   // use 代码块结束后,自动关闭 流
            it.write(inputText) 
        } 
    } catch (e: IOException) { 
        e.printStackTrace() 
    } 
} 

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() 
} 

SharedPreferences
#

使用键值对存储数据 数据存储在 包名/shared_prefs/ 以 xml 形式存储

存储数据
#

获取 sharedPreferences 对象
#

  • Context 类中的getSharedPreferences(文件名,操作模式)
  • Activity 类中的getPreferences(操作模式)

存储
#

  • 使用 edit()方法获取 Editor 对象
  • 存取数据对象
  • 使用 apply 方法保存
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit() 
editor.putString("name", "Tom") 
editor.putInt("age", 28) 
editor.putBoolean("married", false) 
editor.apply() 

读取数据
#

  • 使用 get 方法根据 键 获取对应的 值
val prefs = getSharedPreferences("data", Context.MODE_PRIVATE) 
val name = prefs.getString("name", "") 
val age = prefs.getInt("age", 0) 
val married = prefs.getBoolean("married", false) 

SQLite
#

书中提到使用 Android API 、 SQL 语句 以及 LitePal 方法进行数据库的操作,也可以使用 Greendao,对比可以参考下面文章。

Android 中数据库框架GreenDao与LitePal对比、集成、使用详解,greendao与原生SQLite性能对比_litepal和sqlite,greendao哪个好-CSDN博客

SQLiteOpenHelper
#

SQLiteOpenHelper是一个抽象类 ,包含两个抽象方法:onCreate()和 onUpgrade()

  • getReadableDatabase() )与 getWritableDatabase() :当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方 法返回的对象将以只读的方式打开数据库,而getWritableDatabase()方法则将出现异常。

构建出SQLiteOpenHelper的实例之 后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数 据库了,数据库文件会存放在/data/data//databases/目录下。时,重 写的onCreate()方法也会得到执行,所以通常会在这里处理一些创建表的逻辑。

/*
context: 
name: 数据库名
cursor: 在查询数据的时候返回一个自定义的Cursor,一般传入null即可
version: 数据库版本号
*/
class MyDatabaseHelper(val context: Context, name: String, version: Int) : 
        SQLiteOpenHelper(context, name, null, version) {
    // 构建sql语句
    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) { 
    } 
 
} 

借助getReadableDatabase() 或 getWritableDatabase() 返回的 SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了

添加数据
#

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) 
} 
// insert(表名 为空的列自动赋值NULL ContentValues对象)
db.insert("Book", null, values2) // 插入第二条数据 

更新数据
#

val db = dbHelper.writableDatabase 
val values = ContentValues() 
values.put("price", 10.99) 
db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))   // 加入了数据约束

删除数据
#

val db = dbHelper.writableDatabase 
db.delete("Book", "pages > ?", arrayOf("500")) 

查询数据
#

直接使用 SQL 操作数据库

db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", 
    arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96") 
) 

使用事务
#

事务的特性可以保证让一系列的操作要么全部完成, 要么一个都不会完成。

**使用 db.beginTransaction() 然后执行相关数据库 CRUD 操作,之后调用 db.endTransaction() **

数据库升级
#

当指定的数据库版本号大于当前数据库版本号的时候,就会进入onUpgrade()方法中执 行更新操作。 可以 **为每一个版本号赋予其所对应的数据库变动,然后在onUpgrade()方法 中对当前数据库的版本号进行判断,再执行相应的改变就可以了。 **

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") 
    } 
} 

内容提供器 ContentProvider
#

主要用于不同的应用程序之间实现数据共享功能。

例如电话簿程序共享联系人

运行时权限
#

运行时权限: Android 6.0系统中加入了运行时权限功 能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的 **使用过程中再对某一项权限申请进行授权。 **

  • 普通权限指的是那些不会直接威胁到用户的安全和隐私的权限, 系统会自动授权
  • 危险权限表示那些可能会触及用户隐私或者对设备安全性造成影响的权限需要用户授权

开发时

  • 判断用户是否授权 checkSelfPermission
  • 授权则继续运行,否则就申请授权 requestPermission
class MainActivity : AppCompatActivity() { 
 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
                makeCall.setOnClickListener { 
            // 判断用户是否授权
            if (ContextCompat.checkSelfPermission(this, 
                Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { 
                // 未授权时则需要请求对应权限
                ActivityCompat.requestPermissions(this, 
                    arrayOf(Manifest.permission.CALL_PHONE), 1) 
            } else { 
                call() 
            } 
        } 
    } 

    //  未授权 ->  请求对应权限 -> 请求结果的回调
    // grantResults 为 请求结果
    override fun onRequestPermissionsResult(requestCode: Int, 
            permissions: Array<String>, grantResults: IntArray) { 
        super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
        when (requestCode) { 
            1 -> { 
                if (grantResults.isNotEmpty() && 
                    grantResults[0] == PackageManager.PERMISSION_GRANTED) { 
                    call() 
                } else { 
                    Toast.makeText(this, "You denied the permission", 
                        Toast.LENGTH_SHORT).show() 
                } 
            } 
        } 
    } 
 
    private fun call() { 
        try { 
            val intent = Intent(Intent.ACTION_CALL) 
            intent.data = Uri.parse("tel:10086") 
            startActivity(intent) 
        } catch (e: SecurityException) { 
            e.printStackTrace() 
        } 
    } 
 
}

访问其他程序中的数据
#

ContentResolver
#

通过 getContentResolver 获取 ContentResolver 实例

不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而使用一个Uri参数代替,这个参数被称为内容URI。 内容URI给ContentProvider中的数据建立 了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的 , **path则是用于对同一应用程序中不同的表做区分的,通常会添 加到authority的后面。 **

val uri = Uri.parse("content://com.example.app.provider/table1") 
// 通过 query 查询数据
val cursor = contentResolver.query( 
    uri, 
    projection, 
    selection, 
    selectionArgs, 
    sortOrder) 

查询联系人数据

        // 查询联系人数据 
        contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, 
                null, null, null, null)?.apply { 
            while (moveToNext()) { 
                // 获取联系人姓名 
                val displayName = getString(getColumnIndex( 
                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)) 
                // 获取联系人手机号 
                val number = getString(getColumnIndex( 
                ContactsContract.CommonDataKinds.Phone.NUMBER)) 
                contactsList.add("$displayName\n$number") 
            } 
            adapter.notifyDataSetChanged() 
            close() 
        } 

创建内容提供器
#

创建类继承自 ContentProvider,重写下面方法

  • onCreate 初始化调用
  • query 查询,根据 uri 参数确定查哪张表
  • insert 插入
  • update 更新已有数据
  • delete 删除
  • getType 获取 uri 返回相应的 MIME 类型
class MyProvider : ContentProvider() { 
    // 初始化调用,数据库创建与升级
    override fun onCreate(): Boolean { 
        return false 
    } 
    // 查询数据 
    // uri -- 表   projection -- 列 selection和selectionArgs -- 约束参数  sortOrder -- 结果排序  
    override fun query(uri: Uri, projection: Array<String>?, selection: String?, 
            selectionArgs: Array<String>?, sortOrder: String?): Cursor? { 
        return null 
    }
    // 添加一条数据
    override fun insert(uri: Uri, values: ContentValues?): Uri? { 
        return null 
    } 
} 
    override fun update(uri: Uri, values: ContentValues?, selection: String?, 
            selectionArgs: Array<String>?): Int { 
        return 0 
    } 
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { 
        return 0 
    } 
    // 获取Uri对象所对应的MIME类型
    override fun getType(uri: Uri): String? { 
        return null 
    }

可以使用通配符分别匹配这两种格式的内容URI :

  • *表示匹配任意长度的任意字符
  • #表示匹配任意长度的数字

借助UriMatcher实现匹配内容URI

class MyProvider : ContentProvider() { 
 
    private val table1Dir = 0 
    private val table1Item = 1 
    private val table2Dir = 2 
    private val table2Item = 3 
 
    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) 
 
    init { 
        uriMatcher.addURI("com.example.app.provider", "table1", table1Dir) 
        uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item) 
        uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir) 
        uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item) 
    } 
    ... 
    override fun query(uri: Uri, projection: Array<String>?, selection: String?, 
            selectionArgs: Array<String>?, sortOrder: String?): Cursor? { 
        when (uriMatcher.match(uri)) { 
            table1Dir -> { 
                // 查询table1表中的所有数据 
            } 
            table1Item -> { 
                // 查询table1表中的单条数据 
            } 
            table2Dir -> { 
                // 查询table2表中的所有数据 
            } 
            table2Item -> { 
                // 查询table2表中的单条数据 
            } 
        } 
        ... 
    } 
    ... 
} 

getType 用以获取Uri对象所对应的MIME类型

URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。

  • 必须以vnd开头。
  • 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/。
  • 最后接上vnd..。

手机多媒体
#

通知
#

通知渠道(8.0 以上版本): 每条通知都要属于一个对应的渠道。每个应用程序都可以自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用 户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知 。

通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。 开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认 可这个重要等级的话,可以随时进行修改,开发者对无权再进行调整和变更

创建通知渠道
#

  • 获取通知实例 NotificationManager
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 
    // channelId--渠道ID channelName--渠道名称,表明渠道作用 importance--初始的重要等级
    val channel = NotificationChannel(channelId, channelName, importance) 
    manager.createNotificationChannel(channel) 
} 
  • 创建通知对象,加入 Intent 执行通知点击操作
val notification = NotificationCompat.Builder(context, channelId) 
    .setContentTitle("This is content title")
    .setContentText("This is content text") 
    .setSmallIcon(R.drawable.small_icon) 
    .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon)) 
    .build() 
    
manager.notify(1, notification) 

PendingIntent
#

PendingIntent倾向于在某个合适的时机执行某个动作。所以,也可以把PendingIntent简单地理解为延迟执行的Intent。

NotificationCompat.Builder 存在一个 setContentIntent() 方法, 接收的参数是一个PendingIntent对象。 可以通过PendingIntent构建一个 延迟执行的“意图”,当用户点击这条通知时就会执行相应的逻辑

取消通知的两种方式

  • setAutoCancel
  • manager.cancel

示例

class MainActivity : AppCompatActivity() { 

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as 
        NotificationManager 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 
            val channel = NotificationChannel("normal", "Normal",NotificationManager. 
                                              IMPORTANCE_DEFAULT) 
            manager.createNotificationChannel(channel) 
        } 
        sendNotice.setOnClickListener { 
            val intent = Intent(this, NotificationActivity::class.java) 
            val pi = PendingIntent.getActivity(this, 0, intent, 0) 
            val notification = NotificationCompat.Builder(this, "normal") 
                .setContentTitle("This is content title") 
                .setContentText("This is content text") 
                .setSmallIcon(R.drawable.small_icon) 
                .setLargeIcon(BitmapFactory.decodeResource(resources, 
                                                           R.drawable.large_icon)) 
                // 设置 通知点击跳转Intent
                .setContentIntent(pi) 
                // 点击通知后自动取消 
                .setAutoCancel(true) 
                .build() 
            manager.notify(1, notification) 
        } 
    } 
} 
  • setStyle() 支持富文本

摄像头,音视频
#

摄像头拍照
#

class MainActivity : AppCompatActivity() { 
    val takePhoto = 1 
    lateinit var imageUri: Uri 
    lateinit var outputImage: File 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        takePhotoBtn.setOnClickListener { 
            // 创建File对象,用于存储拍照后的图片 
            outputImage = File(externalCacheDir, "output_image.jpg") 
            if (outputImage.exists()) { 
                outputImage.delete() 
            } 
            outputImage.createNewFile() 
            imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                // 将File对象转换成一个封装过的Uri对象 
                FileProvider.getUriForFile(this, "com.example.cameraalbumtest.
                                           fileprovider", outputImage) 
            } else { 
                Uri.fromFile(outputImage) 
            } 
            // 启动相机程序 
            val intent = Intent("android.media.action.IMAGE_CAPTURE") 
            // 指定图片输出地址
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri) 
            startActivityForResult(intent, takePhoto) 
        } 
    } 
    // 传入返回的结果
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 
        super.onActivityResult(requestCode, resultCode, data) 
        when (requestCode) { 
            takePhoto -> { 
                if (resultCode == Activity.RESULT_OK) { 
                    // 将拍摄的照片显示出来 
                    val bitmap = BitmapFactory.decodeStream(contentResolver. 
                        openInputStream(imageUri)) 
                    imageView.setImageBitmap(rotateIfRequired(bitmap)) 
                } 
            } 
        } 
    } 
    // 根据图片信息确定是否需要旋转图片
    private fun rotateIfRequired(bitmap: Bitmap): Bitmap { 
        val exif = ExifInterface(outputImage.path) 
        val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 
            ExifInterface.ORIENTATION_NORMAL) 
        return when (orientation) { 
            ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90) 
            ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180) 
            ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270) 
            else -> bitmap 
        } 
    } 
    private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap { 
        val matrix = Matrix() 
        matrix.postRotate(degree.toFloat()) 
        val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, 
            matrix, true) 
        bitmap.recycle() // 将不再需要的Bitmap对象回收 
        return rotatedBitmap 
    } 
}

通过相册选择图片
#

class MainActivity : AppCompatActivity() { 
    ... 
    val fromAlbum = 2 
    override fun onCreate(savedInstanceState: Bundle?) { 
        ... 
        fromAlbumBtn.setOnClickListener { 
            // 打开文件选择器 
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) 
            intent.addCategory(Intent.CATEGORY_OPENABLE) 
            // 指定只显示图片 
            intent.type = "image/*" 
            // fromAlbum:当选择完图片回到onActivityResult()方法时,
            // 就会进入fromAlbum的条件下处理图片。
            startActivityForResult(intent, fromAlbum) 
        } 
    } 
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 
        super.onActivityResult(requestCode, resultCode, data) 
        when (requestCode) { 
            ... 
            fromAlbum -> { 
                if (resultCode == Activity.RESULT_OK && data != null) { 
                    data.data?.let { uri -> 
                        // 将选择的图片显示 
                        val bitmap = getBitmapFromUri(uri) 
                        imageView.setImageBitmap(bitmap) 
                    } 
                } 
            } 
        } 
    } 
    private fun getBitmapFromUri(uri: Uri) = contentResolver 
        .openFileDescriptor(uri, "r")?.use { 
        BitmapFactory.decodeFileDescriptor(it.fileDescriptor) 
    } 
    ... 
} 

播放多媒体文件
#

在Android中播放音频文件一般是使用MediaPlayer类实现的

播放音频(视频与之相似)

class MainActivity : AppCompatActivity() { 
 
    private val mediaPlayer = MediaPlayer() 
 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        initMediaPlayer() 
        play.setOnClickListener { 
            if (!mediaPlayer.isPlaying) { 
                mediaPlayer.start() // 开始播放 
            } 
        } 
        pause.setOnClickListener { 
            if (mediaPlayer.isPlaying) { 
                mediaPlayer.pause() // 暂停播放 
            } 
        } 
        stop.setOnClickListener { 
            if (mediaPlayer.isPlaying) { 
                mediaPlayer.reset() // 停止播放 
                initMediaPlayer() 
            } 
        } 
    } 
 
    private fun initMediaPlayer() { 
        val assetManager = assets 
        val fd = assetManager.openFd("music.mp3") 
        mediaPlayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) 
        mediaPlayer.prepare() 
    } 
 
    override fun onDestroy() { 
        super.onDestroy() 
        mediaPlayer.stop() 
        mediaPlayer.release() 
    } 
 
} 

网络
#

使用 webView 显示网页
#

 webView.settings.javaScriptEnabled=true
 webView.webViewClient = WebViewClient()
 webView.loadUrl("https://www.baidu.com")

HttpURLConnection
#

Get 方法
#

// 调用实例
val url = URL("https://www.baidu.com")
val connection = url.openConnection() as HttpURLConnection
// 定义请求方法
connection.requestMethod = "GET"
// 定义请求头
connection.connectTimeout = 8000
connection.readTimeout = 8000
// 获取输入流
val input = connection.inputStream
connection.disconnect()

Post 方法
#

connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
output.writeBytes("username=admin&password=123456")

OKHttp
#

通过 OKHttp 可以组件 GET POST 等请求

Get
#

val client = OkHttpClient()
// 创建Request对象
val request = Request.Builder().build()
val request = Request.Builder()
     .url("https://www.baidu.com")
     .build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()

Post
#

// 构建待提交参数
val requestBody = FormBody.Builder()
    .add("username", "admin")
    .add("password", "123456")
    .build()
val request = Request.Builder()
    .url("https://www.baidu.com")
    .post(requestBody)
    .build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()

XML 解析
#

网路传输数据的两种格式 XML 和 JSON。(其实当前 JSON 更多)

Pull 解析
#

XML 数据

<apps>
  <app>
    <id>1</id>
    <name>Google Maps</name>
    <version>1.0</version>
  </app>
  <app>
    <id>2</id>
    <name>Chrome</name>
    <version>2.1</version>
  </app>
  <app>
    <id>3</id>
    <name>Google Play</name>
    <version>2.3</version>
  </app>
</apps>

解析代码

// 通过网络等获取的 XML数据
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
    parseXMLWithPull(responseData)
}
// 解析XML数据
private fun parseXMLWithPull(xmlData: String) {
    try {
        // 创建实例,输入数据
        val factory = XmlPullParserFactory.newInstance()
        val xmlPullParser = factory.newPullParser()
        xmlPullParser.setInput(StringReader(xmlData))
        // 获取当前解析时间
        var eventType = xmlPullParser.eventType
        var id = ""
        var name = ""
        var version = ""
        // 循环节点解析
        while (eventType != XmlPullParser.END_DOCUMENT) {
            val nodeName = xmlPullParser.name
            when (eventType) {
                // 开始解析某个节点
                XmlPullParser.START_TAG -> {
                    when (nodeName) {
                        "id" -> id = xmlPullParser.nextText()
                        "name" -> name = xmlPullParser.nextText()
                        "version" -> version = xmlPullParser.nextText()
                    }
                }
                // 完成解析某个节点
                XmlPullParser.END_TAG -> {
                    if ("app" == nodeName) {
                        Log.d("MainActivity", "id is $id")
                        Log.d("MainActivity", "name is $name")
                        Log.d("MainActivity", "version is $version")
                    }
                }
            }
            eventType = xmlPullParser.next()
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
}

当 XML 数据发生变化时(更多的子节点),代码就需要修改。

SAX 解析
#

要使用SAX解析,通常情况下会新建一个类继承自DefaultHandler,并重写父类的5个 方法

class ContentHandler : DefaultHandler() {
    private var nodeName = ""
    private lateinit var id: StringBuilder
    private lateinit var name: StringBuilder
    private lateinit var version: StringBuilder
    // 初始化
    override fun startDocument() {
        id = StringBuilder()
        name = StringBuilder()
        version = StringBuilder()
    }
    override fun startElement(uri: String, localName: String, qName: String, attributes:
                           Attributes) {
        // 记录当前节点名
        nodeName = localName
        Log.d("ContentHandler", "uri is $uri")
        Log.d("ContentHandler", "localName is $localName")
        Log.d("ContentHandler", "qName is $qName")
        Log.d("ContentHandler", "attributes is $attributes")
    }
    override fun characters(ch: CharArray, start: Int, length: Int) {
        // 根据当前节点名判断将内容添加到哪一个StringBuilder对象中
        when (nodeName) {
            "id" -> id.append(ch, start, length)
            "name" -> name.append(ch, start, length)
            "version" -> version.append(ch, start, length)
        }
    }
    override fun endElement(uri: String, localName: String, qName: String) {
        if ("app" == localName) {
            Log.d("ContentHandler", "id is ${id.toString().trim()}")
            Log.d("ContentHandler", "name is ${name.toString().trim()}")
            Log.d("ContentHandler", "version is ${version.toString().trim()}")
            // 最后要将StringBuilder清空
            id.setLength(0)
            name.setLength(0)
            version.setLength(0)
        }
    }
    override fun endDocument() {
    }
}

调用

private fun parseXMLWithSAX(xmlData: String) {
    try {
        val factory = SAXParserFactory.newInstance()
        val xmlReader = factory.newSAXParser().XMLReader
        val handler = ContentHandler()
        // 将ContentHandler的实例设置到XMLReader中
        xmlReader.contentHandler = handler
        // 开始执行解析
        xmlReader.parse(InputSource(StringReader(xmlData)))
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

JSON 解析
#

示例数据

[{"id":"5","version":"5.5","name":"Clash of Clans"},
  {"id":"6","version":"7.0","name":"Boom Beach"},
  {"id":"7","version":"3.5","name":"Clash Royale"}]

JSONObject
#

private fun parseJSONWithJSONObject(jsonData: String) {
    try {
        val jsonArray = JSONArray(jsonData)
        for (i in 0 until jsonArray.length()) {
            val jsonObject = jsonArray.getJSONObject(i)
            val id = jsonObject.getString("id")
            val name = jsonObject.getString("name")
            val version = jsonObject.getString("version")
            Log.d("MainActivity", "id is $id")
            Log.d("MainActivity", "name is $name")
            Log.d("MainActivity", "version is $version")
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

GSON
#

自动转换为对象

val gson = Gson()
val person = gson.fromJson(jsonData, Person::class.java)

对于数组

val typeOf = object : TypeToken<List<Person>>() {}.type
val people = gson.fromJson<List<Person>>(jsonData, typeOf)

网络请求回调
#

HttpUtil.sendOkHttpRequest(address, object : Callback {
    override fun onResponse(call: Call, response: Response) {
        // 得到服务器返回的具体内容
        val responseData = response.body?.string()
    }
    override fun onFailure(call: Call, e: IOException) {
        // 在这里对异常情况进行处理
    }
})

Retrofit
#

  • 配置一次根路径,后续接口地址使用相对路径即可
  • 可以对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接 口文件当中
  • 将服务器返回的 JSON数据自动解析成对象

创建 APP 类

class App(val id: String, val name: String, val version: String) 

根据功能创建不同种类的接口文件,其中对应具体服务器的接口方法

// 命名: 以具体的功能种类名开头,以Service结尾
interface AppService { 
    // 返回值必须声明成Retrofit中内置的Call类型
    @GET("get_data.json") 
    fun getAppData(): Call<List<App>> 
} 

使用

// 构建对象并设置根路径
val retrofit = Retrofit.Builder() 
    .baseUrl("http://10.0.2.2/") 
    .addConverterFactory(GsonConverterFactory.create())   // 指定解析库
    .build() 
// 创建接口的动态代理对象  Retrofit会自动检查并通过创建代理对象实现接口。调用方法时由代理对象
// 截调用并执行网络请求
val appService = retrofit.create(AppService::class.java) 
appService.getAppData().enqueue(object : Callback<List<App>> { 
    override fun onResponse(call: Call<List<App>>, 
                                        response: Response<List<App>>) { 
        val list = response.body() 
        if (list != null) { 
            for (app in list) { 
                Log.d("MainActivity", "id is ${app.id}") 
                Log.d("MainActivity", "name is ${app.name}") 
                Log.d("MainActivity", "version is ${app.version}") 
            } 
        } 
    } 
    override fun onFailure(call: Call<List<App>>, t: Throwable) { 
        t.printStackTrace() 
    } 
}) 

复杂接口处理
#

interface ExampleService { 
     // 接口动态变化
    @GET("{page}/get_data.json") 
    fun getData(@Path("page") page: Int): Call<Data> 

    // get方法传参: GET http://example.com/get_data.json?u=<user>&t=<token> 
    @GET("get_data.json") 
    fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data> 

    @DELETE("data/{id}") 
    fun deleteData(@Path("id") id: String): Call<ResponseBody> 
} 

对于服务器响应的数据并不关心时,可以使用ResponseBody,表示Retrofit能够接收任意类型的响应数据,并且不会对 响应数据进行解析。

interface ExampleService { 
    @POST("data/create") 
    fun createData(@Body data: Data): Call<ResponseBody> 

    // 自定义 Header
    @Headers("User-Agent: okhttp", "Cache-Control: max-age=0") 
    @GET("get_data.json") 
    fun getData(): Call<Data> 

    // Header 的动态声明
    @GET("get_data.json") 
    fun getData(@Header("User-Agent") userAgent: String, 
        @Header("Cache-Control") cacheControl: String): Call<Data> 
} 
object ServiceCreator { 
    // 构建单例类避免retrofit重复创建
    private const val BASE_URL = "http://10.0.2.2/" 
    private val retrofit = Retrofit.Builder() 
        .baseUrl(BASE_URL) 
        .addConverterFactory(GsonConverterFactory.create()) 
        .build() 

     fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass) 
} 

创建方法可简化为

object ServiceCreator { 
    ... 
    inline fun <reified T> create(): T = create(T::class.java) 
    //  inline: 确保编译器知道要替换的类型
    // reified:告诉编译器保留类型信息,允许在函数体内访问类型信息
} 

// 使用
val appService = ServiceCreator.create<AppService>() 

服务
#

  • Android 中实现程序后台运行的解决方案
  • 当应用程序进程被杀掉时,所有依赖于该进程的服务也会停止
  • Service并不会自动开启线程,所有的代码 都是默认运行在主线程当中的。也就是说,我们需要在Service的内部手动创建子线程,并在这 里执行具体的任务,否则就有可能出现主线程被阻塞的情况。

Android 多线程编程
#

  • 继承 Thread 类
class MyThread : Thread() {
    override fun run() {
        // 编写具体的逻辑
    }
}

// 启动
MyThread().start()
  • 实现 Runnable 接口
class MyThread : Runnable {
    override fun run() {
        // 编写具体的逻辑
    }
}

// 启动
val myThread = MyThread()
Thread(myThread).start()
  • 编写匿名类
Thread {
    // 编写具体的逻辑
}.start()

子线程中更新 UI
#

Android 中不允许在子线程中更新 UI

通过异步消息处理机制,实现控件的更新。

class MainActivity : AppCompatActivity() {
    val updateText = 1
    // Handler 被绑定到了主线程的消息队列(MessageQueue)上
    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // 在这里可以进行UI操作 when语句
            when (msg.what) {
                updateText -> textView.text = "Nice to meet you"
            }
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        changeTextBtn.setOnClickListener {
            thread {
                val msg = Message()
                msg.what = updateText
                // 发送至主线程的消息队列
                handler.sendMessage(msg) // 将Message对象发送出去
            }
        }
    }
}

异步消息处理的构成

  • Message: 线程之间传递的消息
  • Handler: 发送和处理消息 sendMessage()方法、post()方法 发送消息,传递到 handleMessage()方法 进行处理
  • MessageQueue:消息队列,存放所有 Handler 的消息,每个线程中只有一个
  • Looper: 管理线程 中的消息队列

AsyncTask 使用

  • onProExecute 执行前调用
  • doInBackground 子线程中运行的
  • onProgressUpdate 响应后台中 publishProgress是 更新 UI
  • onPostExecute 任务执行完毕后的操作
// 接收参数: 传入参数,进度,返回值
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
    override fun onPreExecute() {
        progressDialog.show() // 显示进度对话框
    }
    override fun doInBackground(vararg params: Unit?) = try {
        while (true) {
            val downloadPercent = doDownload() // 这是一个虚构的方法
            // 更新UI等操作
            publishProgress(downloadPercent)
            if (downloadPercent >= 100) {
                break
            }
        }
        true
    } catch (e: Exception) {
        false
    }
    // 响应更新UI
    override fun onProgressUpdate(vararg values: Int?) {
        // 在这里更新下载进度
        progressDialog.setMessage("Downloaded ${values[0]}%")
    }
    override fun onPostExecute(result: Boolean) {
        progressDialog.dismiss()// 关闭进度对话框
        // 在这里提示下载结果
        if (result) {
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show()
        }
    }
}

服务
#

class MyService : Service() {
    ...
    override fun onCreate() {
        super.onCreate()
    }
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        return super.onStartCommand(intent, flags, startId)
    }
    override fun onDestroy() {
        super.onDestroy()
    }
}
  • 定义后通过 Intent 启动或停止(startService、stopService)
  • 服务实例只有一个,但每次调用 startService,onStartCommand 就会执行一次

服务的启动和停止

startServiceBtn.setOnClickListener {
    val intent = Intent(this, MyService::class.java)
    startService(intent) // 启动Service
}
stopServiceBtn.setOnClickListener {
    val intent = Intent(this, MyService::class.java)
    stopService(intent) // 停止Service
}

**前台服务:**服务中加入通知(通知栏展示)并调用 startForeground

**IntentService:**异步、自动停止

活动与服务的通信
#

  • 构建 Binder 对象,在 Service 时使用 onBind 方法。
class MyService : Service() {
    private val mBinder = DownloadBinder()
    class DownloadBinder : Binder() {
        fun startDownload() {
            Log.d("MyService", "startDownload executed")
        }
        fun getProgress(): Int {
            Log.d("MyService", "getProgress executed")
            return 0
        }
    }
    override fun onBind(intent: Intent): IBinder {
        return mBinder
    }
        ...
}
  • 活动中调用 Binder 方法。当一个Activity和Service绑定了之后,就可以调用该Service里的Binder提供的方法了。
class MainActivity : AppCompatActivity() {
    lateinit var downloadBinder: MyService.DownloadBinder
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            downloadBinder = service as MyService.DownloadBinder
            downloadBinder.startDownload()
            downloadBinder.getProgress()
        }
        override fun onServiceDisconnected(name: ComponentName) {
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        bindServiceBtn.setOnClickListener {
            val intent = Intent(this, MyService::class.java)
            bindService(intent, connection, Context.BIND_AUTO_CREATE) // 绑定Service
        }
        unbindServiceBtn.setOnClickListener {
            unbindService(connection) // 解绑Service
        }
    }
}

服务的周期
#

生命周期流程:
<font style="color:rgb(6, 10, 38);">onCreate()</font> ➡️ <font style="color:rgb(6, 10, 38);">onStartCommand()</font> ➡️ … (服务运行中) … ➡️ <font style="color:rgb(6, 10, 38);">onDestroy()</font>

通过 bindService() 绑定的生命周期
#

<font style="color:rgb(6, 10, 38);">onCreate()</font> ➡️ <font style="color:rgb(6, 10, 38);">onBind()</font> ➡️ … (服务运行中,可进行交互) … ➡️ <font style="color:rgb(6, 10, 38);">onUnbind()</font> ➡️ <font style="color:rgb(6, 10, 38);">onDestroy()</font>

  • **onBind()**:当客户端绑定到服务时调用。必须在这个方法中返回一个 IBinder 接口实例,以便客户端能通过它与服务通信。
  • **onUnbind()**:当所有客户端都与服务解除绑定时调用。

前台服务
#

从Android 8.0系统开始,只有当应用保持在前台可见状态的情况下,Service 才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。

class MyService : Service() {
    ...
    override fun onCreate() {
        super.onCreate()
        // 在 MainActivity(或其他组件)中通过调用 startForegroundService() 被启动的
        Log.d("MyService", "onCreate executed")
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
        NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel("my_service", "前台Service通知",
                                   NotificationManager.IMPORTANCE_DEFAULT)
            manager.createNotificationChannel(channel)
        }
        val intent = Intent(this, MainActivity::class.java)
        val pi = PendingIntent.getActivity(this, 0, intent, 0)
        val notification = NotificationCompat.Builder(this, "my_service")
            .setContentTitle("This is content title")
            .setContentText("This is content text")
            .setSmallIcon(R.drawable.small_icon)
            .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
            .setContentIntent(pi)
            .build()
        // 改用 startForeground 使得服务转为前台服务
        startForeground(1, notification)
    }
        ...
}

IntentService(已启用)
#

异步的、会自动停止的Service (避免 ANR 以及 线程忘记关闭的问题)

IntentService 在 Android API 30(Android 11)中被正式标记为 deprecated,虽然旧代码仍可运行,但未来版本可能彻底移除。

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        // 打印当前线程的id   已经是在子线程了
        Log.d("MyIntentService", "Thread id is ${Thread.currentThread().name}")
    }
    override fun onDestroy() {
        super.onDestroy()
        Log.d("MyIntentService", "onDestroy executed")
    }
}

基于位置的服务
#

  • 申请 API Key 并下载 SDK
  • 权限声明
  • 获取位置信息包括: 经纬度、地址、地图

Material Design
#

ToolBar
#

在 style.xml 中文件, 指定一个不带ActionBar的主题,通 常有Theme.AppCompat.NoActionBar 和Theme.AppCompat.Light.NoActionBar这两种 主题可选。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
  <!-- 定义新的命名空间以保证兼容性 -->
  xmlns:app="http://schemas.android.com/apk/res-auto" 
  android:layout_width="match_parent" 
  android:layout_height="match_parent"> 
  <androidx.appcompat.widget.Toolbar 
    android:id="@+id/toolbar" 
    android:layout_width="match_parent" 
    android:layout_height="?attr/actionBarSize" 
    android:background="@color/colorPrimary" 
    <!-- 指定使用的主题 -->
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" 
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> 
</FrameLayout> 

在 Activity 中添加 setSupportActionBar(toolbar)

滑动菜单 DrawerLayout
#

  • 第一个子控件显示主页面内容,第二个为滑动菜单中内容
<androidx.drawerlayout.widget.DrawerLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto" 
    android:id="@+id/drawerLayout" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
    <FrameLayout 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"> 
        <androidx.appcompat.widget.Toolbar 
            android:id="@+id/toolbar" 
            android:layout_width="match_parent" 
            android:layout_height="?attr/actionBarSize" 
            android:background="@color/colorPrimary" 
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" 
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> 
    </FrameLayout> 
    <TextView 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:layout_gravity="start" 
        android:background="#FFF" 
        android:text="This is menu" 
        android:textSize="30sp" /> 
</androidx.drawerlayout.widget.DrawerLayout> 

第二个子空间中, ,layout_gravity这个属性是必须指定的 ,以表明活动菜单时左滑还是右化。

class MainActivity : AppCompatActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        setSupportActionBar(toolbar) 
        supportActionBar?.let { 
            // 显示按钮
            it.setDisplayHomeAsUpEnabled(true) 
            // 设置图标篇
            it.setHomeAsUpIndicator(R.drawable.ic_menu) 
        } 
    } 
        ... 
    override fun onOptionsItemSelected(item: MenuItem): Boolean { 
        when (item.itemId) { 
            android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START) 
                ... 
        } 
        return true 
    } 
} 

NavigationView#

可以自定义滑动菜单中顶部和菜单布局

<androidx.drawerlayout.widget.DrawerLayout 
  xmlns:android="http://schemas.android.com/apk/res/android" 
  xmlns:app="http://schemas.android.com/apk/res-auto" 
  android:id="@+id/drawerLayout" 
  android:layout_width="match_parent" 
  android:layout_height="match_parent"> 

  <FrameLayout 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
    <androidx.appcompat.widget.Toolbar 
      android:id="@+id/toolbar" 
      android:layout_width="match_parent" 
      android:layout_height="?attr/actionBarSize" 
      android:background="@color/colorPrimary" 
      android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" 
      app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> 
  </FrameLayout> 
  <com.google.android.material.navigation.NavigationView 
    android:id="@+id/navView" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:layout_gravity="start" 
    <!-- 菜单 -->
    app:menu="@menu/nav_menu" 
    <!-- 导航头像 -->
    app:headerLayout="@layout/nav_header"/> 
</androidx.drawerlayout.widget.DrawerLayout> 

FloatingActionButton 悬浮按钮
#

<com.google.android.material.floatingactionbutton.FloatingActionButton 
  android:id="@+id/fab" 
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content" 
  android:layout_gravity="bottom|end" 
  android:layout_margin="16dp" 
  android:src="@drawable/ic_done" /> 

对应其可以添加点击事件

SnackBar 提示并增加可操作按钮
#

fab.setOnClickListener { view -> 
    Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT) 
        .setAction("Undo") { 
            Toast.makeText(this, "Data restored", Toast.LENGTH_SHORT).show() 
        } 
        .show() 
} 

CoordinatorLayout
#

监听所有子控件的时间,自动做出合理响应

CardVuiew 卡片布局
#

<com.google.android.material.card.MaterialCardView 
  xmlns:android="http://schemas.android.com/apk/res/android" 
  xmlns:app="http://schemas.android.com/apk/res-auto" 
  android:layout_width="match_parent" 
  android:layout_height="wrap_content" 
  android:layout_margin="5dp" 
  app:cardCornerRadius="4dp"> 

  <LinearLayout 
    android:orientation="vertical" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content"> 

    <ImageView 
      android:id="@+id/fruitImage" 
      android:layout_width="match_parent" 
      android:layout_height="100dp" 
      android:scaleType="centerCrop" /> 

    <TextView 
      android:id="@+id/fruitName" 
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center_horizontal" 
      android:layout_margin="5dp" 
      android:textSize="16sp" /> 
  </LinearLayout> 

</com.google.android.material.card.MaterialCardView> 

AppBarLayout
#

AppBarLayout实际 上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一些 Material Design的设计理念。

<androidx.drawerlayout.widget.DrawerLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto" 
    android:id="@+id/drawerLayout" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
 
    <androidx.coordinatorlayout.widget.CoordinatorLayout 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"> 
 
        <com.google.android.material.appbar.AppBarLayout 
            android:layout_width="match_parent" 
            android:layout_height="wrap_content"> 
 
            <androidx.appcompat.widget.Toolbar 
                android:id="@+id/toolbar" 
                android:layout_width="match_parent" 
                android:layout_height="?attr/actionBarSize" 
                android:background="@color/colorPrimary" 
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" 
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
              <!-- 通过app:layout_scrollFlags响应滚动事件 -->
                app:layout_scrollFlags="scroll|enterAlways|snap" /> 
 
        </com.google.android.material.appbar.AppBarLayout> 
 
        <androidx.recyclerview.widget.RecyclerView 
            android:id="@+id/recyclerView" 
            android:layout_width="match_parent" 
            android:layout_height="match_parent" 
          <!-- 指定布局行为 -->
            app:layout_behavior="@string/appbar_scrolling_view_behavior" /> 
        ... 
    </androidx.coordinatorlayout.widget.CoordinatorLayout> 
    ... 
</androidx.drawerlayout.widget.DrawerLayout> 

当RecyclerView滚动的时候就已经将滚动事件通知给AppBarLayout 。

SwipeRefreshLayout 下拉刷新
#

SwipeRefreshLayout用于实现下拉刷新功能的核心类,把想要实现下拉刷新功能的 控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。

<androidx.drawerlayout.widget.DrawerLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto" 
    android:id="@+id/drawerLayout" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
 
    <androidx.coordinatorlayout.widget.CoordinatorLayout 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"> 
        ... 
        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout 
            android:id="@+id/swipeRefresh" 
            android:layout_width="match_parent" 
            android:layout_height="match_parent" 
            app:layout_behavior="@string/appbar_scrolling_view_behavior"> 
 
            <androidx.recyclerview.widget.RecyclerView 
                android:id="@+id/recyclerView" 
                android:layout_width="match_parent" 
                android:layout_height="match_parent" 
                app:layout_behavior="@string/appbar_scrolling_view_behavior" /> 
 
        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> 
        ... 
    </androidx.coordinatorlayout.widget.CoordinatorLayout> 
    ... 
</androidx.drawerlayout.widget.DrawerLayout> 

下拉刷新逻辑

class MainActivity : AppCompatActivity() { 
    ... 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
            ... 
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary) 
        swipeRefresh.setOnRefreshListener { 
            refreshFruits(adapter) 
        } 
    } 
} 
private fun refreshFruits(adapter: FruitAdapter) { 
    thread { 
        Thread.sleep(2000) 
        runOnUiThread { 
            initFruits() 
            adapter.notifyDataSetChanged() 
            swipeRefresh.isRefreshing = false 
        } 
    } 
} 

可折叠式标题栏
#

CollapsingToolbarLayout
#

CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为 AppBarLayout的直接子布局来使用。而AppBarLayout又必须是CoordinatorLayout的子布局

<androidx.coordinatorlayout.widget.CoordinatorLayout 
  xmlns:android="http://schemas.android.com/apk/res/android" 
  xmlns:app="http://schemas.android.com/apk/res-auto" 
  android:layout_width="match_parent" 
  android:layout_height="match_parent"> 
  <com.google.android.material.appbar.AppBarLayout 
    android:id="@+id/appBar" 
    android:layout_width="match_parent" 
    android:layout_height="250dp"> 
    <com.google.android.material.appbar.CollapsingToolbarLayout 
      android:id="@+id/collapsingToolbar" 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" 
      android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" 
      app:contentScrim="@color/colorPrimary" 
      app:layout_scrollFlags="scroll|exitUntilCollapsed"> 

      <ImageView 
        android:id="@+id/fruitImageView" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:scaleType="centerCrop" 
        app:layout_collapseMode="parallax" /> 
      <androidx.appcompat.widget.Toolbar 
        android:id="@+id/toolbar" 
        android:layout_width="match_parent" 
        android:layout_height="?attr/actionBarSize" 
        app:layout_collapseMode="pin" /> 

    </com.google.android.material.appbar.CollapsingToolbarLayout> 
  </com.google.android.material.appbar.AppBarLayout> 
</androidx.coordinatorlayout.widget.CoordinatorLayout> 

contentScrim:趋于折叠状态以及折叠之后的背景色

layout_scrollFlags :** **scroll表 示CollapsingToolbarLayout会随着水果内容详情的滚动一起滚动,exitUntilCollapsed:当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。

layout_collapseMode : pin,表示在折叠的过程中位置始终保持不变 , parallax,表示会在折叠的 过程中产生一定的错位偏移

** 利用系统状态栏空间 **:将控件的android:fitsSystemWindows属性指定成true,就表示该控件会出现在系统状态栏里。

Jetpack⭐⭐
#

Jetpack 是 Google 官方推出的一套**“现代化开发组件库”的总称。它包含了很多部分:架构组件(如 ViewModel、Room)、UI 组件、行为组件(如 Navigation、WorkManager)等。Jetpack Compose 是在这个体系下诞生的,它是 Jetpack 中专门负责“构建用户界面 (UI)”**的新工具包。

MVVM 在 Jetpack 中的对应关系
#

MVVM 角色Jetpack 组件职责说明
View<font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">Activity</font>
/ <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">Fragment</font>
负责 UI 展示和用户交互,观察 ViewModel 中的数据变化并更新界面。
ViewModel<font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">androidx.lifecycle.ViewModel</font>持有和管理 UI 相关的状态数据,为 View 提供数据,不持有 View 的引用,生命周期感知。
ModelRepository / Data Source(如 Room、Retrofit、本地数据库、网络 API 等)负责数据获取、缓存、业务逻辑,与 ViewModel 通信,不直接接触 UI
数据绑定机制<font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">LiveData</font>
(或 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">StateFlow</font>
) + 观察者模式
实现 ViewModel 与 View 之间的自动、生命周期感知的数据绑定

ViewModel
#

Activity 既要负责逻辑处理,又要控制UI展示,甚至还得处理网络回调,等等。项目增大时,会导致项目十分臃肿且难以维护。

ViewModel 专门用于存放与界面相关的数据的,在在一定程度上减少Activity中的逻辑。

ViewModel的生命周期和 Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的 时候才会跟着Activity一起销毁。

不能直接创建ViewModel实例,而是通过ViewModelProvider来获取ViewModel的实例。是 ViewModel 生命周期独立于 Activity 生命周期之外。

比较好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel

基本使用
#

ViewModelProvider(<Activity或Fragment实例>).get(<对应的ViewModel>::class.java) 

向 ViewModel 传递参数
#

如果要传递参数,则可以使用 ViewModelProvider.Factory

// ViewModel  使用构造函数以保存传入参数
class MainViewModel(countReserved: Int) : ViewModel() { 
    var counter = countReserved 
} 

// 创建 MainViewModelFactory 实现 ViewModelProvider.Factory
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory { 
    // 声明周期与Activity 独立
    override fun <T : ViewModel> create(modelClass: Class<T>): T { 
        return MainViewModel(countReserved) as T 
    } 

} 

// Activity
class MainActivity : AppCompatActivity() { 
    lateinit var viewModel: MainViewModel 
    lateinit var sp: SharedPreferences 

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        sp = getPreferences(Context.MODE_PRIVATE) 
        val countReserved = sp.getInt("count_reserved", 0) 
        // viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        // 使用 MainViewModelFactory
        viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)) 
            .get(MainViewModel::class.java)
        // 增加
        plusOneBtn.setOnClickListener { 
            viewModel.counter++ 
            refreshCounter() 
        } 
        // 清0
        clearBtn.setOnClickListener { 
            viewModel.counter = 0 
            refreshCounter() 
        } 
        refreshCounter() 
    } 
    private fun refreshCounter() { 
        infoText.text = viewModel.counter.toString() 
    } 

    override fun onPause() { 
        super.onPause() 
        sp.edit { 
            putInt("count_reserved", viewModel.counter) 
        } 
    }
}

Lifecycles
#

在非 Activity 类中感知 Activity 的生命周期,实现逻辑控制。

class MyObserver : LifecycleObserver { 
    // 根据需要感知哪个声明周期就添加对应方法
    @OnLifecycleEvent(Lifecycle.Event.ON_START) 
    fun activityStart() { 
        Log.d("MyObserver", "activityStart") 
    } 
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP) 
    fun activityStop() { 
        Log.d("MyObserver", "activityStop") 
    } 
} 

@OnLifecycleEvent注解 : 传入了一种生命周期事件。

生命周期事件的类型一共有7种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、 ON_STOP和ON_DESTROY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY类型,表示可以匹配Activity的任何生命周期回调。

// 继承自 AppCompatActivity 或 AndroidX 自带 LifecycleOwner 实例
class MainActivity : AppCompatActivity() { 
    ... 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        ... 
        lifecycle.addObserver(MyObserver()) 
        // 主动获取声明周期对象
        // lifecycle.addObserver(MyObserver(lifecycle))
    } 
    ... 
} 

LiveData
#

作用:**可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。 **

MutableLiveData是一种可变的LiveData,:

getValue()方法用于获取LiveData中包含的数据;
setValue()方法用于给LiveData设置数据,但是只能在主线程中调用;
postValue()方法用于在非主线程中给LiveData设置数据。

**永远只暴露不可变的LiveData给外部。这样在非ViewModel中就只能观察 LiveData的数据变化,而不能给LiveData设置数据。 **

class MainViewModel(countReserved: Int) : ViewModel() { 
    // 定义以为不可变的 live data 在get() 中返回
    //  避免外部修改
    val counter: LiveData<Int> 
        get() = _counter 
    // 使用 MutableLiveData 修饰 并保证不可见
    private val _counter = MutableLiveData<Int>()
    
    init { 
        _counter.value = countReserved 
    } 
    
    fun plusOne() { 
        val count = _counter.value ?: 0 
        _counter.value = count + 1 
    } 
 
    fun clear() { 
        _counter.value = 0 
    } 
} 

使用 viewModel.counter的observe()方法来观察数据的变化。 当数据发生变化时,执行对应逻辑。

使用

class MainActivity : AppCompatActivity() { 
    ... 
    override fun onCreate(savedInstanceState: Bundle?) { 
        ... 
        plusOneBtn.setOnClickListener { 
            viewModel.plusOne() 
        } 
        clearBtn.setOnClickListener { 
            viewModel.clear() 
        } 
        // 实时更新数据
        viewModel.counter.observe(this, Observer { count -> 
            infoText.text = count.toString() 
        }) 
    } 
    override fun onPause() { 
        super.onPause() 
        sp.edit { 
            putInt("count_reserved", viewModel.counter.value ?: 0) 
        } 
    } 
}

Transformations.map()
#

**将实际包含数据的LiveData和仅用于观察数据的 LiveData进行转换。 **

** 有一个User类,User中包含用户的姓名和年龄 。 如果MainActivity中明 确只会显示用户的姓名,而完全不关心用户的年龄,那么这个时候还将整个User类型的 LiveData暴露给外部,就显得不那么合适了 **

map()方法接收两个参数:第一个参数是原始的LiveData对象;第二个参数是一个转换函数,我们在转换函数里编写具体的转换逻辑即可。

class MainViewModel(countReserved: Int) : ViewModel() { 

    private val userLiveData = MutableLiveData<User>() 

    // 将User类型的LiveData自由地转型成任意其他类型的LiveData
    val userName: LiveData<String> = Transformations.map(userLiveData) { user -> 
        "${user.firstName} ${user.lastName}" 
    } 
        ... 
} 

switchMap()
#

不一定所有 Livedata 实例是在 ViewModel中创建的。 比如后端接口返回

处理 ViewModel中的某个LiveData对象是调用另外的方法获取的情况。

object Repository { 
    fun getUser(userId: String): LiveData<User> { 
        val liveData = MutableLiveData<User>() 
        liveData.value = User(userId, userId, 0) 
        // 每次都会返回一个新的 liveData 实例 无法直接观察
        return liveData 
    } 
} 
class MainViewModel(countReserved: Int) : ViewModel() { 
    ... 
    private val userIdLiveData = MutableLiveData<String>() 
    val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId -> 
        Repository.getUser(userId) 
    } 
    fun getUser(userId: String) { 
        userIdLiveData.value = userId 
    } 
} 
class MyViewModel : ViewModel() { 
    private val refreshLiveData = MutableLiveData<Any?>() 
    // 要监听的数据
    val refreshResult = Transformations.switchMap(refreshLiveData) {
                // 某些不需要获取参数的方法
                Repository.refresh()  // 假设Repository中已经定义了refresh()方法 
    } 
    // 在外部调用
    fun refresh() { 
        // 触发一次数据刷新
        refreshLiveData.value = refreshLiveData.value 
    } 
} 

LiveData在内部使用了Lifecycles组件来 自我感知生命周期的变化,从而可以在Activity销毁的时候及时释放引用,避免产生内存泄漏的问题

Room
#

ORM(Object Relational Mapping)也叫对象关系映射。简单来讲,我们使用的编程语言是 面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了 。

  • Entity:用于定义封装实际数据的实体类 Dao ,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
  • Dao:数据访问对象的意思,相当于数据库数据库操作的接口

Dao的内部就是根据业务需求对各种数据库操作进行的封装。**覆盖所有业务需求,使得业务方永远只需要与 Dao 层进行交互。**数据库操作通常有增删改查这4种,因此 Room也提供了@Insert、@Delete、@Update和@Query这4种相应的注解。 根据实际业务需求,编写对应 SQL 语句。

  • Database:定义数据库信息。
// 实体类声明
@Entity 
data class User(var firstName: String, var lastName: String, var age: Int) { 
    @PrimaryKey(autoGenerate = true) 
    var id: Long = 0 
} 

// 定义Dao
@Dao 
interface UserDao { 
    @Insert 
    fun insertUser(user: User): Long 
    @Update 
    fun updateUser(newUser: User) 
    @Query("select * from User") 
    fun loadAllUsers(): List<User> 
    @Query("select * from User where age > :age") 
    fun loadUsersOlderThan(age: Int): List<User> 
    @Delete
    fun deleteUser(user: User) 
 
    @Query("delete from User where lastName = :lastName") 
    fun deleteUserByLastName(lastName: String): Int 
 
} 

// 数据库定义
// 注解声明版本号 和 实体类
@Database(version = 1, entities = [User::class]) 
abstract class AppDatabase : RoomDatabase() { 
    // 获取Dao实例
    abstract fun userDao(): UserDao 
 
    companion object { 
        // 构建单例模式
        private var instance: AppDatabase? = null 
 
        @Synchronized 
        fun getDatabase(context: Context): AppDatabase { 
            instance?.let { 
                return it 
            } 
            return Room.databaseBuilder(context.applicationContext, 
                AppDatabase::class.java, "app_database") 
                .build().apply { 
                instance = this 
            } 
        } 
    } 
 
} 

数据库升级
#

定义匿名类,编写对应数据库修改的 SQL 语句,在数据库实例化时执行。

@Database(version = 2, entities = [User::class, Book::class]) 
abstract class AppDatabase : RoomDatabase() { 

    abstract fun userDao(): UserDao 

    abstract fun bookDao(): BookDao 

    companion object { 
        // 数据库有版本1 升至 版本2的方法
        val MIGRATION_1_2 = object : Migration(1, 2) { 
            override fun migrate(database: SupportSQLiteDatabase) { 
                database.execSQL("create table Book (id integer primary 
                                      key autoincrement not null, name text not null, 
                                      pages integer not null)") 
            } 
        } 

        private var instance: AppDatabase? = null 

        fun getDatabase(context: Context): AppDatabase { 
            instance?.let { 
                return it 
            } 
            return Room.databaseBuilder(context.applicationContext, 
                                        AppDatabase::class.java, "app_database") 
                .addMigrations(MIGRATION_1_2)   // 加入升级方法
                .build().apply { 
                    instance = this 
                } 
        } 
    }
}

WorkManager
#

WorkManager和Service并不相同,也没有直接的联系。 Service是Android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。 而WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情 况下,之前注册的任务仍然将会得到执行 。

使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系 统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减 少CPU被唤醒的次数,从而有效延长电池的使用时间。

特性WorkManagerService
设计目标处理可延迟、可重试、需保证执行的后台任务(即使应用退出)处理即时、持续、与用户交互相关的后台操作
生命周期任务式,任务完成即结束可长期运行,直到主动停止或系统回收
执行时机由系统根据设备状态(电量、网络、充电等)调度立即执行(startService)或绑定执行(bindService)
前台展示不直接支持,需结合 ForegroundService可升级为前台服务(ForegroundService),显示通知保活
交互能力无直接 UI 交互能力可与 Activity 绑定,直接进行进程内通信
应用场景+ 数据同步(如定时同步用户数据到服务器)
+ 图片 / 文件上传(允许延迟,可重试)
+ 定期数据备份
+ 日志上报(非即时性)
+ 周期性任务(如每天凌晨清理缓存)
+ 音乐播放器后台播放
+ 实时定位追踪
+ 即时消息推送(如 IM 聊天的后台连接)
+ 与 Activity 实时交互的后台操作
+ 需要持续运行的前台任务(如文件下载进度展示)
  • 定义后台任务:创建类继承自 Worker,重写 doWork 方法。
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) { 
    override fun doWork(): Result { 
        Log.d("SimpleWorker", "do work in SimpleWorker") 
        return Result.success() 
    } 
}
  • 配置后台任务的运行条件和约束信息:
// 单次定时任务  
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build() 
// 时间间隔不能短于15Min
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, 
    TimeUnit.MINUTES).build() 
  • 执行: WorkManager.getInstance(context).enqueue(request)

其他
#

  • 通过 setInitialDelay 可以延时执行
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java) 
    .setInitialDelay(5, TimeUnit.MINUTES) 
    .build() 
  • 添加标签 addTag 可以对相同变迁的定时任务进行管理
  • 取消所有定时任务 WorkManager.getInstance(this).cancelAllWork()
  • setBackoffCriteria 失败后重复执行方式,多久后重新执行定时任务( 时间最短不能少于10秒钟 )
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java) 
    ... 
    .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) 
    .build() 
  • 通过 then 实现任务链式执行
val sync = ... 
val compress = ... 
val upload = ... 
WorkManager.getInstance(this) 
    .beginWith(sync) 
    .then(compress) 
    .then(upload) 
    .enqueue() 

进阶开发
#

  • 自定义 Application 类全局获取 Context
class MyApplication : Application() { 
    companion object { 
         @SuppressLint("StaticFieldLeak")
        lateinit var context: Context 
    } 
    override fun onCreate() {
        super.onCreate() 
        context = applicationContext 
    } 
} 

// 调用
fun Int.showToast(duration: Int = Toast.LENGTH_SHORT) { 
    Toast.makeText(MyApplication.context, this, duration).show() 
} 
  • 通过对象的 Serializable(序列化实现) 或 Parcelabel(将完整对象分解) 接口实现,使 Intent 传递对象
  • 自定义日志工具
  • 创建定时任务:Alarm 设定实现实现

深色模式
#

  • 使用Force Dark

分析浅色主题应用下的每一层 View,并且在这些View绘制到屏幕之前,自动将它们的颜色转换成更加适合深色主题的颜色。

  • DayNight主题

在系统设置中开启深 色主题时,应用程序会自动使用深色主题,反之则会使用浅色主题 。

指定颜色值引用的方式相当于对控件的颜色进行了硬编码,DayNight主题是不能对这些颜 色进行动态转换的。 在普通情况下,系统仍然会读取values/colors.xml文件中的颜色值,而一旦用户开 启了深色主题,系统就会去读取values-night/colors.xml文件中的颜色值了。

fun isDarkTheme(context: Context): Boolean { 
    val flag = context.resources.configuration.uiMode and 
        Configuration.UI_MODE_NIGHT_MASK 
    return flag == Configuration.UI_MODE_NIGHT_YES 
}