Our sun has about 109 times the diameter of Earth. That implies you can fit 1.3million Earths inside the sun. Yet, according to scientists, it is nothing but an average star when lined up against the really massive stars out there. Imagine this, VY Canis Majoris, a luminous red hypergiant star located in the constellation Canis Major, has about 2000 times the radii of our sun. That means you can fit 9.3 billion suns inside this massive star. Mind blowing isn’t?

Personally I like to marvel at the night sky every few days. I do this either at night or early at dawn. I find quite a meditative and spiritual experience. So I decided to create an app that would allow me catalogue some of the largest stars ever discovered. Not only would I then be able to keep track of these stars, but the project would also serve as a template for creating future apps. I would then be able to share this template with you guys so you can use it in creating your own full apps.

NB/= We had actually created three Largest Stars App earlier. One was with Firebase + Cloud Storage and MVVM. Then two were with Retrofit, using Java and Kotlin, but without MVVM. So today we are creating one with MVVM and Retrofit2. It has all the capabilities of the retrofit one. We create two apps: one with Kotlin, the other with Java.

Demo

Here is the demo:

Concepts You will Learn from this Project.

This project is a teaching project. This means it has been specially designed for teaching concepts to students. Alot of care has been taken with regards to quality and ease of use. Only features intended to be taught have been included. In short it has been designed to be used as a template for creating your projects.Here are some of the concepts and technologies you will learn from this student project:

(a). Kotlin and Java Programming Languages

You will learn both or either of this languages from this project. We’ve written the two projects to be similar in functionality while respecting the features and best practices of each of these languages. If you are person looking to get into learning Kotlin, comparing the Java project with the Kotlin one would speed up your learning.

(b). MVVM

As I said earlier, we had written several Largest Stars Apps earlier. However they are different from this. This is a complete re-write using Model View ViewModel. If you had acquired those projects, you can compare them with this one to see the clear advantages of using Clean Architecture in your app,

 

(c). Data Binding

We make use of data binding in both the Java and Kotlin project. Data binding simplifies the process of setting values to views, or obtaing values from views. It massively saves us from writing boilerplate code.

(d). Retrofit2

We use the latest version of Retrofit. Retrofit is a type safe HTTP client for android. It is extremely reliable and easy to use and very popular. You will see why this popularity is justified.

(e). PHP + MySQL

Our database will be mysql database. Our server side programming language will be PHP. Both are made for each other and work seamlessly, powering majority of the web.You can host mysql/php locally and use XAMPP/WAMP or any local server, or you can host them online and access them from anywhere as we doing with our demo project.

(f). RedbeanPHP

We will use RedbeanPHP as our ORM. Our aim is to avoid writing delicate and error code. Thus we need to avoid writing raw sql statements. We also prefer clean object oriented code like we are used to in android development, rather than those spaghetti PHP code scattered everywhere in the web. RedbeanPHP promotes these and you will see how clean our PHP code is.

(g). Full CRUD

You look at online samples and projects and you find mostly that they use JSON data fetched from APIs. Well that won’t help you very much in launching a real app. You need full CRUD capability and we certainly implement that in this project. You will be able to INSERT into MySQL, SELECT data, UPDATE data and DELETE data among other functionalities.

(h). Pagination

We will teach you how to reliably implement pagination. This will help make your app more efficient while ensuring you don’t consume too much bandwith. We will implement server side load more pagination against our recyclerview. As use scrolls, we load more data. If user reaches end, we tell him/her.

(i). Disk Caching

To even make the app more efficient, what about if we cache data to Disk? Yeah we do this, using a library implementing LRU(Least Recently Used) caching mechanism. You have complete control of the cache size. You can set it. Caching implies we will only load data from the server if the user requests a refresh or makes an update, or reaches the end of the cached list.

(j). Custom Fonts

Yeah. Get full control over the fonts you use in your application. We implement the ability to set application-wide fonts, or set them per widget.

(i). Multipart Uploads

We will use Retrofit2 to perform multipart uploads of images and text to the server. The images will be stored in the server, while their paths alongside other properties get stored in mysql database. We will also be able to download them, update as well as delete them.

(k). Beautiful UIs

We implement some amazing and re-usable UIs in this project. These include:

  • CarouselView
  • CollapsingToolBar
  • Materail Dialogs
  • Dashboard CardViews
  • Beautiful Numbered RecyclerView
  • Progress Cards
  • Material EditTexts
  • Material DatePicker

(l). ClearText Traffic

We show how to implement non-https traffic in this app. This implies we can communicate over in-secure http without being denied permission by the system.

(m). Capture from Camera, Gallery, FilePicker

We provide robust and enough options to obtain the image to be uploaded to the server. You can capture it directly from the camera, or select it from gallery as well as from file picker. Runtime permissions are already implemented for you.

(n). Material Activity Transitions

When transitioning from one activity to the other, we apply beautiful material design animations, gently sliding in or out an activity.

Let’s see some how-to concpets using code snippets from the project in both Kotlin and Java.

How To Design our Model Classes

The model classes are extremely important. They define the basic entities which our apps are about. We have three of them.

(a). Star

This class will model our Star object. It will define its properties. It is the Star objects which will be stored in our mysql database.

Here is the Star in Kotlin Code:

class Star : Serializable {
    /**
     * Let' now come define instance fields for this class. We decorate them with
     * @SerializedName
     * attribute. Through this we are specifying the keys in our json data.
     */
    @SerializedName("id")
    var id: String? = ""
    @SerializedName("name")
    var name: String? = ""
    @SerializedName("description")
    var description: String? = ""
    @SerializedName("type")
    var type: String? = ""
    @SerializedName("galaxy")
    var galaxy: String? = ""
    @SerializedName("dod")
    var dod: String? = ""
    @SerializedName("image_url")
    var imageURL: String? = ""

    override fun toString(): String {
        return name!!
    }
}

And in Java:

public class Star implements Serializable {
    /**
     * Let' now come define instance fields for this class. We decorate them with
     * @SerializedName
     * attribute. Through this we are specifying the keys in our json data.
     */
    @SerializedName("id")
    private String id;
    @SerializedName("name")
    private String name;
    @SerializedName("description")
    private String description;
     @SerializedName("type")
     private String type;
    @SerializedName("galaxy")
    private String galaxy;
    @SerializedName("dod")
    private String dod;
    @SerializedName("image_url")
    private String imageURL;

    //....Getters and Setters here

Implementing Serializable interface will allow us to serialize a Star object to disk then pass it across to another activity. Thus we don’t have to pass individual properties, but rather a whole star.

Take note of the attributes decorated onto our fields. Their contents have to match our JSON data. For example:

 @SerializedName("name")

In the above, we must have a json key name.

Now take note of the properties in the Kotlin project. We have used the elvis operator to assign default values.For examples:

    @SerializedName("name")
    var name: String? = ""

Name, just like the other fields in the above are nullable. Meaning they can be assigned null values. This is important because our data is coming from elsewhere. We cannot guarantee that it is impossible for the server to return us null values. However with the elvis operator, we are saying that if the server passes us null values, we will simply use the default values. This increases safety and helps tackle the null pointer exceptions problem.

(b). ResponseModel

We need a class that can be mapped to our server’s response. We can take advantage of Gson, which will be shipped in our retrofit converter factory. Gson can map valid json data to properly designed java/kotlin class. We will create such a class and call it ResponseModel.

Here is our ResponseModel class in Kotlin:

class ResponseModel {
    /**
     * Our ResponseModel class.
     * ROLES:
     * 1. To be mapped onto our server's response.
     */
    @SerializedName("stars")
    var stars: ArrayList<Star>? = ArrayList()
    @SerializedName("code")
    var code: String? = "-1"
    @SerializedName("message")
    var message: String? = "UNKNOWN MESSAGE"
}
public class ResponseModel {
    @SerializedName("stars")
    private List<Star> stars;
    @SerializedName("code")
    private String code;
    @SerializedName("message")
    private String message;

    //..Getters and setters here

The getters and setters are ommited from the Java code. The values passed in the @SerializedName attribute have to match the json keys for the mapping to be successful. Otherwise you’ll get malformed json errors.

(c). RequestCall class

We need a class to represent our request against the server. By having such a class, we will be able to programmatically represent a request we perform. The class will include the following properties:

  1. Status – Status of the request e.g In Progress, Failed, Succedded
  2. Message – Message obtained from the server.
  3. List – List of stars downloaded from the server, if any.

Here is such a class in Kotlin:

/**
 * Our RequestCall class
 * This class will represent a single request we make against our server
 */
class RequestCall {
    var status = 0
    var message: String? = "UNKNOWN MESSAGE"
    var stars: ArrayList<Star>? = ArrayList()
    var responseModel: ResponseModel? = ResponseModel()
}

And in Java:

public class RequestCall {
    private int status;
    private String message;
    private List<Star> stars;
    private ResponseModel responseModel;

    //...Getters and setters here

Representing Our RestAPI

We will be communicating with our server. We do this by making HTTP requests to the server and which then responds to those requests by exposing JSON data which we download. So far we have prepared a ResponseModel class, which was the class to represent such responses. Now let’s come and define an interface to represent the requests we will be making. These operations are represented using abstract methods that are decorated with special HTTP verbs.

(a). Method to Retrieve all Data via HTTP GET

This method will make a HTTP GET request and download all data without pagination.In Kotlin:

    /**
     * This method will allow us perform a HTTP GET request to the specified url
     * .The response will be a ResponseModel object.
     */
    @GET("index.php")
    fun retrieve(): Call<ResponseModel?>?

And in Java:

    @GET("index.php")
    Call<ResponseModel> retrieve();

(b). Method to Search while Paginating Data

This method will allow us search data from server while paginating. In Kotlin:

    /**
     * This method will allow us to search our data while paginating the search results. We
     * specify the search and pagination parameters as fields.
     */
    @FormUrlEncoded
    @POST("index.php")
    fun search(
        @Field("action") action: String?,
        @Field("query") query: String?,
        @Field("start") start: String?,
        @Field("limit") limit: String?
    ): Call<ResponseModel?>?

And in Java:

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> search(@Field("action") String action,
                               @Field("query") String query,
                               @Field("start") String start,
                               @Field("limit") String limit);

Note that we will use the above method most of the time even if we are not necessarily searching. For example we just send an empty query and data won’t be filtered but only paginated.

(c). Method to Upload Data to server

This method will allow us to upload both images and text to the server. We are making a multipart request. In Kotlin:

    /**
     * This method will allow us perform a HTTP POST request to the specified url.In the process
     * we will upload data to mysql and return a ResponseModel object
     *
     * Multipart Denotes that the request body is multi-part. Parts should be declared as parameters
     * and annotated with @Part.
     */
    @Multipart
    @POST("index.php")
    fun upload(
        @Part("action") action: RequestBody?,
        @Part("name") name: RequestBody?,
        @Part("description") description: RequestBody?,
        @Part("type") type: RequestBody?,
        @Part("galaxy") galaxy: RequestBody?,
        @Part("dod") dod: RequestBody?,
        @Part starImage: MultipartBody.Part?
    ): Call<ResponseModel?>?

While in Java:

    @Multipart
    @POST("index.php")
    Call<ResponseModel> upload(
            @Part("action") RequestBody action,
            @Part("name") RequestBody name,
            @Part("description") RequestBody description,
            @Part("type") RequestBody type,
            @Part("galaxy") RequestBody galaxy,
            @Part("dod") RequestBody dod,
            @Part MultipartBody.Part starImage);

(c). Method to Update Both Images and Text

This method will allow us to upload a new image to replace existing one while also updating texts in the database. We also make a multipart request.In Kotlin:

    /**
     * This method will allow us update our mysql data by making a HTTP POST request.
     * After that
     * we will receive a ResponseModel model object
     */
    @Multipart
    @POST("index.php")
    fun update(
        @Part("action") action: RequestBody?,
        @Part("id") id: RequestBody?,
        @Part("name") name: RequestBody?,
        @Part("description") description: RequestBody?,
        @Part("type") type: RequestBody?,
        @Part("galaxy") galaxy: RequestBody?,
        @Part("dod") dod: RequestBody?,
        @Part starImage: MultipartBody.Part?
    ): Call<ResponseModel?>?

While in Java:

    @Multipart
    @POST("index.php")
    Call<ResponseModel> update(
            @Part("action") RequestBody action,
            @Part("id") RequestBody id,
            @Part("name") RequestBody name,
            @Part("description") RequestBody description,
            @Part("type") RequestBody type,
            @Part("galaxy") RequestBody galaxy,
            @Part("dod") RequestBody dod,
            @Part MultipartBody.Part starImage);

(d). Method to Update Only Text

This method will allow us to update only Text. Because of that, we no longer need to make a multipart request. In Kotlin:

    @FormUrlEncoded
    @POST("index.php")
    fun updateOnlyText(
        @Field("action") action: String?,
        @Field("id") id: String?,
        @Field("name") name: String?,
        @Field("description") description: String?,
        @Field("type") type: String?,
        @Field("galaxy") galaxy: String?,
        @Field("dod") dod: String?
    ): Call<ResponseModel?>?

In Java:

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> updateOnlyText(@Field("action") String action,
                                       @Field("id") String id,
                                       @Field("name") String name,
                                       @Field("description") String description,
                                       @Field("type") String type,
                                       @Field("galaxy") String galaxy,
                                       @Field("dod") String dod);

(e). Method to Delete From Server

This method will allow us to delete data from our server. Both images and text stored in the database will be deleted.In Kotlin:

    /**
     * This method will allow us to remove or delete from database the row with the
     * specified
     * id.
     */
    @FormUrlEncoded
    @POST("index.php")
    fun delete(@Field("action") action: String?, @Field("id") id: String?): Call<ResponseModel?>?

In Java:

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> delete(@Field("action") String action, @Field("id") String id);

Performing Requests and Handling Responses

We’ve already defined methods to represent the HTTP requests we will be making to our server. One thing you may have noticed in the above methods is that they all return Call<ResponseModel>, a retrofit2.Call object, with the generic parameter being a ResponseModel. Those calls will need to be enqueued for them to be actually executed. That execution will happen asynchronously and will lead to either a response or a failure. Failure implies we are unable to make a connection or an error is thrown . Response means we are able to establish connection and communicate. Let’s therefore create two re-usable methods that will allow us to handle both scenarios.

The first method is to allow us handle a response. Note that the response can actually be null. Or the response body can be null. Or the response code can be null. Or the response message can be null. So our aim is to take all these into consideration and handle them appropriately so that our app don’t crash because of some invalid data from the server. We need to handle them because we want to work with them, or display them even though they are coming from elsewhere. In Kotlin:

    private fun handleResponse(response: Response<ResponseModel?>?, r: RequestCall): RequestCall {
        if (response == null) {
            r.status = Constants.FAILED
            r.message = "Response is Null"
        } else if (response.body() == null) {
            r.status = Constants.FAILED
            r.message = "Response Body is Null"
        } else {
            val rm = response.body()
            if (rm!!.code == null) {
                r.status = Constants.FAILED
                r.message = "NULL RESPONSE CODE"
            } else if (rm.message == null) {
                r.status = Constants.FAILED
                r.message = "NULL RESPONSE MESSAGE"
            } else {
                if (rm.code.equals("1", ignoreCase = true)) {
                    r.status =
                        Constants.SUCCEEDED
                    if (rm.stars != null) {
                        r.stars = rm.stars
                    }
                } else {
                    r.status = Constants.FAILED
                }
                r.message = rm.message
            }
            r.responseModel = rm
        }
        return r
    }

In Java:

    private RequestCall handleResponse(Response<ResponseModel> response, RequestCall r) {
        if (response == null) {
            r.setStatus(Constants.FAILED);
            r.setMessage("Response is Null");
        } else if (response.body() == null) {
            r.setStatus(Constants.FAILED);
            r.setMessage("Response Body is Null");
        } else {
            ResponseModel rm = response.body();

            if (rm.getCode() == null) {
                r.setStatus(Constants.FAILED);
                r.setMessage("NULL RESPONSE CODE");
            } else if (rm.getMessage() == null) {
                r.setStatus(Constants.FAILED);
                r.setMessage("NULL RESPONSE MESSAGE");
            } else {

                if (rm.getCode().equalsIgnoreCase("1")) {
                    r.setStatus(Constants.SUCCEEDED);

                    if (rm.getStars() != null) {
                        r.setStars(rm.getStars());
                    }

                } else {
                    r.setStatus(Constants.FAILED);
                }
                r.setMessage(rm.getMessage());

            }
            r.setResponseModel(rm);

        }
        return r;
    }

We’ve specifically specified that our server will return a response code of 1 in case of success and 2 in case of failure.

Well we also need a re-usable method to handle failure. In Kotlin:

    private fun handleFailure(message: String?, requestCall: RequestCall): RequestCall {
        Log.d("RETROFIT", "ERROR: $message")
        requestCall.status = Constants.FAILED
        requestCall.message = message
        return requestCall
    }

In Java:

    private RequestCall handleFailure(String message, RequestCall requestCall) {
        Log.d("RETROFIT", "ERROR: " + message);
        requestCall.setStatus(Constants.FAILED);
        requestCall.setMessage(message);
        return requestCall;
    }

So how do we use the above methods? Well the essence of creating them is for safety in handling our response and failure and also for re-usability. We can now use them in as many operations we are performing as possible. Without them we would need to handle responses and failures for every request we perform.

For example let’s see below how to apply them in th select() function, which will download both images and text from our server without pagination:

    fun select(): MutableLiveData<RequestCall> {
        val mLiveData: MutableLiveData<RequestCall> = MutableLiveData()
        val api = Utils.client!!.create(RestApi::class.java)
        val retrievedData: Call<ResponseModel?>?
        retrievedData = api.retrieve()
        val r = RequestCall()
        r.status = Constants.IN_PROGRESS
        r.message = "Downloading Data.. Please Wait.."
        mLiveData.value = r
        retrievedData!!.enqueue(object : Callback<ResponseModel?> {
            override fun onResponse(call: Call<ResponseModel?>, response: Response<ResponseModel?>) {
                mLiveData.postValue(handleResponse(response, r))
            }

            override fun onFailure(call: Call<ResponseModel?>, t: Throwable) {
                handleFailure(t.message, r)
            }
        })
        return mLiveData
    }

And in Java:

    public MutableLiveData<RequestCall> select() {
        MutableLiveData mLiveData = new MutableLiveData();
        RestApi api = Utils.getClient().create(RestApi.class);
        Call<ResponseModel> retrievedData;
        retrievedData = api.retrieve();

        RequestCall r = new RequestCall();
        r.setStatus(Constants.IN_PROGRESS);
        r.setMessage("Downloading Data.. Please Wait..");

        mLiveData.setValue(r);

        retrievedData.enqueue(new Callback<ResponseModel>() {
            @Override
            public void onResponse(Call<ResponseModel> call, Response<ResponseModel> response) {
                mLiveData.postValue(handleResponse(response, r));
            }

            @Override
            public void onFailure(Call<ResponseModel> call, Throwable t) {
                handleFailure(t.getMessage(), r);
            }
        });
        return mLiveData;
    }

You can see that the method is very clean and concise and doesn’t boilerplate code to check for nulls etc. Now the magic is that as we said earlier, we will re-use those method for every operation we perform like:

  • Uploading Images and Text
  • Updating Images and Text
  • Updating Only Text
  • Retrieving all Text and Images
  • Retrieving Paginated Text and Images
  • Deleting Images and Text

 

Connecting Data and Logic to UI

We will have a ViewModel class whose objective will mainly be to connect our user interface with our business logic. We want to do this in a lifecycle-concious way and that’s why we extend the standard androidx.lifecycle.AndroidViewModel class. Here is our ViewModel class in Kotlin:

class RemoteViewModel(application: Application) : AndroidViewModel(application) {
    private val mRepository: GeneralRepository = GeneralRepository()
    private val error: MutableLiveData<RequestCall>
        get() {
            val mLiveData: MutableLiveData<RequestCall> = MutableLiveData()
            val r = RequestCall()
            r.status = Constants.FAILED
            r.message = "VALIDATION FAILED: Your Data Object is null. You can't post null."
            return mLiveData
        }

    fun upload(imageFile: File?, star: Star?): MutableLiveData<RequestCall> {
        if (star == null) return error
        return if (imageFile == null) error else mRepository.performRequest(
            imageFile,
            star,
            Constants.UPLOAD
        )
    }

    fun updateImageText(imageFile: File?, star: Star?): MutableLiveData<RequestCall> {
        if (star == null) return error
        return if (imageFile == null) error else mRepository.performRequest(
            imageFile,
            star,
            Constants.UPDATE_IMAGE_TEXT
        )
    }

    fun updateOnlyText(star: Star?): MutableLiveData<RequestCall> {
        return if (star == null) error else mRepository.performRequest(
            null,
            star,
            Constants.UPDATE_ONLY_TEXT
        )
    }

    fun delete(star: Star?): MutableLiveData<RequestCall> {
        return if (star == null) error else mRepository.performRequest(
            null,
            star,
            Constants.DELETE
        )
    }

    fun select(): MutableLiveData<RequestCall> {
        return mRepository.select()
    }

    fun search(query: String?, start: String?, limit: String?): MutableLiveData<RequestCall> {
        return mRepository.search(query, start, limit)
    }

}

And in Java:

public class RemoteViewModel extends AndroidViewModel {
    private final GeneralRepository mRepository;

    public RemoteViewModel(@NonNull Application application) {
        super(application);
        mRepository = new GeneralRepository();
    }

    private MutableLiveData<RequestCall> getError(){
        MutableLiveData mLiveData=new MutableLiveData();
        RequestCall r=new RequestCall();
        r.setStatus(Constants.FAILED);
        r.setMessage("VALIDATION FAILED: Your Data Object is null. You can't post null.");
        return mLiveData;
    }
    public MutableLiveData<RequestCall> upload(File imageFile, Star star){
        if (star == null) return getError();
        if (imageFile == null) return getError();
        return mRepository.performRequest(imageFile, star,  Constants.UPLOAD);
    }
    public MutableLiveData<RequestCall> updateImageText(File imageFile, Star star){
        if (star == null) return getError();
        if (imageFile == null) return getError();
        return mRepository.performRequest(imageFile,star,  Constants.UPDATE_IMAGE_TEXT);
    }
    public MutableLiveData<RequestCall> updateOnlyText(Star star){
        if (star == null) return getError();
        return mRepository.performRequest(null, star,Constants.UPDATE_ONLY_TEXT);
    }
    public MutableLiveData<RequestCall> delete(Star star){
        if (star == null) return getError();
        return mRepository.performRequest(null, star, Constants.DELETE);
    }
    public MutableLiveData<RequestCall> select() {
        return mRepository.select();
    }

    public MutableLiveData<RequestCall> search(String query, String start, String limit) {
        return mRepository.search(query,start,limit);
    }
}

You can see that all our ViewModel methods are returning MutableLiveData objects. These will be subscribed to and observed for data changes.

Subscribing To our ViewModel Methods

Well we are using Model View ViewModel as our design pattern. That makes our ViewModel class one of the most important even though it is the simplest. The major aims of MVVM, like any other design pattern is to:

  1. Encourage writing of easy to maintain code.
  2. Make code solid and less-error prone.
  3. Promote re-usability and avoid duplication of code.

So far we’ve looked at our Data/Logic as well as the ViewModel class. Let’s now come to the final part, which is our User Interface. It is here where we will subscribe to the ViewModel methods and listen to changes, thus updating our UI.

For example, here is how we subscribe to the upload process in Kotlin:

    private fun upload(p: Star?) {
        remoteViewModel.upload(chosenFile, p)
            .observe(this, Observer { r: RequestCall? ->
                if (makeRequest(r, "STAR UPLOAD") == SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = true
                    clearEditTexts(
                        b!!.nameTxt,
                        b!!.descriptionTxt,
                        b!!.typeTxt,
                        b!!.galaxyTxt,
                        b!!.dodTxt
                    )
                }
            })
    }

In Java:

    private void upload(Star p) {
        getRemoteViewModel().upload(chosenFile, p).observe(this, r -> {
            if (makeRequest(r, "STAR UPLOAD") == SUCCEEDED) {
                CacheManager.CACHE_IS_DIRTY = true;
                clearEditTexts(b.nameTxt, b.descriptionTxt, b.typeTxt, b.galaxyTxt, b.dodTxt);
            }
        });
    }

You can see we invoke the upload function we had defined in the Viewmodel class and pass the subscriber which is the current activity. We then make our request and listen to changes. If our request succeeds, we mark our cache as dirty. Doing this will allow us to refresh our cache the momment the user navigates to the listings page. We also clear the current edittexts so that the user can go ahead and upload a new star.

Well the update process is almost similar. In Kotlin:

    private fun update(p: Star?) {
        remoteViewModel.updateImageText(chosenFile, p)
            .observe(this, Observer { r: RequestCall? ->
                if (makeRequest(r, "STAR UPDATE") == SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = true
                    finish()
                }
            })
    }

In Java:

    private void update(Star p) {
        getRemoteViewModel().updateImageText(chosenFile, p).observe(this, r -> {
            if (makeRequest(r, "STAR UPDATE") == SUCCEEDED) {
                CacheManager.CACHE_IS_DIRTY = true;
                finish();
            }
        });
    }

However this time around we finish the current activity. This will take us back to the listings page where our data will be immediately refreshed.

What about Delete? Well it is just as easy. In Kotlin:

    private fun delete(p: Star) {
        remoteViewModel.delete(p)
            .observe(this, Observer { r: RequestCall? ->
                if (makeRequest(r, "STAR DELETE") == SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = true
                    finish()
                }
            })
    }

And in Java:

    private void delete(Star p) {
        getRemoteViewModel().delete(p).observe(this, r -> {
            if (makeRequest(r, "PLAYER DELETE") == SUCCEEDED) {
                CacheManager.CACHE_IS_DIRTY = true;
                finish();
            }
        });
    }

Validating Data Before Posting

In our upload page, we want to make sure that the required fields have all been typed before we attempt making an upload or update.In Kotlin:

    private fun validateThenPOST(isForUpdate: Boolean) {
        if (validate(chosenFile, false, b!!.nameTxt, b!!.descriptionTxt, b!!.galaxyTxt)) {
            val p: Star?
            p = if (isForUpdate) {
                receivedStar
            } else {
                Star()
            }
            p!!.name = valOf(b!!.nameTxt)
            p.description = valOf(b!!.descriptionTxt)
            p.type = valOf(b!!.typeTxt)
            p.galaxy = valOf(b!!.galaxyTxt)
            p.dod = valOf(b!!.dodTxt)
            if (isForUpdate) {
                update(p)
            } else {
                upload(p)
            }
        } else {
            show("Please fill up all Fields First")
        }
    }

And in Java:

    private void validateThenPOST(boolean isForUpdate) {
        if (validate(chosenFile, false, b.nameTxt, b.descriptionTxt, b.galaxyTxt)) {
            Star p;
            if (isForUpdate) {
                p = receivedStar;
            } else {
                p = new Star();
            }
            p.setName(valOf(b.nameTxt));
            p.setDescription(valOf(b.descriptionTxt));
            p.setType(valOf(b.typeTxt));
            p.setGalaxy(valOf(b.galaxyTxt));
            p.setDod(valOf(b.dodTxt));

            if (isForUpdate) {
                update(p);
            } else {
                upload(p);
            }

        } else {
            show("Please fill up all Fields First");
        }
    }

We can also warn user if he clicks the back button since he/she may not have intended to go back after typing some data in the edittexts:

    override fun onBackPressed() {
        super.onBackPressed()
        showInfoDialog(this, "GOING BACK", "Are you sure you want to exit this page")
    }

We’ve done it by overriding the onBackPressed() method of the Upload Page and showing an info dialog with buttons.

Setting Data To Fields when activity is resumed via Data Binding

We are making use of data binding in both the Kotlin project as well as the Java Project, as we had said earlier. One of the areas where this saves us from lots of boilerplate code is if we want to set data to our edittexts. For example if you come to the upload page with the interntion of editing a given star, the star’s properties should be set to their appropriate widgets. We can use data binding to achieve this. We will do this inside the onResume() function. Here is the process:

First override the onResume():

    override fun onResume() {
        super.onResume()

Now receive the Star. The Star object will be passed to this editing page from our DetailsActivityy, we need to catch it so that we can edit it:

        val o = receiveStar(intent, this)

Then:

        if (o != null) {
            receivedStar = o
            if (!resumedAfterImagePicker) {
                b!!.star = receivedStar
            }
            if (chosenFile != null) {
                Picasso.get().load(chosenFile!!).error(R.drawable.image_not_found)
                    .into(b!!.topImageView)
            } else {
                loadImageFromNetwork(
                    Constants.IMAGES_BASE_URL + receivedStar!!.imageURL,
                    R.drawable.image_not_found, b!!.topImageView
                )
            }
        } else {
            if (!resumedAfterImagePicker) {
                b!!.star = Star()
            }
        }

Basically we do different things based on whether the star is null or not.It is possible for the star to be null if we open this Upload Page for with intention of uploading a new star. In that case the receive() method will return null. You have to be aware that we are re-using the same CRUD page for uploading,updating and deleting. Also be aware that this onResume() method will also be called after we have captured an image from the camera or picked one from the gallery or filepicker. In that case we want to ignore binding data to widgets, apart from the picked image. The following lines are what do the data binding magic for us, setting our data to our widgets implicitly:

            if (!resumedAfterImagePicker) {
                b!!.star = receivedStar
            }

And for empty fields:

            if (!resumedAfterImagePicker) {
                b!!.star = Star()
            }

Ultimately, you will have to set the content via data binding using DataBindingUtil for the above to be possible. You will do this in the onCreate() method:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        b = DataBindingUtil.setContentView(this, R.layout.activity_upload)
        b!!.callback = this

How to capture Image from Camera, or select from gallery or FilePicker

Here is how to capture the image from camera using our ImagePicker library.Optionally you can select the image from the gallery and file picker as well. This library is one of the easiest and most reliable libraries for this task.

First go to your app level build.gradle and specify the minimum api as 19:

        minSdkVersion 19

Then install the ImagePicker library:

    implementation 'com.github.maayyaannkk:ImagePicker:1.0.4'

Good. Now let’s define the method to capture or select the image. We will define it in our UploadActivity. In Kotlin:

    override fun captureImage() {
        val intent = Intent(this, ImageSelectActivity::class.java)
        intent.putExtra(ImageSelectActivity.FLAG_COMPRESS, false) //default is true
        intent.putExtra(ImageSelectActivity.FLAG_CAMERA, true) //default is true
        intent.putExtra(ImageSelectActivity.FLAG_GALLERY, true) //default is true
        startActivityForResult(intent, 1213)
    }

While in Java:

    protected void captureImage() {
        Intent intent = new Intent(this, ImageSelectActivity.class);
        intent.putExtra(ImageSelectActivity.FLAG_COMPRESS, false);//default is true
        intent.putExtra(ImageSelectActivity.FLAG_CAMERA, true);//default is true
        intent.putExtra(ImageSelectActivity.FLAG_GALLERY, true);//default is true
        startActivityForResult(intent, 1213);
    }

Now all you need is just to handle the capture or selected image. For example let’s say I want to hold it in a file so that we will be able to upload it to the server later on, while in the meantime I show it in an imageview using Picasso. In Kotlin:

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && data != null) {
            if (requestCode == 1213) {
                val filePath =
                    data.getStringExtra(ImageSelectActivity.RESULT_FILE_PATH)
                if (filePath != null) {
                    chosenFile = File(filePath)
                    Picasso.get().load(chosenFile!!).error(R.drawable.image_not_found)
                        .into(b!!.topImageView)
                }
            }
        }
        resumedAfterImagePicker = true
    }

In Java:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == Activity.RESULT_OK && data != null) {
            if (requestCode == 1213) {
                String filePath = data.getStringExtra(ImageSelectActivity.RESULT_FILE_PATH);
                if (filePath != null) {
                    chosenFile = new File(filePath);
                    Picasso.get().load(chosenFile).error(R.drawable.image_not_found).into(b.topImageView);
                }
            }
        }
        resumedAfterImagePicker = true;

    }

And that’s it. We’ve capture image or selected one from gallery or filepicker.

How to Show Info and Single Choice Dialogs using LoveLyDialogs

LovelyDialogs is a material library I use extensively in my projects. It’s reliable and easy to use. The dialogs are faily customizable to my liking.

The first step is to install the library from jcenter. You do this by going to your app-level build.gradle and adding it as a dependency:

    //Material dialogs
    implementation 'com.yarolegovich:lovely-dialog:1.1.0'

Let’s say we want to create utility methods we can re-use in several activities.To create an info dialog in Kotlin:

    @JvmStatic
    fun showInfoDialog(activity: AppCompatActivity, title: String?, message: String?) {
        LovelyStandardDialog(activity, LovelyStandardDialog.ButtonLayout.HORIZONTAL)
            .setTopColorRes(R.color.indigo)
            .setButtonsColorRes(R.color.darkDeepOrange)
            .setIcon(R.drawable.info_3d)
            .setTitle(title)
            .setMessage(message)
            .setPositiveButton("Relax") { }
            .setNeutralButton("Go Home") {
                openActivity(activity, DashboardActivity::class.java)
            }
            .setNegativeButton("Go Back") { activity.finish() }
            .show()
    }

And in Java:

    public static void showInfoDialog(final AppCompatActivity activity, String title,
                                      String message) {
        new LovelyStandardDialog(activity, LovelyStandardDialog.ButtonLayout.HORIZONTAL)
                .setTopColorRes(R.color.indigo)
                .setButtonsColorRes(R.color.darkDeepOrange)
                .setIcon(R.drawable.info_3d)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("Relax", v -> {})
                .setNeutralButton("Go Home", v -> openActivity(activity, DashboardActivity.class))
                .setNegativeButton("Go Back", v -> activity.finish())
                .show();
    }

You can see that we are able to set the buttons layout,set top color resource, set buttons color, set icon, set title, set message and of course the three buttons as well as handling the click events of the buttons.

 

We can also create a single choice dialog. This is a type of dialog that allows users to select an item. In this case we want that when the user clicks a given item, we set the clicked item in the edittext. In Kotlin:

    @JvmStatic
    fun selectGalaxy(activity: AppCompatActivity?, galaxyTxt: EditText) {
        val adapter: ArrayAdapter<String> = ArrayAdapter<String>(
            activity!!.applicationContext,
            android.R.layout.simple_list_item_1,
            GALAXIES
        )
        LovelyChoiceDialog(activity)
            .setTopColorRes(R.color.colorPrimary)
            .setTitle("Galaxies Picker")
            .setTitleGravity(Gravity.CENTER_HORIZONTAL)
            .setIcon(R.drawable.info_3d)
            .setMessage("Select the Galaxy of this Star.")
            .setMessageGravity(Gravity.CENTER_HORIZONTAL)
            .setItems(
                adapter
            ) { position, item -> galaxyTxt.setText(item) }
            .show()
    }

And in Java:

    public static void selectGalaxy(AppCompatActivity activity, final EditText galaxyTxt){
        ArrayAdapter<String> adapter = new ArrayAdapter<>(activity,
                android.R.layout.simple_list_item_1,
                GALAXIES);
        new LovelyChoiceDialog(activity)
                .setTopColorRes(R.color.colorPrimary)
                .setTitle("Galaxies Picker")
                .setTitleGravity(Gravity.CENTER_HORIZONTAL)
                .setIcon(R.drawable.info_3d)
                .setMessage("Select the Galaxy of this Star.")
                .setMessageGravity(Gravity.CENTER_HORIZONTAL)
                .setItems(adapter, (position, item) -> galaxyTxt.setText(item))
                .show();

    }

How to Show and Select Date from MaterialDatePicker

MaterailDatePicker is a third party datepicker library we’ve chosen to use in this project. It’s simple,beautiful and reliable. We will use it to pick dates.

To install it go to the app level build.gradle and add the librart:

    implementation('com.shagi:material-datepicker:1.3') {
        exclude group: 'com.android.support'
    }

We are fetching it from jitpack so make sure you have added jitpack as a repository in the project-level build.gradle:

allprojects {
    repositories {
        google()
        jcenter()
        //register jitpack
        maven { url "https://jitpack.io" }
    }
}

Then, let’s say we to set the selected date to an edittext:

    @JvmStatic
    fun selectDate(activity: AppCompatActivity, dateTxt: EditText) {
        dateTxt.setOnClickListener {
            val dialog =
                DatePickerFragmentDialog.newInstance { view, year, monthOfYear, dayOfMonth ->
                    var monthOfYear = monthOfYear
                    monthOfYear = monthOfYear + 1
                    val month: String
                    val day: String
                    month = if (monthOfYear < 10) {
                        "0$monthOfYear"
                    } else {
                        monthOfYear.toString()
                    }
                    day = if (dayOfMonth < 10) {
                        "0$dayOfMonth"
                    } else {
                        dayOfMonth.toString()
                    }
                    dateTxt.setText("$year-$month-$day")
                }
            dialog.show(activity.supportFragmentManager, "DATE_PICKER")
        }
    }

And in Java:

    public static void selectDate(AppCompatActivity activity, EditText dateTxt){
        dateTxt.setOnClickListener(v -> {
            DatePickerFragmentDialog dialog = DatePickerFragmentDialog.newInstance((view, year, monthOfYear, dayOfMonth) -> {
                monthOfYear = monthOfYear + 1;
                String month,day;
                if(monthOfYear < 10){
                    month = "0"+ monthOfYear;
                }else{
                    month = String.valueOf(monthOfYear);
                }
                if(dayOfMonth < 10){
                    day = "0"+dayOfMonth;
                }else{
                    day = String.valueOf(dayOfMonth);
                }
                dateTxt.setText(year+"-"+month+"-"+day);
            });
            dialog.show(activity.getSupportFragmentManager(),"DATE_PICKER");
        });
    }

NB/= We are adjusting the date month by incrementing it by 1. This is because counting satrts at 0. We want January to be 1 rarher than 0 and December 12 rather than 11.

Caching Server Data to Hard Disk

As we said we will be caching our server data to hard disk. This data will be downloaded via Retrofit. Disk Caching is important because it reduces the number of requests we make to the server thus:

  1. Saving users’ bandwith
  2. Making the app faster by reducing memory usage involved in making requests to the server.

We will be using Reservoir, one of the best libraries available for Disk caching. It’s an old library but still works perfectly. It’s also one of the easiest and convenient disk caching libraries in that it allows us to dump a list to our cache instead of caching our objects individually. You can also use it to cache different lists, you simply differentiate them using a key.

To cache data first we need to install this library by adding the following dependency in our build.gradle:

    implementation 'com.anupcowkur:reservoir:3.1.0'

Then initialize the library, preferably in the App class:

        //Initialize disk caching
        initializeCache(this)

We need to define the above method:

    fun initializeCache(c: Context?) {
        DISK_CACHE_INITIALIZED = try {
            Reservoir.init(c, 1000048) //in bytes
            true
        } catch (e: IOException) {
            // BUG - Don't invoke IOException methods like e.getMessage()
            Log.e("CAMPOSHA", "INITIALIZATION OF RESERVOIR FAILED")
            false
        }
    }

Then here is how you actually cache an arraylist of our objects to disk. The arraylist is our retrofit response.

    @JvmStatic
    fun cacheToDisk(stars: List<Star>) {
        if (DISK_CACHE_INITIALIZED) {
            Reservoir.putAsync(Constants.CACHE_KEY, stars, object : ReservoirPutCallback {
                    override fun onSuccess() { //success
                        CACHE_IS_DIRTY = false
                    }

                    override fun onFailure(e: Exception) { // BUG - Don't invoke Exception methods like e.getMessage()
                        Log.e("CAMPOSHA", "PUTTING CACHE TO DISK FAILED")
                    }
                })
        }
    }

Then to retrieve data from our cache, here is how we do it reactively using LiveData:

    @JvmStatic
    fun loadFromDiskCache(): MutableLiveData<ArrayList<Star>> {
        val mLiveData =
            MutableLiveData<ArrayList<Star>>()
        if (!DISK_CACHE_INITIALIZED) {
            mLiveData.value = ArrayList()
            return mLiveData
        }
        val resultType = object : TypeToken<ArrayList<Star>>() {}.type
        Reservoir.getAsync(Constants.CACHE_KEY, resultType, object : ReservoirGetCallback<ArrayList<Star>> {
                override fun onSuccess(stars: ArrayList<Star>) {
                    mLiveData.value = stars
                }

                override fun onFailure(e: Exception) { // BUG - Don't invoke Exception methods like e.getMessage()
                    Log.e("CAMPOSHA", "ASYNC REFRESH FROM DISK FAILED")
                    mLiveData.value = ArrayList()
                }
            })
        return mLiveData
    }

That’s it. We’ve defined methods to cache our retrofit data to disk, then retrieve that data. This caching library implements LRU caching algorithm. Data is purged based on the least recently used mechanism whenever the size is exceeded.

Conclusion

So that’s it. This is a learning project and a template. You’ve seen snippets of code. The Kotlin and Java projects are both included in the download. We provided lifetime support for those who purchase this project and we will be providing continous updates. Cheers.

Download APK

Test this app by downloading the APK provided below. Just install and run. No setup is required in your part, we already have a hosted demo database.