实验目的

  1. 掌握 Broadcast 编程基础。
  2. 掌握动态注册 Broadcast 和静态注册 Broadcast。
  3. 掌握Notification 编程基础。
  4. 掌握 EventBus 编程基础。

设置Activity为单例模式

manifests中添加

android:launchMode="singleInstance"

还有这几种情况:

(1)standard:每次激活Activity时(startActivity),都创建Activity实例,并放入任务栈;

(2)singleTop:如果某个Activity自己激活自己,即任务栈栈顶就是该Activity,则不需要创建,其余情况都要创建Activity实例;

(3)singleTask:如果要激活的那个Activity在任务栈中存在该实例,则不需要创建,只需要把此Activity放入栈顶,并把该Activity以上的Activity实例都pop

(4)singleInstance:如果应用1的任务栈中创建了MainActivity实例,如果应用2也要激活MainActivity,则不需要创建,两应用共享该Activity实例;

Broadcast使用

BroadcastReceiver(广播接收器),属于 Android 四大组件之一

注册的方式分为两种:静态注册动态注册

静态注册

1.注册广播

创建一个javaStaticReceiver

public class StaticReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("com.janking.sysuhealth.myapplication2.MyStaticFilter")){
            //代表捕获到需要的广播,内容稍后填充
        }
    }

}

然后在配置文件App->manifests->AndroidManifest.xml中加上

<receiver android:name=".StaticReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.janking.sysuhealth.myapplication2.MyStaticFilter" />
    </intent-filter>
</receiver>

这里的com.janking.sysuhealth.myapplication2.MyStaticFilter理论上可以自定义,只要保持一致就可以(后面还会出现)

2.发送广播

这里要实现打开页面就会弹出通知并随机出现一个Food的名字,点击跳到其详情页面

所以在Food列表的onCreate函数(即SecondActivity)中加上

//broadcast
Bundle bundle = new Bundle();
//把整个Food类变量放进bundle
bundle.putSerializable("broadcast_startup", mAdapter.getItem(new Random().nextInt(mAdapter.getItemCount())));//返回一个0到n-1的整数
Intent intentBroadcast = new Intent("com.janking.sysuhealth.myapplication2.MyStaticFilter"); //定义Intent
//下面这一句相当关键,这是Android8.0之后之后的写法
intentBroadcast.setComponent(new ComponentName("com.janking.sysuhealth", "com.janking.sysuhealth.StaticReceiver"));
intentBroadcast.putExtras(bundle);
sendBroadcast(intentBroadcast);

这里我用的API是28,即compileSdkVersion 28,跟之前的版本可能会不一样

关于Android SDK里的compileSdk、minSdk、targetSdk

3.接收广播

要认识到,广播其实也是以Intent的方式传过来的

所以还是这样解析传过来的Food

Bundle bundle = intent.getExtras();
Food food = (Food)bundle.getSerializable("broadcast_startup");

4.发送通知

不管什么时候,看到发送接收跳转这些字眼就只要有数据的传送,一般用的是Intent,这里要用PendingIntent

根据字面意思就知道是延迟的intent,主要用来在某个事件完成后执行特定的ActionPendingIntent包含了IntentContext,所以就算Intent所属程序结束,PendingIntent依然有效,可以在其他程序中使用。
常用在通知栏及短信发送系统中。

创建PendingIntent

//获取pendingIntent
//这里继续使用Click_Food关键词,就不用再Detail里加一个判断语句了
bundle.putSerializable("Click_Food", food);
Intent mainIntent = new Intent(context, Detail.class);
mainIntent.putExtras(bundle);
PendingIntent mainPendingIntent = PendingIntent.getActivity(context, 0, mainIntent, PendingIntent.FLAG_UPDATE_CURRENT);

又提到Android SDK版本的问题了,这里是8.0(API26)以上的使用方法,理论上低版本的方法更容易找到,但是我们目光还是要向前看(/滑稽)

创建通知Notification

//获取状态通知栏管理
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//channel
String CHANNEL_ID = "channel_01";
NotificationChannel mChannel = null;
if (mChannel == null) {
    String name = "my_channel_01";//渠道名字
    String description = "my_package_first_channel"; // 渠道解释说明
    //HIGH或者MAX才能弹出通知到屏幕上
    int importance = NotificationManager.IMPORTANCE_HIGH;
    mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
    mChannel.setDescription(description);
    mChannel.enableLights(true); //是否在桌面icon右上角展示小红点
    manager.createNotificationChannel(mChannel);
}
//notification
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.mipmap.empty_star)
        .setContentTitle("今日推荐")
        .setTicker("您有一条新消息")   //通知首次出现在通知栏,带上升动画效果的
        .setContentText(food.getName())
        .setDefaults(Notification.DEFAULT_ALL)
        .setAutoCancel(true)   //设置这个标志当用户单击面板就可以让通知将自动取消
        .setContentIntent(mainPendingIntent);
//display
Notification notification = mBuilder.build();
manager.notify(0,notification);

(以上三段代码都是放入StaticReceiver中的if语句中)

mChannel = new NotificationChannel(CHANNEL_ID, name, importance);

NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, CHANNEL_ID)

一定要用同一个CHANNEL_ID,不然就会出现下列错误

1539870233846


那么现在,如果点击通知跳转到食品详情页面,Detail.java需要更改什么吗?

不需要,这就像从食品列表跳转到详情一样,解析Food就行了,而且我很偷懒地把从Notification中发出的PendingIntent中的Food关键词也设置成Click_Food,对Detail来讲,它就像从列表跳过来一样!

动态注册

其实在Android 8.0之后是不建议用静态注册的,毕竟不灵活,所以还是关注下动态注册吧

1.注册广播

创建类DynamicReceiver

public class DynamicReceiver extends BroadcastReceiver {
    private static final String DYNAMICACTION = "com.janking.sysuhealth.myapplication2.MyDynamicFilter";
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DYNAMICACTION)) {    //动作检测
        //稍后填充
        }
    }
}

在需要发送广播的类中(这里是Detail)添加

//注册广播
IntentFilter dynamic_filter = new IntentFilter();
dynamic_filter.addAction("com.janking.sysuhealth.myapplication2.MyDynamicFilter");    //添加动态广播的Action
dynamicReceiver = new DynamicReceiver();
registerReceiver(dynamicReceiver, dynamic_filter);    //注册自定义动态广播消息

注意两个地方的Action要一致(下面还会出现)

2.发送广播

这里发送广播的激发事件是点击了Detail中的收藏图标

1539869840890

所以修改collect.setOnClickListener

collect.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if(!display_food.getFavorite()){
            change++;
            Toast.makeText(Detail.this, "已收藏", Toast.LENGTH_SHORT).show();
        }
        else
            Toast.makeText(Detail.this, "重复收藏", Toast.LENGTH_SHORT).show();
        display_food.setFavorite(true);
        //发送广播
        Bundle bundle = new Bundle();
        bundle.putSerializable("broadcast_favorite", display_food);//返回一个0到n-1的整数
        Intent intentBroadcast = new Intent("com.janking.sysuhealth.myapplication2.MyDynamicFilter"); //定义Intent
        //下面一句用来发送静态广播,这里不需要
        //intentBroadcast.setComponent(new ComponentName("com.janking.sysuhealth", "com.janking.sysuhealth.DynamicReceiver"));
        intentBroadcast.putExtras(bundle);
        sendBroadcast(intentBroadcast);

    }
});

3.接收广播

DynamicReceiver中的if语句中添加

Bundle bundle = intent.getExtras();
Food food = (Food)bundle.getSerializable("broadcast_favorite");

4.发送通知

再添加

//获取pendingIntent
Intent mainIntent = new Intent(context, SecondActivity.class);
mainIntent.putExtras(bundle);
PendingIntent mainPendingIntent = PendingIntent.getActivity(context, 0, mainIntent, PendingIntent.FLAG_UPDATE_CURRENT);

//获取状态通知栏管理
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//channel
String CHANNEL_ID = "channel_02";
NotificationChannel mChannel = null;
//不用重复赋值
if (mChannel == null) {
    String name = "my_channel_02";//渠道名字
    String description = "my_package_second_channel"; // 渠道解释说明
    //HIGH或者MAX才能弹出通知到屏幕上
    int importance = NotificationManager.IMPORTANCE_HIGH;
    mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
    mChannel.setDescription(description);
    mChannel.enableLights(true); //是否在桌面icon右上角展示小红点
    manager.createNotificationChannel(mChannel);
}
//notification
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.mipmap.full_star)
        .setContentTitle("已收藏")
        .setTicker("您有一条新消息")   //通知首次出现在通知栏,带上升动画效果的
        .setContentText(food.getName())
        .setDefaults(Notification.DEFAULT_ALL)
        .setAutoCancel(true)   //设置这个标志当用户单击面板就可以让通知将自动取消
        .setContentIntent(mainPendingIntent);
//display
Notification notification = mBuilder.build();
manager.notify(0,notification);

其实本质上这两个广播都是一样的,只是注册方式不一样,处理方式都是差不多的

5. 取消注册

这个是一定要做的,不然会出现内存泄漏的错误

E/ActivityThread: Activity com.janking.sysuhealth.Detail has leaked IntentReceiver com.janking.sysuhealth.DynamicReceiver@c408803 that was originally registered here. Are you missing a call to unregisterReceiver()?

1539871075001

注意:动态广播最好在ActivityonResume()注册、onPause()注销,当然我这里是在Oncreate()中注册的也行

所以在Detail中添加

@Override
protected void onPause() {
    super.onPause();
    unregisterReceiver(dynamicReceiver);
}

重复注册、重复注销也不允许


EventBus的简单使用

其实到这里,数据同步的问题还没有解决

当点击收藏弹出消息时,点击消息只能进去SecondActivity(即食品列表)界面,现在需要它显示为收藏食品列表界面,并且能够更新某些食品

当然直接用广播传来的数据是可以的,不过还是要学会使用多种方法(因为其实用广播处理的话是有点难…….)

1.添加依赖

Gradle Scripts ->build.gradle(Module: app)中的dependencies添加

implementation 'org.greenrobot:eventbus:3.0.0'

2.声明订阅方法

在需要接收数据的Activity(这里是SecondActivity)中添加

//EventBus
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(Food f) {
    //change RecyclerView
    mAdapter.updateData(f.getName(), f.getFavorite());
    //change listview
    myListViewAdapter.addNewItem(f);
    //change floatbutton
    isHome = false;
    mRecyclerView.setVisibility(View.INVISIBLE);
    mListView.setVisibility(View.VISIBLE);
    mButton.setImageResource(R.mipmap.mainpage);
};

不要添加到onCreate()方法或者其它方法中,要直接加到Activity类中,当做一个成员。

不然会出现错误@Override “Annotations are not allowed here”

这里的处理大概就是更新食品总列表中的数据(不可见)以及收藏食品列表中的元素(课件),而且改变下面的FloatingButton,并且使RecyclerView不可见,ListView可见

3.订阅

SecondActivity中添加(比如在onCreate())

EventBus.getDefault().register(this);

4.发送数据

这里就是在点击Detail中的通知后返回到SecondActivity页面,想来想去,也只有一个Food类型的数据要传过来,那就在之前的DynamicReceiver中的if语句中添加一句

EventBus.getDefault().post(food);

就能传送数据了,真的是超级简单啊!!!

5.解决列表重复的问题

Detail页面中多次点击收藏图标的话,虽然我之前做了处理,弹出的Toast会显示“重复收藏”,但是每次点击都会发送广播,每次发送广播都会有一个post(food)的动作,即使不跳过去看,但是其实数据已经右EventBus传到了`SecondActivity了,并且也已经处理了。

所以我想了个办法,对这一句myListViewAdapter.addNewItem(f);做做手脚,找到MyListViewAdapter中的MyListViewAdapter,改为

public void addNewItem(Food f) {
    if(list == null) {
        list = new ArrayList<>();
    }
    for(Food i : list){
        if(i.getName().equals(f.getName())){
            notifyDataSetChanged();
            return;
        }
    }
    list.add(f);
    notifyDataSetChanged();
}

不会重复啦!

效果:(AVD实在有点卡)

broadcast


参考资料

AVD,Guest isn't online after 7 seconds 问题

安卓8.0系统notification适配Failed to post notification on

android show notification with a popup on top of any application