MVP, Retrofit, and Room: Clean Code

Biggest challenge for a developer is to write clean, reusable, and maintainable code. It is a best practice to divide Android app into separate modules, so that each module performs specific task. For example, app module for android app related code, network module for network related code, and so on. With my past experience I have learned that (the hard way though) when ever we need to make changes into app design poorly written code eats up your development time. And you will end up wasting time searching for a solution or fixing up hard to understand code.

In order to, avoid this it’s better to take out some time and decide how you want to design your app while keeping future prospects of design changes into consideration during the initial phase of your project.

This demo will try and resolve these issues (to some extent). In this demo we will try to get weather information from wunderground API. Our first step is to create an Android project. I believe this will be easy for every android developer. Second step would be to get current location. Add following code in your app module’s build.gradle file:

compile 'com.google.android.gms:play-services-location:11.0.0'

Add following code in your MainActivity.java:

@Override
public void onStart() {
    super.onStart();

    if (!checkPermissions()) {
        requestPermissions();
    } else {
        getLastLocation();
    }
}

Above code check if permission is required if yes then it requests permission or else it fetches location.

private void requestPermissions() {
    boolean shouldProvideRationale =
            ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION);

    // Provide an additional rationale to the user. This would happen if the user denied the
    // request previously, but didn't check the "Don't ask again" checkbox.
    if (shouldProvideRationale) {
        Log.i(TAG, "Displaying permission rationale to provide additional context.");

        showSnackbar(R.string.permission_rationale, android.R.string.ok,
                new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        // Request permission
                        startLocationPermissionRequest();
                    }
                });

    } else {
        Log.i(TAG, "Requesting permission");
        // Request permission. It's possible this can be auto answered if device policy
        // sets the permission in a given state or the user denied the permission
        // previously and checked "Never ask again".
        startLocationPermissionRequest();
    }
}

Above code request location access permission. Once permission is granted it uses following method to get the locations:

private void getLastLocation() {
    mFusedLocationClient.getLastLocation()
            .addOnCompleteListener(this, new OnCompleteListener<Location>() {
                @Override
                public void onComplete(@NonNull Task<Location> task) {
                    if (task.isSuccessful() && task.getResult() != null) {
                        mLastLocation = task.getResult();
                        double latitude = mLastLocation.getLatitude();
                        double longitude = mLastLocation.getLongitude();
                        String latlong = latitude+","+longitude;
                    } else {
                        Log.w(TAG, "getLastLocation:exception", task.getException());
                        showSnackbar(getString(R.string.no_location_detected));
                    }
                }
            });
}

But how do we know that user has acted upon permission dialog. Following code check’s if user has accepted the permission request or has blocked it:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    Log.i(TAG, "onRequestPermissionResult");
    if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
        if (grantResults.length <= 0) {
            // If user interaction was interrupted, the permission request is cancelled and you
            // receive empty arrays.
            Log.i(TAG, "User interaction was cancelled.");
        } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // Permission granted.
            getLastLocation();
        } else {
            // Permission denied.

            // Notify the user via a SnackBar that they have rejected a core permission for the
            // app, which makes the Activity useless. In a real app, core permissions would
            // typically be best requested during a welcome-screen flow.

            // Additionally, it is important to remember that a permission might have been
            // rejected without asking the user for permission (device policy or "Never ask
            // again" prompts). Therefore, a user interface affordance is typically implemented
            // when permissions are denied. Otherwise, your app could appear unresponsive to
            // touches or interactions which have required permissions.
            showSnackbar(R.string.permission_denied_explanation, R.string.settings,
                    new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            // Build intent that displays the App settings screen.
                            Intent intent = new Intent();
                            intent.setAction(
                                    Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                            Uri uri = Uri.fromParts("package",
                                    BuildConfig.APPLICATION_ID, null);
                            intent.setData(uri);
                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            startActivity(intent);
                        }
                    });
        }
    }
}

Now we are ready to request weather information from wunderground API. But before that you need to generate wunderground API key. Once you get the API key we are good to make weather information request. But wait, before we do that we need to organize our app a bit. So let’s add another module in our project and name it ‘webservicesmanager’.

MVP

We have organised our app module using MVP. MVP enables our app module well structured and independent of network module (webservicesmanager module). Activity delegates it’s tasks to presenter and presenter interacts with the models and services. In this example, when we have successfully obtained the location we will ask MainPresenter.java class to get us weather information. Add following code in getLastLocation method as follows:

private void getLastLocation() {
    .
    .
    .                       
                        String latlong = latitude+","+longitude;
                        mPresenter.getWeatherInfo(latlong);
     .
     .
     .
}

The interface Contract.java defines the contract between MainActivity.java and MainPresenter.java. This makes our Activities and presenter work independent of each other and changes in one does not affect other. So when we make getWeatherInfo() request from presenter we do it using the defined contract. And the request is delegated to presenter. When presenter is ready with the response it notifies the Activity using the same contract, in this case onSucess and onError methods.

In MainActivity, create an instance of presenter by using the following code:

private Contract.presenter mPresenter;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mPresenter = new MainPresenter(this, this);
    .
    .
    .
}

Following is the code for Contract.java interface:

public interface Contract {

    interface view {
        void onSucess(String cityName);
        void onError(String errorMsg);
    }

    interface presenter {
        void getWeatherInfo(String latlong);
    }
}

The presenter in turn invokes the MainService.java class. This class does the parsing of response and deals with webservicesmanager module. Hence, if in future you change implementation of web-services module the only class you have to change is MainServices.java rest of the application code will remain intact. Enabling us to minimize changes and keeping our code clean.

public class MainService implements NetworkResponseHandler {
    MainPresenter mainPresenter;
    NetworkRequestManager networkRequestManager;

    public MainService(MainPresenter mainPresenter) {
        this.mainPresenter = mainPresenter;
        this.networkRequestManager = new NetworkRequestManager(this);
    }

    public void getWeatherInfo(String latlong) {
        networkRequestManager.getWeatherInfo(latlong);
    }

    @Override
    public void onResponse(Call<String> call, Response<String> response) {
        // handle response here.  
    }

    private void mapWithDataModel(WeatherInfoModel weatherInfoModel) {

        if(weatherInfoModel != null){
            WeatherInfo weatherInfo = new WeatherInfo();
            String cityName = "";
            String weatherConditionIconUrl ="";
            String CurrentTemp = "";
            String humidity = "";
            String feelsLike = "";
            String dayHighTemp = "";
            String dayLowTemp = "";
            CurrentObservation currentObservation = weatherInfoModel.getCurrentObservation();
            if(currentObservation != null){
                weatherConditionIconUrl = currentObservation.getIconUrl();
                CurrentTemp = String.valueOf(currentObservation.getTempC());
                humidity = currentObservation.getRelativeHumidity();
                feelsLike = currentObservation.getFeelslikeC();
                cityName = currentObservation.getDisplayLocation().getCity();
            }
            Forecast forcast = weatherInfoModel.getForecast();
            if(forcast != null){
                Simpleforecast simepleForcast = forcast.getSimpleforecast();
                if(simepleForcast != null){
                    List<Forecastday_> forecastdayList = simepleForcast.getForecastday();
                    if(!forecastdayList.isEmpty()){
                        dayHighTemp = forecastdayList.get(0).getHigh().getCelsius();
                        dayLowTemp = forecastdayList.get(0).getLow().getCelsius();
                    }
                }
            }
            weatherInfo.setWeatherConditionIconUrl(weatherConditionIconUrl);
            weatherInfo.setCityName(cityName);
            weatherInfo.setCurrentTemp(CurrentTemp);
            weatherInfo.setDayHighTemp(dayHighTemp);
            weatherInfo.setDayLowTemp(dayLowTemp);
            weatherInfo.setHumidity(humidity);
            weatherInfo.setFeelsLike(feelsLike);
            mainPresenter.onSuccess(cityName);
        }
    }

    @Override
    public void onFailure(Call<String> call, Throwable t) {
        mainPresenter.onError(mainPresenter.getContext().getResources().getString(R.string.request_error));
    }

    //notify presenter using this callback interface
    public interface ServiceCallBack{
        void onSuccess(String cityName);
        void onError(String s);
    }
}

NetworkResponseHandler.java is defined in webservicesmanager module.  This interface provide us with the responses of retrofit requests. Adding another level of abstraction. If in future, you want to change you implementation from retrofit to volley and delegating volleys onsucess and onfailure to notify changes using this interface. This enables app module completely intact of underlying implementation of webservicemanager module.

Retrofit

Lets take a look on integrating retroft in webservicesmanager module. First step is to get retrofit dependency in modules gradle:

compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-scalars:2.1.0'

Create an interface for calling wunderground API, this interface will act as network service to be invoked from mock server as shown in the code below:

public interface WeatherApiInterface {
    @GET
    Call<String> getWeatherDetails(@Url String url);
}

@GET specifies the network request type. @Url specifies the URL will be appended with Base service url. If your request have query parameters you can specify them using @Query.

Create WeatherServiceManager class, this class provides retrofit instance for making network calls. Following is the code for this class:

public class WeatherServiceManager {
    private static Retrofit retrofit = null;
    private static WeatherServiceManager weatherServiceManager;

    private WeatherApiInterface weatherApiInterface = null;

    private WeatherServiceManager(){
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(Constants.BASE_URL)
                    .addConverterFactory(ScalarsConverterFactory.create())
                    .build();
        }
        weatherApiInterface = retrofit.create(WeatherApiInterface.class);
    }

    public static WeatherServiceManager getInstance(){
        if(weatherServiceManager == null){
            weatherServiceManager = new WeatherServiceManager();
        }
        return weatherServiceManager;
    }

    public WeatherApiInterface getWeatherApiInterface(){
        return weatherApiInterface;
    }

}

Above class builds retrofit and provides network service interface (WeatherApiInterface) instance. We will be using this instance for calling actual wunderground API.

Create NetworkRequestManager.java class for performing network requests. Following code:

public class NetworkRequestManager {
    private NetworkResponseHandler networkResponseHandler;

    public NetworkRequestManager(NetworkResponseHandler networkResponseHandler){
        this.networkResponseHandler = networkResponseHandler;
    }

    public void getWeatherInfo(String latlong){
        String url = Constants.WUNDERGROUND_API_PART +Constants.WUNDERGROUND_API_KEY+ Constants.WUNDERGROUND_QUESRY_PART + latlong + Constants.JSON_FILE_EXTENSION;
        WeatherApiInterface weatherApiInterface = WeatherServiceManager.getInstance().getWeatherApiInterface();
        if(weatherApiInterface != null){
            Call<String> call = weatherApiInterface.getWeatherDetails(url);
            call.enqueue(networkResponseHandler);
        }
    }
}

This class adds weather information request into retrofit queue, and will be handled by retrofit automatically. All the callbacks will be received by our main service class because our MainService class implements NetworkResponseHandler which is specified as retrofit call back handler in call.enqueue() method in above code.

Room Persistence

WebService response will be handled by MainService  class. This is the place where we can parse the service response and successful parsing of response we are good to store the information into database. Parsing can be done in presenter but that will make our presenter cluttered and hard to understand hence it is best to keep parsing code out or presenter.

Prior to room database access was done through SQL queries and there was not mechanism for verification of these queries. On top of that developers need to write lots of boiler plate code. Room takes care of all these concerns.

Room provides three main components:

  1. Entity: It represents data for a single table row. Room provide construction of entity using annotations.
  2. DAO: It defines the method that access the database. Annotations are used to bind SQL with each method declared in DAO.
  3. Database: It defines the list of entities and database version. The content of this class defines list of DAO’s.

WeatherInfo class is the entity, that we will be using it to map with table row. Following is the code for this class:

@Entity(tableName = "weather")
public class WeatherInfo {
    @PrimaryKey(autoGenerate = true)
    private int uid;
    @ColumnInfo(name = "city_name")
    private String cityName;
    @ColumnInfo(name = "icn")
    private String weatherConditionIconUrl;
    @ColumnInfo(name = "current_temp")
    private String CurrentTemp;
    @ColumnInfo(name = "day_high")
    private String dayHighTemp;
    @ColumnInfo(name = "day_low")
    private String dayLowTemp;
    @ColumnInfo(name = "humidity")
    private String humidity;
    @ColumnInfo(name = "feels_like")
    private String feelsLike;

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public String getWeatherConditionIconUrl() {
        return weatherConditionIconUrl;
    }

    public void setWeatherConditionIconUrl(String weatherConditionIconUrl) {
        this.weatherConditionIconUrl = weatherConditionIconUrl;
    }

    public String getCurrentTemp() {
        return CurrentTemp;
    }

    public void setCurrentTemp(String currentTemp) {
        CurrentTemp = currentTemp;
    }

    public String getDayHighTemp() {
        return dayHighTemp;
    }

    public void setDayHighTemp(String dayHighTemp) {
        this.dayHighTemp = dayHighTemp;
    }

    public String getDayLowTemp() {
        return dayLowTemp;
    }

    public void setDayLowTemp(String dayLowTemp) {
        this.dayLowTemp = dayLowTemp;
    }

    public String getHumidity() {
        return humidity;
    }

    public void setHumidity(String humidity) {
        this.humidity = humidity;
    }

    public String getFeelsLike() {
        return feelsLike;
    }

    public void setFeelsLike(String feelsLike) {
        this.feelsLike = feelsLike;
    }
}

@Entity defines the table name associate with this entity. In this case, it table name is weather. @Primarykey set the variable as primary key and autogenerate = true will auto increment the primary key value. @ColumnInfo create column with defined name.

Following code is used to define the DAO:

@Dao
public interface WeatherInfoDao {

    @Query("SELECT * FROM weather")
    List<WeatherInfo> getAll();

    @Query("SELECT COUNT(*) from weather")
    int countWeatherInfos();

    @Query("SELECT * FROM weather where city_name LIKE  :cityName")
    WeatherInfo findByName(String cityName);

    @Insert
    void insertAll(WeatherInfo... weatherinfos);

    @Insert
    void insert(WeatherInfo weatherinfo);

    @Delete
    void delete(WeatherInfo weatherinfo);
}

@Query maps raw query with API. @Insert will user the underlying API for inserting data into table. Similarly, @Delete maps the API for deleting records from table.

Following code specifies the Database class. Here, we list down all the DAO’s used by our app:

@Database(entities = {WeatherInfo.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

    private static AppDatabase INSTANCE;

    public abstract WeatherInfoDao weatherDao();

    public static AppDatabase getAppDatabase(Context context) {
        if (INSTANCE == null) {
            INSTANCE =
                    Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "user-database")
                            // allow queries on the main thread.
                            // Don't do this on a real app! See PersistenceBasicSample for an example.
                            .allowMainThreadQueries()
                            .build();
        }
        return INSTANCE;
    }

    public static void destroyInstance() {
        INSTANCE = null;
    }

    @Override
    protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
        return null;
    }

    @Override
    protected InvalidationTracker createInvalidationTracker() {
        return null;
    }
}

To get an instance of database use the following code:

AppDatabase appDatabase = AppDatabase.getAppDatabase(mainPresenter.getContext());
appDatabase.weatherDao().insert(weatherInfo);

In our, demo application we are mapping the parsed JSON models into WeatherInfo entity the then pass the entity instance to insert API.  AppDatabase.getAppDatabase(mainPresenter.getContext()) creates and instance of appdatabase class. Then we can use the weatherDao instance to perform CURD operations.

Make sure to destroy the app instance. Use following code snippet:

AppDatabase.destroyInstance();

We can further improve the clean code by making use of Dagger 2 and RxJava. Writing clean code can be achieved by discussing the requirements and providing solutions are per the problem. As each applications has different set of requirements and hence, there can be multiple solutions to achieve good design.

Summary

In this demo we have used MVP for keeping application clean and independent of each other. Contract defined between view and presenter keeps both view and presenter related to each other in a well structured manner.

We have separated out network related tasks and app specific components by creating separate modules and using network module as a library in app module. By doing so we have kept the dependency flow one directional. Any changes to the network modules are abstracted out by using interfaces appropriately.

Complete code for this demo can be downloaded from here.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s