Android - Java


Create Android Studio Project

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

Create Project 1

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

Create Project 2

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.java
  • SecondFragment.java
  • fragment_first.xml
  • fragment_second.xml
  • nav_graph.xml

Android requires requesting permission to use Bluetooth Low Energy and P2P Wifi, open the AndroidManifest.xml and add the following:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />


It should look like this now:

Create Project 3


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:

dependencies {
    // ...
    implementation "live.ditto:ditto:1.0.0"
}

android {
    // ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}


Install Ditto

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 Layouts

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

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

Create UI Layouts 1

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 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/AppTheme.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/AppTheme.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:

Create UI Layouts 2


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

Create UI Layout 3

Name the resource file dialog_new_task

Create UI Layout 4

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

Create UI Layout 5

Define Strings

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

<resources>
    <string name="app_name">ToDo</string>
    <string name="action_settings">Settings</string>
    <string name="title_activity_main">ToDo</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 Java class by right clicking the app folder within java in the project view:

Create UI Layout 6

Name the new file NewTaskDialogFragment:

Create UI Layout 7

Replace the contents of NewTaskDialogFragment with this:

package com.dittolive.todo;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.fragment.app.DialogFragment;

public class NewTaskDialogFragment extends DialogFragment {

    interface NewTaskDialogListener {
        void onDialogSave(DialogFragment dialogFragment, String task);
        void onDialogCancel(DialogFragment dialogFragment);
    }

    NewTaskDialogListener newTaskDialogListener = null;

    static NewTaskDialogFragment newInstance(int title) {
        NewTaskDialogFragment f = new NewTaskDialogFragment();
        Bundle args = new Bundle();
        args.putInt("dialog_title", title);
        f.setArguments(args);
        return f;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        int title = getArguments().getInt("dialog_title");
        final NewTaskDialogListener dialogListener = this.newTaskDialogListener;

        View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_new_task, null);
        final TextView task = dialogView.findViewById(R.id.editText);
        final DialogFragment dialogFragment = this;

        return new AlertDialog.Builder(getActivity())
            .setView(dialogView)
            .setTitle(title)
            .setPositiveButton(R.string.save,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        dialogListener.onDialogSave(dialogFragment, task.getText().toString());
                    }
                }
            )
            .setNegativeButton(android.R.string.cancel,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        dialogListener.onDialogCancel(dialogFragment);
                    }
                }
            )
            .create();
    }

    @SuppressWarnings("deprecation")
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            this.newTaskDialogListener = (NewTaskDialogListener) activity;
        } catch (ClassCastException e) {
            throw new 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 the code below. Don't worry about all of the imports - lots of them aren't going to be used until later on in the guide.

package com.dittolive.todo;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;

import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Date;
import java.text.SimpleDateFormat;

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

public class MainActivity extends AppCompatActivity implements NewTaskDialogFragment.NewTaskDialogListener, TasksAdapter.ItemClickListener {

    private RecyclerView recyclerView = null;
    private RecyclerView.Adapter viewAdapter = null;
    private RecyclerView.LayoutManager viewManager = null;

    private Ditto ditto = null;
    private DittoCollection collection = null;
    private DittoLiveQuery liveQuery = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

    }
}

Run Sync Project with Gradle Files if it doesn't automatically. The file should look like this now:

Configure Main Activity Part 1

Don't worry that the file doesn't compile yet. We need to add a method, onDialogSave() next to resolve this. Simply continue with the instructions below.

Add New Task Functions

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

@Override
public void onDialogSave(DialogFragment dialog, String task) {
    // Add the task to Ditto
    Date date = new Date(System.currentTimeMillis());
    SimpleDateFormat sdf;
    sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
    String currentDateString = sdf.format(date);
    Map<String, Object> content = new HashMap<>();
    content.put("text", task);
    content.put("isComplete", false);
    content.put("dateCreated", currentDateString);
    this.collection.insert(content);
}

@Override
public void onDialogCancel(DialogFragment dialog) { }

protected void showNewTaskUI() {
    NewTaskDialogFragment newFragment = NewTaskDialogFragment.newInstance(R.string.add_new_task_dialog_title);
    FragmentManager fm = this.getSupportFragmentManager();
    newFragment.show(fm, "newTask");
}

@Override
public void onItemClick(DittoDocument task) {
    ditto.getStore().collection("tasks").findByID(task.id).update(mutDoc -> {
        try {
            mutableDoc.get("isComplete").set(!mutableDoc.get("isComplete").getBooleanValue());
        } catch (DittoError err) {
        }
    });
}

Don't worry that the file doesn't compile yet. We need to add the TaskAdapter implementation to resolve this. Simply continue with the instructions below.


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:

Create Task View 1

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:

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

Create Task View 2


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:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // Setup the layout
    this.viewManager = new LinearLayoutManager(this);
    final TasksAdapter tasksAdapter = new TasksAdapter(getApplicationContext());
    tasksAdapter.setClickListener(this);
    this.viewAdapter = tasksAdapter;

    this.recyclerView = findViewById(R.id.recyclerView);
    this.recyclerView.setHasFixedSize(true);
    this.recyclerView.setLayoutManager(viewManager);
    this.recyclerView.setAdapter(viewAdapter);

    this.recyclerView.addItemDecoration(new 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:

class TasksAdapter extends RecyclerView.Adapter<TasksAdapter.TaskViewHolder> {
    private List<DittoDocument> tasks = new ArrayList<>();
    private LayoutInflater inflater;
    private ItemClickListener clickListener;

    TasksAdapter(Context context) {
        this.inflater = LayoutInflater.from(context);
    }

    @NonNull
    @Override
    public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.task_view, parent, false);
        return new TaskViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final TaskViewHolder holder, int position) {
        DittoDocument task = tasks.get(position);
        holder.textView.setText(task.get("text").getStringValue());
        holder.checkBoxView.setChecked(task.get("isComplete").getBooleanValue());
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // NOTE: Cannot use position as this is not accurate based on async updates
                clickListener.onItemClick(tasks.get(holder.getAdapterPosition()));
            }
        });
    }

    @Override
    public int getItemCount() {
        return tasks.size();
    }

    class TaskViewHolder extends RecyclerView.ViewHolder {
        TextView textView;
        CheckBox checkBoxView;

        TaskViewHolder(View view) {
            super(view);
            this.textView = view.findViewById(R.id.taskTextView);
            this.checkBoxView = view.findViewById(R.id.taskCheckBox);
        }
    }

    void setClickListener(ItemClickListener itemClickListener) {
        this.clickListener = itemClickListener;
    }

    public interface ItemClickListener {
        void onItemClick(DittoDocument task);
    }

    List<DittoDocument> tasks() {
        return this.tasks;
    }

    int set(List<DittoDocument> tasksToSet) {
        this.tasks.clear();
        this.tasks.addAll(tasksToSet);
        return this.tasks.size();
    }

    int inserts(List<Integer> indexes) {
        for (int i = 0; i < indexes.size(); i ++) {
            this.notifyItemRangeInserted(indexes.get(i), 1);
        }
        return this.tasks.size();
    }

    int deletes(List<Integer> indexes) {
        for (int i = 0; i < indexes.size(); i ++) {
            this.notifyItemRangeRemoved(indexes.get(i), 1);
        }
        return this.tasks.size();
    }

    int updates(List<Integer> indexes) {
        for (int i = 0; i < indexes.size(); i ++) {
            this.notifyItemRangeChanged(indexes.get(i), 1);
        }
        return this.tasks.size();
    }

    void moves(List<DittoLiveQueryMove> moves) {
        for (int i = 0; i < moves.size(); i ++) {
            this.notifyItemMoved(moves.get(i).getFrom(), moves.get(i).getTo());
        }
    }

    int setInitial(List<DittoDocument> tasksToSet) {
        this.tasks.addAll(tasksToSet);
        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:

// Swipe to delete based on https://medium.com/@kitek/recyclerview-swipe-to-delete-easier-than-you-thought-cff67ff5e5f6
abstract class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
    private Drawable deleteIcon;
    private int intrinsicWidth;
    private int intrinsicHeight;
    private ColorDrawable background = new ColorDrawable();
    private int backgroundColor = Color.parseColor("#f44336");
    private Paint clearPaint = new Paint();

    SwipeToDeleteCallback(Context context) {
        super(0, ItemTouchHelper.LEFT);
        deleteIcon = context.getDrawable(android.R.drawable.ic_menu_delete);
        intrinsicWidth = deleteIcon.getIntrinsicWidth();
        intrinsicHeight = deleteIcon.getIntrinsicHeight();
        clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {

    }

    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        View itemView = viewHolder.itemView;
        int itemHeight = itemView.getBottom() - itemView.getTop();
        boolean isCanceled = dX == 0f && !isCurrentlyActive;

        if (isCanceled) {
            clearCanvas(c, itemView.getRight() + dX, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom());
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
            return;
        }

        // Draw the red delete background
        background.setColor(backgroundColor);
        background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom());
        background.draw(c);

        // Calculate position of delete icon
        int deleteIconTop = itemView.getTop()+ (itemHeight - intrinsicHeight) / 2;
        int deleteIconMargin = (itemHeight - intrinsicHeight) / 2;
        int deleteIconLeft = itemView.getRight() - deleteIconMargin - intrinsicWidth;
        int deleteIconRight = itemView.getRight() - deleteIconMargin;
        int 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 void clearCanvas(Canvas c, Float left, Float top, Float right, Float bottom) {
        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!


Integrate Ditto

To finish the app, we now need to integrate Ditto. We will initialize it in the onCreate() function within MainActivity. Furthermore, we will add handlers for the swipe to delete and listening for row clicks to mark a task as completed (or in-completed). Replace the existing onCreate() code with this:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // Setup the layout
    this.viewManager = new LinearLayoutManager(this);
    final TasksAdapter tasksAdapter = new TasksAdapter(getApplicationContext());
    tasksAdapter.setClickListener(this);
    this.viewAdapter = tasksAdapter;

    this.recyclerView = findViewById(R.id.recyclerView);
    this.recyclerView.setHasFixedSize(true);
    this.recyclerView.setLayoutManager(viewManager);
    this.recyclerView.setAdapter(viewAdapter);

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

    // Create an instance of Ditto
    DefaultAndroidDittoDependencies androidDependencies = new DefaultAndroidDittoDependencies(getApplicationContext());
    final Ditto ditto = new Ditto(androidDependencies);
    this.ditto = ditto;

    // Set your Ditto access license
    // The SDK will not work without this!
    ditto.setAccessLicense("<INSERT ACCESS LICENSE>");

    // This starts Ditto's background synchronization
    ditto.startSync();

    // Add swipe to delete
    SwipeToDeleteCallback swipeToDeleteCallback = new SwipeToDeleteCallback(this) {
        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
            DittoDocument task = tasksAdapter.tasks().get(viewHolder.getAdapterPosition());
            ditto.getStore().collection("tasks").findByID(task.id).remove();
        }
    };

    // Configure the RecyclerView for swipe to delete
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(swipeToDeleteCallback);
    itemTouchHelper.attachToRecyclerView(recyclerView);

    // Respond to new task button click
    FloatingActionButton addTaskButton = findViewById(R.id.addTaskButton);
    addTaskButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            showNewTaskUI();
        }
    });

    // Listen for clicks to mark tasks [in]complete
    tasksAdapter.setClickListener(this);

    // This function will create a "live-query" that will update
    // our RecyclerView
    setupTaskList();

    // This will check if the app has location permissions
    // to fully enable Bluetooth
    checkLocationPermission();
}

The important things to note is that you need an access license to use Ditto. If you do not have one yet, reach out and we can supply one. To enable background synchronization, we need to call startSync() which allows you to control when synchronization occurs. For this application we want it to run the entire time the app is in use.


Setup Live Query

Finally, we then use Ditto's key API to observe changes to the database by creating a live-query in the setupTaskList() function. This allows us to set the initial state of the RecyclerView after the query is immediately run and then subsequently get callbacks for any new data changes that occur locally or that were synced from other devices:

Note, that we are using the observe API in Ditto. This API performs two functions. First, it sets up a local observer for data changes in the database that match the query and second it creates a subscription for the same query that will be used to request this data from other devices. For simplicity, we are using this combined API, but you can also call them independently. To learn more, see the Observing Changes section in the documentation.

void setupTaskList() {
    // We will create a long-running live query to keep UI up-to-date
    this.collection = this.ditto.getStore().collection("tasks");

    final TasksAdapter adapter = (TasksAdapter) this.viewAdapter;
    final Activity activity = this;

    // We use observe to create a live query and a subscription to sync this query with other devices
    this.liveQuery = collection.findAll().sort("dateCreated", DittoSortDirection.Ascending).observe((documents, event) -> {
        final List<DittoDocument> tasks = (List<DittoDocument>) documents;
        if (event instanceof DittoLiveQueryEvent.Update) {
            final DittoLiveQueryEvent.Update updateEvent = (DittoLiveQueryEvent.Update) event;
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    adapter.set(tasks);
                    adapter.inserts(updateEvent.insertions);
                    adapter.deletes(updateEvent.deletions);
                    adapter.updates(updateEvent.updates);
                    adapter.moves(updateEvent.moves);
                }
            });

        } else if (event instanceof DittoLiveQueryEvent.Initial) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    adapter.setInitial(tasks);
                }
            });

        }
    });
}

This is a best-practice when using Ditto, since it allows your UI to simply react to data changes which can come at any time given the ad-hoc nature of how Ditto synchronizes with nearby devices.

Check For Location Permissions

Android requires you to request location permissions to fully enable Bluetooth Low Energy (since Bluetooth can be involved with location tracking). Insert this function in MainActivity:

void checkLocationPermission() {
    // On Android, parts of Bluetooth LE and WiFi Direct require location permission
    // Ditto will operate without it but data sync may be impossible in certain scenarios
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
        // For this app we will prompt the user for this permission every time if it is missing
        // We ignore the result - Ditto will automatically notice when the permission is granted
        String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
        ActivityCompat.requestPermissions(this, permissions, 0);
    }
}

Ensure Imports

Just in case your project did not auto import as you went along, you can replace the import statements in MainActivity.java with these:

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;

import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Date;
import java.text.SimpleDateFormat;

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

Build and Run!

🎉 You now have a fully functioning ToDo app. Build and run it on a device. The simulator will not show any data sync because neither Bluetooth or the necessary network system is available to allow simulators to find each other or another device.

Android ToDo App Syncing
Top