今天早上我有一些空闲时间,所以我决定为我的应用程序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进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。