Activity

Activity是什么

Activity是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交 互。一个应用程序中可以包含零个或多个Activity,但不包含任何Activity的应用程序很少见, 谁也不想让自己的应用永远无法被用户看到吧

Activity的基本使用方法

到现在为止,你还没有手动创建过Activity呢,因为第1章中的MainActivity是Android Studio 自动帮我们创建的。手动创建Activity可以加深我们的理解,因此现在是时候自己动手了。选择“Add No Activity”

手动创建Activity

现在右击com.example.activitytest包→New→Activity→Empty Activity,会弹出一个创建 Activity的对话框,我们将Activity命名为FirstActivity,并且不要勾选Generate Layout File 和Launcher Activity这两个选项

勾选Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件,勾选 Launcher Activity表示会自动将FirstActivity设置为当前项目的主Activity。由于你是第一次 手动创建Activity,这些自动生成的东西暂时都不要勾选,下面我们将会一个个手动来完成。勾 选Backwards Compatibility表示会为项目启用向下兼容旧版系统的模式,这个选项要勾上

你需要知道,项目中的任何Activity都应该重写onCreate()方法,而目前我们的FirstActivity 中已经重写了这个方法,这是Android Studio自动帮我们完成的,代码如下所示

class FirstActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
}
}

创建和加载布局

前面我们说过,Android程序的设计讲究逻辑和视图分离,最好每一个Activity都能对应一个布 局。布局是用来显示界面内容的,我们现在就来手动创建一个布局文件。

右击app/src/main/res目录→New→Directory,会弹出一个新建目录的窗口,这里先创建一个 名为layout的目录。然后对着layout目录右键→New→Layout resource file,又会弹出一个 新建布局资源文件的窗口,我们将这个布局文件命名为first_layout,根元素默认选择为 LinearLayout

可看到如下代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

由于我们刚才在创建布局文件时选择了LinearLayout作为根元素,因此现在布局文件中已经有 一个LinearLayout元素了。我们现在对这个布局稍做编辑,添加一个按钮,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
androdi:layout_height="wrap_content"
androdi:text="Button 1"
/>
</LinearLayout>

可以看到,按钮已经成功显示出来了,这样一个简单的布局就编写完成了。那么接下来我们要 做的,就是在Activity中加载这个布局

class FirstActivity:AppCompatActivity{
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
}
}

可以看到,这里调用了setContentView()方法来给当前的Activity加载一个布局,而在 setContentView()方法中,我们一般会传入一个布局文件的id。在第1章介绍项目资源的时 候我曾提到过,项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创 建的first_layout.xml布局的id现在已经添加到R文件中了。在代码中引用布局文件的方法 你也已经学过了,只需要调用R.layout.first_layout就可以得到first_layout.xml布 局的id,然后将这个值传入setContentView()方法即可

在AndroidManifest文件中注册

我们打开 app/src/main/AndroidManifest.xml文件瞧一瞧,代码如下所示

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.activitytest">
<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">
<activity android:name=".FirstActivity">
</activity>
</application>
</manifest>

可以看到,Activity的注册声明要放在标签内,这里是通过标签 来对Activity进行注册的。那么又是谁帮我们自动完成了对FirstActivity的注册呢?当然是 Android Studio了。在过去,当创建Activity或其他系统组件时,很多人会忘记要去Android Manifest.xml中进行注册,从而导致程序运行崩溃,很显然Android Studio在这方面做得更加 人性化

在标签中,我们使用了android:name来指定具体注册哪一个Activity,那么这 里填入的.FirstActivity是什么意思呢?其实这不过是 com.example.activitytest.FirstActivity的缩写而已。由于在最外层的 标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注 册Activity时,这一部分可以省略,直接使用.FirstActivity就足够了

不过,仅仅是这样注册了Activity,我们的程序仍然不能运行,因为还没有为程序配置主 Activity。也就是说,程序运行起来的时候,不知道要首先启动哪个Activity。配置主Activity 的方法其实在第1章中已经介绍过了,就是在标签的内部加入 标签,并在这个标签里添加和这两句声明即可。

除此之外,我们还可以使用android:label指定Activity中标题栏的内容,标题栏是显示在 Activity最顶部的,待会儿运行的时候你就会看到。需要注意的是,给主Activity指定的label不 仅会成为标题栏中的内容,还会成为启动器(Launcher)中应用程序显示的名称。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.activitytest">
<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">
<activity android:name=".FirstActivity"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="androiod.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

这样,FirstActivity就成为我们这个程序的主Activity了,点击桌面应用程序图标时首先打开的 就是这个Activity。另外需要注意,如果你的应用程序中没有声明任何一个Activity作为主 Activity,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序。这 种程序一般是作为第三方服务供其他应用在内部进行调用的。

在Activity 中使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通 知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间,我们现在就尝试 一下如何在Activity中使用Toast。

首先需要定义一个弹出Toast的触发点,正好界面上有个按钮,那我们就让这个按钮的点击事件 作为弹出Toast的触发点吧。在onCreate()方法中添加如下代码:

override fun onCreate(savedInstaceState:Bundle?){
super.onCreate(savedInstaceState)
steContentView(R.layout.first_layout)
val button1:Buntton = findViewById(R.id.button1)
button1.setOnClickListener{
Toast.makeText(this,"You clicked Button 1",Toast.LENGTH_SHORT).show()
}
}

在Activity中,可以通过findViewById()方法获取在布局文件中定义的元素,这里我们传入 R.id.button1来得到按钮的实例,这个值是刚才在first_layout.xml中通过android:id属 性指定的。findViewById()方法返回的是一个继承自View的泛型对象,因此Kotlin无法自动 推导出它是一个Button还是其他控件,所以我们需要将button1变量显式地声明成Button类 型。得到按钮的实例之后,我们通过调用setOnClickListener()方法为按钮注册一个监听 器,点击按钮时就会执行监听器中的onClick()方法。因此,弹出Toast的功能当然是要在 onClick()方法中编写了。 Toast的用法非常简单,通过静态方法makeText()创建出一个Toast对象,然后调用show() 将Toast显示出来就可以了。这里需要注意的是,makeText()方法需要传入3个参数。第一个 参数是Context,也就是Toast要求的上下文,由于Activity本身就是一个Context对象,因此 这里直接传入this即可。第二个参数是Toast显示的文本内容。第三个参数是Toast显示的时 长,有两个内置常量可以选择:Toast.LENGTH_SHORT和Toast.LENGTH_LONG

于findViewById()方法的使用,我还得再多讲一些。我们已经知道,findViewById()方 法的作用就是获取布局文件中控件的实例,但是前面的例子比较简单,只有一个按钮,如果某 个布局文件中有10个控件呢?没错,我们就需要调用10次findViewById()方法才行。这种 写法虽然很正确,但是很笨拙,于是就滋生出了诸如ButterKnife之类的第三方开源库,来简化 findViewById()方法的调用。

不过,这个问题在Kotlin中就不复存在了,因为使用Kotlin编写的Android项目在 app/build.gradle文件的头部默认引入了一个kotlin-android-extensions插件,这个插件会根 据布局文件中定义的控件id自动生成一个具有相同名称的变量,我们可以在Activity里直接使用 这个变量,而不用再调用findViewById()方法了

在Activity中使用Menu

首先在res目录下新建一个menu文件夹,右击res目录→New→Directory,输入文件夹 名“menu”,点击“OK”。接着在这个文件夹下新建一个名叫“main”的菜单文件,右击menu文 件夹→New→Menu resource file

文件名输入“main”,点击“OK”完成创建,然后在main.xml中添加如下代码

<menu xmlns:android="http://schemas.androdi.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add"/>
<item
android:id="@+id/remove_item"
android:title="Remove"/>
</menu>

这里我们创建了两个菜单项,其中标签用来创建具体的某一个菜单项,然后通过 android:id给这个菜单项指定一个唯一的标识符,通过android:title给这个菜单项指定一 个名称

接着回到FirstActivity中来重写onCreateOptionsMenu()方法,重写方法可以使用Ctrl + O 快捷键

然后在onCreateOptionsMenu()方法中编写如下代码:

override fun onCreateOptionsMenu(menu:Menu?):Boolean{
menuInflater.inflate(R.menu.main,menu)
return true
}

在继续讲解这段代码之前,我还得再介绍一个Kotlin的语法糖。如果你熟悉Java的话,应该知道 Java Bean的概念,它是一个非常简单的Java类,会根据类中的字段自动生成相应的Getter和 Setter方法,如下所示:

 public class Book { 

private int pages;

public int getPages() {
return pages;
}

public void setPages(int pages) {
this.pages = pages;
}
}

在Kotlin中调用这种语法结构的Java方法时,可以使用一种更加简便的写法,比如用如下代码来 设置和读取Book类中的pages字段:

val book = Book()
book.pages=500
val bookPages=book.pages

这里看上去好像我们并没有调用Book类的setPages()和getPages()方法,而是直接对 pages字段进行了赋值和读取。其实这就是Kotlin给我们提供的语法糖,它会在背后自动将上述 代码转换成调用setPages()方法和getPages()方法

而我们刚才在onCreateOptionsMenu()方法中编写的menuInflater就使用了这种语法糖, 它实际上是调用了父类的getMenuInflater()方法。getMenuInflater()方法能够得到一 个MenuInflater对象,再调用它的inflate()方法,就可以给当前Activity创建菜单了。 inflate()方法接收两个参数:第一个参数用于指定我们通过哪一个资源文件来创建菜单,这 里当然是传入R.menu.main;第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当 中,这里直接使用onCreateOptionsMenu()方法中传入的menu参数。最后给这个方法返回 true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单将无法显示。

当然,仅仅让菜单显示出来是不够的,我们定义菜单不仅是为了看的,关键是要菜单真正可用 才行,因此还要再定义菜单响应事件。在FirstActivity中重写onOptionsItemSelected()方 法,如下所示

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

在onOptionsItemSelected()方法中,我们通过调用item.itemId来判断点击的是哪一个 菜单项。另外,其实这里也应用了刚刚学到的语法糖,Kotlin实际上在背后调用的是item的 getItemId()方法。接下来我们将item.itemId的结果传入when语句当中,然后给每个菜单 项加入自己的逻辑处理,这里我们就活学活用,弹出一个刚刚学会的Toas

销毁一个Activity

修改按钮监听器中的代码,如下所示

button1.setOnClickListener{
finish()
}

使用Intent在Activity之间穿梭

使用显示Intent

你应该已经对创建Activity的流程比较熟悉了,那我们现在在ActivityTest项目中再快速地创建 一个Activity。 还是右击com.example.activitytest包→New→Activity→Empty Activity,会弹出一个创建 Activity的对话框,这次我们命名为SecondActivity,并勾选Generate Layout File,给布局 文件起名为second_layout,但不要勾选Launcher Activity选项

现在第二个Activity已经创建完成,剩下的问题就是如何去启 动它了,这里我们需要引入一个新的概念:Intent

Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执 行的动作,还可以在不同组件之间传递数据。Intent一般可用于启动Activity、启动Service以 及发送广播等场景,由于Service、广播等概念你暂时还未涉及,那么本章我们的目光无疑就锁 定在了启动Activity上面。

Intent大致可以分为两种:显式Intent和隐式Intent。我们先来看一下显式Intent如何使用

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class cls)。这个构造函数接收两个参数:第一个参数Context要求提供一个启动Activity的上下 文;第二个参数Class用于指定想要启动的目标Activity,通过这个构造函数就可以构建出 Intent的“意图”。那么接下来我们应该怎么使用这个Intent呢?Activity类中提供了一个 startActivity()方法,专门用于启动Activity,它接收一个Intent参数,这里我们将构建好 的Intent传入startActivity()方法就可以启动目标Activity了

修改FirstActivity中按钮的点击事件,代码如下所示:

button1.setOnClickListener{
val intent = Intent(this,SecondActitivy::class.java)
startActivity(intent)
}

我们首先构建了一个Intent对象,第一个参数传入this也就是FirstActivity作为上下文,第二 个参数传入SecondActivity::class.java作为目标Activity,这样我们的“意图”就非常明 显了,即在FirstActivity的基础上打开SecondActivity。注意,Kotlin中 SecondActivity::class.java的写法就相当于Java中SecondActivity.class的写法。 接下来再通过startActivity()方法执行这个Intent就可以了

重新运行程序,在FirstActivity的界面点击一下按钮,可以看到,我们已经成功启动SecondActivity了。如果你想要回到上一个Activity怎么办呢? 很简单,按一下Back键就可以销毁当前Activity,从而回到上一个Activity了。 使用这种方式来启动Activity,Intent的“意图”非常明显,因此我们称之为显式Intent

使用隐式Intent

相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出想要启动哪一个Activity,而是 指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮 我们找出合适的Activity去启动

什么叫作合适的Activity呢?简单来说就是可以响应这个隐式Intent的Activity,那么目前 SecondActivity可以响应什么样的隐式Intent呢?额,现在好像还什么都响应不了,不过很快就可以了。

通过在标签下配置的内容,可以指定当前Activity能够响应的 action和category,打开AndroidManifest.xml,添加如下代码

<activity android:name=".SecondeActivity">
<intent-filter>
<action android:name="con.example.activityest.ACCTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

在标签中我们指明了当前Activity可以响应 com.example.activitytest.ACTION_START这个action,而标签则包含了 一些附加信息,更精确地指明了当前Activity能够响应的Intent中还可能带有的category。只 有和中的内容同时匹配Intent中指定的action和category时,这个 Activity才能响应该Intent

修改FirstActivity中按钮的点击事件,代码如下所示:

button1.setOnClickListener{
val intent = Intent("com.example.activitytest.ACTION_START")
startActivity(intent)
}

可以看到,我们使用了Intent的另一个构造函数,直接将action的字符串传了进去,表明我们 想要启动能够响应com.example.activitytest.ACTION_START这个action的Activity。 前面不是说要和同时匹配才能响应吗?怎么没看到哪里有指定 category呢?这是因为android.intent.category.DEFAULT是一种默认的category, 在调用startActivity()方法的时候会自动将这个category添加到Intent中。

每个Intent中只能指定一个action,但能指定多个category。目前我们的Intent中只有一个 默认的category,那么现在再来增加一个吧。

button1.setOnClickListener{
val intent = Intent("com.example.activitytest.ACTION_START")
intent.addCategory("com.example.activitytest.MY_CATEGORY")
startActivity(intent)
}

更多的Intent的用法

使用隐式Intent,不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity,这就 使多个应用程序之间的功能共享成为了可能。比如你的应用程序中需要展示一个网页,这时你 没有必要自己去实现一个浏览器(事实上也不太可能),只需要调用系统的浏览器来打开这个 网页就行了。

修改FirstActivity中按钮点击事件的代码,如下所示:

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

这里我们首先指定了Intent的action是Intent.ACTION_VIEW,这是一个Android系统内置 的动作,其常量值为android.intent.action.VIEW。然后通过Uri.parse()方法将一个 网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。 当然,这里再次使用了前面学习的语法糖,看上去像是给Intent的data属性赋值一样。 重新运行程序,在FirstActivity界面点击按钮就可以看到打开了系统浏览器

在上述代码中,可能你会对setData()方法部分感到陌生,这是我们前面没有讲到的。这个方 法其实并不复杂,它接收一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据 通常是以字符串形式传入Uri.parse()方法中解析产生的。

与此对应,我们还可以在标签中再配置一个标签,用于更精确地指 定当前Activity能够响应的数据。标签中主要可以配置以下内容。

android:scheme。用于指定数据的协议部分,如上例中的https部分。 android:host。用于指定数据的主机名部分,如上例中的部分。 android:port。用于指定数据的端口部分,一般紧随在主机名之后。 android:path。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内 容。 android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有当标签中指定的内容和Intent中携带的Data完全一致时,当前Activity才能够响应 该Intent。不过,在标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其 实只需要指定android:scheme为https,就可以响应所有https协议的Intent了。

右击com.example.activitytest包→New→Activity→Empty Activity,新建ThirdActivity, 并勾选Generate Layout File,给布局文件起名为third_layout,点击“Finish”完成创建。然 后编辑third_layout.xml,将里面的代码替换成如下内容

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 3"
/>
</LinearLayout>

ThirdActivity中的代码保持不变即可,最后在AndroidManifest.xml中修改ThirdActivity的注 册信息

<activity android:name=".ThirdActivity">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.ViEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https/">
</intent-filter>
</activity>

我们在ThirdActivity的中配置了当前Activity能够响应的action是 Intent.ACTION_VIEW的常量值,而category则毫无疑问地指定了默认的category值,另 外在标签中,我们通过android:scheme指定了数据的协议必须是https协议,这样 ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent了。另外,由于Android Studio认为所有能够响应ACTION_VIEW的Activity都应该加上BROWSABLE的category,否 则就会给出一段警告提醒。加上BROWSABLE的category是为了实现deep link功能,和我们 目前学习的东西无关,所以这里直接在标签上使用tools:ignore属性将 警告忽略即可。

可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。选择 Chrome还会像之前一样打开浏览器,并显示百度的主页,而如果选择了ActivityTest,则会启 动ThirdActivity。JUST ONCE表示只是这次使用选择的程序打开,ALWAYS则表示以后一直使 用这次选择的程序打开。需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的 Intent的,但实际上这个Activity并没有加载并显示网页的功能,所以在真正的项目中尽量不要 出现这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象

除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电 话。下面的代码展示了如何在我们的程序中调用系统拨号界面。

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

首先指定了Intent的action是Intent.ACTION_DIAL,这又是一个Android系统的内置动 作。然后在data部分指定了协议是tel,号码是10086。重新运行一下程序,在FirstActivity 的界面点击一下按钮

向下一个Activity传递数据

经过前面几节的学习,你已经对Intent有了一定的了解。不过到目前为止,我们只是简单地使 用Intent来启动一个Activity,其实Intent在启动Activity的时候还可以传递数据,下面我们来 一起看一下。

在启动Activity时传递数据的思路很简单,Intent中提供了一系列putExtra()方法的重载,可 以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从 Intent中取出就可以了。比如说FirstActivity中有一个字符串,现在想把这个字符串传递到 SecondActivity中,你就可以这样编写:

button1.setOnClickListener{
val data = "Hello SecondActivity"
val intent = Intent(this,SecondActivity::class.java)
intent.putExtra("extra_data",data)
startActivity(intent)
}

这里我们还是使用显式Intent的方式来启动SecondActivity,并通过putExtra()方法传递了 一个字符串。注意,这里putExtra()方法接收两个参数,第一个参数是键,用于之后从 Intent中取值,第二个参数才是真正要传递的数据

然后在SecondActivity中将传递的数据取出,并打印出来,代码如下所示

class SecondActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstaceState)
setContentView(R.layout.second_layout)
val extraData = intent.getStringExtra("extra_data")
Log.d("SecondActivity","extra data is $extraData")
}
}

上述代码中的intent实际上调用的是父类的getIntent()方法,该方法会获取用于启动 SecondActivity的Intent,然后调用getStringExtra()方法并传入相应的键值,就可以得 到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()方法来获取传 递的数据。如果传递的是整型数据,则使用getIntExtra()方法;如果传递的是布尔型数据, 则使用getBooleanExtra()方法,以此类推。

返回数据给上一个Activity

既然可以传递数据给下一个Activity,那么能不能够返回数据给上一个Activity呢?答案是肯定 的。不过不同的是,返回上一个Activity只需要按一下Back键就可以了,并没有一个用于启动 Activity的Intent来传递数据,这该怎么办呢?其实Activity类中还有一个用于启动Activity的 startActivityForResult()方法,但它期望在Activity销毁的时候能够返回一个结果给上 一个Activity。毫无疑问,这就是我们所需要的。

startActivityForResult()方法接收两个参数:第一个参数还是Intent;第二个参数是请 求码,用于在之后的回调中判断数据的来源。我们还是来实战一下,修改FirstActivity中按钮的 点击事件,代码如下所示

button1.setOnCLickListener{
val intent = Intent(this,SecondActivity::class.java)
startActivityForResult(intent,1)
}

这里我们使用了startActivityForResult()方法来启动SecondActivity,请求码只要是一 个唯一值即可,这里传入了1。接下来我们在SecondActivity中给按钮注册点击事件,并在点 击事件中添加返回数据的逻辑,代码如下所示:

class SecnodActivity:AppCompatActivity(){
override fum onCreate(savedInstancerState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.second_layout)
button2.setOnClickListener{
val intent = Intent()
intent.putExtra("data_return","Hello FirstActivity")
setResult(RESULT_OK,intent)
finish()
}
}
}

可以看到,我们还是构建了一个Intent,只不过这个Intent仅仅用于传递数据而已,它没有指定 任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用了setResult()方法。这个 方法非常重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:第一 个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这 两个值;第二个参数则把带有数据的Intent传递回去。最后调用了finish()方法来销毁当前 Activity。

由于我们是使用startActivityForResult()方法来启动SecondActivity的,在 SecondActivity被销毁之后会回调上一个Activity的onActivityResult()方法,因此我们 需要在FirstActivity中重写这个方法来得到返回的数据,如下所示

override fun onActivityResult(requestCode:int,resultCode:Int,data:Intent?){
super.onActivityResult(requestCode,resultCode,data)
when(requestCode){
1->if(request==RESULT_OK){
val returnedData = data?.getStringExtra("dat_return")
Log.d("FirstActivity","returned data is $returnedData")
}
}
}

onActivityResult()方法带有3个参数:第一个参数requestCode,即我们在启动Activity 时传入的请求码;第二个参数resultCode,即我们在返回数据时传入的处理结果;第三个参 数data,即携带着返回数据的Intent。由于在一个Activity中有可能调用 startActivityForResult()方法去启动很多不同的Activity,每一个Activity返回的数据都 会回调到onActivityResult()这个方法中,因此我们首先要做的就是通过检查 requestCode的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们再通过 resultCode的值来判断处理结果是否成功。最后从data中取值并打印出来,这样就完成了向 上一个Activity返回数据的工作

你可能会问,如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到 FirstActivity,这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,我们可以通 过在SecondActivity中重写onBackPressed()方法来解决这个问题,代码如下所示:

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

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

Activity的生命周期

掌握Activity的生命周期对任何Android开发者来说都非常重要,当你深入理解Activity的生命 周期之后,就可以写出更加连贯流畅的程序,并在如何合理管理应用资源方面发挥得游刃有 余。你的应用程序也将会拥有更好的用户体验。

返回栈

经过前面几节的学习,相信你已经发现了Android中的Activity是可以层叠的。我们每启动一个 新的Activity,就会覆盖在原Activity之上,然后点击Back键会销毁最上面的Activity,下面的 一个Activity就会重新显示出来。

其实Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity 的集合,这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况 下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我 们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前 一个入栈的Activity就会重新处于栈顶的位置。系统总是会显示处于栈顶的Activity给用户。

Activity 状态

每个Activity在其生命周期中最多可能会有4种状态。

运行状态

当一个Activity位于返回栈的栈顶时,Activity就处于运行状态。系统最不愿意回收的就是 处于运行状态的Activity,因为这会带来非常差的用户体验

暂停状态

当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会 觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会 占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的 Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收 可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去 考虑回收这种Activity。

停止状态

当一个Activity不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然 会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要 内存时,处于停止状态的Activity有可能会被系统回收

销毁状态

一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的 Activity,以保证手机的内存充足。

Activity的生存期

Activity类中定义了7个回调方法,覆盖了Activity生命周期的每一个环节,下面就来一一介绍这7个方法

  • onCreate()。这个方法你已经看到过很多次了,我们在每个Activity中都重写了这个方法,它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局绑定事件等等
  • onStart()。这个方法在Activity由不可见变为可见的时候调用
  • onResume()。这个方法在Activity准备好和用户进行交互的时候调用。此时的Activity一定位于返回栈的栈顶,并且处于运行状态
  • onPause()。这个方法在系统准备去启动或者恢复另一个Activity的时候调用。我们通常会在这个方法将消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不如会影响到新的栈顶Actiity的使用
  • onStop()。这个方法在Activity完全不可见时候调用。它和onPause()方法的主要区别在于,如果启动新的Activity时一个对话框式的Activity,那么onPause()方法会得到执行,而onStop方法并不会执行
  • onDestory()。这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态
  • onRestart()。。这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity 被重新启动了

以上7个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将Activity分为以 下3种生存期。

完整生存期。Activity在onCreate()方法和onDestroy()方法之间所经历的就是完整生 存期。一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在 onDestroy()方法中完成释放内存的操作。

可见生存期。Activity在onStart()方法和onStop()方法之间所经历的就是可见生存 期。在可见生存期内,Activity对于用户总是可见的,即便有可能无法和用户进行交互。我 们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()方法中对资 源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不 会占用过多内存。

前台生存期 。Activity在onResume()方法和onPause()方法之间所经历的就是前台生存 期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互 的,我们平时看到和接触最多的就是这个状态下的Activity。

体验Activity的生命周期

讲了这么多理论知识,是时候进行实战了。下面我们将通过一个实例,让你可以更加直观地体 验Activity的生命周期。 这次我们不准备在ActivityTest这个项目的基础上修改了,而是新建一个项目。因此,首先关闭 ActivityTest项目,点击导航栏File→Close Project。然后新建一个ActivityLifeCycleTest项 目,新建项目的过程你应该已经非常清楚了,不需要我再进行赘述,这次我们允许Android Studio帮我们自动创建Activity和布局,这样可以省去不少工作,创建的Activity名和布局名都 使用默认值。 这样主Activity就创建完成了,我们还需要分别再创建两个子Activity——NormalActivity和 DialogActivity,下面一步步来实现。

右击com.example.activitylifecycletest包→New→Activity→Empty Activity,新建 NormalActivity,布局起名为normal_layout。然后使用同样的方式创建DialogActivity,布 局起名为dialog_layout

现在编辑normal_layout.xml文件,将里面的代码替换成如下内容:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layouot_height="match_parent">
<TextView
android:layout_width="match_parent"
android:ayout_height="wrap_content"
android:text="This is a normal activity"
/>
</LinearLayout>

在这个布局中,我们非常简单地使用了一个TextView,用于显示一行文字,在下一章中你将会 学到关于TextView的更多用法

然后编辑dialog_layout.xml文件,将里面的代码替换成如下内容:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertacial"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a dialog activity"
/>
</LinearLayout>

两个布局文件的代码几乎没有区别,只是显示的文字不同而已

其实从名字上就可以看出,这两个Activity一个是普通的Activity,一个是对话框式的 Activity。可是我们并没有修改Activity的任何代码,两个Activity的代码应该几乎是一模一样 的,那么是在哪里将Activity设成对话框式的呢?别着急,下面我们马上开始设置。修改 AndroidManifest.xml的标签的配置,如下所示:

<activity android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog">
</activity>
<activity android:name=".NormalActivity">
</activity>

这里是两个Activity的注册代码,但是DialogActivity的代码有些不同,我们给它使用了一个 android:theme属性,用于给当前Activity指定主题,Android系统内置有很多主题可以选 择,当然我们也可以定制自己的主题,而这里的@style/Theme.AppCompat.Dialog则毫无 疑问是让DialogActivity使用对话框式的主题。

接下来我们修改activity_main.xml,重新定制主Activity的布局,将里面的代码替换成如下内 容

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/startNormalActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start NormalActivity" />
<Button
android:id="@+id/startDialogActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start DialogActivity" />
</LinearLayout>

最后修改MainActivity中的代码,如下所示

class MainActivity:AppCompatActivity(){
private val tag = "MainActivity"
override fun onCreate(savedInstanceState)
Log.d(tag,"onCreate")
setContentView(R.layout.activity_main)
val startNormalActivity:Button = findViewById(R.id.startNormalActivity)
val startDialogActivity:Button = findViewById(R.id.startDialogActivity)
startNormalActivity.setOnClickListener{
val intent = Intent(this,NormalActivity::class.java)
startActivity(intent)
}
startDialogActivity.setOnClickListener{
val intent = Intent(this,DialogActivity::class.java)
startActivity(intent)
}
override fun onStart(){
super.onStart()
Log.d(tag,"onStart")
}
override fun onResume(){
super.onResume()
Log.d(tag,"onResume")
}
override fun onPause(){
super.onPause()
Log.d(tag,"onPause")
}
override fun onStop(){
super.onStop()
Log.d(tag,"onStop")
}
override fun onDestroy(){
super.onDestroy()
Log.d(tag,"onDestroy")
}
override fun onRestart(){
super.onRestart()
Log.d(tag,"onRestart")
}
}

在onCreate()方法中,我们分别为两个按钮注册了点击事件,点击第一个按钮会启动 NormalActivity,点击第二个按钮会启动DialogActivity。然后在Activity的7个回调方法中分 别打印了一句话,这样就可以通过观察日志来更直观地理解Activity的生命周期。

Activity被回收了怎么办

前面我们说过,当一个Activity进入了停止状态,是有可能被系统回收的。那么想象以下场景: 应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了 停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回 Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行 onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况 下会被重新创建一次。 这样看上去好像一切正常,可是别忽略了一个重要问题:Activity A中是可能存在临时数据和状 态的。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动 NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键 回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。

如果我们的应用出现了这种情况,是会比较影响用户体验的,所以得想想办法解决这个问题。 其实,Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在 Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题

onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法 用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数 据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取 值,第二个参数是真正要保存的内容

在MainActivity中添加如下代码就可以将临时数据进行保存了

override fun onSaveInstanceState(outSide:Bundle){
super.onSaveInstanceState(outState)
val tempData = "Something you just typed"
outState.putString("data_key",tempData)
}

数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直 使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是 null,但是如果在Activity被系统回收之前,你通过onSaveInstanceState()方法保存数 据,这个参数就会带有之前保存的全部数据,我们只需要再通过相应的取值方法将数据取出即 可。

修改MainActivity的onCreate()方法,如下所示:

override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
Log.d(tag,"onCreate")
setContentView(R.layout.activity_main)
if(savedInstanceState!=null){
val tempData = savadInstanceState.getString("data_key")
Log.d(tag,"tempData is $tempData")
............
}
}

Activity的启动模式

Activity的启动模式对你来说应该是个全新的概念,在实际项目中我们应该根据特定的需求为每 个Activity指定恰当的启动模式。启动模式一共有4种,分别是standard、singleTop、 singleTask和singleInstance,可以在AndroidManifest.xml中通过给标签指定 android:launchMode属性来选择启动模式。下面我们来逐个进行学习。

standard

tandard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用 这种启动模式。到目前为止,我们写过的所有Activity都是使用的standard模式。经过上一节 的学习,你已经知道了Android是使用返回栈来管理Activity的,在standard模式下,每当启 动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的 Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该 Activity的新实例。

singleTop

可能在有些情况下,你会觉得standard模式不太合理。Activity明明已经在栈顶了,为什么再 次启动的时候还要创建一个新的Activity实例呢?别着急,这只是系统默认的一种启动模式而 已,你完全可以根据自己的需要进行修改,比如使用singleTop模式。当Activity的启动模式指 定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接 使用它,不会再创建新的Activity实例。

我们还是通过实践来体会一下,修改AndroidManifest.xml中FirstActivity的启动模式,如下 所示

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

singleTask

使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,但是正如你在上一节所看到 的,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的。那么有没有 什么办法可以让某个Activity在整个应用程序的上下文中只存在一个实例呢?这就要借助 singleTask模式来实现了。当Activity的启动模式指定为singleTask,每次启动该Activity时, 系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例, 并把在这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新的 Activity实例

singleInstance

singleInstance模式应该算是4种启动模式中最特殊也最复杂的一个了,你也需要多花点工夫来 理解这个模式。不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新 的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动 一个新的返回栈)

Activity的最佳实践

知晓当前是在哪一个Activity

我们还是在ActivityTest项目的基础上修改,首先需要新建一个BaseActivity类。右击 com.example.activitytest包→New→Kotlin File/Class,在弹出的窗口中输入 BaseActivity,创建类型选择Class

注意,这里的BaseActivity和普通Activity的创建方式并不一样,因为我们不需要让 BaseActivity在AndroidManifest.xml中注册,所以选择创建一个普通的Kotlin类就可以 了。然后让BaseActivity继承自AppCompatActivity,并重写onCreate()方法,如下所 示

open class BaseActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
Log.d("BaseActivity",javaClass.simpleName)
}
}

我们在onCreate()方法中加了一行日志,用于打印当前实例的类名。这里我要额外说明一 下,Kotlin中的javaClass表示获取当前实例的Class对象,相当于在Java中调用 getClass()方法;而Kotlin中的BaseActivity::class.java表示获取BaseActivity类的 Class对象,相当于在Java中调用BaseActivity.class。在上述代码中,我们先是获取了当 前实例的Class对象,然后再调用simpleName获取当前实例的类名。

接下来我们需要让BaseActivity成为ActivityTest项目中所有Activity的父类,为了使 BaseActivity可以被继承,我已经提前在类名的前面加上了open关键字。然后修改 FirstActivity、SecondActivity和ThirdActivity的继承结构,让它们不再继承自 AppCompatActivity,而是继承自BaseActivity。而由于BaseActivity又是继承自 AppCompatActivity的,所以项目中所有Activity的现有功能并不受影响,它们仍然继承了 Activity中的所有特性

现在每当我们进入一个Activity的界面,该Activity的类名就会被打印出来,这样我们就可以时 刻知晓当前界面对应的是哪一个Activity了。

随时随地退出程序

如果目前你手机的界面还停留在ThirdActivity,你会发现当前想退出程序是非常不方便的,需 要连按3次Back键才行。按Home键只是把程序挂起,并没有退出程序。如果我们的程序需要 注销或者退出的功能该怎么办呢?看来要有一个随时随地都能退出程序的方案才行。其实解决思路也很简单,只需要用一个专门的集合对所有的Activity进行管理就可以了。下面我 们就来实现一下。

object ActivityCollector{
private val activities = ArrayList<Activity>()

fun addActivity(activity:Activity){
activities.add(activity)
}

fun removeActivity(activity:Activity){
activities.remove(activity)
}

fun finishAll(){
for(activity in activities){
if(!activity.isFinishing){
activity.finish()
}
}
activities.clear()
}
}

接下来修改BaseActivity中的代码,如下所示

open class BaseActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
Log.d("BaseActivity",javaClass.simpleName)
ActivityCollector.addActivity(this)
}

override fun onDestroy(){
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}

在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方 法,表明将当前正在创建的Activity添加到集合里。然后在BaseActivity中重写 onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明从集 合里移除一个马上要销毁的Activity

从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll() 方法就可以了。例如在ThirdActivity界面想通过点击按钮直接退出程序,只需将代码改成如下 形式

class ThirdActivity:BaseActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
Log.d("ThirdActivity","Task id is $taskId")
setContentView(R.layout.third_layout)
val button3:Button = findViewById(R.id.Button3)
button3.setOnClickListener{
ActivityColletor.finishAll()
}
}
}

当然你还可以在销毁所有Activity的代码后面再加上杀掉当前进程的代码,以保证程序完全退 出,杀掉进程的代码如下所示

android.os.Process.killProcess(android.os.Process.myPid())

killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方 法来获得当前程序的进程id。需要注意的是,killProcess()方法只能用于杀掉当前程序的 进程,不能用于杀掉其他程序

启动Activity的最佳方法

假设SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必 须传递过来,那么我们很容易会写出如下代码:

val intent = Intent(this,SecondActivity::class.java)
intent.putExtra("param1","data1")
intent.putExtra("param2","data2")

虽然这样写是完全正确的,但是在真正的项目开发中经常会出现对接的问题。比如 SecondActivity并不是由你开发的,但现在你负责开发的部分需要启动SecondActivity,而你 却不清楚启动SecondActivity需要传递哪些数据。这时无非就有两个办法:一个是你自己去阅 读SecondActivity中的代码,另一个是询问负责编写SecondActivity的同事。你会不会觉得很 麻烦呢?其实只需要换一种写法,就可以轻松解决上面的窘境。 修改SecondActivity中的代码,如下所示

class SecondActivity:BaseActivity(){
compain object{
fun actionStart(context:Context,data1:String,data2:String){
val intent = Intent(context,SecondActivity::class.java)
intent.putExtra("param1",data1)
intent.putExtra("param2",data2)
context.startActivity(intent)
}
}
}

在这里我们使用了一个新的语法结构companion object,并在companion object中定义 了一个actionStart()方法。之所以要这样写,是因为Kotlin规定,所有定义在companion object中的方法都可以使用类似于Java静态方法的形式调用。关于companion object的更 多内容,我会在本章的Kotlin课堂中进行讲解

接下来我们重点看actionStart()方法,在这个方法中完成了Intent的构建,另外所有 SecondActivity中需要的数据都是通过actionStart()方法的参数传递过来的,然后把它们 存储到Intent中,最后调用startActivity()方法启动SecondActivity。

这样写的好处在哪里呢?最重要的一点就是一目了然,SecondActivity所需要的数据在方法参 数中全部体现出来了,这样即使不用阅读SecondActivity中的代码,不去询问负责编写 SecondActivity的同事,你也可以非常清晰地知道启动SecondActivity需要传递哪些数据。另 外,这样写还简化了启动Activity的代码,现在只需要一行代码就可以启动SecondActivity, 如下所示

button1.setOnClickListener{
SecondActivity.actionStart(this,"data1","data2")
}

Kotlin课堂:标准函数和静态方法

标准函数with、run和apply

Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用所 有的标准函数

下面我们从with函数开始学起。with函数接收两个参数:第一个参数可以是一个任意类型的对 象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象 的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。示例代码如下:

val result = with(obj) { 
//
这里是obj的上下文
"value" // with函数的返回值
}

那么这个函数有什么作用呢?它可以在连续调用同一个对象的多个方法时让代码变得更加精 简,下面我们来看一个具体的例子。

比如有一个水果列表,现在我们想吃完所有水果,并将结果打印出来,就可以这样写

val list = listOf("Apple","Banana","Orange","Pear","Grape")
val builder = StringBuilder()
builder.append("Start eating fruits.\n")
for(fruit in list){
builder.append(fruit).append("\n")
}
builder.append("Ate all fruits.")
val result = builder.toString()
println(result)

仔细观察上述代码,你会发现我们连续调用了很多次builder对象的方法。其实这个时候就可 以考虑使用with函数来让代码变得更加精简,如下所示:

val list = listOf("Apple","Banana","Orange","Pear","Grape")
val result = with(StringBuilder()){
append("Start eating fruits.\n")
for(fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits")
toString
}
println(result)

这段代码乍一看可能有点迷惑性,其实很好理解。首先我们给with函数的第一个参数传入了一 个StringBuilder对象,那么接下来整个Lambda表达式的上下文就会是这个 StringBuilder对象。于是我们在Lambda表达式中就不用再像刚才那样调用 builder.append()和builder.toString()方法了,而是可以直接调用append()和 toString()方法。Lambda表达式的最后一行代码会作为with函数的返回值返回,最终我们 将结果打印出来。 这两段代码的执行结果是一模一样的,但是明显第二段代码的写法更加简洁一些,这就是with 函数的作用

下面我们再来学习另外一个常用的标准函数:run函数。run函数的用法和使用场景其实和 with函数是非常类似的,只是稍微做了一些语法改动而已。首先run函数通常不会直接调用, 而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表 达式中提供调用对象的上下文。其他方面和with函数是一样的,包括也会使用Lambda表达式 中的最后一行代码作为返回值返回。示例代码如下

val result = obj.run { 
//
这里是obj的上下文
"value" // run函数的返回值
}

那么现在我们就可以使用run函数来修改一下吃水果的这段代码,如下所示

val list = listOf("Apple","Banana","Orange","Pear","Grape")
val result = StringBuilder().run{
append("Start eating fruits.\n")
for(fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits")
toSring()
}
println(result)

最后我们再来学习标准函数中的apply函数。apply函数和run函数也是极其类似的,都要在某 个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下 文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。示例代码如下:

val result = obj.apply { 
// 这里是obj的上下文
}
// result == obj

那么现在我们再使用apply函数来修改一下吃水果的这段代码,如下所示

val list = listOf("Apple","Banana","Orange","Pear","Grape")
val result = StringBuilder().apply{
append("Start eating fruits.\n")
for(fruit in list){
append(fruit).append("\n")
}
}

注意这里的代码变化,由于apply函数无法指定返回值,只能返回调用对象本身,因此这里的 result实际上是一个StringBuilder对象,所以我们在最后打印的时候还要再调用它的 toString()方法才行。这段代码的执行结果和前面两段仍然是完全相同的,我就不再重复演 示了

回想一下刚刚在最佳实践环节编写的启动Activity的代码

val intent = Intent(context,SecnodActivity::class.java)
intent.putExtra("param1","data1")
intent.putExtra("param2","data2")
context.startActivity(intent)

这里每传递一个参数就要调用一次intent.putExtra()方法,如果要传递10个参数,那就得 调用10次。对于这种情况,我们就可以使用标准函数来对代码进行精简,如下所示:

val intent = Intent(context,SecondActivity::class.java).apply{
putExtra("param1","data1")
putExtra("param2","data2")
}
context.startActivity(intent)

可以看到,由于Lambda表达式中的上下文就是Intent对象,所以我们不再需要调用 intent.putExtra()方法,而是直接调用putExtra()方法就可以了。传递的参数越多,这 种写法的优势也就越明显

定义静态方法

像工具类这种功能,在Kotlin中就非常推荐使用单例类的方式来实现,比如上述的Util工具 类,如果使用Kotlin来实现的话就可以这样写

object Util{
fun doAction(){
println("do action")
}
}

虽然这里的doAction()方法并不是静态方法,但是我们仍然可以使用Util.doAction()的 方式来调用,这就是单例类所带来的便利性。

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如 果我们只是希望让类中的某一个方法变成静态方法的调用方式该怎么办呢?这个时候就可以使 用刚刚在最佳实践环节用到的companion object了,示例如下

class Util{
fun doAction1(){
println("do action1")
}
companion object{
fun doAction2(){
println("do action2")
}
}
}

然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层 方法。下面我们来逐个学习一下。

如果我们给单例类或companion object中的方 法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法,如下所 示

class Util{
fun doAction1(){
println("do action1")
}
companion object{
@JvmStatic
fun doAction2(){
println("do action2")
}
}
}

注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在 一个普通方法上,会直接提示语法错误。

再来看顶层方法,顶层方法指的是那些没有定义在任何类中的方法,比如我们在上一节中编写 的main()方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一 个顶层方法,那么它就一定是静态方法

想要定义一个顶层方法,首先需要创建一个Kotlin文件。对着任意包名右击 → New → Kotlin File/Class,在弹出的对话框中输入文件名即可。注意创建类型要选择File,点击“OK”完成创建,这样刚刚的包名路径下就会出现一个Helper.kt文件。现在我们在这个文件 中定义的任何方法都会是顶层方法