Integrate Ktor in Kotlin Multiplatform in a structured way

First of all declare internet permission in AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>Code language: HTML, XML (xml)
const val BASE_URL = ""
fun constructUrl(url: String): String {
return when {
url.contains(BASE_URL) -> url
url.startsWith("/") -> BASE_URL + url.drop(1)
else -> BASE_URL + url
}
}

Create an object file named HttpClientFactory.kt

import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

object HttpClientFactory {

    fun create(engine: HttpClientEngine): HttpClient {
        return HttpClient(engine) {
            install(ContentNegotiation) {
                json(
                    json = Json {
                        ignoreUnknownKeys = true
                    }
                )
            }
            install(HttpTimeout) {
                socketTimeoutMillis = 20_000L
                requestTimeoutMillis = 20_000L
            }
            install(Logging) {
                logger = object : Logger {
                    override fun log(message: String) {
                        println(message)
                    }
                }
                level = LogLevel.ALL
            }
            defaultRequest {
                contentType(ContentType.Application.Json)
            }
        }
    }
}Code language: JavaScript (javascript)

Create a domain directory and add the following files

interface ErrorCode language: JavaScript (javascript)

Create a file named Result.kt

sealed interface Result<out D, out E: Error> {
    data class Success<out D>(val data: D): Result<D, Nothing>
    data class Error<out E: com.abdulmateen.mycmptemplate.core.domain.Error>(val error: E):
        Result<Nothing, E>
}

inline fun <T, E: Error, R> Result<T, E>.map(map: (T) -> R): Result<R, E> {
    return when(this) {
        is Result.Error -> Result.Error(error)
        is Result.Success -> Result.Success(map(data))
    }
}

fun <T, E: Error> Result<T, E>.asEmptyDataResult(): EmptyResult<E> {
    return map {  }
}

inline fun <T, E: Error> Result<T, E>.onSuccess(action: (T) -> Unit): Result<T, E> {
    return when(this) {
        is Result.Error -> this
        is Result.Success -> {
            action(data)
            this
        }
    }
}
inline fun <T, E: Error> Result<T, E>.onError(action: (E) -> Unit): Result<T, E> {
    return when(this) {
        is Result.Error -> {
            action(error)
            this
        }
        is Result.Success -> this
    }
}

typealias EmptyResult<E> = Result<Unit, E>Code language: HTML, XML (xml)

Create a file named DataError.kt

sealed interface DataError : Error {
    enum class Remote: DataError {
        REQUEST_TIMEOUT,
        TOO_MANY_REQUESTS,
        NO_INTERNET,
        SERVER,
        SERIALIZATION,
        UNKNOWN
    }
 enum class Local : DataError {
        DISK_FULL,
        UNKNOWN
    }
}

iInitiate Ktor with the help of Koin Dependency Injection.
iIn CommonMain module

actual val platformModule: Module = module {
    single<HttpClientEngine> { OkHttp.create() }
}Code language: HTML, XML (xml)

In AndroidMain module

actual val platformModule: Module = module {
    single<HttpClientEngine> { OkHttp.create() }
}Code language: HTML, XML (xml)

In iosMain module

actual val platformModule: Module
    get() = module {
        single<HttpClientEngine> { Darwin.create() }
    }Code language: JavaScript (javascript)

in jvmMain module

actual val platformModule: Module
    get() = module {
        single<HttpClientEngine> { OkHttp.create() }
    }Code language: JavaScript (javascript)

Error handling in presentation, available inside core directory

Update your values/strings.xml inside commonMain module

sealed interface UiText {
    data class DynamicString(val value: String): UiText
    class StringResourceId(
        val id: StringResource,
        val args: Array<Any> = arrayOf()
    ): UiText

    @Composable
    fun asString(): String {
        return when(this) {
            is DynamicString -> value
            is StringResourceId -> stringResource(resource = id, formatArgs = args)
        }
    }
}Code language: HTML, XML (xml)
<string name="error_unknown">Oops, something went wrong.</string>
<string name="error_request_timeout">The request timed out.</string>
<string name="error_no_internet">Couldn't reach server, please check your internet connection.</string>
<string name="error_too_many_requests">Your quota seems to be exceeded.</string>
<string name="error_serialization">Couldn't parse data.</string>Code language: HTML, XML (xml)

Add file in core/presentation named DataErrorToStringResource.kt

fun DataError.toUiText(): UiText {
    val stringRes = when(this) {
        DataError.Local.DISK_FULL -> Res.string.error_disk_full
        DataError.Local.UNKNOWN -> Res.string.error_unknown
        DataError.Remote.REQUEST_TIMEOUT -> Res.string.error_request_timeout
        DataError.Remote.TOO_MANY_REQUESTS -> Res.string.error_too_many_requests
        DataError.Remote.NO_INTERNET -> Res.string.error_no_internet
        DataError.Remote.SERVER -> Res.string.error_unknown
        DataError.Remote.SERIALIZATION -> Res.string.error_serialization
        DataError.Remote.UNKNOWN -> Res.string.error_unknown
    }

    return UiText.StringResourceId(stringRes)
}Code language: JavaScript (javascript)
private const val BASE_URL = "https://fakestoreapi.com/"

class KtorUserDataSource(
    private val httpClient: HttpClient
): RemoteUserDataSource {
    override suspend fun login(
        username: String,
        password: String
    ): Result<LoginResponseDto, DataError.Remote> {
        return safeCall<LoginResponseDto> {
            httpClient.post(
                urlString = BASE_URL + "auth/login"
            ) {
                setBody(
                    LoginRequestDto(
                        username = username,
                        password = password
                    )
                )
            }
        }
    }
}Code language: HTML, XML (xml)
class LoginRepositoryImpl(
    private val remoteUserDataSource: RemoteUserDataSource
): LoginRepository {
    override suspend fun login(
        username: String,
        password: String
    ): Result<String, DataError.Remote> {
        return remoteUserDataSource.login(username, password).map { it.token }
    }
}Code language: HTML, XML (xml)
class LoginViewModel(
    private val repository: LoginRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState = _uiState.asStateFlow()


    fun uiAction(action: LoginUiAction) {
        when(action){
            LoginUiAction.OnLoginClicked -> { validateField() }
            is LoginUiAction.UpdateLoadingStatus -> {
                _uiState.update {
                    it.copy(
                        isLoading = action.loading
                    )
                }
            }
            is LoginUiAction.UpdatePassword -> {
                _uiState.update {
                    it.copy(
                        password = action.text
                    )
                }
            }
            is LoginUiAction.UpdatePasswordErrorStatus -> {
                _uiState.update {
                    it.copy(
                        hasPasswordError = action.hasError,
                        passwordErrorMessage = action.errorMessage
                    )
                }
            }
            is LoginUiAction.UpdateUsername -> {
                _uiState.update {
                    it.copy(
                        username = action.text
                    )
                }
            }
            is LoginUiAction.UpdateUsernameErrorStatus -> {
                _uiState.update {
                    it.copy(
                        hasUsernameError = action.hasError,
                        usernameErrorMessage = action.errorMessage
                    )
                }
            }

            LoginUiAction.TogglePasswordVisibility -> {
                _uiState.update {
                    it.copy(
                        passwordVisibility = !it.passwordVisibility
                    )
                }
            }
            LoginUiAction.ToggleAlertDialog -> {
                _uiState.update {
                    it.copy(
                        isAlertDialogOpened = !it.isAlertDialogOpened
                    )
                }
            }
        }
    }

    private fun validateField() {
        viewModelScope.launch {
            _uiState.update {
                it.copy(
                    isLoading = true
                )
            }
            val isUsernameValid = Validator.validateNonEmpty(_uiState.value.username)
            if (!isUsernameValid.isValid)
                return@launch

            val isPasswordValid = Validator.validateNonEmpty(_uiState.value.password)
            if (!isPasswordValid.isValid)
                return@launch

            login()
        }
    }

    private suspend fun login(){
        repository.login(
            username = uiState.value.username,
            password = uiState.value.password
        )
            .onSuccess {result ->
                _uiState.update {
                    it.copy(
                        isLoading = false,
                        navigateToMain = true
                    )
                }
            }
            .onError {error ->
                _uiState.update {
                    it.copy(
                        isLoading = false,
                        errorMessage = error.toUiText(),
                        isAlertDialogOpened = true
                    )
                }
            }
    }
}

Now we are going to instantiate ktor with the help of Koin dependency Injection, so if you want to setup koin, you can follow given link https://codeinchunks.com/setup-koin-dependency-injection-in-kotlin-multiplatform/

Update following code in commonMain.kt module

val sharedModule = module {
    single { HttpClientFactory.create(get()) }
    singleOf(::KtorUserDataSource).bind<RemoteUserDataSource>()
    singleOf(::LoginRepositoryImpl).bind<LoginRepository>()
    viewModelOf(::LoginViewModel)
}Code language: PHP (php)

Update initKoin.kt file if exists otherwise create it and add following code

fun initKoin(config: KoinAppDeclaration? = null){
    startKoin {
        config?.invoke(this)
        modules(sharedModule , platformModule)
    }
}Code language: JavaScript (javascript)

Thanks & regards to Philipp Lackner

Leave a Reply

Your email address will not be published. Required fields are marked *