如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (上)

安卓(Android)平台上的MVVM架构:关键知识点、优劣分析及实践示例 本文旨在探讨安卓平台上广泛应用的Model-View-ViewModel(MVVM)架构模式的核心概念、主要优点与潜在不足,并通过实际示例代码阐明其在实际项目中的应用方式。MVVM作为一款推动关注点分离和提高软件质量的架构方案,在安卓应用开发中起着至关重要的作用。 阅读详情

前言


本章我们基于重构的方式进行一个 MVVM 的实战,我们将一个新闻列表的普通实现,一步一步的改造成 MVVM 的架构模式,一共分为上中下三个章节;

传统方式实现


首先咱们来看具体实现的最终效果,就是一个新闻列表页面,拉取新闻标题和新闻列表;

整体页面的构建也很简单,就是 TabLayout + ViewPager + Fragment + RecyclerView + DataBinding 来搭建页面,然后通过 RxJava + Retrofit 拉取数据,并通过 adapter 展示到页面;

image.png

页面的构建并不复杂,整体的一个布局排列如上图所示;外层 Fragment 作为根 View 来包裹了一个 TabLayout 和 ViewPager 用来显示新闻标题和标题内容列表;列表以 RecyclerView 进行数据的渲染,整体实现如下:

image.png

新闻列表页面的构建也比较简单,就是 Adapter 适配两种不同类型(一种是纯文本、一种是带图片带文本)的 item,通过 ViewHolder 创建不同的布局 View;

public class NewsListRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private final int VIEW_TYPE_PICTURE_TITLE = 1;
    private final int VIEW_TYPE_TITLE = 2;
    private List<NewsListBean.Contentlist> mItems;
    private Context mContext;

    NewsListRecyclerViewAdapter(Context context) {
        mContext = context;
    }

    void setData(List<NewsListBean.Contentlist> items) {
        mItems = items;
        notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
        if (mItems != null) {
            return mItems.size();
        }
        return 0;
    }

    @Override
    public int getItemViewType(int position) {
        if (mItems != null && mItems.get(position).imageurls != null && mItems.get(position).imageurls.size() > 1) {
            return VIEW_TYPE_PICTURE_TITLE;
        }
        return VIEW_TYPE_TITLE;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view;
        if (viewType == VIEW_TYPE_PICTURE_TITLE) {
            view = LayoutInflater.from(mContext).inflate(R.layout.picture_title_view, parent, false);
            return new PictureTitleViewHolder(view);
        } else if (viewType == VIEW_TYPE_TITLE) {
            view = LayoutInflater.from(mContext).inflate(R.layout.title_view, parent, false);
            return new TitleViewHolder(view);
        }

        return null;
    }

    private class PictureTitleViewHolder extends RecyclerView.ViewHolder {
        public TextView titleTextView;
        public AppCompatImageView picutureImageView;

        public PictureTitleViewHolder(@NonNull View itemView) {
            super(itemView);
            titleTextView = itemView.findViewById(R.id.item_title);
            picutureImageView = itemView.findViewById(R.id.item_image);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    WebviewActivity.startCommonWeb(mContext, "News", v.getTag()+"");
                }
            });
        }
    }


    private class TitleViewHolder extends RecyclerView.ViewHolder {
        public TextView titleTextView;

        public TitleViewHolder(@NonNull View itemView) {
            super(itemView);
            titleTextView = itemView.findViewById(R.id.item_title);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    WebviewActivity.startCommonWeb(mContext, "News", v.getTag()+"");
                }
            });
        }
    }
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        holder.itemView.setTag(mItems.get(position).link);
        if(holder instanceof PictureTitleViewHolder){
            ((PictureTitleViewHolder) holder).titleTextView.setText(mItems.get(position).title);
            Glide.with(holder.itemView.getContext())
                    .load(mItems.get(position).imageurls.get(0).url)
                    .transition(withCrossFade())
                    .into(((PictureTitleViewHolder) holder).picutureImageView);
        } else if(holder instanceof TitleViewHolder) {
            ((TitleViewHolder) holder).titleTextView.setText(mItems.get(position).title);
        }
    }
}

传统方式,我们会在 Adapter 中写如静态 ViewHolder,但这其实是违背六大原则中的单一职责的,我们来一步一步的拆解成 MVVM 的模式;

我们在搭建一个 App 的架构的时候,追求模块化、层次化、控件化;

控件化的意思就是自定义 View,自定义 View 有什么好处呢?可以将 ViewHolder 的创建放到这个自定义 View 中,满足了单一职责,所有 View 相关的操作都在这个自定义 View,剥离了这个 adapter,

我们先来改造 ViewHolder 的剥离;

ViewHolder 重构

我们要将 ViewHolder 从 adapter 中剥离出来,这里采用控件化的方式,将两个 ViewHolder 剥离到自定义 View 中,我们在目录下新建 view 层级;

image.png

然后创建两个自定义 View,一个是 TitleView,一个是 PictureTitleView;

public class TitleView extends LinearLayout {
    
    public TitleView(Context context) {
        super(context);
    }
}

PictureTitleView

public class PictureTitleView extends LinearLayout {
    public PictureTitleView(Context context) {
        super(context);
    }
}

然后创建对应的 xml 布局 title_view.xml 和 picture_title_view.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.view.View" />
        <import type="android.text.TextUtils" />
        <import type="androidx.databinding.ObservableField" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:orientation="vertical">

        <TextView
            android:layout_margin="16dp"
            android:id="@+id/item_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="2"
            android:gravity="center_vertical"
            android:textColor="#303030"
            android:textSize="16sp"
            android:textStyle="bold"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="#303030" />
    </LinearLayout>
</layout>

picture_title_view.xml

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

    <data>
        <import type="android.view.View" />
        <import type="android.text.TextUtils" />
        <import type="androidx.databinding.ObservableField" />
    </data>

    <LinearLayout xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:padding="16dp">

            <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:card_view="http://schemas.android.com/apk/res-auto"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                card_view:cardBackgroundColor="@android:color/transparent"
                card_view:cardCornerRadius="5dp"
                android:layout_marginRight="10dp"
                card_view:cardElevation="0dp"
                card_view:contentPadding="0dp">

                <ImageView
                    android:id="@+id/item_image"
                    android:layout_width="100dp"
                    android:layout_height="70dp"
                    android:scaleType="fitXY" />
            </androidx.cardview.widget.CardView>

            <TextView
                android:id="@+id/item_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:ellipsize="end"
                android:gravity="center_vertical"
                android:maxLines="2"
                android:textColor="#303030"
                android:textSize="16sp"
                android:textStyle="bold"
                tools:text="myFileName.java" />

        </LinearLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="#303030" />
    </LinearLayout>
</layout>

我们现在将布局加载到这两个自定义 view 中,通过 inflater 的方式

public class TitleView extends LinearLayout {

    TitleViewBinding mBinding;

    public TitleView(Context context) {
        super(context);

        init();
    }

    private void init() {
        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mBinding = DataBindingUtil.inflate(inflater, R.layout.title_view, this, false);
        mBinding.getRoot().setOnClickListener(view -> {
            // 处理点击跳转逻辑         
        });
        addView(mBinding.getRoot());
    }
}

PictureTitleView 同一样的实现方式;View 创建好了,我们接下来需要绑定数据,分别创建对应的 Model

public class TitleViewModel {

    /**
     * 文章标题
     */
    String title;
    /**
     * 文章跳转url
     */
    String jumpUrl;
}

model 创建之后,我们需要通过 DataBinding 进行与 view 的绑定;

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.view.View" />
        <import type="android.text.TextUtils" />
        <import type="androidx.databinding.ObservableField" />

        <variable
            name="titleViewModel"
            type="com.xiangxue.news.homefragment.newslist.views.titleview.TitleViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:orientation="vertical">

        <TextView
            android:layout_margin="16dp"
            android:id="@+id/item_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="2"
            android:text="@{titleViewModel.title}"
            android:gravity="center_vertical"
            android:textColor="#303030"
            android:textSize="16sp"
            android:textStyle="bold"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="#303030" />
    </LinearLayout>
</layout>

在 xml 中通过 DataBinding 给 TextView 的 android:text=“@{titleViewModel.title}” 就完成了view 获取数据的操作,然后我们需要将给 TitleViewModel 赋值,在 TitleView 中从提供一个 setData 的接口来完成 TitleViewModel 的赋值操作

public void setData(TitleViewModel model) {
    mBinding.setTitleViewModel(model);
    // 数据更新,让其更新到 View(xml) 上
    mBinding.executePendingBindings();
}

到这里,我们就可以替换 Adapter 中 onCreateViewHolder 的逻辑;但是我们在替换的时候发现:

image.png

RecyclerView.ViewHolder 不能被实例化,那么怎么办呢?我们来搞一个 BaseViewHolder

public class BaseViewHolder extends RecyclerView.ViewHolder {
    private View itemView;
    public BaseViewHolder(@NonNull View itemView) {
        super(itemView);
        this.itemView = itemView;
    }
}

然后,我们在 adapter 中进行替换;

public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view;
    if (viewType == VIEW_TYPE_PICTURE_TITLE) {
        return new BaseViewHolder(new PictureTitleView(parent.getContext()));
    } else if (viewType == VIEW_TYPE_TITLE) {
        return new BaseViewHolder(new TitleView(parent.getContext()));
    }
    return null;
}

这样我们就可以把 adapter 中 ViewHolder 创建的逻辑也去掉了,但是删除之后,我们发现 onBindViewHolder 报错了,我们来处理下

image.png

我们将 ViewHolder 替换成 holder.itemView,并构造 TitleViewModel,然后 set 给 TitleView;

if(holder.itemView instanceof TitleView) {
    TitleViewModel titleViewModel = new TitleViewModel();
    titleViewModel.title = mItems.get(position).title;
    titleViewModel.jumpUrl = mItems.get(position).link;
    ((TitleView) holder.itemView).setData(titleViewModel);
}

同理,PictureTitleView 也是这样处理,但是 PictureTitleView 的图片这块要特殊处理下:

xml 中通过自定义命名空间引入一个自定义方法

<ImageView
    android:id="@+id/item_image"
    android:layout_width="100dp"
    android:layout_height="70dp"
    app:loadImageUrl="@{pictureTitleViewModel.imgUrl}"
    android:scaleType="fitXY" />

然后,通过 @BindingAdapter 注解来和这个方法进行绑定

@BindingAdapter("loadImageUrl")
public static void loadImageUrl(ImageView imageView, String imgUrl) {
    Glide.with(imageView.getContext())
            .load(imgUrl)
            .transition(withCrossFade())
            .into(imageView);
}

onBindViewHolder 中的 PictureViewHolder 改造如下:

if(holder.itemView instanceof PictureTitleView) {
    PictureTitleViewModel pictureTitleViewModel = new PictureTitleViewModel();
    pictureTitleViewModel.title = mItems.get(position).title;
    pictureTitleViewModel.jumpUrl = mItems.get(position).link;
    pictureTitleViewModel.imgUrl = mItems.get(position).imageurls.get(0).url;
    ((PictureTitleView) holder.itemView).setData(pictureTitleViewModel);
}

改造后的 onCreateViewHolder 和 onBindViewHolder 如下:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view;
    if (viewType == VIEW_TYPE_PICTURE_TITLE) {
        return new BaseViewHolder(new PictureTitleView(parent.getContext()));
    } else if (viewType == VIEW_TYPE_TITLE) {
        return new BaseViewHolder(new TitleView(parent.getContext()));
    }
    return null;
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    holder.itemView.setTag(mItems.get(position).link);
    if(holder.itemView instanceof PictureTitleView) {
        PictureTitleViewModel pictureTitleViewModel = new PictureTitleViewModel();
        pictureTitleViewModel.title = mItems.get(position).title;
        pictureTitleViewModel.jumpUrl = mItems.get(position).link;
        pictureTitleViewModel.imgUrl = mItems.get(position).imageurls.get(0).url;
        ((PictureTitleView) holder.itemView).setData(pictureTitleViewModel);
    } else if(holder.itemView instanceof TitleView) {
        TitleViewModel titleViewModel = new TitleViewModel();
        titleViewModel.title = mItems.get(position).title;
        titleViewModel.jumpUrl = mItems.get(position).link;
        ((TitleView) holder.itemView).setData(titleViewModel);
    }
}

可以看到,清爽了很多;

但是,估计好多人就有疑问了,数据的处理为什么放在了这个 adapter 层,感觉不符合设计规则,是的,我们来进行数据处理的重构;

Model 重构

数据的加载逻辑,我们放在了 NewListFragment 中,并且 TitleViewModel 和 PictureTitleViewModel 有重复的地方,我们可以抽取到一个 BaseViewModel 中;

public class BaseViewModel {
    /**
     * 文章标题
     */
    public String title;
    public String jumpUrl;
}

然后 TitleViewModel 和 PictureTitleViewModel 都继承 BaseViewModel

public class TitleViewModel extends BaseViewModel {
}
public class PictureTitleViewModel extends BaseViewModel {
    public String imgUrl;
}

而 NewListFragment 中的 load 方法改造如下:

protected void load() {
    TecentNetworkApi.getService(NewsApiInterface.class)
            .getNewsList(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),
                    getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME), String.valueOf(mPage))
            .subscribeOn(Schedulers.io())
            .flatMap(new Function<NewsListBean, ObservableSource<ArrayList<BaseViewModel>>>() {
                @Override
                public ObservableSource<ArrayList<BaseViewModel>> apply(NewsListBean newsChannelsBean) throws Exception {
                    if (mPage == 0) {
                        contentlist.clear();
                    }
                    ArrayList<BaseViewModel> viewModels = new ArrayList<>();
                    for (NewsListBean.Contentlist contentlist : newsChannelsBean.showapiResBody.pagebean.contentlist) {
                        if (contentlist.imageurls != null && contentlist.imageurls.size() > 0) {
                            PictureTitleViewModel pictureTitleViewModel = new PictureTitleViewModel();
                            pictureTitleViewModel.imgUrl = contentlist.imageurls.get(0).url;
                            pictureTitleViewModel.title = contentlist.title;
                            pictureTitleViewModel.jumpUrl = contentlist.link;
                            viewModels.add(pictureTitleViewModel);
                        } else {
                            TitleViewModel titleViewModel = new TitleViewModel();
                            titleViewModel.title = contentlist.title;
                            titleViewModel.jumpUrl = contentlist.link;
                            viewModels.add(titleViewModel);
                        }
                    }
                    return Observable.just(viewModels);
                }
            })
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<ArrayList<BaseViewModel>>() {
                @Override
                public void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {
                    contentlist = baseViewModels;
                    mAdapter.setData(contentlist);
                    mPage ++;
                    viewDataBinding.refreshLayout.finishRefresh();
                    viewDataBinding.refreshLayout.finishLoadMore();
                }
            });
}

adatper 中,将 data 格式改造成 ArrayList BaseViewModel> 整体改造如下:

public class NewsListRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private final int VIEW_TYPE_PICTURE_TITLE = 1;
    private final int VIEW_TYPE_TITLE = 2;
    private List<BaseViewModel> mItems;
    private Context mContext;

    NewsListRecyclerViewAdapter(Context context) {
        mContext = context;
    }

    void setData(List<BaseViewModel> items) {
        mItems = items;
        notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
        if (mItems != null) {
            return mItems.size();
        }
        return 0;
    }

    @Override
    public int getItemViewType(int position) {
        if (mItems != null && mItems.get(position) instanceof PictureTitleViewModel) {
            return VIEW_TYPE_PICTURE_TITLE;
        }
        return VIEW_TYPE_TITLE;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view;
        if (viewType == VIEW_TYPE_PICTURE_TITLE) {
            return new BaseViewHolder(new PictureTitleView(parent.getContext()));
        } else if (viewType == VIEW_TYPE_TITLE) {
            return new BaseViewHolder(new TitleView(parent.getContext()));
        }
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if(holder.itemView instanceof PictureTitleView) {
            ((PictureTitleView) holder.itemView).setData((PictureTitleViewModel)mItems.get(position));
        } else if(holder.itemView instanceof TitleView) {
            TitleViewModel titleViewModel = new TitleViewModel();
            ((TitleView) holder.itemView).setData((TitleViewModel)mItems.get(position));
        }
    }
}

看到这的时候,好多人可能会继续发出疑问了,都是 setData 那是不是也可以合并成一行代码呢?答案是可以的,我们需要一个 BaseView

public interface BaseView<DATA extends BaseViewModel> {
    void setData(DATA data);
}

我们所有的 View 中都有 setData 方法,所以这个方法我们抽取到 base 层, TitleView 和 PictureTitleView 都实现这个接口;

public class TitleView extends LinearLayout implements BaseView<TitleViewModel> {}
public class PictureTitleView extends LinearLayout implements BaseView<PictureTitleViewModel> {}

然后,我们的 Adapter 改造如下:

public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    if(holder.itemView instanceof PictureTitleView) {
        ((BaseView<PictureTitleViewModel>) holder.itemView).setData((PictureTitleViewModel)mItems.get(position));
    } else if(holder.itemView instanceof TitleView) {
        TitleViewModel titleViewModel = new TitleViewModel();
        ((BaseView<TitleViewModel>) holder.itemView).setData((TitleViewModel)mItems.get(position));
    }
}

到这的时候,可以看到我们其实可以在我们的 BaseViewHolder 中进行改造了,

public class BaseViewHolder extends RecyclerView.ViewHolder {
    private BaseView<BaseViewModel> itemView;
    public BaseViewHolder(@NonNull BaseView<BaseViewModel> itemView) {
        super((View) itemView);
        this.itemView = itemView;
    }
    
    public void bind(BaseViewModel data) {
        itemView.setData(data);
    }
}

然后,我们的 onBindViewHolder 改造如下:

public class NewsListRecyclerViewAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
        if (viewType == VIEW_TYPE_PICTURE_TITLE) {
            return new BaseViewHolder(new PictureTitleView(parent.getContext()));
        } else if (viewType == VIEW_TYPE_TITLE) {
            return new BaseViewHolder(new TitleView(parent.getContext()));
        }
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
        holder.bind(mItems.get(position));
    }
}

我们的 adapter 变得更清爽了一步~

好了,上篇文章就介绍到这里吧~

下一章预告


MVVM 实战新闻客户端(中)

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~~

字节跳动Android客户端实习 3+1 面经,内部面试官透露通关秘籍 身边很多朋友知道我在字节担任面试官,写文章的初衷之一也是希望能多一个为团队网络人才的渠道。但是一段时间下来发现真正找我咨询、内推的人并不多,不少同学对字节的面试有所忌惮,总担心准备不够。 实际上字节特别是客户端的面试没有网上传说的那么可怕。碍于公司制度,我不能把我面试别人的具体内容写出来,但是可以分享一些候选人自己写的面经供大家参考。 比如今天分享的这个面经,内容靠谱,能够反映出抖音客户端面试的难度。当然面试内容是因人而异的,所以主要是帮大家了解面试范围和难度,无需拘泥于具体题目 背景介绍 我是一名本.. 阅读详情

相关推荐

【面经分享】字节抖音Android客户端3+1轮已通过

不少同学对字节的面试有所忌惮,不愿轻易尝试。其实字节尤其是客户端的面试并没有大家想象的那么难,今天分享一篇面经,内容真实,希望帮助大家更好地了解字节客户端面试的出题范围和难度。 我是一名本科应届生,学校专业是软件开发。距离毕业之期不到半年,学校也在催促我们早点出校实习,年前通过一名学长内推去了字节跳动的抖音面试。12 月 31 号投的简历,1 月 6 号收到 HR 电话。隔这么长时间还是挺害怕的,以为简历都被筛掉了。 下面是我在字节抖音的面经分享,一二面和 HR 面都是凭印象回忆的,可能会漏掉一些问题。

chenjingsb的博客 2364

Android端MVVM从入门到实战(第一篇) - MVVM和四大官方组件

MVVM是相对于MVC和MVP的一个概念,是一种架构模式。这篇文章主要介绍MVVM本身的特点以及AndroidMVVM相关的四个组件——LifeCycle、ViewModel、LiveData和DataBinding,并完成一个简单的MVVM小项目。

wccadab的博客 8043

Android统一开发标准深度解析:从MVVM架构到工程化实践

在移动开发领域,架构模式是构建可维护、可扩展应用的核心基础。MVVM(Model-View-ViewModel)作为一种主流架构,通过数据绑定和生命周期感知机制,实现了UI逻辑与业务逻辑的清晰分离,其原理在于利用观察者模式确保数据状态与界面呈现的一致性。这种架构的技术价值在于显著提升了代码的可测试性和团队协作效率,是应对复杂业务场景的工程化基石。在实际应用场景中,结合Android Jetpack组件库(如ViewModel、LiveData/StateFlow)和统一的代码规范,能够有效解决项目碎片化与维

weixin_33802505的博客 422

Android架构组件:MVVM模式的实战应用

MVVM模式是一种将用户界面与业务逻辑和数据模型分离的设计模式,它通过将应用程序分为三个主要部分——Model(模型)、View(视图)和ViewModel(视图模型)——来实现这一目标。在Android开发中,这种模式尤其有用,因为它能够帮助开发者构建更加清晰、模块化和易于测试的应用程序。Model(模型)负责表示数据以及定义操作数据的业务逻辑。通常包含数据访问逻辑,如从数据库或网络获取数据。不关心数据如何被显示或在哪里被显示。View(视图)

Dingdangr的博客 1269

Android 开发网易面试凉凉经,面试官:基础不牢,技术不够深入,无缘offer

前言 网易的面试结果已经出来好几天了,一直拖着不是很想写面经,反正这会儿闲着无聊,又总是要写的(一来呢是当做一种记录吧,二来呢留给自己和需要的人看,好有个方向)就这会儿写了吧... 首先还是感谢吧,感谢网易面试官的肯定和建议。 网易一共经历了四轮面试,前三轮技术面,第四轮总监面。二面完了之后,HR 邀约去杭州参加现场面的,但是因为距离原因拒绝了,后来改成视频面,说实话,我现在有点后悔当初的决定了... 俗话说,见面三分情嘛哈哈哈哈 网易一面(28 min) 自我介绍 项目经历(老老实实一五一

weixin_44339238的博客 1811

基于Kotlin与MVVM的校园零食配送Android应用开发实战

在移动应用开发中,架构设计与数据持久化是构建稳定产品的基石。MVVM模式通过分离视图与业务逻辑,配合LiveData和协程,可有效提升响应式编程体验;Room作为Jetpack官方数据库组件,以注解和Flow驱动本地存储,实现UI自动刷新。Retrofit与OkHttp的网络层封装则统一了接口调用与异常处理,为离线缓存策略提供支撑。从通用技术原理出发,结合校园封闭场景下的即时配送需求,本文以校园零食配送APP为例,完整拆解商品浏览、购物车、订单流转、配送员接单等功能的实现过程,涵盖Kotlin语言实践、Re

weixin_33519829的博客 357

mvvm在android上的实践

首先我们来看一下MVVM的架构模式: MVVM(Model-View-ViewModel)是一种软件架构模式,用于设计和组织用户界面(UI)驱动的应用程序。它由三个主要组件组成:

yihanss的博客 721

Android MVVM开发模式实践

MVVM模式相关概念 MVC:(VIew-Model-Controller)早期将VIew、Model、Controller代码块进行划分,使得程序大部分分离,降低耦合。 MVP:(VIew-Model-Presenter)由于MVC中View和Model之间的依赖太强,导致Activity中的代码过于臃肿。为了他们可以绝对独立的存在,慢慢演化出了MVP。在MVP中View并不直接使用Model,它...

学无止境 2398

如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (下)

本章我们基于重构的方式进行一个 MVVM 的实战,我们将一个新闻列表的普通实现,一步一步的改造成 MVVM 的架构模式,一共分为上中下三个章节,本章继续上一章,开始下篇的讲解;

IT_Android的博客 959

Android面试指南与参考答案——基于领星安卓开发职位要求

本文基于领星安卓开发职位要求,整理Android开发岗位常见面试问题与参考答案。核心内容包括:1.基础知识:Android四层架构、SDK组件及API使用;2.编程语言:Kotlin优势、Jetpack组件应用及MVVM/MVP架构对比;3.开发工具:性能优化方法及版本适配策略;4.实战经验:第三方SDK集成和APP发布流程。针对1-3年经验者及应届生,提供代码示例和项目实例说明,覆盖框架搭建、性能优化等关键技术点,帮助候选人系统准备面试,突出技术能力和问题解决思维。

郑伟强dev的专栏 1193

Android新闻App开发实战:从架构设计到性能优化的完整项目指南

在移动应用开发领域,Android客户端开发是核心技术方向之一,其核心在于构建高效、可维护的应用架构。现代Android开发通常采用分层架构思想,结合Jetpack组件库实现数据驱动UI的响应式编程。通过ViewModel管理界面数据、LiveData实现数据观察,开发者能够有效处理配置变更导致的数据丢失问题。Repository模式作为业务逻辑层,统一管理网络请求与本地缓存策略,结合Retrofit处理HTTP请求、Room实现本地持久化,为应用提供稳定可靠的数据源。这种架构在新闻类App等数据驱动型应用

weixin_30846347的博客 208

如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (中)

本章我们基于重构的方式进行一个 MVVM 的实战,我们将一个新闻列表的普通实现,一步一步的改造成 MVVM 的架构模式,一共分为上中下三个章节,本章继续上一章,开始中篇的讲解;

CTZL123456的博客 1042

ExpressLRS 915MHz vs 2.4GHz:实测对比与TX12遥控器的最佳搭配方案

本文深入对比了ExpressLRS 915MHz与2.4GHz两种频段在穿透能力、延迟、抗干扰等方面的实测表现,并结合RadioMaster TX12遥控器的硬件特性,提供了从微型花飞到极限远航等不同场景下的最佳搭配方案与EdgeTX系统配置指南,帮助FPV玩家根据核心飞行需求做出精准选择。

milk8的博客 444

android mvvm livedata_各种版本的 玩Android 来啦!

一直说给大家整理一波各个版本的 wanandroid 客户端,好了,来啦!由于优秀客户端实在太多,本文只能按照技术栈的不同,选出几个具有代表性的项目。大家可以根据自己感兴趣的技术栈选择客户端进行学习,当然也可以根据体验效果好坏进行选择,文末有文中提到的所有 Apk下载。1Java 版本(一)基于 :Material Design + MVP + Rxjava2 + Retrofit +...

weixin_39883374的博客 313

IBM Storwize V3700存储维护实战:硬盘更换、电源模块热插拔与带外管理IP配置

在企业IT基础设施中,存储阵列是数据持久化的核心组件,其稳定运行直接关系到业务连续性。存储阵列通常采用RAID技术实现数据冗余,并通过热插拔、冗余电源等设计保障高可用性。带外管理则提供了独立于业务网络的硬件级监控与管理通道,是运维的关键手段。本次实战以一台IBM Storwize V3700存储阵列为例,详细记录了在业务低峰期,依据严谨的操作流程,对出现SMART预警的硬盘进行在线更换,并对有异响的冗余电源模块实施热插拔更换。针对管理界面强制要求设置VIP才能配置带外管理地址的限制,文章提供了通过SSH命令

weixin_29172963的博客 303

自动驾驶传感器选型实战:摄像头、激光雷达与毫米波雷达融合策略

自动驾驶感知系统的核心在于多传感器协同,而非单一技术优劣。摄像头擅长语义识别但受光照和天气制约;激光雷达提供高精度测距却面临成本、温漂与多径干扰等物理瓶颈;毫米波雷达则以全天候鲁棒性与精准测速成为安全底线的守门人。三者融合不是简单叠加,而是基于场景约束(如暴雨、强光、高速跟车)、成本边界(BOM控制)与时间窗口(量产节奏)的工程权衡。真实量产方案中,‘纯视觉’实为视觉主责+毫米波兜底,‘激光雷达上车’本质是用硬件真值加速算法迭代,而传统车企更倾向以毫米波为仲裁者协调冲突。理解这些底层原理,才能在传感器选型、

weixin_32495691的博客 276

字节跳动抖音安卓客户端日常实习 3+1 面经(已 OC)

一二面和 HR 面都是凭印象回忆的,可能会漏掉一些问题。 一面(1 月 13 日,68 min) 提问 自我介绍 简单讲讲你项目的整个设计过程 你提到项目中用的是 MVVM 架构,讲讲你对 MVVM 的认识? MVVM 的优点有什么呢? MVP 中 Model、View 和 Presenter 的持有关系是怎么样的? MVVM 中的持有关系呢? MVVM 中各个层在 Android 开发中的对应关系? 你的 MVVM 用到了 Jetpack 里的 ViewModel,那么它是如何保持数据的呢? V

Androidbye的博客 559

LWIP协议栈在STM32H7上的内存优化实战:如何用32KB RAM实现稳定千兆以太网通信

本文深入探讨了在STM32H7微控制器上,如何通过内存优化策略,在仅32KB的D2 SRAM中稳定运行LWIP协议栈以实现千兆以太网通信。文章详细分析了STM32H7的内存架构、LWIP的内存消耗瓶颈,并提供了从描述符表与PBUF池精确分配、链接脚本修改到MPU区域划分的一整套实战优化方案,旨在解决大数据量传输卡死等常见配置问题。

gg901的博客 603

字节跳动(抖音)Android客户端实习 3+1 面经

或者说,不同的操作会触发不同的调用吗,比如有的调用,有的不调用?这个没答上来,然后面试官很耐心地解释了这个概念,实际上就是两个 View 叠在一起,下层被上层挡住的部分是看不见的,但还是会进行绘制。这个没答上来,然后面试官很耐心地解释了这个概念,实际上就是两个 View 叠在一起,下层被上层挡住的部分是看不见的,但还是会进行绘制。如果用 Kotlin 去调用一个具有返回值的 Java 函数,那么这个返回值是可空的还是非空的呢?3. 你提到项目中用的是 MVVM 架构,讲讲你对 MVVM 的认识?

2401_84149724的博客 900

Pyhon在振动信号处理中的高级应用(一):振动信号特征(时域、频域)提取

文章目录一、概述二、算法原理2.1 时域统计特征三、python代码实现 一、概述 二、算法原理   这里我们借鉴西安交通大学机械学院雷教授的论文《Fault diagnosis of rotating machinery based on multiple ANFIS combination with GAs》相关研究成果。雷教授在论文中共列举了4类共24种振动信号的时域和频域特征。 2.1 时域统计特征 p1=∑n=1Nx(n)NP2=∑n=1N(x(n)−p1)2N−1p3=(∑n=1N∣x(n)∣N

白银时代的博客 1万+

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

Robot_PLC_HMI_Automation 6098
上一篇: 如何应对Android面试官 -> 玩转 MVx(MVC、MVP、MVVM、MVI)
下一篇: 如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (中)
老A说
博客等级 码龄14年 1181粉丝 85原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值