You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 01:40:24