【问题标题】:How can I use MVVM with the UI components of the App/activity and AsyncTask如何将 MVVM 与 App/activity 和 AsyncTask 的 UI 组件一起使用
【发布时间】:2020-10-11 07:28:41
【问题描述】:

据我所知,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“ViewModel”有两个问题

  1. 首先在 MainActivity 中的当前 getData 方法中,它包含一些只能在 View 层工作的组件,如项目列表,recyclerView 需要设置View.GONE 以防响应不成功, progressBar,emptyView TextView,需要通知列表是否有变化的适配器,最后我需要上下文来使用创建 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);
        }
    }

【问题讨论】:

标签: java android mvvm android-asynctask retrofit


【解决方案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。你可以试试 google 的CodeLab 来练习使用房间。

奖励:您无需像这样编辑网址:

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

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

您可以根据需要使用@Path@query

【讨论】:

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

由于您的问题有点宽泛,我没有提供任何相同的源代码,而是提到可以清楚地解决 MVVM 中提到的问题的示例。

干净的代码架构可以遵循,这将清楚地分开每一层的职责。

首先需要重新构建应用程序架构,以便每一层在 MVVM 中都有指定的角色。您可以按照以下模式进行操作。

  1. 只有 View Model 才能访问 UI 层
  2. 视图模型将与用例层连接
  3. 用例层将与数据层连接
  4. 任何层都不会循环引用其他组件。
  5. 所以现在对于数据库,存储库将决定需要从哪个部分获取数据
  6. 这可以来自网络或数据库。

所有这些点(除了数据库部分)都被Medium Article 覆盖,每个步骤都被实际的API 覆盖。 除了该单元测试,还包括在内。

此项目中使用的库是

  1. 协程
  2. 改造
  3. Koin(依赖注入)可以替换为 dagger2 是必需的
  4. MockWebServer(测试)
  5. 语言:科特林

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

编辑

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

仍然要将 Kotlin 转换为 Java,转到 Menu > Tools > Kotlin > Decompile Kotlin to Java 选项

【讨论】:

  • 感谢有条理的回答,我查看了这个项目,但不幸的是,我直到现在才知道 kotlin 语言,我知道有几个关于这个主题的项目,但是有 Java 代码的版本吗?
  • 您可能不知道 Kotlin,但结构保持不变。 Studio 中有将 Kotlin 转换为 Java 的选项。我可以在编辑后的答案中添加参考... :)
  • More over Google 致力于开发支持 Kotlin 的库。所以学习 Kotlin 显然会对你有所帮助:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-12
  • 2020-07-02
  • 1970-01-01
  • 2021-10-02
  • 1970-01-01
  • 2021-08-26
  • 2017-10-26
相关资源
最近更新 更多