The following procedures are based on Android Studio 4.1 and Kotlin 1.4.
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 the newer version of Android Studio the Basic Activity template includes additional files that are not needed 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>
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")}
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, 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))}
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)returnTaskViewHolder(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
}}
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 {returnfalse}
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!