首页 🍔Android-Kotlin,🚴‍♂️第一行代码学习笔记

全局大喇叭,详解广播机制

广播机制

标准广播

异步执行,广播发出后,所有的BroadcastReceiver几乎会同时受到这条消息,没有先后顺序,效率高,无法被截断

有序广播

同步执行,广播发出后,同一时刻只会有一个BroadcastReceiver能接收到这条消息,当这个BroadcastReceiver中的逻辑执行完成后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的,优先级高的BoradcastReceiver先收到,并且可以截断,后面的BroadcastReceiver就无法收到

接收系统广播

注册BroadcastReceiver有两种方法,在代码中注册和在AndroidManifest.xml中注册。前者称为动态注册,后者静态

动态注册监听时间变化

新建一个类让他继承自BroadcastReceiver,并重写父类的onReceiver()方法。

package com.example.broadcastreceiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class  MainActivity : AppCompatActivity() {

    lateinit var timeChangeReceiver: TimeChangeReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val intentFilter = IntentFilter()
        intentFilter.addAction("android.intent.action.TIME_TICK")
        timeChangeReceiver = TimeChangeReceiver()
        registerReceiver(timeChangeReceiver, intentFilter)
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(timeChangeReceiver)
    }

    inner class TimeChangeReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
        }
    }
}

定义一个内部类TimeChangeReceiver继承BroadcastReceiver然后重写onReceive()。每当系统时间发生变化onReceive()方法就会被执行
系统时间发生变化时发出的就是android.intent.action.TIME_TICK的action所以在onCreate()方法中创建一个intentFilter的实例并添加android.intent.action.TIME_TICK。
也就是说想监听什么广播就填加相应的action,随后创建了一个TimeChangeReceiver的实例,然后调用registerReceiver()方法进行注册,将TimeChangeReceiver的实例和intentFilter的实例传进去。这样TimeChangeReceiver就会收到所有值为android.intent.action.TIME_TICK的广播。
动态注册的BroadcastReceiver一定要取消。

静态注册实现开机启动

右击com.example.broadcasttest包->new->Other->Broadcast Receiver,新建

package com.example.broadcastreceiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class BootCompleteReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show()
    }
}

静态的BroadcastReceiver一定要在AndroidManifest.xml文件中注册。用AS快捷方式创建的BroadcastReceiver已经注册
不过目前的BootCompleteReceiver是无法收到开机广播的,因为我们还需要对AndroidManifest.xml文件进行修改

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.broadcastreceiver">

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    ...
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
        
    </application>

</manifest>

系统启动完成后会发出一条android.intent.action.BOOT_COMPLETED的广播,因此我们要在<receiver>标签中添加一条<intent-filter>标签,并在里面声明相应的action
`<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
`声明开机广播的权限

发送自定义广播

发送标准广播

发送广播之前要先定义一个BroadcastReceiver接收广播。新建,并在onReceive()中加入

override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_LONG).show()
}

在AndroidManifest.xml中对这个BoradcastReceiver进行修改:

<intent-filter>
    <action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>

这里让新建的BroadcastReceiver接收一条值为com.example.broadcasttest.MY_BROADCAST的广播,所以我们要发送一条com.example.broadcasttest.MY_BROADCAST
添加一个Button

button.setOnClickListener {
    val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
    intent.setPackage(packageName)
    sendBroadcast(intent)
}

新建一个Intent对象,并把要发送的广播值传入,然后调用Intent的setPackage()方法,传入当前包名,最后调用sendBroadcast()方法发送
静态注册的BroadcastReceiver无法接收隐式广播,默认情况下我们发送的广播就是隐式广播,因此这里一定要调用setPackage()方法,指定这条广播发送给那个应用程序的,从而让它变成一条显示广播

发送有序广播

新建BroadcastReceiver,MyBroadcastReceiver,添加Toast。
在AndroidManifest.xml中对新建的BroadcastReceiver进行配置添加

<intent-filter>
    <action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>

然后修改MainActivity中的代码
将sendBroadcast()方法修改为sendOrderBroadcast(intent, null)
sendOrderBrocast()接收两个参数:第一个参数仍然是Intent,第二个参数是与权限相关的字符串,这里传入null

设置BroadcastReceiver的先后顺序

在AndroidManifest.xml中修改MyBroadcastReceiver的<intent-filter>

<intent-filter android:priority="100">
    <action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>

通过android:priority属性设置了优先级,优先级高的可以先收到广播

截断

修改MyBroadcastReceiver中的代码

package com.example.broadcastreceiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class AnotherBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show()
        abortBroadcast()
    }
}

如果在onReceive()方法中调用了abortBroadcast()方法,表示将这条广播截断,后面的BroadcastReceiver将无法收到这条广播

广播最佳实践:实现强制下线功能

新建BroadcastBestPractice项目,强制下线功能需要先关闭所有Activity,然后退回登录界面,新建一个ActivityCollector类用于管理所有的Activity

package com.example.broadcastbestpractice

import android.app.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作为所有Activity的父类

package com.example.broadcastbestpractice

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

open class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ActivityCollector.addActivity(this)
    }

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

创建一个LoginActivity作为登录界面,并让AS帮我们自动生成相应的布局文件。然后编辑布局文件activity_login.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">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp" >
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account" />
        <EditText
            android:id="@+id/accountEdit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical" />
    </LinearLayout>
    
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password" />
        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/passwordEdit"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            android:inputType="textPassword" />
    </LinearLayout>
    
    <Button
        android:id="@+id/login"
        android:layout_width="200dp"
        android:layout_height="60dp"
        android:layout_gravity="center_horizontal"
        android:text="Login" />
</LinearLayout>

然后修改LoginActivity

package com.example.broadcastbestpractice.ui.login

import android.app.Activity
import android.content.Intent
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import com.example.broadcastbestpractice.BaseActivity
import com.example.broadcastbestpractice.MainActivity

import com.example.broadcastbestpractice.R
import kotlinx.android.synthetic.main.activity_login.*

class LoginActivity : BaseActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        val login = findViewById<Button>(R.id.login)
        login.setOnClickListener {
            val account = accountEdit.text.toString()
            val password = passwordEdit.text.toString()
            if (account == "admin" && password == "123456") {
                val intent = Intent(this, MainActivity::class.java)
                startActivity(intent)
                finish()
            } else {
                Toast.makeText(this, "account or password is invalid", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

将LoginActivity的继承结构改成继承自BaseActivity,然后判断用户名密码,成功则跳转到MainActivity,否则提示密码错误

修改activity_main.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
        android:id="@+id/forceOffline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send force offline broadcast" />

</LinearLayout>

添加一个按钮强制下线

修改MainActivity

package com.example.broadcastbestpractice

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        forceOffline.setOnClickListener { 
            val intent = Intent("com.example.broadcastbestpractice.FORCE_OFFLINE")
            sendBroadcast(intent)
        }
    }
}

注册按钮发送一条广播,广播的值为"com.example.broadcastbestpractice.FORCE_OFFLINE",这条广播就是通知应用程序强制用户下线的。所以强制下线的逻辑并不是在MainActivity中写,而应该写在接收这条广播的BroadcastReceiver里,这样强制下线的功能就不会依附于任何界面

静态注册的BroadcastReceiver无法弹出对话框这样的UI控件,又不能在每一个Activity中注册一个动态的BroadcastReceiver
只需要在BaseActivity中动态注册一个BroadcastReceiver就可以了,因为所有的Activity都继承自BaseActivity
修改BaseActivity

package com.example.broadcastbestpractice

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.example.broadcastbestpractice.ui.login.LoginActivity

open class BaseActivity : AppCompatActivity() {
    lateinit var receiver: ForeOfflineReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ActivityCollector.addActivity(this)
    }

    override fun onResume() {
        super.onResume()
        val intentFilter = IntentFilter()
        intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE")
        receiver = ForeOfflineReceiver()
        registerReceiver(receiver, intentFilter)
    }
    
    override fun onPause() {
        super.onPause()
        unregisterReceiver(receiver)
    }

    override fun onDestroy() {
        super.onDestroy()
        ActivityCollector.removeActivity(this)
    }
    
    inner class ForeOfflineReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            AlertDialog.Builder(context).apply { 
                setTitle("Warning")
                setMessage("You are forced to be offline. Please try to login again.")
                setCancelable(false)
                setPositiveButton("OK") {_, _->
                    ActivityCollector.finishAll()
                    val i = Intent(context, LoginActivity::class.java)
                    context.startActivity(i)
                }
                show()
            }
        }
    }
}

onReceive()方法中加入AlertDialog.Builder构建了一个对话框,这里一定要调用setCancelable()方法将对话框设置为不可取消。
然后使用setPositiveButton()方法给对话框注册确定按钮,当用户点击OK时,就调用ActivityCollector的finishAll()方法销毁所有Activity,并重启LoginActivity

注册ForceOfflineReceiver

在BaseActivity中重写了onResume()和onPause(),然后分别在这两个方法里注册和取消注册ForceOfflineReceiver
之前注册和取消注册都是在onCreate()和onDestroy()中。
这是因为我们需要保证只有栈顶的Activity才能接受到这条强制下线广播,所以写在onResume()和onPause()

到这所有的强制下线逻辑就已经完成,接下来修改AndroidManifest.xml

<activity
    android:name=".ui.login.LoginActivity"
    android:label="@string/title_activity_login">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

将主Activity设置为LoginActivity,而不是MainActivity,这样打开应用时就是先打开登录界面




文章评论