I was developing an Kotlin Multiplatform App, which I use Ktor, to network calls, and SqlDelite to manage the local datasource.
So I try to implement the same structure which build Android Studio when you create a KMM project.
My problem is that the api which I use to connect asynchronously, I need to pass two headers in order to authenticated in it. I try using headers in my HttpClient, and the parameter in the get method of my client.
I don't know is that the proper way, or as it's seems it not.
Regarding to my backend setUp there are the following classes:
UnsplashApi.kt
class UnsplashApi(private val client: HttpClient = ktorClient) {
suspend fun getPets(): List<UnsplashPhoto> = client.get(UNSPLASH_URL){
parameter("query", "cats")
parameter("page", 1)
parameter("per_page", 10)
}
//TODO("Actualizar el endpoint para incorporar la funcionalidad de buscar por nombre")
suspend fun getPetsByName(pet: String): List<UnsplashPhoto> = client.get(UNSPLASH_URL){
headers{
append(HttpHeaders.Accept, "v1")
append("Authorization:","Client-ID $CLIENT_ID")
}
parameter("query", pet)
parameter("page", 1)
parameter("per_page", 10)
}
}
internal val kotlinxSerializer = KotlinxSerializer(
Json(
JsonConfiguration(isLenient = true, ignoreUnknownKeys = true)
)
)
internal val ktorClient = HttpClient {
defaultRequest {
header("Authorization:", "Client-ID $CLIENT_ID")
header("Accept-Version:","v1")
}
install(JsonFeature) {
serializer = kotlinxSerializer
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
PetsRepository.kt
internal expect fun cache(): PetsDatabase
class PetsRepository(
private val api: UnsplashApi,
private val queries: PetsDBQueries = cache().petsDBQueries
) {
constructor() : this(api = UnsplashApi())
/**
* If [force] is set to true, attempt to load data from remote api.
* If remote api is not available. throw [RefreshDataException]
*
* If [force] is set to false, attempt to load data from local cache.
* If local cache is not available, propagate the exception encountered
*/
fun fetchMembersAsFlow(force: Boolean): Flow<List<Pets>> {
return if (force) getMembersFromRemote() as Flow<List<Pets>> else getMembersFromCache()
}
private fun cacheMembers(pets: List<Pets>) {
queries.deleteAll()
pets.forEach { pet ->
queries.insertPet(
pet.id,
pet.description,
pet.url,
pet.email
)
}
}
/**
* Retorna un Flow con una lista de Pets, y guardando en la BD local
* los pets recuperados de la llamada de red.
*/
private fun getMembersFromRemote(): Flow<List<UnsplashPhoto>> {
println("Getting members from remote")
return flow {
val unsplashPhotos = api.getPets()
Log.d("info", "size pets llamada de red: ${unsplashPhotos.size}")
Log.d("info", "pets $unsplashPhotos")
//TODO(""Convert UnplashPhoto o Pets")
var pets : List<Pets> = unsplashPhotos.map {
Pets(it.id!!.toLong(), it.description!!, it.urls.full, it.user.name)
}
cacheMembers(pets)
emit(api.getPets())
}
.catch { error(RefreshDataException()) }
.flowOn(applicationDispatcher)
}
private fun getMembersFromCache(): Flow<List<Pets>> {
println("Getting members from cache")
fun loadMembers() = queries.selectAll()
.executeAsList()
.map { Pets(id = it.id, description = it.description, url = it.url, email = it.emailUser) }
return flow { emit(loadMembers()) }
.catch { error(RefreshDataException()) }
.flowOn(applicationDispatcher)
}
}
Regarding my androidApp folder clases:
PetsViewModel.kt
class PetsViewModel(
private val repository: PetsRepository
) : ViewModel() {
private val _pets = MutableLiveData<List<Pets>>()
val pets: LiveData<List<Pets>> = _pets
private val _error = MutableLiveData<Throwable>()
val error: LiveData<Throwable> = _error
private val _isRefreshing = MutableLiveData<Boolean>()
val isRefreshing: LiveData<Boolean> = _isRefreshing
init {
loadPets()
}
fun loadPets(force: Boolean = false) {
viewModelScope.launch {
repository.fetchMembersAsFlow(force)
.onStart {
_isRefreshing.value = true
}.onCompletion {
_isRefreshing.value = false
}.catch {
_error.value = it
Log.d("info", "STACKTRACE: ${it.stackTrace.contentToString()} + ${it.cause}")
}.collect {
_pets.value = it
}
}
}
}
PetsAdapter.kt
class PatsAdapter(var pets: List<Pets>) : RecyclerView.Adapter<PatsAdapter.MemberViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemberViewHolder {
val itemView =
LayoutInflater.from(parent.context).inflate(R.layout.list_item_member, parent, false)
return MemberViewHolder(itemView)
}
override fun getItemCount() = pets.size
override fun onBindViewHolder(holder: MemberViewHolder, position: Int) {
holder.bind(pets[position])
}
class MemberViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(pet: Pets) {
Picasso.get().load(pet.url).into(itemView.memberAvatar)
itemView.tvItemDescription.text = pet.description
itemView.tvItemAutor.text = pet.email
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val repository by lazy {
(application as Application).membersRepository
}
private val viewModel by lazy { PetsViewModel(repository) }
private lateinit var adapter: PatsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout.activity_main)
platformMessage.text = createPlatformMessage()
setupRecyclerView()
viewModel.pets.observe(this, Observer {
if (it.isEmpty()) {
Toast.makeText(this, R.string.empty_cache, Toast.LENGTH_LONG).show()
} else {
showData(it)
}
})
viewModel.error.observe(this, Observer {
showError(it)
})
viewModel.isRefreshing.observe(this, Observer {
pullToRefresh.isRefreshing = it
})
pullToRefresh.setOnRefreshListener {
viewModel.loadPets(force = true)
}
}
private fun showData(pets: List<Pets>) {
adapter.pets = pets
adapter.notifyDataSetChanged()
}
private fun showError(error: Throwable) {
val errorMessage = when (error) {
is RefreshDataException -> getString(string.refresh_data_error)
else -> getString(string.unknown_error)
}
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
}
private fun setupRecyclerView() {
membersRecyclerView.layoutManager = LinearLayoutManager(this)
adapter = PatsAdapter(emptyList())
membersRecyclerView.adapter = adapter
}
}
Application.kt
class Application : Application() {
val membersRepository by lazy { PetsRepository() }
override fun onCreate() {
super.onCreate()
appContext = this
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
I don't include the part of sqlDelite because isn't relevant to the main goal of this question, which is the structure of Api.kt class where Ktor is use to make the network connection.
[EDIT]
Added the logcat of the request:
2021-12-21 11:33:23.706 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - REQUEST: https://api.unsplash.com/?client_id=7DQCWc0Hr_GGIOUeMAVKxqvz9lsVtCpvauRvXJnNq_E%2Fsearch%2Fphotos&query=cats&page=1&per_page=10
2021-12-21 11:33:23.707 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
2021-12-21 11:33:23.708 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - COMMON HEADERS
2021-12-21 11:33:23.709 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - -> Accept-Version: v1
2021-12-21 11:33:23.709 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - -> Accept: application/json
2021-12-21 11:33:23.709 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - -> Accept-Charset: UTF-8
2021-12-21 11:33:23.709 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - CONTENT HEADERS
2021-12-21 11:33:23.711 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - BODY Content-Type: null
2021-12-21 11:33:23.763 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - BODY START
2021-12-21 11:33:23.763 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient -
2021-12-21 11:33:23.763 6813-6866/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - BODY END
2021-12-21 11:33:23.799 6813-6878/com.jshvarts.kmp.android D/NetworkSecurityConfig: No Network Security Config specified, using platform default
2021-12-21 11:33:23.800 6813-6878/com.jshvarts.kmp.android I/DpmTcmClient: RegisterTcmMonitor from: $Proxy0
[EDIT]
After change my api.kt class like this:
class UnsplashApi(private val client: HttpClient = ktorClient) {
suspend fun getPets(): List<UnsplashPhoto> = client.get(UNSPLASH_URL){
//parameter("client_id", CLIENT_ID)
parameter("query", "cats")
parameter("page", 1)
parameter("per_page", 10)
}
//TODO("Actualizar el endpoint para incorporar la funcionalidad de buscar por nombre")
suspend fun getPetsByName(pet: String): List<UnsplashPhoto> = client.get(UNSPLASH_URL){
parameter("query", pet)
parameter("page", 1)
parameter("per_page", 10)
}
}
internal const val UNSPLASH_URL = "https://api.unsplash.com/search/photos"
private const val CLIENT_ID : String = ....
internal val kotlinxSerializer = KotlinxSerializer(
Json(
JsonConfiguration(isLenient = true, ignoreUnknownKeys = true)
)
)
internal val ktorClient = HttpClient {
defaultRequest {
header("Accept-Version","v1")
header("Authorization", "Client-ID $CLIENT_ID")
}
install(JsonFeature) {
serializer = kotlinxSerializer
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
I start to get this in the logcat:
2021-12-21 11:53:32.715 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - RESPONSE: 200 OK
2021-12-21 11:53:32.716 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
2021-12-21 11:53:32.718 7396-7450/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - BODY Content-Type: application/json
2021-12-21 11:53:32.718 7396-7450/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-1] INFO io.ktor.client.HttpClient - BODY START
2021-12-21 11:53:32.725 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - FROM: https://api.unsplash.com/search/photos?query=cats&page=1&per_page=10
2021-12-21 11:53:32.725 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - COMMON HEADERS
2021-12-21 11:53:32.725 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Accept-Ranges: bytes
2021-12-21 11:53:32.725 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Access-Control-Allow-Headers: *
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Access-Control-Allow-Origin: *
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Access-Control-Expose-Headers: Link,X-Total,X-Per-Page,X-RateLimit-Limit,X-RateLimit-Remaining
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Access-Control-Request-Method: *
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Age: 445
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Cache-Control: no-cache, no-store, must-revalidate
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Connection: keep-alive
2021-12-21 11:53:32.726 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Content-Type: application/json
2021-12-21 11:53:32.727 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Date: Tue, 21 Dec 2021 10:53:33 GMT
2021-12-21 11:53:32.727 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Etag: W/"9545b4e1f85112ca6cc4c543c08a7a37"
2021-12-21 11:53:32.727 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Link: <https://api.unsplash.com/search/photos?page=1000&per_page=10&query=cats>; rel="last", <https://api.unsplash.com/search/photos?page=2&per_page=10&query=cats>; rel="next"
2021-12-21 11:53:32.727 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Server: Cowboy
2021-12-21 11:53:32.727 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Strict-Transport-Security: max-age=31536000; includeSubDomains
2021-12-21 11:53:32.727 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Vary: Accept-Encoding, Origin,Authorization,Accept-Language,client-geo-region,Accept
2021-12-21 11:53:32.728 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Via: 1.1 vegur, 1.1 varnish, 1.1 varnish
2021-12-21 11:53:32.728 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> Warning: The tags property in this endpoint is deprecated. https://changelog.unsplash.com/deprecations/2021/07/12/tags-search-deprecation.html
2021-12-21 11:53:32.728 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Android-Received-Millis: 1640084012689
2021-12-21 11:53:32.728 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Android-Response-Source: NETWORK 200
2021-12-21 11:53:32.728 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Android-Selected-Protocol: http/1.1
2021-12-21 11:53:32.728 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Android-Sent-Millis: 1640084012597
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Cache: MISS, HIT
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Cache-Hits: 0, 1
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Per-Page: 10
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Ratelimit-Limit: 50
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Ratelimit-Remaining: 49
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Request-Id: 6d958f7f-491a-42b9-8f48-47670b8c5836
2021-12-21 11:53:32.729 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Runtime: 0.086386
2021-12-21 11:53:32.730 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Served-By: cache-iad-kcgs7200028-IAD, cache-mad22057-MAD
2021-12-21 11:53:32.730 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Timer: S1640084013.381487,VS0,VE1
2021-12-21 11:53:32.730 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Total: 10000
2021-12-21 11:53:32.730 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - -> X-Unsplash-Version: v1
2021-12-21 11:53:32.887 7396-7451/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-2] INFO io.ktor.client.HttpClient - {"total":10000,"total_pages":1000,"results":[{"id":"ZCHj_2lJP00","created_at":"2020-06-15T00:30:27-04:00","updated_at":"2021-12-20T07:12:05-05:00","promoted_at":"2020-06-15T04:16:29-04:00","width":5304,"height":7952,"color":"#a6d9d9","blur_hash":"LRJcqDIUL3s..mX8rXRPOZnirWXT","description":null,"alt_description":"white and brown long fur cat","urls":{"raw":"https://images.unsplash.com/photo-1592194996308-7b43878e84a6?ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA\u0026ixlib=rb-1.2.1","full":"https://images.unsplash.com/photo-1592194996308-7b43878e84a6?crop=entropy\u0026cs=srgb\u0026fm=jpg\u0026ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA\u0026ixlib=rb-1.2.1\u0026q=85","regular":"https://images.unsplash.com/photo-1592194996308-7b43878e84a6?crop=entropy\u0026cs=tinysrgb\u0026fit=max\u0026fm=jpg\u0026ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA\u0026ixlib=rb-1.2.1\u0026q=80\u0026w=1080","small":"https://images.unsplash.com/photo-1592194996308-7b43878e84a6?crop=entropy\u0026cs=tinysrgb\u0026fit=max\u0026fm=jpg\u0026ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA\u0026ixlib=rb-1.2.1\u0026q=80\u0026w=400","thumb":"https://images.unsplash.com/photo-1592194996308-7b43878e84a6?crop=entropy\u0026cs=tinysrgb\u0026fit=max\u0026fm=jpg\u0026ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA\u0026ixlib=rb-1.2.1\u0026q=80\u0026w=200"},"links":{"self":"https://api.unsplash.com/photos/ZCHj_2lJP00","html":"https://unsplash.com/photos/ZCHj_2lJP00","download":"https://unsplash.com/photos/ZCHj_2lJP00/download?ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA","download_location":"https://api.unsplash.com/photos/ZCHj_2lJP00/download?ixid=MnwyMjczNDJ8MHwxfHNlYXJjaHwxfHxjYXRzfGVufDB8fHx8MTY0MDA4MzU2OA"},"categories":[],"likes":882,"liked_by_user":false,"current_user_collections":[],"sponsorship":null,"topic_submissions":{"animals":{"status":"approved","approved_on":"2020-06-16T07:38:49-04:00"},"wallpapers":{"status":"approved","approved_on":"2021-04-23T06:55:04-04:00"}},"user":{"id":"1LMzZNX562k","updated_at":"2021-12-21T05:20:13-05:00","username":"alvannee","name":"Alvan Nee","first_name":"Alvan","last_name":"Nee","twitter_username":"Alvan Nee","portfolio_url":null,"bio":"I really love unsplash!!!!!","location":"Shanghai, China","links":{"self":"https://api.unsplash.com/users/alvannee","html":"https://unsplash.com/@alvannee","photos":"https://api.unsplash.com/users/alvannee/photos","likes":"https://api.unsplash.com/users/alvannee/likes","portfolio":"https://api.unsplash.com/users/alvannee/portfolio","following":"https://api.unsplash.com/users/alvannee/following","followers":"https://api.unsplash.com/users/alvannee/followers"},"profile_image":{"small":"https://images.unsplash.com/profile-1617947361627-4a8765a9b014image?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=faces\u0026cs=tinysrgb\u0026fit=crop\u0026h=32\u0026w=32","medium":"https://images.unsplash.com/profile-1617947361627-4a8765a9b014image?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=faces\u0026cs=tinysrgb\u0026fit=crop\u0026h=64\u0026w=64","large":"https://images.unsplash.com/profile-1617947361627-4a8765a9b014image?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=faces\u0026cs=tinysrgb\u0026fit=crop\u0026h=128\u0026w=128"},"instagram_username":"alvan_nee","total_collections":0,"total_likes":68,"total_photos":191,"accepted_tos":true,"for_hire":false,"social":{"instagram_username":"alvan_nee","portfolio_url":null,"twitter_username":"Alvan Nee","paypal_email":null}},"tags":[{"type":"landing_page","title":"cat","source":{"ancestry":{"type":{"slug":"images","pretty_slug":"Images"},"category":{"slug":"animals","pretty_slug":"Animals"},"subcategory":{"slug":"cat","pretty_slug":"Cat"}},"title":"Cat Images \u0026 Pictures","subtitle":"Download free cat images","description":"9 lives isn't enough to capture the amazing-ness of cats. You need high-quality, professio
2021-12-21 11:53:32.887 7396-7451/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-2] INFO io.ktor.client.HttpClient - BODY END
2021-12-21 11:53:32.894 7396-7471/com.jshvarts.kmp.android W/System.err: [DefaultDispatcher-worker-7] INFO io.ktor.client.HttpClient - RESPONSE https://api.unsplash.com/search/photos?query=cats&page=1&per_page=10 failed with exception: kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 0: Expected '[, kind: LIST'.
I was reading by some forums that maybe it can be related which the need of decode the response.
As I say I really just start which ktor and I don't know how implement correctly
Take thanks in advance !