美文网首页
《安卓-深入浅出MVVM教程》应用篇-04 State Lcee

《安卓-深入浅出MVVM教程》应用篇-04 State Lcee

作者: IT天宇 | 来源:发表于2017-10-24 00:01 被阅读332次

    简介

    背景

    这几年 MVP 架构在安卓界非常流行,几乎已经成为主流框架,它让业务逻辑 和 UI操作相对独立,使得代码结构更清晰。


    MVVM 在前端火得一塌糊涂,而在安卓这边却基本没见到几个人在用,看到介绍 MVVM 也最多是讲 DataBinding 或 介绍思想的。偶尔看到几篇提到应用的,还是对谷歌官网的Architecture Components 文章的翻译。

    相信大家看别人博客或官方文档的时候,总会碰到一些坑。要么入门教程写得太复杂(无力吐槽,前面写一堆原理,各种高大上的图,然并卵,到实践部分一笔带过,你确定真的是入门教程吗)。要么就是简单得就是一个 hello world,然后就没有下文了(看了想骂人)。


    实在看不下去的我,决定插手你的人生。

    目录

    《安卓-深入浅出MVVM教程》大致分两部分:应用篇、原理篇。
    采用循序渐进方式,内容深入浅出,符合人类学习规律,希望大家用最少时间掌握 MVVM。

    应用篇:

    01 Hello MVVM (快速入门)
    02 Repository (数据仓库)
    03 Cache (本地缓存)
    04 State Lcee (加载/空/错误/内容视图)
    05 Simple Data Source (简单的数据源)
    06 Load More (加载更多)
    07 DataBinding (数据与视图绑定)
    08 RxJava2
    09 Dragger2
    10 Abstract (抽象)
    11 Demo (例子)
    12-n 待定(欢迎 github 提建议)

    原理篇

    01 MyLiveData(最简单的LiveData)
    02-n 待定(并不是解读源码,那样太无聊了,打算带你从0撸一个 Architecture)

    关于提问

    本人水平和精力有限,如果有大佬发现哪里写错了或有好的建议,欢迎在本教程附带的 github仓库 提issue。
    What?为什么不在博客留言?考虑到国内转载基本无视版权的情况,一般来说你都不是在源出处看到这篇文章,所以留言我也一般是看不到的。

    教程附带代码

    https://github.com/ittianyu/MVVM

    应用篇放在 app 模块下,原理篇放在 implementation 模块下。
    每一节代码采用不同包名,相互独立。

    前言

    上一节我们加入了缓存。这一节我来回答上一次的问题。

    重新请求数据

    下拉刷新 或者 重发请求 是常见的需求。
    怎么在 MVVM 中实现呢?

    其实第一节就有提到,LiveData 直接调用 setValue 或 PostValue 就会触发一次数据更新操作。

    为了使得 username 可以改变,我们要把他定义为一个可变的 LiveData。
    那么我们怎么在 username 改变的时候去更新 user 呢?

    我们可以通过 Transformations.switchMap 来生成一个监听 username 的 LiveData

    public class UserViewModel extends ViewModel {
        private UserRepository userRepository = UserRepository.getInstance();
        private MutableLiveData<String> ldUsername;
        private LiveData<User> ldUser;
    
        public LiveData<User> getUser() {
            if (null == ldUser) {
                ldUsername = new MutableLiveData<>();
                ldUser = Transformations.switchMap(ldUsername, new Function<String, LiveData<User>>() {
                    @Override
                    public LiveData<User> apply(String username) {
                        return userRepository.getUser(username);
                    }
                });
            }
            return ldUser;
        }
    
        public void reload(String username) {
            ldUsername.setValue(username);
        }
    
    }
    

    当需要请求数据的时候,View 中调用 reload 方法即可触发。
    这里不再往下讲了,因为还要加入多状态。

    多状态视图

    最常见的状态是 Loading Content Empty Error

    Bean

    所以首先想到的是定义枚举类型

    public enum Status {
        Loading,
        Content,
        Empty,
        Error,
    }
    

    有了类型该怎么使用?
    作为 MVVM 架构,核心思想是根据 Data 渲染 View,所以很自然想到根据状态和数据来渲染 View。然而状态和数据是难以分离的,所以一般会想到通过一个 bean 来把数据和状态包装起来。

    public class Lcee<T> {
        public final Status status;
        public final T data;
        public final Throwable error;
    
        public Lcee(Status status, T data, Throwable error) {
            this.status = status;
            this.data = data;
            this.error = error;
        }
    }
    

    为了方便外部使用,我们再定义一些工厂方法。

        public static <T> Lcee<T> content(T data) {
            return new Lcee<>(Status.Content, data, null);
        }
    
        public static <T> Lcee<T> error(T data, Throwable error) {
            return new Lcee<>(Status.Error, data, error);
        }
        public static <T> Lcee<T> error(Throwable error) {
            return error(null, error);
        }
    
        public static <T> Lcee<T> empty(T data) {
            return new Lcee<>(Status.Empty, data, null);
        }
        public static <T> Lcee<T> empty() {
            return empty(null);
        }
    
        public static <T> Lcee<T> loading(T data) {
            return new Lcee<>(Status.Loading, data, null);
        }
        public static <T> Lcee<T> loading() {
            return loading(null);
        }
    

    Model

    DataSource
    因为 bean 变了,所以 DataSource 和实现都得修改。

    先把接口的返回值改成 LiveData<Lcee<User>>

    public interface UserDataSource {
        LiveData<Lcee<User>> queryUserByUsername(String username);
    }
    

    UserRepository
    UserRepository 里面的返回值也要改

    public LiveData<Lcee<User>> getUser(String username) {
    ...
    }
    

    LocalUserDataSource

    然后先改本地数据源 LocalUserDataSource
    这里出现了一个 MediatorLiveData,这是什么鬼?
    它也是一个 LiveData,不过可以添加多个数据源
    我没听错吧?他自己不是数据源吗?
    是的,他可以观察其他数据。

    因为这样,我们就可以实现 LiveData<User> 转换为 LiveData<Lcee<User>>

    @Override
    public LiveData<Lcee<User>> queryUserByUsername(String username) {
        final MediatorLiveData<Lcee<User>> data = new MediatorLiveData<>();
        data.setValue(Lcee.<User>loading());
    
        data.addSource(userService.queryByUsername(username), new Observer<User>() {
            @Override
            public void onChanged(@Nullable User user) {
                if (null == user) {
                    data.setValue(Lcee.<User>empty());
                } else {
                    data.setValue(Lcee.content(user));
                }
            }
        });
        return data;
    }
    

    RemoteUserDataSource

    而远程数据源就不用转换了,只需要修改一下 LiveData 的类型。

    @Override
    public LiveData<Lcee<User>> queryUserByUsername(String username) {
        final MutableLiveData<Lcee<User>> data = new MutableLiveData<>();
        data.setValue(Lcee.<User>loading());
    
        userApi.queryUserByUsername(username)
                .enqueue(new Callback<User>() {
                    @Override
                    public void onResponse(Call<User> call, Response<User> response) {
                        User user = response.body();
                        if (null == user) {
                            data.setValue(Lcee.<User>empty());
                            return;
                        }
                        data.setValue(Lcee.content(user));
                        // update cache
                        LocalUserDataSource.getInstance().addUser(user);
                    }
    
                    @Override
                    public void onFailure(Call<User> call, Throwable t) {
                        t.printStackTrace();
                        data.setValue(Lcee.<User>error(t));
                    }
                });
        return data;
    }
    

    ViewModel

    因为数据变了,ViewModel 也得做小修改,实际上就是改了返回值

    public LiveData<Lcee<User>> getUser() {
    ...
    }
    

    View

    View 要根据数据渲染,现在数据改了, View 自然也要改。

    重新请求数据

    为了可以实现输入用户名然后进行重新请求数据,我们得不得加入编辑框和按钮,其实就是 左边输入框,右边按钮

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:orientation="horizontal">
    
        <EditText
            android:id="@+id/et_username"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:imeOptions="actionSearch"
            android:singleLine = "true"
            android:text="ittianyu" />
    
        <Button
            android:id="@+id/btn_search"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="search" />
    
    </LinearLayout>
    

    相应的,在 Activity 中初始化

    etUsername = (EditText) findViewById(R.id.et_username);
    

    为了方便取值,定义一个方法

    private String getUsername() {
        return etUsername.getText().toString();
    }
    

    还记得一开始说的,重新请求数据吗,现在可以调用这样一个方法就实现重新请求数据了。

    private void reload() {
        // reload
        userViewModel.reload(getUsername());
    }
    

    多状态视图

    XML
    为了显示4种状态视图,我们需要先定义
    把状态视图整合到一起后就是这样的。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:orientation="horizontal">
    
            <EditText
                android:id="@+id/et_username"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:imeOptions="actionSearch"
                android:singleLine = "true"
                android:text="ittianyu" />
    
            <Button
                android:id="@+id/btn_search"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="search" />
    
        </LinearLayout>
    
        <FrameLayout
            android:id="@+id/v_root"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <LinearLayout
                android:id="@+id/v_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:orientation="vertical">
    
                <TextView
                    android:id="@+id/tv_id"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center" />
    
                <TextView
                    android:id="@+id/tv_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center" />
    
            </LinearLayout>
    
            <FrameLayout
                android:id="@+id/v_error"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:gravity="center"
                    android:text="Network error, click to reload" />
    
            </FrameLayout>
    
            <FrameLayout
                android:id="@+id/v_empty"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:gravity="center"
                    android:text="User not exist" />
    
            </FrameLayout>
    
            <FrameLayout
                android:id="@+id/v_loading"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
                <ProgressBar
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center" />
    
            </FrameLayout>
    
        </FrameLayout>
    </LinearLayout>
    

    初始化View
    然后记得初始化

    vContent = findViewById(R.id.v_content);
    vError = findViewById(R.id.v_error);
    vLoading = findViewById(R.id.v_loading);
    vEmpty = findViewById(R.id.v_empty);
    

    修改观察的数据类型
    这里也要修改观察的数据类型

    private void initData() {
    ...
        userViewModel.getUser().observe(this, new Observer<Lcee<User>>() {
            @Override
            public void onChanged(@Nullable Lcee<User> data) {
                updateView(data);
            }
        });
    
        reload();
    }
    

    响应事件
    具体怎么渲染的等下再说,我们还需要处理一些用户点击事件。

    • 用户点击 错误/空 视图后,触发重新加载。
    • 点击搜索按钮/键盘回车后,隐藏键盘,触发重新加载
    private void initEvent() {
        View.OnClickListener reloadClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                hideKeyboard();
                reload();
            }
        };
        vError.setOnClickListener(reloadClickListener);
        vEmpty.setOnClickListener(reloadClickListener);
    
        findViewById(R.id.btn_search).setOnClickListener(reloadClickListener);
    
        etUsername.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_ENTER) {
                    hideKeyboard();
                    reload();
                    return true;
                }
                return false;
            }
        });
    }
    
    private void hideKeyboard() {
        ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
                .hideSoftInputFromWindow(UserActivity.this.getCurrentFocus().getWindowToken(),
                        InputMethodManager.HIDE_NOT_ALWAYS);
    }
    

    渲染
    接下来就是重点,渲染 view
    根据四种状态去渲染

    private void updateView(Lcee<User> lcee) {
        switch (lcee.status) {
            case Content: {
                showContent();
                tvId.setText(lcee.data.getId() + "");
                tvName.setText(lcee.data.getName());
                break;
            }
            case Empty: {
                showEmpty();
                break;
            }
            case Error: {
                showError();
                break;
            }
            case Loading: {
                showLoading();
                break;
            }
        }
    }
    

    同时为了方便调用,我们封装 4 个方法来切换 4 种视图

    private void showContent() {
        vContent.setVisibility(View.VISIBLE);
        vEmpty.setVisibility(View.GONE);
        vError.setVisibility(View.GONE);
        vLoading.setVisibility(View.GONE);
    }
    
    private void showEmpty() {
        vContent.setVisibility(View.GONE);
        vEmpty.setVisibility(View.VISIBLE);
        vError.setVisibility(View.GONE);
        vLoading.setVisibility(View.GONE);
    }
    
    private void showError() {
        vContent.setVisibility(View.GONE);
        vEmpty.setVisibility(View.GONE);
        vError.setVisibility(View.VISIBLE);
        vLoading.setVisibility(View.GONE);
    }
    
    private void showLoading() {
        vContent.setVisibility(View.GONE);
        vEmpty.setVisibility(View.GONE);
        vError.setVisibility(View.GONE);
        vLoading.setVisibility(View.VISIBLE);
    }
    

    总结

    到此为止,我们解决了一大难题。
    What?你已经掌握 MVVM 了?


    不存在的.jpg

    好戏才刚刚开始

    相关文章

      网友评论

          本文标题:《安卓-深入浅出MVVM教程》应用篇-04 State Lcee

          本文链接:https://www.haomeiwen.com/subject/xaizuxtx.html