Loading...
Android

Great Android App Architecture [Part 5 – Finishing up!]

Thanks you people for bearing with me for last 4 articles [ 123, 4 ]  This will be the last one in the series. Hopefully! Without wasting any time in chitter chatter, lets get to business.

So as of now, our app displays only one quote. Now we need to make it display multiple quotes (say 10), we have to call the API with second parameter as 10 and make little changes to our data model and repository classes. Before we do it, I want you to imagine kind of changes you would require to do if you don’t use the current architecture which we are using. First of all, you will have to change the API call (which will be ultra easy in our case, thank you Retrofit), then changes in DBHelper classes (again, thanks to Room, it will be easy in our case) and changes in JSON to Java objects conversion mechanism you would have used. Seems like lot of work to me.

To get multiple quotes, we have to convert our LiveData<Quote> to LiveData<List<Quote>>. QuoteAPI interface will stay exactly same as before, only return type of getQuote method will change.

public interface QuoteAPI {

    //I insist you put your own key here fellas

    @Headers({
            "X-Mashape-Key: your_Api_key",
            "Content-Type: application/x-www-form-urlencoded",
            "Accept: application/json"
    })

    @GET("/")
    Call <Quote> getQuote(@Query("cat") String category,
                              @Query("count") int count);

}

 

In similar manner, wherever we have written LiveData<Quote> in QuoteRepository.java, QuoteViewModel.java and QuoteDAO.java, we will replace with LiveData<List<Quote>>. Easy peasy! I will not paste code again here for brevity. You can always get source on GitHub.

Only notable difference we are going to make in our data side is in our pojo class Quote.java

@Entity
public class Quote{

    @PrimaryKey(autoGenerate = true)
    private int id=0;

    private String quote;
    private String author;
    private String category;

    public String getQuote() {
        return quote;
    }

    public void setQuote(String quote) {
        this.quote = quote;
    }

    //getter setters omitted for brevity
}

 

Can you spot, what little difference we made?

@PrimaryKey(autoGenerate = true)

We set autogenerate property of PrimaryKey annotation to make sure that multiple quote records will be stored in DB. And we are done with our changes in data side.

As far as, our view side matters, only difference is that, now we will be receiving list of quotes instead of single quote. So I wrote simple Recycler View adapter class for displaying quotes. For implementing Recycler View, we need to write view of item in xml and adapter class to fill it with data. I will not be discussing about recycler view implementation here. You can learn more about Recycler View here and here.

public class AdapterQuotes extends RecyclerView.Adapter<AdapterQuotes.MyViewHolder>{
    private LayoutInflater inflater;
    private List<Quote> quoteItems;
    private Context context;
    //private Typeface type;

    public AdapterQuotes(Context context){
        this.quoteItems=new ArrayList<>();
        inflater= LayoutInflater.from(context);
        this.context = context;
        //type = Typeface.createFromAsset(context.getAssets(),"fonts/angelina.TTF");
    }

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

    @Override
    public void onBindViewHolder(AdapterQuotes.MyViewHolder holder, int position) {
        holder.quote.setText(quoteItems.get(position).getQuote());
        holder.author.setText(quoteItems.get(position).getAuthor());

        //just a fancy animation, nothing else
        setFadeAnimation(holder.itemView);
    }

    private void setFadeAnimation(View view) {
        ScaleAnimation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
       //AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
        anim.setDuration(300);
        view.startAnimation(anim);
    }

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

    public void insertItems(List<Quote> quotes){
        //add items at the top
        for(int i=0;i<quotes.size();i++){
            quoteItems.add(0,quotes.get(i));
        }
        notifyItemRangeInserted(0,quotes.size()-1);
    }

    public void clearItems(){
        quoteItems.clear();
    }

    class MyViewHolder extends RecyclerView.ViewHolder{
        TextView quote, author;

        MyViewHolder(View itemView) {
            super(itemView);
            quote = (TextView)itemView.findViewById(R.id.quote);
            author = (TextView)itemView.findViewById(R.id.author);

           // quote.setTypeface(type);
            //author.setTypeface(type);
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="10dp">

    <TextView
        android:id="@+id/quote"
        android:textSize="20sp"
        android:layout_margin="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/author"
        android:textSize="20sp"
        android:layout_margin="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


</LinearLayout>

And our updated FragmentQuote.java for accumulating the recycler view changes.

public class FragmentQuote extends LifecycleFragment{

    private QuoteViewModel quoteViewModel; //data model
    private SwipeRefreshLayout swipeRefreshLayout;  //swipe to refresh data
    private RecyclerView recyclerView;
    private AdapterQuotes adapterQuotes;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_quote,container,false);
        recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view_quotes);
        swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_to_refresh);
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                //changes will be reflected automatically, thanks to LiveData
                quoteViewModel.refreshData();
            }
        });

        //set up recycler view
        adapterQuotes = new AdapterQuotes(getContext());
        recyclerView.setAdapter(adapterQuotes);
        recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(getContext()));

        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        //create dataModel object
        //remember, doesn't matter how many times this method called, same object will be returned
        quoteViewModel = ViewModelProviders.of(this).get(QuoteViewModel.class);
        quoteViewModel.init( getArguments().getString("category")
                ,getArguments().getInt("count"));

        //observer data for any changes
        //no need to unregister this receiver anywhere, lifecycle aware components, duh
        quoteViewModel.getQuote().observe(this, new Observer<List<Quote>>() {
            @Override
            public void onChanged(@NonNull List<Quote> quote) {
                if(quote.size()==0) return;;

                adapterQuotes.insertItems(quote);
                recyclerView.scrollToPosition(0);

                if(swipeRefreshLayout.isRefreshing()){
                    swipeRefreshLayout.setRefreshing(false);
                }
            }
        });
    }

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >

    <android.support.v4.widget.SwipeRefreshLayout 
        xmlns:android="http://schemas.android.com/apk/res/android" 
        android:id="@+id/swipe_to_refresh" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content">

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

        </android.support.v7.widget.RecyclerView>

    </android.support.v4.widget.SwipeRefreshLayout>

</LinearLayout>

Code here is pretty much self explanatory, still if you face any problem in understanding any part, just leave a comment and I will respond as soon as possible. Or you can reach out directly to me via mail.

As you can see, how easily we changed our app from displaying one quote to multiple quotes. Within minutes. Changes in data side and view side were totally independent and nothing to with each other.

Now imagine, tomorrow you are installing your own server and want to use your own REST API for fetching the quote. No matter how complex your app has become in future, how big it has grown or how sophisticated UI you have written for it, if you have kept structure intact, it will be a piece of cake for changing the data source. (Only change in QuoteRepository and QuoteAPI classes in our case!)

I would like to demonstrate one more functionality we will add right now for our app very quickly. We would like if our users can save/star any quote he/she likes with one touch and separate fragment to access that quotes. For that, let’s create one more table in our db for storing starred quotes. Procedure to do so, very easy!

Create another POJO for our saved quote and name it SavedQuote.java. It will be same as Quote.java, only difference being extra parameter for storing timestamp at which quote was starred. I will skip code for brevity. (Refer github for code)
In MyQuoteDB.java, add our new POJO class in @Database annotation in entities. Like this

@Database(entities = {Quote.class, SavedQuote.class}, version = version_number)

Add 3 new methods in QuoteDAO class for accessing data from our newly created table.

    @Dao
    public interface QuoteDAO {

        @Insert(onConflict = REPLACE)
        void cacheOnDB(List<Quote> quote);

        @Query("SELECT * FROM quote")
        LiveData<List<Quote>>getCachedQuotes();

        //for saved quotes (starred quotes)
        @Insert(onConflict = REPLACE)
        void saveOnDevice(SavedQuote quote);

        //no need of live data, we won't be making any changes to it
        @Query("SELECT * FROM savedquote")
        List<SavedQuote> getSavedQuotes();

        @Delete
        void deleteQuote(SavedQuote quote);
    }
    

And we are done with the data part here. How easy it was! I will leave the UI part as exercise for you. Just create one more recycler view item for showing starred quotes, fragment to display them (similar to what we are using for earlier quotes) and button on each quote item to star the quote. And that will be pretty much of it. Invoke the methods we written for saving the quote appropriately on button clicks. After little more UI tweaks, our application looks something like shown below. Decent enough!

Some of you might argue, why you have written all set off different classes for the functionality of starred quotes, we could have managed it in existing classes and stuff. But our goal here is not to create highly optimized, small size apk but maintainable, testable and robust apk. If we want to make changes to any functionality in future, each of those should be independent enough so as to reduce development work and regression testing time.

You can always fork the repository on github to play around the application and make changes if you want. Feel free to reach me in case of any doubt!

I will be writing more articles about tips and tricks for making most of your android application in future, subscribe our newsletter for getting the articles directly in your inbox. Subscription box can be found on top right side of this article of you are on PC and at the bottom of this article if you are on mobile device.

Adios!

 

8 comments
  1. fucking

    When someone writes an piece of writing he/she maintains the image of a user in his/her mind that how a user can be aware of
    it. So that’s why this piece of writing is perfect. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *