Geliştiriciler · API v1.0.0
Kotlin Entegrasyonu (Android)
Bu rehber Kotlin ile geliştirilen native Android uygulamaları içindir (klasik View sistemi veya Jetpack Compose). Java kullanıyorsanız Android (Java) rehberine bakın. Genel API referansı için Genel bakış.
Desteklenen sürümler
| Bileşen | Minimum | Önerilen |
|---|---|---|
| Android | 6.0 (API 23, minSdk 23) |
targetSdk / compileSdk 35+ |
| Kotlin | 1.9 | 2.x |
| Coroutines | 1.7 | güncel |
| Android Gradle Plugin | 8.0 | güncel |
| Firebase BoM | 33.x | güncel |
Nasıl çalışır?
NotiPilot 1.0.0 bildirimleri Expo Push Service üzerinden teslim eder. Uygulamanız FCM token'ını alır, bunu bir Expo Push Token'a dönüştürür ve NotiPilot'a kaydeder. Bildirimler FCM üzerinden cihaza ulaşır.
1. Ön hazırlık (bir kez)
- Firebase konsolunda Android uygulamanızı ekleyin,
google-services.jsondosyasınıapp/klasörüne koyun. - expo.dev üzerinde bir proje oluşturun ve proje ID'sini (UUID) not edin. Uygulamanızın Expo ile yazılmış olması gerekmez.
- Firebase → Proje Ayarları → Hizmet Hesapları → Yeni özel anahtar oluştur ile indirdiğiniz JSON'u expo.dev → Proje → Credentials → Android → FCM V1 service account key alanına yükleyin.
- NotiPilot panelinde: Expo Project ID = Expo proje ID'si, Expo Access Token = expo.dev → Access Tokens, Android Package =
applicationId.
2. Gradle (Kotlin DSL)
build.gradle.kts (proje):
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
}
app/build.gradle.kts:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.gms.google-services")
}
android {
defaultConfig {
minSdk = 23
buildConfigField("String", "NOTIPILOT_BASE_URL", "\"https://app.notipilot.com/api/v1\"")
buildConfigField("String", "NOTIPILOT_APP_ID", "\"EXPO-PROJE-ID-NIZ\"")
}
buildFeatures { buildConfig = true }
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
implementation("com.google.firebase:firebase-messaging")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0")
}
3. AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application ...>
<service
android:name=".push.NotiPilotMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="default" />
</application>
4. NotiPilot.kt
package com.example.app.push
import android.content.Context
import android.os.Build
import com.example.app.BuildConfig
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.util.Locale
import java.util.UUID
object NotiPilot {
private val http = OkHttpClient()
private val JSON = "application/json; charset=utf-8".toMediaType()
fun deviceUid(context: Context): String {
val prefs = context.getSharedPreferences("notipilot", Context.MODE_PRIVATE)
return prefs.getString("device_uid", null) ?: UUID.randomUUID().toString().also {
prefs.edit().putString("device_uid", it).apply()
}
}
/** Uygulama açılışında (izin verildikten sonra) ve onNewToken'da çağırın. */
suspend fun registerDevice(
context: Context,
fcmToken: String? = null,
extraAttributes: Map<String, Any?> = emptyMap(),
): JSONObject = withContext(Dispatchers.IO) {
val uid = deviceUid(context)
val token = fcmToken ?: FirebaseMessaging.getInstance().token.await()
val expoToken = exchangeForExpoToken(context, uid, token)
val attributes = JSONObject().apply {
put("locale", Locale.getDefault().language) // "tr"
put("country", Locale.getDefault().country) // "TR"
put("app_version", appVersion(context))
put("os_version", Build.VERSION.RELEASE)
extraAttributes.forEach { (k, v) -> put(k, v ?: JSONObject.NULL) }
}
post("/register-device", JSONObject().apply {
put("app_id", BuildConfig.NOTIPILOT_APP_ID)
put("device_uid", uid)
put("token", expoToken)
put("platform", "android")
put("provider", "expo")
put("attributes", attributes)
})
}
/** Kullanıcı giriş yaptığında çağırın. */
suspend fun identify(context: Context, externalId: String, attributes: Map<String, Any?>? = null) =
withContext(Dispatchers.IO) {
post("/identify-device", JSONObject().apply {
put("app_id", BuildConfig.NOTIPILOT_APP_ID)
put("device_uid", deviceUid(context))
put("external_id", externalId)
attributes?.let { put("attributes", JSONObject(it)) }
})
}
/** FCM token'ını Expo Push Token'a dönüştürür. */
private fun exchangeForExpoToken(context: Context, uid: String, fcmToken: String): String {
val body = JSONObject().apply {
put("type", "fcm")
put("deviceId", uid.lowercase())
put("development", false)
put("appId", context.packageName)
put("deviceToken", fcmToken)
put("projectId", BuildConfig.NOTIPILOT_APP_ID)
}
val request = Request.Builder()
.url("https://exp.host/--/api/v2/push/getExpoPushToken")
.post(body.toString().toRequestBody(JSON))
.build()
http.newCall(request).execute().use { res ->
val json = JSONObject(res.body?.string().orEmpty().ifBlank { "{}" })
check(res.isSuccessful) { "Expo token exchange failed: ${res.code} $json" }
return json.getJSONObject("data").getString("expoPushToken")
}
}
private suspend fun post(path: String, body: JSONObject, attempt: Int = 0): JSONObject {
val request = Request.Builder()
.url(BuildConfig.NOTIPILOT_BASE_URL + path)
.post(body.toString().toRequestBody(JSON))
.build()
val (code, json) = http.newCall(request).execute().use { res ->
res.code to JSONObject(res.body?.string().orEmpty().ifBlank { "{}" })
}
if ((code == 429 || code >= 500) && attempt < 3) {
delay(json.optLong("retry_after", 1L shl attempt) * 1000)
return post(path, body, attempt + 1)
}
return json
}
private fun appVersion(context: Context): String =
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: ""
}
5. NotiPilotMessagingService.kt
package com.example.app.push
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.example.app.MainActivity
import com.example.app.R
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.json.JSONObject
class NotiPilotMessagingService : FirebaseMessagingService() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onNewToken(token: String) {
scope.launch { runCatching { NotiPilot.registerDevice(applicationContext, token) } }
}
override fun onMessageReceived(message: RemoteMessage) {
// Expo içeriği `notification` bloğunda veya `data` içinde (title / message / body) gönderebilir.
val title = message.notification?.title ?: message.data["title"] ?: return
val body = message.notification?.body ?: message.data["message"].orEmpty()
// Panelden gönderilen özel veri `data["body"]` alanında JSON string olarak gelir.
val custom = message.data["body"]?.let { runCatching { JSONObject(it) }.getOrNull() }
show(this, title, body, custom)
}
companion object {
const val CHANNEL_ID = "default"
fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, "Genel", NotificationManager.IMPORTANCE_HIGH)
context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}
fun show(context: Context, title: String, body: String, data: JSONObject?) {
createChannel(context)
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
data?.keys()?.forEach { key -> putExtra(key, data.optString(key)) }
}
val id = System.currentTimeMillis().toInt()
val pending = PendingIntent.getActivity(
context, id, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(pending)
.build()
val manager = NotificationManagerCompat.from(context)
if (manager.areNotificationsEnabled()) manager.notify(id, notification)
}
}
}
6. İzin ve kayıt
Jetpack Compose
@Composable
fun NotiPilotSetup() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
fun register() = scope.launch {
runCatching { NotiPilot.registerDevice(context, extraAttributes = mapOf("city" to "Istanbul")) }
.onFailure { Log.w("NotiPilot", "register failed", it) }
}
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) register()
}
LaunchedEffect(Unit) {
NotiPilotMessagingService.createChannel(context)
if (Build.VERSION.SDK_INT >= 33 &&
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
register()
}
}
}
Activity (View sistemi)
class MainActivity : AppCompatActivity() {
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) register()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NotiPilotMessagingService.createChannel(this)
if (Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
register()
}
// Bildirime tıklanarak açıldıysa özel veriler intent extras içindedir
intent.getStringExtra("screen")?.let { /* yönlendirme */ }
}
private fun register() = lifecycleScope.launch {
runCatching { NotiPilot.registerDevice(this@MainActivity) }
.onFailure { Log.w("NotiPilot", "register failed", it) }
}
}
Kullanıcı giriş yaptığında: NotiPilot.identify(context, user.id) (bir coroutine içinde).
Kontrol listesi
-
google-services.jsoneklendi, FCM V1 servis hesabı anahtarı Expo projesine yüklendi - Paneldeki Expo Project ID =
NOTIPILOT_APP_ID - Paneldeki Android Package =
applicationId - Android 13+ için
POST_NOTIFICATIONSizni isteniyor -
defaultbildirim kanalı oluşturuluyor -
onNewTokeniçinderegisterDeviceçağrılıyor -
countryve mümkünsecityattributes olarak gönderiliyor
Sorun giderme için Android rehberindeki tabloya bakın.