Jetpack Compose Task App
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. (Get Started)
Install the required prerequisites. (Prerequisites - Kotlin)
Install the latest version of Android Studio Arctic Fox.
Install and Set Up
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
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.
Add Jetpack Compose Dependencies
In your app Module build.gradle file, add the additional dependencies.
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
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 declarevar 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 withDefaultAndroidDittoDependencies
as follows:
Now you will be able to access this Ditto anywhere in your application like so:
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.
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:
These Task documents will all be in the "tasks"
collection. We will be referencing this collection throughout this tutorial with:
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.
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:
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 theTasksListScreen
tasks/edit
which will bring you theEditScreen
but will be for creating tasks. Notice that we will give anull
to thetaskId
. This same screen will be in a “create” mode if thetaskId
isnull
tasks/edit/{taskId}
which will bring you theEditScreen
but will be for editing tasks. Notice that there is a"{taskId}"
portion to this route. Similar to web apps, we will parse out aTask._id
string from the route and use that for editing.
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.
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
istrue
, we will show a filled circle icon and a strike through style for thebody
. - If the
task.isCompleted
isfalse
, we will show a filled circle icon and a strike-through style for thebody
. - If the user taps the
Icon
, we will call aonToggle: ((task: Task) -> Unit)?
, we will reverse theisCompleted
fromtrue
tofalse
orfalse
totrue
- If the user taps the
Text
, we will call aonClickBody: ((task: Task) -> Unit)?
. We will use this to navigate to theEditScreen
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.
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 aList<Task>
and loops over it in aColumn
with a.forEach
loop. - Each iteration of the loop will render a
Task(task)
- We’ve also added
onClickBody
andonToggle
callback that matches theTask.onClickBody
andTask.onToggle
functions.
We’ve also included a TaskListPreview
so that you can add some test data.
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
.
- 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 theTasksListScreen
can observeLocal for changes. When anyMutableLiveData
type changes, Jetpack Compose will intelligently tell@Composable
types to reload with the necessary changes. - Create a
liveQuery
andsubscription
by observing/subscribing to all the tasks documents. Remember ourTask
data class that we created? We will now map all theDittoDocument
to aList<Task>
and set them to the tasks. - Ditto’s
DittoLiveQuery
andDittoSubscription
types should be disposed by callingclose()
once theViewModel
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.
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.
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 anavController
as a parameter. This variable is used to navigate toEditScreen
depending on if the user clicks afloatingActionButton
or aTasksListScreen.onClickBody
. See the navigation section for more information on the routes - Create a reference to the
TasksListScreenViewModel
withval tasksListViewModel: TasksListScreenViewModel = viewModel();
- Now let’s tell the
@Composable
to observe theviewModel.tasks
as State object withval tasks: List<Task> by tasksListViewModel.tasks.observeAsState(emptyList())
. The syntaxby
and functionobserveAsState(emptyList())
will tell the@Composable
to subscribe to changes. For more information aboutobserveAsState
andViewModel
, click here. - We’ll add a
TopAppBar
andExtendedFloatingActionButton
along with ourTaskList
all wrapped in aScaffold
view.Scaffold
are handy ways to layout a more “standard” Android screen. Learn more aboutScaffold
s here - Set the
ExtendedFloatingActionButton.onClick
handler to navigate to thetask/edit
route of thenavController
- Use our
TaskList
inside of theScaffold.content
. Pass thetasks
from step 2. into theTaskList
- Bind the
TaskList.onToggle
to thetasksListViewModel.toggle
- Bind the
TaskList.onClickBody
to thenavController.navigate("tasks/edit/${task._id}")
. This will tell thenavController
to go theEditScreen
(we will create this in the next section)
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 deleteButton
. - A
body: String
andisCompleted: Boolean
. - Respective callback parameters for changes in the
TextField
and save and deleteButton
(see steps 4 to 6). - An
TextField
which we use to edit theTask.body
. - A
Switch
which is used to edit theTask.isCompleted
. - A
Button
for saving a task. - A
Button
for deleting a task.
We’ve also included a @Preview
of the EditForm
:
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 ataskId: String?
. If thetaskId == null
, then the user is attempting to create aTask
. If thetaskId != null
, the user has supplied to theEditScreen
ataskId
to edit. - If
taskId != null
, we will fetch a task from Ditto, and assign it toisCompleted: MutableLiveData<Boolean>
andbody: MutableLiveData<String>
and assigncanDelete: MutableLiveData<Boolean>
totrue
- We add a
save
functionality to either.upsert
or.update
into Ditto depending if the_id
isnull
or not. - We add another function,
delete
, to call.remove
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 atask: String?
. See the section on navigation to reference these values. - Create a reference to the
EditScreenViewModel
- Call
setupWithTask
with thetaskId
from the constructor. TheEditScreenViewModel
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 theEditScreenViewModel
’sMutableLiveData
properties and extract the value to feed into our views. - Create a Scaffold with a
TopAppBar
andcontent { EditForm... }
- Like before, we will bind all the change handlers from the
EditForm
and the values back to theviewModel
- Upon saving or deleting, we will tell the
navController
topopBackStack
, which will cause the app to go back to theTasksListScreen
Run the App!
Congratulations you have successfully created a task app using Ditto!
Was this page helpful?