# 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`: ```kotlin dependencies { implementation("com.bytelyst:platform-sdk:1.0.0") } ``` ### Maven ```xml com.bytelyst platform-sdk 1.0.0 ``` ## Quick Start ```kotlin 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 ```kotlin import com.bytelyst.platform.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class BroadcastManager( private val client: BLBroadcastClient ) { private val _messages = MutableStateFlow>(emptyList()) val messages: StateFlow> = _messages private val _unreadCount = MutableStateFlow(0) val unreadCount: StateFlow = _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 ```kotlin 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 ```kotlin @Composable fun AppRoot() { val broadcastClient = remember { BLBroadcastClient(config) } Box(modifier = Modifier.fillMaxSize()) { NavigationHost() // Modal overlay BroadcastModal(client = broadcastClient) } } ``` ## Survey Client ### Basic Usage ```kotlin import com.bytelyst.platform.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class SurveyManager( private val client: BLSurveyClient ) { private val _activeSurvey = MutableStateFlow(null) val activeSurvey: StateFlow = _activeSurvey private val _currentQuestionIndex = MutableStateFlow(0) val currentQuestionIndex: StateFlow = _currentQuestionIndex private val _answers = MutableStateFlow>(emptyMap()) val answers: StateFlow> = _answers private val _isComplete = MutableStateFlow(false) val isComplete: StateFlow = _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 ```kotlin @Composable fun AppRoot() { val surveyClient = remember { BLSurveyClient(config) } Box(modifier = Modifier.fillMaxSize()) { NavigationHost() // Survey modal overlay SurveyModal(client = surveyClient) } } ``` ### Custom Survey UI ```kotlin @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(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>(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 ```kotlin 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 ```kotlin FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (task.isSuccessful) { val token = task.result coroutineScope.launch { broadcastClient.registerDeviceToken(token, Platform.ANDROID) } } } ``` ## Error Handling ```kotlin 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 ```kotlin 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: ```kotlin 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