One of the most important aspects of app development is User Management. Being able to register users, authenticate users, update user details, block or delete user accounts. This app is a template you can use to create an app that has to include these complex functionalities. It’s a fully working project written in Kotlin and PHP. Our user details will be stored in the server in MySQL database, Our server side language is PHP.

Let’s explore some of the features developed, technologies used and functionalities offered by this project:

Technologies

Let’s start by looking at the technologies we use:

1. Programming Language

Our Programming Language is Kotlin. Whether you are an advanced, intermediate or beginner developer, the code written in this app is readable, high quality and maintenable. You can use this project to enhance your Kotlin skills.

To use Kotlin to develop android project, you need to start by adding the following in your dependencies closure, inside the buildscript closure, inside the project-level build.gradle:

        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

Then add dependency for the Kotlin standard library in your app level build.gradle:

    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

Then apply Kotlin-Android specific extensions in your build.gradle:

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

2. Design Pattern

We use Model View ViewModel(MVVM) to craft this app. This ensure we structure our code in a way that our business logic is decoupled from the User Interface. This makes the code testsable, readable and easy to maintain. We use some of the APIs included in the lifecycle extensions package like the ViewModel and LiveData to help with this.

    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

3. PHP and MySQL

PHP is the most popular server side programming language. We will use PHP to write our server code. This code will interact with our MySQL database, performing CRUD operations against it. While there are some ugly, messy and unstructured PHP out there in the web, we will write clean Object Oriented PHP code that is friendly to Kotlin/Java developers.

MySQL on the other hand is the most popular (Relational Database Management System)RDBMS in the world. We’ve chosen to use it to store our user data because of it’s speed, popularity and easy integration with PHP>

For example here is a simple PHP class to hold our static values like database table and roles:

    class Constants{
        static $TABLE_USER = 'my_tb_name';
        static $ROLES = array("ADMINISTRATOR","EDITOR","SUBSCRIBER");
    }

Here is our UMS class which will contain all our methods to interact with our database.

    class UMS{

        public function doesUserExist($email){

            $sql="SELECT * FROM ".Constants::$TABLE_USER." WHERE email='$email'";
            $user=R::getAll($sql);
            if(count($user) > 0){
                return true;
            }else{
                return false;
            }
        }
        ...

For example in the above function we are checking if a user exists in the database table. If it does we return true, otherwise false.

4. RedbeanPHP

RedbeanPHP is a mature and easy to use PHP Object Relational Mapper. It supports multiple databases like MySQL, SQLite and PostgreSQL. It abstracts away from us the writing of SQL queries so that we access our data either via Object Oriented mechanism or procedurally.

For example see how intuitive our code for updating a user’s text properties is:

        public function updateOnlyText($id,$name,$gender,$bio,$country,$dob){
            $user = R::load(Constants::$TABLE_USER,$id);
            $user->name = $name;
            $user->gender = $gender;
            $user->bio = $bio;
            $user->country = $country;
            $user->dob = $dob;
            $id = R::store( $user );

            if($id > 0){
                print(json_encode(array("code" => 1, "message"=>"User Text Successfully Updated.")));
            }else{
               print(json_encode(array("code" => 2, "message"=>"Not Updated  .")));
            }

        }

RebeanPHP can auto-create for us a database table based on the data we supply it. It can then update the table scheme when the data types passed changes. However we can also lock or freeze the current schema when we are ready for production.

5. Data Binding

We will use data binding to help us connect our data to our UI. This will save us from writing lots of boilerplate code. To use data binding in Kotlin, go over to the app level build.gradle file, inside the android{ } closure and add the following:

    dataBinding {
        enabled = true
    }

Then apply the Kotlin-Kapt plugin at the top:

apply plugin: 'kotlin-kapt'

6. AndroidX

We will be using androidx in our project. To use androidx in a kotlin project, start by going over to the gradle.properties file and add the following:

android.useAndroidX=true

Then add the following in your dependencies closure:

    implementation 'androidx.core:core-ktx:1.2.0'

Then we will include several androidx libraries:

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'

7. ClearText communication

We will allow our app to be able to communicate over secure urls utilizing HTTPS or non-secure ones using HTTP. To do so start by creating a folder inside the res directory. Call it xml, then create an xml file network_security_config and add the following:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>

Then go to the AndroidManifest.xml and register the above file:

    <application
        ....
        android:networkSecurityConfig="@xml/network_security_config"
        ....

Now our app will be able to communicate over secure https e.g:

    const val BASE_URL = "https://camposha.info/php/user/ums/"

and non-secure http e.g:

    const val BASE_URL = "https://camposha.info/php/user/ums/"

8. Material Transitions

We will apply material transition animations while navigating from one activity to another. This makes our app look much smoother and modern. We start by creating our animation files inside the anim folder. For example we have the slide_in_left.xml:

<?xml version="1.0" encoding="utf-8"?>

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

and slide_in_right.xml:

<?xml version="1.0" encoding="utf-8"?>

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

Then in the styles.xml we create a style that references our animations:

    <style name="MyActivityAnimations" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item>
        <item name="android:activityOpenExitAnimation">@anim/slide_out_left</item>
        <item name="android:activityCloseEnterAnimation">@anim/slide_in_left</item>
        <item name="android:activityCloseExitAnimation">@anim/slide_out_right</item>
    </style>

Then apply that style in all our themes where we need the transitions applied:

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowAnimationStyle">@style/MyActivityAnimations</item>
    </style>

9. Retrofit2

Retrofit is one of the most popular HTTP Client for android and java/kotlin. We use the very latest version currently: 2.7.1. We will use it alongside Gson. Gson will map our JSON data to our kotlin model classes.

Here is how we install Retrofit

    implementation 'com.squareup.retrofit2:retrofit:2.7.1'

then install Gson

    implementation 'com.squareup.retrofit2:converter-gson:2.7.1'

We will then create our RestApi interface. This interface will include abstract methods that will represent the requests we make to our server:

interface RestApi {
    /**
     * 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?>?

    ....

Then in our Utils class, we will create a factory method to return us Retrofit instance we can use. It is in this method where we will also set the converter factory:

    private var retrofit: Retrofit? = null
    /**
     * This method will return us our Retrofit instance which we can use to initiate HTTP
     * calls.
     */
    val client: Retrofit?
        get() {
            if (retrofit == null) {
                retrofit = Retrofit.Builder()
                    .baseUrl(Constants.BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
            }
            return retrofit
        }

Then in our Repository class

Demo

Below is the Gif Demo. You will also find the APK demo at the end of this post.

 

Functionalities

Here are some of the fuctionalities you will learn to include in your project:

1. How to Create User Account

This app is a user management system. Users can sign up by creating an account. Account creation is based on the fields or properties we define in our User object. Hence the first step is to create a User object:

class User : 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("gender")
    var gender: String? = ""
    @SerializedName("bio")
    var bio: String? = ""
    @SerializedName("country")
    var country: String? = ""
    @SerializedName("email")
    var email: String? = ""
    @SerializedName("password")
    var password: String? = ""
    @SerializedName("role")
    var role: String? = ""
    @SerializedName("status")
    var status: String? = ""
    @SerializedName("dob")
    var dob: String? = ""
    @SerializedName("registration_date")
    var registrationDate: String? = ""
    @SerializedName("image_url")
    var imageURL: String? = ""
    @SerializedName("friends")
    var friends: String? = ""

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

You can see we’ve included quite plenty of properties for our User object. We’ve initialized these properties to empty using the elvis operator. These properties will require different type of widgets e.g:

  1. EditTexts
  2. Single-Choice Dialogs
  3. DatePicker
  4. Camera,ImagePicker

Once we’ve created our User object, we will need to create an abstract method inside our RestAPI interface to represent this operation of creating a user account. This method will define the properties we will be sending to the server. So in our RestAPI.kt you will find the below method:

    /**
     * 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 createAccount(
        @Part("action") action: RequestBody?,
        @Part("name") name: RequestBody?,
        @Part("gender") gender: RequestBody?,
        @Part("bio") bio: RequestBody?,
        @Part("country") country: RequestBody?,
        @Part("email") email: RequestBody?,
        @Part("password") password: RequestBody?,
        @Part("role") role: RequestBody?,
        @Part("status") status: RequestBody?,
        @Part("date_registered") date_registered: RequestBody?,
        @Part("dob") dob: RequestBody?,
        @Part profileImage: MultipartBody.Part?
    ): Call<ResponseModel?>?

We will be making a HTTP POST Multipart request, in the process uploading the profile image and sending other properties like name,gender,bio,country,email,password etc.

Then in our Repository class, we will start by creating two helper methods. The first will take a string and convert it to and return a RequestBody:

    private fun rb(text: String?): RequestBody {
        return RequestBody.create(MediaType.parse("text/plain"), text)
    }

The second will take a file and convert and return a multipart.part object:

    private fun getMultipart(file: File): MultipartBody.Part {
        /*
        RequestBody is an abstract class to represent the content of our HTTP request
        You instantiate using create(MediaType,content). MediaType is the content type.
        */
        val requestFile =
            RequestBody.create(MediaType.parse("image/jpeg"), file)
        return MultipartBody.Part.createFormData("image", file.name, requestFile)
    }

Then we will create our performRequest() method which will perform our request. We will use this method to not only create account but also do other operations like updating account,deleting account, friending a user, unfriending a user, signing in etc.

    fun performRequest(imageToUpload: File?, p: User, action: String?): MutableLiveData<RequestCall> {

        //Perform our request

    }

Then in our ViewModel class we will expose the above performRequest to observers. Note that we can have different observers, from either different parts of the application. We’ve protected our logic which we did define in our repository class from our UI. We expose our data in realtime using MutableLiveData objects which we return.

    fun createAccount(imageFile: File?, user: User?): MutableLiveData<RequestCall> {
        if (user == null) return error
        return if (imageFile == null) error else mRepository.performRequest(
            imageFile,
            user,
            Constants.CREATE_ACCOUNT
        )
    }

The RequestCall which we’ve been passing as a generic parameter of our MutableLiveData will represent any request we make against our server:

class RequestCall {
    var status = 0
    var message: String? = "UNKNOWN MESSAGE"
    var user: User? = null
    var users: ArrayList<User>? = ArrayList()
}

The status will represent the status of the request. We define these statuses in our Constants class:

    const val IN_PROGRESS = 0
    const val SUCCEEDED = 1
    const val FAILED = -1

The message will be the progress messages,success messages and error messages. Messages will be returned from the server but can also be set manually.

Then inside our AccountUpdateActivity, which we will re-use for both account creation and update, we come and subscribe to our observer from our ViewModel:

    /**
     * This method will allow us create a user account
     */
    private fun createAccount(u: User?) {
        if (validate(chosenFile,true,emailTxt,passwordTxt,emailTxt)){
            remoteViewModel.createAccount(chosenFile, u)
                .observe(this, Observer { r: RequestCall? ->
                    if (makeRequest(r, "ACCOUNT CREATION") == SUCCEEDED) {
                        CacheManager.CACHE_IS_DIRTY = true
                        if(r!!.user != null){
                            CURRENT_USER=r.user
                            PrefUtils.save(this, r.user!!)
                            openPage(DashboardActivity::class.java)
                            finish()
                        }else{
                            PrefUtils.delete(this)
                            show("Now Login")
                            finish()
                        }
                    }
                })
        }
    }

We are receiving a nullable user above, then validating our fields, then invoke our createAccount() method. That method will return a MutableLiveData object which we observe. The makeRequest() method will return the status of the operation. If that status is Constants.SUCCEEDED, then we mark our cache as dirty. If our returned user is not null, we save it to the shared preferences, then open the dashboard page. If the process was a success but the returned user is null, we simply delete the current user from shared preferences, then navigate back to the login page and ask the user to login.

Then we handle the button click event to either update or create a new account. Remember we are using the same activity for both purposes. We are using the same button to initiate either of those actions. We simply check the current user value. If it is null then we know that we are creating a new account. If it is not null then we know we are updating the current user:

        goBtn.setOnClickListener {
            if (CURRENT_USER == null){
                validateThenPOST(false)
            }else{
                validateThenPOST(true)
            }
        }

How to Update a User Account

A logged in user in our User Management System can update his or her account. All properties including the profile image but excluding the email and password can be update in our AccountUpdateActivity. The email cannot be updated. The password can be reset in a seperate activity which we will look at later.

The first step is to go to our RestAPI interface and add two abstract methods that will be responsible for the update. The first method allows us to update only text properties. The second method will involve us making a multipart request to our server. In the process we will send both image and text. We will automatically use the first method if the user doesn’t change an image. However if the user changes the image then we automatically use the second method.

Go ahead add the first method:

    /**
     * This method will be responsible for updating our text properties.
     * We will use it for no image is selected.
     */
    @FormUrlEncoded
    @POST("index.php")
    fun updateOnlyText(
        @Field("action") action: String?,
        @Field("id") id: String?,
        @Field("name") name: String?,
        @Field("gender") gender: String?,
        @Field("bio") bio: String?,
        @Field("country") country: String?,
        @Field("dob") dob: String?
    ): Call<ResponseModel?>?

Then we define our second method:

    /**
     * This method will allow us update our mysql data by making a HTTP POST request.
     * After that we will receive a ResponseModel model object. In this case we are
     * making a multipart request
     */
    @Multipart
    @POST("index.php")
    fun update(
        @Part("action") action: RequestBody?,
        @Part("id") id: RequestBody?,
        @Part("name") name: RequestBody?,
        @Part("gender") gender: RequestBody?,
        @Part("bio") bio: RequestBody?,
        @Part("country") country: RequestBody?,
        @Part("dob") dob: RequestBody?,
        @Part profileImage: MultipartBody.Part?
    ): Call<ResponseModel?>?

Then once we’ve defined the two methods, we go ahead and invoke them in our Repository class:

    fun performRequest(imageToUpload: File?, p: User, action: String?): MutableLiveData<RequestCall> {

        //we invoke them here

    }

Then in our ViewModel class we define two methods to expose the two functionalities to the user interface:

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

and:

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

Both methods will be returning a MutableLiveData object, with the generic parameter being a RequestCall object. That RequestCall will contain the status of the request, the message associated with the requests and an optional list of users to be downloaded.

Now we come to our AccountUpdateActivity.kt and susbcribe to the viewmodel methods we’ve defined above:

    /**
     * This method will allow us Update user's Text properties. If no image
     * is selected then this method will be automatically invoked.
     */
    private fun updateOnlyText(u: User?) {
        remoteViewModel.updateOnlyText(u)
            .observe(this, Observer { r: RequestCall? ->
                if (makeRequest(r, "ACCOUNT UPDATE") == SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = true
                    CURRENT_USER=u
                    show("Returned user is null")
                    PrefUtils.save(this, u!!)
                }
            })
    }

If we succeed we mark our cache as dirty, re-assign the CURRENT_USER value, and save the updated user in the SharedPreferences.

Then we will also have the method for updating images and text:

    /**
     * Update both images and text. This method will automatically be invoked if we capture
     * or select an image from the gallery
     */
    private fun updateImageText(u: User?) {
        remoteViewModel.updateImageText(chosenFile, u)
            .observe(this, Observer { r: RequestCall? ->
                if (makeRequest(r, "ACCOUNT UPDATE") == SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = true
                        CURRENT_USER=u
                        show("Returned user is null")
                        PrefUtils.save(this, u!!)
                }
            })
    }

Again we’ve saved the updated user in shared preferences and marked our cache as dirty.

We will then create a method that will validate our data, then either create a new account or update an existing account:

    /**
     * Let's validate our data then post it
     */
    private fun validateThenPOST(isForUpdate: Boolean) {
        if (validate(chosenFile, false, b!!.nameTxt, b!!.genderTxt, b!!.countryTxt)) {
            val u: User? = if (isForUpdate) {
                receivedUser
            } else {
                User()
            }
            u!!.name = valOf(b!!.nameTxt)
            u.gender = valOf(b!!.genderTxt)
            u.bio = valOf(b!!.bioTxt)
            u.country = valOf(b!!.countryTxt)
            u.dob = valOf(b!!.dobTxt)
            if (isForUpdate) {
                if(chosenFile != null){
                    updateImageText(u)
                }else{
                    updateOnlyText(u)
                }
            } else {
                u.email= valOf(emailTxt)
                u.password= valOf(passwordTxt)
                u.imageURL=""
                u.status="ACTIVE"
                u.role="ADMIN"
                createAccount(u)
            }
        } else {
            show("Please fill up all Fields First")
        }
    }

How to Delete Account

Deleting an account is one of the functionalities included in this app. Users can delete their account by navigating over to the profile page and clicking the delete button. Once you click delete, we connect to the server and delete you account permanently. Then we empty our sharedpreferences and mark our cache as dirty.

Let’s go over to the RestAPI interface and we’ll find the deleteAccount abstract method already defined:

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

In the method you can see we are passing two parameters. The first is a string to identify the action we are performing from other HTTP POST requests we will be making. The second is the id of the account to be deleted.

Then again we will invoke the method above in our performRequest() method:

    fun performRequest(imageToUpload: File?, p: User, action: String?): MutableLiveData<RequestCall> {

        // we delete account
    }

Then in our ViewModel, we expose this delete functionality to the UI. It is in the ViewModel where we will be validating our nullable user before invoking the performRequest() in our Repository class:

    fun deleteAccount(user: User?): MutableLiveData<RequestCall> {
        return if (user == null) error else mRepository.performRequest(
            null,
            user,
            Constants.DELETE_ACCOUNT
        )
    }

As we said earlier, the deletes will be occuring in the ProfileActivity:

    /**
     * This function will be responsible for deleting our user account.
     */
    private fun delete(u: User) {
        remoteViewModel.deleteAccount(u)
            .observe(this, Observer { r: RequestCall? ->
                if (makeRequest(r, "ACCOUNT DELETE") == Constants.SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = true
                    PrefUtils.delete(this)
                    CURRENT_USER =null
                    finish()
                }
            })
    }

We delete the account,mark our cache as dirty, delete user from sharedpreferences as well, set the CURRENT_USER to null, then finish the current activity. We will be deleting the user when the delete button is clicked:

        deleteAccountBtn.setOnClickListener {
            delete(CURRENT_USER!!)
        }

How to Fetch All Users

You can select all users at once using a HTTP GET request. We start by defining an abstract method in our RestAPI interface, decorating with @GET:

    /**
     * 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?>?

Then in our Repository class we create a method called select() that will return a LiveData with our fetched dataset:

    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
    }

Then we can expose that functionality to the UI using the following method:

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

How to Fetch or Search Data while Paginating

We have seen so far how to select all rows in our database from our server at once. Well that isn’t very efficient if you have massive amounts of data. A better approach is to page/paginate data. You can define the number of users to fetch for every request. We will then load data on demand, as the user scrolls through the already downloaded data.

To do this pagination, we need to tell the server where the next page should start and the amount of data we want. You can also include a query to filter down the data.Let’s go to our RestAPI interface an create an abstract method that will cater for these parameters we need to send:

    /**
     * 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?>?

Then in our Repository class:

    fun search(query: String?, start: String?, limit: String?): MutableLiveData<RequestCall> {
        val mLiveData = MutableLiveData<RequestCall>()
        val api = Utils.client!!.create(RestApi::class.java)
        val searchCall =  api.search(Constants.FETCH_PAGINATED_USERS, query, start, limit)
        val requestCall = RequestCall()
        requestCall.status = Constants.IN_PROGRESS
        requestCall.message = "Fetching Paginated Data.. Please wait.."
        mLiveData.value = requestCall
        searchCall!!.enqueue(object : Callback<ResponseModel?> {
            override fun onResponse(call: Call<ResponseModel?>, response: Response<ResponseModel?>) {
                mLiveData.postValue(handleResponse(response, requestCall))
            }

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

We are passing our action,query,page start as well as page limit.

Then in our ViewModel class:

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

Then in our ListingsActivity:

    /**
     * This method will download for us data from php mysql based on supplied query string
     * as well as pagination parameters. We are basiclally searching or selecting data
     * without searching. However all the arriving data is paginated at the server level.
     */
    private fun fetch(start: String, limit: String) {
        remoteViewModel.search("", start, limit)
            .observe(this, Observer { r: RequestCall ->
                val result = makeRequest(r, "USERS DOWNLOAD")
                if (result == Constants.SUCCEEDED) {
                    CacheManager.CACHE_IS_DIRTY = false
                    if (r.users!!.size > 0) {
                        for (s in r.users!!) {
                            if (!itemAlreadyExists(s)) {
                                MEM_CACHE.add(s)
                                /*
                                we then dump the memory cache to disk for
                                permanent caching
                                */
                                cacheToDisk(MEM_CACHE)
                            }
                        }
                        //add current page to adapter
                        adapter!!.addAll(r.users, true)
                        adapter!!.notifyDataSetChanged()
                        //extract image urls from our list and setup carousel
                        networkImages =  getImageURLs(MEM_CACHE)
                        setupCarousel()
                    } else {
                        show("Reached End")
                        HAS_ALREADY_REACHED_END=true
                    }
                }
            })
    }