Written by Adrian Kremski
Android Developer
Published January 24, 2017

Realm Mobile Platform: Offline-first TaskManager app – Intro

This article is the first one in a series of five. Each part will describe the steps of building offline-first TaskManager app with realtime synchronization. Part one is devoted to defining the technology stack and the UI of the app.

Part 2: REALM MOBILE PLATFORM: OFFLINE-FIRST TASKMANAGER APP – BASICS OF REALM
Part 3: REALM MOBILE PLATFORM: ANDROID “TASKMANAGER” – REALM OBJECT SERVER
Part 4: REALM MOBILE PLATFORM: OFFLINE-FIRST TASKMANAGER APP – AUTHENTICATION
Part 5: Coming soon

Here is the preview of the app that we are going to develop during the series.

apka_final

1. Technology stack

Realm Mobile Platform

A flexible platform for creating offline-first, reactive mobile apps effortlessly.

This platform integrates two technologies we are going to use:

Realm Object Server
Realm Object Server (released around September 2016) is basically a backend responsible for realtime data synchronization, resolving conflicts in data changes, handling various events and authentication. In can be deployed on servers or in the cloud.

Realm Mobile Database
Realm Mobile Database is a cross-platform database, supporting both iOS and Android and it’s open source. For those who are familiar with SQLite database (default choice of persistence in Android) or ORM libraries like ORMLite/ActiveAndroid/GreenDao, you should know that in Realm gives no SQLite at all. Realm Mobile Database is a Zero-copy object store. Sounds familiar? Probably not (I had not known that before attending the Droidcon NYC 2015 – Realm: Building a mobile database).

In this article we are going to use the Developer Edition of RPM.

Ubuntu 16.04. hosted on Amazon EC2

The instance of our Realm Object Server backend will be available through the Ubuntu instance.

2. Useful links

3. TaskManager UI

Before getting to the “cool” stuff, we are going to prepare some basic UI for the app. However, if you are impatient (like I am), you can just checkout to commit 09adf4d in the TaskManager github repository and start from there.

3.1 Gradle

Core dependencies for UI.

compile 'com.android.support:appcompat-v7:25.0.1'
compile "com.android.support:support-annotations:25.0.1"
compile "com.android.support:support-v13:25.0.1"
compile "com.android.support:design:25.0.1"
compile "com.android.support:recyclerview-v7:25.0.1"
compile "com.android.support:cardview-v7:25.0.1"
compile "com.jakewharton:butterknife:7.0.1"
compile 'com.gordonwong:material-sheet-fab:1.2.1' //Library containing the fab button

3.2 Main screen UI

Screenshot_20160718-205804

activity_main.xml

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

    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/coordinator_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </FrameLayout>

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

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

    </android.support.design.widget.CoordinatorLayout>

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

</RelativeLayout>

toolbar.xml
Basic toolbar with a title.

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

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/colorPrimary"
            app:layout_collapseMode="pin"
            app:layout_scrollFlags="scroll|enterAlways"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <TextView
                    android:id="@+id/title"
                    android:text="TaskManager"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:textColor="#fff"
                    android:textStyle="bold"
                    android:textSize="16sp"
                    android:singleLine="true" />


            </FrameLayout>
        </android.support.v7.widget.Toolbar>
    </android.support.design.widget.AppBarLayout>
</merge>

fab.xml
Custom implementation of fab button (copied from this sample)

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <pl.adriankremski.realmtaskmanager.views.Fab
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:clickable="true"
        android:src="@drawable/ic_create_white_24dp"
        app:layout_anchor="@id/recycler"
        app:layout_anchorGravity="bottom|right|end"/>
</merge>

Fab.java

public class Fab extends FloatingActionButton implements AnimatedFab {

    private static final int FAB_ANIM_DURATION = 200;

    public Fab(Context context) {
        super(context);
    }

    public Fab(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Fab(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * Shows the FAB.
     */
    @Override
    public void show() {
        show(0, 0);
    }

    /**
     * Shows the FAB and sets the FAB's translation.
     *
     * @param translationX translation X value
     * @param translationY translation Y value
     */
    @Override
    public void show(float translationX, float translationY) {
        // Set FAB's translation
        setTranslation(translationX, translationY);

        // Only use scale animation if FAB is hidden
        if (getVisibility() != View.VISIBLE) {
            // Pivots indicate where the animation begins from
            float pivotX = getPivotX() + translationX;
            float pivotY = getPivotY() + translationY;

            ScaleAnimation anim;
            // If pivots are 0, that means the FAB hasn't been drawn yet so just use the
            // center of the FAB
            if (pivotX == 0 || pivotY == 0) {
                anim = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f,
                        Animation.RELATIVE_TO_SELF, 0.5f);
            } else {
                anim = new ScaleAnimation(0, 1, 0, 1, pivotX, pivotY);
            }

            // Animate FAB expanding
            anim.setDuration(FAB_ANIM_DURATION);
            anim.setInterpolator(getInterpolator());
            startAnimation(anim);
        }
        setVisibility(View.VISIBLE);
    }

    /**
     * Hides the FAB.
     */
    @Override
    public void hide() {
        // Only use scale animation if FAB is visible
        if (getVisibility() == View.VISIBLE) {
            // Pivots indicate where the animation begins from
            float pivotX = getPivotX() + getTranslationX();
            float pivotY = getPivotY() + getTranslationY();

            // Animate FAB shrinking
            ScaleAnimation anim = new ScaleAnimation(1, 0, 1, 0, pivotX, pivotY);
            anim.setDuration(FAB_ANIM_DURATION);
            anim.setInterpolator(getInterpolator());
            startAnimation(anim);
        }
        setVisibility(View.INVISIBLE);
    }

    private void setTranslation(float translationX, float translationY) {
        animate().setInterpolator(getInterpolator()).setDuration(FAB_ANIM_DURATION)
                .translationX(translationX).translationY(translationY);
    }

    private Interpolator getInterpolator() {
        return AnimationUtils.loadInterpolator(getContext(), R.interpolator.msf_interpolator);
    }
}

sheet.xml
Sheet for adding new tasks in our application.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- Overlay that dims the screen -->
    <com.gordonwong.materialsheetfab.DimOverlayFrameLayout
        android:id="@+id/overlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Circular reveal container for the sheet -->
    <io.codetail.widget.RevealLinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="end|bottom"
        android:orientation="vertical">

        <!-- Sheet that contains your items -->
        <android.support.v7.widget.CardView
            android:id="@+id/fab_sheet"
            android:layout_width="350dp"
            android:layout_height="200dp"
            android:layout_gravity="center_horizontal">

            <EditText
                android:id="@+id/text"
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:layout_gravity="top"
                android:gravity="top|left"
                android:inputType="textMultiLine"
                android:lines="4"
                android:maxLines="6"
                android:minLines="4"
                android:singleLine="false" />

            <Button
                android:id="@+id/add_task"
                android:background="@drawable/ripple"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:layout_gravity="bottom"
                android:textColor="#fff"
                android:textStyle="bold"
                android:text="Add task"
                android:textAllCaps="true" />

        </android.support.v7.widget.CardView>
    </io.codetail.widget.RevealLinearLayout>
</merge>

3.3 MainActivity

Finally, let’s connect everything in our MainActivity.

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.recycler)
    RecyclerView recyclerView;

    @Bind(R.id.fab)
    Fab fab;

    @Bind(R.id.fab_sheet)
    View sheetView;

    @Bind(R.id.overlay)
    View overlayView;

    @Bind(R.id.text)
    TextView textInputField;

    private MaterialSheetFab materialSheetFab;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toolbar toolbar = ButterKnife.findById(this, R.id.toolbar);
        fab.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.colorPrimary));
        setSupportActionBar(toolbar);
        setupMaterialSheetFab();
    }

    private void setupMaterialSheetFab() {
        int sheetColor = ContextCompat.getColor(this, R.color.background_light);
        int fabColor = ContextCompat.getColor(this, R.color.colorPrimary);

        materialSheetFab = new MaterialSheetFab(fab, sheetView, overlayView,
                sheetColor, fabColor);

        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) sheetView.getLayoutParams();
        Point size = new Point();
        getWindowManager().getDefaultDisplay().getSize(size);
        params.width = (int) (size.x * 0.9); // we don't want our sheet to cover whole width of the screen
        sheetView.setLayoutParams(params);
    }

    @OnClick(R.id.add_task)
    public void addTask() {
        if (materialSheetFab.isSheetVisible()) {
            materialSheetFab.hideSheet();
        }
    }

    @Override
    public void onBackPressed() {
        if (materialSheetFab.isSheetVisible()) {
            materialSheetFab.hideSheet();
        } else {
            super.onBackPressed();
        }
    }
}

Result
nkbewtfk2z

3.4 Managing tasks

Since our basic UI of the main screen is done, we can now proceed to implement one of our main functionalities. To do that, we are going to define the Task model, corresponding TaskRowHolder, and finally the TaskAdapter. These classes are very simple, so I guess that no explanation is needed.

Task

public class Task {

    private boolean completed;
    private String text;

    public Task(String text) {
        this.text = text;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

TaskRowHolder

public class TaskRowHolder extends RecyclerView.ViewHolder {

    @Bind(R.id.task_name)
    TextView taskNameLabel;

    @Bind(R.id.checkbox)
    CheckBox checkBox;

    private Task task;

    public TaskRowHolder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                task.setCompleted(isChecked);
            }
        });
    }

    public void setTask(Task task) {
        this.task = task;
        taskNameLabel.setText(task.getText());
        checkBox.setChecked(task.isCompleted());
    }
}

TaskAdapter

public class TaskAdapter extends RecyclerView.Adapter<TaskRowHolder> {

    private List<Task> tasks = new LinkedList<>();

    public void addTask(Task task) {
        tasks.add(0, task);
        notifyItemInserted(0);
    }

    @Override
    public TaskRowHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_task_row, parent, false);
        return new TaskRowHolder(itemView);
    }

    @Override
    public void onBindViewHolder(TaskRowHolder holder, int position) {
        holder.setTask(tasks.get(position));
    }

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

    public void removeTask(int position) {
        tasks.remove(position);
        notifyItemRemoved(position);
    }
}

view_task_row.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="4dp"
    card_view:cardCornerRadius="4dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="4dp"
        android:orientation="horizontal"
        android:padding="8dp">

        <TextView
            android:id="@+id/task_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="8dp"
            android:layout_alignParentLeft="true"
            android:layout_toLeftOf="@id/checkbox"
            android:fontFamily="sans-serif-light"
            android:textSize="18sp" />

        <CheckBox
            android:id="@+id/checkbox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginLeft="8dp" />

    </RelativeLayout>
</android.support.v7.widget.CardView>

Now we need to connect our code to MainActivity.

public class MainActivity extends AppCompatActivity {

    ...

    private TaskAdapter taskAdapter = new TaskAdapter();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        recyclerView.setAdapter(taskAdapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(getBaseContext()));
    }

    ...

    @OnClick(R.id.add_task)
    public void addTask() {
        ...

        Task newTask = new Task(textInputField.getText().toString());
        textInputField.setText("");
        taskAdapter.addTask(newTask);
    }
}

Result

nkbewtfk2z

3.5 Login screen UI

We need some kind of authentication since our data will be connected to particular users. Therefore, in one of the incoming posts we are going to implement the auth logic using Realm (yes, Realm provides this feature :)).
However, we will also need the UI part of login to collect the user data.

A pretty simple layout of the login screen. It will have two fields (username + password) and two actions (register + login).

login_activity.xml

<RelativeLayout 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"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:paddingTop="10dp"
        android:gravity="center"
        android:textStyle="bold"
        android:textColor="@color/colorAccent"
        android:text="TaskManager"/>

    <LinearLayout
        android:layout_below="@id/title"
        android:id="@+id/form"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:orientation="vertical"
        android:paddingBottom="10dp"
        android:paddingTop="16dp">

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <AutoCompleteTextView
                android:id="@+id/username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="test@gmail.com"
                android:hint="Login"
                android:inputType="textAutoComplete"
                android:maxLines="1"
                android:singleLine="true" />

        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <EditText
                android:id="@+id/password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="test"
                android:hint="Password"
                android:imeActionId="@+id/log_in"
                android:imeActionLabel=""
                android:imeOptions="actionUnspecified"
                android:inputType="textPassword"
                android:maxLines="1"
                android:singleLine="true" />

        </android.support.design.widget.TextInputLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_marginTop="16dp"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <Button
                android:id="@+id/register"
                style="?android:textAppearanceSmall"
                android:layout_weight="0.5"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="Register"
                android:textStyle="bold" />

            <Button
                android:id="@+id/login"
                style="?android:textAppearanceSmall"
                android:layout_weight="0.5"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="Login"
                android:textStyle="bold" />
        </LinearLayout>

    </LinearLayout>

    <ProgressBar
        android:layout_below="@id/form"
        android:id="@+id/progress"
        android:layout_centerHorizontal="true"
        style="?android:attr/progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:visibility="gone"
        tools:visibility="visible" />
</RelativeLayout>

Out Login Screen implementation is also really basic, but we will come back to it later.

LoginActivity

public class LoginActivity extends AppCompatActivity {

    @Bind(R.id.username)
    TextView usernameLabel;

    @Bind(R.id.password)
    TextView passwordLabel;

    @Bind(R.id.progress)
    View progressView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login_activity);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.login)
    public void login() {
        showMainScreen();
    }

    private void showMainScreen() {
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        startActivity(intent);
    }

    @OnClick(R.id.register)
    public void register() {
    }
}

Don’t forget to update AndroidManifest!

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="pl.adriankremski.taskmanager">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        ...

    </application>

</manifest>

Result

nkbewtfk2z

Thanks for reading!

The next part of the series is already available!
Part 2: REALM MOBILE PLATFORM: OFFLINE-FIRST TASKMANAGER APP – BASICS OF REALM

Written by Adrian Kremski
Android Developer
Published January 24, 2017