文章目录
一. Android总览
1. 系统架构
应用层:所有安装在手机上的应用程序
应用框架层:提供了构建应用程序时可能用到的各种API
系统运行库层:通过一些C/C++库为Android系统提供了主要的特性支持。如SQLite,OpenGL|ES
Linux内核层:Android系统是基于Linux内核的,这一层为Android设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等。
2. 开发环境
Gradle项目:可以先下载到本地,后边建立项目快一点
Gradle配置教学
Gradle理论知识
3. 在红米手机上运行
连接usb-设置-我的设备-全部参数-连点6次版本号-返回首页,更多设置-开发者选项
4. 项目资源详解
4.1 整体结构
4.2 res文件
drawable 放图片
mipmap 放应用图标
values 字符串、样式、颜色
layout 布局
xml 一些其他配置规则,如数据提取的规则等
引用规则
4.3 build.gradle文件
4.4 AndroidMainfest.xml文件
- 属性:android:exported
`android:exported="true"` 是 Android 应用开发中的一个属性,用于定义应用的组件是否可以被其他应用访问。
在 Android 应用的 `AndroidManifest.xml` 文件中,应用的组件(如 `Activity`、`Service`、`BroadcastReceiver`
和 `ContentProvider`)可以设置 `android:exported` 属性。这个属性的值可以是 `true` 或 `false`,其含义如下:
- `android:exported="true"`: 组件可以被其他应用访问和启动。这意味着其他应用可以通过意图(Intent)启动该组件。
- `android:exported="false"`: 组件不能被其他应用访问。只有本应用内部的组件可以启动它。
这个属性在 Android 12(API 级别 31)及更高版本中变得更为重要,系统要求所有具有意图过滤器的组件必须显式声明
`android:exported` 属性,以确保应用的安全性。如果没有声明这个属性,应用在安装时会发生错误。
二. Activity(四大组件)
1. 创建一个Activity并实现一些简单功能
选择No Activity方式创建一个空的Project
切换成project模式,到主目录下创建一个kt文件
写一个主类 + res里面的布局文件 + AndroidMainfest.xml注册
实现功能包含:
- 创建
- Toast显示文本提示
- menu菜单栏 onCreateOptionsMenu函数中调用menuInflater.inflate方法
- 打印当前activity的名称
package com.example.myactivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class FirstActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.mylayout)
//给按钮设置点击事件
val button1:Button = findViewById(R.id.button1)
button1.setOnClickListener{
Toast.makeText(this,"This is my first attempt",Toast.LENGTH_SHORT).show()
}
//销毁当前activity
val button2:Button = findViewById(R.id.button2)
button2.setOnClickListener{
finish()
}
}
//menu菜单选择栏
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
//inflate 方法将指定的菜单资源文件 R.menu.main 填充到传入的 menu 对象中。
//这样,菜单项就会被创建并显示在选项菜单中
menuInflater.inflate(R.menu.main,menu)
return true//true表示会显示菜单
}
//响应事件
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.add_item -> Toast.makeText(this,"Add",Toast.LENGTH_SHORT).show()
R.id.remove_item -> Toast.makeText(this,"remove",Toast.LENGTH_SHORT).show()
}
return true
}
//当前是哪一个activity
Log.d("BaseActivity", javaClass.simpleName)
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<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.MyActivity"
tools:targetApi="31">
<activity android:name=".FirstActivity"
android:label="FirstActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
menu菜单栏需要在res目录下建立menu文件夹
<?xml version="1.0" encoding="utf-8"?>
<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>
2. 显式和隐式启动
场景描述:在一个Activity中启动另外一个Activity
准备:再创建一个activity
2.1 显式启动 (常用)
写法一:
val intent = Intent(this,SecondActivity::class.java)
intent.putExtra("param1", "data1")
intent.putExtra("param2", "data2")
startActivity(intent)
存在问题 如果不知道第二个activity的代码,我怎么传数据?
写法二:
修改第二个Activity的代码,新增如下:
companion object {
@JvmStatic
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)
}
}
在第一个activity中添加如下代码:
SecondActivity.actionStart(this,"data1","data2")
2.2 隐式启动
(1) 标准用法
修改第二个activity的xml文件,指定类型;让intent去响应该类型
#为方便演示 添加了两个类型
<activity android:name=".SecondActivity" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.example.activitytest.MY_CATEGORY"/>
</intent-filter>
</activity>
button1.setOnClickListener {
val intent = Intent("com.example.activitytest.ACTION_START")
intent.addCategory("com.example.activitytest.MY_CATEGORY") //1个intent可以响应多个类型
startActivity(intent)
}
(2)其他用法
启动浏览器
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.baidu.com")
startActivity(intent)
}
打电话
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
自己也可以指定activity能够响应的类型
3. 继承实现关系
- Context
- ContextWrapper
- ContextThemWrapper
- Activity
- ComponentActivity:提供对Lifecycle、ViewModel、LiveData等架构组件的支持
- FragmentActivity:提供对于fragment的支持
- AppCompatActivity:提供了对于ActionBar和Material Design主题和样式的支持
4. 生命周期
4.1 返回栈
Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。
在默认情况下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置。系统总是会显示处于栈顶的Activity给用户。
4.2 状态
运行状态:处于返回栈栈顶。系统最不愿意回收
期间触发的函数及顺序为: onCreate() ->onStart() -> onResume()。
暂停状态:部分可见,但不处于栈顶。如对话框形式的Activity只会占用屏幕中间的部分区域。系统不愿意回收。
运行状态到暂停状态所触发的函数及顺序为:onResume() -> onPuased()。
暂停状态恢复至运行状态所触发的函数及顺序为:onPuased() -> onResume()。
停止状态:不处于栈顶,完全不可见。系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
暂停状态到停止状态所触发的函数及顺序为:onPuased() -> onStop()。
停止状态恢复至运行状态所触发的函数及顺序为:onStop() -> onRestart() -> onStart() -> onResume()。
销毁状态:从返回栈中移除后。系统最倾向于回收这种Activity。
4.3 生命周期
完整生存期:create初始化 和 destroy释放内存
可见生存期:strart 加载对用户可见的资源和 stop释放资源。
前台生存期:resume和pause。
onPause () : 准备去启动或者恢复另外一个Activity的时候调用。通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法一定要快,不然会影响到新的栈顶Activity的使用。
onStop (): 这个方法在Activity完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行
4.4 onSaveInstanceState() 方法 (TODO)
5. 启动模式
5.1 standard
每当启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置对于使用standard模式的
Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity
5.2 singleTop
当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接
使用它,不会再创建新的Activity实例。
5.3 singleTask
让某个Activity在整个应用程序的上下文中只存在一个实例
当Activity的启动模式指定为singleTask,每次启动该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例,并把在这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新的Activity实例。
5.4 singleInstance
singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)
三. UI布局
1. 常见控件
1.1 控件常用属性
- 可控性
所有的Android控件都具有这个属性,可以通过android:visibility进行指定,可选值有3种:visible、invisible和gone。
visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。
invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
gone则表示控件不仅不可见,而且不再占用任何屏幕空间
1.2 具体控件
- TextView
固定文本框
<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="This is TextView"/>
- Button
按钮
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false" %默认会把所有字母变成大写;加上改行可取消
android:text="Button" />
实现点击事件的两种方式:
//方式一:
button.setOnClickListener {
// 在此处添加逻辑
}
//方式二:继承View.OnClickListner
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 -> {
// 在此处添加逻辑
}
}
- EditText
可输入文本框
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here" %提示语,可实现移动到就不见的效果
android:maxLines="2" %最大行数,超过就向上滚东
/>
- ImageView
用于显示图片资源
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img_1"
/>
//与button联动实现点击图片切换到另外一张图片的功能
val imageView:ImageView = findViewById(R.id.imageView)
button2.setOnClickListener{
imageView.setImageResource(R.drawable.img_2)
}
- ProgressBar
进度条
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal" %默认是圆形的进度条,可设置成水平的
android:max="100"%进度条的长度
/>
//改变进度条的可见性
button2.setOnClickListener{
imageView.setImageResource(R.drawable.img_2)
if(progressBar.visibility== View.VISIBLE){
progressBar.visibility = View.GONE
}else{
progressBar.visibility = View.VISIBLE
}
}
// 加载进度条
val button3:Button = findViewById(R.id.button3)
button3.setOnClickListener {
progressBar.progress = progressBar.progress + 20
if(progressBar.progress==100){
progressBar.visibility = View.GONE
}
}
- AlterDialog
可以在当前界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽其他控件的交互能力
R.id.button4->{
AlertDialog.Builder(this).apply {
setTitle("This is Dialog")
setMessage("Something important.")
setCancelable(false)
setPositiveButton("OK") { dialog, which ->
}
setNegativeButton("Cancel") { dialog, which ->
}
show()
}
}
2. 常见布局
2.1 LinearLayout 线性布局
- android:orientation=“vertical”
- android:orientation=“horizontal”
- 其他属性
android:layout_gravity用于指定控件在布局中的对齐方式
android:layout_weight。这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用
系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"
/>
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"
/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send"
/>
</LinearLayout>
2.2 RelativeLayout 相对布局
- 相对于父布局
android:layout_alignParentLeft
android:layout_alignParentTop
android:layout_alignParentRight
android:layout_alignParentBottom
android:layout_centerInParent - 相对于其他控件
android:layout_below表示让一个控件位于另一个控件的下方
android:layout_toLeftOf表示让一个控件位于另一个控件的左侧
android:layout_toRightOf表示让一个控件位于另一个控件的右侧
android:layout_alignLeft表示让一个控件的左边缘和另一个控件的左边缘对齐
android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐
android:layout_alignTop
android:layout_alignBottom
2.3 FrameLayout
FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景少了很多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角
使用layout_gravity属性来指定控件在布局中的对齐方式
3. ListView
由于手机屏幕空间比较有限,能够一次性在屏幕上显示的内容并不多,当我们的程序中有大量的数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕。
- Demo演示:
功能1,显示水果图片和名称,并能进行滚动查看;
功能2,点击水果能够弹出提示。
- 代码实现
核心思路,包含了三个元素:listView,数据模型,Adapter,即MVC架构。
建一个ListView,填充对应的数据模型(新建一个水果类和XML布局文件),然后通过适配器Adapter建立二者的关系。
滚动翻页功能:重写Adapter里面的getView方法(关键点,需要通过缓存等来进行优化性能)。
点击功能:setOnItemClickListener方法。
核心代码 如何写适配器
//水果类
class Fruit(val name:String,val imageId:Int)
//适配器:核心代码
//getView 实现滚动
//优化点1:convertView用于缓存,优化点:不用反复加载布局
//优化点:内部类ViewHolder缓存,无需反复加载控件实例
class FruitAdapter(activity:Activity,val resourceId:Int,data:List<Fruit>)//第二个变量是对应的布局文件!
:ArrayAdapter<Fruit>(activity, resourceId,data) {
var tagId:Int = 0 //打印信息的标记,非必要
inner class ViewHolder (val fruitImage:ImageView,val fruitName:TextView)//内部类用于缓存图片和名称
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view:View
val viewHolder:ViewHolder
if(convertView==null){ //convertView用于缓存,优化点:不用反复加载布局
view = LayoutInflater.from(context).inflate(resourceId,parent,false)//用于加载视图
val fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
viewHolder = ViewHolder(fruitImage,fruitName)//缓存,无需反复加载控件实例
view.tag = viewHolder //tag
Log.d("位置" + position, "创建新convertView,设置tagId:" + tagId)//打印信息 验证缓存
tagId ++
} else{
view = convertView
viewHolder = view.tag as ViewHolder
Log.d("位置" + position, convertView.getTag().toString() + " 复用convertView");//打印信息 验证缓存
}
val fruit = getItem(position)
if(fruit!=null){
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
//只要看listview演示那一段即可
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_second)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
Log.d("BaseActivity",javaClass.simpleName)
//listView演示
initFruits()
val adapter = FruitAdapter(this,R.layout.fruit_item,fruitList)
val listView:ListView = findViewById(R.id.listView)
listView.adapter = adapter
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
} //点击事件
}
//通过第一个Activity启动的 可以不管。
companion object {
@JvmStatic
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)
}
}
//初始化水果
private val fruitList = ArrayList<Fruit>()
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.img_1))
fruitList.add(Fruit("Banana", R.drawable.img_2))
fruitList.add(Fruit("Orange", R.drawable.img_1))
fruitList.add(Fruit("Watermelon", R.drawable.img_1))
fruitList.add(Fruit("Pear", R.drawable.img_1))
fruitList.add(Fruit("Grape", R.drawable.img_1))
fruitList.add(Fruit("Pineapple", R.drawable.img_1))
fruitList.add(Fruit("Strawberry", R.drawable.img_1))
fruitList.add(Fruit("Cherry", R.drawable.img_1))
fruitList.add(Fruit("Mango", R.drawable.img_1))
}
}
}
- 核心方法解析
public View getView(int position, View convertView , ViewGroup parent){…} 他是在滚动的时候被嗲用的
view的tag属性
listView.setOnItemClickListener
4. RecylerView
核心部分:子项布局 + 适配器
- 引入依赖
implementation ("androidx.recyclerview:recyclerview:1.3.0")
-
Fruit类,Fruit_item布局文件同上一节
-
写Adapter
class PlusFruitAdapter(val fruitList:List<Fruit>):
RecyclerView.Adapter<PlusFruitAdapter.ViewHolder>(){
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 onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
viewHolder.itemView.setOnClickListener{
Toast.makeText(parent.context, "you clicked view ",
Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
Toast.makeText(
parent.context, "you clicked image",
Toast.LENGTH_SHORT
).show()
}
}
主程序
class MainActivity : ComponentActivity() {
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
initFruits()
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL//实现水平滚动
recyclerView.layoutManager = layoutManager
val adapter = PlusFruitAdapter(fruitList)
recyclerView.adapter = adapter
}
private val fruitList = ArrayList<Fruit>()
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.img_1))
fruitList.add(Fruit("Banana", R.drawable.img_2))
fruitList.add(Fruit("Orange", R.drawable.img_1))
fruitList.add(Fruit("Watermelon", R.drawable.img_1))
fruitList.add(Fruit("Pear", R.drawable.img_1))
fruitList.add(Fruit("Grape", R.drawable.img_1))
fruitList.add(Fruit("Pineapple", R.drawable.img_1))
fruitList.add(Fruit("Strawberry", R.drawable.img_1))
fruitList.add(Fruit("Cherry", R.drawable.img_1))
fruitList.add(Fruit("Mango", R.drawable.img_1))
}
}
}
5. 自定义布局和控件
5.1 引入布局:解决重复布局的问题
在layout目录下新建一个title.xml布局
在需要引入布局的XML文件中,加入 <include layout="@layout/title" />
5.2 自定义控件
应用场景:标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前Activity
- 新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
LayoutInflater.from(context).inflate(R.layout.title, this)
titleBack.setOnClickListener {
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
}
}
}
代码解析:
LayoutInflater.from(context): 这行代码获取一个LayoutInflater对象,该对象用于从XML文件中加载视图。context参数可以是Activity、Fragment或任何实现Context接口的对象。
.inflate(R.layout.title, this): 这行代码使用LayoutInflater对象将R.layout.title对应的XML布局文件加载并实例化为一个视图对象。this参数指定了将加载的视图添加到哪个父视图中
- 在布局文件中添加这个自定义控件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
四. Fragment
1. 基本用法
Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间
1.1 简单使用
- 写xml文件,布局
- 新建Fragment类,继承Fragment,导入AndroidX的包
class LeftFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.left_fragment, container, false)
}
}
方法解析:
`ViewGroup` 是 Android 中的一个重要概念,它是 `View` 类的子类,用于管理和布局其他 `View` 对象。
它是一个容器类,可以包含多个 `View` 对象。
在上面的代码中,`container` 参数就是一个 `ViewGroup` 对象。它代表了当前 `Fragment` 要被放置的父容器。
通常情况下,这个父容器是 `Activity` 的根视图或者是另一个 `ViewGroup`。
`inflater.inflate(R.layout.left_fragment, container, false)` 这行代码的作用是:
- 使用 `LayoutInflater` 将 `R.layout.left_fragment` 这个布局文件解析成一个 `View` 对象。
- 将这个 `View` 对象添加到 `container` 这个 `ViewGroup` 中。
- `false` 表示不要自动将新创建的 `View` 添加到 `container` 中,而是由开发者手动控制添加的时机。
总之, `ViewGroup` 在这里的作用是提供一个容器,用于承载 `Fragment` 的布局视图。
开发者可以根据需求选择合适的 `ViewGroup` 作为 `Fragment` 的父容器。
- 修改主视图activity_main文件
<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" #包名导入Fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
1.2 动态添加Fragment
功能描述:一个Activity分成两个Fragment,左Fragment里面放了一个Button,点击之后,第二个Fragment会切换成另外一个,且第二个Fragment具有回退功能。
class MainActivity : AppCompatActivity() {
...//核心代码
private fun replaceFragment(fragment: Fragment) {
val fragmentManager = supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.addToBackStack(null) //实现回退功能
transaction.commit()
}
private fun replaceFragment(fragment: Fragment) {
val fragmentManager = supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.commit()
}
}
引入FragmentManager进行管理。
1.3 Fragment与Activity之间的通信
- Activity中调用Fragment
// 方法一:
val fragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as LeftFragment
// 方法二:引入Kotlin Extension插件后
val fragment = leftFrag as LeftFragment
- Fragment中调用Activity
if (activity != null) {
val mainActivity = activity as MainActivity
}
- Fragment与Fragment
首先在一个Fragment中可以得到与它相关联的Activity,然后再通过这个Activity去获取另外一个Fragment的实例,这样就实现了不同Fragment之间的通信功能。
2. 生命周期
2.1 状态
运行:当一个Fragment所关联的Activity正处于运行状态时,该Fragment也处于运行状态;
暂停:当一个Activity进入暂停状态时(由于另一个未占满屏幕的Activity被添加到了栈顶),与它相关联的Fragment就会进入暂停状态;
停止:当一个Activity进入停止状态时,与它相关联的Fragment就会进入停止状态,或者通过调FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前调用了addToBackStack()方法,这时的Fragment也会进入停止状态。总的来说,进入停止状态的Fragment对用户来说是完全不可见的,有可能会被系统回收。 (注意第二种情况)
销毁:Fragment总是依附于Activity而存在,因此当Activity被销毁时,与它相关联的Fragment就会进入销毁状态。或者通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前并没有调用addToBackStack()方法,这时的Fragment也会进入销毁状态。
2.2 生命周期
onAttach(): 当activity与fragment建立关联时调用
onCreateView(): 当为Fragment创建视图(加载布局)调用
onActivityCreated(): 确保与Fragment相关联的Activity已经创建完毕时调用
onDestroyView(): 当与Fragment关联的视图被移除时调用
onDetach():当Activity与Frament解除关联时调用
3. 实现简易版新闻APP
代码链接
核心技术:RecyleView、Fragment、限定符布局
难点:如何根据单页还是双页给RecyleView的子项设置不同的点击事件
News 新闻数据类:标题、内容;
NewsContentFragment:单双页模式下的新闻内容,核心方法refresh能够根据传入的新闻标题和内容刷新页面;
NewsTitleFragment:使用RecyleView实现新闻标题,单页模式下点击子项跳转到NewsContent,双页模式下,点击子项则刷新NewsContentFragment中的内容;
NewsContent:里面包含了NewsContentFragment(复用了Fragment),通过actionStart让另外一个activity启动,并根据传入的标题和内容,去调用NewsContentFragment.refresh方法
五. 广播 (四大组件)
1. 基本知识
1.1 分类
1.2 注册
- 动态注册
写一个类继承BroadCastReceiver类,重写Receiver方法;intentFilter().addAction添加广播类型;通过reisterReceiver注册;当关闭Activity的时候记得取消注册。
该类型的注册必须程序启动才能用,想要程序未启动时接收广播采用静态注册
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()
}
}
}
- 静态注册
在Android 8.0系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。
class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show()
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> # 开启权限
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<receiver
android:name=".BootCompleteReceiver" # 广播接收者
android:enabled="true"
android:exported="true">
</receiver>
</application>
</manifest>
1.3 发送自定义广播
- 写好广播接收者:AndroidManifest.xml中对BroadcastReceiver修改 + BroadcastReceiver
class MyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context, "received in MyBroadcastReceiver",
Toast.LENGTH_SHORT).show()
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
...
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
</application>
</manifest>
- 广播发送者
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
// 核心代码:intent发送广播
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
//调用Intent的setPackage()方法,并传入当前应用程序的包名。
//packageName是getPackageName()的语法糖写法,用于获取当前应用程序的包名
intent.setPackage(packageName)
sendBroadcast(intent) // 标准广播
// sendOrderedBroadcast(intent, null) // 有序广播
}
...
}
...
}
2. 简单Demo:实现强制下线功能
工程代码
项目结构:
ActivityCollector:管理所有的类
BaseActivty:所有类的父类,重写 onResume 方法处理广播消息
LoginActivity:处理登录逻辑
MainActivity:一个按钮处理下线逻辑
过程中遇到的问题:
- EditText的getText()方法 必须在onClick方法里才能用
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = accountEdit.getText().toString();
String password = passwordEdit.getText().toString();
// 如果账号是admin且密码是123456,就认为登录成功
if (account.equals("admin") && password.equals("123456")) {
Intent intent = new Intent(LoginActivity.this, MainActivity.
class);
startActivity(intent);
finish();
} else {
Toast.makeText(LoginActivity.this, "帐号或密码错误,请重新输入", Toast.LENGTH_SHORT).show();
}
}
});
- AlterDialog的用法
class ForceOfflineReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("下线警告");
builder.setMessage("你即将离线,请重新登录");
builder.setCancelable(false);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCollector.finishAll(); // 销毁所有活动
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent); // 重新启动LoginActivity
}
});
builder.show();
}
}
六. Service(四大组件)
1. 引入
Service是Android中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。Service的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service仍然能够保持正常运行。
实际上Service并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在Service的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况
2. 基本用法
- 创建
继承Service() -> 重写抽象方法onBind
onCreate()方法会在Service创建的时候调用,onStartCommand()方法会在每次Service启动的时候调用,onDestroy()方法会在Service销毁的时候调用。 - 启动和停止
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
startService(intent) // 启动Service
}
stopServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
stopService(intent) // 停止Service
}
}
}
- Activity和Service通信
比如在MyService里提供一个下载功能,然后在Activity中可以决定何时开始下载,以及随时查看下载进度
class MyService : Service() {
private val mBinder = DownloadBinder() //创建一个专门的Binder对象来对下载功能进行管理
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
}
...
}
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
}
}
}
3. 生命周期
一旦在项目的任何位置调用了Context的startService()方法,相应的Service就会启动,并回调onStartCommand()方法。如果这个Service之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。
Service启动了之后会一直保持运行状态,直到stopService()或stopSelf()方法被调用,或者被系统回收。
调用Context的bindService()来获取一个Service的持久连接,这时就会回调Service中的onBind()方法。
七. ContentProvider (四大组件)
1. 介绍
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险
2. 基本用法
如果想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。
ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中**insert()**方法用于添加数据,**update()**方法用于更新数据,**delete()**方法用于删除数据,**query()**方法用于查询数据。
八. 数据持久化技术
1. 文件存储
Context类提供了一些方法
存储数据:openFileOutput() BufferedWriter(OutputStreamWriter(output))
读取数据:openFileInput(“data”) BufferedReader(InputStreamReader(input))
2. SharedPreferences
不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串
- 存储数据
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")
}
3. SQLite数据库
SQLiteOpenHelper
九. 网络技术
1. Webview控件
在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView.settings.javaScriptEnabled=true
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
}
}
2.HTTP访问网络
2.1 HttpURLConnection
发送数据
val url = URL("https://www.baidu.com")
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
提交数据
connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
output.writeBytes("username=admin&password=123456")
2.2 OkHttp
更常用,但后续会结合Retrofit使用,更简便
get请求
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string() //解析Response的内容
post请求
val requestBody = FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build()
val request = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody)
.build()
3. 解析XML格式数据
- pull
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()
}
}
- SAX解析方式
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()
}
}
4. 解析JSON格式数据
4.1 使用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()
}
}
4.2 使用GSON
基本用法
5. 手动完成网络接口回调的操作
基于OKHttp
object HttpUtil {
...
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)
}
}
可以把callback写成
HttpUtil.sendOkHttpRequest(address, object : Callback {
override fun onResponse(call: Call, response: Response) {
// 得到服务器返回的具体内容
val responseData = response.body?.string()
}
override fun onFailure(call: Call, e: IOException) {
// 在这里对异常情况进行处理
}
})
6. Retrofit入门
6.1 基本用法
- 引入依赖
- 建Entity类
- 写接口
interface AppService {
@GET("get_data.json")
fun getAppData(): Call<List<App>> //注解 + 返回值call
}
- Retrofit创建
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java) //创建Service接口的动态代理对象
appService.getAppData().enqueue(object : Callback<List<App>> { //enqueue实现异步
// 需要注意的是,当发起请求的时候,Retrofit会自动在内部开启子线程,当数据回调到Callback中之后,
// Retrofit又会自动切换回主线程,整个操作过程中我们都不用考虑线程切换问题
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()
}
})
- 网络安全配置 (HTTP明文)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.retrofittest">
<uses-permission android:name="android.permission.INTERNET" /> //权限
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_config"> //安全配置
...
</application>
</manifest>
安全配置文件:允许我们以明文的方式在网络上传输数据,而HTTP使用的就是明文传输方式
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
6.2 常用注解用于处理复杂接口
6.3 Kotlin中标准写法
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
inline fun <reified T> create(): T = create(T::class.java) //泛型实例化
}
调用方式:
val appService = ServiceCreator.create<AppService>()
十. Material Design
1. Toolbar
Toolbar 是 Android 中常用的一种应用程序顶部栏控件。它提供了一种通用的方式来显示应用程序的标题、导航按钮、菜单项等内容
2 滑动菜单
- DrawerLayout:
在应用中创建一个可以从屏幕边缘滑出的小侧边栏,用于导航或其他操作
- NavigationView:
NavigationView 是一个用于显示导航菜单的控件,通常与 DrawerLayout 一起使用,提供了一个滑动抽屉式的侧边栏菜单
3. 悬浮按钮和可交互提示
- FloatingActionButton:
FloatingActionButton (FAB) 是一种常见的 UI 控件,通常用于在应用程序中执行主要操作。它通常位于屏幕的右下角,并以圆形形状出现。
Snackbar:
Snackbar 是一种轻量级的消息提示控件,它会在屏幕下方显示一条简短的消息。与 Toast 不同,Snackbar 可以提供用户交互,如撤销操作。
- CoordinatorLayout:
CoordinatorLayout 是一个特殊的 ViewGroup,可以协调子视图之间的交互和动画。它常与 FloatingActionButton 和 Snackbar 一起使用,以实现更好的用户体验。
4. 卡片式布局
- MaterialCardView:
本质是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉
- AppBarLayout:
实际上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一些
Material Design的设计理念。解决一些遮挡问题
5. 下拉刷新
- SwipeRefreshLayout是一个Android支持库中的控件,它提供了一种简单的方式来实现下拉刷新功能。当用户下拉SwipeRefreshLayout时,会触发一个刷新事件,可以在该事件中执行数据刷新的操作。
- 要在Android应用中使用SwipeRefreshLayout,需要在XML布局文件中添加SwipeRefreshLayout控件,并将需要添加下拉刷新功能的内容放置在SwipeRefreshLayout内部。通常,SwipeRefreshLayout包裹一个RecyclerView或者ListView等可以滚动的控件。
- 在Java代码中,需要设置一个OnRefreshListener监听器来监听下拉刷新事件,并在回调方法中编写数据刷新的逻辑。当用户下拉时,OnRefreshListener中的onRefresh方法会被调用,你可以在这个方法中执行数据加载或刷新的操作。
6. 可折叠式标题栏
- CollapsingToolbarLayout
是 Android Support Design Library中的一个布局控件,用于实现可折叠的工具栏效果。它通常与 AppBarLayout 和 Toolbar一起使用,以创建具有折叠和展开效果的标题栏或工具栏。这个效果常见于大型应用的界面,如新闻应用或社交媒体应用中,当用户向下滚动页面时,工具栏会向上折叠,显示更多内容;当用户向上滚动页面时,工具栏会展开。
十一. Jetpack
1. ViewModel
- 生命周期
当 Activity 处于前台的时候被销毁了,那么得到的 ViewModel 是之前实例过的 ViewModel;如果 Activity 处于后台时被销毁了,那么得到的 ViewModel 不是同一个。举例说,如果 Activity 因为配置发生变化而被重建了,那么当重建的时候,ViewModel 是之前的实例;如果因为长期处于后台而被销毁了,那么重建的时候,ViewModel 就不是之前的实例了。
浅谈 ViewModel 的生命周期控制
AndroidX设计架构MVVM之ViewModel生命周期分析 - 使用及原理
ViewModelProvider: 提供获取 ViewModel 实例的类,ViewModel 都是从这个类中获取。
ViewModelStoreOwner:核心接口,该接口的实现类表示能够为外部提供 ViewModelStore 实例。
ViewModelStore: 核心类,存储 ViewModel 实例的类,实际是一个HashMap。
Factory: 核心接口,负责创建 ViewModel 实例。
// 获取viewModel类
val viewModel = ViewModelProvider(activity).get(HiViewModel::class.java)
2. LifeCycles
- 使用
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")
}
}
lifecycleOwner.lifecycle.addObserver(MyObserver())
- 五种生命状态
3. LiveData
- 使用
// VM中
val counter = MutableLiveData<Int>()
// Activity中
viewModel.counter.observe(this, Observer { count ->
infoText.text = count.toString()
}
标准写法:在非ViewModel中就只能观察LiveData的数据变化,而不能给LiveData设置数据
class MainViewModel(countReserved: Int) : ViewModel() {
val counter: LiveData<Int>
get() = _counter
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
}
}
public class MainViewModel extends ViewModel {
private final MutableLiveData<Integer> _counter = new MutableLiveData<>();
private final LiveData<Integer> counter = _counter;
public MainViewModel(int countReserved) {
_counter.setValue(countReserved);
}
public LiveData<Integer> getCounter() {
return counter;
}
public void plusOne() {
Integer count = _counter.getValue();
if (count != null) {
_counter.setValue(count + 1);
} else {
_counter.setValue(1);
}
}
public void clear() {
_counter.setValue(0);
}
}
- map和switchmap
实现数据转换 - MediatorLiveData
MediatorLiveData是 LiveData的子类,允许您合并多个 LiveData 源。只要任何原始的 LiveData 源对象发生更改,就会触发 MediatorLiveData 对象的观察者。
4. Room
属于ORM框架。ORM(Object Relational Mapping)也叫对象关系映射
5. Workmanager
WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等