如何应对 Android 面试官 -> 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();}@Overridepublic int getItemCount() {if (mItems != null) {return mItems.size();}return 0;}@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onClick(View v) {WebviewActivity.startCommonWeb(mContext, "News", v.getTag()+"");}});}}@Overridepublic 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><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:animateLayoutChanges="true"android:orientation="vertical"><TextViewandroid: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"/><Viewandroid: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"><LinearLayoutandroid: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"><ImageViewandroid:id="@+id/item_image"android:layout_width="100dp"android:layout_height="70dp"android:scaleType="fitXY" /></androidx.cardview.widget.CardView><TextViewandroid: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><Viewandroid: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" /><variablename="titleViewModel"type="com.xiangxue.news.homefragment.newslist.views.titleview.TitleViewModel" /></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:animateLayoutChanges="true"android:orientation="vertical"><TextViewandroid: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"/><Viewandroid: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 中通过自定义命名空间引入一个自定义方法

<ImageViewandroid: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>>>() {@Overridepublic 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>>() {@Overridepublic 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();}@Overridepublic int getItemCount() {if (mItems != null) {return mItems.size();}return 0;}@Overridepublic int getItemViewType(int position) {if (mItems != null && mItems.get(position) instanceof PictureTitleViewModel) {return VIEW_TYPE_PICTURE_TITLE;}return VIEW_TYPE_TITLE;}@Overridepublic 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;}@Overridepublic 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> {@Overridepublic 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;}@Overridepublic void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {holder.bind(mItems.get(position));}
}

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

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

下一章预告


MVVM 实战新闻客户端(中)

欢迎三连


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/30777.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

connect-caption-and-trace——用于共同建模图像、文本和人类凝视轨迹预测

介绍 论文地址&#xff1a;https://arxiv.org/abs/2105.05964 源码地址&#xff1a;https://github.com/facebookresearch/connect-caption-and-trace 在过去&#xff0c;计算机视觉和自然语言处理领域的模型和算法的发展只有偶尔的重叠&#xff0c;但近年来&#xff0c;这两…

python-04

str.spilt() str.spilt(str" ", num string.count(str)); str&#xff1a;分隔符&#xff0c;默认为所有的空字符&#xff0c;包括空格、换行符"\n"、制表符"\t"等。 num&#xff1a;分隔次数 str "小时候 总有他们在耳边叮咛嘱咐 小…

第四届人工智能、机器人和通信国际会议(ICAIRC 2024)

第四届人工智能、机器人和通信国际会议&#xff08;ICAIRC 2024&#xff09; 2024 4th International Conference on Artificial Intelligence, Robotics, and Communication 2024年12月27-29日 | 中国厦门 重要信息 会议官网&#xff1a;www.icairc.net 录用通知时间&…

▶《强化学习的数学原理》(2024春)_西湖大学赵世钰 Ch1 基本概念

PPT 截取有用信息。 课程网站做习题。总体 MOOC 过一遍 1、视频 学堂在线 习题 2、相应章节 过电子书 复习 3、总体 MOOC 过一遍 学堂在线 课程页面链接 中国大学MOOC 课程页面链接 B 站 视频链接 PPT和书籍下载网址&#xff1a; 【github链接】 onedrive链接&#xff1a; 【…

Python4 操作MySQL数据库

通过python的pymysql库连接到本地的MySQL数据库&#xff0c;并执行查询操作来获取数据&#xff0c;然后打印出每一行的数据&#xff0c;这里以一个简单的学生表为例进行介绍。 1. MySQL的安装与数据准备 首先需要安装MySQL&#xff0c;在安装完成之后使用Navicat与本地数据库…

Docker 部署项目,真的太雅了~

大家好&#xff0c;我是南城余&#xff01; 最近在找工作&#xff0c;正好手里有台服务器&#xff0c;之前项目上线用的宝塔部署项目上线&#xff0c;在公司实习了一年后&#xff0c;发现如今项目部署都使用的是容器化部署方案&#xff0c;也就是类似于和 Docker 一样的部署方案…

海外仓系统能解决海外仓哪些难题?海外仓标准化管理实用指南

海外仓管理问题常常导致业务流程变慢&#xff0c;根据我们的调查显示&#xff0c;至少有48%的海外仓每周都会出现一些“小意外”。甚至这些小问题每天都在发生&#xff0c;问题的出现已经严重影响到了海外仓业务的进行。今天我们将重点分析海外仓比较常见的一些问题&#xff0c…

springboot vue 开源 会员收银系统 (7) 收银台的完善 新增开卡 结算

前言 完整版演示 开发版演示 在前面的开发中&#xff0c;我们成功完成了商品分类和商品信息的搭建&#xff0c;开发了收银台基础。现在&#xff0c;我们将进一步完善收银台的功能&#xff0c;添加开卡和结算功能&#xff0c;并在后台实现会员卡的创建和订单保存。同时&#xff…

地瓜网络技术综合助手教你一键下载腾讯会议高清视频

当您错过腾讯会议的直播课程&#xff0c;不必担心&#xff0c;地瓜网络技术综合助手帮您轻松获取视频回放。 只需几个简单步骤&#xff0c;即可在手头保留珍贵的学习资料。 首先&#xff0c;启动地瓜网络技术综合助手&#xff0c; 进行软件初始化并开启监测功能。 接下来&…

智慧乡村和美人家信息化系统

一、简介 智慧乡村和美人家信息化系统是一个综合管理平台&#xff0c;集成了首页概览、一张图可视化、数据填报、智能评估、便捷申报、公开公示、任务管理、活动发布和灵活配置等功能。该系统不仅提升了乡村管理效率&#xff0c;也优化了家庭生活的便捷性。通过一张图&#xf…

BEVM背靠比特大陆打造新赛道,算力RWA成下一个千亿市场?

众所周知&#xff0c;在加密行业&#xff0c;每隔一段时间就会有一个新的概念或者一个新词出现&#xff0c;并引来社区和资本的追捧关注&#xff0c;笔者近期在浏览新闻时&#xff0c;发现了一个特别有意思的新概念——算力RWA&#xff0c;在社区引起了不少讨论。 该词最早出现…

Flutter【组件】按钮

简介 flutter 按钮组件。提供一种封装按钮组件的思路&#xff0c;并不支持过多的自定义属性。根据使用场景及设计规范进行封装&#xff0c;使用起来比较方便。 github地址&#xff1a;https://github.com/ThinkerJack/jac_uikit pub地址&#xff1a;https://pub.dev/package…

el-upload组件校验不通过预览列表依然显示图片问题解决

如图校验不通过的图片依然显示在预览列表了&#xff0c;需要在校验不通过的时候移除图片 <el-uploadclass"upload-cls":action"ossSignature.host":auto-upload"false"ref"upload":list-type"listType":limit"limi…

如何在React中使用CSS模块,并解释为什么使用它们比传统CSS更有益?

在React中使用CSS模块是一种将CSS类名局部化到单个组件的方法&#xff0c;从而避免了全局作用域中的类名冲突。CSS模块允许你为组件编写样式&#xff0c;并确保这些样式只应用于该组件&#xff0c;而不会影响到其他组件。 以下是在React中使用CSS模块的步骤&#xff1a; 安装C…

医疗器械3D全景展会在线漫游创造数字化时代的展览新篇章

在数字化浪潮的引领下&#xff0c;VR虚拟网上展会正逐渐成为企业展示品牌实力、吸引潜在客户的首选平台。我们与广交会携手走过三年多的时光&#xff0c;凭借优质的服务和丰富的经验&#xff0c;赢得了客户的广泛赞誉。 面对传统展会活动繁多、企业运营繁忙的挑战&#xff0c;许…

深入浅出Git原理与Gitflow流程

1 Git原理 版本控制系统在软件开发和团队协作中扮演着至关重要的角色。它们帮助开发人员跟踪和管理代码的变化&#xff0c;协调多人同时编辑同一代码库&#xff0c;回溯历史版本&#xff0c;并解决代码冲突等问题。Git作为当今最流行的分布式版本控制系统&#xff0c;为开发人…

C++ 59 之 纯虚函数和抽象类

#include <iostream> #include <string> using namespace std;class Cal { // 类中有纯虚函数&#xff0c;这个类也叫做抽象类&#xff0c;无法实现实例化 public:int m_a;int m_b;// 虚函数// virtual int getRes(){// return 0;// }// 纯虚函数 作用和虚函数…

Python第二语言(十三、PySpark实战)

目录 1.开篇 2. PySpark介绍 3. PySpark基础准备 3.1 PySpark安装 3.2 掌握PySpark执行环境入口对象的构建 3.3 理解PySpark的编程模型 4. PySpark&#xff1a;RDD对象数据输入 4.1 RDD对象概念&#xff1a;PySpark支持多种数据的输入&#xff0c;完成后会返回RDD类的对…

有关计算素数的算法

归纳编程学习的感悟, 记录奋斗路上的点滴, 希望能帮到一样刻苦的你! 如有不足欢迎指正! 共同学习交流! 🌎欢迎各位→点赞 👍+ 收藏⭐ + 留言​📝黑暗的笼罩更会凸显光明的可贵! 一、引言 什么是素数 素数,也被称为质数,是指在大于1的自然数中,只能被1和它本身…

[Shell编程学习路线]——for循环应用技巧 语法和案例

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f6e0;️Shell编程专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月20日16点21分 &#x1f004;️文章质量&#xff1a;96分 目录 ————前言———— for 循环语句 基本结构 图示原理…