- Swift SDK README: Installation, Broadcast/Survey clients, SwiftUI integration, push notifications - Kotlin SDK README: Gradle setup, Jetpack Compose components, FCM integration
16 KiB
16 KiB
ByteLyst Platform SDK — Android (Kotlin)
Kotlin SDK for the ByteLyst platform. Provides broadcast messaging, surveys, authentication, telemetry, and more.
Installation
Gradle
Add to your build.gradle.kts:
dependencies {
implementation("com.bytelyst:platform-sdk:1.0.0")
}
Maven
<dependency>
<groupId>com.bytelyst</groupId>
<artifactId>platform-sdk</artifactId>
<version>1.0.0</version>
</dependency>
Quick Start
import com.bytelyst.platform.*
// Configure the SDK
val config = BLPlatformConfig(
productId = "lysnrai",
baseURL = "https://api.bytelyst.io/v1",
getAuthToken = { authRepository.getToken() }
)
// Create clients
val broadcastClient = BLBroadcastClient(config)
val surveyClient = BLSurveyClient(config)
Broadcast Client
Basic Usage
import com.bytelyst.platform.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class BroadcastManager(
private val client: BLBroadcastClient
) {
private val _messages = MutableStateFlow<List<InAppMessage>>(emptyList())
val messages: StateFlow<List<InAppMessage>> = _messages
private val _unreadCount = MutableStateFlow(0)
val unreadCount: StateFlow<Int> = _unreadCount
fun startListening() {
client.startPolling(60000L) { messages ->
_messages.value = messages
_unreadCount.value = messages.count { it.status == MessageStatus.UNREAD }
}
}
fun stopListening() {
client.stopPolling()
}
suspend fun markRead(messageId: String) {
client.markRead(messageId)
}
suspend fun dismiss(messageId: String) {
client.markDismissed(messageId)
}
suspend fun handleTap(message: InAppMessage) {
client.trackClick(message.id)
message.ctaUrl?.let { url ->
// Open URL with your navigation system
navigationService.openUrl(url)
}
markRead(message.id)
}
}
Jetpack Compose Integration
import com.bytelyst.platform.ui.*
@Composable
fun AppContent() {
val broadcastManager = remember { BroadcastManager(broadcastClient) }
val messages by broadcastManager.messages.collectAsState()
val unreadCount by broadcastManager.unreadCount.collectAsState()
LaunchedEffect(Unit) {
broadcastManager.startListening()
}
Scaffold(
topBar = {
// Banner for unread messages
InAppMessageBanner(
client = broadcastClient,
position = BannerPosition.TOP
)
}
) { padding ->
MainContent(modifier = Modifier.padding(padding))
}
}
Modal Messages
@Composable
fun AppRoot() {
val broadcastClient = remember { BLBroadcastClient(config) }
Box(modifier = Modifier.fillMaxSize()) {
NavigationHost()
// Modal overlay
BroadcastModal(client = broadcastClient)
}
}
Survey Client
Basic Usage
import com.bytelyst.platform.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class SurveyManager(
private val client: BLSurveyClient
) {
private val _activeSurvey = MutableStateFlow<ActiveSurvey?>(null)
val activeSurvey: StateFlow<ActiveSurvey?> = _activeSurvey
private val _currentQuestionIndex = MutableStateFlow(0)
val currentQuestionIndex: StateFlow<Int> = _currentQuestionIndex
private val _answers = MutableStateFlow<Map<String, SurveyAnswer>>(emptyMap())
val answers: StateFlow<Map<String, SurveyAnswer>> = _answers
private val _isComplete = MutableStateFlow(false)
val isComplete: StateFlow<Boolean> = _isComplete
suspend fun checkForSurveys() {
val result = client.getActiveSurvey()
result.onSuccess { survey ->
survey?.let {
_activeSurvey.value = it
client.startSurvey(it.id)
}
}
}
suspend fun submitAnswer(question: Question, answer: SurveyAnswer) {
val survey = _activeSurvey.value ?: return
val result = client.submitAnswer(
surveyId = survey.id,
questionId = question.id,
answer = answer
)
result.onSuccess { response ->
_currentQuestionIndex.value = response.currentQuestionIndex
_answers.value = _answers.value + (question.id to answer)
if (response.isComplete) {
completeSurvey()
}
}
}
suspend fun completeSurvey() {
val survey = _activeSurvey.value ?: return
val result = client.completeSurvey(survey.id)
result.onSuccess { completion ->
if (completion.success) {
_isComplete.value = true
if (completion.incentiveClaimed) {
showIncentiveToast(
amount = completion.incentiveAmount,
type = completion.incentiveType
)
}
}
}
}
suspend fun dismiss() {
val survey = _activeSurvey.value ?: return
client.dismissSurvey(survey.id)
_activeSurvey.value = null
_currentQuestionIndex.value = 0
_answers.value = emptyMap()
}
}
Jetpack Compose Survey Modal
@Composable
fun AppRoot() {
val surveyClient = remember { BLSurveyClient(config) }
Box(modifier = Modifier.fillMaxSize()) {
NavigationHost()
// Survey modal overlay
SurveyModal(client = surveyClient)
}
}
Custom Survey UI
@Composable
fun CustomSurveyView(
manager: SurveyManager = viewModel()
) {
val survey by manager.activeSurvey.collectAsState()
val currentIndex by manager.currentQuestionIndex.collectAsState()
val isComplete by manager.isComplete.collectAsState()
if (survey != null && !isComplete) {
val question = survey!!.questions[currentIndex]
val isLast = currentIndex == survey!!.questions.size - 1
Column(modifier = Modifier.padding(16.dp)) {
// Progress
LinearProgressIndicator(
progress = (currentIndex + 1) / survey!!.questions.size.toFloat(),
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Question ${currentIndex + 1} of ${survey!!.questions.size}",
style = MaterialTheme.typography.labelSmall
)
// Question
Text(
text = question.text,
style = MaterialTheme.typography.titleLarge
)
question.description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Answer input based on type
QuestionInput(
question = question,
onAnswer = { answer ->
coroutineScope.launch {
manager.submitAnswer(question, answer)
}
}
)
// Navigation
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
if (!question.required) {
TextButton(onClick = { /* Skip */ }) {
Text("Skip")
}
}
Button(
onClick = { /* Submit */ },
enabled = canSubmit(question)
) {
Text(if (isLast) "Complete" else "Next")
}
}
}
}
}
@Composable
fun QuestionInput(
question: Question,
onAnswer: (SurveyAnswer) -> Unit
) {
when (question.type) {
QuestionType.SINGLE_CHOICE, QuestionType.DROPDOWN -> {
var selected by remember { mutableStateOf<String?>(null) }
Column {
question.options?.forEach { option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
selected = option.id
onAnswer(SurveyAnswer(
type = "single_choice",
value = JsonObject(mapOf("value" to JsonPrimitive(option.id)))
))
}
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = option.emoji ?: "")
Text(text = option.text, modifier = Modifier.weight(1f))
RadioButton(
selected = selected == option.id,
onClick = null
)
}
}
}
}
QuestionType.MULTIPLE_CHOICE -> {
var selected by remember { mutableStateOf<Set<String>>(emptySet()) }
Column {
question.options?.forEach { option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
selected = if (selected.contains(option.id)) {
selected - option.id
} else {
selected + option.id
}
onAnswer(SurveyAnswer(
type = "multiple_choice",
value = JsonObject(mapOf("values" to JsonArray(selected.map { JsonPrimitive(it) })))
))
}
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = option.emoji ?: "")
Text(text = option.text, modifier = Modifier.weight(1f))
Checkbox(
checked = selected.contains(option.id),
onCheckedChange = null
)
}
}
}
}
QuestionType.NPS, QuestionType.RATING, QuestionType.SCALE -> {
var rating by remember { mutableIntStateOf(0) }
val minValue = question.minValue ?: if (question.type == QuestionType.NPS) 0 else 1
val maxValue = question.maxValue ?: if (question.type == QuestionType.NPS) 10 else 5
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
(minValue..maxValue).forEach { value ->
Button(
onClick = {
rating = value
onAnswer(SurveyAnswer(
type = "rating",
value = JsonObject(mapOf("value" to JsonPrimitive(value)))
))
},
colors = ButtonDefaults.buttonColors(
containerColor = if (rating == value) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
)
) {
Text("$value")
}
}
}
}
QuestionType.TEXT_SHORT -> {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = {
text = it
onAnswer(SurveyAnswer(
type = "text",
value = JsonObject(mapOf("value" to JsonPrimitive(it)))
))
},
modifier = Modifier.fillMaxWidth()
)
}
QuestionType.TEXT_LONG -> {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = {
text = it
onAnswer(SurveyAnswer(
type = "text",
value = JsonObject(mapOf("value" to JsonPrimitive(it)))
))
},
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 120.dp),
maxLines = 6
)
}
else -> { /* Handle other types */ }
}
}
Push Notifications
Firebase Cloud Messaging
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.bytelyst.platform.*
class PushNotificationService : FirebaseMessagingService() {
private lateinit var broadcastClient: BLBroadcastClient
override fun onCreate() {
super.onCreate()
val config = BLPlatformConfig(
productId = "lysnrai",
baseURL = "https://api.bytelyst.io/v1",
getAuthToken = { authRepository.getToken() }
)
broadcastClient = BLBroadcastClient(config)
}
override fun onNewToken(token: String) {
super.onNewToken(token)
// Register device token
coroutineScope.launch {
broadcastClient.registerDeviceToken(token, Platform.ANDROID)
}
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
// Handle broadcast notification
message.data["broadcastId"]?.let { broadcastId ->
showNotification(message)
}
}
}
Register Device Token
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
coroutineScope.launch {
broadcastClient.registerDeviceToken(token, Platform.ANDROID)
}
}
}
Error Handling
val result = client.getActiveSurvey()
result.onSuccess { survey ->
survey?.let { showSurvey(it) }
}.onError { error ->
when (error) {
is BLApiError.Unauthorized -> {
// Re-authenticate user
authRepository.refreshToken()
}
is BLApiError.NetworkError -> {
// Retry with exponential backoff
Log.w("Survey", "Network error, will retry")
}
is BLApiError.ServerError -> {
Log.e("Survey", "Server error: ${error.code}")
}
else -> {
Log.e("Survey", "Unknown error: ${error.message}")
}
}
}
Configuration
Environment-Based Config
sealed class Environment(
val baseURL: String
) {
object Development : Environment("http://localhost:4003")
object Staging : Environment("https://api-staging.bytelyst.io/v1")
object Production : Environment("https://api.bytelyst.io/v1")
}
val config = BLPlatformConfig(
productId = "lysnrai",
baseURL = Environment.Production.baseURL,
getAuthToken = { authRepository.getToken() },
enableLogging = BuildConfig.DEBUG
)
Offline Support
The SDK automatically caches survey responses offline:
val client = BLSurveyClient(
config = config,
enableOfflineCache = true // Enabled by default
)
// Responses are queued when offline
// Flush manually or on network restore
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
coroutineScope.launch {
client.flushOfflineQueue()
}
}
})
Requirements
- Android 8.0+ (API 26+)
- Kotlin 1.9+
- Jetpack Compose (optional, for UI components)
License
MIT © ByteLyst