• 初稿标题:[Approaching Android with MVVM — ribot labs — Medium]
  • 原文小编:[Joe Birch]
  • 译文出自:[丹佛掘金翻译布署]
  • 译者:[Sausure]
  • 校对者:[EthanWu (ethan-wu)]、[dodocat
    (Quanqi)]、[foolishgao]

Coding.jpg

DuangDuangDuang!在起来今日的丹佛掘金队(Denver Nuggets)干货分享前,稀土君要先插播一条广告,没错,那便是:

Coding 技术小馆第①站北京的移位,丹佛掘金(Denver Nuggets) CEO也是前者理事阴明会做一场有关 Vue.js
组件化的享用,同时其余的嘉宾有:Coding 前端高工刘辉分享《Redux
与 React 服务器端渲染》、AWS 化解方案架构师赵霏《用 Javascript
玩转云计算》、野狗联合创办人肖光宇分享《Web 前端的实时化》。

没错,你没看错,正是咱们显然可以靠脸吃饭却偏偏要用实力说话的阴明大大也会上台分享哦。他将做的是关于三个轻量级
MVVM 框架 Vue.js 组件化内容的享用,为了避防有个别同学还不知晓 MVVM
是何等,本期大家特地准备了那篇有关 MVVM 情势的介绍,希望能支援到您哦。

要么快点看文章吧。

-------------------

自身旁观了一段时间安卓的数目绑定类库,决定尝试下它的「Model-View-ViewModel」方式。因为本身一度和
@matto1990
协作开发过一款应用 HackerNews
Reader
,所以自身控制利用那种方式再一次达成它。

MVVM.png

那篇小说通过一款简单的 app 来论证 MVVM
方式,小编建议您先看看那么些[项目],让你差不离领悟下它。

什么是 MVVM 模式?

Model-View-ViewModel 正是将内部的 View
的意况和作为抽象化,让大家得以将 UI 和事情逻辑分开。当然这个工作
ViewModel 已经帮我们做了,它能够取出 Model 的数目同时援救处理
View 中由于必要出示内容而涉及的事体逻辑。

MVVM情势是透过以下多个为主零部件组成,各种都有它和谐万分的剧中人物:

  • Model-包涵了事情和认证逻辑的数据模型
  • View-定义荧屏中 View 的组织,布局和外观
  • ViewModel-扮演「View」和「Model」之间的大使,接济处理 View
    的任何工作逻辑

whatIsMVVM.png

那这和我们早就用过的 MVC 形式有怎么着分歧吧?以下是 MVC 的构造

  • ViewController 的顶端,而 ModelController
    的底部
  • Controller 需要同时关切 ViewModel
  • View 只好分晓 Model 的留存并且能在 Model 的值变更时收到通告

MVVM 形式和 MVC 某些类似,但有以下两样:

  • ViewModel 替换了 Controller,在 UI 层之下
  • ViewModelView 揭露它所急需的多少和指令对象
  • ViewModel 接收来自 Model 的数据

你能够看出那二种形式抱有相似的布局,但新加入的 ViewModel
是用差异的法子将零件们关系起来的,它是双向的,而 MVC 只可以单向连接。

席卷起来,MVVM 是由 MVC 发展而来-通过在 Model 之上而在 View
之下扩张3个非视觉的机件今后自 Model 的多寡映射到 View
中。接下来,我们将越多地察看 MVVM 的那种特征。

The Hacker News reader

正如前方提及过的,作者将自身原本的七个品种拆开为这篇小说服务。那款应用有以下几种特性:

  • 翻开帖子列表
  • 翻看单个帖子
  • 查看帖子下的评论和介绍
  • 翻看钦赐小编的帖子

作者们这么做是为着减小代码库的层面,尤其便于去打听那几个操作是如何实行的。上边包车型大巴图样能让您赶快理解它是怎么工作的:

howITWorks.png

左手的图纸展现的是帖子的列表,它也是那款应用的关键部分,接下去右侧的图样展示的是该帖子的评头品足列表,它和前者有一般的地方,但也有一对不一,大家将在末端看到。

显示帖子

comentsShow.png

各种帖子新闻都用 RecyclerView 所蕴藏的 CardView
包装起来,正如上海教室呈现的。

行使 MVVM 大家能够将不相同层抽象出来很好的落到实处这么些卡片,那意味着每种 MVVM
组件只要处理它被分配的义务即可。通过应用前边介绍的 MVVM
的不比组件,组合在一起后能协会出大家的帖子卡片实例,那么大家该怎么将它们从布局中抽离出来?

abstract.png

Model

一言以蔽之的话,Model 由那么些帖子的业务逻辑组成,包含部分像 id,name,text
之类的特性,以下代码呈现了此类的一部分代码:

public class Post {

public Long id;
public String by;
public Long time;
public ArrayList<Long> kids;
public String url;
public Long score;
public String title;
public String text;
@SerializedName("type")
public PostType postType;

public enum PostType {
    @SerializedName("story")
    STORY("story"),
    @SerializedName("ask")
    ASK("ask"),
    @SerializedName("job")
    JOB("job");

    private String string;

    PostType(String string) {
        this.string = string;
    }

    public static PostType fromString(String string) {
        if (string != null) {
            for (PostType postType : PostType.values()) {
                if (string.equalsIgnoreCase(postType.string)) return postType;
            }
        }
        return null;
    }
}

public Post() { }
}

为了可读性,上边的 POST 类中去掉了一部分 Parcelable 变量和艺术

那里你能够看看 Post 类只含有全数它的质量,没有一点其他逻辑 –
其余机件会处理它们。

View

View 的天职是概念布局,外观和布局。

View 最好能一心通过 XML 来定义,就算它包含些许 Java
代码也不该有工作逻辑部分。

View 会通过绑定从 ViewModel 中取出数据。在运作时,若
ViewModel 的品质的值有变动的话它会打招呼 View 来更新UI。

第贰,我们先给 RecyclerView
传入四个自定义的适配器。为此,我们供给让大家的 BindingHolder
类持有对 Binding 的引用。

public static class BindingHolder extends RecyclerView.ViewHolder {
private ItemPostBinding binding;
public BindingHolder(ItemPostBinding binding) {
    super(binding.cardView);
    this.binding = binding;
}
}

onBindViewHolder() 方法才是真的将 ViewModelView
绑定的地方。大家获得二个 ItemPostBinding 对象(它会被 item_post
布局自动生成),然后将新建的 PostViewModel 对象传给它的
ViewModel 引用。

ItemPostBinding postBinding =  holder.binding;
postBinding.setViewModel(new PostViewModel(mContext,
                         mPosts.get(position), mIsUserPosts));

上边便是总体的 PostAdaper 类:

public class PostAdapter extends RecyclerView.Adapter<PostAdapter.BindingHolder> {
private List<Post> mPosts;
private Context mContext;
private boolean mIsUserPosts;

public PostAdapter(Context context, boolean isUserPosts) {
    mContext = context;
    mIsUserPosts = isUserPosts;
    mPosts = new ArrayList<>();
}

@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    ItemPostBinding postBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.getContext()),
            R.layout.item_post,
            parent,
            false);
    return new BindingHolder(postBinding);
}

@Override
public void onBindViewHolder(BindingHolder holder, int position) {
    ItemPostBinding postBinding = holder.binding;
    postBinding.setViewModel(new PostViewModel(mContext, mPosts.get(position), mIsUserPosts));
}

@Override
public int getItemCount() {
    return mPosts.size();
}

public void setItems(List<Post> posts) {
    mPosts = posts;
    notifyDataSetChanged();
}

public void addItem(Post post) {
    mPosts.add(post);
    notifyDataSetChanged();
}

public static class BindingHolder extends RecyclerView.ViewHolder {
    private ItemPostBinding binding;

    public BindingHolder(ItemPostBinding binding) {
        super(binding.cardView);
        this.binding = binding;
    }
}
}

看下大家的XML布局,首先我们要将具有的布局都包含在layout标签下,同时选取data标签来声称大家的
ViewModel:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel" type="com.hitherejoe.mvvm_hackernews.viewModel.PostViewModel" /></data>
<!-- Other layout views -->
</layout>

声明 ViewModel 能够让我们在全方位布局中援引它,在 item_post
布局中大家会一再用到 ViewModel

androidText-你能够从 ViewModel
中引用相应的点子给文本视图设置情节。正如下边你所观望的
@{viewModel.postTitle},它从 ViewModel 中引用了
getPostTitle() 方法-它将赶回相应帖子的标题。

onClick-我们也得以引用单击事件到布局文件中。如你所阅览的,@{viewModel.onClickPost}
是指从 ViewModel 中引用 onClickPost()
方法-它将回来一个能处理单击事件的 OnClickListener 对象。

visibility – 控制去 comments activity
的入口,注重于该帖子是还是不是有相应的褒贬。通过检查 comments list
的长短来控制该 visibility 的值,这几个操作都以在 ViewModel
中形成的。在那边,大家引用了它的 getCommentsVisiblity()
方法来计量是不是该显示

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
    <variable name="viewModel" type="com.hitherejoe.mvvm_hackernews.viewModel.PostViewModel" />
</data>

<android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="2dp"
    android:layout_marginBottom="2dp"
    card_view:cardCornerRadius="2dp"
    card_view:cardUseCompatPadding="true">

    <LinearLayout
        android:id="@+id/container_post"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:orientation="vertical"
        android:onClick="@{viewModel.onClickPost}">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="16dp"
            android:background="@drawable/touchable_background_white">

            <TextView
                android:id="@+id/text_post_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:text="@{viewModel.postTitle}"
                android:textColor="@color/black_87pc"
                android:textSize="@dimen/text_large_title"
                android:onClick="@{viewModel.onClickPost}"/>

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <TextView
                    android:id="@+id/text_post_points"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentLeft="true"
                    android:text="@{viewModel.postScore}"
                    android:textSize="@dimen/text_body"
                    android:textColor="@color/hn_orange" />

                <TextView
                    android:id="@+id/text_post_author"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@+id/text_post_points"
                    android:text="@{viewModel.postAuthor}"
                    android:textColor="@color/black_87pc"
                    android:textSize="@dimen/text_body"
                    android:bufferType="spannable"
                    android:onClick="@{viewModel.onClickAuthor}"/>

            </RelativeLayout>

        </LinearLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@color/light_grey" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:background="@color/white">

            <TextView
                android:id="@+id/text_view_post"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="16dp"
                android:background="@drawable/touchable_background_white"
                android:clickable="true"
                android:textColor="@color/black"
                android:textSize="@dimen/text_small_body"
                android:textStyle="bold"
                android:text="@string/view_button"
                android:onClick="@{viewModel.onClickPost}"/>

            <TextView
                android:id="@+id/text_view_comments"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="16dp"
                android:background="@drawable/touchable_background_white"
                android:clickable="true"
                android:textColor="@color/hn_orange"
                android:textSize="@dimen/text_small_body"
                android:text="@string/comments_button"
                android:onClick="@{viewModel.onClickComments}"
                android:visibility="@{viewModel.commentsVisibility}"/>

        </LinearLayout>

    </LinearLayout>

</android.support.v7.widget.CardView>

</layout>

如此那般狠抓在太棒了,大家能抽象出展现逻辑到大家的布局文件中,让我们的
ViewModel 来关怀它们。

ViewModel

ViewModel 扮演了 ViewModel
之间使者的剧中人物,让它来关切全数关乎到 View 的事务逻辑,同时它能够访问
Model 的格局和品质,那几个最后会功用到 View 中。通过
ViewModel,能够移除原本要求在其余组件中回到或处理的多少。

在这里,[PostViewModel] 用 Post 对象来拍卖 CardView
必要体现的剧情,在底下的类中,你能够看到一文山会海的形式,种种方法对最终效果于大家的帖子视图。

  • getPostTitle()-通过 Post 对象回来二个帖子的标题
  • getPostAuthor()-那一个点子首先会从利用的resources中赢得相应-
    的字符串,然后传入Post对象的author天性对它举办格式化,假如isUserPosts
    等于true大家就供给出席下划线,最终回到该字符串。
  • getCommentsVisibility()-该方法决定是不是出示有关评论的TextView
    onClickPost()-该格局重回相应View要求的- OnClickListener

那些事例证明分裂的事情逻辑都有大家的 ViewModel 来处理。上面正是咱们PostViewModel 类的完整代码以及那多少个被 item_post 布局引用的法门。

public class PostViewModel extends BaseObservable {

private Context context;
private Post post;
private Boolean isUserPosts;

public PostViewModel(Context context, Post post, boolean isUserPosts) {
    this.context = context;
    this.post = post;
    this.isUserPosts = isUserPosts;
}

public String getPostScore() {
    return String.valueOf(post.score) + context.getString(R.string.story_points);
}

public String getPostTitle() {
    return post.title;
}

public Spannable getPostAuthor() {
    String author = context.getString(R.string.text_post_author, post.by);
    SpannableString content = new SpannableString(author);
    int index = author.indexOf(post.by);
    if (!isUserPosts) content.setSpan(new UnderlineSpan(), index, post.by.length() + index, 0);
    return content;
}

public int getCommentsVisibility() {
    return  post.postType == Post.PostType.STORY && post.kids == null ? View.GONE : View.VISIBLE;
}

public View.OnClickListener onClickPost() {
    return new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Post.PostType postType = post.postType;
            if (postType == Post.PostType.JOB || postType == Post.PostType.STORY) {
                launchStoryActivity();
            } else if (postType == Post.PostType.ASK) {
                launchCommentsActivity();
            }
        }
    };
}

public View.OnClickListener onClickAuthor() {
    return new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            context.startActivity(UserActivity.getStartIntent(context, post.by));
        }
    };
}

public View.OnClickListener onClickComments() {
    return new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            launchCommentsActivity();
        }
    };
}

private void launchStoryActivity() {
    context.startActivity(ViewStoryActivity.getStartIntent(context, post));
}

private void launchCommentsActivity() {
    context.startActivity(CommentsActivity.getStartIntent(context, post));
}
}

是或不是很爽?正如您看看的,大家的 PostViewModel 关注之下地点:

  • 维护 Post 对象的质量,最终会在 View 中展示
  • 对这一个属性举行对应的格式化
  • 通过 onclick 属性给相应的views对提供点击事件的协助
  • 通过 Post 对象的属性处理有关 views 的突显

测试 ViewModel

动用 MVVM 的第一次全国代表大会利益是大家得以很简单对 ViewModel 进行单元测试。在
PostViewModel 中,能够写些简单的测试方法来证实大家的 ViewModel
是还是不是科学贯彻。

shouldGetPostScore()-测试getPostScore()方法,确认该帖子的得分是还是不是科学地格式化成字符串对象并赶回。
shouldGetPostTitle()-测试getPostTitle()方法,确认该帖子的标题被正确重回。
shouldGetPostAuthor()-测试getPostAuthor()方法,确认重返的帖子的作者被正确地格式化了
shouldGetCommentsVisiblity()-测试getCommentsVisibility()方法是不是正确再次来到了visibility属性的值,它将会用在帖子的
Comments
按钮中。大家传入二个分包不相同意况的ArrayLists来承认它是不是能正确再次来到。

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = DefaultConfig.EMULATE_SDK, manifest = DefaultConfig.MANIFEST)
public class PostViewModelTest {

private Context mContext;
private PostViewModel mPostViewModel;
private Post mPost;

@Before
public void setUp() {
    mContext = RuntimeEnvironment.application;
    mPost = MockModelsUtil.createMockStory();
    mPostViewModel = new PostViewModel(mContext, mPost, false);
}

@Test
public void shouldGetPostScore() throws Exception {
    String postScore = mPost.score + mContext.getResources().getString(R.string.story_points);
    assertEquals(mPostViewModel.getPostScore(), postScore);
}

@Test
public void shouldGetPostTitle() throws Exception {
    assertEquals(mPostViewModel.getPostTitle(), mPost.title);
}

@Test
public void shouldGetPostAuthor() throws Exception {
    String author = mContext.getString(R.string.text_post_author, mPost.by);
    assertEquals(mPostViewModel.getPostAuthor().toString(), author);
}

@Test
public void shouldGetCommentsVisibility() throws Exception {
    // Our mock post is of the type story, so this should return gone
    mPost.kids = null;
    assertEquals(mPostViewModel.getCommentsVisibility(), View.GONE);
    mPost.kids = new ArrayList<>();
    assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE);
    mPost.kids = null;
    mPost.postType = Post.PostType.ASK;
    assertEquals(mPostViewModel.getCommentsVisibility(), View.VISIBLE);
}
}

今后大家得以知道的 ViewModel 已经正确工作了!!

评论

完结评论的艺术和前面很像但依然略微不相同。

有八个例外的 ViewModel 被用来操作这一次评论,
[CommentHeaderViewModel] 和
[CommentViewModel]。正如你在[CommentAdapter]中来看的,大家的
View 有二种的两样类型:

private static final int VIEW_TYPE_COMMENT = 0;
private static final int VIEW_TYPE_HEADER = 1;

要是该帖子是三个发问的帖子,我们将在显示器的顶端突显一个头顶,它显示所问的难点-接着评论会符合规律显示在底下。同时你应该会注意到在
onCreateViewHolder() 中我们会因此判断 VIEW_TYPE
来加载不相同的布局,它会回来三种不一样布局中的当中一种。

if (viewType == _VIEW_TYPE_HEADER_) {
ItemCommentsHeaderBinding commentsHeaderBinding =
DataBindingUtil._inflate_(
        LayoutInflater._from_(parent.getContext()),
        R.layout._item_comments_header_,
        parent,
        false);
return new BindingHolder(commentsHeaderBinding);
} else {
ItemCommentBinding commentBinding =
    DataBindingUtil._inflate_(
        LayoutInflater._from_(parent.getContext()),
        R.layout._item_comment_,
        parent,
        false);
return new BindingHolder(commentBinding);
}

接着在大家的 onBindViewHolder()
方法中我们会基于区其他视图类型来创建绑定。那是因为不相同的 ViewModel
对尾部有差异的处理形式

if (getItemViewType(position) == _VIEW_TYPE_HEADER_) {
ItemCommentsHeaderBinding commentsHeaderBinding =
                    (ItemCommentsHeaderBinding) holder.binding;
commentsHeaderBinding.setViewModel(new
                      CommentHeaderViewModel(mContext, mPost));
} else {
int actualPosition = (postHasText()) ? position - 1 : position;
ItemCommentBinding commentsBinding =
                           (ItemCommentBinding) holder.binding;
mComments.get(actualPosition).isTopLevelComment =
                                           actualPosition == 0;
commentsBinding.setViewModel(new CommentViewModel(
                     mContext, mComments.get(actualPosition)));
}

那便是它们的差别点,评论部分有多个分化的 ViewModel 类型 —
取决于该帖子是还是不是是发问类的帖子。

总结

一经没错使用,数据绑定类库或许会转移大家开发应用的方法。当然,还有其余措施实现数据的绑定,使用
MVVM 形式只是里面包车型地铁一种途径。

诸如,你能够在布局中引用我们的 Model
然后通过它的变量引用直接待上访问它的习性:

<data>
<variable name="post" type="your.package.name.model.Post"/>
</data>
<TextView
...
android:text="@{post.title}"/>

并且大家得以很简单从adapers和classes中移除一些基础的来得逻辑。上面有种很前卫的主意达成我们那种须要:

<data>
<import type="android.view.View"/>
</data>
<TextView
...
android:visibility="@{post.hasComments ? View.Visible :
View.Gone}"/>

funny.gif

那便是本身看齐上边完毕情势的神色!

自笔者以为这是多少绑定类库中倒霉的地方,它将 View 的呈现逻辑包涵到了
View
中。不仅会导致杂乱,也让大家的测试和调节和测试变的尤为艰苦,因为它将逻辑和布局混淆在一道。

本来,认定MVVM是支付应用的不易方法还为前卫早,但这一次尝试也让自身有时机见识到以往项指标一种倾向。借使您想阅读更多关于数据绑定类库的作品,你能够看[这里]。同时微软也有一篇关于MVVM通俗易懂的[文章]。

本身很情愿听听你们想法,假若你们有其余的视角和提出能够每30日发 Tweet
和本身谈谈!


什么样,看完全小学说是否持有收获了吗?假使你还想获取越多更幽默的干货,不如点击这里,报名本月十号在日本东京设立的
Coding 线下前端活动,听阴明大大的分享呢。

相关文章

网站地图xml地图