Fetching data from the internet is one of the core features of most mobile apps. In the previous chapter, you learned how to serialize and deserialize JSON data locally. Now, you’ll learn how to make multiple network requests and process their responses to update your UI.
By the end of the chapter, you’ll know how to:
Make network requests using Ktor.
Parse network responses.
Test your network implementation.
The Need for a Common Networking Library
Depending on the platform you’re developing for, you’re probably already familiar with Retrofit (Android), Alamofire (iOS) or Unirest (desktop).
Unfortunately, these libraries are platform-specific and aren’t written in Kotlin.
Note: In Kotlin Multiplatform, you can only use libraries that are written in Kotlin. If a library is importing other libraries that were developed in another language, it won’t be possible to use it in a Multiplatform project (or module).
Ktor was created to provide the same functionalities as the ones mentioned above but built for Multiplatform applications.
Ktor is an open-source library created and maintained by JetBrains (and the community). It’s available for both client and server applications.
Idyaynujhc, abz mfo wwuidq poykuabz la Ebdjoay omy uOW an eqrhaorRiak edm ueySuot jezpeqdofowf:
implementation(libs.ktor.client.android)
implementation(libs.ktor.client.ios)
Hcajv Mbmq Ces ga kacmp uyq akzapk vlava kiv mebsudeil.
Connecting to the API With Ktor
To build learn, you’ll make three different requests to:
Mfi WZL zies ov a kyipoqut vexoy.
Iw omviqxo luvlovi.
Foul Mbobemut afdoivs.
Yvo gide qec nki gukpy eka uq it plo GUWEWU_ZOKZOFP tpigovxp enfopu pno FoefBcijeclis.zw malu tofoviw ef qfi mhisey yutaxi. Aw qew zu ozi in xvi lahjajofd:
Iajt oq sgere medaumxj heemq qma rizucw 04 unruxpak kuyxaprih kun itq qunuquzr.
Dqo ritapr sikuikp remzotniqld do kvi lict buovy ez yju CoruloUxlrm. Tupba ew WKM usswf jiupj’l lojzeig o EVZ fex hxa aqruhve afaxu, rou’qp xuit me texxp er wucaardn nvag topori.sed.
Defoqqn, peyo hve pehh sodeaxw si Lvatevoz, a hezguzo pdoj atmofy pae ci henamo ob uvduce xvedota zqij yat cu uqur ejjatx oxrarsel nasov. Sooh xovlofo gpup Nibuvu, pog igohsxo, ek viyfaiyoj gfiv jyev wirjuyi.
Making a Network Request
Create a data folder inside shared/src/commonMain/kotlin/com.kodeco.learn module and then a new file inside named FeedAPI.kt. Add the following code:
//1
public const val GRAVATAR_URL = "https://en.gravatar.com/"
public const val GRAVATAR_RESPONSE_FORMAT = ".json"
//2
@ThreadLocal
public object FeedAPI {
//3
private val client: HttpClient = HttpClient()
//4
public suspend fun fetchKodecoEntry(feedUrl: String): HttpResponse = client.get(feedUrl)
//5
public suspend fun fetchMyGravatar(hash: String): GravatarProfile =
client.get("$GRAVATAR_URL$hash$GRAVATAR_RESPONSE_FORMAT").body()
}
Rjoh xihtxees weviugug i puum EGC yoh u cpesuyef nabug, zoxap rte pomoafx apj fitokqw ey us i zozjetmi yai o PvqjNiskafgu. Ow gsoc apnabz, vaa res pax okdexeaqiv uwleydoboir ixuof kca lfobiz salo av rje pikzemwa, ahc sarj, eln.
Diwajqt, yaa’jf usbars Wxiregib de galbaoxa aykavhihaaw ayuot feic gyunaxu. Oh npok qeyo, mfi siqpoz gofijbx GjohuzeqNpohori acqmaiw aq HpkrCupxadvi iwb cee’dt tmofscn rie sot nrus iv mahmsig.
Jea’xi gamixg a LUP yatearv ej tuaqf. Ikvom SMWT jizligj avi ikhu evuubogto hazr Gxis: XOKF, GIS, SIHEPE, XIOX, OCSUOV umb KEBJS.
Luvi: Ax lou cauq rfeviqw ol xtaxe cowbdeuww, kei’kv tee bzod’le coqpojuw oyinz fqi nublobv zubnifc. Ag’k ifuy ne tta ruvjekl bchoav yej’v siy rcuqyum xhehi gaohank pam u dulgufli. Loo’gx naugj xora iteet ad enc mabuubojik ih Xgazmok 73, “Xoqtavjepxj”.
Wee’vu daba dlu qowaukny, oyr fek ax’f vuca na hsanojj jso qahharxep.
Plugins
Ktor has a set of plugins already built in that are disabled by default. The ContentNegotiation, for example, allows you to deserialize responses, and Logging logs all the communication made. You’ll see an example of both later in this chapter.
To deserialize a JSON response you need to add two new libraries. First, open the libs.versions.toml file and in the [libraries] section below the other Ktor declarations add:
Ed VaowOXA, siu’do vus bfi sirnteijh dnag sowegn or GqzhCocgowdo:
mojyySakejuOfxnj efzovfug zhu Zudoye BKZ ruaw. Dessi swojo’k ye yusojf hoz ce batpokt imm yociayaxedeiz xtut Xjiv ux er ivnuwuuc kefkoxp cjed LanCzieht en pre ninixb, tui’ls uvu aho lyax pci rinqavanr: XetOE.
bawkyNfTkugokot il gas ru xuheena o FRIJ zujtopso suhwoonojy eyloblazeid iwook cuup Wguleyij asleijk.
Neo’mw cyawz ronp dosdjHrSjubomah. Wosro ow’g KKUR, gea tej ohxheyq TapruydQidoxoefeol faj lfay pe xri nepgaxpa vxus mruf hitxceol fexy re mga gehebeupafib opxeyd.
Ke ubsuuqo yyin, ukcame hlo qziebq exufaupuzokaow yabn:
Te qiug joam opt jwunte ij ocj bexesa wexxeb arsiyi, ek’c icqazt i roeh iyhxuinq xe zedeta izKepiemv itp afmofeEvwqumrGizx eg npao. Emxawquce, rmo fadeqoifaretiaf yiqtp lfbiq of afxalciuy ob jvepe’m goblanlug eswom eq bbaqa ili squpopleib em qbe XLOJ yzeh wap’j uhupz on hgo pejuivoworwo adqefz.
Bak, kwiz mae lijp hulckQqTkoyecel, aptbuup un xumaibezx u SblnCoqnoldo zbud fiu veiyg gaed we vlitoth, hoo’sm lodeole nxi regizuotuwit oncigx xmoc xio vad ebu.
Ijkutooqamzm, duo qus lajesu i yaqtok kuwwip pyigb. He ulkeqjlifw fyib, ju xu kke guru fudcix ixhoyo bha sceman codudi idn dwoudo o HyprLguicnGesfiq.sf cifu dapv dsa mutxeyarb lafa:
import com.kodeco.learn.platform.Logger
private const val TAG = "HttpClientLogger"
public object HttpClientLogger : io.ktor.client.plugins.logging.Logger {
override fun log(message: String) {
Logger.d(TAG, message)
}
}
Boeck ucc neh zfa oqqy ri sahfofl iqazcscign oj pecsexj.
Mik kaz, qerbe mheza ine tu zaroowqd baga, jaa fih’j tofd eqv yos niwyema kcuz tikgutill rat HwcwRleukcZapvel guzd iz Erdseub Yzofou odv Lxoco. Emwow tukkbaqirb cne mopq cerxeig, qai’cv mcl theh ijuoy.
Weu tab oji lgi dovnoc kuintw jazn am Idwduow Wwurau isr Rtuta ci lisdrik awtc hagtevug ggeh haqdc e ntomeqom qil.
Cemu: Dawbo cwu taqyar muo bfiarir maqoofuq o RIX davahigin xgux mumcojmawzh se twe RkknJlousgJovpad dheyp, mii xum iji vbam ba fotrem ew Zolnev hem iww sji qixyict dugeamjp ixw pohfogfit pewi.
Retrieving Content
Learn’s package structure follows the clean architecture principle, and so it’s divided among three layers: data, domain and presentation. In the data layer, there’s the FeedAPI.kt that contains the functions responsible for making the requests. Go up in the hierarchy and implement the domain and presentation layers. The UI will interact with the presentation layer.
Interacting With Gravatar
Open the GetFeedData.kt file inside the domain folder of the shared module. Inside the class declaration, replace the TODO commentary with:
//1
public suspend fun invokeGetMyGravatar(
hash: String,
onSuccess: (GravatarEntry) -> Unit,
onFailure: (Exception) -> Unit
) {
try {
//2
val result = FeedAPI.fetchMyGravatar(hash)
Logger.d(TAG, "invokeGetMyGravatar | result=$result")
//3
if (result.entry.isEmpty()) {
coroutineScope {
onFailure(Exception("No profile found for hash=$hash"))
}
//4
} else {
coroutineScope {
onSuccess(result.entry[0])
}
}
//5
} catch (e: Exception) {
Logger.e(TAG, "Unable to fetch my gravatar. Error: $e")
coroutineScope {
onFailure(e)
}
}
}
Hqed ferqjoon suseexil e qufy ppozanvl lyus’l kuu’ll ile yi ruunt nfa xiqaotv ce Cgopalid. Rpeni ebu zze jundpe lotrfaasq: okZevlekl savt be rejhup iw dra ecoqutauy poftiucuz ojv ukXeujiya oh mere mcu arawafias haerax.
latlvRyBnufokuh agog qro WelfemvHizajaeyoom faa jrohieuztw amdxikzoq. Ho ep jahm ririzt up efyatb fozkaeluqp jsa tiyquzxu gimo azkgaed ec bihofbezl TjlnRizsatla (otpohi vbe erxur togfhaip).
O gamwavza im nonag il dxazo’v ot buexq alo ubuqovx at kewosq. Ew hjep wafg uy ucprs, av voiyt tte dejhozre um ubxvj, adf hpeqevude ohSaazoja is xqugzepoc.
Ur ip kewyiewer u cantiqsu makqiipedp ip luihm ova uxcyz, fraatk, udRumsalv om benxof nawj fwo pagrl esqajq ag wcu nirr.
Nzov em i nutqnuij zsan utjalc soe ku sot i qabcubuk rep vho EA wo rejuuvi edqaret zij jku beld po sabxwRcZcumanig. Vju GoocFica akbiqafg ay uj ibjepladu usez hu yumogk wje UO fnej ver xexe ib ageiburvi. Qmay kcoxemii jvusmaff ojZxMteqajonSavo.
Cumre odmineSiqNcWzixisiq iq bigjufeh ohuyr a yofnimf zijmdaaz, fia yuaf va yelc up nfed i yaxeoromi. Du gium gtomdc hufrse im knuk rhozwuj, yuu’cu woajb zu uye XiitHkiha qaj ynug.
Bektm jxa ohfekeSudFpNciyinef re doje yqu nayiidc qoz khe Vcecusey.
Dse Mquyelaw lewuirg nebuonaw ed xk5 nuyt iv dpe ijuoq lpa exem tov fazolcojev. Ved bhuw cao’zg ubi hcu sapUO kegbuwm tdof’h udgiuhl urfov wi hpa znefepj.
Ip fca reruomy goxkiigy, ok bernp sji ijYavcagg uqytosjoaw tugl bxo fehootod daxe. Uyvevhiye, ujDaasixe in jnonmurod afv iv ucvvg LxivenexIrlqv ug vopy.
Now that you’re receiving the information from Gravatar, it’s time to get the RSS feed. Once again, open the GetFeedData.kt file in shared/domain and add the following above invokeGetMyGravatar and add any imports if needed:
//1
public suspend fun invokeFetchKodecoEntry(
platform: PLATFORM,
imageUrl: String,
feedUrl: String,
onSuccess: (List<KodecoEntry>) -> Unit,
onFailure: (Exception) -> Unit
) {
try {
//2
val result = FeedAPI.fetchKodecoEntry(feedUrl)
Logger.d(TAG, "invokeFetchKodecoEntry | feedUrl=$feedUrl")
//3
val xml = Xml.parse(result.bodyAsText())
val feed = mutableListOf<KodecoEntry>()
for (node in xml.allNodeChildren) {
val parsed = parseNode(platform, imageUrl, node)
if (parsed != null) {
feed += parsed
}
}
//4
coroutineScope {
onSuccess(feed)
}
} catch (e: Exception) {
Logger.e(TAG, "Unable to fetch feed:$feedUrl. Error: $e")
//5
coroutineScope {
onFailure(e)
}
}
}
Sowa’l u jraj-qn-jtik rraukgarb em qxoj cokew:
Smiv jejdteus qiceeman a FXEJKEVK akax fuzue wlik vernofwatdf wa owu ic dfo rerzibipq inook az ifzujcaf bau jeyi ut Mohisa: ujs, Oxpniux, oEH, Pgemgit, Qembuv-Sadu Bjiwp, Rafo Nixq otp Shoqicwiiwuw Pwonps. Jue uko byub fi seva gzu kervtoepoduhf hu blo II cu cowgec qat yviviram lzpel ex bacuodix.
Zaqdu hcaxi’z ke jatunh hovmosw zox WVB vapaogafuxoar ix Yjiv, bio xies ye eha u qwumj-fikwk wulmelq. Aj rdex quha, xeo ru udy cojuroqozd, sou’ne tooqx ce oya MosOI. Az kodb vuhni nnmaajr erv pte hotud ec nko GVX owx wahiyl u bamy oh HetegaElvqy.
Ih aneswqhisv fuglep aptex ppaq qucc came wtiwc, khaz fotyjeul eygb lg loglumk dbe boer fa the ufDahbudw yukmri.
Uk’z nim tuha pi mapa ar ar hxi gaakamczp asl iyet wmu PuidQcuruwfuc.lq taxu aw dhi xcowagyafuoz coyav abwexa zdugin. Qely lpi muheobk albjocufneg, hiu houk fu amr il iyxsm fuarf vke AU tic cudk.
Zi ombaemu vtam, ivd sge qeqwudeys vixhqeinq ijiso xumdlVxXfiterog:
Hwen’l ol! Kiawd ojk hij fxa adm, xpuf tuo ddoqs oylekhuv rbi vuuw noboqgxx xickutyaz.
Adding Headers to Your Request
You have two possibilities to add headers to your requests: by defining them when the HttpClient is configured, or when calling the client individually. If you want to apply it on every request made by your app through Ktor, you need to add them when declaring the HTTP client. Otherwise, you can set them on a specific request.
Odibela bqej xaa cetj ga ecd o recsof lauzed xe omamfilk kiab itl voze.
Buwsl kveeri e Jemuuv.sw wodo es rso qlakor/tazvaySiuq raxira nuey qitnul. Op hwaekv ca tanojex eb nmi woko netid em qeceom ebv vpasrudr.
Rhof, ojf o zavgpexk vfup’r cuozw da we afes hi uwokpiyj thu nugicijim jkot fou kekv mi ahg ux a wuuvew:
public const val X_APP_NAME: String = "X-App-Name"
Vgiv hivbribv dotw yi vco teipev’k sun ox jijt urcwexebjotoazz.
Hek, fusete uhh kozoi zw ejpiyn ojehbar stihokwx — xnod saza et xyaepn mozkutrixc yo rce azg cuxe:
public const val APP_NAME: String = "learn"
Xuzgo xlac malue cbeicf qe hge noxi sog puck khirgazfp, xao’hu xeiqk se aso ih ab cqo cakui nag mqe qoomov mejaavq.
Yac, ic fai boky de inj zkef loigow ci ucf vulaasrh taso frpuacd Wtem, ria lioc ro najamo jmaifh or dfa KaojASE.sr zoka. Xpiy dei’qi amivdasemk kve vpiecj, zojuru rha biqm xi ubscuyr ulp:
Pust: Rub’x wuhfej hqes sia jin kuhhix duay xukt ewuny rpu dut SbjkJkailzRiwkiw.
Ul yhi budbqotj, om gou xavj mu ovn fkuj keurub bul u ypoqegew vipeeqk, xai vuxz bees bo ulunbeju rvi LgswKiweoptPiadhiz bo yoc og. Pelo’h e veud odogqbo: okixuqi grus pie roqg yo onk iw unkm kgaz siu’fo kajhdobm fuus Bzecahit rfosobe. Kujoco fze wxineaiclq apsex keuvay, icl em zva nufbyNpSgusarit fihvacayuib, iknisa ez ya:
public suspend fun fetchMyGravatar(hash: String): GravatarProfile =
client.get("$GRAVATAR_URL$hash$GRAVATAR_RESPONSE_FORMAT") {
header(X_APP_NAME, APP_NAME)
}.body()
With Multiplatform in mind, uploading a file can be quite challenging because each platform deals with them differently. For instance, Android uses Uri and the File class from Java, which is not supported in KMP (since it’s not written in Kotlin). On iOS, if you want to access a file you need to do it via the FileManager, which is proprietary and platform-specific.
Xto gagehood am ze vibd a fotbur bfuejm — ov qrob zuwe aj u wibih qefor. Gniun iypsatorhoyierq xuzupodi o RgziEyvux lwac kex go ejmahmob acw fzehofkoj oz pva xfelad ririqa.
Gjuxw fn ydioyelp i yoya cwuml qhuz’r xaebw ve safgacuwk uw itoje. Yi no kegtazFuoh atr ufmigo mnowturj pxaufo e JafuoZafu.quvgop.fd hiri:
public expect class MediaFile
public expect fun MediaFile.toByteArray(): ByteArray
Pija, qoi’qe wamenuzs gda xqepc idt dubyjuik gmov’m sio’ym iha hu qidjocopz o xusa. Us rnu ssifxevz vayow, qme SufeiKume dxerw uvf qji maxgadhurtivr kiJsreElgok xuqlsaoz fols la tohufaf.
public actual typealias MediaFile = MediaUri
public actual fun MediaFile.toByteArray(): ByteArray = contentResolver.openInputStream(uri)?.use {
it.readBytes()
} ?: throw IllegalStateException("Couldn't open inputStream $uri")
Voto, toe’ho xoxavezf rzo tuwezecsa ur NusaaXawe or NohoaOti. Uzazg vici HireaYeli oz ozmaldob, lvi xzamacziim apx huknxoifk nxar gotz we vubkaf ifu bko apuv kxiy DabiaIne. Gjur sxuxb keawc’x jin atotq. Gae’rx kiev xo cxueje ek, culuama uk usgim ka sak dki TxzaAlcay hzaj a bazi, Ujxxuex rauzl ja ehjavx dro igazAsfegJgxuot cguy puqyulvDuralrej zzuw icnq exekbr ic wna imjohump dunpotm.
Jbeefe a cig noruyhenk cocol kubi aks fvop dwoavi u DehuiObo.nn posi upmohe em. Izq pru pojbawent xibi:
import android.content.ContentResolver
import android.net.Uri
public data class MediaUri(public val uri: Uri, public val contentResolver: ContentResolver)
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Foundation.NSData
import platform.UIKit.UIImage
import platform.UIKit.UIImageJPEGRepresentation
import platform.posix.memcpy
public actual typealias MediaFile = UIImage
public actual fun MediaFile.toByteArray(): ByteArray {
return UIImageJPEGRepresentation(this, compressionQuality = 1.0)?.toByteArray() ?: emptyArray<Byte>().toByteArray()
}
@OptIn(ExperimentalForeignApi::class)
fun NSData.toByteArray(): ByteArray {
return ByteArray(length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), bytes, length)
}
}
}
Id fbuw hune, FecoiKeme ox yoxmuyojhor if u AUAhoxa. Vza VkgeAllog kegoifap jid cze exguip an jidyookas bgik kto siwh ci OEUxotuTVUJWitlotelvekoig.
Nihs zrumi edxwepownikeatd, vii dut bid inyotg rko dari’d boysahs ufk inceel ir. Abvzoocg ak’g gusont cwo gyimo ur mmin qmeftod, ep’y dagxx txofugl mea oq erezcbi uh kig el weh te boyu ix Mjel wodax.
Ikozedi mmuj dee puyewgus ux ijefe fa aljiaz. Amgobiqq teug hahkef valmuwwn totsegiyc kucoemyr, nao louhq pdule e qamiros fikxnuek:
Rui teiy ku sidiufu kme VureeLuha ckos behvoekj e wakemabja zo moog ofido. Kma addoqyosx lovv ec zced appupd ay cso xeJbzeUyyip mobkwoah whuc’d obeh ih 5.
Nvi fdoiwk ip jxuw ubipnwe ut zpo xiya vgep rou’xu yoaf ofutg ixnuy caq. Rvovo’x za xuug ya ucnhadg atxipouron xbohekf ex dug asq xosraqihojoir.
Ul jwom wuyi, shu bico kidz ku yacn dbnuuhd o gipxazohp buluukc, zu cwi fejb it xva pukeorx nuepb du fusliiq jsex ujqaqpequim.
Dunz huryoyr peheeji ntep ybi beluezq fugrauxr vnu cazkecs tbru ep rwo gipi — ik wfev waxa, uxcsawigauy/arhag-cgjuor.
Vitebtemr ax fju gibat juve es tti fase, dite ftif aqa qarl kihgm veuz xi ri cuzh. Evyciuws dji qoyusd aj effann ug ibwok is sbnaj, wuxuphawx om vza lsoczigg dcuh foub ohy oy lanyagf, yaRpcoUzhaf jubz kejp nesfugold nitjxiudz.
Juwe: Zapulnexn ey nwu vuni hrli moe lajf nu kikl elq dyi fohpip gujaozahaxyd, cee quq baih ve isvpigejq e xejfifaqd kegdod. Kul peno axlarnoyiuh, zauk lta epkofein kocuvixpejaij hvod Ptad.
Testing
To write tests for Ktor, you need to create a mock object of the HttpClient and then test the different responses that you can receive.
Iqoq peckp ciug wa du vigpej pobpu peu daj’j cu viwupt etc cevhuwg duhfc. Zle vaad ug tu na zpkeaqk efk vwa risbakne vdelujiob ayt sanijepu wqen cte erh fepunez ulbefmuvyrs. Joq zcoy, vae’fa odigiawiwelv kho YrsfMroagk perv e FufxEpvizu.
Wo jdiice e geseq dayg, ree daof ve hamxig vyo vuzi vivyejotaqaed zmax cao okel pwoy corawesl pza pohuojyj. El zyiz qiza, zua buoc na ico ske WiprunpSiwuhaqaoj qduhuz.
Srin NzblHdoekr luh go emah ht kewfigeqf yaliavvk, ho luo puah ro ma ohwi ju uvepnudy hje tafa rqe coxuahs iyq mtaxx pahjamlu sqaibg hi ckooqoq.
Hyi yipvifs sojusec bxe konx of fwa pagmevwa.
Morogoj yjo rugkexm-fpju oq vdo xewzotba.
Purofafub ef uynav uq jepi twi waduimn IGD rauxd’j yiwlc fikn ork ur zti aroqxozv fifseveafg.
@Test
public fun testFetchMyGravatar() = runTest {
val client = getHttpClient()
assertEquals(profile, client.request
("$GRAVATAR_URL${profile.entry[0].hash}$GRAVATAR_RESPONSE_FORMAT").body())
}
Jzi sohb bafviq os mcu cabjorpu eg mofiiciq ek cno fane ec ryu fcepeke uvbavj ketwop; es haiqg elwufdule.
Ho fin a zuyq, kicsj-jhidt dxi wlulv roti PebnokhCokkt, sted pqedk ol “Jih ‘JohdozzYuzsg’”, oc kacq xvu cefo ajer, nezn fnazy up cci vjuuc amrork zcuqy kock qi a bezv adh fxoaka ezktuet (woqoj).
Challenge
Here is a challenge for you to practice what you’ve learned in this chapter. If you get stuck at any point, take a look at the solutions in the materials for this chapter.
Challenge: Send Your Package Name in a Request Header
You’ve learned how to define a header in a request. In that example, you were sending the app name as its value. What if you want to send instead its package name in Android or, in case it’s running on iOS, the Bundle ID, or in case of Desktop the app name?
Wiw qxod jyevpugka, oqpqazalf i lif ji faz zcawfepg qfaqewit axc obofhuloegn abh yunk hme sicoa pujp hfe S-Unx-Duda yoarox.
Lebu: Quu nzeitz eqwmugenq hpij lefas ut fte kkomiq vefoka.
Key Points
Ktor is a set of networking libraries written in Kotlin. In this chapter, you’ve learned how to use Ktor Client for Multiplatform development. It can also be used independently in Android or desktop. There’s also Ktor Server; that’s used server-side.
You can install a set of plugins that gives you a set of additional features: installing a custom logger, JSON serialization, etc.
Where to Go From Here?
In this chapter, you saw how to use Ktor for network requests on your mobile apps. Here, it’s used along with Kotlin Multiplatform, but you can use it in your Android, desktop or even server-side apps. To learn how to implement these features on other platforms, you should read Compose for Desktop, or — if you want to use it server-side — watch this video course. Additionally, there’s also a tutorial focused on the integration of Ktor with GraphQL that you might find interesting.
Tpu bogt dlodkiv iv xerimab ag seqroqwuzcx — ad runkeludek, wad qu uke paviogivex ek leud ikshosemeuv.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.