Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use MVVM with the UI components of the App/activity and AsyncTask

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"

  1. 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.GONE in 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?

  1. 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);
        }
    }
like image 590
Dr Mido Avatar asked Jun 21 '20 10:06

Dr Mido


2 Answers

Actually the question is too broad to answer because there are many ways to implement for this case. First of all, never pass view objects to viewModel. ViewModel is used to notify changes to ui layer with LiveData or rxJava without retaining the view instance. You may try this way.

    class PostViewModel extends ViewModel {

    private final MutableLiveData<PostList> postListLiveData = new MutableLiveData<PostList>();
    private final MutableLiveData<Boolean> loadingStateLiveData = new MutableLiveData<Boolean>();
    private String token = "";

    public void getData() {
        loadingStateLiveData.postValue(true);
      
    //        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) {
                loadingStateLiveData.postValue(false);
                postListLiveData.setValue(response.body());
                token = response.body().getNextPageToken(); //===> the token

            }

            @Override
            public void onFailure(Call<PostList> call, Throwable t) {
                loadingStateLiveData.postValue(false);
            }
        });

    }

    public LiveData<PostList> getPostListLiveData(){
        return postListLiveData;
    }

    public LiveData<Boolean> getLoadingStateLiveData(){
        return loadingStateLiveData;
    }
}

and you may observe the changes from your activity like this.

postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
    postsViewModel.getPostListLiveData().observe(this,postList->{
        if(isYourPostListEmpty(postlist)) {
            recyclerView.setVisibility(View.GONE);
            emptyView.setVisibility(View.VISIBLE);
            items.addAll(postList.getItems());
            adapter.notifyDataSetChanged();
        }else {
            recyclerView.setVisibility(View.VISIBLE);
            emptyView.setVisibility(View.GONE);

        }
    });


    postsViewModel.getLoadingStateLiveData().observe(this,isLoading->{
        if(isLoading) {
            progressBar.setVisibility(View.VISIBLE);
        }else {
            progressBar.setVisibility(View.GONE);
        }
    });

For my personal prefer, I like using Enum for error handling, but I can't post here as it will make the answer very long. For your second question, use Room from google. It will make you life a lot easier. It work very well with mvvm and it natively support liveData. You can try CodeLab from google to practise using room.

Bonus: You don't need to edit the url like this:

String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();

    if (token != "") {
        url = url + "&pageToken=" + token;
    }

You can use @Path or @query based on your requirements.

like image 54
WHOA Avatar answered Sep 22 '22 09:09

WHOA


As your question is bit broad , I am not giving any source code for the same, Rather mentioning samples which clearly resolves issues mentioned with MVVM.

Clean Code Architecture can be followed which will clearly separate the responsibilities of each layer.

First of all application architecture needs to be restructured so that each layer has designated role in MVVM. You can follow the following pattern for the same.

  1. Only View Model will have access to UI layer
  2. View model will connect with Use Case layer
  3. Use case layer will connect with Data Layer
  4. No layer will have cyclic reference to other components.
  5. So now for Database, Repository will decide, from which section the data needs to be fetched
  6. This can be either from Network or from DataBase.

All these points (except Database part) are covered over Medium Article, were each step is covered with actual API's . Along with that unit test is also covered.

Libraries used are in this project are

  1. Coroutines
  2. Retrofit
  3. Koin (Dependency Injection) Can be replaced with dagger2 is required
  4. MockWebServer (Testing)
  5. Language: Kotlin

Full Source code can be found over Github

Edit

Kotlin is the official supported language for Android Development now. I suggest you should lean and migrate your java android projects to Kotlin.

Still for converting Kotlin to Java, Go to Menu > Tools > Kotlin > Decompile Kotlin to Java Option

like image 39
Sreehari Avatar answered Sep 21 '22 09:09

Sreehari