As I know that the ViewModel should be secluded from the UI/View and contains only the logic that observes the data that's coming from the server or database
In my App, I used REST API "retrofit" and blogger API and I tried to migrate/upgrade the current code to MVVM but there are a few problems, let's go to the code
BloggerAPI Class
public class BloggerAPI {
private static final String BASE_URL =
"https://www.googleapis.com/blogger/v3/blogs/4294497614198718393/posts/";
private static final String KEY = "the Key";
private PostInterFace postInterFace;
private static BloggerAPI INSTANCE;
public BloggerAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
postInterFace = retrofit.create(PostInterFace.class);
}
public static String getBaseUrl() {
return BASE_URL;
}
public static String getKEY() {
return KEY;
}
public static BloggerAPI getINSTANCE() {
if(INSTANCE == null){
INSTANCE = new BloggerAPI();
}
return INSTANCE;
}
public interface PostInterFace {
@GET
Call<PostList> getPostList(@Url String url);
}
public Call<PostList>getPosts(String url){
return postInterFace.getPostList(url);
}
}
this getData method I used in the Mainctivity to retrieve blog posts
public void getData() {
if (getItemsByLabelCalled) return;
progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
final Call<PostList> postList = BloggerAPI.getINSTANCE().getPosts(url);
postList.enqueue(new Callback<PostList>() {
@Override
public void onResponse(@NonNull Call<PostList> call, @NonNull Response<PostList> response) {
if (response.isSuccessful()) {
progressBar.setVisibility(View.GONE);
PostList list = response.body();
Log.d(TAG, "onResponse: " + response.body());
if (list != null) {
token = list.getNextPageToken();
items.addAll(list.getItems());
adapter.notifyDataSetChanged();
for (int i = 0; i < items.size(); i++) {
items.get(i).setReDefinedID(i);
}
if (sqLiteItemsDBHelper == null || sqLiteItemsDBHelper.getAllItems().isEmpty()) {
SaveInDatabase task = new SaveInDatabase();
Item[] listArr = items.toArray(new Item[0]);
task.execute(listArr);
}
}
} else {
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
int sc = response.code();
switch (sc) {
case 400:
Log.e("Error 400", "Bad Request");
break;
case 404:
Log.e("Error 404", "Not Found");
break;
default:
Log.e("Error", "Generic Error");
}
}
}
@Override
public void onFailure(@NonNull Call<PostList> call, @NonNull Throwable t) {
Toast.makeText(MainActivity.this, "getData error occured", Toast.LENGTH_LONG).show();
Log.e(TAG, "onFailure: " + t.toString());
Log.e(TAG, "onFailure: " + t.getCause());
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
});
}
I created the PostsViewModel to trying to think practically how to migrate the current code to use MVVM
public class PostsViewModel extends ViewModel {
public MutableLiveData<PostList> postListMutableLiveData = new MutableLiveData<>();
public void getData() {
String token = "";
// if (getItemsByLabelCalled) return;
// progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
BloggerAPI.getINSTANCE().getPosts(url).enqueue(new Callback<PostList>() {
@Override
public void onResponse(Call<PostList> call, Response<PostList> response) {
postListMutableLiveData.setValue(response.body());
}
@Override
public void onFailure(Call<PostList> call, Throwable t) {
}
});
}
}
and it's used thus in MainActivity
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.postListMutableLiveData.observe(this, postList -> {
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
});
now there are two problems using this way of MVVM "ViewModel"
- first in the current getData method in the MainActivity it's contains some components that should work only in the View layer like the items list, the recyclerView needs to set
View.GONEin case of response unsuccessful, progressBar, emptyView TextView, the adapter that needs to notify if there are changes in the list, and finally I need the context to used the create the Toast messages.
To solve this issue I think to add the UI components and other things into the ViewModel Class and create a constructor like this
public class PostsViewModel extends ViewModel {
Context context;
List<Item> itemList;
PostAdapter postAdapter;
ProgressBar progressBar;
TextView textView;
public PostsViewModel(Context context, List<Item> itemList, PostAdapter postAdapter, ProgressBar progressBar, TextView textView) {
this.context = context;
this.itemList = itemList;
this.postAdapter = postAdapter;
this.progressBar = progressBar;
this.textView = textView;
}
but this is not logically with MVVM arch and for sure cause memory leaking also I will not be able to create the instance of ViewModel with regular way like this
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.postListMutableLiveData.observe(this, postList -> {
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
});
and must be used like this
postsViewModel = new PostsViewModel(this,items,adapter,progressBar,emptyView);
so the first question is How to bind these UI components with the ViewModel?
- second in the current getata I used the SaveInDatabase class use the AsyncTask way to save all items in the SQLite database the second question is How to move this class to work with ViewModel? but it also needs to work in the View layer to avoid leaking
the SaveInDatabase Class
static class SaveInDatabase extends AsyncTask<Item, Void, Void> {
@Override
protected Void doInBackground(Item... items) {
List<Item> itemsList = Arrays.asList(items);
// runtimeExceptionDaoItems.create(itemsList);
for (int i = 0 ; i< itemsList.size();i++) {
sqLiteItemsDBHelper.addItem(itemsList.get(i));
Log.e(TAG, "Size :" + sqLiteItemsDBHelper.getAllItems().size());
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
}
}