一、显式Intent和隐式Intent
参考
Android理解:显式和隐式Intent
Android中Intent概述及使用
显式的Intent:如果Intent中明确包含了要启动的组件的完整类名(包名及类名),那么这个Intent就是explict的,即显式的。使用显式Intent最典型的情形是在你自己的App中启动一个组件,因为你自己肯定知道自己的要启动的组件的类名。比如,为了响应用户操作通过显式的Intent在你的App中启动一个Activity或启动一个Service下载文件。
隐式的Intent:如果Intent没有包含要启动的组件的完整类名,那么这个Intent就是implict的,即隐式的。虽然隐式的Intent没有指定要启动的组件的类名,但是一般情况下,隐式的Intent都要指定需要执行的action。一般,隐式的Intent只用在当我们想在自己的App中通过Intent启动另一个App的组件的时候,让另一个App的组件接收并处理该Intent。例如,你想在地图上给用户显示一个位置,但是你的App又不支持地图展示,这时候你可以将位置信息放入到一个Intent中,然后给它指定相应的action,通过这样隐式的Intent请求其他的地图型的App(例如Google Map、百度地图等)来在地图中展示一个指定的位置。隐式的Intent也体现了Android的一种设计哲学:我自己的App无需包罗万象所有功能,可以通过与其他App组合起来,给用户提供很好的用户体验。而连接自己的App与其他App的纽带就是隐式Intent。
- 显式Intent
第一个参数是活动上下文,第二个是目标活动的class
FirstActivity:
button.setOnClickListener(new OnClickListener(){
public void onClick(View v){
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});
- 隐式Intent
由系统分析Intent,并找出合适的activity去启动。
FirstActivity
button.setOnClickListener(new OnClickListener(){
public void onClick(View v){
Intent intent = new Intent("com.example.activitytest.ACTION_START");
//setAction方式是一样的
//Intent intent = new Intent();
//intent.setAction("com.example.activitytest.ACTION_START");
intent.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(intent);
}
});
SecondActivity
<activity android:name="SecondActivity">
<intent-filter>
<!-- 意图过滤器, 只要满足了action(动作), 和 category(分类),那么就启动这个界面 -->
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="com.example.activitytest.MY_CATEGORY"/>
<category android:name="android.intent.category.DEFAULT"/>
//default category一定要有,这是默认的
</intent-filter>
通过设置Action字符串,表明自己的意图,即我想干嘛,需要由系统解析,找到能够处理这个Intent的Activity并启动。比如我想打电话,则可以设置Action为"android.intent.action.DIAL"字符串,表示打电话的意图,系统会找到能处理这个意图的Activity,例如调出拨号面板。
有几点需要注意:
1、这个Activity其他应用程序也可以调用,只要使用这个Action字符串。这样应用程序之间交互就很容易了,例如手机QQ可以调用QQ空间,可以调用腾讯微博等。因为如此,为了防止应用程序之间互相影响,一般命名方式是包名+Action名,例如命名"abcdefg"就很不合理了,就应该改成"com.example.app016.MyTest"。
2、当然,你可以在自己的程序中调用其他程序的Action。
例如可以在自己的应用程序中调用拨号面板:
Intent intent = new Intent(Intent.ACTION_DIAL);
// 或者Intent intent = new Intent("android.intent.action.DIAL");
// Intent.ACTION_DIAL是内置常量,值为"android.intent.action.DIAL"
startActivity(intent);
3、一个Activity可以处理多种Action
只要你的应用程序够牛逼,一个Activity可以看网页,打电话,发短信,发邮件。。。当然可以。
Intent的Action只要是其中之一,就可以打开这个Activity。
<activity
android:name="com.example.app016.SecondActivity">
<intent-filter>
<!-- 可以处理下面三种Intent -->
<action android:name="com.example.app016.SEND_EMAIL"/>
<action android:name="com.example.app016.SEND_MESSAGE"/>
<action android:name="com.example.app016.DAIL"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
对于一个Action字符串,系统有可能会找到一个Activity能处理这个Action,也有可能找到多个Activity,也可能一个都找不到。
1、找到一个Activity
很简单,直接打开这个Activity。这个不需要解释。
2、找到多个Acyivity
系统会提示从多个activity中选择一个打开。
例如我们自己开发一个拨号面板应用程序,可以设置activity的<intent-filter>中Action name为"android.intent.action.DIAL",这样别的程序调用拨号器时,用户可以从Android自带的拨号器和我们自己开发的拨号器中选择。
<activity
android:name="com.example.app016.SecondActivity">
<intent-filter>
<action android:name="android.intent.action.DIAL"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
这也就是当Android手机装上UC浏览器后,打开网页时会弹出选择Android自带浏览器还是UC浏览器,可能都会遇到过。
3、一个Activity都没找到
一个都没找到的话,程序就会出错,会抛出ActivityNotFoundException。比如随便写一个action字符串:
Intent intent = new Intent("asasasas");
startActivity(intent);
所以应该注意try catch异常。
Intent intent = new Intent("asasasas");
try
{
startActivity(intent);
}
catch(ActivityNotFoundException e)
{
Toast.makeText(this, "找不到对应的Activity", Toast.LENGTH_SHORT).show();
}
或者也可以使用Intent的resolveActivity方法判断这个Intent是否能找到合适的Activity,如果没有,则不再startActivity,或者可以直接禁用用户操作的控件。
Intent intent = new Intent(Intent.ACTION_DIAL);
if(intent.resolveActivity(getPackageManager()) == null)
{
// 设置控件不可用
}
注意resolveActivity方法返回值就是显式Intent上面讲到的ComponentName对象,一般情况下也就是系统找到的那个Activity。但是如果有多个Activity可供选择的话,则返回的Component是com.android.internal.app.ResolverActivity,也就是用户选择Activity的那个界面对应的Activity,这里不再深究。
Intent intent = new Intent(Intent.ACTION_DIAL);
ComponentName componentName = intent.resolveActivity(getPackageManager());
if(componentName != null)
{
String className = componentName.getClassName();
Toast.makeText(this, className, Toast.LENGTH_SHORT).show();
}
当创建了一个显式Intent去启动Activity或Service的时候,系统会立即启动Intent中所指定的组件。
当创建了一个隐式Intent去使用的时候,Android系统会将该隐式Intent所包含的信息与设备上其他所有App中manifest文件中注册的组件的Intent Filters进行对比过滤,从中找出满足能够接收处理该隐式Intent的App和对应的组件。如果有多个App中的某个组件都符合条件,那么Android会弹出一个对话框让用户选择需要启动哪个App。
Intent Filter,即Intent过滤器,一个组件可以包含0个或多个Intent Filter。Intent Filter是写在App的manifest文件中的,其通过设置action或uri数据类型等指明了组件能够处理接收的Intent的类型。如果你给你的Activity设置了Intent Filter,那么这就使得其他的App有可能通过隐式Intent启动你的这个Activity。反之,如果你的Activity不包含任何Intent Filter,那么该Activity只能通过显式Intent启动,由于我们一般不会暴露出我们组件的完整类名,所以这种情况下,其他的App基本就不可能通过Intent启动我们的Activity了(因为他们不知道该Activity的完整类名),只能由我们自己的App通过显式Intent启动。
需要注意的是,为了确保App的安全性,我们应该总是使用显式Intent去启动Service并且不要为该Service设置任何的Intent Filter。通过隐式的Intent启动Service是有风险的,因为你不确定最终哪个App中的哪个Service会启动起来以响应你的隐式Intent,更悲催的是,由于Service没有UI的在后台运行,所以用户也不知道哪个Service运行了。
二、传递数据
- 向下一个活动传递数据
FirstActivity:
button.setOnClickListener(new OnClickListener(){
public void onClick(View v){
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
***intent.putExtra("extra_data","Hello");***
startActivity(intent);
}
});
SecondActivity
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
//getIntExtra getBooleanExtra 取值方式与传递的数据要一致
}
- 返回数据给上一个活动
startActivityForResult方法也是用于启动活动的,但这个方法期望在活动销毁时能够返回结果给上一个活动。
FirstActivity:
button.setOnClickListener(new OnClickListener(){
public void onClick(View v){
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivityForResult(intent,1);//请求码
}
});
//使用startActivityForResult启动活动,会在销毁后回调onActivityResult方法
protected void onActivityResult(int requestCode,int resultCode,Intent data){
switch(requestCode){
case 1:
if(resultCode == RESULT_OK){
String returnData = data.getStringExtra("data_return");
....
}
}
}
SecondActivity
Intent intent = new Intent();
***intent.putExtra("data_return","Hello");***
setResult(RESULT_OK,intent);//专门用于向上一个活动返回数据
}
利用Intent的Extra部分来存储我们想要传递的数据,可以传送int, long, char等一些基础类型,对复杂的对象就无能为力了。
传复杂对象参考
Android Bundle总结
Android五种数据传递方法汇总
Intent传递对象——Serializable和Parcelable区别
Android系统中Parcelable和Serializable的区别
1.使用Serializable很简单,指定Person类implements Serializable即可。注意,要指定serialVersionUID,否则反序列化会出问题。
可参考如何让Android Studio 自动生成 serialVersionUID
Person person = new Person();
person.setName("tom");
person.setAge(20);
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("person_data",person);
startActivity(intent);
SecondActivity:
Person person = (Person)getIntent().getSerializableExtra("person_data");
2.Parcelable稍微复杂一些,要实现几个方法。
public class Person implements Parcelable{
private String name;
private int age;
public int describeContents(){
return 0;
}
public void writeToParcel(Parcel dest,int flags){
dest.writeString(name);
dest.writeInt(age);
}
public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>(){
public Person createFromParcel(Parcel source){
Person person = new Person();
person.name = source.readString();
person.age = source.readInt();
return person;
}
public Person[] newArray(int size){
return new Person[size];
}
};
SecondActivity:
Person person = (Person)getIntent().getPacelableExtra("person_data");
三、Dart&Henson
参考
【译】使用 Dart & Henson 改进 Android Intents
用Dart&Henson玩转Activity跳转
上面传递数据的方式很好地处理了组件的创建与通信,但仍然有一些问题需要注意:
- 目标组件作为一个实体,对输入没有任何控制权。在我们的例子里,itemId 是必需的,但如果没有传递它,DetailActivity 最好的处理方式也只能是抛出异常。
- Intent 的创建(完全)不够健壮,并没有对 extra 中的 key 或 value 做任何检查。
解决这些问题的一个可能的方案是 Intent 工厂模式。它主要由一些工厂方法组成,包含了应用程序里用到的各种 Intent。比如像下面这样的 Intent 工厂:
public class IntentFactory {
public Intent newDetailActivityIntent(Context context, String itemId, boolean showMap) {
Intent intent = new Intent(context, DetailActivity.class);
intent.putExtra(EXTRA_ITEM_ID, itemId);
intent.putExtra(EXTRA_SHOW_MAP, showMap);
return intent;
}
...
}
然而,这种解决方案有一些局限,并不是一个很好的办法。
- Intent 工厂是一个集中类,这个类可能会变得很大且复杂。
- 它违背了开放/闭合原则。对修改并没有关闭,我们将总是需要给每个新的 Activity 添加一个新方法。
- 目标组件应该是唯一知晓参数细节与逻辑的地方。
-
可选参数处理。同一个组件有不同的需求,是否应该写不同的方法?还是写一个方法并使用默认值?
它会诱使后续的开发人员模仿,进而产生其他的 Intent 工厂,最终演变成大泥球模式,使得代码越来越糟。
有一个类似的策略可以分散这些工厂方法到各自的目标组件。也就是指,每个组件可以包含一个(或多个)静态方法,用于生成这些启动它自身的 Intent。这个办法可以解决开放/闭合原则的问题,分解 Intent 工厂,也许还可以避免大泥球模式。尽管如此,关于可选参数的问题依然存在。有人说 builder 模式可以?我们自己实现它?...
Dart 是一个 Android 开源库(Henson实际是Dart项目的子项目)。它绑定 Activity 字段到 Intent extra,Butter Knife 也是用类似的方案,关联 Activity 与 XML 布局中的View。
这里我们假设要从MainActivity跳转到DetailActivity,DetailActivity中要接受三个参数分别是String name,int age和User user。DetailActivity需要接受上述三个参数,仅仅通过@InjectExtra注解即可,然后在onCreate
中执行Dart.inject(this),详细的代码为:
public class DetailActivity extends AppCompatActivity {
@InjectExtra
String name = "default name";
@InjectExtra
int age = 0;
@Nullable
@InjectExtra
User user;
@BindView(R.id.tvName)
TextView tvName;
@BindView(R.id.tvAge)
TextView tvAge;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ButterKnife.bind(this);
Dart.inject(this);
initView();
}
private void initView() {
// 使用name,age,user
}
}
String name = "default name";这句话给了name一个默认值,但是当Dart.inject执行后会被传递过来的数据覆盖。
@Nullable加在了user上说明这个数据可以不用传递。接受端就这么多代码,下面让我们看看发送端如果发送数据,如何跳转到DetailActivity。
注意编写上述代码后,我们要先编译下项目,编译好后Henson会通过注解处理器生成可以跳转到DetailActivity的DSL(领域特定语言)段,方便其他组件对DetailActivity的跳转,我们在MainActivity上只要写上下列代码,即可完成界面跳转和数据传递:
User user = new User();
user.setAge(Integer.parseInt(age.getText().toString()));
user.setName(name.getText().toString());
startActivity(
Henson.with(this)
.gotoDetailActivity()
.age(27)
.name("jason")
.user(user)
.build()
);
当你写完Henson.with(this)后代码提示会自动弹出.gotoDetailActivity(),Henson帮助你提示你DetailActivity是可以被跳转的;随后你继续写下.gotoDetailActivity()后,又自动弹出.age()方法,提示你传入一个int类型给age,写好后,又自动弹出.name()方法,以此类推,最后以一个build()收场。
注意,这里你可以不写.user(),因为在DetailActivity中我们指定它是nullable的,可以不传。但是.age()和.name()都是会强制弹出让你填写数据的。如果我偏向要对调name和age的传入顺序呢?发现不行~如果你写完.gotoDetailActivity()后发现只有.age()的提示,却没有.name()的,即强制要求你先传递age。这里作者说明后才知道他们是按照字幕顺序来排数据传入的,目前没有更好的方法。