Chuyển đến nội dung chính

Working with MVP and Retrofit 2.0 in Android

https://medium.com/@shri_chaudhari/working-with-mvp-and-retrofit-2-0-in-android-4016253ab3fc
Recently I have observed that many Android developers are talking about the app architecture, usage of new libraries in the project. So I was thinking why not create a post which will cover some of the topics, so I have decided to go with MVP along with Retrofit 2.0.
This post is what I have researched and learnt from various resources. You can even find an example for the same in this blog. I believe that many of you are already aware of MVP and Retrofit, so without going into much details, I will give short descriptions and then start with the example.

MVP (Model-View-Presenter)

The MVP stands for Model View Presenter. In this architecture, each layer is defined to do certain tasks.







Fig 1. MVP Architecture

  • Model is responsible for handling all business logic, communication with backend server and database operations.
  • The View is responsible for displaying data to a user in the form of UI screens. In Android usually, Activity implements the View.
  • The Presenter is acted as a middleman between View and Model. It basically retrieves data from Model and returns it to View for display Purpose.

Retrofit 2.0








Fig 2. Retrofit

Retrofit is type-safe HTTP client for Android and Java created by Square Inc. It is an open source library which simplifies the HTTP communication. Using this library, developers can do the network stuff much easier.

Project Overview

After going through the basics of MVP and Retrofit, its time to use them in the project. For this project, we will use TMDB API. The internet movie database is a web page similar to IMDB where you can find a list of movies with their description, images, trailers and so on. We are going to show the list of popular movies in recycle view and also details of the selected movie. First of all, we will have to get the API key from this website to access the database.
Here is the final project we are about to build.







Fig 3. Movie App Screenshot

Project structure:

After we will be completed by this application, you can see below project structure for our application.







Fig 4. Project Structure

So let’s gear up and start the project.

1. build.gradle

Add following dependencies to the project.
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// View support library
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
// Glide - image processing library
implementation 'com.github.bumptech.glide:glide:4.7.1'
// Network library
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
//Calendar view
implementation ('com.wdullaer:materialdatetimepicker:3.5.2') {
exclude group: 'com.android.support'
}
}

2. Implementing Retrofit

Before we can start designing model class, first we need to know the structure of JSON data coming from TMDb API. For this example, I am going to use most popular movies API. Using this (http://api.themoviedb.org/3/movie/popular?api_key=YOUR_API_KEY) API we will get the list of 50 most popular movies in the following JSON format.
{  
   "page":1,
   "total_results":19862,
   "total_pages":994,
   "results":[  
      {  
         "vote_count":2023,
         "id":299536,
         "video":false,
         "vote_average":8.7,
         "title":"Avengers: Infinity War",
         "popularity":725.420399,
         "poster_path":"\/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg",
         "original_language":"en",
         "original_title":"Avengers: Infinity War",
         "genre_ids":[  
            12,
            878,
            14,
            28
         ],
         "backdrop_path":"\/bOGkgRGdhrBYJSLpXaxhXVstddV.jpg",
         "adult":false,
         "overview":"As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos.",
         "release_date":"2018-04-25"
      },
      ...
   ]
}
a. Now create a Model class Movie.Java


public class Movie {
private int id;
private String title;
@SerializedName("release_date")
private String releaseDate;
@SerializedName("vote_average")
private float rating;
@SerializedName("poster_path")
private String thumbPath;
@SerializedName("overview")
private String overview;
@SerializedName("backdrop_path")
private String backdropPath;
@SerializedName("credits")
private Credits credits;
@SerializedName("runtime")
private String runTime;
@SerializedName("tagline")
private String tagline;
@SerializedName("homepage")
private String homepage;
public Movie(int id, String title, String releaseDate, float rating, String thumbPath, String overview, String backdropPath, Credits credits, String runTime, String tagline, String homepage) {
this.id = id;
this.title = title;
this.releaseDate = releaseDate;
this.rating = rating;
this.thumbPath = thumbPath;
this.overview = overview;
this.backdropPath = backdropPath;
this.credits = credits;
this.runTime = runTime;
this.tagline = tagline;
this.homepage = homepage;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getReleaseDate() {
return releaseDate;
}
public void setReleaseDate(String releaseDate) {
this.releaseDate = releaseDate;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getThumbPath() {
return thumbPath;
}
public void setThumbPath(String thumbPath) {
this.thumbPath = thumbPath;
}
public String getOverview() {
return overview;
}
public void setOverview(String overview) {
this.overview = overview;
}
public String getBackdropPath() {
return backdropPath;
}
public void setBackdropPath(String backdropPath) {
this.backdropPath = backdropPath;
}
public Credits getCredits() {
return credits;
}
public void setCredits(Credits credits) {
this.credits = credits;
}
public String getRunTime() {
return runTime;
}
public void setRunTime(String runTime) {
this.runTime = runTime;
}
public String getTagline() {
return tagline;
}
public void setTagline(String tagline) {
this.tagline = tagline;
}
public String getHomepage() {
return homepage;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
@Override
public String toString() {
return "Movie{" +
"id=" + id +
", title='" + title + '\'' +
", releaseDate='" + releaseDate + '\'' +
", rating=" + rating +
", thumbPath='" + thumbPath + '\'' +
", overview='" + overview + '\'' +
", backdropPath='" + backdropPath + '\'' +
", credits=" + credits +
", runTime='" + runTime + '\'' +
", tagline='" + tagline + '\'' +
", homepage='" + homepage + '\'' +
'}';
}
}

b. Create a MovieListResponse.Java as there are some more fields like page no, total pages etc.

public class MovieListResponse {
@SerializedName("page")
private int page;
@SerializedName("results")
private List<Movie> results;
@SerializedName("total_results")
private int totalResults;
@SerializedName("total_pages")
private int totalPages;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public List<Movie> getResults() {
return results;
}
public void setResults(List<Movie> results) {
this.results = results;
}
public int getTotalResults() {
return totalResults;
}
public void setTotalResults(int totalResults) {
this.totalResults = totalResults;
}
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
}


. Now lets create instance class for Retrofit. Creating a class called ApiClient.Java
public class ApiClient {
public static final String BASE_URL = "http://api.themoviedb.org/3/";
private static Retrofit retrofit = null;
public static final String IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w200/";
public static final String BACKDROP_BASE_URL = "https://image.tmdb.org/t/p/w780/";
/**
* This method returns retrofit client instance
*
* @return Retrofit object
*/
public static Retrofit getClient() {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
}

d. Creating interface ApiInterfac.Java for defining the end points.

public interface ApiInterface {
@GET("movie/popular")
Call<MovieListResponse> getPopularMovies(@Query("api_key") String apiKey, @Query("page") int PageNo);
@GET("movie/{movie_id}")
Call<Movie> getMovieDetails(@Path("movie_id") int movieId, @Query("api_key") String apiKey, @Query("append_to_response") String credits);
}

3. Implementing MVP

We are ready with our API call, now let’s check how we can use this api in MVP model. For this blog, I am going to explain you an example for movies list activity.
a. Create a contract interface MovieListContract.Java. This interface will contain all the methods that our View, Model and Presenter going to implements.
public interface MovieListContract {
interface Model {
interface OnFinishedListener {
void onFinished(List<Movie> movieArrayList);
void onFailure(Throwable t);
}
void getMovieList(OnFinishedListener onFinishedListener, int pageNo);
}
interface View {
void showProgress();
void hideProgress();
void setDataToRecyclerView(List<Movie> movieArrayList);
void onResponseFailure(Throwable throwable);
}
interface Presenter {
void onDestroy();
void getMoreData(int pageNo);
void requestDataFromServer();
}
}




b. Now let’s create Model class MovieListModel.Java. This class will have actual business logic for fetching data from a server. This class will implement Model Interface from Contract Interface given above.

public class MovieListModel implements MovieListContract.Model {
private final String TAG = "MovieListModel";
/**
* This function will fetch movies data
* @param onFinishedListener
* @param pageNo : Which page to load.
*/
@Override
public void getMovieList(final OnFinishedListener onFinishedListener, int pageNo) {
ApiInterface apiService =
ApiClient.getClient().create(ApiInterface.class);
Call<MovieListResponse> call = apiService.getPopularMovies(API_KEY, pageNo);
call.enqueue(new Callback<MovieListResponse>() {
@Override
public void onResponse(Call<MovieListResponse> call, Response<MovieListResponse> response) {
List<Movie> movies = response.body().getResults();
Log.d(TAG, "Number of movies received: " + movies.size());
onFinishedListener.onFinished(movies);
}
@Override
public void onFailure(Call<MovieListResponse> call, Throwable t) {
// Log error here since request failed
Log.e(TAG, t.toString());
onFinishedListener.onFailure(t);
}
});
}
}




If you have observed in getMovieList() method we are fetching data with the help of retrofit.

c. Let’s create an Activity MovieListActivity.Java which will act as View in our MVP. This class will handle all UI rendering and UI listeners. This class will implement View interface from our contract interface.

public class MovieListActivity extends AppCompatActivity implements MovieListContract.View, MovieItemClickListener,
ShowEmptyView {
private static final String TAG = "MovieListActivity";
private MovieListPresenter movieListPresenter;
private RecyclerView rvMovieList;
private List<Movie> moviesList;
private MoviesAdapter moviesAdapter;
private ProgressBar pbLoading;
private FloatingActionButton fabFilter;
private TextView tvEmptyView;
private int pageNo = 1;
//Constants for load more
private int previousTotal = 0;
private boolean loading = true;
private int visibleThreshold = 5;
int firstVisibleItem, visibleItemCount, totalItemCount;
private GridLayoutManager mLayoutManager;
// Constants for filter functionality
private String fromReleaseFilter = "";
private String toReleaseFilter = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_movie_list);
getSupportActionBar().setTitle(getString(R.string.most_popular_movies));
initUI();
setListeners();
//Initializing presenter
movieListPresenter = new MovieListPresenter(this);
movieListPresenter.requestDataFromServer();
}
/**
* This method will initialize the UI components
*/
private void initUI() {
rvMovieList = findViewById(R.id.rv_movie_list);
moviesList = new ArrayList<>();
moviesAdapter = new MoviesAdapter(this, moviesList);
mLayoutManager = new GridLayoutManager(this, 2);
rvMovieList.setLayoutManager(mLayoutManager);
rvMovieList.addItemDecoration(new GridSpacingItemDecoration(2, dpToPx(this, 10), true));
rvMovieList.setItemAnimator(new DefaultItemAnimator());
rvMovieList.setAdapter(moviesAdapter);
pbLoading = findViewById(R.id.pb_loading);
fabFilter = findViewById(R.id.fab_filter);
tvEmptyView = findViewById(R.id.tv_empty_view);
}
/**
* This function will contain listeners for all views.
*/
private void setListeners() {
rvMovieList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
visibleItemCount = rvMovieList.getChildCount();
totalItemCount = mLayoutManager.getItemCount();
firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition();
// Handling the infinite scroll
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && (totalItemCount - visibleItemCount)
<= (firstVisibleItem + visibleThreshold)) {
movieListPresenter.getMoreData(pageNo);
loading = true;
}
// Hide and show Filter button
if (dy > 0 && fabFilter.getVisibility() == View.VISIBLE) {
fabFilter.hide();
} else if (dy < 0 && fabFilter.getVisibility() != View.VISIBLE) {
fabFilter.show();
}
}
});
fabFilter.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Going to filter screen
Intent movieFilterIntent = new Intent(MovieListActivity.this, MovieFilterActivity.class);
movieFilterIntent.putExtra(KEY_RELEASE_FROM, fromReleaseFilter);
movieFilterIntent.putExtra(KEY_RELEASE_TO, toReleaseFilter);
startActivityForResult(movieFilterIntent, ACTION_MOVIE_FILTER);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ACTION_MOVIE_FILTER) {
if (resultCode == Activity.RESULT_OK) {
// Checking if there is any data to filter
fromReleaseFilter = data.getStringExtra(KEY_RELEASE_FROM);
toReleaseFilter = data.getStringExtra(KEY_RELEASE_TO);
moviesAdapter.setFilterParameter(fromReleaseFilter, toReleaseFilter);
moviesAdapter.getFilter().filter("");
}
}
}
@Override
public void showProgress() {
pbLoading.setVisibility(View.VISIBLE);
}
@Override
public void hideProgress() {
pbLoading.setVisibility(View.GONE);
}
@Override
public void setDataToRecyclerView(List<Movie> movieArrayList) {
moviesList.addAll(movieArrayList);
moviesAdapter.notifyDataSetChanged();
// This will help us to fetch data from next page no.
pageNo++;
}
@Override
public void onResponseFailure(Throwable throwable) {
Log.e(TAG, throwable.getMessage());
Toast.makeText(this, getString(R.string.communication_error), Toast.LENGTH_LONG).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
movieListPresenter.onDestroy();
}
@Override
public void onMovieItemClick(int position) {
if (position == -1) {
return;
}
Intent detailIntent = new Intent(this, MovieDetailsActivity.class);
detailIntent.putExtra(KEY_MOVIE_ID, moviesList.get(position).getId());
startActivity(detailIntent);
}
@Override
public void showEmptyView() {
rvMovieList.setVisibility(View.GONE);
tvEmptyView.setVisibility(View.VISIBLE);
}
@Override
public void hideEmptyView() {
rvMovieList.setVisibility(View.VISIBLE);
tvEmptyView.setVisibility(View.GONE);
}
}




d. Now here comes our Presenter, create a class MovieListPresenter.Java. This class will act as the mediator between View and Model. It will have a reference to both View and Model. This class will implement Presenter interface from our contract interface.

public class MovieListPresenter implements MovieListContract.Presenter, MovieListContract.Model.OnFinishedListener {
private MovieListContract.View movieListView;
private MovieListContract.Model movieListModel;
public MovieListPresenter(MovieListContract.View movieListView) {
this.movieListView = movieListView;
movieListModel = new MovieListModel();
}
@Override
public void onDestroy() {
this.movieListView = null;
}
@Override
public void getMoreData(int pageNo) {
if (movieListView != null) {
movieListView.showProgress();
}
movieListModel.getMovieList(this, pageNo);
}
@Override
public void requestDataFromServer() {
if (movieListView != null) {
movieListView.showProgress();
}
movieListModel.getMovieList(this, 1);
}
@Override
public void onFinished(List<Movie> movieArrayList) {
movieListView.setDataToRecyclerView(movieArrayList);
if (movieListView != null) {
movieListView.hideProgress();
}
}
@Override
public void onFailure(Throwable t) {
movieListView.onResponseFailure(t);
if (movieListView != null) {
movieListView.hideProgress();
}
}
}

f you take a look at our presenter it taking request from view for getting the list of movies and sending that request to model for processing.
Wooo! Now we know how we can use Retrofit and MVP in our project.

Demo Video

Run the app now you will see the following app running on your phone.


Nhận xét

Bài đăng phổ biến từ blog này

Camera2 - Android

Ở bài viết này mình xin giới thiệu về cách sử dụng Camera2 trong android SDK 21. Với các lập trình viên android việc sử dụng Camera có rất nhiều trong ứng dụng: Camera Capture Images, Barcode - QR Code Reader, AR, Video Record,.... Nhiều ứng dụng chỉ ở tầng ứng dụng sử dụng thông qua  Intent  như vậy hệ thống tự động được gọi ở mức tối ưu nhất, nhưng cũng không ít ứng dụng cần can thiệp vào tầng  native  để xử lý Với  Camera   developer.android.com  đã  deprecate  nó đã không còn được sử dụng cơ bản trong các ứng dụng nữa vì rất nhiều nguyên nhân trong đó phải kể tới: tốn tài nguyên, thời gian capture khá chậm và đặc biệt phục vụ nhu cầu ngày càng cao của người dùng như 'Chụp ảnh liên tục, chụp nhiều ảnh và tự động lấy nét' thì  Camera không đáp ứng được Với  Camera2  đã đáp ứng được những thiếu xót trên ngoài ra việc customize cho ảnh là rất dễ dàng mang lại chất lượng cao ngoài ra việc sử dụng cũng không có nhiều thay ...

TỰ ĐỘNG HUỶ ACTIVITY SAU KHI STARTACTIVITY

Trước giờ để huỷ một Activity khi bạn thường dùng hàm  finish()  đúng không nào? Không đi đâu xa là khi bạn Intent từ một Activity này sang Activity khác mà muốn huỷ luôn Activity đầu tiên luôn thì bạn sẽ dùng đoạn code y chang bên dưới chứ? Cơ chế của Activity là khi bạn chuyển từ một Activity này sáng Activity khác thì nó sẽ Activity đó vào stack, và khi back về thì Activity sẽ được hiện lên lại và chạy vào onResume(), nếu bạn chưa hiểu về  vòng đời của Activity  thì xem lại bài viết kèm video tại blog mình nhé, mình ví dụ khá chi tiết. Intent intent = new Intent(MainActivity.this, LoginActivity.class); startActivity(intent); finish(); Đoạn code trên nghĩa là chuyển từ MainActivity sang LoginActivity và sau đó huỷ luôn MainActivity đúng không nào? Tuy nhiên đó không phải là cách duy nhất mà chúng ta làm đâu bởi Android hỗ trợ chúng ta một số thuộc tính mà bạn không phải dùng code Java để làm. Không lưu Activity vào stack Cũng logic như bài toán phí...

Sự khác nhau giữa let, apply, with, run và also trong Kotlin

Với những ai đã sử dụng Kotlin để phát triển ứng dụng, chắc hẳn đã không ít lần sử dụng các standard functions run, with, let, also và apply. Để hiểu và sử dụng thành thục chúng không phải là dễ. Và dưới đây là những điều đúc kết lại được. https://viblo.asia/p/su-khac-nhau-giua-let-apply-with-run-va-also-trong-kotlin-RQqKLNdml7z Scoping functions Có thể hiểu đơn giản, scoping function là phạm vi ảnh hưởng nhất định của một hàm. Nó là điều cốt lõi để phân biệt giữa các scoping functions: run, with, T.run, T.let, T.also và T.apply. Dưới đây là minh hoạ phạm vi của hàm run: fun test ( ) { var mood = "I am sad" run { val mood = "I am happy" println ( mood ) // I am happy } println ( mood ) // I am sad } Ở trên, ta có thể thấy rõ ràng trong phạm vi của hàm run, biến mood đã được định nghĩa lại trước khi in ra mà không làm ảnh hưởng tới phần khác của chương trình 3 attributes of scoping functions 1.Normal vs. extension fun...