如何在App/Activity的UI组件和AsyncTask中使用MVVM?

5
据我所知, ViewModel 应该与 UI/View 相分离,仅包含观察从服务器或数据库获取的数据的逻辑。
在我的应用程序中,我使用了 REST API "Retrofit" 和博客 API,并尝试将当前代码迁移到 MVVM。但是出现了一些问题,请查看以下代码。 BloggerAPI 类
    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);
    }
}

我在Mainctivity中使用了getData方法来检索博客文章

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);
        }
    });

}

我创建了PostsViewModel,旨在实际考虑如何将当前代码迁移到使用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) {

            }
        });

    }
}

这样就可以在MainActivity中使用了。

 postsViewModel =  ViewModelProviders.of(this).get(PostsViewModel.class);

        postsViewModel.postListMutableLiveData.observe(this, postList -> {
            items.addAll(postList.getItems());
            adapter.notifyDataSetChanged();
        });

现在使用MVVM的方式存在两个问题,主要涉及IT技术。

  1. 首先,在MainActivity中,getData方法包含了一些应该只在View层工作的组件,例如items列表、recyclerView需要在响应不成功时设置View.GONE、progressBar、emptyView TextView、需要在列表更改时通知的adapter,以及最后我需要上下文来创建Toast消息。

为了解决这个问题,我认为应该将UI组件和其他内容添加到ViewModel类中,并创建如下构造函数:

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;
    }

但这与MVVM架构不符,并且肯定会导致内存泄漏,也无法像常规方式那样创建ViewModel的实例。

 postsViewModel =  ViewModelProviders.of(this).get(PostsViewModel.class);

        postsViewModel.postListMutableLiveData.observe(this, postList -> {
            items.addAll(postList.getItems());
            adapter.notifyDataSetChanged();
        });

并且必须像这样使用

postsViewModel = new PostsViewModel(this,items,adapter,progressBar,emptyView);

所以第一个问题是如何将这些UI组件与ViewModel绑定?

  1. 在当前的getata中,我使用了SaveInDatabase类并使用AsyncTask方式将所有项目保存到SQLite数据库中。第二个问题是如何将此类移动以与ViewModel一起工作?但它还需要在View层中工作以避免泄漏。

SaveInDatabase

    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);
        }
    }

这会有帮助吗?https://medium.com/@sreeharikv112/android-unit-testing-clean-code-architecture-with-mvvm-73eb2992cab7 - Sreehari
2个回答

1
由于您的问题比较广泛,我不会提供任何源代码,而是提供解决与MVVM相关问题的示例。
可以遵循 Clean Code Architecture,以清晰地分离每个层的职责。
首先需要重新构建应用程序架构,以便每个层在MVVM中具有指定的角色。您可以按照以下模式进行操作。
  1. 仅视图模型将访问UI层。
  2. 视图模型将连接使用案例层。
  3. 使用案例层将连接数据层。
  4. 没有层将循环引用其他组件。
  5. 因此,现在对于数据库,存储库将决定从哪个部分获取数据。
  6. 这可以来自网络或数据库。
除了数据库部分外,所有这些点都在Medium Article中涵盖,每个步骤都包含实际的API。还包括单元测试。
此项目中使用的如下:
  1. 协程
  2. Retrofit
  3. Koin(依赖注入)可以用dagger2替代
  4. MockWebServer(测试)
  5. 语言:Kotlin

完整的源代码可以在Github找到

编辑

Kotlin现在是Android开发的官方支持语言。我建议您应该学习并将您的Java Android项目迁移到Kotlin。

如果要将Kotlin转换为Java,请转到菜单 > 工具 > Kotlin > 反编译 Kotlin 为 Java选项


谢谢你的有条理的回答。我看了这个项目,但不幸的是,我现在还不懂Kotlin语言。 我知道有关于这个主题的几个项目,但是否有Java代码版本? - Dr Mido
你可能不知道Kotlin,但结构仍然相同。在Studio中有将Kotlin转换为Java的选项。我可以在编辑后的答案中放置一个参考... :) - Sreehari
1
此外,Google正在努力开发支持Kotlin的库。因此,学习Kotlin显然会对您有所帮助 :) - Sreehari

1

实际上,这个问题太过广泛,难以回答,因为有许多方法可以实现。首先,永远不要将视图对象传递给viewModel。ViewModel用于通过LiveData或rxJava通知ui层发生的更改,而不会保留视图实例。你可以尝试这种方式。

    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;
    }
}

您可以通过以下方式观察到来自您的活动的更改。

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);
        }
    });

对于个人偏好,我喜欢使用Enum来处理错误,但我不能在这里发布,因为它会使答案变得非常长。至于您的第二个问题,请使用谷歌的Room。它将使您的生活变得更加轻松。它与mvvm很好地配合,并且本地支持liveData。您可以尝试使用谷歌的CodeLab来练习使用room。
附加福利:您不需要像这样编辑url:
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();

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

你可以根据需求使用@Path@query

感谢您的参与,首先,“isYourPostListEmpty(postlist)”是什么?我认为您忘记在您的答案中声明此方法了。其次,在应用这些更改后,我应该从MainActivity中删除当前的getData方法吗?因为我在不同的地方使用它,例如在检查互联网连接时:“if(Utils.hasNetworkAccess(this)){ getData();”在“swipeRefreshLayout”和onNavigationItemSelected上,或者我可以简单地从“if(isYourPostListEmpty(postlist)”到结尾复制此代码,并将此代码放入MainActivity中的getData方法中? - Dr Mido
我编辑URL的原因是需要使用封装来保护类成员,例如“BASE_URL”和“KEY”,这些是最危险的信息,不应该将其声明为公共成员,所以我创建了一个getter。此外,我还需要编辑URL以添加标记来应用分页以获取下一个10个帖子,我想您忘记在“onResponse”中添加它了。 - Dr Mido
我留下了 isYourPostListEmpty(postlist) 方法供你实现,并认为方法名本身就很明显。如果不知道响应对象结构和代码结构,我怎么能知道列表是否为空呢?你可以自由使用你的 getData 方法。对于 API 部分,那些不是隐藏秘密密钥和使用分页参数的正确方式。无论如何,这只是你的选择,我无法提供完整的实现,因为网络调用本身就可以成为一篇完整的博客文章。首先尝试阅读我提供的链接,然后相应地重构你的代码。 - WHOA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接