一、AndroidManifest.xml
AndroidManifest.xml:配置清单文件,用来描述项目。像四大组件的注册、一些权限都需要在这份清单文件中申明。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wj.activitydemo">
<uses-permission android:name="android.permission.CALL_PHONE" />
<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/Theme.MyActivityDemo">
<activity
android:name=".PayResultActivity"
android:exported="true" />
<activity
android:name=".PayThePhoneBillActivity"
android:exported="true" />
<activity
android:name=".MessageActivity"
android:exported="true" />
<activity
android:name=".CallActivity"
android:exported="true" />
<activity
android:name=".ReceiveActivity"
android:exported="true" />
<activity
android:name=".TransmitActivity"
android:exported="true" />
<activity
android:name=".BrowserActivity"
android:exported="true" />
<activity
android:name=".LoginInfo2Activity"
android:exported="true">
<intent-filter>
<action android:name="com.wj.activitydemo.LOGIN_INFO" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".LoginInfoActivity"
android:exported="true" />
<activity
android:name=".LoginActivity"
android:exported="true" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
二、Activity之间跳转
1、什么是activity
官方解释为:ativity是单一独立的,它用于处理用户操作。几乎所有的Activity都与用户交互,所以Activity负责创建窗口来放你所设置的UI内容。
综上,我们可以理解Activity是一个容器,是一个窗口。
An activity is a single, focused thing that the user can do. Almost all activities interact with the user, so the Activity class takes care of creating a window for you in which you can place your UI with setContentView(View).
2、通过显示意图来实现界面跳转
目标:实现界面的跳转并把数据传到另一个界面
登陆界面代码:
public class LoginActivity extends AppCompatActivity implements View.OnClickListener {
private EditText et_account;
private EditText et_password;
private Button bt_login;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
//找到控件
initView();
//设置监听
initListener();
}
private void initView() {
et_account = findViewById(R.id.et_account);
et_password = findViewById(R.id.et_password);
bt_login = findViewById(R.id.bt_login);
}
private void initListener() {
bt_login.setOnClickListener(this);
}
@Override
public void onClick(View view) {
//处理登录
handleLogin();
}
private void handleLogin() {
String account = et_account.getText().toString().trim();
if (TextUtils.isEmpty(account)) {
//友好提示
Toast.makeText(this, "账号为空,请输入!", Toast.LENGTH_SHORT).show();
return;
}
String password = et_password.getText().toString().trim();
if (TextUtils.isEmpty(password)) {
//友好提示
Toast.makeText(this, "密码为空,请输入!", Toast.LENGTH_SHORT).show();
return;
}
//有账号密码后把信息传给另一个界面
//显示意图跳转
Intent intent = new Intent(this, com.wj.myactivitydemo.LoginInfoActivity.class);
intent.putExtra("账号", account);
intent.putExtra("密码", password);
//通过startActivity方法来跳转
startActivity(intent);
//结束当前页面
finish();
}
}
登录界面示意图:
接收信息界面代码:
public class LoginInfoActivity extends AppCompatActivity {
private TextView tv_info;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login_info);
tv_info = findViewById(R.id.tv_info);
Intent intent = getIntent();
String account = intent.getStringExtra("账号");
String password = intent.getStringExtra("密码");
tv_info.setText("您的账号:" + account + " 密码:" + password);
}
}
接收信息界面示意图:
运行效果图1:
运行效果图2:
运行效果图3:
3、通过隐式意图来实现界面跳转
登录界面以及接受信息界面同2,仅意图跳转不同
首先在AndroidManifest.xml中添加配置:
<activity
android:name=".LoginInfo2Activity"
android:exported="true">
<intent-filter>
<action android:name="com.wj.myactivitydemo.LOGIN_INFO" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
然后通过隐式意图来实现界面跳转以及接收信息,效果同2。
Intent intent = new Intent();
intent.setAction("com.wj.myactivitydemo.LOGIN_INFO");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("账号", account);
intent.putExtra("密码", password);
startActivity(intent);
4、通过显示、隐式意图来跳转到第三方应用
通过显示意图来跳转到浏览器
相关代码:
public class BrowserActivity extends AppCompatActivity implements View.OnClickListener {
private Button bt_skip1;
private Button bt_skip2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browser);
bt_skip1 = findViewById(R.id.bt_skip1);
bt_skip2 = findViewById(R.id.bt_skip2);
bt_skip1.setOnClickListener(this);
bt_skip2.setOnClickListener(this);
}
@Override
public void onClick(View v) {
//显式意图跳转浏览器
if (v == bt_skip1) {
Intent intent = new Intent();
intent.setPackage("com.android.browser");
//第一种写法
// intent.setClassName("com.android.browser", "com.android.browser.BrowserActivity");
//第二种写法
ComponentName componentName = new ComponentName("com.android.browser", "com.android.browser.BrowserActivity");
intent.setComponent(componentName);
startActivity(intent);
//隐式意图跳转浏览器
} else if (v == bt_skip2) {
Intent intent = new Intent();
//设置action、category、包名
intent.setAction("android.intent.action.SEARCH");
intent.addCategory("android.intent.category.DEFAULT");
intent.setPackage("com.android.browser");
startActivity(intent);
}
}
}
相关布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".BrowserActivity">
<Button
android:id="@+id/bt_skip1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="显式意图跳转到浏览器" />
<Button
android:id="@+id/bt_skip2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="隐式意图跳转到浏览器" />
</LinearLayout>
示意图:
三、获取界面的返回值
1、实现基本数据类型传递
传递界面代码:
public class TransmitActivity extends AppCompatActivity implements View.OnClickListener {
private Button bt_receive1;
private Button bt_receive2;
//1、实现基本数据类型传递
//2、实现对象传递
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transmit);
initView();
initListener();
}
private void initView() {
bt_receive1 = findViewById(R.id.bt_receive1);
bt_receive2 = findViewById(R.id.bt_receive2);
}
private void initListener() {
bt_receive1.setOnClickListener(this);
bt_receive2.setOnClickListener(this);
}
@Override
public void onClick(View v) {
//实现基本数据类型传递
if (v == bt_receive1) {
Intent intent = new Intent(this, ReceiveActivity.class);
intent.putExtra("intKey", 100);
intent.putExtra("booleanKey", true);
startActivity(intent);
//实现对象传递
} else if (v == bt_receive2) {
Intent intent = new Intent(this, ReceiveActivity.class);
User user = new User();
user.setAge(22);
user.setName("james");
user.setTall(174.5f);
intent.putExtra("userKey", user);
startActivity(intent);
}
}
}
传递界面布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".TransmitActivity">
<Button
android:id="@+id/bt_receive1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="实现基本数据类型传递" />
<Button
android:id="@+id/bt_receive2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="实现对象传递" />
</LinearLayout>
传递界面示意图:
接收界面代码:
public class ReceiveActivity extends AppCompatActivity {
private String TAG = "ReceiveActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_receive);
Intent intent = getIntent();
//实现基本数据类型传递
if (intent != null) {
int intValue = intent.getIntExtra("intKey", -1);
Log.d(TAG, "int value :" + intValue);
boolean booleanValue = intent.getBooleanExtra("booleanKey", false);
Log.d(TAG, "boolean value :" + booleanValue);
//实现对象传递
com.wj.myactivitydemo.User user = intent.getParcelableExtra("userKey");
if (user != null) {
Log.d(TAG, "user name:" + user.getName());
Log.d(TAG, "user age:" + user.getAge());
Log.d(TAG, "user tall:" + user.getTall());
}
}
}
}
实现基本数据类型传递时查看log:
2、实现对象传递
首先定义一个User类实现Parcelable接口
//实现Parcelable接口
public class User implements Parcelable {
private int age;
private String name;
private float tall;
public User() {
}
protected User(Parcel in) {
age = in.readInt();
name = in.readString();
tall = in.readFloat();
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getTall() {
return tall;
}
public void setTall(float tall) {
this.tall = tall;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(age);
dest.writeString(name);
dest.writeFloat(tall);
}
}
实现对象传递时查看log:
3、实现拨打电话以及发短信功能
拨打电话主要代码:
Intent intent = new Intent();
//通过隐式意图传递数据 实现跳转到拨打号码界面
//调用拨号面板
intent.setAction("android.intent.action.DIAL");
intent.addCategory("android.intent.category.DEFAULT");
//设置要拨打的号码
Uri uri = Uri.parse("tel:10086");
intent.setData(uri);
startActivity(intent);
发短信主要代码:
Intent intent = new Intent();
//调用发送短信息
intent.setAction(Intent.ACTION_SENDTO);
//设置要发送的号码
Uri uri = Uri.parse("smsto:13814093704");
intent.setData(uri);
//设置要发送的信息内容
intent.putExtra("sms_body", "Welcome to Android!");
startActivity(intent);
4、Activity之间数据回传
案例:模拟话费充值界面:我们从第一个界面,点击充值跳转到第二个界面,充值完成以后,回到第一个界面提示。
充值界面代码:
public class PayResultActivity extends AppCompatActivity implements View.OnClickListener {
private TextView et_pay;
private TextView bt_pay;
private TextView bt_cancel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pay_result);
initView();
initListener();
}
private void initListener() {
et_pay.setOnClickListener(this);
bt_pay.setOnClickListener(this);
bt_cancel.setOnClickListener(this);
}
private void initView() {
et_pay = findViewById(R.id.et_pay);
bt_pay = findViewById(R.id.bt_pay);
bt_cancel = findViewById(R.id.bt_cancel);
}
@Override
public void onClick(View view) {
if (view == bt_pay) {
handlePay();
} else if (view == bt_cancel) {
handleCancelPay();
}
}
private void handlePay() {
String payNumber = et_pay.getText().toString().trim();
if (TextUtils.isEmpty(payNumber)) {
Toast.makeText(this, "请输入充值金额!", Toast.LENGTH_SHORT).show();
return;
}
//进行充值
Intent intent = new Intent();
intent.putExtra("resultContent", "充值成功!");
setResult(2, intent);
finish();
}
private void handleCancelPay() {
//取消充值
Intent intent = new Intent();
intent.putExtra("resultContent", "取消充值!");
setResult(3, intent);
finish();
}
}
充值界面布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".PayResultActivity">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="这是充值界面"
android:textColor="@color/black"
android:textSize="30sp" />
<EditText
android:id="@+id/et_pay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:hint="请输入充值金额"
android:inputType="number"
android:maxLines="1"
android:textSize="20sp" />
<Button
android:id="@+id/bt_pay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="充值"
android:textSize="20sp" />
<Button
android:id="@+id/bt_cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="取消充值"
android:textSize="20sp" />
</LinearLayout>
回调界面代码:
public class PayThePhoneBillActivity extends AppCompatActivity implements View.OnClickListener {
private Button bt_pay_the_phone_bill;
private TextView tv_result;
private final int PAY_REQUEST_CODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pay_the_phone_bill);
initView();
initListener();
}
private void initView() {
bt_pay_the_phone_bill = findViewById(R.id.bt_pay_the_phone_bill);
tv_result = findViewById(R.id.tv_result);
}
private void initListener() {
bt_pay_the_phone_bill.setOnClickListener(this);
tv_result.setOnClickListener(this);
}
@Override
public void onClick(View view) {
//跳转到充值界面
Intent intent = new Intent(this, PayResultActivity.class);
//startActivity(intent);
startActivityForResult(intent, PAY_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PAY_REQUEST_CODE) {
String resultContent = null;
if (resultCode == 2) {
//充值成功
resultContent = data.getStringExtra("resultContent");
} else if (resultCode == 3) {
//充值失败
resultContent = data.getStringExtra("resultContent");
}
tv_result.setText(resultContent);
}
}
}
充值界面示意图:
充值成功返回图:
取消充值示意图:
四、Activity的生命周期
定义两个activity,重写所有的生命周期,在activity1设置一个按钮可以跳转到activity2,通过打印log查看不同场景下生命周期的变化。
1、activity1----->Home----->activity1
2022-05-13 10:33:00.709 31324-31324/com.wj.demo D/TAG: onCreate1
2022-05-13 10:33:00.845 31324-31324/com.wj.demo D/TAG: onStart1
2022-05-13 10:33:00.851 31324-31324/com.wj.demo D/TAG: onResume1
2022-05-13 10:33:25.306 31324-31324/com.wj.demo D/TAG: onPause1
2022-05-13 10:33:25.959 31324-31324/com.wj.demo D/TAG: onStop1
2022-05-13 10:33:45.322 31324-31324/com.wj.demo D/TAG: onRestart1
2022-05-13 10:33:45.323 31324-31324/com.wj.demo D/TAG: onStart1
2022-05-13 10:33:45.327 31324-31324/com.wj.demo D/TAG: onResume1
2、activity1----->activity2----->activity1
2022-05-13 10:36:44.165 31726-31726/com.wj.demo D/TAG: onCreate1
2022-05-13 10:36:44.300 31726-31726/com.wj.demo D/TAG: onStart1
2022-05-13 10:36:44.305 31726-31726/com.wj.demo D/TAG: onResume1
2022-05-13 10:36:49.798 31726-31726/com.wj.demo D/TAG: onPause1
2022-05-13 10:36:49.823 31726-31726/com.wj.demo D/TAG: onCreate2
2022-05-13 10:36:49.850 31726-31726/com.wj.demo D/TAG: onStart2
2022-05-13 10:36:49.853 31726-31726/com.wj.demo D/TAG: onResume2
2022-05-13 10:36:50.419 31726-31726/com.wj.demo D/TAG: onStop1
2022-05-13 10:36:59.776 31726-31726/com.wj.demo D/TAG: onPause2
2022-05-13 10:36:59.789 31726-31726/com.wj.demo D/TAG: onRestart1
2022-05-13 10:36:59.790 31726-31726/com.wj.demo D/TAG: onStart1
2022-05-13 10:36:59.792 31726-31726/com.wj.demo D/TAG: onResume1
2022-05-13 10:37:00.350 31726-31726/com.wj.demo D/TAG: onStop2
2022-05-13 10:37:00.352 31726-31726/com.wj.demo D/TAG: onDestroy2
3、横竖屏切换activity
默认打开竖屏界面
2022-06-20 10:07:11.815 11323-11323/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onCreate
2022-06-20 10:07:11.817 11323-11323/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onStart
2022-06-20 10:07:11.819 11323-11323/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onResume
从竖屏转为横屏
2022-06-20 10:10:12.675 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onPause
2022-06-20 10:10:12.677 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onStop
2022-06-20 10:10:12.678 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onDestroy
2022-06-20 10:10:12.730 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onCreate
2022-06-20 10:10:12.734 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onStart
2022-06-20 10:10:12.737 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onResume
从横屏转为竖屏
2022-06-20 10:10:51.841 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onPause
2022-06-20 10:10:51.843 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onStop
2022-06-20 10:10:51.844 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onDestroy
2022-06-20 10:10:51.898 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onCreate
2022-06-20 10:10:51.900 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onStart
2022-06-20 10:10:51.902 11950-11950/com.wj.activitylifecycledemo D/HorizontalScreenActivity: onResume
结论:横竖屏切换时activity会销毁重新创建。
常用场景:游戏开发、视频播放场景。
如何解决横竖屏切换activity销毁重新创建:
- 禁止旋转,指定屏幕方向(游戏)
android:screenOrientation="landscape"/
- 对配置不敏感(视频播放器)
android:configChanges="keyboardHidden|screenSize|orientation"
例:以看视频为场景,上面一个ImageView,下面一个SeekBar,横竖屏切换时进度条会重置。为了避免此现象,当横竖屏切换时需要保存进度条。
相关代码:
public class HorizontalScreenActivity extends AppCompatActivity {
private String TAG = "HorizontalScreenActivity";
private SeekBar seekBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_horizontal_screen);
Log.d(TAG, "onCreate");
initView();
}
private void initView() {
seekBar = findViewById(R.id.seekBar);
//设置初始化数据
seekBar.setMax(100);
seekBar.post(new Runnable() {
@Override
public void run() {
seekBar.setProgress(0);
}
});
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
}
竖屏示意图:
横屏示意图:
五、Activity的启动模式
任务栈:特点是先进后出。把它比喻成一个盒子,先放进来的东西后拿出来。创建两个activity,通过改变AndroidManifest.xml中第二个activity的启动模式来学习Activity的启动模式。
第一个activity相关代码:
public class FirstActivity extends AppCompatActivity implements View.OnClickListener {
private Button bt1;
private Button bt2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
initView();
initListener();
}
private void initListener() {
bt1.setOnClickListener(this);
bt2.setOnClickListener(this);
}
private void initView() {
bt1 = findViewById(R.id.bt1);
bt2 = findViewById(R.id.bt2);
}
@Override
public void onClick(View view) {
if (view == bt1) {
startActivity(new Intent(this, FirstActivity.class));
} else if (view == bt2) {
startActivity(new Intent(this, SecondActivity.class));
}
}
}
第二个activity相关代码:
public class SecondActivity extends AppCompatActivity implements View.OnClickListener {
private Button bt3;
private Button bt4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
initView();
initListener();
}
private void initView() {
bt3 = findViewById(R.id.bt3);
bt4 = findViewById(R.id.bt4);
}
private void initListener() {
bt3.setOnClickListener(this);
bt4.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if (view == bt3) {
startActivity(new Intent(this, FirstActivity.class));
} else if (view == bt4) {
startActivity(new Intent(this, SecondActivity.class));
}
}
}
1、standard标准模式
standard模式每次都会创建新的任务栈,并置于当前栈顶。当点击返回键时其实就是销毁当前任务,可以理解为移除当前任务也就是出栈的过程,所以创建了多少个任务就需要点击多少次返回键去退出。
使用场景:大多数场景使用的都是standard模式。如果不进行配置,那么默认的就是standard启动模式。
android:launchMode="standard"
2、singleTop栈顶复用模式
singleTop模式表示如果栈顶已经是当前任务那么就不会再创建新的任务。
使用场景:一般来说为了保证只有一个任务而不被多次创建采用这种模式。例如浏览器的书签、应用的通知推送。
android:launchMode="singleTop"
3、singleTask栈内复用模式
singleTask模式表示如果我们要创建的任务没有,那么就会创建任务并置于栈顶。如果我们要创建的任务已经存在,那么就会把这个任务以上的全部任务从栈中移除并使得当前任务成为栈顶任务。当栈顶任务已经是我们要打开的任务时,也就不会再创建新的任务了。
使用场景:当任务占的资源相对较大时使用singleTask模式。
android:launchMode="singleTask"
4、singleInstance单实例模式
前面三种启动模式都是在同一个任务栈中,singleInstance模式特别之处在于它是一个独立的任务栈。作为单独的一个对象,独占一个栈,不会再创建只会把它提前。
使用场景:在整个系统中只有一个实例,例如launcher。
android:launchMode="singleInstance"