Skip to content

Instantly share code, notes, and snippets.

@aadilmallick
Last active February 11, 2024 23:47
Show Gist options
  • Save aadilmallick/884b3c4151453f277f301d40b91b0e97 to your computer and use it in GitHub Desktop.
Save aadilmallick/884b3c4151453f277f301d40b91b0e97 to your computer and use it in GitHub Desktop.

Android recipes

Notification Model

Here i wanted to create a resuable way to create notifications and notification channels. In API 33+ you have to create your channel in an Application() context, but below that level, you can create it wherever.

We pass in the context to have reusable notification models we can use anywhere in our code, so that a notification model isn't just tied down to one activity or service. I want receivers, activities, and services all to receive the same model. The context doesn't really matter anyway.

import android.app.NotificationManager
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat

class NotificationsModelV2(private val CHANNEL_ID: String, private val NOTIFICATION_ID: Int, private val CHANNEL_NAME: String) {
    private var channelIsCreated = false

    fun createNotification(context: Context, title: String, body: String) {
        if (!channelIsCreated) {
            createNotificationsChannel(context)
        }
        
        // build notification
        val notification = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .build()

        // send notification
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(NOTIFICATION_ID, notification)
    }

    fun createNotificationsChannel(context: Context) {
        val channel = android.app.NotificationChannel(
            CHANNEL_ID,
            CHANNEL_NAME,
            NotificationManager.IMPORTANCE_HIGH
        )
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
        channelIsCreated = true
    }
}

Then here is the object that we use to keep all of our notification models in one place.

object NotificationsGlobalObject {
    private var NOTIFICATION_ID = 10
    val messageNotificationsModel =
        NotificationsModelV2(
            "alarm-manager-practice",
            getNotificationId(),
            "Alarm Manager Practice Notifications"
        )
    private fun getNotificationId(): Int {
        return NOTIFICATION_ID++
    }
}

With auto-incrementing notification ids we ensure that we never run into a conflict.

Custom API requester

You need to install the GSON dependency to convert strings into JSON.

// install gson
implementation("com.google.code.gson:gson:2.10")

These two classes use OKHttp and generics to provide type-safe requesting of JSON APIs. They are reusable and easy to use. These are the basic steps you'll take to use them:

  1. Choose which type of data you're going to get back: array or object. Then choose the class you want accordingly
  2. Provide url and data class that represents fetched data

Here is the class:

package com.example.coffeemasters

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import okhttp3.OkHttpClient
import okhttp3.Request

data class Todo(
    val userId: Int,
    val id: Int,
    val title: String,
    val completed: Boolean
)

class APIRequesterObjects<T>(public var url: String, private var TClass: Class<T>) {
    private val client = OkHttpClient()

    private val request = Request.Builder()
        .url(url)
        .build()

    suspend fun getData(): T? {
        val response = withContext(Dispatchers.IO) {
            client.newCall(request).execute()
        }

        val responseData = response.body?.string()
        if (responseData == null) {
            return null
        }

        // Deserialize JSON string using Gson
        val gson = Gson()
        return gson.fromJson(responseData, TClass) as T
    }
}

class APIRequesterArrays<T>(
    private val url: String,
    private val typeToken: TypeToken<List<T>>
) {
    private val client = OkHttpClient()

    private val request = Request.Builder()
        .url(url)
        .build()
    
    companion object {
        fun <T> createClient(url: String, TClass: Class<T>) : APIRequesterArrays<T> {
            val typeToken = object : TypeToken<List<T>>() {}
            return APIRequesterArrays<T>(
                url = url,
                typeToken = typeToken
            )
        }
    }

    suspend fun getData(): List<T>? {
        val response = withContext(Dispatchers.IO) {
            client.newCall(request).execute()
        }

        val responseData = response.body?.string()
        if (responseData == null) {
            return null
        }

        // Deserialize JSON string using Gson
        val gson = Gson()
        return gson.fromJson(responseData, typeToken.type)
    }
}

Then here is how you would use the object version, with steps:

  1. Create default state to represent fetched data
  2. Create an instance of the api requester
  3. Run a LaunchedEffect {}, which allows you to use coroutines. Use the IO thread to make the network call, and then use the main thread to update the state. Make sure to use state.value with the value property, otherwise it doesn't work for some reason.
@Destination
@Composable
fun SecondScreen(
    args: SecondScreenArgs
) {
    // 1. instantiate
    val apiRequesterTodo = APIRequesterObjects(
        url = "https://jsonplaceholder.typicode.com/todos/1",
        TClass = Todo::class.java
    )
    
    // 2. create default state
    var todo = remember {
        mutableStateOf(Todo(0, 0, "title", false))
    }
    
    // 3. create launched effect that fetches in IO thread, 
    // then updates UI in main thread
    LaunchedEffect(key1 = Unit) {
        val todoData = withContext(Dispatchers.IO) {
            apiRequesterTodo.getData()
        }
        todo.value = todoData!!
    }


    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
            .padding(16.dp)
    ) {
        Text(text = "Second Screen message from first screen ${args.someArg}")
        Text(text = "Todo: ${todo.value.toString()}")
    }
}

To request arrays, it's a bit different:

// 1. create type token and pass in list of data class
val typeToken = object : TypeToken<List<Todo>>() {}
// 2. create instance
val apiRequesterTodo = APIRequesterArrays<Todo>(
    url = "https://jsonplaceholder.typicode.com/todos",
    typeToken = typeToken
)
// 3. create list state
var todos = remember {
    mutableStateListOf<Todo>()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment