Code snippets can be found throughout the post to aid understanding and implementation.
Non-MVP App
Initially, we have a simple application where a suggested drink is displayed to the user upon clicking a button. However, all the code is currently in the main activity, making it difficult to maintain and test. To address this, we'll refactor the app into the MVP architecture.
MVP Diagram
Before diving into the code, let's understand the MVP architecture diagram. The communication flows between the View, Presenter, and Model layers, with an abstraction layer in place. The View communicates with the Presenter, which in turn interacts with the Model.
Creating the View
In our case, the View is already implemented in the main activity. We'll create an interface, View
, to serve as an abstraction layer for the View. This interface will define methods for showing progress, hiding progress, and displaying the suggested drink to the user. We will create a contract interface which will hold View. Here's the code snippet for the Contract:
public interface IMainContract {
interface View {
void showProgress();
void hideProgress();
void showDrinkToUser(String drinkName);
}
}
We'll implement this interface in the main activity to establish the communication channel between the View and Presenter.
Creating the Presenter
Next, we'll create the Presenter. The Presenter is responsible for fetching the suggested drink from the business logic. We'll create an interface, Presenter
, to define the methods for the Presenter. Here's the code snippet for the updated Contract:
public interface IMainContract {
interface View {
void showProgress();
void hideProgress();
void showDrinkToUser(String drinkName);
}
interface Presenter {
void suggestDrink();
}
}
We'll implement this interface in the Presenter class and establish the communication between the Presenter and View. Here's the code snippet for the Presenter implementation:
public class MainPresenter implements IMainContract.Presenter {
IMainContract.View view;
MainPresenter(IMainContract.View view) {
this.view = view;
}
@Override
public void suggestDrink() {
view.showProgress();
// Repository code will go here
}
}
Creating the Model (Repository Pattern):
To handle the business logic, we'll use the Repository Pattern. We'll create a MainRepository
class, which will act as the bridge between the Presenter and the data sources. We'll create an interface, IDataSource
, to define the methods for the data sources. Here's the code snippet:
public interface IDataSource {
interface LoadDataCallback{
void onDataLoaded(String drinkName);
void onDataNotAvailable(Exception e);
}
void suggestNewDrink(LoadDataCallback callback);
void addDrink(String drinkName);
}
We'll implement this interface in the Repository class and handle the business logic inside the method suggestNewDrink().
Multiple Data Source Implementation
In real-world applications, data is often retrieved from multiple sources, such as a local database and a remote API. In our Repository class, we'll implement multiple sources of data using the RemoteDataSource
and LocalDataSource
. Presenter will communicate with the Repository to suggest the drink without knowing the source data. It is the responsibility of the repository to fetch the data and return it to the presenter.
When the call is made from the presenter, repository will look into the local data source for the data, if the data is not available then it will call the remote data source to provide the data. When the data is provided, it will add this data to the local data source as well as returning it to the presenter. Here is the code snippet for the Repository class:
public class MainRepository implements IDataSource{
private final IDataSource mRemoteDataSource;
private final IDataSource mLocalDataSource;
public MainRepository(IDataSource mRemoteDataSource, IDataSource mLocalDataSource) {
this.mRemoteDataSource = mRemoteDataSource;
this.mLocalDataSource = mLocalDataSource;
}
@Override
public void suggestNewDrink(LoadDataCallback callback) {
// Query the local storage if available. If not, query the network.
mLocalDataSource.suggestNewDrink(new LoadDataCallback() {
@Override
public void onDataLoaded(String drinkName) {
callback.onDataLoaded(drinkName);
}
@Override
public void onDataNotAvailable(Exception e) {
getFromRemoteDataSource(callback);
}
});
}
@Override
public void addDrink(String drinkName) {
}
private void refreshLocalDataSource(String drinkName) {
mLocalDataSource.addDrink(drinkName);
}
private void getFromRemoteDataSource(LoadDataCallback callback) {
mRemoteDataSource.suggestNewDrink(new LoadDataCallback() {
@Override
public void onDataLoaded(String drinkName) {
refreshLocalDataSource(drinkName);
callback.onDataLoaded(drinkName);
}
@Override
public void onDataNotAvailable(Exception e) {
callback.onDataNotAvailable(e);
}
});
}
}
In the local data source, we will maintain the drinks data in an arraylist. In the real-world app local database is used. So, in this one we will add new drinks as soon as they are retrieved from the remote data source. Here is the implementation for the local data source:
public class LocalDataSource implements IDataSource{
ArrayList<String> drinksListLocal = null;
public LocalDataSource() {
drinksListLocal = new ArrayList<>();
}
@Override
public void suggestNewDrink(LoadDataCallback callback) {
try {
if (drinksListLocal.size() == 0) {
callback.onDataNotAvailable(new Exception("Not available"));
return;
}
String drinkName = drinksListLocal.get(new Random().nextInt(drinksListLocal.size()));
callback.onDataLoaded(drinkName);
} catch (Exception e) {
callback.onDataNotAvailable(e);
}
}
@Override
public void addDrink(String drinkName) {
drinksListLocal.add(drinkName);
}
}
In remote data source, we will maintain the drinks data in an array list instead of an API. To mimic server request we will add 1s delay for long background execution. When the drink is suggested from the remote data source we will add that drink in the local data source as well as returning it to the view model. Here is the code snippet for that:
public class RemoteDataSource implements IDataSource{
String[] drinksListRemote = {"Spiking coffee", "Sweet Bananas", "Tomato Tang", "Apple Berry Smoothie"};
@Override
public void suggestNewDrink(LoadDataCallback callback) {
ExecutorService executor = Executors.newSingleThreadExecutor();
//Before executing background task
executor.execute(new Runnable() {
@Override
public void run() {
//Background work here
try {
Thread.sleep(1000); // Mimic server request / long execution
String drinkName = drinksListRemote[new Random().nextInt(drinksListRemote.length)];
callback.onDataLoaded(drinkName);
} catch (InterruptedException e) {
callback.onDataNotAvailable(e);
} catch (Exception e){
callback.onDataNotAvailable(e);
}
}
});
}
@Override
public void addDrink(String drinkName) {
}
}
With the implementation in place, we can now run the application and observe the behavior. The data will be fetched from the local database, if data is not available then remote API based data will be retrieved.
Checkout the complete code: https://github.com/Faisal-FS/Drink-Suggester
Conclusion
In this tutorial, we learned how to refactor a real-world application into the MVP architecture with the Repository Pattern. By separating concerns and establishing clear communication channels between layers, our application becomes more maintainable, testable, and scalable. Check out the accompanying video for a visual demonstration. If you found this tutorial helpful, don't forget to like and subscribe for more informative content. See you in the next one!
No comments:
Post a Comment
Share your thoughts ...