使用 WorkManager 实现定期通知

今天早上我有一些空闲时间,所以我决定为我的应用程序Transactions解决一个问题。问题是添加一个循环通知功能,提醒应用程序的用户记录他们当天的交易。

所以,我做了一些研究,并决定使用以下两种方法之一:

1. Firebase 云消息传递(简单的方法)

Firebase Cloud Messaging 是一项服务,可让您向 Android、iOS 或 Web 应用程序发送通知或数据负载。发送后台通知(仅当您的应用程序在后台时才有效)相当容易。您可以admin-sdk在 Firebase 控制台中使用 firebase 或通知编写器向目标令牌、客户端设备订阅的主题或所有用户发送带有可选负载(图像或键值对)的通知整个应用程序。当应用程序在后台时,通知会出现在系统托盘中,点击它会启动应用程序。设置定期通知也非常简单,您只需更改频率即可。

对于Transactions,我只是将 SDK 依赖项添加到我的build.gradle文件中,运行应用程序,退出应用程序(这样它就在后台,前台通知的工作方式非常不同),然后使用通知编写器发送测试通知。一旦成功,我就设置了每天晚上 10 点的定期通知。


FCM 还允许您随时随地修改通知,这是一个额外的优势。但是,我想要更多的功能:

  • 用户应该能够将默认的提醒时间晚上 10 点更改为他们喜欢的时间
  • 如果愿意,用户应该能够选择退出此功能。

虽然第二个功能可以通过让应用注册或注销daily-reminderFCM 中的主题来实现,但第一个功能需要云功能或合适的后端,我希望将此实现保留在设备上。所以,我转向了第二个选项。

2. 带有定期工作请求的 WorkManager

我之前参加过一个codelab ,它教授了如何使用WorkManager设置通知。尽管它专注于即时的一次性请求,但我知道它WorkManager具有可以以固定间隔重复执行的延迟、定期工作请求的功能。我可以为此添加一些初始延迟以获得我想要的行为。

我首先设置了一个NotificationHelperKotlin 单例对象,其目的是创建通知通道(在 Android 8+ 上需要)、在PendingIntent点击通知时启动应用程序以及通知本身。

通知处理程序

object NotificationHandler {
    private const val CHANNEL_ID = "transactions_reminder_channel"

    fun createReminderNotification(context: Context) {
        //  No back-stack when launched
        val intent = Intent(context, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(context, 0, intent,
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE
            else PendingIntent.FLAG_UPDATE_CURRENT)

        createNotificationChannel(context) // This won't create a new channel everytime, safe to call

        val builder = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.mipmap.ic_launcher_round)
            .setContentTitle("Remember to add your transactions!")
            .setContentText("Logging your transactions daily can help you manage your finances better.")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent) // For launching the MainActivity
            .setAutoCancel(true) // Remove notification when tapped
            .setVisibility(VISIBILITY_PUBLIC) // Show on lock screen

        with(NotificationManagerCompat.from(context)) {
            notify(1, builder.build())
        }
    }

    /**
     * Required on Android O+
     */
    private fun createNotificationChannel(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "Daily Reminders"
            val descriptionText = "This channel sends daily reminders to add your transactions"
            val importance = NotificationManager.IMPORTANCE_HIGH
            val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            val notificationManager: NotificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }
}

我计划createReminderNotification从调用该方法Worker时开始调用该方法。所以,我开始为WorkManager. 在我们查看代码之前WorkManager,根据 AndroidX 团队的说法,这就是:

WorkManager是持久工作的推荐解决方案。当通过应用程序重新启动和系统重新启动保持计划时,工作是持久的。因为大多数后台处理最好通过持久工作来完成,所以WorkManager 是推荐用于后台处理的主要 API。

WorkManager 抽象出 Android 拥有的无数后台处理 API,包括 FirebaseJobDispatcher、GcmNetworkManager 和 Job Scheduler。它提供一个单一的表面,然后根据上下文和 API 级别将工作委托给这些 API。这些是支持的Work请求类型WorkManager:


它的功能无法在一篇文章中描述,所以我希望代码和注释能够传达正在发生的事情的含义。首先WorkManager,我们将此依赖项添加到我们的build.gradle(针对 Kotlin):

implementation "androidx.work:work-runtime-ktx:2.7.1"

提醒通知工作者

这些是以固定间隔运行的请求TimeUnit,例如天、小时、分钟等。这里要注意的重要一点是,WorkManager它不能保证 aWork将在设定的时间执行。由于电池优化等原因,它可能会延迟,但它确实保证Work迟早会执行。我从扩展Worker类开始创建一个新Worker的,其doWork()方法WorkManager在它被触发时执行。

class ReminderNotificationWorker(private val appContext: Context, workerParameters: WorkerParameters) : Worker(appContext, workerParameters) {
    override fun doWork(): Result {
        NotificationHandler.createReminderNotification(appContext)
        return Result.success()
    }
}

每当ReminderNotificationWorker执行时,它都会创建一个新的提醒通知。我在这里没有包含任何错误处理,我计划稍后添加。

之后,我定义了一个静态的,或者 Kotlin 喜欢称之为的companion object方法,被称为schedule,它接受一个Context,以及每天应该生成通知的时间。此方法还处理某些情况,例如当用户选择的时间早于当前时间时,它将第一次触发延迟到第二天。这听起来很容易,但手动实现却出奇地复杂,Calendar最后我求助于课堂。

companion object {

        /**
         * @param hourOfDay the hour at which daily reminder notification should appear [0-23]
         * @param minute the minute at which daily reminder notification should appear [0-59]
         */
        fun schedule(appContext: Context, hourOfDay: Int, minute: Int) {
            log("Reminder scheduling request received for $hourOfDay:$minute")
            val now = Calendar.getInstance()
            val target = Calendar.getInstance().apply {
                set(Calendar.HOUR_OF_DAY, hourOfDay)
                set(Calendar.MINUTE, minute)
            }

            if (target.before(now)) {
                target.add(Calendar.DAY_OF_YEAR, 1)
            }

            log("Scheduling reminder notification for ${target.timeInMillis - System.currentTimeMillis()} ms from now")

            val notificationRequest = PeriodicWorkRequestBuilder<ReminderNotificationWorker>(24, TimeUnit.HOURS)
                .addTag(TAG_REMINDER_WORKER)
                .setInitialDelay(target.timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS).build()
            WorkManager.getInstance(appContext)
                .enqueueUniquePeriodicWork(
                "reminder_notification_work",
                ExistingPeriodicWorkPolicy.REPLACE,
                notificationRequest
            )
        }
    }

我们UniquePeriodicWork用来确保我们只存在一个实例,ReminderNotificationWorker因为我不想用不正确的通知来惹恼用户。Work如果用户选择退出,该标签稍后用于识别和取消所有请求。我意识到此时我需要处理用户偏好,因此我编写了一个SharedPreferences帮助类来帮助我们与SharedPreferencesAndroid 提供的键值存储进行交互。

偏好商店

class PreferenceStore(context: Context) {
    private val sharedPref: SharedPreferences = context.getSharedPreferences("transactions_shared_pref", Context.MODE_PRIVATE)

    /**
     * @return the hour of the day in which the reminder should be shown, default is 22, if canceled -1
     * @return the minute at which the reminder should be shown, default is 0, if canceled -1
     */
    fun getReminderTime(): Pair<Int, Int> {
        return Pair(
            sharedPref.getInt(SHARED_PREF_REMINDER_HOUR, 22),
            sharedPref.getInt(SHARED_PREF_REMINDER_MINUTE, 0)
        )
    }

    fun setReminderTime(hour: Int, minute: Int) {
        sharedPref.edit {
            putInt(SHARED_PREF_REMINDER_HOUR, hour)
            putInt(SHARED_PREF_REMINDER_MINUTE, minute)
        }
    }

    fun cancelReminder() {
        sharedPref.edit {
            putInt(SHARED_PREF_REMINDER_HOUR, -1)
            putInt(SHARED_PREF_REMINDER_MINUTE, -1)
        }
    }

    fun isDefaultReminderSet() = sharedPref.getBoolean("is_default_reminder_set", false)

    fun saveDefaultReminderIsSet() {
        sharedPref.edit { putBoolean("is_default_reminder_set", true) }
    }
}

首次启动应用程序时,我们将默认提醒设置为晚上 10 点,然后由用户自定义。我可能已经删除了底部的一些冗余方法,但找不到可行的解决方案。现在,让我们转到 ViewModel。

主视图模型

class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val app = application
    private val prefStore = PreferenceStore(application)

    fun scheduleReminderNotification(hourOfDay: Int, minute: Int) {
        prefStore.setReminderTime(hourOfDay, minute)
        ReminderNotificationWorker.schedule(app, hourOfDay, minute)
    }

    fun getReminderTime() = prefStore.getReminderTime()

    fun cancelReminderNotification() {
        log("Cancelling reminder notification")
        prefStore.cancelReminder()
        WorkManager.getInstance(app).cancelAllWorkByTag(TAG_REMINDER_WORKER)
    }

    /**
     * This sets the default time at the first launch of the app
     */
    private fun checkAndSetDefaultReminder() {
        if (!prefStore.isDefaultReminderSet()) {
            scheduleReminderNotification(DEFAULT_REMINDER_HOUR, DEFAULT_REMINDER_MINUTE)
            prefStore.saveDefaultReminderIsSet()
        }
    }

我们扩展类,因为初始化数据库实例和共享首选项存储需要AndroidViewModel应用程序实例,或者更确切地说是. 我计划很快将使用 Hilt 的依赖注入添加到应用程序中,这将消除对此的需求。在我们的 ViewModel 中,每次初始化 ViewModel 时都会调用该方法。现在这没问题(或者是吗?:p),因为我们只使用一个跨片段共享的 ViewModel,因为 Transactions 是一个相对较小的应用程序,但它很快就会迁移到更合适的地方,比如 Application 类。其他方法的意图应该通过名称足够清楚。现在,让我们看看允许用户配置通知的方法:ContextWorkManagercheckAndSetDefaultReminderBottomSheetDialogFragment

通知BottomSheet

class NotificationsBottomSheet : BottomSheetDialogFragment() {
    private lateinit var binding: FragmentNotificationsBottomSheetBinding
    private val viewModel by viewModels<MainViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentNotificationsBottomSheetBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setInitial()

        binding.checkboxReminder.setOnCheckedChangeListener { buttonView, isChecked ->
            if (isChecked) {
                // Was previously removed by user, now user wants to re-enable.
                viewModel.scheduleReminderNotification(DEFAULT_REMINDER_HOUR, DEFAULT_REMINDER_MINUTE)
                binding.textViewReminderTime.isEnabled = true
                binding.textViewReminderTime.text = get12HourTime(DEFAULT_REMINDER_HOUR, DEFAULT_REMINDER_MINUTE)
            } else {
                viewModel.cancelReminderNotification()
                binding.textViewReminderTime.isEnabled = false
                binding.textViewReminderTime.text = "Not set"
            }
        }

        binding.textViewReminderTime.setOnClickListener {
            val time = viewModel.getReminderTime()

            val picker = MaterialTimePicker.Builder()
                .setTimeFormat(TimeFormat.CLOCK_12H)
                .setHour(time.first)
                .setMinute(time.second)
                .setTitleText("Select Reminder Time")
                .build()
            picker.show(parentFragmentManager, "notification-time-picker")

            picker.addOnPositiveButtonClickListener {
                val hour = picker.hour
                val minute = picker.minute
                viewModel.scheduleReminderNotification(hour, minute)
                val newTime = viewModel.getReminderTime()
                binding.textViewReminderTime.text = get12HourTime(newTime.first, newTime.second)
            }
        }
    }

    private fun setInitial() {
        val setTime = viewModel.getReminderTime()
        if (setTime.first == -1 || setTime.second == -1 ) {
            binding.checkboxReminder.isChecked = false
            binding.textViewReminderTime.isEnabled = false
            binding.textViewReminderTime.text = "Not set"
        } else {
            binding.checkboxReminder.isChecked = true
            binding.textViewReminderTime.isEnabled = true
            binding.textViewReminderTime.text = get12HourTime(setTime.first, setTime.second)
        }
    }
}

从首选项中获取和设置初始值。用户可以通过单击复选框来启用或禁用通知。时间可以通过点击文本来配置,默认值为 10 PM。我们使用 MaterialTimePicker小部件让用户可以轻松选择时间,而且与TimerPickerDialog在应用程序其他地方使用的单独的 相比,它的实现也非常简单。

每隔24小时,WorkManager会触发提醒通知Worker。然后,工作人员创建一个通知(这就是工作),点击它可以将用户直接带到应用程序。这使我们能够实现上述功能,同时利用WorkManager的强大AndroidX库,在设备上保持实现。

总结

写到这里也结束了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个关于Flutter的学习思路及方向,从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。
由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的文末卡片免费获取。

还有免费的高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值