Retrofit is an awesome networking library because of it’s simplicity and ease of use. I have already written an article about Retrofit in plain android. In this article we are going to learn how use Retrofit with RxJava.

We’ll consider a simple use case of Notes App that consumes REST API and stores the notes on cloud storage.

1. Prerequisite

Before start reading this article, I suggest you go through below tutorials and get familiar with the concepts.

2. App Design

I am keeping the app design to be very minimal. The app will have only one screen displaying the notes in a list manner. And there would be a FAB button and couple of Dialogs to create or edit the notes.

android-rxjava-retrofit-tutorial-notes-app

3. Notes REST API

To demonstrate this example, I have created a simple Notes REST API that simulates realtime api. You can perform CRUD operations (Create, Read, Update and Delete) on the api. Note that, as this is a demo app, you can create at max 15 notes only. I suggest you use this API for testing purpose only, don’t use it in your realtime apps.

Base URL: https://demo.androidhive.info
Example: https://demo.androidhive.info/notes/all

Header Field Value Description
Authorization 3d83ba21699XXX Use the api_key received in /register call
Content-Type application/json
Endpoint Method Params
/notes/user/register POST `device_id` – Unique identifier of the device
/notes/all GET NONE
/notes/new POST `note` – Note text
/notes/{id} PUT `id` – Id of the note to be updated
(Replace the {id} with actual value in the URL)
/notes/{id} DELETE `id` – Id of the note to be deleted

4. Creating New Project

Now let’s begin by creating a new project in Android Studio. Once the project is created, we’ll do the basic setup like adding required dependencies, resource files and layouts those required for app design.

1. Create a new project in Android Studio from File ⇒ New Project and select Basic Activity from templates.

2. Add RecyclerView, ButterKnife and Retrofit dependencies to your app/build.gradle and Sync the project.

dependencies {
    // ...

    implementation 'com.android.support:recyclerview-v7:26.1.0'

    // RxJava
    implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

    // Butterknife
    implementation "com.jakewharton:butterknife:8.8.1"
    annotationProcessor "com.jakewharton:butterknife-compiler:8.8.1"

    // Retrofit and OkHttp
    // OkHttp interceptors for logging
    implementation "com.squareup.retrofit2:retrofit:2.0.0"
    implementation "com.squareup.retrofit2:converter-gson:2.0.0"
    implementation "com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0"
    implementation "com.squareup.okhttp3:okhttp:3.0.1"
    implementation "com.squareup.okhttp3:okhttp-urlconnection:3.0.1"
    implementation "com.squareup.okhttp3:logging-interceptor:3.4.1"
}

3. Add the below resources to respective colors.xml, strings.xml and dimens.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#111</color>
    <color name="colorPrimaryDark">#FFF</color>
    <color name="colorAccent">#36D3B2</color>
    <color name="msg_no_notes">#999</color>
    <color name="hint_enter_note">#89c3c3c3</color>
    <color name="timestamp">#858585</color>
    <color name="note_list_text">#232323</color>
</resources>
<resources>
    <string name="app_name">RxJava Retrofit</string>
    <string name="action_settings">Settings</string>
    <string name="activity_title_home">Notes</string>
    <string name="msg_no_notes">No notes found!</string>
    <string name="lbl_new_note_title">New Note</string>
    <string name="hint_enter_note">Enter your note!</string>
</resources>
<resources>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="activity_margin">16dp</dimen>
    <dimen name="dot_margin_right">10dp</dimen>
    <dimen name="msg_no_notes">26sp</dimen>
    <dimen name="margin_top_no_notes">120dp</dimen>
    <dimen name="lbl_new_note_title">20sp</dimen>
    <dimen name="dimen_10">10dp</dimen>
    <dimen name="input_new_note">20sp</dimen>
    <dimen name="dot_height">30dp</dimen>
    <dimen name="dot_text_size">40sp</dimen>
    <dimen name="timestamp">14sp</dimen>
    <dimen name="note_list_text">18sp</dimen>
</resources>

4. Create an xml named array.xml under res ⇒ values. This file contains list of color codes those required to generate a random color dot before every note.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="mdcolor_400">
        <item name="red_400" type="color">#e84e40</item>
        <item name="pink_400" type="color">#ec407a</item>
        <item name="purple_400" type="color">#ab47bc</item>
        <item name="deep_purple_400" type="color">#7e57c2</item>
        <item name="indigo_400" type="color">#5c6bc0</item>
        <item name="blue_400" type="color">#738ffe</item>
        <item name="light_blue_400" type="color">#29b6f6</item>
        <item name="cyan_400" type="color">#26c6da</item>
        <item name="teal_400" type="color">#26a69a</item>
        <item name="green_400" type="color">#2baf2b</item>
        <item name="light_green_400" type="color">#9ccc65</item>
        <item name="lime_400" type="color">#d4e157</item>
        <item name="yellow_400" type="color">#ffee58</item>
        <item name="orange_400" type="color">#ffa726</item>
        <item name="deep_orange_400" type="color">#ff7043</item>
        <item name="brown_400" type="color">#8d6e63</item>
        <item name="grey_400" type="color">#bdbdbd</item>
        <item name="blue_grey_400" type="color">#78909c</item>
    </array>
    <array name="mdcolor_500">
        <item name="red_500" type="color">#e51c23</item>
        <item name="pink_500" type="color">#e91e63</item>
        <item name="purple_500" type="color">#9c27b0</item>
        <item name="deep_purple_500" type="color">#673ab7</item>
        <item name="indigo_500" type="color">#3f51b5</item>
        <item name="blue_500" type="color">#5677fc</item>
        <item name="light_blue_500" type="color">#03a9f4</item>
        <item name="cyan_500" type="color">#00bcd4</item>
        <item name="teal_500" type="color">#009688</item>
        <item name="green_500" type="color">#259b24</item>
        <item name="light_green_500" type="color">#8bc34a</item>
        <item name="lime_500" type="color">#cddc39</item>
        <item name="yellow_500" type="color">#ffeb3b</item>
        <item name="orange_500" type="color">#ff9800</item>
        <item name="deep_orange_500" type="color">#ff5722</item>
        <item name="brown_500" type="color">#795548</item>
        <item name="grey_500" type="color">#9e9e9e</item>
        <item name="blue_grey_500" type="color">#607d8b</item>
    </array>
</resources>

5. Open styles.xml and modify it as below. Here we are modifying the default theme and applying Light theme to our app.

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Light" />
    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

6. Open AndroidManifest.xml and add INTERNET permission as we are going to make HTTP calls.

<uses-permission android:name="android.permission.INTERNET" />

Now, as we have all the resources in place, let’s give a structure to our project. I am creating few packages to keep the project organised.

7. Create the packages named app, network, network/model, utils and view.

Below image gives you a heads-up about the final project structure and files we are aiming.

notes-app-project-structure

8. Under app package, create a class named Const.java. This file contains global static variables. I am declaring the REST API base URL here.

public class Const {
    public static final String BASE_URL = "https://demo.androidhive.info/";
}

9. Utils package contains helper files required for this app. Under utils package, create a class named PrefUtils.java.

This class stores and retrieves the API Key that needs to be sent in every HTTP call as Authorization header field.

import android.content.Context;
import android.content.SharedPreferences;
public class PrefUtils {
    /**
     * Storing API Key in shared preferences to
     * add it in header part of every retrofit request
     */
    public PrefUtils() {
    }

    private static SharedPreferences getSharedPreferences(Context context) {
        return context.getSharedPreferences("APP_PREF", Context.MODE_PRIVATE);
    }

    public static void storeApiKey(Context context, String apiKey) {
        SharedPreferences.Editor editor = getSharedPreferences(context).edit();
        editor.putString("API_KEY", apiKey);
        editor.commit();
    }

    public static String getApiKey(Context context) {
        return getSharedPreferences(context).getString("API_KEY", null);
    }
}

10. Create two more classes named MyDividerItemDecoration.java and RecyclerTouchListener.java. These two classes adds divider line and touch listener to RecyclerView.

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;

public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;
    private int mOrientation;
    private Context context;
    private int margin;

    public MyDividerItemDecoration(Context context, int orientation, int margin) {
        this.context = context;
        this.margin = margin;
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left + dpToPx(margin), top, right - dpToPx(margin), bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top + dpToPx(margin), right, bottom - dpToPx(margin));
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

    private int dpToPx(int dp) {
        Resources r = context.getResources();
        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));
    }
}
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

public class RecyclerTouchListener implements RecyclerView.OnItemTouchListener {

    private ClickListener clicklistener;
    private GestureDetector gestureDetector;

    public RecyclerTouchListener(Context context, final RecyclerView recycleView, final ClickListener clicklistener) {

        this.clicklistener = clicklistener;
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                View child = recycleView.findChildViewUnder(e.getX(), e.getY());
                if (child != null && clicklistener != null) {
                    clicklistener.onLongClick(child, recycleView.getChildAdapterPosition(child));
                }
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        View child = rv.findChildViewUnder(e.getX(), e.getY());
        if (child != null && clicklistener != null && gestureDetector.onTouchEvent(e)) {
            clicklistener.onClick(child, rv.getChildAdapterPosition(child));
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    }

    public interface ClickListener {
        void onClick(View view, int position);
        void onLongClick(View view, int position);
    }
}

4.1 Adding Retrofit Network Layer

Now we have reached to core part of the article i.e adding Retrofit network layer. We’ll start by adding few POJO classes necessary to serialize the JSON.

BaseResponse – As every response will have a error node, we define the error node in BaseResponse class and extend this class in other models.

User – User response once the device is registered. For now this model will have apiKey only.

Note – Defines the note object with id, note and timestamp fields.

11. Under network/model package, create a class named BaseResponse.java

public class BaseResponse {
    String error;

    public String getError() {
        return error;
    }
}

12. Create another model User.java under network/model and extend the class from BaseResponse.

import com.google.gson.annotations.SerializedName;

public class User extends BaseResponse {

    @SerializedName("api_key")
    String apiKey;

    public String getApiKey() {
        return apiKey;
    }
}

13. Create class Note.java under network/model and extend it from BaseResponse.

public class Note extends BaseResponse{
    int id;
    String note;
    String timestamp;

    public int getId() {
        return id;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }

    public String getTimestamp() {
        return timestamp;
    }
}

14. Under network package create a class named ApiClient.java and add below code. In this class we configure the Retrofit client by adding necessary configuration.

  • The Authorization header field is added if the API Key is present in Shared Preferences. This header field is mandatory in every http call except /register call. Without proper API Key, all the calls will be denied.
  • HttpLoggingInterceptor are added to print the Request / Response in LogCat for debugging purpose. You can notice the logged information in the LogCat of Android Studio.
import android.content.Context;
import android.text.TextUtils;

import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import info.androidhive.rxjavaretrofit.app.Const;
import info.androidhive.rxjavaretrofit.utils.PrefUtils;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.TlsVersion;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * Created by ravi on 20/02/18.
 */

public class ApiClient {
    private static Retrofit retrofit = null;
    private static int REQUEST_TIMEOUT = 60;
    private static OkHttpClient okHttpClient;

    public static Retrofit getClient(Context context) {

        if (okHttpClient == null)
            initOkHttp(context);

        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(Const.BASE_URL)
                    .client(okHttpClient)
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }

    private static void initOkHttp(final Context context) {
        OkHttpClient.Builder httpClient = new OkHttpClient().newBuilder()
                .connectTimeout(REQUEST_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(REQUEST_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(REQUEST_TIMEOUT, TimeUnit.SECONDS);

        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        httpClient.addInterceptor(interceptor);

        httpClient.addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request original = chain.request();
                Request.Builder requestBuilder = original.newBuilder()
                        .addHeader("Accept", "application/json")
                        .addHeader("Content-Type", "application/json");

                // Adding Authorization token (API Key)
                // Requests will be denied without API key
                if (!TextUtils.isEmpty(PrefUtils.getApiKey(context))) {
                    requestBuilder.addHeader("Authorization", PrefUtils.getApiKey(context));
                }

                Request request = requestBuilder.build();
                return chain.proceed(request);
            }
        });

        okHttpClient = httpClient.build();
    }
}

15. Create a class named ApiService.java under network package. This class holds the interface methods of every endpoint by defining the endpoint, request and response Observable.

I hope you already had gone through RxJava Understanding Observables and aware of different types of Observables and their purpose.

  • The Single Observable is used for the the endpoints register, notes/new and notes/all as single response will be emitted.
  • Completable Observable is used for both update and delete endpoints as they won’t give any response but the status of the call. You can also notice PUT method is used to update and DELETE method is used to delete a note.
import java.util.List;
import info.androidhive.rxjavaretrofit.network.model.Note;
import info.androidhive.rxjavaretrofit.network.model.User;
import io.reactivex.Completable;
import io.reactivex.Single;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;

/**
 * Created by ravi on 20/02/18.
 */

public interface ApiService {
    // Register new user
    @FormUrlEncoded
    @POST("notes/user/register")
    Single<User> register(@Field("device_id") String deviceId);

    // Create note
    @FormUrlEncoded
    @POST("notes/new")
    Single<Note> createNote(@Field("note") String note);

    // Fetch all notes
    @GET("notes/all")
    Single<List<Note>> fetchAllNotes();

    // Update single note
    @FormUrlEncoded
    @PUT("notes/{id}")
    Completable updateNote(@Path("id") int noteId, @Field("note") String note);

    // Delete note
    @DELETE("notes/{id}")
    Completable deleteNote(@Path("id") int noteId);
}

Using ApiClient and ApiService classes you can make a network call. For example, fetching all notes call can be made like below.

ApiService apiService = ApiClient.getClient(getApplicationContext())
                .create(ApiService.class);

// Fetching all notes
apiService.fetchAllNotes()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(new DisposableSingleObserver<List<Note>>() {
                    @Override
                    public void onSuccess(List<Note> notes) {
                        // Received all notes
                    }

                    @Override
                    public void onError(Throwable e) {
                        // Network error
                    }
                });

4.2 Adding Main Interface

At this stage we have everything in place. The only remaining part is building the main interface and integrate the Retrofit calls to display the data.

16. Create a layout file named note_list_row.xml. In this view file we define the design of the single row item of RecyclerView.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:foreground="?attr/selectableItemBackground"
    android:paddingBottom="@dimen/dimen_10"
    android:paddingLeft="@dimen/activity_margin"
    android:paddingRight="@dimen/activity_margin"
    android:paddingTop="@dimen/dimen_10">

    <TextView
        android:id="@+id/dot"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/dot_height"
        android:layout_marginRight="@dimen/dot_margin_right"
        android:layout_marginTop="@dimen/dimen_10"
        android:includeFontPadding="false"
        android:lineSpacingExtra="0dp"
        android:textSize="@dimen/dot_text_size" />

    <TextView
        android:id="@+id/timestamp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/dot"
        android:textColor="@color/timestamp"
        android:textSize="@dimen/timestamp" />

    <TextView
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/timestamp"
        android:layout_toRightOf="@id/dot"
        android:textColor="@color/note_list_text"
        android:textSize="@dimen/note_list_text" />

</RelativeLayout>

17. Under view package, Create a class named NotesAdapter.java. This adapter class renders the RecyclerView with defined layout and data.

  • If you notice the app design, every note is prepending with coloured dot. To generate the dot with a random color, getRandomMaterialColor() method is used. It basically chooses a random color from array.xml
  • formatDate() converts the received timestamp from the API to MMM d format. For example, the timestamp 2018-02-21 10:01:03 is converted to Feb 21.
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import info.androidhive.rxjavaretrofit.R;
import info.androidhive.rxjavaretrofit.network.model.Note;

public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.MyViewHolder> {

    private Context context;
    private List<Note> notesList;

    public class MyViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.note)
        TextView note;

        @BindView(R.id.dot)
        TextView dot;

        @BindView(R.id.timestamp)
        TextView timestamp;

        public MyViewHolder(View view) {
            super(view);
            ButterKnife.bind(this, view);
        }
    }


    public NotesAdapter(Context context, List<Note> notesList) {
        this.context = context;
        this.notesList = notesList;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.note_list_row, parent, false);

        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Note note = notesList.get(position);

        holder.note.setText(note.getNote());

        // Displaying dot from HTML character code
        holder.dot.setText(Html.fromHtml("&#8226;"));

        // Changing dot color to random color
        holder.dot.setTextColor(getRandomMaterialColor("400"));

        // Formatting and displaying timestamp
        holder.timestamp.setText(formatDate(note.getTimestamp()));
    }

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

    /**
     * Chooses random color defined in res/array.xml
     */
    private int getRandomMaterialColor(String typeColor) {
        int returnColor = Color.GRAY;
        int arrayId = context.getResources().getIdentifier("mdcolor_" + typeColor, "array", context.getPackageName());

        if (arrayId != 0) {
            TypedArray colors = context.getResources().obtainTypedArray(arrayId);
            int index = (int) (Math.random() * colors.length());
            returnColor = colors.getColor(index, Color.GRAY);
            colors.recycle();
        }
        return returnColor;
    }

    /**
     * Formatting timestamp to `MMM d` format
     * Input: 2018-02-21 00:15:42
     * Output: Feb 21
     */
    private String formatDate(String dateStr) {
        try {
            SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date date = fmt.parse(dateStr);
            SimpleDateFormat fmtOut = new SimpleDateFormat("MMM d");
            return fmtOut.format(date);
        } catch (ParseException e) {

        }

        return "";
    }
}

18. Create another layout named note_dialog.xml. This layout will have an EditText to create or update a note.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_margin"
    android:paddingRight="@dimen/activity_margin"
    android:paddingTop="@dimen/activity_margin">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/dimen_10"
        android:fontFamily="sans-serif-medium"
        android:lineSpacingExtra="8sp"
        android:text="@string/lbl_new_note_title"
        android:textColor="@color/colorAccent"
        android:textSize="@dimen/lbl_new_note_title"
        android:textStyle="normal" />

    <EditText
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        android:gravity="top"
        android:hint="@string/hint_enter_note"
        android:inputType="textCapSentences|textMultiLine"
        android:lines="4"
        android:textColorHint="@color/hint_enter_note"
        android:textSize="@dimen/input_new_note" />

</LinearLayout>

19. Open the layout files of main activity i.e activity_main.xml and content_main.xml add AppBarLayout and RecyclerView.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/coordinator_layout"
    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="info.androidhive.rxjavaretrofit.view.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@android:color/white"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

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

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

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@drawable/ic_add_white_24dp" />

</android.support.design.widget.CoordinatorLayout>
<?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"
    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="info.androidhive.rxjavaretrofit.view.MainActivity"
    tools:showIn="@layout/activity_main">

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

    <TextView
        android:id="@+id/txt_empty_notes_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/margin_top_no_notes"
        android:fontFamily="sans-serif-light"
        android:text="@string/msg_no_notes"
        android:textColor="@color/msg_no_notes"
        android:textSize="@dimen/msg_no_notes" />

</RelativeLayout>

20. Finally open MainActivity.java do the below modifications.

  • registerUser() – Makes /register call to register the device. Every device is uniquely identified by randomUUID(), so the notes will be bound to a device.
  • fetchAllNotes() – Fetches all the notes created from the device and displays them in RecyclerView. The API returns the notes in random order, so the map() operator is used to sort the notes in descending order by note id.
  • createNote() – Creates new note and adds it to RecyclerView. The newly created note is added to 0 position and notifyItemInserted(0) is called to refresh the list.
  • updateNote() – Updates existing note and notifies the adapter by calling notifyItemChanged()
  • deleteNote() – Deletes existing note and notifies the adapter by calling notifyItemRemoved() method.
  • showNoteDialog() – Opens a Dialog to Create or Update a note. This dialog will be opened by tapping on FAB.
  • showActionsDialog() – Open a Dialog with Edit or Delete options. This dialog will be opened by long pressing Note row.
  • CompositeDisposable – Disposes all the subscriptions in onDestroy() method
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.jakewharton.retrofit2.adapter.rxjava2.HttpException;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;

import butterknife.BindView;
import butterknife.ButterKnife;
import info.androidhive.rxjavaretrofit.R;
import info.androidhive.rxjavaretrofit.network.ApiClient;
import info.androidhive.rxjavaretrofit.network.ApiService;
import info.androidhive.rxjavaretrofit.network.model.Note;
import info.androidhive.rxjavaretrofit.network.model.User;
import info.androidhive.rxjavaretrofit.utils.MyDividerItemDecoration;
import info.androidhive.rxjavaretrofit.utils.PrefUtils;
import info.androidhive.rxjavaretrofit.utils.RecyclerTouchListener;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Function;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private ApiService apiService;
    private CompositeDisposable disposable = new CompositeDisposable();
    private NotesAdapter mAdapter;
    private List<Note> notesList = new ArrayList<>();

    @BindView(R.id.coordinator_layout)
    CoordinatorLayout coordinatorLayout;

    @BindView(R.id.recycler_view)
    RecyclerView recyclerView;

    @BindView(R.id.txt_empty_notes_view)
    TextView noNotesView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.setTitle(getString(R.string.activity_title_home));
        setSupportActionBar(toolbar);


        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                showNoteDialog(false, null, -1);
            }
        });

        // white background notification bar
        whiteNotificationBar(fab);

        apiService = ApiClient.getClient(getApplicationContext()).create(ApiService.class);

        mAdapter = new NotesAdapter(this, notesList);
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL, 16));
        recyclerView.setAdapter(mAdapter);

        /**
         * On long press on RecyclerView item, open alert dialog
         * with options to choose
         * Edit and Delete
         * */
        recyclerView.addOnItemTouchListener(new RecyclerTouchListener(this,
                recyclerView, new RecyclerTouchListener.ClickListener() {
            @Override
            public void onClick(View view, final int position) {
            }

            @Override
            public void onLongClick(View view, int position) {
                showActionsDialog(position);
            }
        }));

        /**
         * Check for stored Api Key in shared preferences
         * If not present, make api call to register the user
         * This will be executed when app is installed for the first time
         * or data is cleared from settings
         * */
        if (TextUtils.isEmpty(PrefUtils.getApiKey(this))) {
            registerUser();
        } else {
            // user is already registered, fetch all notes
            fetchAllNotes();
        }
    }

    /**
     * Registering new user
     * sending unique id as device identification
     * https://developer.android.com/training/articles/user-data-ids.html
     */
    private void registerUser() {
        // unique id to identify the device
        String uniqueId = UUID.randomUUID().toString();

        disposable.add(
                apiService
                        .register(uniqueId)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeWith(new DisposableSingleObserver<User>() {
                            @Override
                            public void onSuccess(User user) {
                                // Storing user API Key in preferences
                                PrefUtils.storeApiKey(getApplicationContext(), user.getApiKey());

                                Toast.makeText(getApplicationContext(),
                                        "Device is registered successfully! ApiKey: " + PrefUtils.getApiKey(getApplicationContext()),
                                        Toast.LENGTH_LONG).show();
                            }

                            @Override
                            public void onError(Throwable e) {
                                Log.e(TAG, "onError: " + e.getMessage());
                                showError(e);
                            }
                        }));
    }

    /**
     * Fetching all notes from api
     * The received items will be in random order
     * map() operator is used to sort the items in descending order by Id
     */
    private void fetchAllNotes() {
        disposable.add(
                apiService.fetchAllNotes()
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .map(new Function<List<Note>, List<Note>>() {
                            @Override
                            public List<Note> apply(List<Note> notes) throws Exception {
                                // TODO - note about sort
                                Collections.sort(notes, new Comparator<Note>() {
                                    @Override
                                    public int compare(Note n1, Note n2) {
                                        return n2.getId() - n1.getId();
                                    }
                                });
                                return notes;
                            }
                        })
                        .subscribeWith(new DisposableSingleObserver<List<Note>>() {
                            @Override
                            public void onSuccess(List<Note> notes) {
                                notesList.clear();
                                notesList.addAll(notes);
                                mAdapter.notifyDataSetChanged();

                                toggleEmptyNotes();
                            }

                            @Override
                            public void onError(Throwable e) {
                                Log.e(TAG, "onError: " + e.getMessage());
                                showError(e);
                            }
                        })
        );
    }

    /**
     * Creating new note
     */
    private void createNote(String note) {
        disposable.add(
                apiService.createNote(note)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeWith(new DisposableSingleObserver<Note>() {

                            @Override
                            public void onSuccess(Note note) {
                                if (!TextUtils.isEmpty(note.getError())) {
                                    Toast.makeText(getApplicationContext(), note.getError(), Toast.LENGTH_LONG).show();
                                    return;
                                }

                                Log.d(TAG, "new note created: " + note.getId() + ", " + note.getNote() + ", " + note.getTimestamp());

                                // Add new item and notify adapter
                                notesList.add(0, note);
                                mAdapter.notifyItemInserted(0);

                                toggleEmptyNotes();
                            }

                            @Override
                            public void onError(Throwable e) {
                                Log.e(TAG, "onError: " + e.getMessage());
                                showError(e);
                            }
                        }));
    }

    /**
     * Updating a note
     */
    private void updateNote(int noteId, final String note, final int position) {
        disposable.add(
                apiService.updateNote(noteId, note)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeWith(new DisposableCompletableObserver() {
                            @Override
                            public void onComplete() {
                                Log.d(TAG, "Note updated!");

                                Note n = notesList.get(position);
                                n.setNote(note);

                                // Update item and notify adapter
                                notesList.set(position, n);
                                mAdapter.notifyItemChanged(position);
                            }

                            @Override
                            public void onError(Throwable e) {
                                Log.e(TAG, "onError: " + e.getMessage());
                                showError(e);
                            }
                        }));
    }

    /**
     * Deleting a note
     */
    private void deleteNote(final int noteId, final int position) {
        Log.e(TAG, "deleteNote: " + noteId + ", " + position);
        disposable.add(
                apiService.deleteNote(noteId)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeWith(new DisposableCompletableObserver() {
                            @Override
                            public void onComplete() {
                                Log.d(TAG, "Note deleted! " + noteId);

                                // Remove and notify adapter about item deletion
                                notesList.remove(position);
                                mAdapter.notifyItemRemoved(position);

                                Toast.makeText(MainActivity.this, "Note deleted!", Toast.LENGTH_SHORT).show();

                                toggleEmptyNotes();
                            }

                            @Override
                            public void onError(Throwable e) {
                                Log.e(TAG, "onError: " + e.getMessage());
                                showError(e);
                            }
                        })
        );
    }

    /**
     * Shows alert dialog with EditText options to enter / edit
     * a note.
     * when shouldUpdate=true, it automatically displays old note and changes the
     * button text to UPDATE
     */
    private void showNoteDialog(final boolean shouldUpdate, final Note note, final int position) {
        LayoutInflater layoutInflaterAndroid = LayoutInflater.from(getApplicationContext());
        View view = layoutInflaterAndroid.inflate(R.layout.note_dialog, null);

        AlertDialog.Builder alertDialogBuilderUserInput = new AlertDialog.Builder(MainActivity.this);
        alertDialogBuilderUserInput.setView(view);

        final EditText inputNote = view.findViewById(R.id.note);
        TextView dialogTitle = view.findViewById(R.id.dialog_title);
        dialogTitle.setText(!shouldUpdate ? getString(R.string.lbl_new_note_title) : getString(R.string.lbl_edit_note_title));

        if (shouldUpdate && note != null) {
            inputNote.setText(note.getNote());
        }
        alertDialogBuilderUserInput
                .setCancelable(false)
                .setPositiveButton(shouldUpdate ? "update" : "save", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialogBox, int id) {

                    }
                })
                .setNegativeButton("cancel",
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialogBox, int id) {
                                dialogBox.cancel();
                            }
                        });

        final AlertDialog alertDialog = alertDialogBuilderUserInput.create();
        alertDialog.show();

        alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Show toast message when no text is entered
                if (TextUtils.isEmpty(inputNote.getText().toString())) {
                    Toast.makeText(MainActivity.this, "Enter note!", Toast.LENGTH_SHORT).show();
                    return;
                } else {
                    alertDialog.dismiss();
                }

                // check if user updating note
                if (shouldUpdate && note != null) {
                    // update note by it's id
                    updateNote(note.getId(), inputNote.getText().toString(), position);
                } else {
                    // create new note
                    createNote(inputNote.getText().toString());
                }
            }
        });
    }

    /**
     * Opens dialog with Edit - Delete options
     * Edit - 0
     * Delete - 0
     */
    private void showActionsDialog(final int position) {
        CharSequence colors[] = new CharSequence[]{"Edit", "Delete"};

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Choose option");
        builder.setItems(colors, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (which == 0) {
                    showNoteDialog(true, notesList.get(position), position);
                } else {
                    deleteNote(notesList.get(position).getId(), position);
                }
            }
        });
        builder.show();
    }

    private void toggleEmptyNotes() {
        if (notesList.size() > 0) {
            noNotesView.setVisibility(View.GONE);
        } else {
            noNotesView.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Showing a Snackbar with error message
     * The error body will be in json format
     * {"error": "Error message!"}
     */
    private void showError(Throwable e) {
        String message = "";
        try {
            if (e instanceof IOException) {
                message = "No internet connection!";
            } else if (e instanceof HttpException) {
                HttpException error = (HttpException) e;
                String errorBody = error.response().errorBody().string();
                JSONObject jObj = new JSONObject(errorBody);

                message = jObj.getString("error");
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (JSONException e1) {
            e1.printStackTrace();
        } catch (Exception e1) {
            e1.printStackTrace();
        }

        if (TextUtils.isEmpty(message)) {
            message = "Unknown error occurred! Check LogCat.";
        }

        Snackbar snackbar = Snackbar
                .make(coordinatorLayout, message, Snackbar.LENGTH_LONG);

        View sbView = snackbar.getView();
        TextView textView = sbView.findViewById(android.support.design.R.id.snackbar_text);
        textView.setTextColor(Color.YELLOW);
        snackbar.show();
    }

    private void whiteNotificationBar(View view) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            int flags = view.getSystemUiVisibility();
            flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
            view.setSystemUiVisibility(flags);
            getWindow().setStatusBarColor(Color.WHITE);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        disposable.dispose();
    }
}

Run and test the app once. If you have followed article carefully, you can see the app running smoothly.

rxjava-retrofit-notes-app
android-rxjava-retrofit-notes-app

I hope this article explained Retrofit very well. If you have any queries, please comment below.

Author

  • Abner Escocio

    Muitíssimo obrigado cara. Ajudou muito!!

  • yuri

    it does not work to subscribe. any verify email sent to me….could i know why?

  • Swati Agarwal

    Hi Ravi,
    How can we check if my application is running at all either in foreground or in background

  • sameer khader

    Hello Ravi,

    how can we check if the fragment in the activity is alive in case of foreground for push notification?

  • Jaspreet Singh

    what about changing between two different themes ,do you have any tuts in that ?

  • binod lamichhane

    can you please write an article that uploads image,possibly with multiple images(like registration) with other data using same retrofit and rxjava?it would be great help for me.

  • Yup, I was supposed to be github doc. I’ll update.

  • karthick Ramanathan

    hi Ravi, one doubt, on creating from server we did not create id and timestamp to server and how on success in creating an on fetching data automatically assigned to the elements in model class, because we did not provide serialized name in model class.

    my main doubt is on fetching data how data’s automatically assigned in id, note,timestamp in model class

  • Sai Teja

    Can you share the project url?

  • Nguyễn Linh

    @ravi8x:disqus I have question can you help me.
    Call back onSuccess of Rx use for range http code 200 to ?.
    Can you give me link document to check it.

  • Hasnain Ahmad

    @ravi8x:disqus I am using Retrofit and RxJava in my project I have around 4 APIs of GET fetching data from 4 tables from server and have to insert in the SQLite tables using ActiveAndroid ORM. I want to show a progress bar while downloading the data until it completes and inserted locally. Local table count must be equal to server table count each table count.
    I also want to call each API in specific order on each API response to next one. Which RxJava and Retrofit library keywords, operators and filters should be used?

    • Are you using any Sync Adapters to download the data?

      • Hasnain Ahmad

        Yes i have put my network calls in AbstractThreadedSyncAdapter’s onPerformSync method. Is there any way to put your all network calls in an utility class and on button click fire all network calls from anywhere from the application.

  • Karl Ghosn

    I’m having this issue appear when I try to fetch all the notes “Failed to connect to MySQL: Connection refused”. What’s happening?

    • Pls check now. MySQL service is down on the server.

      Thanks for notifying me:)

      • Karl Ghosn

        Yes it’s working now thank you 😀

  • xxxQDAxxx

    I’m getting HTTP 400 error, and in okHttp it says: D/OkHttp: {“error”:”Authentication token is missing or invalid!”}. Is there something wrong in your server or is it just me?

    • Token is sent with every request in ApiClient. Make sure the token exists and appropriate.

      • xxxQDAxxx

        Oh okay my bad. I got some typo error that’s why I got this error. Thanks for this tutorial.

  • waqas akram

    Error HTTP 404 Not Found
    When i am trying to register.
    I think pages are moved.

    • Could you post the url here.

      • waqas akram

        Thanks There was typo in my end.

  • adoolf

    When insert new note I am getting error ” unknown error occurred “.
    I think there is an error with apiservice

  • kamaldeep kumar

    Hi here, What is benefit of RXJava with Retrofit. I have not much knowledge about RXJava

    • Retrofit is used to make HTTP calls. This article explains how that library can be integrated when using RxJava.

      • kamaldeep kumar

        thanks for reply , yes retrofit can also used direct For HTTP Calling , But why we integration with Rxjava. So any benefit of Rxjava With Retrofit Http Calling

        • Pls read the getting started with RxJava article.

  • Le Quoc Tuan

    Very good! Thank you very much!

  • Mofe-hendy Ejegi

    Awesome tutorial Ravi, I’ve been silently following you for years now 😜. I Really need to understand something; would using RxJava make me lose access to the helpful “OnResponse” and “OnFailure” callback methods associated with Retrofit Calls?

    • You can catch the same on onSuccess, onComplete and onError methods.

  • Jose Carlos Montero

    Hi Ravi, the article is great 🙂 However the remote API you set up seems to be down. Can you take a look at it or have you decided to bring it down?

    Thanks in advance!

    • Could you check now. The server is down. Restarted services and it’s working fine now.

      Thank for letting me know:)

  • Kaushik Goel

    Can you please tell me how to make a online and offline Android application?. i am trying to hit a service using retrofit and then storing the data in the local using room. But i am facing one problem. Each time it’s hitting the service, it is storing the data. How to make it happen just once.?

  • Kaushik Goel

    how to make filter and sort features ? in android application

  • First Last

    How can I mock DisposableSingleObserver for unit tests?
    Can I check onSuccess and onError cases?

  • Tigani Mohammed

    ravi you are awesome thanks guru for all your effort to android community …..I started implement your code sample using mvp ,rxjava,dagger2,kotlin and retrofit

  • George Mujuru

    Hi @ravi8x:disqus is the server still running. I just finished converting the application from Java to kotlin and i am getting a HTTP 500 internal server error

    • Thanks @georgemujuru:disqus . Server is down. It’s working now.

  • Shoaziz

    Hello Ravi Tamada, you are my teacher) I learnt a lot things from your articles. Thank you.
    I have a question about parsing json array to recyclerview. I want to parse json array one by one, I mean first parsing 1st element of array then display, parsing 2nd element then display. Is it correct way to display json data like this?

    • No, we don’t have to parse one by one. The entire json can be parsed once, add the items to Arraylist and then refresh the recyclerview by calling notifiyDatasetChanged()

  • Iridis

    Hello, I think server is down. Could you check it please?

    • Yes. It’s working now. Thanks for letting me know.