SDK Guides
...
Task App Tutorials for Kotlin
Jetpack Compose: Defining UI
kotlin android task tutorial walkthrough example jetpack compose once you've completed the prerequisite steps, build the ui for your task app with jetpack compose prerequisites create your ditto account and app ( docid\ hk2s3ld8sj ti5snxwkq1 ) install the required prerequisites ( docid\ epbxiu5voq5l44ink8f6d ) install the latest version of https //developer android com/studio install and set up create the app https //docs ditto live/android/tutorial/jetpack compose/setup#1 1 create the app click file and select new project from the new project modal, enter the following while recommended, the following values are not essential for completing this tutorial name "tasks" package name "live ditto tasks" save location choose a directory minimum sdk "api 23 android 6 (marshmallow)" 3 finally, click finish and wait for android studio to set up the project install ditto https //docs ditto live/android/tutorial/jetpack compose/setup#1 2 install ditto android requires requesting permission to use bluetooth low energy and wifi aware follow the docid\ epbxiu5voq5l44ink8f6d environment setup and android platform permissions setup sections to setup your android environment add jetpack compose dependencies in your app module build gradle file, add the additional dependencies build gradle dependencies { // existing dependencies // add the following dependencies below // jetpack compose view model implementation 'androidx lifecycle\ lifecycle viewmodel compose 1 0 0 alpha07' // live data implementation "androidx compose runtime\ runtime livedata $compose version" // jetpack compose navigation implementation "androidx navigation\ navigation compose 2 4 0 alpha07" } add vector icons https //docs ditto live/android/tutorial/jetpack compose/setup#1 4 add vector icons we will need a couple of additional icons to show the tasks' completed, and incompleted states we will reference these vector resources in our code later right click res > drawable and add a new vector asset click the "add" icon and select it for size `24` repeat the previous steps for adding a circle filled (icon name "brightness 1") circle outline (icon name "panorama fish eye") you should have have 3 additional drawables with the following names ic baseline add 24 xml ic baseline brightness 1 24 xml ic outline panorama fish eye 24 xml configure ditto create application class https //docs ditto live/android/tutorial/jetpack compose/configure ditto#2 2 create application class typically, applications with ditto will need to run ditto as a singleton to construct ditto it'll need access to a live android context since the application class is a singleton and has the necessary context , we can create a subclass called tasksapplication kt add a companion object and declare var ditto ditto? = null this will create a static variable that we can always access throughout our entire application in the override fun oncreate() , construct ditto with defaultandroiddittodependencies as follows tasksapplication kt import android app application import live ditto ditto import live ditto dittoidentity import live ditto android defaultandroiddittodependencies class tasksapplication application() { companion object { var ditto ditto? = null; } override fun oncreate() { super oncreate() // construct a defaultandroiddittodependencies object with the applicationcontext val androiddependencies = defaultandroiddittodependencies(applicationcontext) // for this example we will use a development identity val identity = dittoidentity onlineplayground( dependencies = androiddependencies, token = "replace me", appid = "replace me",) ditto = ditto(androiddependencies, identity) } } now you will be able to access this ditto anywhere in your application like so val docs = tasksapplication ditto!! store\["tasks"] find("!isdeleted") exec() start ditto sync when android studio created the project, it should have created a file called mainactivity kt in this file, we will take the singleton tasksapplication ditto!! and begin to start the sync process with startsync() the app will show a toast error if startsync encounters a mistake don't worry if an error occurs or if you omit this step, ditto will continue to work as a local database however, it's advised that you fix the errors to see the app sync across multiple devices mainactivity class mainactivity componentactivity() { val ditto = tasksapplication ditto override fun oncreate(savedinstancestate bundle?) { super oncreate(savedinstancestate) try { ditto!! startsync() } catch (e dittoerror) { // 2 toast maketext( this\@mainactivity, """ uh oh! there was an error trying to start ditto's sync feature that's okay, it will still work as a local database this is the error ${e localizedmessage} """ trimindent(), toast length long ) show() } setcontent { // } ditto!! store\["tasks"] find("isdeleted == true") evict() } fun checkpermissions() { val missing = dittosyncpermissions(this) missingpermissions() if (missing isnotempty()) { this requestpermissions(missing, 0) } } override fun onrequestpermissionsresult( requestcode int, permissions array\<out string>, grantresults intarray ) { super onrequestpermissionsresult(requestcode, permissions, grantresults) // regardless of the outcome, tell ditto that permissions maybe changed ditto? refreshpermissions() } } create a task data class ditto is a document database, which represents all of its rows in the datastore in a json like structure in this tutorial, we will define each task like so { " id" "123abc", "body" "get milk", "iscompleted" true } these task documents will all be in the "tasks" collection we will be referencing this collection throughout this tutorial with val taskscollection = tasksapplication ditto!! store\["tasks"] ditto documents have a flexible structure oftentimes, in strongly typed languages like kotlin, we will create a data structure to give more definition to the app create a new kotlin file called task kt in your project data class task( val id string = uuid randomuuid() tostring(), val body string, val iscompleted boolean ) { constructor(document dittodocument) this( document\[" id"] stringvalue, document\["body"] stringvalue, document\["iscompleted"] booleanvalue ) { } } this data class takes a dittodocument and safely parses out the values into native kotlin types we also added an additional constructor that allows for us to preview data without requiring ditto so now in our application if we want a list\<task> we write the following code val tasks list\<task> = tasksapplication ditto!! store\["tasks"] find("!isdeleted") exec() map { it > task(it) } once we set up our user interface, you'll notice that reading these values becomes a bit easier with this added structure navigation creating a root navigation this application will have two screens which are just jetpack compose views taskslistscreen kt a list where we can show the tasks editscreen kt where we can edit, create, and delete the task create a file called root kt file and add a navigation controller and a navhost to the root of our application you'll notice references to taskslistscreen and editscreen , don't worry we will add them there the root of our application hosts a navcontroller which we use to switch between each screen there are 3 routes tasks which will bring you the taskslistscreen tasks/edit which will bring you the editscreen but will be for creating tasks notice that we will give a null to the taskid this same screen will be in a "create" mode if the taskid is null tasks/edit/{taskid} which will bring you the editscreen but will be for editing tasks notice that there is a "{taskid}" portion to this route similar to web apps, we will parse out a task id string from the route and use that for editing root kt @composable fun root() { val navcontroller = remembernavcontroller() // a surface container using the 'background' color from the theme surface(color = r colors white) { navhost(navcontroller = navcontroller, startdestination = "tasks") { composable("tasks") { taskslistscreen(navcontroller = navcontroller) } composable("tasks/edit") { editscreen(navcontroller = navcontroller, taskid = null) } composable("tasks/edit/{taskid}") { backstackentry > val taskid string? = backstackentry arguments? getstring("taskid") editscreen(navcontroller = navcontroller, taskid = taskid) } } } } https //docs ditto live/android/tutorial/jetpack compose/navigation#3 1 creating a root navigation setting the mainacivity to render root now back in the mainacivity kt file look for setcontent{ } and replace it completely with the following highlighted lines class mainactivity componentactivity() { override fun oncreate(savedinstancestate bundle?) { super oncreate(savedinstancestate) val ditto = tasksapplication ditto try { ditto!! startsync() } catch (e dittoerror) { toast maketext( this\@mainactivity, """ uh oh! there was an error trying to start ditto's sync feature that's okay, it will still work as a local database this is the error ${e localizedmessage} """ trimindent(), toast length long ) show() } // highlight start setcontent { root() } // highlight end } } show the list of tasks in the last part of the tutorial, we referenced a class called taskslistscreen this screen will show a list\<task> using a jetpack compose column create a taskrow views each row of the tasks will be represented by a @composable taskrow which takes in a task and two callbacks which we will use later if the task iscompleted is true , we will show a filled circle icon and a strike through style for the body if the task iscompleted is false , we will show a filled circle icon and a strike through style for the body if the user taps the icon , we will call a ontoggle ((task task) > unit)? , we will reverse the iscompleted from true to false or false to true if the user taps the text , we will call a onclickbody ((task task) > unit)? we will use this to navigate to the editscreen for brevity, we will skip discussions on styling as it's best to see the code snippet below we've also included included a @preview taskrowpreview which allows you to quickly see the end result with some test data taskrow\ kt @composable fun taskrow( task task, ontoggle ((task task) > unit)? = null, onclickbody ((task task) > unit)? = null) { val iconid = if (task iscompleted) r drawable ic baseline brightness 1 24 else r drawable ic baseline panorama fish eye 24 val color = if (task iscompleted) r color purple 200 else android r color darker gray var textdecoration = if (task iscompleted) textdecoration linethrough else textdecoration none row( modifier fillmaxwidth() padding(12 dp) ) { image( imagevector vectorresource( id = iconid ), "localized description", colorfilter = tint(colorresource(id = color)), modifier = modifier padding(end = 16 dp) size(24 dp, 24 dp) clickable { ontoggle? invoke(task) }, alignment = centerend ) text( text = task body, textdecoration = textdecoration, fontsize = 16 sp, modifier = modifier alignbybaseline() fillmaxwidth() clickable { onclickbody? invoke(task) }) } } / used to preview the code / @preview(showbackground = true) @composable fun taskrowpreview() { column() { taskrow(task = task(uuid randomuuid() tostring(), "get milk", true)) taskrow(task = task(uuid randomuuid() tostring(), "do homework", false)) taskrow(task = task(uuid randomuuid() tostring(), "take out trash", true)) } } create a @composable tasklist next, we will need to show a list\<task> by looping over it and creating a taskrow for each element this gives us a scrollable list behavior the tasklist takes in a list\<task> and loops over it in a column with a foreach loop each iteration of the loop will render a task(task) we've also added onclickbody and ontoggle callback that matches the task onclickbody and task ontoggle functions we've also included a tasklistpreview so that you can add some test data @composable fun taskslist( tasks list\<task>, ontoggle ((taskid string) > unit)? = null, onselectedtask ((taskid string) > unit)? = null ) { column() { tasks foreach { task > taskrow( task = task, onclickbody = { onselectedtask? invoke(it id) }, ontoggle = { ontoggle? invoke(it id) } ) } } } @preview( showbackground = true, showsystemui = true, device = devices pixel 3 ) @composable fun tasklistpreview() { taskslist( tasks = listof( task(uuid randomuuid() tostring(), "get milk", true), task(uuid randomuuid() tostring(), "get oats", false), task(uuid randomuuid() tostring(), "get berries", true), ) ) } create a @composable taskslistscreenviewmodel the entire screen's data will be completely controlled by a jetpack compose viewmodel the use of viewmodel is a design pattern called https //proandroiddev com/architecture in jetpack compose mvp mvvm mvi 17d8170a13fd which strives to separate all data manipulation (model and viewmodel) and data presentation (ui or view) into distinct areas of concern when it comes to ditto, we recommend that you never include references to ditto in @composable types all interactions with ditto for insert , update , find , remove and observelocal should be within a viewmodel now create a new file called taskslistscreenviewmodel kt add a property called val tasks mutablelivedata\<list\<task>> = mutablelivedata(emptylist()) this will house all of our tasks that the taskslistscreen can observelocal for changes when any mutablelivedata type changes, jetpack compose will intelligently tell @composable types to reload with the necessary changes create a livequery and subscription by observing/subscribing to all the tasks documents remember our task data class that we created? we will now map all the dittodocument to a list\<task> and set them to the tasks ditto's dittolivequery and dittosubscription types should be disposed by calling close() once the viewmodel is no longer necessary for a simple application, this isn't necessary but it's always good practice once you start building more complex applications taskslistscreenviewmodel kt class taskslistscreenviewmodel viewmodel() { val tasks mutablelivedata\<list\<task>> = mutablelivedata(emptylist()) val livequery = tasksapplication ditto!! store\["tasks"] find("!isdeleted") observelocal { docs, > tasks postvalue(docs map { task(it) }) } val subscription = tasksapplication ditto!! store\["tasks"] find("!isdeleted") subscribe() fun toggle(taskid string) { tasksapplication ditto!! store\["tasks"] findbyid(dittodocumentid(taskid)) update { mutabledoc > val mutabledoc = mutabledoc? let { it } ? return\@update mutabledoc\["iscompleted"] set(!mutabledoc\["iscompleted"] booleanvalue) } } override fun oncleared() { super oncleared() livequery close() subscription close() } } https //developer android com/jetpack/compose/state#viewmodel state one of the features that we added to the taskrow is to toggle the iscompleted flag of the document once a user clicks on the circle icon we will need to hook this functionality up to edit the ditto document this toggle function will take the task , find it by its id and switch its iscompleted value to the opposite value taskslistscreenviewmodel kt // fun toggle(taskid string) { tasksapplication ditto!! store\["tasks"] findbyid(dittodocumentid(taskid)) update { mutabledoc > val mutabledoc = mutabledoc? let { it } ? return\@update mutabledoc\["iscompleted"] set(!mutabledoc\["iscompleted"] booleanvalue) } } notice that we do not have to manipulate the tasks value calling update will automatically fire the livequery to update the tasks you can always trust the livequery to immediately update the val tasks mutablelivedata\<list\<task>> there is no reason to poll or force reload ditto will automatically handle the state changes create the taskslistscreen finally, let's create the taskslistscreen this @composable is where the navcontroller , taskslistscreenviewmodel and tasklist all come together the following code for taskslistscreen is rather small but a lot of things are happening follow the steps and look for the appropriate comments that line up to the numbers below the taskslistscreen takes a navcontroller as a parameter this variable is used to navigate to editscreen depending on if the user clicks a floatingactionbutton or a taskslistscreen onclickbody see the create a reference to the taskslistscreenviewmodel with val taskslistviewmodel taskslistscreenviewmodel = viewmodel(); now let's tell the @composable to observe the viewmodel tasks as state object with val tasks list\<task> by taskslistviewmodel tasks observeasstate(emptylist()) the syntax by and function observeasstate(emptylist()) will tell the @composable to subscribe to changes for more https //developer android com/jetpack/compose/state#viewmodel state we'll add a topappbar and extendedfloatingactionbutton along with our tasklist all wrapped in a scaffold view scaffold are handy ways to layout a more "standard" android screen https //developer android com/reference/kotlin/androidx/compose/material/package summary#scaffold(androidx compose ui modifier,androidx compose material scaffoldstate,kotlin function0,kotlin function0,kotlin function1,kotlin function0,androidx compose material fabposition,kotlin boolean,kotlin function1,kotlin boolean,androidx compose ui graphics shape,androidx compose ui unit dp,androidx compose ui graphics color,androidx compose ui graphics color,androidx compose ui graphics color,androidx compose ui graphics color,androidx compose ui graphics color,kotlin function1) set the extendedfloatingactionbutton onclick handler to navigate to the task/edit route of the navcontroller use our tasklist inside of the scaffold content pass the tasks from step 2 into the tasklist bind the tasklist ontoggle to the taskslistviewmodel toggle bind the tasklist onclickbody to the navcontroller navigate("tasks/edit/${task id}") this will tell the navcontroller to go the editscreen (we will create this in the next section) taskslistscreen kt @composable fun taskslistscreen(navcontroller navcontroller) { // 2 val taskslistviewmodel taskslistscreenviewmodel = viewmodel(); // 3 val tasks list\<task> by taskslistviewmodel tasks observeasstate(emptylist()) // 4 scaffold( topbar = { topappbar( title = { text("tasks jetpack compose") }, backgroundcolor = colorresource(id = r color purple 700) ) }, floatingactionbutton = { extendedfloatingactionbutton( icon = { icon(icons filled add, "") }, text = { text(text = "new task") }, // 5 onclick = { navcontroller navigate("tasks/edit") }, elevation = floatingactionbuttondefaults elevation(8 dp) ) }, floatingactionbuttonposition = fabposition end, content = { taskslist( // 6 tasks = tasks, // 7 ontoggle = { taskslistviewmodel toggle(it) }, // 8 onclickbody = { task > navcontroller navigate("tasks/edit/${task}") } ) } ) } editing tasks our final screen will be the editscreen the editscreen will be in charge of 3 functions editing an existing task creating a task and inserting it into the tasks collection deleting an existing task creating the @composable editform the editform is a simple layout that includes a constructor candelete boolean which determines whether or not to show a delete button a body string and iscompleted boolean respective callback parameters for changes in the textfield and save and delete button (see steps 4 to 6) an textfield which we use to edit the task body a switch which is used to edit the task iscompleted a button for saving a task a button for deleting a task we've also included a @preview of the editform editform kt @composable fun editform( // 1 candelete boolean, // 2 body string, // 3 onbodytextchange ((body string) > unit)? = null, // 2 iscomplete boolean = false, // 3 oniscomplete ((iscompleted boolean) > unit)? = null, // 3 onsavebuttonclicked (() > unit)? = null, // 3 ondeletebuttonclicked (() > unit)? = null, ) { column(modifier = modifier padding(16 dp)) { text(text = "body ") // 4 textfield( value = body, onvaluechange = { onbodytextchange? invoke(it) }, modifier = modifier fillmaxwidth() padding(bottom = 12 dp) ) row( modifier = modifier fillmaxwidth() padding(bottom = 12 dp), arrangement spacebetween ) { text(text = "is complete ") // 5 switch(checked = iscomplete, oncheckedchange = { oniscomplete? invoke(it) }) } // 6 button( onclick = { onsavebuttonclicked? invoke() }, modifier = modifier padding(bottom = 12 dp) fillmaxwidth(), ) { text( text = "save", modifier = modifier padding(8 dp) ) } if (candelete) { // 7 button( onclick = { ondeletebuttonclicked? invoke() }, colors = buttondefaults buttoncolors( backgroundcolor = color red, contentcolor = color white), modifier = modifier fillmaxwidth(), ) { text( text = "delete", modifier = modifier padding(8 dp) ) } } } } @preview( showbackground = true, device = devices pixel 3 ) @composable fun editformpreview() { editform(candelete = true, "hello") } creating the editscreenviewmodel like the taskslistscreenviewmodel , the editscreenviewmodel is a viewmodel for the editscreen create a file called editscreenviewmodel kt this viewmodel will be given a setupwithtask function that takes in a taskid string? if the taskid == null , then the user is attempting to create a task if the taskid != null , the user has supplied to the editscreen a taskid to edit if taskid != null , we will fetch a task from ditto, and assign it to iscompleted mutablelivedata\<boolean> and body mutablelivedata\<string> and assign candelete mutablelivedata\<boolean> to true we add a save functionality to either insert or update into ditto depending if the id is null or not we add another function, delete , to call remove editscreenviewmodel kt class editscreenviewmodel viewmodel() { var id string? = null; // 2 var body = mutablelivedata\<string>("") var iscompleted = mutablelivedata\<boolean>(false) var candelete = mutablelivedata\<boolean>(false) // 1 fun setupwithtask(taskid string?) { candelete value = taskid != null val taskid string = taskid? let { it } ? return; val doc dittodocument = tasksapplication ditto!! store\["tasks"] findbyid(dittodocumentid(taskid)) exec()? let { it } ? return; val task = task(doc) id = task id body value = task body iscompleted value = task iscompleted } // 3 fun save() { if ( id == null) { // insert tasksapplication ditto!! store\["tasks"] insert(mapof( "body" to body value, "iscompleted" to iscompleted value )) } else { // update tasksapplication ditto!! store\["tasks"] findbyid(dittodocumentid( id!!)) update { mutabledoc > val mutabledoc = mutabledoc? let { it } ? return\@update mutabledoc\["body"] set(body value ? "") mutabledoc\["iscompleted"] set(iscompleted value ? "") } } } // 4 fun delete() { tasksapplication ditto!! store\["tasks"] upsert(mapof( " id" to id!!, "isdeleted" to true )) } } creating the editscreen just like the taskslistscreen in the previous section, we will now create an editscreen kt add a constructor that accepts a navcontroller and a task string? see to reference these values create a reference to the editscreenviewmodel call setupwithtask with the taskid from the constructor the editscreenviewmodel will now know if the user is attempting to edit or create a new task to help the user show if they are attempting or edit or create, we will show a topappbar text with an appropriate title we will call observeasstate on the editscreenviewmodel 's mutablelivedata properties and extract the value to feed into our views create a scaffold with a topappbar and content { editform } like before, we will bind all the change handlers from the editform and the values back to the viewmodel upon saving or deleting, we will tell the navcontroller to popbackstack , which will cause the app to go back to the taskslistscreen @composable fun editscreen(navcontroller navcontroller, taskid string?) { // 1 // 2 val editscreenviewmodel editscreenviewmodel = viewmodel(); // 3 editscreenviewmodel setupwithtask(taskid = taskid) // 4 val topbartitle = if (taskid == null) "new task" else "edit task" // 5 val body string by editscreenviewmodel body observeasstate("") val iscompleted boolean by editscreenviewmodel iscompleted observeasstate(initial = false) val candelete boolean by editscreenviewmodel candelete observeasstate(initial = false) // 6 scaffold( topbar = { topappbar( title = { text(topbartitle) }, backgroundcolor = colorresource(id = r color purple 700) ) }, content = { // 7 editform( candelete = candelete, body = body, onbodytextchange = { editscreenviewmodel body value = it }, iscomplete = iscompleted, oniscomplete = { editscreenviewmodel iscompleted value = it }, onsavebuttonclicked = { editscreenviewmodel save() // 8 navcontroller popbackstack() }, ondeletebuttonclicked = { editscreenviewmodel delete() // 8 navcontroller popbackstack() } ) } ) } run the app! https //docs ditto live/ios/tutorial/swift/edit screen#4 4 run the app congratulations you have successfully created a task app using ditto! 🎉