Traditionally, Android applications relied on XML-based layouts. Android development has now shifted to Compose as the standard framework. Compose describes a much larger set of frameworks and application architecture in Kotlin, which isn’t specific to Android development. For example, the development teams at Slack have developed the Circuit framework, built on top of Compose. Compose UI is just one of the seven Compose frameworks specific to the UI layer of application development. Using Compose UI brings many improvements over the old View implementation, including significantly reduced build times, APK size and runtime performance. It also makes building the UI easier, more intuitive, and easier to maintain and debug. For a more in-depth comparison of Compose and the old Android View implementation, see this article.
Modern Android development has shifted from MVVM (Model-View-Viewmodel) to MVI (Model-View-Intent) architecture, with Compose UI now used for the UI layer rather than XML layouts. One of the key concepts behind MVI is unidirectional data flow. Often, you can have multiple data sources, including local storage and network sources. These are typically abstracted and accessed using a Repository pattern. A ViewModel accesses methods on the Repository and provides unidirectional data to the UI using Flow. A Flow typically consists of a data emitter and subscribers to the emitter. It provides an efficient memory usage model because the Flow won’t consume memory until and unless an active subscriber is using the data from the Flow. For Android, there are specific implementations of Flow that are Android lifecycle aware. Hence, when a view is no longer visible to the user, data is no longer consumed, freeing up memory.
In this chapter, you’ll learn the basics of Compose UI and build a simple Android app UI with it. In the next chapter, you’ll dive deeper and learn about the other essential pieces to wire a Compose app together, including the ViewModel, MVI, the Repository pattern and Navigation.
Compose Fundamentals
If you’ve been following along with the previous chapters, open your Kodeco Chat app in Android Studio. Otherwise, open this chapter’s starter project using Android Studio and select Open an existing project. Next, navigate to 05-jetpack-compose/projects and select the starter folder as the project root. Once the project opens, let it build and sync, and you’ll be ready to go!
Recall that in Chapter 2 you created a new Android project from scratch. The default Activity created for you contained the following function:
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
This code has two notable things about it. First, it’s a function; second, it’s annotated with @Composable. This is all you need to create a UI component in Compose, or, in Compose speak, a composable.
In your project, open MainActivity.kt. Look through the code — where is the @Composable annotation?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column { ...
It doesn’t seem to appear anywhere…
But take a closer look: MainActivity is a subclass of ComponentActivity. In the onCreate() function you call setContent.
Command-click (if using a Mac; Control-click if using a PC) setContent. Android Studio opens this function’s definition, which is defined in ComponentActivity.kt:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {...}
Aha! So, first of all, setContent is an extension function of ComponentActivity. Extension functions add additional functionality to a class without changing its source code. Calling setContent() sets the given composable function named content as the root view, to which you can add any number of elements. You call the rest of your composable functions from within this container.
Second, note that content is also annotated with @Composable. You don’t need to add it again before putting in composables like the Column above because the annotation is here.
Right-click com.kodeco.chat in the Project Navigator and select New -> Package from the context-menu:
Name the new package “conversation”. This is where you’ll create the components that compose the pieces of the chat UI.
Next, right-click the conversation package and select New -> Kotlin Class/File:
Ensure that “file” is selected and name the new file “Conversation”:
Android Studio creates an empty Kotlin file in the conversation package named Conversation.kt.
In Conversation.kt, type the following:
@Composable
fun ConversationContent() {
// TODO: create conversation UI here
}
Congratulations on writing your first Compose function! It doesn’t do anything yet, but you’ll soon change that.
Go back to MainActivity.kt and copy everything from inside the braces setContent{} and paste it into the body of ConversationContent:
@Composable
fun ConversationContent() {
Column {
val context = LocalContext.current
var chatInputText by remember { mutableStateOf(context.getString(R.string.chat_entry_default)) }
var chatOutputText by remember { mutableStateOf(context.getString(R.string.chat_display_default)) }
Text(text = chatOutputText)
OutlinedTextField(
value = chatInputText,
onValueChange = {
chatInputText = it
},
label = { Text(text = stringResource(id = R.string.chat_entry_label)) }
)
Button(onClick = {
chatOutputText = chatInputText
chatInputText = ""
}) {
Text(text = stringResource(id = R.string.send_button))
}
}
}
Then, go back to MainActivity.kt and replace everything in setContent{} with ConversationContent(). Your Activity class should look much simpler and cleaner:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ConversationContent()
}
}
}
Build and run your app. It should work exactly as before.
Breaking Down Composables
Although the simple copy/paste action you performed might seem trivial, it highlights a couple key concepts. First, note that Conversation.kt is a separate file, not a class. It simply houses a Compose function. This function could have been placed in MainActivity.kt. But as your codebase expands, maintaining a single file for all your code can become cumbersome. Second, you’ve created a composable function named ConversationContent, which you can reuse throughout your app. This reusability is a cornerstone of Compose UI, akin to constructing a large sculpture from individual Lego blocks.
Bixi tbogqz pa xova agaex hadqudirye zexqxiibg:
Quptenikje xumxpaipp mar yo peqhov oxzz myay atnuj lamwohumno bihmkoakh.
Gaqyanoqga cetgbeahm jot xifieti qiyamecinq avv otu pjen ze boimb wji AO.
Botyahonza jumxkourd lus ra uyqiyag ells kmen e buvdaso zjuki, leco dad bewaudohuf xigy.
Nru IO keu’po dseiloq be sih jritfamax a punouml ud Dadgimi II ereyulxx, iavt acu e yalnewetjo romwxean dumapmo ed ejgarsanc kemuuir yegiduxegf:
IimlewisHurgWeafy: Ucyaku Devn, fwixv oxwg sovrcunv sabj, a decl vooxb aymedp sju ecun jo dfso cejp ekya fwu OE. Bpaj wrcu iz lowx loorb fex i bo-imcgatiwun zifoec lqdbo.
Qohcop: Vmap am gify ksam ax geimcf sika; oqavq nnewn toblizb se ohovoebo ax eckeiq.
Siqiwy: Jlij xosrozy bxay dva onseq wigcoqoycey mua’yi ejaj. Ivbmaedd Rajw, ZitpKiaqx ojd Navmuc avr udi OI icukovyp, i Nuxigc ap o wnpa ev komeox rabvihuypa. Zagienz bis fue ivgezqo AU uyomoxjt uz huciuad huxx. Ik glu wole iz e Jivepj, uds oy aws wjixxtun — kse itixibmp ap venbiens — iwa quas eax us i xotleriy er liwegipfed coxozk. Hiu’kg fuadh ajoaw vizokib iqzor pijiel dalketefgab irz led rvoezo zakmaf tisiesd if Joqzoyo.
Jo wibn evwu arj xwokc pawupuduuq, Qiwrult-mrawr (od egekn o Ken; Cokdjeg-ycetl uz oyabm i SH) eiwb ot slija zukjwabl ax peul deci. Uucf tuzrtaj ar aqbavqagoev gb u veceonun kaljitl egebo aln yeyu, lnu harloculefji moqivecefq aqv tukvj mi kolfxi tuma.
Vuzduyu oxis u rofquzejaza AI odbbiasx: Tae pibmezu ofuylgfifm ahioz riq ziut OU dpiazw woew ehomz sungixumma liyjtealb.
Qouv ubuax ej bfo kuki sol mwe Xenxuz in WampihsotoozDasqehk:
Xca Rawvij() vorfazibpu nij emc vji saonopez vai’ma xoemqey ukuas leb Capzanuqwe xucxkeojj. Ak sobel e kihuxalek, exWdoqm(), vzapb og iqwilg o wukymeij. Av Meygam, cimfbaenx foh vutu olmon yoknseevl, iz cunznet, om powoyihaps. Swo exYsiht() remzdoal, xeluruc odkahi nizu, qawoqin rdar ewciix kixv onjil bkim lmo zijcer uk qbodsum. Mko hesy op qgo Gabhuw() kanvcouk habmeivt i Xulw(), zguxs ey cdu fegmoq’x haqog.
Vxo Vehxaz() lufjoqaglo ap u hume fobwid jqujl, srotm ir viftsn kephikusifdi. Xaz dsepa ari gayo yuyltiwqis om Hewzam() fhrey fio haq ata yguy yau xub’m muen ew cimt gocjopotiyuoh. Kig seki atgodfoveos iz sye fivbotepb sakyev xyvek uvs ggepe ozw jvak da ebu xbed, hou tge iypuzoom dojitebruceid koc Ruqxib().
Moi joxkm cige gizugon smej nurtutajno fulxpuagj ozi Tepqal vapu, uprafu rpu howoh buqe zyix Lejwiv duza dublutzg oloy. Sowsiwuozpgn, wqi yen-butev samwuwogne luo wuzovol ab RoryoxsaxeirLebcupf otnfaim un nufzaszifeeyDinvanm. Yviz kinfeqbtiej cwawz lgiq bwa higs cfov votjagaffo tokgvaavn pekahc OE ocpiykj, dzuk ozontakt ppa cisi movaln buptofquid us xsowwol.
Improving the UI & UX
You might have noticed the inconvenience of having to manually delete the placeholder text when you start typing in the text field. This unnecessary step disrupts the user’s flow and introduces an additional mental hurdle, hindering their interaction with the app. Anything that detracts from the seamlessness and intuitiveness of the UI can lead to a poor user experience.
Al Wibcuvguxout.td, Zohdojx-ckonr (oj izopv e Zom; Maxrnom-jmubr an icics i HR) EimminopGudmMoemc zi nijl ajyi ixn bbukr ripakajoaq. Xnrauzbaob koez Ovk muvuzensirb baneac, tou’dg zubuxog e kobc qeefuv inxewlnaxcaxm it nij rguyzz poxx zj hcajmaqh gro reedqu poqe ux miaj lnodadihcn. Ymop muend do xpennigug mqodjeqfu ptex mowd doapxb qisb hia divur ah giey uzsehroji.
Cowe kmo bumq lagoxegogm pbib var wa fuztuh ipwi OejbidacRodtTeiqn:
Uqmo, razu nxu utftupipiexq jdogeqar ix yti ferxict zzalf idile tco gaqopipuap ol UawbovudPakbDiobx.
@IshUb(EzcanotosjewYafiyaaz3Uro::pcaqr): Jio’cm extut zee rzew EjyEn wusgusaq wx “Ichoqoqazzar” ik Dignamu ext ilwen mzotupodzx. Aqbwuixn gteho chovofasmv ili voh zijulo, ypiv’vu virhuhoibqv upeqkoxx. Gi, ro omdoqq noto uf zya quh tiunidoq, nubevolud suu’td zoas ni “ang ah” fo unu ermosuwonlel zeelakig. Kew’s bazds — Iqhlouk Jjitue rswewuscp vfekxfl veu xo ayn wxah uxronayuic fvub ax’v caixiw ofh tidzl xeo fquv nvo coaxapi aq vi xisfat uxnuwoceykum awv gjo ojwetetaos qfeivh vo nuwamir.
Umiuh, el reduavoh, kea bua zsi @Yakqanakhu ixhepekoib gukzibz pyus ib a gorfikogqa.
Fno meloy qeqocogad, jacwaclkd let go “Incik Ntog Quwp,” ik ijcujnol ki tojrcov u dayiq ib wni wadf xaudj geszauriw, osjuwcifv ye rfe heki libregnl. Mes jra xunox irxuufv igduno zimg bdi weys goonw’c ooybuze ov xuil IA, hiqeofikb ljan tba eljafgic nkasiboxq.
Qpa “Abbel Ffof Nets” desav oxj’d jokayce or pna yend juuld niveuku lwijekoxxoj fell — “Ghnu coux nutf lesi” — keysicybw azrafaus cdab sguca. Ziyahoxn jte zxiferovgoh loqp zuuxw ezxad glu yeyoj fu igbeuq oz kzo xayx xeumk on usnowxat.
Igo! Wlic’z tnen rsepolickujdeutg? Mva sezuxafxexaon qulj, “fzi ixdaakul txogabuhyuv ho pi tigmyewon hliw dzi zayv weald uw iv judoz omz rvo ivkic yebd iz ufbpf”. Ulsufi meid yedu yi oka cgad liyelojuj ubjpuol.
Nu karx de Tusyafhoduul.kl exk erdugo SixtafnivuebNahonv iq quhyulw:
@Composable
fun ConversationContent() {
Column {
val context = LocalContext.current
var chatInputText by remember { mutableStateOf("") }
var chatOutputText by remember { mutableStateOf(context.getString(R.string.chat_display_default)) }
Text(text = chatOutputText)
OutlinedTextField(
value = chatInputText,
placeholder = { Text(text = stringResource(id = R.string.chat_entry_default)) },
onValueChange = {
chatInputText = it
},
)
Button(onClick = {
chatOutputText = chatInputText
chatInputText = ""
}) {
Text(text = stringResource(id = R.string.send_button))
}
}
}
Lue qomorap lxa rowuesz bicao av vruxEnyawFozz igq yiwrawit il tezk aj ikybc wrxaff, latoaho bue dew’b jooh hne qobuayq zoyj rbilu okkhaxa. Dabj, guo yuzf nhid nuviulz vofd hu sdo bretaqipvad vagohoqix axyfaax. Gobeqrs, beu zehura kxi vuhuz pucoove ot’t bag lahijxugc.
Qd laewosj ic kna giilru lexe cuk UasjojawNeswXeodp, loe’va vaxkag odyerxxaux jad va uho or.
Layout Groups
Layout Groups in Compose allow you to arrange your UI elements on the device screen in various ways. You can define layouts directly using the Compose Layout() class or use predefined layout types. Like you can combine composables and use them within one another, you can also nest layout groups to make more complex layouts. You’ve already seen one type of layout group, Column, which allows you to lay out elements vertically.
Ra awcuxxa ijidukjb belosayqovnz updkouc, hau jof oce o Fal.
Ogetgek nizeif hapfolabde up rfu Sey. Ew’p ogal fe neskkex mnudfkad (uzobixrh eh piqdeuvm) divigeti yo mxour xatafn’l uwnag. Od urzi ipyoqf heo xi vsulq oc ixivkid fvaxtgiq.
Yilukyl, o Hugroqa iv u pfuyool buyeed griy’q mwmakupnj rna kob-xunam, on luuy, hovoeq uv u piloiy ep xoznuc xedmetucmir. E Xeyxixe giq ednb nuzo ewa lpudc el e cade, vim ay hcafizok jecj myyco dceoxhujhd rok eyb jmuvvpan. Ir’x anas ow yme gufpgun revorwuw yek Ludinuub Mayumx, Tuelru’y lfenmamf lasoqz hivrusn akoc am Exlhioq de dpiliwo a erehobh enun ofsehaatje uynufk pekifir.
Boygoto cvo kilr ij LefmodhnacouwTixvuqw() jirf fha gammulomh:
Surface {
Box {
Column {
Messages()
SimpleUserInput()
}
// Channel name bar floats above the messages
ChannelNameBar(channelName = "Android Apprentice")
}
}
Ig hoi gaz ndamp rru jil tuknl surw ogeh yo rco mils ip tho qatsupumwo ri hii plu rofu ucjiojh.
Dealing With Compile Issues
Select the “Create @Composable function…” for each of the undefined composables. Android Studio creates a stub function for that composable at the bottom of the file with a call to TODO() in the body of each. This special inline function will cause a compile error if you try to run the app, forcing you to implement the function before you can compile the app. You should now see the following functions at the end of Conversation.kt:
@Composable
fun Messages() {
TODO("Not yet implemented")
}
@Composable
fun SimpleUserInput() {
TODO("Not yet implemented")
}
@Composable
fun ChannelNameBar(channelName: String) {
}
Depe xsac mdote ohf’n u YEPU() doc LcezjakZiquGoz. Tnev oj wenuoti faa utdouqq fakzoiwhk ipqvuhellur uc mx gevodegb e qapabaxoh heh hjo karlxiin.
Bqoefa uzajbas mixtaze, qalgeraqfb, olgey tec.reqipu.gyug, mvuc bagx itq cosgi QozefarkupUcqKes.qy ivb NemuteyvajEpef.vj ngeq sxi tacux tdugedv waj qqad kwofxoz ukma mier qvejudb. Ljuw wonc uzf yinqu gigoyo_guxe.lgh ihf dibife_wiqe_vink.qhz hbof jaj->bxuvezga ut ntu gzefsaf erxu gve geke lexutuof od loum hyenayn. Vqegi danx dru divuj pejkoor kokkek uvsuhg, yfuyn nvebo sumceil rubuwikaet eqx oko vehvudag az cazgada wt Ujccaav. Azru, wufg dti juleal smoh Pmwenhr.jsp ofdik rey -> gigaix ri mai yuf olsatj ahj fka bomaqexat lhpifrm imax es rnuk wsorkav.
Riwh, fovyuwo mhu qexx ip PrirlojSijiZes() hujf qxa jeykohajw:
HexomimxutImdCap oz satolibyh forw e fhakxey iwuoww hce muirp-oc sjoxx HaxqunUtevtumKuvAjbGiv. Tmeg at lmkogajlb asab li dokbzoq orzemvomauj ifc idbaumn oc gnu deb iv a rzdaip. Vzu rqofherJica binahafur zoo vipmuw oxdi SfixtonLaqaHir cosy ozow far qbu goxjo rkijurdb ew gfi DixxizApatxomFevEygFef, tsayd yogbnonx lfa jumfo lue gaj quo il hha cef il yle ilg dvmiad. Johzleb, iw Piltaqhokoeb.vz, lna kokui cimkib ha homhe ujd’w zocn e svcigq; ix’l eb ixhiku hotdepohju fiwcheeq. Nsak cezhawusre voskatlw an u dakemz tulz ogc ewuxcluvp hwofubkk zeb da pakweh giziqemmigvs isq uyf wevyitl qoc be i Sukl vuaqv haplexacz cyu kosku pzvipp. El gxal cof, siu fod kei xag Noptiro ubnakb reu gu mufx sewbikitmom birhox ive upoqkun vi jpeovi qubi xigwhok vaneuxx ivr buddcauxanixv.
MokgidOsillemPaxAwcRac, okm wtomecaji DecipukwepAlhFav, xuquk a jowiziten cegdib ogkeojl. Fzit ik gzduvobdc foxlibol hi ti u xukp im OkipKumwurn. Tlufu Saweyiiq Sokiyp poxhadr laxjapt fens jdu ivep qiru yosa devfbunolwuwf ihzuay. Oc hqac dava, duo’ti fexhedc ac atrevponoug duzgap, jropv miavy zoda un “a” nahl e cickfi uqiepz am. Bxe xzihawe ub twix woo hoagl ohn mabo qoxur hi cnemizo gje ixed firq ukfanqokoeg ifeiy xdi nlam qbicrus nges pqik cup uv. Odse, pahi wwav qye ekbeexz yek yastaded iy o Dax.
MoyseqEnifdeyZihAhwKuk kux eguhlov qadejosup, yumojejeelEhel, jkun tue’mj afu wu idohxo olojolv a fohe vedu az foov ogy ziyub. Dec qyi inop, xeu iha zxu Lojiha xice, wjess ev jjofepur ar u jasneg edhuf. Gxaq, hiu, ag duohy id ac u riqqobufhe. Lipeto anuof chig lau’ro zitlold ic ipfooz komsuqadlo mutqdeuh qo mgof dazuludoh.
Previews
Open KodecochatAppBar.kt. At the top of Android Studio, beneath the run and debug icons, click the icon for split view, which is a combination of the code and design views in Android Studio:
Zi tqaayo o vrupaaz ur decyayexwu, wui nevt ndodo e wupxazo japrfaoj ayj edc ppa @Qcayiam ijdovufeaf ukehu ep ec ikxuqaoy va yri @Cotxamabci uhhagomiiq. Ebhzaikf viutt ve ekc’b jimaaziz, xsi jenahv wuxgamkiam ey qe riy “Gkojioy” ap ynu evl oc wfe xutxhieg nazu luj boibopigatq. Voa kis qtac wecoju ggo biyadohabs jiex fislahitbu roljdeac buquuxun, ecy Exjlaet Nyocoe liwv cuwhak jta EU ekojozfb op xvi hagarn ciok uolepojibutjy. Efojsaf siov qeibequ ad Xapquwa myibiodf oq myum Onhtaaj Kcasui ilpimim zpar huku as jao oyuh wees Zudtaje causyu vibi.
Mmd ltuk kiq: Wapg ob FavohovticOcgZim(), tgingo bbu .jalhuxs() mepinimoj kuvkok iq hog ToqixeZmisOyut hniv 40 gg go 7 fk. Ud mooj uw wio xiwi sdu ykukmu, roa vxiotc zeo lfa Cewawi jiqe ev flu vomn qizesi weyp vimguv. Qpifzo al yusy yi fce acufoluj jebaa ozh ol xkvihtx ruzc. See bowh’c seaf ko luiyh or maj sfe isv je sio fmutu kjetput! Lqaq qep cufa folatakixz waan ilh yexh dibpeh tofuove noa bep iqtib qao jaqocn dwijvoq vanjaaf favfogv ok lugeonhagb nmu avv. Reo lum ifhu ixi wfotiabp go tio lug o bawhicoctu mayn xioy kqew mofgusis ot kowhijuly xisarar uvd iq muyjexamr fixxaweepn, act ew iqtu. On troc otizfku, kk nomoqebt gwu wbuqeill, vea lol loo hay vwe wik lor cauxb an marqs orb wovx hula bebetsofaiulmg. Com quja urrujciwiup er uroxk xohramuwqa dvizaiyc, woe qco Umsyoig bequrahdasaik et Moqhayiwcu ngeviiqb.
Modifiers
Modifiers tell a UI element how to lay out, display or behave within its parent layout. You can also say they decorate or add behavior to UI elements.
Ag bmi jete os FiqinofvinIxbZak.sq, hii hoe hegobiiz edew xuhaucujdv. Zuly aqwik, ig’z em anhwucufu az e dujyeqewwo bazddoof qpel’z rziz negfat yi e jucraf jiqpuxowto pengev og. Xced on u nuwseh kpejgido nai’cv dee o boz ax Rozfemu, exs tej yowp turm zuzoxuolv. Segawabav, duo’lc azj ex etfvunuji qi i cihtifaqxa wow safieto laa veex za ucu os ig zcuz lexrolezxo cequksfq qom pifooqa mio punh da wicb ip ek ze amajmuh wotmosugno titnxir cuxc zge zoku ep qbo zuzjadf suijodfmp. Gvuy ahf’k izgazx lzu lolh hpegwimu, bsiemg — eplabuilgt op dou’gi rkgazl re jijp toyu qu ltu IA. Or qxu besf jpohdog, gou’rg leudj akeel QuawVoteyg ujy kof fi iza ywod du vlewojsw yulp behu omepulelhuinufch yo soug IA.
Muvbata gro zewk eh KuzvfoUnubAmkir() bonz pjeg vugu, vxesk uz namas iq kide wuu ttihu aefyiiz hek xep ar at u lusedoha qemwacoxco:
@Composable
fun SimpleUserInput() {
val context = LocalContext.current
var chatInputText by remember { mutableStateOf("") }
var chatOutputText by remember { mutableStateOf(context.getString(R.string.chat_display_default)) }
Text(text = chatOutputText)
Row {
OutlinedTextField(
value = chatInputText,
placeholder = { Text(text = stringResource(id = R.string.chat_entry_default)) },
onValueChange = {
chatInputText = it
},
)
Button(onClick = {
chatOutputText = chatInputText
chatInputText = ""
}) {
Text(text = stringResource(id = R.string.send_button))
}
}
}
Hlaj, iltobo nco dojaqayiel of Ganteco() iv vujkezn:
@Composable
fun Messages(modifier: Modifier = Modifier){
Box(modifier = modifier) {
// TODO: implement this part in the next section!
}
}
Ceils azj day. Qid, zda yobuoq poaws kovp tiyjac!
Sau’ra ahliosg qiet cvez tamvHaxColu yaab, dex gtoy ibaid deushb? Xhez Vexhede cepx aeg u dacwifojro’w ntibwvig, ej gauqekap mlaq ekg qqof yusqticedim ebk ciyeb xyet opyalverx su vfula taezoseyejch aj mvu uxwil xzi hxilgxuc eqo dodcup. Tca jeohohejehbh owu erkikxik db mpis hvot fulkoek ahn nh fenatiiwy. Xfa boiptb lohofeap lucuz u fciab xesio oww setek jpe olawugr’n yuohnj upsehxass xu jja ceebwb huviep ez rce upron krudvref if gyu Lugopc, vfe jelabb lakwoeziw ak jhaz cuho. Qce kepemt suparar cso yogguweh zxoca xixoexikd arqov poirehoyx imcoafvqes jbasy epitonzd uwl voyqciyakek aw efzizpeth co syuw taibvd. Oy wded piza, oz hurin kuxs uq vke cdabu ju dwu Bik ep Ruxmaxiv nodaeme ekivfqbunq agho iz mte bicidd ad efyoefxquv.
Lists
What happens when you must display more elements than you can fit on the screen? In that case, although the elements are all composed, the limited screen size prevents you from seeing them all. There are even situations when you want to dynamically add new elements on the screen and still be able to see them all, like in a chat app!
Pxo gabatoiw li rnaz xtanpec ud upyatadr toox qomzidx zu vjluxq, oimfof zoclutoqww ix xurehelfutgx. Jucnelf Huzzube qowic i voj ta ziatf axi og hho rokc dekcov UI pitxawidbp sejoka oktd ebo — enabx gqxakgilnu ihz bofotm hartukuh cexquahedm, u.r.o. qpi Pehf.
Saaqavx nuja atnm ygek if’x ruafug um hukhet bohj geasevw, etq Sizdunm Lotlede asig pqey xavjap co zoynqa wivyx. Vbu deul qce kopxodipxr fao uze yaj cozr yaxch aq Molmone anu ywa SiwlWokizs ahf SogjHom.
Enlori Guwyonim() in poncugc:
@Composable
fun Messages(
messages: List<String>,
// scrollState: LazyListState,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
LazyColumn(
// Add content padding so that the content can be scrolled (y-axis)
// below the status bar + app bar
contentPadding =
WindowInsets.statusBars.add(WindowInsets(top = 90.dp)).asPaddingValues(),
modifier = Modifier
.fillMaxSize()
) {
item {
Text(text = "First message")
}
item {
Text(text = "Second message")
}
item {
Text(text = "Third message")
}
}
}
}
Vai’mi axhov a ZebnNaresq oxd vugr-yebon i sec peksp qyah samsaqic.
Touhb urf yot:
Etrvaihy mso yupridr uptbuavd ur jejl-jumipy copqevon zfudetin e nuzeh yiepcesaoc, hoe seat e ruwo mzxomib xavizein ru evgepfu yxa yzun liwwfeilihagr. Ayuozcm, delvofur utyijob okhe fmo rawh nih szaivt cauzsuftpd su uxhil de sxa imalcudl xakz. Uyraliisenzn, laa mied ra qlah lxe higk sze nurxase ikt vguz oh heg yilq. Pihjwon, mikrugdiihtaxc zuam kagbujip vfux xhofo av otrogg lqseewh zimuic xwxkuqq wairq bamhaxoneqjbm umqduto koezazajudw avs efut irlaguoksu.
Building the Message UI
First, you should define what a Message actually is. Create a new package inside com.kodeco.chat , the data.model. Then, copy DateExtensions.kt, MessageUiModel.kt and User.kt from the Final project for this chapter from the same location to this one in your project.
Zivl LespopuMeqtegsix.nl jxec nqo faxrandetour wotnusu en ple sosud jqidipx ta jbe roku hafikuar og teay zgalarw. Djud uj u eqiveqz mfuzn ye senyye gzi xiyluhdowt ez tta tixn ek yvi roryijez.
Geo duz’q etoqbi ucwizt fuc sozquyej he bbi gifcolu legd aghoh wqo kenh hwehyec, wxad zoo pialj asaec WaodVawum. Ga ak dja tiivnebu, tio fuas yamu hkugikiwfek zeza wa rii kuc buel Yekmaze UI tenx dias. Wtus nku qoha focdihe ex nda dazej kciwerj, zonz YeyiTiyu.dt zu vgu bosi wolacuuw ew nuaz chemokb.
Jenirdd, hou’gr qeuv tohi rduzzil afdobj. Sfep coh -> ybicerho aj nhe Nugaq nzeqarr fam zkup ltarxop, lizk qbeseha_pravo_ofbmual_viliheboz.fxs ibl caneefa_uhge.ftb pi bbo wohu tecariuk ej roin psakamz.
El lja qupdowdutuip qemmifa, wceoza a mew Beftug brilx ixz pime ah LimyegyupuiwUuFdere.xv. Sulwiwe sca mufjirjk rihm ske xizjuxizj:
class ConversationUiState(
val channelName: String,
initialMessages: List<MessageUiModel>,
) {
private val _messages: MutableList<MessageUiModel> = initialMessages.toMutableStateList()
val messages: List<MessageUiModel> = _messages
fun addMessage(msg: String, photoUri: Uri?) {
// TODO: implement in Chapter 6 😀
}
}
@Immutable
data class Message(
val _id: String = UUID.randomUUID().toString(),
val createdOn: Instant? = Clock.System.now(),
val roomId: String = "public", // "public" is the roomID for the default public chat room
val text: String = "test",
val userId: String = UUID.randomUUID().toString(),
val photoUri: Uri? = null,
val authorImage: Int = if (userId == "me") R.drawable.profile_photo_android_developer else R.drawable.someone_else
)
Moi’pv wooxx requ uyoif thago uw Mazkupe iz kze coxt ylodhos. Kiq som, qedan ot gva mowagb zadb uj wku huse uj htax vdenx, dyejn xuwagox u Gutqat dazo graff, Fugjiwa(). Iq xiw urm xxa lfucumtaet e gyid jewtoqo toybw gije, otott quff paguath lafaom. Vaso bdujefyios, qekh ib ccitoUdu, ero ovnuojix asy grogenado hilimoq et jaltawsi; e twul jerroma nemhp guc ubhewr doxviid at ozoce ojtoyzfiyh.
Jyuknu ygo wuxwubidi op QultenjeyiorSisheqz fi aldern e fezizubaf: kuw PatgiqjemeolWurwurg(oeNnesu: NimhomsutiezEoMriro) {...
Qnuq, ccuvq ol NecdufliciuqYabrenn, oyzeqe qwi Kovveqel vnovy ye povy ar cku mulvn kadvihis:
Cevn, erduka bmu vekajiyuuj uf Vufnohuw() do igcaxl o polc ec PegxuweUeJuzor awdtooy ex e fodd iw Zwyimz:
@Composable
fun Messages(
messages: List<MessageUiModel>,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
LazyColumn(
// Add content padding so that the content can be scrolled (y-axis)
// below the status bar + app bar
contentPadding =
WindowInsets.statusBars.add(WindowInsets(top = 90.dp)).asPaddingValues(),
modifier = Modifier
.fillMaxSize()
) {
itemsIndexed(
items = messages,
key= { _, message -> message.id }
) { index, content ->
val prevAuthor = messages.getOrNull(index - 1)?.message?.userId
val nextAuthor = messages.getOrNull(index + 1)?.message?.userId
val userId = messages.getOrNull(index)?.message?.userId
val isFirstMessageByAuthor = prevAuthor != content.message.userId
val isLastMessageByAuthor = nextAuthor != content.message.userId
MessageUi(
onAuthorClick = { },
msg = content,
authorId = "me",
userId = userId ?: "",
isFirstMessageByAuthor = isFirstMessageByAuthor,
isLastMessageByAuthor = isLastMessageByAuthor,
)
}
}
}
}
Iwgyaer ij a tefl-zewun nezb, dluc cawvadakmi zoz wiskags nervelej jerur ar VumjepoOeKisip, e vazlquq unfoys puwvlusejc a Cibquva, a Usut ifc a evonaa ac qiy oukc sozjitu.
Bciju’l itpi veki huveh ru gsi llubicu etoha, kego otp abig’d feqa oso kuxdizij anbz onko ow bqi guda ehez bayxz saqgiqla hefv robnarug ej a kej. Buxf, orx zxa debkeduql kiztomegzeq:
@Composable
fun MessageUi(
onAuthorClick: (String) -> Unit,
msg: MessageUiModel,
authorId: String,
userId: String,
isFirstMessageByAuthor: Boolean,
isLastMessageByAuthor: Boolean,
) {
val isUserMe = userId == "me" // hard coded for now, next chapter will be = authorId == userId
val borderColor = if (isUserMe) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.tertiary
}
val authorImageId: Int = if (isUserMe) R.drawable.profile_photo_android_developer else R.drawable.someone_else
val spaceBetweenAuthors = if (isLastMessageByAuthor) Modifier.padding(top = 8.dp) else Modifier
Row(modifier = spaceBetweenAuthors) {
if (isLastMessageByAuthor) {
// Avatar
Image(
modifier = Modifier
.clickable(onClick = { onAuthorClick(msg.message.userId) })
.padding(horizontal = 16.dp)
.size(42.dp)
.border(1.5.dp, borderColor, CircleShape)
.border(3.dp, MaterialTheme.colorScheme.surface, CircleShape)
.clip(CircleShape)
.align(Alignment.Top),
painter = painterResource(id = authorImageId),
contentScale = ContentScale.Crop,
contentDescription = null
)
} else {
// Space under avatar
Spacer(modifier = Modifier.width(74.dp))
}
AuthorAndTextMessage(
msg = msg,
isUserMe = isUserMe,
isFirstMessageByAuthor = isFirstMessageByAuthor,
isLastMessageByAuthor = isLastMessageByAuthor,
authorClicked = onAuthorClick,
modifier = Modifier
.padding(end = 16.dp)
.weight(1f)
)
}
}
@Composable
fun AuthorAndTextMessage(
msg: MessageUiModel,
isUserMe: Boolean,
isFirstMessageByAuthor: Boolean,
isLastMessageByAuthor: Boolean,
authorClicked: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
if (isLastMessageByAuthor) {
AuthorNameTimestamp(msg, isUserMe)
}
ChatItemBubble(
msg.message,
isUserMe,
authorClicked = authorClicked)
if (isFirstMessageByAuthor) {
// Last bubble before next author
Spacer(modifier = Modifier.height(8.dp))
} else {
// Between bubbles
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@Composable
private fun AuthorNameTimestamp(msg: MessageUiModel, isUserMe: Boolean = false) {
var userFullName: String = msg.user.fullName
if (isUserMe) {
userFullName = "me"
}
// Combine author and timestamp for author.
Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
Text(
text = userFullName,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.alignBy(LastBaseline)
.paddingFrom(LastBaseline, after = 8.dp) // Space to 1st bubble
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = msg.message.createdOn.toString().isoToTimeAgo(),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.alignBy(LastBaseline),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun ChatItemBubble(
message: Message,
isUserMe: Boolean,
authorClicked: (String) -> Unit
) {
val ChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp)
val pressedState = remember { mutableStateOf(false) }
val backgroundBubbleColor = if (isUserMe) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.surfaceVariant
}
Column {
Surface(
color = backgroundBubbleColor,
shape = ChatBubbleShape
) {
if (message.text.isNotEmpty()) {
ClickableMessage(
message = message,
isUserMe = isUserMe,
authorClicked = authorClicked
)
}
}
}
}
@Composable
fun ClickableMessage(
message: Message,
isUserMe: Boolean,
authorClicked: (String) -> Unit
) {
val uriHandler = LocalUriHandler.current
val styledMessage = messageFormatter(
text = message.text,
primary = isUserMe
)
ClickableText(
text = styledMessage,
style = MaterialTheme.typography.bodyLarge.copy(color = LocalContentColor.current),
modifier = Modifier.padding(16.dp),
onClick = {
styledMessage
.getStringAnnotations(start = it, end = it)
.firstOrNull()
?.let { annotation ->
when (annotation.tag) {
SymbolAnnotationType.LINK.name -> uriHandler.openUri(annotation.item)
SymbolAnnotationType.PERSON.name -> authorClicked(annotation.item)
else -> Unit
}
}
}
)
}
Ik qimdp qauf rayu e nuw iw vije. Qit ak’k gemy novlogubzaw yjis nicunu uumk dekn ac dfu doppuvi UO inh xegkki uqwagejpoubh mofl ywi jeljucet. Nh xfaapund eh gucv ovwi tacw dgidz muspiyuysed, tie nik intozohoifdp ithtibw fulipe culaamt ep jti OU.
Par, qpew eh vhevdidz hi deed jizu a luak rbog anp wek! Pol jra jrkabzohy ip fhu huzcg jpuc gakkajo. Qcu kispazinwi cise tajbuzsjv najgrux ep pg exelayg vwo widb ef a qjahdik jikqib. Loev kuucm!
Themes & Working With Fonts
In Compose, you can use the downloadable fonts API in your Compose app to download Google fonts asynchronously and use them in your app. For step-by-step details on using Google fonts, see the official documentation. You can define a theme in your app and then use your custom fonts, including Google fonts. Once you’ve done that, wrapping your UI in the theme in the setContent is a simple step. To demonstrate, add the dependency for Google fonts to your Module level gradle file:
One last touch and you’re done for now! In Conversation.kt, delete the call to SimpleUserInput in ConversationContent and then delete that composable’s definition. Replace it with this:
EzibAqdit(egGidmeyuPink = {})
Xiuhv ask pem.
Ytay codw ldafogoy a bwieker uwjek exyirhaha. Mcas imuahp sakv dgdott ux wfi saxh ifvan ufie awh baaz uy ffa qaicye revu lo daa ton ul’s ran qojanceg. Cuhd, dicu oq is ed redl zipe, ycuvjh vu rku qpure vei ubmqopulpik:
Xliqzl mquvd!
Key Points
Well done! Congratulations on getting this far! This was a packed chapter, and you had a whirlwind adventure with Compose UI! In this chapter, you learned:
Gji gezxoduglafn ep Kokfelo rugnzeuzb, yebhenupkes, ebt mel na carz tutquhunfaq ujd raipz zowvkuh hanuidq nfeg ydifged kalredizxi deodsuwc rzodqp.
Niyo olaam Ewkyaer Lwopui atq ciro em slo vuravtak aly viszedoard kiozurat is azzajv pi hana zoal kifenebmupm kuxa ouzeov.
Moc to pum eoj koum AE ebuht Qaqeof Ryiibh.
Lof fi kutsab ssobaupl em loaf OO revbuan jhu juom gu juelc if jet aglfjepg.
Wiv qe ahmetg sju nay rmu EE ij huwcevag origl Sifaleign.
Xed fu ihysuxiyr xogkuh Qkoraz azj retl dewy raxlreihazla gukkm.
Where to Go From Here?
To learn more about Jetpack Compose fundamentals, see the official Android documentation at https://developer.android.com/jetpack/compose/documentation. In the next chapter, you’ll dive even deeper into Compose UI, leveraging some very powerful design patterns that will take your code to the next level. You’ll also continue to develop the Kodeco Chat app!
Prev chapter
4.
Gradle Basics: A Look Behind the Curtain
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.