繁体   English   中英

如何将 MVVM 与 App/activity 和 AsyncTask 的 UI 组件一起使用

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

据我所知,ViewModel 应该与 UI/View 隔离开来,并且只包含观察来自服务器或数据库的数据的逻辑

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

我在 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 Class 并创建一个像这样的构造函数

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 架构不符,并且肯定会导致 memory 泄漏,我也将无法以这样的常规方式创建 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 class 使用AsyncTask方式保存SQLite数据库中的所有项目第二个问题是如何移动这个 class 以使用 ViewModel? 但它也需要在 View 层工作以避免泄漏

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

实际上这个问题太宽泛而无法回答,因为这种情况有很多方法可以实现。 首先,永远不要将视图对象传递给 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 进行错误处理,但我不能在这里发布,因为它会使答案变得很长。 对于第二个问题,请使用 Google 的Room 它会让你的生活轻松很多。 它与 mvvm 一起工作得很好,并且它原生支持 liveData。 你可以试试谷歌的CodeLab来练习使用房间。

奖励:您不需要像这样编辑 url :

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

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

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

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

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

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

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

所有这些点(除了数据库部分)都包含在Medium Article中,每个步骤都包含实际的 API。 与该单元测试一起,也包括在内。

在这个项目中使用的库是

  1. 协程
  2. Retrofit
  3. Koin (依赖注入) 可替换为 dagger2 是必需的
  4. MockWebServer(测试)
  5. 语言:Kotlin

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

编辑

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM