Introduction
In this tutorial, you'll learn how to create a background service using Kotlin to listen to push notifications even when the app is closed, handle them and save them to SQLite.
Even though the context is a Flutter app, this is applicable to native development as you'll have to write native Android code anyway.
Jump to the final code or keep reading for a detailed walkthrough.
Context
In a recent personal project (Notificador LTV), I had to add push notification capabilities to the Flutter app. I used OneSignal for it and the integration was really easy.
One requirement was to save the received notification in the SQLite database. Implement everything in Flutter was straightforward using sqflite
, however another requirement was that this behavior should also happen when the user has closed the app completely. For that, I needed to implement a background service on Android.
OneSignal provides the NotificationExtenderService
class that you can extend to add such functionality so you only need to deal with the SQLite code.
Setup
Listen to push notifications
The first step is to create a class that extends NotificationExtenderService
. Create CustomNotificationExtender.kt
under the folder android/app/src/main/kotlin/your/package/name
, alongside with MainActivity.kt
.
Add the following code to this file:
package your.package.name
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult
class CustomNotificationExtender : NotificationExtenderService() {
protected override fun onNotificationProcessing(receivedResult: OSNotificationReceivedResult): Boolean {
return false
}
}
This class only listens to the background notifications and allows them to be displayed on the UI by returning false
- check OneSignal's documentation about it.
Declare class in manifest
Add the following to your AndroidManifest.xml
:
<service
android:name=".CustomNotificationExtender" // change this if your class has another name
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="com.onesignal.NotificationExtender" />
</intent-filter>
</service>
Describe the database
In order to interact with SQLite, you need to describe the database and tables used. Create a helper class for it and for the sake of the example, the table will have only two columns: id
and title
.
Inside the same file you've created add the following code:
package your.package.name
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult
object MyData {
const val TABLE_NAME = "mytable"
const val COLUMN_NAME_ID = "id"
const val COLUMN_NAME_TITLE = "title"
}
class MyDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
private val CREATE_MY_TABLE =
"CREATE TABLE ${MyData.TABLE_NAME} (" +
"${MyData.COLUMN_NAME_ID} INTEGER PRIMARY AUTOINCREMENT," +
"${MyData.COLUMN_NAME_TITLE} TEXT)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_MY_TABLE)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onCreate(db)
}
companion object {
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "my_database.db"
}
}
class CustomNotificationExtender : NotificationExtenderService() {
protected override fun onNotificationProcessing(receivedResult: OSNotificationReceivedResult): Boolean {
return false
}
}
Now you've created an object that describes some static values for table and columns name - it will be helpful later on - and an object that extends SQLite's OpenHelper that describes what it should do when connecting to the database - and versions it too.
Persist the notification data
Now you need to access the data received and persist it. Let's break it into a few steps:
- Update imports
- Access the data received
- Check if the data should be persisted
- Persist the data
- Handle errors
Update imports
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult
Access the data received
The receivedResult
variable has the property payload
of type OSNotificationPayload
which can be used to get the notification message, additional data and more. It also has restoring
and isAppInFocus
which gives the state of the app.
In this example, we only want the title as the id is autogenerated when a record is inserted:
val title = receivedResult.payload.title
Check if the data should be persisted
Depending on how you're handling notifications on the Flutter code, you may not want to do anything in the native code. If that's the case, you can do something like:
if (receivedResult.isAppInFocus || receivedResult.restoring) {
Log.i("ExtenderIgnored", "App is in focus or restoring")
// `false` allows the notification to be displayed
return false
}
You may want to run a query on SQLite) before deciding if the data should be persisted - prevent two notifications with the same title in this example:
val context = getBaseContext()
val dbHelper = MyDbHelper(context)
val db = dbHelper.getWritableDatabase()
val cursor = db.query(MyData.TABLE_NAME, null, "title = ?", arrayOf(title), null, null, null, null)
// Don't persist data if another record has the same title
if (cursor.count > 0) {
// `false` allows the notification to be displayed
return false
}
Persist the data
You need a ContentValues
object to persist data:
val values = ContentValues()
values.put(MyData.COLUMN_NAME_TITLE, title)
db.insert(MyData.TABLE_NAME, null, values)
// `false` allows the notification to be displayed
return false
Handle errors
Some of the code above can fail, therefore you should wrap it in a try/catch
.
Final code
package your.package.name
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult
object MyData {
const val TABLE_NAME = "mytable"
const val COLUMN_NAME_ID = "id"
const val COLUMN_NAME_TITLE = "title"
}
class MyDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
private val CREATE_MY_TABLE =
"CREATE TABLE ${MyData.TABLE_NAME} (" +
"${MyData.COLUMN_NAME_ID} INTEGER PRIMARY AUTOINCREMENT," +
"${MyData.COLUMN_NAME_TITLE} TEXT)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_MY_TABLE)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onCreate(db)
}
companion object {
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "my_database.db"
}
}
class CustomNotificationExtender : NotificationExtenderService() {
protected override fun onNotificationProcessing(receivedResult: OSNotificationReceivedResult): Boolean {
try {
if (receivedResult.isAppInFocus || receivedResult.restoring) {
Log.i("ExtenderIgnored", "App is in focus or restoring")
return false
}
val title = receivedResult.payload.title
val context = getBaseContext()
val dbHelper = MyDbHelper(context)
val db = dbHelper.getWritableDatabase()
val cursor = db.query(MyData.TABLE_NAME, null, "title = ?", arrayOf(title), null, null, null, null)
// Don't persist data if another record has the same title
if (cursor.count > 0) {
return false
}
val values = ContentValues()
values.put(MyData.COLUMN_NAME_TITLE, title)
db.insert(MyData.TABLE_NAME, null, values)
return false
} catch (e: Exception) {
// Report it to an external service if you wish
Log.e("NotificationExtenderServiceError", e.toString())
return false
}
}
}
Notes
Check OneSignal's documentation for more details.
If you're using a different solution or have any suggestions to improve this example, share them in the comments.
As this was my first time writing native Android code, if I've done something wrong or not recommended, please let me know.
I haven't implemented the same behaviour on iOS so if you have resources to link on this topic, share them in the comments.
I hope you enjoyed this post and follow me on any platform for more.