This Day In History Jetpack Compose App - Welcome Screen

Objective:

Build Translation API and real Welcome ViewModel, and connect the Welcome Screen to the real API. At the end of this exercise, the Welcome Screen will look and behave like below, executing the Translation API and providing real welcome message translation based on device's language.

Components built

We will add the following components:

  • Translation API.
  • Retrofit Translation API Factory.
  • Welcome ViewModel.

Code

Translation API

Add TranslationApi interface to the com.coroutines.api.translation package in the API module.


package com.coroutines.api.translation


import com.coroutines.data.models.TranslateResult
import kotlinx.coroutines.flow.Flow

interface TranslationApi {
    fun getTranslation(language: String, text: String): Flow
}

Add TranslationApiService interface to the com.coroutines.api.translation package in the API module.


package com.coroutines.api.translation

import com.coroutines.data.models.TranslateResult
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query

interface TranslationApiService {

    @GET("t?client=any_client_id_works&sl=auto&tbb=1&ie=UTF-8&oe=UTF-8")
    @Headers("Content-Type: application/json")
    suspend fun getTranslation(
        @Query("tl") language: String,
        @Query("q") text: String,
    ): TranslateResult

    companion object {
        const val BASE_URL = "https://translate.google.so/translate_a/"
    }
}

Add TranslationApiImpl class to the com.coroutines.api.translation package in the API module.


package com.coroutines.api.translation


import com.coroutines.data.models.TranslateResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

open class TranslationApiImpl(private val apiService: TranslationApiService) : TranslationApi {
    override fun getTranslation(language: String, text: String): Flow {
        return flow { emit(apiService.getTranslation(language = language, text = text)) }
    }
}

Welcome ViewModel

Add WelcomeViewModel class to the com.coroutines.thisdayinhistory.ui.viewmodels in the app project.


package com.coroutines.thisdayinhistory.ui.viewmodels

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.coroutines.api.translation.TranslationApi
import com.coroutines.data.models.TranslateRequestParams
import com.coroutines.thisdayinhistory.ui.state.WelcomeScreenUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout

class WelcomeViewModel
constructor(private val translationApiService: TranslationApi):
    ViewModel(), IWelcomeViewModel {
    private var _prompt: String = DEFAULT_LANGUAGE_PROMPT
    private val _screenState = MutableStateFlow(
        WelcomeScreenUiState.Initial(
            DEFAULT_WELCOME_MESSAGE
        )
    )
    override val uiScreenState: StateFlow
        get() = _screenState

    override val prompt by lazy {
        _prompt
    }

    override fun setDefaultLanguageWelcomeMessage(welcomeMessage: String){
        //this should really be [@assistedinject] when integrating with Hilt
        _screenState.value = WelcomeScreenUiState.Initial(welcomeMessage)
    }
    @Suppress("TooGenericExceptionCaught")
    override fun translate(translateRequestParams: TranslateRequestParams){
        viewModelScope.launch {
            _screenState.value =
                WelcomeScreenUiState.WaitingForTranslation(translateRequestParams.welcomeText)
            try {
                withTimeout(TIMEOUT_MS) {
                    translationApiService
                        .getTranslation(
                            translateRequestParams.language,
                            "${translateRequestParams.welcomeText}|${translateRequestParams.languagePrompt}"
                        )
                        .map {
                            it.getResult()
                        }
                        .catch {
                            _prompt = translateRequestParams.languagePrompt
                            _screenState.value =
                                WelcomeScreenUiState.Error(it.message ?: DEFAULT_ERROR_MESSAGE)
                        }
                        .collect { resultState ->
                            _prompt = resultState.substringAfter("|")
                            val welcomeMessage = resultState.substringBefore("|")
                            _screenState.value = WelcomeScreenUiState.Success(welcomeMessage)
                        }
                }
            }
            catch (e: Exception){
                _screenState.value = WelcomeScreenUiState.Error(e.message ?: DEFAULT_ERROR_MESSAGE)
            }
        }
    }

    companion object {
        private const val DEFAULT_ERROR_MESSAGE  = "Something went wrong. Please try again later."
        private const val DEFAULT_WELCOME_MESSAGE  = "History Cat"
        private const val DEFAULT_LANGUAGE_PROMPT  =  "Please choose your language"
        private const val TIMEOUT_MS = 3000L
    }
}

Retrofit

And RetrofitTranslationApiFactory to the com.coroutines.thisdayinhistory.ui.screens.welcome in the app module. Note: later on, we will add Hilt for dependency injection, and all dependencies including Translation API will be provided via Hilt. For now, to keep it simple, we will inject manually, and this RetrofitTranslationApiFactory is a temporary soluton, to be removed in the future.


package com.coroutines.thisdayinhistory.ui.screens.welcome

object RetrofitTranslationApiFactory {

    val baseUrl = TranslationApiService.BASE_URL

    fun getInstance(): Retrofit {
        return Retrofit.Builder().baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

Welcome Screen Composable

Modify the WelcomeScreen composable to accept a manually constructed TranslationAPIImpl.


package com.coroutines.thisdayinhistory.ui.screens.welcome

@Composable
fun WelcomeScreen (
    navController: NavController = rememberNavController(),
    settings: AppConfigurationState,
    viewModel: IWelcomeViewModel = WelcomeViewModel(TranslationApiImpl(RetrofitTranslationApiFactory.getInstance().create(
        TranslationApiService::class.java)))
) { //.... code