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 Error
Code 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