【7】应用开发——Activity全面讲解

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。

文章目录

  • 一、Activity
    • 1.1 Activity基础
      • 1.1.1 手动创建和加载布局
      • 1.1.2 在AndroidManifest文件中注册
      • 1.1.2 在Activity中添加Toast(吐司弹窗)
      • 1.1.2 在Activity中使用Menu(菜单)
    • 1.2 使用Intent实现页面的跳转
      • 1.2.1 显式Intent的使用
      • 1.2.2 隐式Intent的使用
      • 1.2.3 更多隐式Intent的用法
    • 1.3 使用Intent实现数据的传递
      • 1.3.1 向下一个Activity传递数据
      • 1.3.2 通过startActivityForResult()向上一个Activity返回数据
      • 1.3.3 通过registerForActivityResult()向上一个Activity返回数据
    • 1.4 Activity的生命周期
      • 1.4.1 返回栈
      • 1.4.2 Activity的生命周期
      • 1.4.3 Activity被回收了怎么办
    • 1.5 Activity的启动模式
      • 1.5.1 standard模式(标准)
      • 1.5.2 singleTop模式(栈顶复用)
      • 1.5.3 singleTask模式(栈内复用)
      • 1.5.4 singleInstance模式(单实例)
  • 二、Activity使用的小技巧
    • 2.1 随时随地退出程序
    • 2.2 启动一个Activity的最佳写法

一、Activity

1.1 Activity基础

1.1.1 手动创建和加载布局

Android Studio会在创建一个新Activity时自动帮我们生成相应的layout布局文件并在Activity内引用这个布局。但是我们这次选择手动创建和加载布局。
创建first_layout.xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <!-- 添加一个按钮Button -->
    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 1" />
        
</LinearLayout>

在Activity中手动加载这个布局:

class FirstActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //引用相应的布局
        setContentView(R.layout.first_layout)
    }

在这里调用了 setContentView() 方法,并传入一个first_layout.xml布局文件的id作为参数,这样就可以给FirstActivity加载一个布局了。

在Android项目中,R是一个特殊的类,用于引用所有的资源,如布局(layout)、图片(drawable)、字符串(string)等,我们在项目中添加的任何资源都会在R文件中生成一个相应的资源id

1.1.2 在AndroidManifest文件中注册

在程序运行前,我们需要告诉应用程序应该先启动哪个Activity。下面是如何为程序配置主Activity的示例代码:

<?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
        ···>
        <activity
            android:name=".FirstActivity"
            android:label="This is First Activity!" // 指定Activity标题栏内容
            android:exported="true">
            // Begin 为程序配置主Activity
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            // End 为程序配置主Activity
        </activity>
    </application>

</manifest>

这样FirstActivity就是我们这个程序的主Activity了,当我们打开App时最先显示的就是这个Activity

1.1.2 在Activity中添加Toast(吐司弹窗)

findViewById():返回一个继承自View的泛型对象,所以Kotlin无法推导出该对象是什么控件,所以我们需要显式的将变量声明成Button类型。
makeText():通过makeText()方法 可以创建一个Toast对象,然后调用show()方法将Toast显示出来。makeText()方法需要传入三个参数:①Context上下文②要显示的内容③显示的时长。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.first_layout)
        //为按钮绑定view控件
        val button1: Button = findViewById(R.id.button1)
        //为按钮设置点击触发事件
        button1.setOnClickListener {
                         上下文     Toast要显示的内容        显示时长       记得调用show()不然Toast无法显示
            Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
        }
    }

1.1.2 在Activity中使用Menu(菜单)

在res目录下新建一个menu文件夹:res—>New—>Directory—>输入文件名menu—>点击OK
在menu文件夹下新建一个main菜单文件:menu—>New—>Menu resource file—>输入main—>点击OK

<?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>

【7】应用开发——Activity全面讲解_第1张图片

Android Studio中重写的快捷键是Ctrl + O
【7】应用开发——Activity全面讲解_第2张图片
我们在FirstActivity中重写了onCreateOptionsMenu() 方法,这里面menuInflater实际上是调用了父类的getMenuInflater方法得到一个MenuInflater对象,然后再调用它的inflate()方法,就可以给当前Activity创建菜单了。inflate()方法需要接收两个参数:①通过哪个资源文件创建菜单 ②将菜单添加到哪个Menu对象

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
                       menu资源文件   menu对象
    menuInflater.inflate(R.menu.main, menu)
    return true
}

接下来就可以自定义菜单的响应事件了,重写 onOptionsItemSelected() 方法来实现:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
                                                                              调用show()显示Toast
        R.id.add_item -> Toast.makeText(this, "You clicked Add.",Toast.LENGTH_SHORT).show()
        R.id.remove_item -> Toast.makeText(this, "You clicked Remove.",Toast.LENGTH_SHORT).show()
    }
    return true
}

1.2 使用Intent实现页面的跳转

Intent是Android程序中各组件之间进行交互的一种重要方式。它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent又分为两种:

  • 显式Intent:指明目标组件,并将其完全限定名作为参数
  • 隐式Intent:没有指明目标组件,而是通过cation + category来进行匹配,让系统选择合适的组件进行处理

1.2.1 显式Intent的使用

我们先来学习一下显式Intent,我们这次让Android Studio帮我们新建一个SecondActivity(自动生成layout并在AndroidManifest中注册):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 2" />

</LinearLayout>

像下面这种,直接表明我们想要从当前Activity跳转到SecondActivity的Intent就叫做显式Intent,因为我们直接显式的声明了我们想要从A页面跳转到B页面。接下来,我们为FirstActivity中的按钮添加点击事件:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.first_layout)
    // button1的点击事件
    button1.setOnClickListener {
                            上下文      要启动的目标Activity
        val intent = Intent(this, SecondActivity::class.java)
        startActivity(intent)
    }
}

在上面的代码中我们声明了一个Intent对象,Intent对象要求传入两个参数:①上下文和②目标Activity。这里面Kotlin语言的SecondActivity::class.java相当于Java语言中的SecondActivity.class。然后我们通过startActivity()方法来实现页面的跳转,并向其中传入intent参数。

1.2.2 隐式Intent的使用

相比于显式Intent,隐式Intent就要含蓄的多。隐式Intent并没有明确指明目标组件,而是通过actioncategory等信息来让系统判断要启动哪个Activity。需要注意的是,使用隐式Intent时需要确保至少存在一个能够处理该Intent的组件,否则会导致应用崩溃
在这里插入图片描述
AndroidManifest.xml中给SecondActivity配置隐式Intent的方法:

  • < action > 标签 中指明当前Activity可以响应“com.example.activitytest.ACTION_START”这个action。
  • < category >标签 中指明该Intent除了action还必须带有"android.intent.category.DEFAULT"的category才可以响应。

注意:只有Activity的< action >和< category >中的内容同时匹配Intent中指定的action和category时,这个Activity才能响应该Intent。

<application
    · · ·>
    <activity
        android:name=".SecondActivity"
        android:exported="true" //注意这里必须为true
        android:label="This is SecondActivity">
        <intent-filter>
            //只有Activity中的action和category都和Intent匹配成功才能响应
            <action android:name="com.example.activitytest.ACTION_START" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>

FirstActivity.java中修改按钮点击逻辑:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.first_layout)
    button1.setOnClickListener {
        //为当前intent配置action
        val intent = Intent("com.example.activitytest.ACTION_START")
        startActivity(intent)
    }

这次你点击FirstActivity中的Button按钮,实现跳转到SecondActivity页面就是通过隐式Intent来实现的。一个Intent只能配置一个action,但是却可以配置多个category。我们在上面的例子中使用了默认的category:“android.intent.category.DEFAULT”。下面我们额外增加一个category,看一下会发生什么:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.first_layout)
   button1.setOnClickListener {
       //该intent配置了一个ACTION_START的action和一个MY_CATEGORY的category
       val intent = Intent("com.example.activitytest.ACTION_START")
       intent.addCategory("com.example.activitytest.MY_CATEGORY")
       startActivity(intent)
   }

这时如果你点击FirstActivity中的Button1按钮并不会实现跳转,而是会导致crash。这是因为我们刚才在Intent中添加了一个自定义的category:“com.example.activitytest.MY_CATEGORY”。而SecondActivity中的< intent-filter >标签中没有声明可以响应这个自定义的category:

<intent-filter>
        <action android:name="com.example.activitytest.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
 </intent-filter>

在前面我们讲过,Activity中的action和category必须都和Intent中的action和category匹配成功,Activity才能响应这个Intent。这里只成功匹配了action,但是却没有可以与之相匹配的category,所以会导致程序crash。现在我们为SecondActivity配置这个自定义的category:

<intent-filter>
        <action android:name="com.example.activitytest.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
        //为SecondActivity添加一个category声明
        <category android:name="com.example.activitytest.MY_CATEGORY" />
 </intent-filter>

这次SecondActivity就可以响应action(ACTION_START)和category(DEFAULT/MY_CATEGORY)了。当我们再次点击FirstActivity中的Button1按钮时就可以成功跳转到SecondActivity了。

1.2.3 更多隐式Intent的用法

隐式Intent不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity。接下来我们尝试调用系统的浏览器打开一个网页。修改FirstActivity中的按钮点击逻辑:

button1.setOnClickListener {
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("https://www.baidu.com")
    startActivity(intent)
}

这里我们指定这个Intent的action是“Intent.ACTION_VIEW”,并通过Uri.parse()将地址字符串解析成一个uri对象,然后将这个uri对象传递给Intent。这次在FirstActivity中点击Button1按钮,可以看到我们成功跳转到系统浏览器并打开百度了。

除了打开指定的页面,我们还可以在AndroidManifest文件中为Activity的< intent-filter>标签额外配置一个< data>标签用来更精准的指定当前Activity能够响应的数据。< data>标签中主要配置以下内容:

  • android:scheme:指定数据协议(例如:https)
  • android:host:指定数据主机名(例如:www.baidu.com)
  • android:port:指定数据端口
  • android:path:指定主机名和端口之后的部分(例如:域名后面的内容)
  • android:mimeType:指定可以处理的数据类型

①我们新建一个ThirdActivity.java文件用来实验< data >标签中的scheme配置。它的third_layout布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/button3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 3" />
</LinearLayout>

在AndroidManifest.xml中:

<activity
    android:name=".ThirdActivity"
    android:exported="false"
    tools:ignore="AppLinkUrlError">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        //指定数据协议必须为"https"
        <data android:scheme="https" />
    </intent-filter>
</activity>

这次我们在FirstActivity中点击Button3时,系统就会弹出提示框要我们选择使用哪个App来打开百度网页。由于我们在清单文件中配置了ThirdActivity可以响应https网页请求的操作,所以我们的ActivityTest应用也会出现在选择框里供用户选择。(这里只是演示data标签的使用,实际上ThirdActivity并不具备可以响应网页请求的功能。)

②接下来我们看一下如何在我们的程序中调用系统拨号盘

button1.setOnClickListener {
    val intent = Intent(Intent.ACTION_DIAL)
    intent.data = Uri.parse("tel:10086")
    startActivity(intent)
}

这样我们在点击FirstActivity的Button1按钮时就会调用系统默认的拨号盘并自动帮我们填入10086号码了。

1.3 使用Intent实现数据的传递

Intent不仅可以用来启动Activity,也可以在启动Activity的时候传递数据

1.3.1 向下一个Activity传递数据

Intent提供了一系列 putExtra() 方法的重载,使得我们可以将数据以键值对的形式暂存在Intent中,在启动另一个Activity后再将这些数据从Intent中取出来。例如我们想将FirstActivity中的data字符串传递给SecondActivity,就可以这么写:

button1.setOnClickListener {
    //需要传递给SecondActivity的数据data
    val data = "Hello SecondActivity"
    val intent = Intent(this, SecondActivity::class.java)
    //将要传递的数据通过putExtra方法以键值对的形式暂存在Intent中
                       key       value
    intent.putExtra("extra_data", data)
    startActivity(intent)
}

接下来,我们需要在SecondActivity中取出FirstActivity传递过来的数据:

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        //通过key来接收另一个Activity传递过来的数据
        val extraData = intent.getStringExtra("extra_data")
        Log.d("SecondActivity", "extra_data is : $extraData")
    }
}

这次我们点击FirstActivity页面的button就会跳转到SecondActivity页面中,并且在打印出FirstActivity传递过来的字符串:
在这里插入图片描述
除了getStringExtra的方法,我们还可以是用getIntExtra、getBooleanExtra等方法来向Intent中保存不同类型的数据。

1.3.2 通过startActivityForResult()向上一个Activity返回数据

Intent不仅可以传递数据给下一个Activity,也可以返回数据给上一个Activity。返回数据给上一个页面有两种触发方式:

  • ①在Activity销毁时返回结果给上一个Activity
  • ②按下Back返回键时返回数据给上一个Activity

①在Activity销毁时返回结果给上一个Activity
Activity类中还有一个 startActivityForResult() 方法,它会在目标Activity销毁时返回一些数据给当前ActivitystartActivityForResult()方法接收两个参数:①Intent ②请求码(必须>=0,用于判断数据来源的)。 我们修改FirstActivity中的按钮的点击事件:

button1.setOnClickListener {
    val intent = Intent(this, SecondActivity::class.java)
                                   请求码(标识跳转的目标Activity)
    startActivityForResult(intent, 1001)
}

接下来我们需要实现SecondActivity中按钮的点击事件:
SecondActivity中的这个Intent没有任何"意图",它的作用仅仅是用于传递数据。setResult()方法非常重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:①向上一个Activity返回处理结果(RESULT_OK或RESULT_CANCELED) ②带有数据的Intent。

button2.setOnClickListener {
    val intent = Intent()
    //向上一个Activity返回的数据
                        key                 value
    intent.putExtra("data_return", "Hello FirstActivity")
              处理结果   带有数据的intent
    setResult(RESULT_OK, intent)
    finish()
}

现在SecondActivity中点击Button3就能够销毁当前页面向FirstActivity传递数据了。由于我们是使用startActivityForResult()方法启动SecondActivity的,当SecondActivity被销毁后会调用上一个Activity的onActivityResult()方法。因此我们需要在FirstActivity中重写这个方法来得到返回的数据:

                                请求码           处理结果        带有返回数据的Intent
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    //通过请求码确定返回的数据是否来自SecondActivity
    when (requestCode) {
        //若数据来自SecondActivity并且处理结果为OK
        1001 -> if (resultCode == RESULT_OK) {
            //将返回的数据保存至returnedData中
            val returnedData = data?.getStringExtra("data_return")
            Log.d("FirstActivity", "returned data is: $returnedData")
        }
    }
}

在这里插入图片描述
按下Back返回键时返回数据给上一个Activity
在上面的例子中我们是通过在SecondActivity中点击Button2按钮来触发页面销毁并返回FirstActivity的。那么当用户点击Back返回按钮返回到FirstActivity时该怎么返回数据给FirstActivity呢?我们可以通过在SecondActivity中重写onBackPressed()方法来解决:

override fun onBackPressed() {
    val intent = Intent()
    intent.putExtra("data_return", "Hello FirstActivity")
    setResult(RESULT_OK, intent)
    finish()
}

这样当用户按下Back按钮就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。

我们可以通过startActivityForResult()方法打开另一个Activity,并在另一个Activity销毁时返回一些数据给上一个Activity。遗憾的是startActivityForResult()方法在低版本Android上已经被弃用,转而用registerForActivityResult()方法进行替代。


2024年2月2日更新:

1.3.3 通过registerForActivityResult()向上一个Activity返回数据

①在Activity销毁时返回结果给上一个Activity
由于startActivityForResult()方法在高版本Android上被弃用了,所以我们来学习一下如何使用他的替代方法。下面的示例中使用到了ViewBinding来绑定控件,如果你还没使用过ViewBinding可以参考我的第23篇文章。
我们期望有MainActivity——>SecondActivity后,销毁SecondActivity并返回数据给MainActivity。下面是MainActivity.kt中的代码:

class MainActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityMainBinding

    private val requestDataLauncher =
        //注册ActivityResult的观察者 当启动的Activity返回结果时 这个观察者会被触发
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data = result.data?.getStringExtra("mData")
                Toast.makeText(this,data,Toast.LENGTH_SHORT).show()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mBinding.mButton1.setOnClickListener {
            val intent = Intent(this, ThirdActivity::class.java)
            //通过ActivityResult观察者启动一个Activity 并在其销毁后返回结果
            requestDataLauncher.launch(intent)
        }
    }
}

下面是SecondActivity中的代码:

class ThirdActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityThirdBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityThirdBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mBinding.mButton2.setOnClickListener {
            val data = "This is data for MainActivity!"
            val intent = Intent()
            // 将数据存放到intent中
            intent.putExtra("mData", data)
            setResult(RESULT_OK, intent)
            finish()
        }
    }
}

这样当我们在SecondActivity中点击按钮finish当前Activity时,就会将数据返回给MainActivity。
按下Back返回键时返回数据给上一个Activity
在旧版本中我们是通过重写onBackPressed()方法来实现返回按键的监听的,但是在Android 13版本中onBackPressed()方法被弃用了,我们可以使用AndroidX 的API来实现滑动手势功能的回调。
首先在build.gradle.kts文件中中添加以下依赖:

dependencies {
    implementation("androidx.activity:activity-ktx:1.9.0-alpha02")
    · · ·
}

然后在AndroidManifest.xml清单文件中将enableOnBackInvokedCallback属性设置为true:

<application
	 android:enableOnBackInvokedCallback="true"
	 · · ·
</application>

最后注册OnBackPressedCallback()方法来处理返回手势:

class SecondActivity: AppCompatActivity() {

    private lateinit var mBinding: ActivityThirdBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityThirdBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        
		// 在Activity中处理返回手势
        onBackPressed(true){
            val data = "This is data for MainActivity!"
            val intent = Intent()
            intent.putExtra("mData", data)
            setResult(RESULT_OK, intent)
            finish()
        }
    }
}

fun AppCompatActivity.onBackPressed(isEnabled: Boolean, callback: () -> Unit) {
    onBackPressedDispatcher.addCallback(this,
        object : OnBackPressedCallback(isEnabled) {
            override fun handleOnBackPressed() {
                callback()
            }
        })
}

这样当用户按下Back按钮就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。


1.4 Activity的生命周期

1.4.1 返回栈

在Android中Activity是可以层叠的:

  • 每当我们启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶(覆盖在原来的Activity上)。
  • 每当我们点击Back键或者调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,下面的Activity就会重新处于栈顶位置并显示出来。

1.4.2 Activity的生命周期

  • onCreate( ):Activity第一次被创建时调用(例如:加载布局、绑定事件)。
  • onStart( ):Activity由不可见变为可见时调用,此时Activity不可交互(位于后台并未获得焦点,界面尚未被绘制)。
  • onResume( ):Activity 准备好和用户进行交互 时调用,此时Activity可见(位于栈顶)并且可交互。
  • onPause( ):另一个Activity(未占满屏幕或者全透明)跑到前台时,原来Activity的onPause( )方法会被调用,此时原来的Activity不可见。
  • onStop( ):Activity完全不可见时调用。
  • onDestroy( ):Activity被销毁之前调用,之后Activity将变为销毁状态。
  • onRestart( ):Activity被重新启动时调用。
    【7】应用开发——Activity全面讲解_第3张图片

1.4.3 Activity被回收了怎么办

想象以下场景:应用中有一个页面A,用户在页面A的基础上启动了页面B,此时页面A就进入了停止状态。若这个时候系统内存不足,就很可能将页面A回收掉了。当用户按下Back键返回页面A时,其实还是会正常显示页面A的。但是会重新执行页面A的onCreate()方法而不是onRestart()方法,因为页面A在被回收后会被重新创建一次。需要注意的是,页面A中可能存在临时数据(例如文本框中已经输入的文字)。在上面的情况下,当用户按下Back键返回页面A时会发现页面A中的临时数据(例如文本框中已经输入的文字)都没了,这可太糟糕了!

为此,Activity专门提供了一个 onSaveInstanceState()方法 来解决这个问题。onSaveInstanceState()方法可以保证在页面被回收之前一定会被调用,因此我们可以通过这个方法来解决临时数据的保存问题。onSaveInstanceState()方法携带一个Bundle类型的参数,我们可以用putString()、putInt()等方法将数据以键值对的形式保存到Bundle中
例如:MainActivity——>NormalActivity(此时MainActivity被回收、临时数据丢失)——>MainActivity。我们就可以在MainActivity中添加以下代码来保存当前页面中的临时数据:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    val tempData = "Something you wish to save"
                         key        value
    outState.putString("data_key", tempData)
}

这样就将临时数据保存下来了,那么该如何恢复呢?Activity的onCreate()方法其实也有一个Bundle类型的参数:

override fun onCreate(savedInstanceState: Bundle?) {···}

这个Bundle参数一般情况下都是null。但是如果你在Activity被回收之前,通过onSaveInstanceState()方法保存了数据,那么这个Bundle参数就会带有之前保存的全部数据。为了恢复MainActivity页面中的数据,我们只需要将数据提取出来即可:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    if (savedInstanceState != null) {
        //通过key将保存的临时数据取出
        val tempData = savedInstanceState.getString("data_key")
        Log.d("MainActivity", "tempData is :$tempData")
    }
}

取出值后再做相应的恢复操作就可以啦,比如说将文本内容再重新赋值给文本输入框等等,这里只是简单的打印一下。

1.5 Activity的启动模式

在Kotlin中,Activity的启动模式决定了Activity与返回栈之间的关联方式。Activity的启动模式一共有四种:① standard模式 、② singleTop模式 、③ singleTask模式、 ④ singleInstance模式。我们可以在AndroidManifest.xml文件中通过给< activity>标签指定android:launchMode=”XXX”来选择启动模式

1.5.1 standard模式(标准)

standard模式是Activity的默认启动方式。在standard模式下,每当启动一个新的Activity,他就会在返回栈中入栈,并且处于栈顶的位置。例如我们在A页面中有一个按钮,当我们点击这个按钮时会再次打开一个A页面。如果我们点击3次按钮,那么就会创建出三个新的A页面实例(实例标识是不一样的)。此时我们需要连续按3次Back键才能退出程序。

1.5.2 singleTop模式(栈顶复用)

singleTop模式下,若启动Activity时发现该Activity已经位于返回栈的栈顶,则认为可以直接复用它,不会再创建新的Activity实例

<activity
    android:name=".SingleTopActivity"
    //singleTop模式
    android:launchMode="singleTop"
    android:label="This is SingleTopActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

这次如果我们重复之前的动作,不管我们点击多少次按钮,都不会再创建新的Activity实例。因为当前A页面已经处于返回栈的栈顶,每当再次启动一个A页面时,都会直接使用处于栈顶的Activity。因此A页面只会有一个实例,我们只需要按一次Back返回键就能退出程序。
不过,若我们由A页面跳转到B页面,然后再由B页面跳转到A页面。这时栈顶发生了变化(A1——>B——>A2),所以这两个A页面的实例是两个完全不同的实例。当你按下Back返回键,会从A2页面回到B页面,再按一次Back返回键会从B页面回到A1页面,再次按下Back键才会退出程序(A2——>B——>A1——>Exit)。

1.5.3 singleTask模式(栈内复用)

在启动新的Activity时,系统会先在返回栈中查找是否已经存在该Activity的实例

  • 如果存在,则直接使用该Activity实例,并把在这个Activity之上的所有其他Activity统统出栈
  • 如果不存在,就会创建一个新的Activity实例
<activity
    android:name=".SingleTaskActivity"
    //singleTask模式
    android:launchMode="singleTask">
    ···
</activity>

如果我们还是由A页面跳转到B页面,然后再由B页面跳转到A页面,这时栈顶的状态是:A1——>B——>A1。当我们在B页面中启动A页面时,会发现返回栈中已经存在一个A页面的实例,并且是位于B页面之下。这时B页面会从返回栈中出栈,而A页面会重新成为栈顶。此时A页面会调用onRestart()方法,B页面会调用onDestroy()方法。
【7】应用开发——Activity全面讲解_第4张图片

1.5.4 singleInstance模式(单实例)

singleInstance模式不同于以上三种模式。系统会为这种模式的Activity创建一个新的返回栈来管理这个Activity,并且这个返回栈中只会有这个Activity的实例。这么做的意义是什么呢?
设想一下,我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,用前面三种模式是没办法实现的。由于每个应用程序都有自己的返回栈,在前面三种模式的情况下,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。
而使用singleInstance模式就可以解决这个问题。在这种模式下,系统会创建一个单独的返回栈来管理这个Activity。不管是哪个应用程序来访问这个Activity,都共用这个单独的返回栈,这样就解决了共享Activity实例的问题。

<activity
    android:name=".SingleInstanceActivity"
    //singleInstance模式
    android:launchMode="SingleInstanceActivity">
    ···
</activity>

我们来举个简单的例子,一共有A、B、C三个页面。我们只设置B页面的启动模式为singleInstance,然后进行页面跳转:A页面——>B页面——>C页面。这时你会发现,A页面的和C页面是存放在同一个返回栈中。由于B页面我们设置它的启动模式为singleInstance模式,所以B页面实例是存放在一个单独的返回栈中
这时如果我们在C页面按下Back返回键,那么是会直接回到A页面的,这是因为A、C两页面的实例是存放在同一个返回栈中的。当我们在A页面中再按下Back返回键时,会直接回到B页面,这是因为A、C页面的返回栈已经空了,于是就显示了另一个返回栈的栈顶,也就是B页面所在的返回栈,这时我们再按下Back返回键,这时所有返回栈都为空了,也就退出了程序(C页面——>A页面——>B页面——>Exit)。

二、Activity使用的小技巧

通过前面的学习你已经掌握了Activity的大部分基础内容了。为了方便你更好的在项目中使用和管理Activity,你还需要掌握几个小技巧。这些小技巧会在你的项目或者工作中起到很大的作用,在后续的实战文章(例如第13、16章)中也会有涉及到。

2.1 随时随地退出程序

当我们的项目越来越复杂,会有很多Activity,例如我们从A界面——>B界面——>C界面。这个时候,如果我们想退出应用程序是非常不方便的,我们需要连按多次Back键才行(按下Home键只是把程序挂起,并没有退出程序)。通过下面这个小技巧,就可以轻松实现注销或退出功能
我们新建一个名为ActivityController的单例类用来管理全局的Activity。通过ActivityController,不论在那个界面,只需要调用finishAllActivities()方法就可以关闭所有界面。ActivityController.kt的代码如下:

/**
 * 用于管理全局Activity的单例类
 */
object ActivityController {

    //用于管理所有Activity的集合
    private val activities = ArrayList<Activity>()

    //往集合中添加Activity
    fun addActivity(targetActivity: Activity) {
        activities.add(targetActivity)
    }

    //从集合中移除Activity
    fun removeActivity(targetActivity: Activity) {
        activities.remove(targetActivity)
    }

    //关闭集合中的所有的Activity
    fun finishAllActivities() {
        for (activity in activities) {
            //判断Activity是否正在销毁中
            if (!activity.isFinishing) {
                activity.finish()
            }
        }
        //销毁所有Activity后清空集合
        activities.clear()
    }
}

我们将ActivityController声明为单例类,这样类中的所有方法都可以直接通过类名来访问。首先声明了一个ArrayList< Activity>集合用来存放所有的Activity,然后提供了addActivity()、removeActivity()、finishAllActivities()方法用来管理这个Activity集合。在finishAllActivities()方法中,我们遍历Activity集合中的每一个Activity,然后调用finish()方法将它们逐一销毁,最后调用clear()方法清空这个Activity集合。

在创建好单例工具类后,紧接着创建一个BaseActivity作为所有Activity的父类,这样便于我们对全局Activity进行管理。我们在BaseActivity的onCreate()方法和onDestroy()方法中实现了Activity集合元素的添加和移除。当某个Activity继承自BaseActivity后,在该Activity的生命周期中会自动调用父类BaseActivity的onCreate()和onDestroy()方法,进而实现Activity集合的添加和移除:

/**
 * 全局Activity的父类
 */
open class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        //将当前Activity添加到全局Activity列表中的
        ActivityController.addActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        //从全局Activity列表中移除当前Activity
        ActivityController.removeActivity(this)
    }
}

这样我们若想实现退出应用程序或者注销登陆操作,只需要调用ActivityController.finishAllActivities() 方法就可以了。

//销毁Activity列表中所有的Activity
ActivityController.finishAllActivities()

如果你想在销毁所有Activity后再杀掉当前应用程序的进程,以保证程序完全退出。你可以在调用finishAllActivities()方法后加上以下代码:

//kill当前应用程序进程
android.os.Process.killProcess(android.os.Process.myPid())

需要注意的是killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序(不然那就太离谱了)。

2.2 启动一个Activity的最佳写法

在实际项目或者工作中有很多Activity可能并不是你负责开发的,而你却要启动这个Activity。这个时候你很可能不知道要向该Activity中传递那些数据。我们可以通过下面这个方法轻松解决:

class NewsContentActivity : AppCompatActivity() {
	· · ·
	// 通过companion object定义一个静态方法
    companion object {
        fun actionStart(context: Context, title: String, content: String) {
            val intent = Intent(context, NewsContentActivity::class.java).apply {
                putExtra("news_title", title)
                putExtra("news_content", content)
            }
            context.startActivity(intent)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    	· · ·
	}
	· · ·
}

假如我们需要跳转到NewsContentActivity,并且要求传入新闻的标题和新闻内容这两个数据。我们可以通过在companion object{ }结构声明一个静态方法actionStart(),然后通过intent将NewsContentActivity所需要的所有数据传递过来。这样我们如果想要在其他页面启动NewsContentActivity,可以使用actionStart()方法,Android Studio会要求我们传入所需要的数据才能启动NewsContentActivity。

NewsContentActivity.actionStart(parent.context,news.title,news.content)

效果如下图:
在这里插入图片描述

你可能感兴趣的:(奇妙的Kotlin之旅,kotlin,android,开发语言)