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.
- RxJava Understanding Observables to understand different types of Observables available in RxJava and the purpose of each.
- Android Working with Retrofit HTTP Library Implementation of Retrofit HTTP library without RxJava
- Android Working with Recycler View Basic list rendering using an Adapter.
- Android ButterKnife View binding / injection using ButterKnife library
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.
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.
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("•")); // 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.
I hope this article explained Retrofit very well. If you have any queries, please comment below.