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.
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
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:
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 see below.
TasksApplication.kt
|
import android.app.Application
import live.ditto.Ditto
import live.ditto.DittoIdentity
import live.ditto.android.DefaultAndroidDittoDependencies
class TasksApplication:Application(){companionobject{var ditto: Ditto?=null;}overridefunonCreate(){super.onCreate()// construct a DefaultAndroidDittoDependencies object with the applicationContextval androidDependencies =DefaultAndroidDittoDependencies(applicationContext)// for this example we will use a Development identityval 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
overridefunonCreate(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()}funcheckPermissions(){val missing =DittoSyncPermissions(this).missingPermissions()if(missing.isNotEmpty()){this.requestPermissions(missing,0)}}overridefunonRequestPermissionsResult(
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:
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.
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.
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
|
@ComposablefunRoot(){val navController =rememberNavController()// A surface container using the 'background' color from the themeSurface(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)}}}}
Now back in the MainAcivity.kt file look for setContent{ } and replace it completely with the following highlighted lines.
Kotlin
|
class MainActivity :ComponentActivity(){overridefunonCreate(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 @ComposableTaskRow 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 @PreviewTaskRowPreview which allows you to quickly see the end result with some test data.
TaskRow.kt
|
@ComposablefunTaskRow(
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)@ComposablefunTaskRowPreview(){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.
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.
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 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.
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.
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:
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
Create 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 information about observeAsState and ViewModel, click here.
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
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)
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.
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:
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-extensionsin the the plugins section of build.gradle to enable.
build.gradle
|
plugins {
// ...
id 'kotlin-android-extensions'
}
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.
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:
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:
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:
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.
Name the resource file dialog_new_task
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:
We need to create a few string constants. Open strings.xml in the /res/values folder and replace it with this XML:
XML
|
<resources><stringname="app_name">Tasks</string><stringname="action_settings">Settings</string><stringname="title_activity_main">Tasks</string><stringname="add_new_task_dialog_title">Add New Task</string><stringname="save">Save</string></resources>
Right click on the layouts folder in the project and Go to File → New → XML → Layout XML. Name the file task_view:
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:
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:
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>(){privateval tasks = mutableListOf<DittoDocument>()var onItemClick:((DittoDocument)-> Unit)?=nullclassTaskViewHolder(v: View): RecyclerView.ViewHolder(v)overridefunonCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {val view = LayoutInflater.from(parent.context).inflate(R.layout.task_view, parent,false)returnTaskViewHolder(view)}overridefunonBindViewHolder(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])}}overridefungetItemCount()=this.tasks.size
funtasks(): List<DittoDocument>{returnthis.tasks.toList()}funset(tasks: List<DittoDocument>): Int {this.tasks.clear()this.tasks.addAll(tasks)returnthis.tasks.size
}funinserts(indexes: List<Int>): Int {for(index in indexes){this.notifyItemRangeInserted(index,1)}returnthis.tasks.size
}fundeletes(indexes: List<Int>): Int {for(index in indexes){this.notifyItemRangeRemoved(index,1)}returnthis.tasks.size
}funupdates(indexes: List<Int>): Int {for(index in indexes){this.notifyItemRangeChanged(index,1)}returnthis.tasks.size
}funmoves(moves: List<DittoLiveQueryMove>){for(move in moves){this.notifyItemMoved(move.from, move.to)}}funsetInitial(tasks: List<DittoDocument>): Int {this.tasks.addAll(tasks)this.notifyDataSetChanged()returnthis.tasks.size
}}
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: