website logo
Legacy DocsPortal
⌘K
Welcome to Ditto
Onboarding
Ditto Basics
SDK Setup Guides
Platform Manual
HTTP API
Kafka Connector
Use Cases
FAQs
Troubleshooting Guide
Support
Docs powered by
Archbee
SDK Setup Guides
...
Kotlin
Task App Tutorials for Kotlin

Jetpack Compose: Defining UI

35min

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

1

Create your Ditto account and app (Onboarding)

2

Install the required prerequisites (Prerequisites - Kotlin)

3

Install the latest version of Android Studio Arctic Fox

Install and Set Up

1

Create the App​

Click File > New Project and fill out the information on the form similar to the screenshot below. These are recommended values however they are not crucial to complete this tutorial:

  • Name:
  • Package Name:
  • Save location: choose a directory
  • Minimum SDK:

Finally click "Finish" and wait for Android Studio to setup the project.

Document image

Document image




2

Install Ditto​

Android requires requesting permission to use Bluetooth Low Energy and WiFi Aware.

Follow the Kotlin Installation Environment Setup and Android Platform Permissions Setup sections to setup your Android environment.

3

Add Jetpack Compose Dependencies

In your application's 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"
}

4

Add Vector Icons​

We will need a couple of additional icons to show the tasks' completed, incompleted states. We will reference these vector resources in our code later.

Right click on the res > drawable package and add a new Vector Asset

Document image


Click the "add" icon and select it for size `24`

Document image


Repeat the previous steps for adding:

  • a circle_filled (icon name: "Brightness 1")
  • circle_outline (icon name: "Panorama fish eye")
Document image

Document image


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

1

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

  1. 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.
  2. In the override fun onCreate(), construct ditto with DefaultAndroidDittoDependencies see below.
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:

Kotlin
|
val docs = TasksApplication.ditto!!.store["tasks"].find("!isDeleted").exec()

2

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()
    }
}

3

Create a Task Data Class

Ditto is a document database, which represents all of its rows in the database a JSON-like structure. In this tutorial, we will define each task like so:

Text
|
{
  "_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:

Kotlin
|
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.

Kotlin
|
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:

Kotlin
|
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

1

Creating a Root Navigation

This application will have two Screens which are just Jetpack Compose views.

  1. TasksListScreen.kt - A list where we can show the tasks.
  2. 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:

  1. tasks which will bring you the TasksListScreen
  2. 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
  3. 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)
            }
        }
    }
}


​

2

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.

Kotlin
|
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.

1

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.

  1. If the task.isCompleted is true, we will show a filled circle icon and a strike through style for the body.
  2. If the task.isCompleted is false, we will show a filled circle icon and a strike through style for the body.
  3. 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
  4. 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.

Document image

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))
    }
}




2

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.

  1. The TaskList takes in a List<Task> and loops over it in a Column with a .forEach loop.
  2. Each iteration of the loop will render a Task(task)
  3. 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.

Kotlin
|
@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),
        )
    )
}

3

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 MVVM or Model View ViewModel 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.

  1. Now create a new file called TasksListScreenViewModel.kt
  2. 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.
  3. 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.
  4. 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()
    }
}


You can learn more about ViewModels on the official Android Documentation.

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.

4

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:

  1. 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 navigation section for more information on the routes
  2. Create reference to the TasksListScreenViewModel with val tasksListViewModel: TasksListScreenViewModel = viewModel();
  3. 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 information about observeAsState and ViewModel, click here.
  4. 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. Learn more about Scaffolds here
  5. Set the ExtendedFloatingActionButton.onClick handler to navigate to the task/edit route of the navController
  6. Use our TaskList inside of the Scaffold.content. Pass the tasks from step 2. into the TaskList
  7. Bind the TaskList.onToggle to the tasksListViewModel.toggle
  8. 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
  • Creating a
  • Deleting an existing
1

Creating the @Composable EditForm

The EditForm is a simple layout that includes:

  1. A constructor canDelete: Boolean which determines whether or not to show a delete Button
  2. A body: String and isCompleted: Boolean
  3. Respective callback parameters for changes in the TextField and save and delete Button (see steps 4, 5, 6)
  4. An TextField which we use to edit the Task.body
  5. A Switch which is used to edit the Task.isCompleted
  6. A Button for saving a task.
  7. A Button for deleting a task

We've also included a @Preview of the EditForm:

Document image

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")
}

2

Creating the EditScreenViewModel

Like the TasksListScreenViewModel, the EditScreenViewModel is a ViewModel for the EditScreen. Create a file called EditScreenViewModel.kt.

  1. 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.
  2. 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
  3. We add a save functionality to either .insert or .update into Ditto depending if the _id is null or not.
  4. 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
        ))
    }
}

3

Creating the EditScreen

Just like the TasksListScreen in the previous section, we will now create an EditScreen.kt.

  1. Add a constructor that accepts a navController and a task: String?. See the section on navigation to reference these values.
  2. Create a reference to the EditScreenViewModel
  3. 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.
  4. To help the user show if they are attempting or edit or create, we will show a TopAppBar Text with an appropriate title.
  5. We will call observeAsState on the EditScreenViewModel's MutableLiveData properties and extract the value to feed into our views.
  6. Create a Scaffold with a TopAppBar and content { EditForm... }
  7. Like before, we will bind all the change handlers from the EditForm and the values back to the viewModel
  8. Upon saving or deleting, we will tell the navController to popBackStack, which will cause the app to go back to the TasksListScreen
Kotlin
|
@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()
                }
            )
        }
    )
}

4

Run the App!​

Congratulations you have successfully created a task app using Ditto! 🎉

Create Android Studio Project

This guide is based on Android Studio 4.1 and Kotlin 1.4

The first step is to create a project. Go to File → New → New Project and select Basic Activity:

Next, fill out the options with the product name: "Tasks", choose Kotlin, and set the minimum API level to 26:

Document image


In newer version of Android Studio the Basic Activity template includes additional files that are not need for this tutorial. To continue, remove the following if they exist:

  • FirstFragment.kt
  • SecondFragment.kt
  • fragment_first.xml
  • fragment_second.xml
  • nav_graph.xml

Install Ditto

To install Ditto, we need to add it as a dependency in the build.gradle script for the app, as well as ensuring that we have the relevant Java -compatibility set.

Android requires requesting permission to use Bluetooth Low Energy and Wifi Direct. For instructions, see Jetpack Compose: Defining UI.

Add Extensions

For the UI in this example, we are still using Kotlin synthetics, which are no longer bundled automatically. We need to add kotlin-android-extensions in the the plugins section of build.gradle to enable.

build.gradle
|
plugins {
    // ...
    id 'kotlin-android-extensions'
}

Document image


Be sure to Sync Project with Gradle Files after you add Ditto as a dependency. Click the elephant icon with the blue arrow in the top right to manually trigger if it doesn't prompt.

At this point, you have the basic project in place! Now we need to start to build the UI elements.

Create UI

Set up the interface for your task app:

  1. Adjust your default layout files. (Adjusting Existing Layouts)
  2. Set up a toolbar and button for adding new tasks. (Kotlin Task App Quickstart
  3. Create an alert and define the string values.
  4. Create a dialog box for adding new tasks.

Adjusting Existing Layouts​

Navigate to the content_main.xml layout file and replace the XML in the text representation view. This will remove the existing text view and a recycler view that we will use to display the list of tasks:

Now navigate to activity_main.xml layout file and replace the XML in the text representation view. This will adjust the floating action button to use a white add icon:

content_main.xml
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


The layout should look like this:

Document image


Now navigate to activity_main.xml layout file and replace the XML in the text representation view. This will adjust the floating action button to use a white add icon:

activity_main.xml
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.Tasks.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/Theme.Tasks.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <include layout="@layout/content_main" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/addTaskButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:tint="#FFFFFF"
        app:srcCompat="@android:drawable/ic_input_add" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>


Now navigate to activity_main.xml layout file and replace the XML in the text representation view. This will adjust the floating action button to use a white add icon:

XML
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.Tasks.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/Theme.Tasks.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <include layout="@layout/content_main" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/addTaskButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:tint="#FFFFFF"
        app:srcCompat="@android:drawable/ic_input_add" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>


The layout should look like this now:

Document image


Create AlertDialog Layout​

We now need to create a new layout resource file to define our alert dialog. Right click on the layouts folder in the project and Go to File → New → XML → Layout XML.

Document image


Name the resource file dialog_new_task

Document image


Open the new dialog_new_task.xml layout file and replace the XML in the text representation view. This will add an editable text input to allow the user to enter the task:

XML
|
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:inputType="text" />
</LinearLayout>



The layout should look like this now:

Document image


Define Strings

We need to create a few string constants. Open strings.xml in the /res/values folder and replace it with this XML:

XML
|
<resources>
    <string name="app_name">Tasks</string>
    <string name="action_settings">Settings</string>
    <string name="title_activity_main">Tasks</string>
    <string name="add_new_task_dialog_title">Add New Task</string>
    <string name="save">Save</string>
</resources>


Create DialogFragment​​

To use the AlertDialog we will create a DialogFragment. Create a new Kotlin class by right clicking the app folder within java in the project view:

Document image


Name the new file NewTaskDialogFragment:

Document image


Replace the contents of NewTaskDialogFragment.kt with this:

NewTaskDialogFragment.kt
|
package live.ditto.tasks

import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.widget.TextView
import androidx.fragment.app.DialogFragment

class NewTaskDialogFragment: DialogFragment() {

    interface NewTaskDialogListener {
        fun onDialogSave(dialog: DialogFragment, task: String)
        fun onDialogCancel(dialog: DialogFragment)
    }

    var newTaskDialogListener: NewTaskDialogListener? = null

    companion object {
        fun newInstance(title: Int): NewTaskDialogFragment {
            val newTaskDialogFragment = NewTaskDialogFragment()
            val args = Bundle()
            args.putInt("dialog_title", title)
            newTaskDialogFragment.arguments = args
            return newTaskDialogFragment
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // 5
        val title = arguments!!.getInt("dialog_title")
        val builder = AlertDialog.Builder(activity)
        builder.setTitle(title)

        val dialogView = activity!!.layoutInflater.inflate(R.layout.dialog_new_task, null)
        val task = dialogView.findViewById<TextView>(R.id.editText)

        builder.setView(dialogView)
            .setPositiveButton(R.string.save) { _, _ -> newTaskDialogListener?.onDialogSave(this, task.text.toString()) }
            .setNegativeButton(android.R.string.cancel) { _, _ -> newTaskDialogListener?.onDialogCancel(this) }
        return builder.create()
    }

    @Suppress("DEPRECATION")
    override fun onAttach(activity: Activity) { // 6
        super.onAttach(activity)
        try {
            newTaskDialogListener = activity as NewTaskDialogListener
        } catch (e: ClassCastException) {
            throw ClassCastException("$activity must implement NewTaskDialogListener")
        }
    }
}



Configure Main Activity Part I

We need to import Ditto and create a few variables. Open the MainActivity file and replace the existing code with this:

MainActivity
|
package live.ditto.tasks

import android.os.Bundle
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import androidx.recyclerview.widget.RecyclerView
import androidx.fragment.app.DialogFragment
import java.time.Instant

import kotlinx.android.synthetic.main.activity_main.*

import live.ditto.*
import live.ditto.android.DefaultAndroidDittoDependencies


class MainActivity : AppCompatActivity(), NewTaskDialogFragment.NewTaskDialogListener {
    private lateinit var recyclerView: RecyclerView
    private lateinit var viewAdapter: RecyclerView.Adapter<*>
    private lateinit var viewManager: RecyclerView.LayoutManager

    private var ditto: Ditto? = null
    private var collection: DittoCollection? = null
    private var liveQuery: DittoLiveQuery? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
    }
}



Run Sync Project with Gradle Files if it doesn't automatically.

Add New Task Functions​

We will add a function and override two now that MainActivity is an abstract class. Insert this code after onCreate() function in the class:

MainActivity
|
override fun onDialogSave(dialog: DialogFragment, task:String) {
    // Add the task to Ditto
    this.collection!!.upsert(mapOf("body" to task, "isCompleted" to false))
}

override fun onDialogCancel(dialog: DialogFragment) { }

fun showNewTaskUI() {
    val newFragment = NewTaskDialogFragment.newInstance(R.string.add_new_task_dialog_title)
    newFragment.show(supportFragmentManager,"newTask")
}



Create A Task View Layout​

Right click on the layouts folder in the project and Go to File → New → XML → Layout XML. Name the file task_view:

Document image


Open the task_view.xml layout file and replace the XML in the text representation view. This will add a text view and checkbox to display the task in each row of the RecyclerView:

task_view.xml
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/linearLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/taskTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="TextView"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/taskCheckBox"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <CheckBox
        android:id="@+id/taskCheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:backgroundTint="#FFFFFF"
        android:clickable="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/taskTextView"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>



The layout should look like this now:

Document image


Configure Main Activity Part II

We now need to continue to configure the MainActivity to customize the RecyclerView, to display the tasks and add the logic for the user actions. Replace the onCreate() function with this code that will configure the recycler view:

MainActivity
|
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)

    // Setup the layout
    viewManager = LinearLayoutManager(this)
    val tasksAdapter = TasksAdapter()
    viewAdapter = tasksAdapter

    recyclerView = findViewById<RecyclerView>(R.id.recyclerView).apply {
        setHasFixedSize(true)
        layoutManager = viewManager
        adapter = viewAdapter
    }

    recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))

}



​Add TasksAdapter​

We need to declare a RecyclerView.Adapter to provide a data source to the RecyclerView. Add this code to the bottom of MainActivity, as a new class within the file:

MainActivity
|
class TasksAdapter: RecyclerView.Adapter<TasksAdapter.TaskViewHolder>() {
    private val tasks = mutableListOf<DittoDocument>()

    var onItemClick: ((DittoDocument) -> Unit)? = null

    class TaskViewHolder(v: View): RecyclerView.ViewHolder(v)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.task_view, parent, false)
        return TaskViewHolder(view)
    }

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task = tasks[position]
        holder.itemView.taskTextView.text = task["body"].stringValue
        holder.itemView.taskCheckBox.isChecked = task["isCompleted"].booleanValue
        holder.itemView.setOnClickListener {
            // NOTE: Cannot use position as this is not accurate based on async updates
            onItemClick?.invoke(tasks[holder.adapterPosition])
        }
    }

    override fun getItemCount() = this.tasks.size

    fun tasks(): List<DittoDocument> {
        return this.tasks.toList()
    }

    fun set(tasks: List<DittoDocument>): Int {
        this.tasks.clear()
        this.tasks.addAll(tasks)
        return this.tasks.size
    }

    fun inserts(indexes: List<Int>): Int {
        for (index in indexes) {
            this.notifyItemRangeInserted(index, 1)
        }
        return this.tasks.size
    }

    fun deletes(indexes: List<Int>): Int {
        for (index in indexes) {
            this.notifyItemRangeRemoved(index, 1)
        }
        return this.tasks.size
    }

    fun updates(indexes: List<Int>): Int {
        for (index in indexes) {
            this.notifyItemRangeChanged(index, 1)
        }
        return this.tasks.size
    }

    fun moves(moves: List<DittoLiveQueryMove>) {
        for (move in moves) {
            this.notifyItemMoved(move.from, move.to)
        }
    }

    fun setInitial(tasks: List<DittoDocument>): Int {
        this.tasks.addAll(tasks)
        this.notifyDataSetChanged()
        return this.tasks.size
    }
}


Add Swipe To Delete​

To match the iOS getting started app, we also want to add swipe to delete functionality. Insert this code at the bottom of MainActivity as a new class:

MainActivity
|
// Swipe to delete based on https://medium.com/@kitek/recyclerview-swipe-to-delete-easier-than-you-thought-cff67ff5e5f6
abstract class SwipeToDeleteCallback(context: Context) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {

    private val deleteIcon = ContextCompat.getDrawable(context, android.R.drawable.ic_menu_delete)
    private val intrinsicWidth = deleteIcon!!.intrinsicWidth
    private val intrinsicHeight = deleteIcon!!.intrinsicHeight
    private val background = ColorDrawable()
    private val backgroundColor = Color.parseColor("#f44336")
    private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }


    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        return false
    }

    override fun onChildDraw(
        c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
        dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
    ) {

        val itemView = viewHolder.itemView
        val itemHeight = itemView.bottom - itemView.top
        val isCanceled = dX == 0f && !isCurrentlyActive

        if (isCanceled) {
            clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
            return
        }

        // Draw the red delete background
        background.color = backgroundColor
        background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
        background.draw(c)

        // Calculate position of delete icon
        val deleteIconTop = itemView.top + (itemHeight - intrinsicHeight) / 2
        val deleteIconMargin = (itemHeight - intrinsicHeight) / 2
        val deleteIconLeft = itemView.right - deleteIconMargin - intrinsicWidth
        val deleteIconRight = itemView.right - deleteIconMargin
        val deleteIconBottom = deleteIconTop + intrinsicHeight

        // Draw the delete icon
        deleteIcon!!.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom)
        deleteIcon.setTint(Color.parseColor("#ffffff"))
        deleteIcon.draw(c)

        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
    }

    private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) {
        c?.drawRect(left, top, right, bottom, clearPaint)
    }
}


Almost there! At this point, we have most of the app created, but we now need to integrate Ditto!

Updated 27 Sep 2023
Did this page help you?
PREVIOUS
Kotlin Task App Quickstart
NEXT
Compatibility with Kotlin
Docs powered by
Archbee
TABLE OF CONTENTS
Prerequisites
Install and Set Up
Create the App​
Install Ditto​
Add Jetpack Compose Dependencies
Add Vector Icons​
Configure Ditto
Create Application Class​
Start Ditto Sync
Create a Task Data Class
Navigation
Creating a Root Navigation
Setting the MainAcivity to render Root
Show the List of Tasks
Create a TaskRow views
Create a @Composable TaskList
Create a @Composable TasksListScreenViewModel
Create the TasksListScreen
Editing Tasks
Creating the @Composable EditForm
Creating the EditScreenViewModel
Creating the EditScreen
Run the App!​
Create Android Studio Project
Install Ditto
Add Extensions
Create UI
Adjusting Existing Layouts​
Create AlertDialog Layout​
Define Strings
Create DialogFragment​​
Configure Main Activity Part I
Add New Task Functions​
Create A Task View Layout​
Configure Main Activity Part II
​Add TasksAdapter​
Add Swipe To Delete​
Docs powered by
Archbee