androidkotlinktorktor-client

Android app crashes when Ktor is unable to connect to remote URL


The app I'm trying to build using Kotlin, Dagger-Hilt and Ktor has a ViewModel that periodically checks for new content on a remote server:

private fun loadRemoteContent() {
    viewModelScope.launch {
        val lastUpdateCheck = preferencesRepository.readDate(Settings.LAST_UPDATE_CHECK_DATE)
        val lastUpdateCheckDate = LocalDate.ofEpochDay(lastUpdateCheck ?: today.toEpochDay() )

        val period = Period.between(lastUpdateCheckDate, today)

        if (lastUpdateCheck == null || period.days >= 1) {
            val remoteHash = repository.getHash()
            val localHash = preferencesRepository.readString(Settings.LOCAL_HASH_STRING)

            if (remoteHash != localHash) {
                val remoteContent = repository.getAllRemote()
                repository.insertAllRemote(remoteContent)

                preferencesRepository.setString(Settings.LOCAL_HASH_STRING, remoteHash)
            }

            preferencesRepository.setDate(Settings.LAST_UPDATE_CHECK_DATE, today.toEpochDay())
        }
    }
}

The repository in turn has the following function:

suspend fun getAllRemote(): List<Item> = withContext(backgroundDispatcher) {
    ktorHttpClient
        .prepareGet("https://someurl.com/remotecontent.json")
        .execute { response: HttpResponse ->
            val remoteContent = response.body<List<ItemResponse>>()
            remoteContent.map { remote ->
                Item(
                    id = remote.id,
                    title = remote.title,
                    summary = remote.summary,
                    link = remote.link
                )
            }
        }
}

I have configured Ktor in the AppModule.kt file like this:

@Provides
@Singleton
fun provideKtorHttpClient(): HttpClient {
    return HttpClient(Android) {
        expectSuccess = true
        install(ContentNegotiation) {
            json(
                Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                }
            )
        }
        install(Logging) {
            logger = object : Logger {
                override fun log(message: String) {
                    Log.v("http log: ", message)
                }
            }
            level = LogLevel.ALL
        }

        install(DefaultRequest) {
            header(HttpHeaders.ContentType, ContentType.Application.Json)
        }

        install(HttpRequestRetry) {
            retryOnServerErrors(maxRetries = 5)
            exponentialDelay()
        }
    }
}

When run without an internet connection on an emulator inside Android Studio, Logcat shows the app trying to establish connection 5 times and then crashes with just this message:

2023-05-09 22:36:42.321 31747-31747 AndroidRuntime          com.example.myapp          E  FATAL EXCEPTION: main
                                                                                                    Process: com.example.myapp, PID: 31747

Could someone help me figure out what it is that I'm missing in the Ktor configuration or the functions, so that when there's no internet connection, the app may continue to run without crashing?

Thank you!


Solution

  • The key to the problem, as suggested by Aleksei Tirman, was in using a try {} catch {} block, like this:

    suspend fun getAllRemote(): List<Item> = withContext(backgroundDispatcher) {
        try {
            ktorHttpClient
                .prepareGet("https://some-remote-api.xyz")
                .execute { response: HttpResponse ->
                    val remoteItems = response.body<List<ItemResponse>>()
                    remoteItems.map { remote ->
                        Item(
                            id = remote.id,
                            title = remote.title,
                            summary = remote.summary,
                            link = remote.link
                        )
                    }
                }
        } catch (e: ServerResponseException) {
            Log.d("Ktor", "Error getting remote items: ${e.response.status.description}")
            emptyList()
        } catch (e: ClientRequestException) {
            Log.d("Ktor", "Error getting remote items: ${e.response.status.description}")
            emptyList()
        } catch (e: RedirectResponseException) {
            Log.d("Ktor", "Error getting remote items: ${e.response.status.description}")
            emptyList()
        }
    }