Dynamic UI in Android Using Kotlin
Image source: figma.com

Dynamic UI in Android Using Kotlin

Building a dynamic UI in Android is crucial when working with content that changes based on backend responses. This guide explains how to develop a UI that supports images, videos, text, and scrollable media (like carousels or reels) using Kotlin, Jetpack Compose, Retrofit, and sealed classes. The approach is similar to how platforms like Instagram, Facebook, and X handle media rendering.

So, before we begin, we also need help from backend developers to structure the API responses correctly. Now, let’s dive into building a dynamic media UI!

Overview

The backend provides data that specifies the content type — image video text or scrollable(for image carousels or reels). We use a custom Retrofit converter to map this data to sealed classes, allowing us to display the UI accordingly.

1. Defining the Sealed Class

We use a sealed class to represent different media types:

sealed class MediaType {
    data class Image(val url: String, val description: String, val likedBy: Int) : MediaType()
    data class Video(val url: String) : MediaType()
    data class Text(val content: String) : MediaType()
    data class Scrollable(val items: List<Image>) : MediaType()
    object Unknown : MediaType()
}        

2. Implementing a Custom Retrofit Converter with Gson

Instead of manually parsing JSON, we use Gson to handle the deserialization:

class MediaTypeConverterFactory(private val gson: Gson) : Converter.Factory() {
    override fun responseBodyConverter(
        type: Type, annotations: Array<out Annotation>, retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
        return Converter { responseBody ->
            try {
                val jsonArray = gson.fromJson(responseBody.charStream(), JsonArray::class.java)
                jsonArray.map { jsonElement ->
                    val jsonObject = jsonElement.asJsonObject
                    when (jsonObject.get("type")?.asString) {
                        "image" -> gson.fromJson(jsonObject, MediaType.Image::class.java)
                        "video" -> gson.fromJson(jsonObject, MediaType.Video::class.java)
                        "text" -> gson.fromJson(jsonObject, MediaType.Text::class.java)
                        "scrollable" -> MediaType.Scrollable(
                            gson.fromJson(jsonObject.get("items"), object : TypeToken<List<MediaType.Image>>() {}.type)
                        )
                        else -> MediaType.Unknown
                    }
                }
            } catch (e: JsonSyntaxException) {
                emptyList<MediaType>()
            }
        }
    }
}        

3. Setting Up Retrofit with Caching & Network Retry

To configure Retrofit with caching and retry support:

val gson = Gson()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(MediaTypeConverterFactory(gson))
    .client(okHttpClient)
    .build()
interface MediaService {
    @GET("media")
    suspend fun getMedia(@Query("page") page: Int): List<MediaType>
}        

To improve performance and handle network failures gracefully:

val cacheSize = (5 * 1024 * 1024).toLong() // 5 MB Cache
val cache = Cache(File(context.cacheDir, "http_cache"), cacheSize)

val okHttpClient = OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor { chain ->
        var request = chain.request()
        request = if (isNetworkAvailable(context)) {
            request.newBuilder().header("Cache-Control", "public, max-age=5").build()
        } else {
            request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=604800").build()
        }
        chain.proceed(request)
    }
    .retryOnConnectionFailure(true)
    .build()        

4. Managing Media Data in ViewModel with Pagination

To handle UI state effectively, we define a MediaUIState data class:

data class MediaUIState(
    val mediaList: List<MediaType> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
    val currentPage: Int = 1,
    val isEndReached: Boolean = false
)        

The MediaViewModel fetches media and supports pagination:

class MediaViewModel(private val mediaService: MediaService) : ViewModel() {
    private val _mediaState = MutableStateFlow(MediaUIState())
    val mediaState: StateFlow<MediaUIState> = _mediaState.asStateFlow()


    fun fetchMedia(page: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            _mediaState.value = _mediaState.value.copy(isLoading = true, error = null)
            try {
                val mediaList = mediaService.getMedia(page)
                _mediaState.value = _mediaState.value.copy(
                    mediaList = _mediaState.value.mediaList + mediaList,
                    isLoading = false,
                    currentPage = page,
                    isEndReached = mediaList.isEmpty()
                )
            } catch (e: IOException) {
                _mediaState.value = _mediaState.value.copy(isLoading = false, error = "Network error: ${e.message}")
            } catch (e: Exception) {
                _mediaState.value = _mediaState.value.copy(isLoading = false, error = "Unexpected error: ${e.message}")
            }
        }
    }
}        

5. Implementing Pagination in Jetpack Compose UI

@Composable
fun MediaScreen(viewModel: MediaViewModel = viewModel()) {
    val mediaState by viewModel.mediaState.collectAsStateWithLifecycle()
    val listState = rememberLazyListState()

    if (mediaState.isLoading && mediaState.mediaList.isEmpty()) {
        SkeletonLoader(modifier = Modifier.fillMaxWidth())
    }
    else LazyColumn(state = listState) {
        items(mediaState.mediaList) { media ->
            MediaItem(media)
        }
        if (mediaState.isLoading) {
            item { SkeletonLoader() }
        }
    }
    LaunchedEffect(listState) {
        snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
            .distinctUntilChanged()
            .collect { lastVisibleItemIndex ->
                if (!mediaState.isLoading && lastVisibleItemIndex == mediaState.mediaList.size - 1 && !mediaState.isEndReached) {
                    viewModel.fetchMedia(mediaState.currentPage + 1)
                }
            }
    }
}        

Summary

By using a custom Retrofit converter with Gson, we can automatically transform JSON responses into sealed class instances without manually parsing them in the ViewModel. We avoid unnecessary exceptions by handling errors properly, ensuring a smooth app experience. Caching and retry mechanisms improve performance, while MutableStateFlow providing efficient state management in Jetpack Compose. With MediaUIState, we track loading, errors, and pagination to enable seamless infinite scrolling.

要查看或添加评论,请登录

Evangelist Apps的更多文章

社区洞察