基于Kotlin+Volley实现API数据更新检测与本地通知方案
实现非Firebase的API内容更新检测与本地通知
问题背景
开发一款从API获取文档/源URL列表的Android App,已用Volley实现基础数据获取功能,需要定期检查API新增/更新内容,触发本地通知提醒用户更新本地保存项,不使用Firebase服务。
解决方案
1. 本地数据对比逻辑(核心)
通过对比本地存储的旧数据与API返回的新数据,识别新增或更新项。利用SharedPreferences存储本地数据的JSON字符串,以Pubs类的Number字段作为唯一标识,结合内容对比完成检测。
修改你的fetchPubs()方法,加入对比逻辑:
private fun fetchPubs() { _binding.contentMain.loading.visibility = View.VISIBLE request = JsonArrayRequest( Request.Method.GET, Config.BASE_URL, JSONArray(),{ response -> val items: List<Pubs> = Gson().fromJson(response.toString(), object : TypeToken<List<Pubs>>() {}.type) val sortedItems = items.sortedWith(compareBy { it.Number }) // 1. 获取本地存储的旧数据 val sharedPref = getSharedPreferences("AppPrefs", MODE_PRIVATE) val oldPubsJson = sharedPref.getString("saved_pubs", "") val oldPubs: List<Pubs>? = if (oldPubsJson.isNotEmpty()) { Gson().fromJson(oldPubsJson, object : TypeToken<List<Pubs>>() {}.type) } else null // 2. 对比新/旧数据,找出新增/更新项 val newOrUpdatedItems = mutableListOf<Pubs>() if (oldPubs != null) { val oldNumbers = oldPubs.map { it.Number }.toSet() // 新增项:API返回的Number不在本地集合中 newOrUpdatedItems.addAll(sortedItems.filter { !oldNumbers.contains(it.Number) }) // 更新项:Number相同但内容不同 sortedItems.forEach { newPub -> oldPubs.find { it.Number == newPub.Number }?.let { oldPub -> if (Gson().toJson(newPub) != Gson().toJson(oldPub)) { newOrUpdatedItems.add(newPub) } } } } else { // 首次加载,所有数据都是新增项 newOrUpdatedItems.addAll(sortedItems) } // 3. 保存最新数据到本地 sharedPref.edit().putString("saved_pubs", Gson().toJson(sortedItems)).apply() // 4. 有更新则触发通知 if (newOrUpdatedItems.isNotEmpty()) { showUpdateNotification(newOrUpdatedItems.size) } // 原有UI更新逻辑 pubsList?.clear() pubsList?.addAll(sortedItems) _binding.contentMain.recyclerView.recycledViewPool.clear() adapter?.notifyDataSetChanged() _binding.contentMain.loading.visibility = View.GONE setupData() Log.i("LENGTH OF DATA", "${items.size}") }, {error -> println(error.printStackTrace()) Toasty.error(applicationContext, getString(string.no_internet), Toast.LENGTH_SHORT, true).show() } ) MyApplication.instance.addToRequestQueue(request!!) }
2. 本地通知实现(无需Firebase)
使用Android原生NotificationManager创建通知渠道并发送通知,适配Android 8.0+的渠道要求:
private fun showUpdateNotification(updateCount: Int) { val channelId = "pubs_update_channel" // 创建通知渠道(Android 8.0+ 必填) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, "文档更新通知", NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "API有新文档或更新时推送" } val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } // 点击通知跳转至主页面 val intent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } val pendingIntent = PendingIntent.getActivity( this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) // 构建通知内容 val notification = NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.ic_notification) // 替换为你的通知图标 .setContentTitle("发现文档更新") .setContentText("共${updateCount}项内容新增或更新") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntent) .setAutoCancel(true) .build() // 发送通知 (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(1, notification) }
3. 定期后台检查(用WorkManager)
使用官方推荐的WorkManager实现后台定期任务,适配Android后台限制:
步骤3.1 添加依赖
在app/build.gradle的dependencies中加入:
implementation "androidx.work:work-runtime-ktx:2.8.1"
步骤3.2 创建Worker类
封装API请求和数据对比逻辑,在后台执行:
class PubsUpdateWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { return try { // 协程封装Volley请求 val response = fetchPubsFromApi() val items: List<Pubs> = Gson().fromJson(response.toString(), object : TypeToken<List<Pubs>>() {}.type) val sortedItems = items.sortedWith(compareBy { it.Number }) // 本地数据对比逻辑(同fetchPubs中的代码) val sharedPref = applicationContext.getSharedPreferences("AppPrefs", Context.MODE_PRIVATE) val oldPubsJson = sharedPref.getString("saved_pubs", "") val oldPubs: List<Pubs>? = if (oldPubsJson.isNotEmpty()) { Gson().fromJson(oldPubsJson, object : TypeToken<List<Pubs>>() {}.type) } else null val newOrUpdatedItems = mutableListOf<Pubs>() if (oldPubs != null) { val oldNumbers = oldPubs.map { it.Number }.toSet() newOrUpdatedItems.addAll(sortedItems.filter { !oldNumbers.contains(it.Number) }) sortedItems.forEach { newPub -> oldPubs.find { it.Number == newPub.Number }?.let { oldPub -> if (Gson().toJson(newPub) != Gson().toJson(oldPub)) { newOrUpdatedItems.add(newPub) } } } } else { newOrUpdatedItems.addAll(sortedItems) } // 有更新则发通知并保存数据 if (newOrUpdatedItems.isNotEmpty()) { showUpdateNotification(applicationContext, newOrUpdatedItems.size) sharedPref.edit().putString("saved_pubs", Gson().toJson(sortedItems)).apply() } Result.success() } catch (e: Exception) { // 失败后自动重试 Result.retry() } } // 协程封装Volley GET请求 private suspend fun fetchPubsFromApi(): JSONArray { return suspendCancellableCoroutine { continuation -> val request = JsonArrayRequest( Request.Method.GET, Config.BASE_URL, JSONArray(), { response -> continuation.resume(response) }, { error -> continuation.resumeWithException(error) } ) MyApplication.instance.addToRequestQueue(request) continuation.invokeOnCancellation { request.cancel() } } } // 通知逻辑(复用之前的代码,调整context参数) private fun showUpdateNotification(context: Context, updateCount: Int) { val channelId = "pubs_update_channel" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, "文档更新通知", NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "API有新文档或更新时推送" } val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } 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, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) val notification = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) .setContentTitle("发现文档更新") .setContentText("共${updateCount}项内容新增或更新") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntent) .setAutoCancel(true) .build() (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(1, notification) } }
步骤3.3 启动定期任务
在MainActivity的onCreate中初始化定期检查:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... 原有初始化代码 ... // 设置每12小时检查一次(可根据需求调整周期) val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) // 仅在联网时执行 .build() val periodicRequest = PeriodicWorkRequestBuilder<PubsUpdateWorker>(12, TimeUnit.HOURS) .setConstraints(constraints) .build() WorkManager.getInstance(this).enqueueUniquePeriodicWork( "PubsUpdateTask", ExistingPeriodicWorkPolicy.KEEP, periodicRequest ) }
注意事项
- 数据对比优化:若数据量较大,对比JSON字符串效率低,可要求API返回
lastUpdated时间戳字段,通过时间戳对比替代内容对比。 - 通知权限:Android 13+需申请
POST_NOTIFICATIONS权限,在AndroidManifest.xml中添加权限声明,并在运行时请求。 - 后台适配:WorkManager会自动适配Android的后台限制,避免被系统杀死,比Timer/AlarmManager更可靠。
内容的提问来源于stack exchange,提问作者DrewCodes
相关产品推荐
相关产品推荐

