ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android Studio购物商城界面开发:Fragment+RecyclerView实现多Tab导航

Android Studio购物商城界面开发:Fragment+RecyclerView实现多Tab导航 简介面向Android初学者的购物商城界面源码项目基于ListView与BaseAdapter实现商品列表展示、自定义条目布局及滚动加载等核心逻辑适合正在学习Android UI设计、数据绑定与适配器模式的开发者将其作为对照练习或课程设计参考。压缩包共518个文件体积约9.05MB内含完整的Android工程java文件负责业务逻辑与Adapter实现xml文件定义界面布局及ListView、item_product等组件结构gradle及manifest文件用于构建配置png、jpg图片作为商品配图另附可直接安装的apk产物目录划分清晰导入Android Studio后即可运行并查看效果。目前已有13542人浏览学习。通过阅读和运行该项目可以具体掌握自定义Adapter中getCount、getItem、getItemId、getView四个方法的作用与编写方式学会在item产品条目中通过ImageView与TextView组合展示商品图片、名称和价格同时还能结合OnScrollListener实现滚动到底部加载更多商品的扩展功能源码结构简洁、注释明确十分适合初学者仿写并在此基础上增加下拉刷新、网络图片加载等更丰富的商城交互功能。1. 一个用 Android Studio 做购物商城界面的最小闭环它到底在解决谁的什么问题拿到「Android Studio 实现简单的购物商城界面源码」这个需求大多数人是刚学完四大组件和布局想用一套完整界面把知识串起来或者课程设计、毕业设计需要交一个能跑的 App。这个标题的关键词不在「购物」而在「界面」——它不涉及真实的支付、订单、库存而是把首页商品流、分类、购物车、我的这四个 Tab 的骨架搭出来让 App 装上能看、能点、能滑动。解决的是「从空工程到可演示 UI」这一步适合还没进公司、没接触过大型项目的 Android 初学者以及需要一个干净模板来魔改成自己毕设的人。我下面这套方案不引第三方 UI 库全部用 Android 自带的布局和控件在 Android Studio 里新建一个空工程就能复现。2. 拆页面结构首页、分类、购物车、我的四项 Tab 怎么落地到 Fragment 底部导航2.1 为什么这个标题绕不开 Fragment 和 BottomNavigationView做一个带底部导航的商城界面常见做法有两种一种是用多个 Activity 各自维护页面另一种是用单 Activity 多 Fragment。我强烈建议初学者直接走第二种原因不复杂——购物商城界面的四个 Tab 有大量共享状态比如购物车角标、登录态、搜索关键词如果用多个 Activity这些数据得靠 Intent 传来传去或者自己写全局单例代码很快就臭了。而 Fragment 本身就活在 Activity 的容器里切换只是 replace 或 hide/show状态保留也更自然。Android Studio 里新建工程时模板自带一个MainActivity但默认模板是单页面。我们需要把它改成承载四个 Fragment 的容器。底层导航我选BottomNavigationView而不是TabLayout因为后者更偏向页面内切换前者才是标准的 App 级导航。两者的区别在 Material 规范里讲得很清楚BottomNavigationView 适合 3 到 5 个顶级目的地购物商城的首页、分类、购物车、我的正好落在 3 到 5 这个区间。2.2 build.gradle 里先放好依赖别等到 sync 报错才回头打开工程后第一件事不是写布局而是确认build.gradleModule 级别里的依赖。很多新手在这里踩的第一个坑就是依赖缺失BottomNavigationView用的时候报「无法解析符号」原因往往是只引了appcompat没引material。// 文件路径app/build.gradleModule 级别的 build.gradle dependencies { // AndroidX 基础库工程默认自带 implementation androidx.appcompat:appcompat:1.6.1 // Material 组件库BottomNavigationView、CardView 都从这里来 implementation com.google.android.material:material:1.11.0 // 约束布局用来做复杂页面的自适应 implementation androidx.constraintlayout:constraintlayout:2.1.4 // Fragment 官方库 implementation androidx.fragment:fragment-ktx:1.6.2 // RecyclerView 官方库 implementation androidx.recyclerview:recyclerview:1.3.2 }我一般会先把依赖注释写清楚再让 Gradle 去 sync这样报错时能很快定位是哪一行没拉下来。上面用的是我本地能拉到的一组常见稳定版本你完全可以按 Android Studio 提示的最新版本替换——但注意material和appcompat这两个库如果版本相差太远偶尔会出现Theme.MaterialComponents找不到的问题这时优先统一升到当前稳定版。参数上真正需要记住的就一条BottomNavigationView必须搭配 Material 主题否则运行时会直接崩报You need to use a Theme.AppCompat theme之类的错。2.3 搭建容器布局activity_main.xml 里的上下结构Activity 的布局结构很简单上面一个FragmentContainerView占满剩余空间下面一个BottomNavigationView固定高度。这里有个细节FragmentContainerView是官方推荐的 Fragment 容器比直接写fragment标签更灵活因为它允许你在代码里动态替换而不是把某个 Fragment 写死在布局里。!-- 文件路径res/layout/activity_main.xml -- ?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical !-- 容器四个 Fragment 都替换到这里 -- androidx.fragment.app.FragmentContainerView android:idid/fragment_container android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / !-- 底部导航固定四 Tab -- com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottom_nav android:layout_widthmatch_parent android:layout_heightwrap_content app:labelVisibilityModelabeled app:menumenu/menu_bottom_nav / /LinearLayout这里layout_weight1是关键参数它让容器吃掉底部导航以外的所有垂直空间。app:labelVisibilityModelabeled表示始终显示文字标签购物商城这种场景下我一般建议保持开启因为「首页」「分类」这类两字词不用猜。如果你更在意视觉清爽改成unlabeled只显示图标也行但不是这个标题下该优先做的事。底部导航的菜单需要在res/menu/menu_bottom_nav.xml里定义注意item的title和icon都要配齐否则导航栏显示会变得很怪。图标我直接用系统自带的android.R.drawable里的图标省去自己找图标的麻烦。!-- 文件路径res/menu/menu_bottom_nav.xml -- ?xml version1.0 encodingutf-8? menu xmlns:androidhttp://schemas.android.com/apk/res/android item android:idid/nav_home android:iconandroid:drawable/ic_menu_compass android:title首页 / item android:idid/nav_category android:iconandroid:drawable/ic_menu_sort_by_size android:title分类 / item android:idid/nav_cart android:iconandroid:drawable/ic_menu_agenda android:title购物车 / item android:idid/nav_mine android:iconandroid:drawable/ic_menu_myplaces android:title我的 / /menu说实话android.R.drawable里的图标比较素但作为源码演示足够了并且不引入任何资源版权问题。等你的界面能跑了再换成自己设计的 selector 图标也不迟。3. 首页布局实战Toolbar 搜索、商品卡片、双列网格怎么搭出来3.1 先画骨架fragment_home.xml 的层级安排首页是购物商城界面的脸面也是这个标题下工作量最重的一块。它的标准结构从上到下是搜索栏Toolbar 或搜索框、Banner 轮播位、分类快捷入口、商品瀑布/双列网格。其中 Banner 轮播如果做复杂了很耗时我会先放一个ViewPager2的原生轮播不引第三方 Banner 库商品展示用RecyclerView配GridLayoutManager双列网格。这个组合也是商城类 App 最常见的实现路径。先写层级骨架!-- 文件路径res/layout/fragment_home.xml -- ?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical !-- 顶部搜索栏 -- LinearLayout android:layout_widthmatch_parent android:layout_height48dp android:gravitycenter_vertical android:orientationhorizontal android:paddingHorizontal12dp EditText android:idid/search_input android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 android:backgrounddrawable/bg_search_box android:hint搜索商品 android:paddingHorizontal12dp android:maxLines1 / TextView android:idid/search_btn android:layout_widthwrap_content android:layout_heightmatch_parent android:gravitycenter android:text搜索 android:textColor#FF5722 android:paddingHorizontal12dp / /LinearLayout !-- 内容区 -- androidx.recyclerview.widget.RecyclerView android:idid/home_recycler android:layout_widthmatch_parent android:layout_heightmatch_parent android:clipToPaddingfalse android:padding8dp / /LinearLayout为什么内容区直接上 RecyclerView 而不是 ScrollView 里套多个布局因为商城首页本质上是一个长列表RecyclerView 天然回收视图、支持多种 item type后续往里加 Banner、加快捷入口、加商品条目都只是给 Adapter 增加 item 类型的事。如果一开始就用 ScrollView 把所有内容写死界面确实很快能出来但后续每一步改动都会变成在泥潭里挣扎。我见过太多课程设计就是毁在 ScrollView 套三四个 ListView 上滑动手势冲突、布局重绘卡顿全是自己给自己埋的雷。已有 1 个赞。这个细节你可以直接抄走——商城首页一律 RecyclerView 多 item 方案别用 ScrollView ListView 嵌套。3.2 商品卡片CardView 与圆角、阴影参数的设置商品卡片是首页的核心视觉单元。我用 Material 库里的CardView作为容器里面放商品图、标题、价格。CardView 自带圆角和阴影比手动写 shape drawable 省事得多。!-- 文件路径res/layout/item_product_card.xml -- ?xml version1.0 encodingutf-8? com.google.android.material.card.MaterialCardView xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_margin4dp app:cardBackgroundColor#FFFFFF app:cardCornerRadius12dp app:cardElevation2dp app:strokeWidth0dp LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical !-- 商品占位图先用纯色背景后面换成图片加载 -- View android:layout_widthmatch_parent android:layout_height120dp android:background#F5F5F5 / !-- 商品标题 -- TextView android:idid/tv_product_name android:layout_widthmatch_parent android:layout_heightwrap_content android:ellipsizeend android:maxLines2 android:paddingHorizontal8dp android:paddingTop8dp android:textColor#333333 android:textSize14sp / !-- 价格 -- TextView android:idid/tv_product_price android:layout_widthmatch_parent android:layout_heightwrap_content android:paddingHorizontal8dp android:paddingTop4dp android:paddingBottom8dp android:textColor#FF5722 android:textSize16sp android:textStylebold / /LinearLayout /com.google.android.material.card.MaterialCardView这里三个参数值得单独拎出来讲。cardCornerRadius12dp决定了卡片四个角的圆润程度商城 App 一般在 8dp 到 16dp 之间太小显得生硬太大显得玩具感cardElevation2dp控制阴影高度关系到立体感的强弱双列网格里我建议 2dp 到 3dp 就好阴影太重会让列表显得脏strokeWidth0dp是关键——默认 MaterialCardView 会带一圈描边你在预览里看到的淡灰色边框就是它设成 0 才得到真正的纯白卡片。这三个参数适合照抄因为它们对应的是绝大多数商城界面的视觉基线。已有 3 个赞。3.3商品瀑布流?先别上 StaggeredGridLayoutManager商品列表该用GridLayoutManager还是StaggeredGridLayoutManager瀑布流这个选择题其实很影响实现复杂度。瀑布流的视觉效果好商品卡片参差不齐很像淘宝、京东的商品流但代价是item 高度不一致会导致复用时的闪烁新手很难控制而且瀑布流对图片高度依赖如果拿到的商品图全是正方形瀑布流和网格没有任何区别。我给的方案是先用GridLayoutManager配spanCount2让所有商品卡片统一高度。等你把整个商城的骨架调通再改成瀑布流也只是换个 manager 的事。这样做的理由很现实界面的第一要务是「每个 Tab 都能跑」而不是「首页漂亮得能上架」。视觉效果可以后面迭代跑不起来的界面连迭代的资格都没有。4. 让界面活起来商品数据、Adapter、点击事件的最少代码4.1 先写死后台数据一个 Data 类和一个模拟数据源商城界面的数据暂时不接后端常见做法是先写一个Product数据类再用一个静态方法返回写死的商品列表。别小看这一步——把数据源独立出来以后换成从 JSON 接口读取只需要改数据源这一层界面代码一行都不用动。这也是我在这个标题下反复强调的「边界」思想界面和数据的边界要清晰。// 文件路径model/Product.kt // 商品数据类只保存商品展示所需的最小字段 data class Product( val id: Int, // 商品唯一标识 val name: String, // 商品名称 val price: String, // 价格用 String 而不是 Double避免精度问题 val imageResId: Int // 图片资源 ID )字段说明price用 String 存是因为购物商城界面的价格往往有「¥9.90」这种带格式的展示需求用 Double 存再格式化反而容易出问题imageResId先指向 drawable 里的占位图后面接入 Glide 加载网络图时把imageResId换成imageUrl: String即可。模拟数据我直接放在一个DataSource对象里// 文件路径data/DataSource.kt object DataSource { // 模拟商品列表先用 20 条数据让滚动效果跑起来 fun getProducts(): ListProduct List(20) { index - Product( id index, name 商品标题 ${index 1}这里展示两行以内的描述, price ¥${String.format(%.2f, (index 1) * 9.9)}, imageResId R.drawable.ic_placeholder_image ) } }这段代码里最关键的是List(20) { index - ... }Kotlin 里这个语法会一次性生成 20 条商品数据够 RecyclerView 滑出翻页效果。新手容易犯的错是只造 3 条数据结果列表都撑不满一屏还以为自己写错了。模拟数据至少要 20 条这是让「滚动」这件事有体验可言的最低线。4.2 商品 AdapterViewHolder 和点击回调的写法RecyclerView的 Adapter 是这个工程里最需要理解透的一块代码。它负责三件事创建 ViewHolder、把数据绑定到 ViewHolder、告诉列表有多少条数据。点击事件则通过回调接口暴露给外部不要让点击逻辑写在 Adapter 里面否则以后维护起来很头疼。// 文件路径adapter/ProductAdapter.kt class ProductAdapter( private val products: ListProduct, private val onItemClick: (Product) - Unit // 点击回调由外部传入 ) : RecyclerView.AdapterProductAdapter.ProductViewHolder() { // 1. 创建 ViewHolder把 item_product_card.xml 解析成 View override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_product_card, parent, false) return ProductViewHolder(view) } // 2. 绑定数据把 Product 的字段塞进对应控件 override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { val product products[position] holder.nameView.text product.name holder.priceView.text product.price } // 3. 条目数量RecyclerView 靠这个决定渲染多少条 override fun getItemCount(): Int products.size inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val nameView: TextView itemView.findViewById(R.id.tv_product_name) val priceView: TextView itemView.findViewById(R.id.tv_product_price) init { // 条目点击事件把当前的 Product 通过回调传出去 itemView.setOnClickListener { val position bindingAdapterPosition if (position ! RecyclerView.NO_POSITION) { onItemClick(products[position]) } } } } }这段代码里有两个细节是实战中很重要但新手容易忽略的。第一个是bindingAdapterPosition而不是getAdapterPosition()前者考虑了列表增删后的位置变化不容易崩第二个是NO_POSITION的判断因为 RecyclerView 的 item 可能被复用到屏幕外此时位置无效不做判断就会抛IndexOutOfBoundsException。还有个小问题用findViewById每次绑定都得找层控件性能有点浪费但在这个标题下的数据量级完全够用ViewBinding 属于进阶优化项。如果你已经把 ViewBinding 用熟了可以直接替换成 ViewBinding 版本。4.3 MainActivity 里组装四个 Fragment 的切换与出现次数问题最后是把所有零件装进MainActivity。这里有个高频大坑很多人在onNavItemSelected里直接replace()结果每次切换都会重新创建 Fragment购物车滚动的浏览位置切走再回来就没了。正确做法是show()/hide()配合首次添加。我一般写成下面这样。// 文件路径MainActivity.java用 Java 写是为了照顾还没转 Kotlin 的读者 public class MainActivity extends AppCompatActivity { private final Fragment[] fragments new Fragment[4]; private int currentIndex 0; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); BottomNavigationView bottomNav findViewById(R.id.bottom_nav); bottomNav.setOnItemSelectedListener(item - { int index 0; if (item.getItemId() R.id.nav_home) index 0; else if (item.getItemId() R.id.nav_category) index 1; else if (item.getItemId() R.id.nav_cart) index 2; else if (item.getItemId() R.id.nav_mine) index 3; switchTab(index); return true; }); // 先把四个 Fragment 实例化好 fragments[0] new HomeFragment(); fragments[1] new CategoryFragment(); fragments[2] new CartFragment(); fragments[3] new MineFragment(); switchTab(0); } private void switchTab(int index) { FragmentTransaction ft getSupportFragmentManager().beginTransaction(); // 隐藏当前 Fragment if (fragments[currentIndex] ! null) { ft.hide(fragments[currentIndex]); } // 如果目标 Fragment 还没添加过就添加进来否则直接显示 if (!fragments[index].isAdded()) { ft.add(R.id.fragment_container, fragments[index]); } else { ft.show(fragments[index]); } currentIndex index; ft.commit(); } }hide()和show()的本质是把 Fragment 的 View 从「可见」切到「不可见」并不是销毁重建所以 Fragment 里的状态——比如 RecyclerView 的滚动位置、EditText 的输入内容——都还在。代价是这四个 Fragment 的 View 会一直持有内存但商城界面这种轻量页面完全担得住。如果用replace()内存省了体验碎了我不推荐在底部导航这种场景下用replace做切换。这个方案是 Android 官方 BottomNavigationView 样例一直沿用的思路。5. 避坑排查在 Android Studio 里复现这个界面最容易翻车的 6 处5.1 Gradle 依赖拉不下来现象是初次构建卡死或报Could not resolve现象新建工程后第一次 Sync经过漫长的等待最后 Build Output 里报Could not resolve com.google.android.material:material:...。原因多数不是依赖写错而是 Gradle 默认从 google() 仓库拉取时网络不通畅或者仓库顺序错了。解决检查settings.gradle新版工程或build.gradle旧版工程里的仓库配置把google()放在mavenCentral()前面另外把 Gradle 的离线模式关掉——有些人为了提速开了离线依赖缺失时它不会重新联网拉取。这个位置很容易劝退初学者因为报错信息里满屏的链接和版本号看起来非常吓人但它本质上就是仓库访问的事。5.2 BottomNavigationView 运行时崩溃You need to use a Theme.AppCompat theme现象编译通过一装上运行闪退。Logcat 里明确写着You need to use a Theme.AppCompat theme (or descendant) with this activity。原因BottmNavigationView 依赖 Material 主题的属性而新建工程默认主题是Theme.Material3.DayNight.NoActionBar这组直接从 Material Components 库来的主题确实能跑但实际上如果你把主题不小心改成了android:style/Theme.Material或干脆没设置主题就会触发这个崩。解决打开res/values/themes.xml确认应用主题的 parent 是Theme.Material3.DayNight或Theme.MaterialComponents.DayNight.DarkActionBar且MainActivity的 manifest 里没有单独指定别的主题。5.3 商品图片显示成空白或巨大色块ImageView的scaleType和占位图尺寸问题现象用了ImageView放商品图有的图片被拉伸得变形有的直接不显示。原因网络图或本地图的宽高比和ImageView的布局尺寸不一致scaleType默认值fitCenter的行为和你预期不同另外如果ImageView的layout_width和layout_height都写了wrap_content大图会把布局撑爆。解决给商品图和 Banner 图的 ImageView 固定高度比如layout_height120dplayout_widthmatch_parentscaleType用centerCrop它会按中心裁剪并填满控件不拉伸变形多余部分切掉。占位图建议在 drawable 里放一张 400x400 的纯色图不要用 1024x1024 的大图否则列表滑动时会掉帧——这也是这个标题下最容易忽略的性能问题。5.4 RecyclerView 嵌套滚动时的卡顿与崩溃不要在onBindViewHolder里做耗时操作现象列表能显示但快速滑动时明显掉帧甚至偶发IllegalStateException。原因很多时候不是 RecyclerView 本身的问题而是有人在onBindViewHolder里做了文件读取、网络请求或复杂的字符串格式化这些操作在主线程执行导致每一帧的绑定时间超过 16ms。解决onBindViewHolder里只做控件赋值和简单判断图片加载交给 Glide 或 Coil 这类异步库如果列表里有大量findViewById考虑 ViewBinding。排查时可以在 Logcat 里开Systrace或Layout Inspector看每一帧的耗时但不用把它当默认操作先把绑定逻辑变干净就够解决九成问题。5.5 四个 Tab 切换后数据丢失replace()的错误使用现象首页滚到下面切到购物车再切回来首页回到了顶部购物车加了几件商品切走再回来购物车空了。原因百分百是用了replace()做 Fragment 切换replace 会销毁旧的 Fragment 重新创建状态自然全没了。解决按第 4 章的hide()/show()方案改。注意commit()要放在switchTab方法的最后不要在show()和hide()之间做其他 FragmentTransaction否则会报Fragment already added的异常。5.6 中文字体显示粗劣或出现豆腐块sp单位与厂商字体现象同一行文字在不同手机上大小不一样或者在某个 ROM 上中文显示成方框。解决文字大小一律用sp不要用dp或 px方框问题多数是布局里写了android:fontFamilysans这类不完整的字体声明删掉即可。这里属于「玄学」问题不好复现但大概率是主题里默认字体被改过。遇到字体显示异常优先检查有没有在themes.xml或 Adapter 里给TextView强行指定过字体。这个坑我在帮别人调代码时见到过两三次每次都花了不少时间排查。6. 交作业之前换肤、空态、间距这三件小事能大幅提升观感界面能跑通之后别急着提交我一般会再做三件事统一主题色、处理间距和空态、验证极端状态。这三件事不问代码量大小只问界面观感。主题色在res/values/themes.xml里的colorPrimary、colorSecondary和colorSurface三个属性里改。商城类 App 的主题色通常是一个高饱和的橙色或红色比如淘系的橙色#FF5722你要做的只是把这三个色值换掉然后重启 App 看整体效果——但注意colorPrimary会同时影响状态栏颜色、按钮颜色、选中态图标颜色所以换一种主题色全 App 的效果都会跟着变。这是「换肤」的最小实现路径不需要引任何换肤框架。间距问题我给一个可抄的方案在RecyclerView上添加一个ItemDecoration用它统一控制条目间距而不是靠每条 item 自己的layout_margin。前者是全局的、可以在代码里动态调整后者会导致早一条晚一条间距不一致。不要小看这个细节双列网格的间距如果不一致眼睛的观感会非常明显。最后是空态。购物车和搜索结果是必然出现空数据的空列表直接白屏会显得很「残」。在每个 Fragment 里加一个简单的空态 View放在列表下层数据为空时显示、有数据时隐藏代码量不到十行但观感完全两样。我现在的习惯是拿到这类界面需求先搭骨架让四个 Tab 能切换再做商品的展示最后才回头处理这些视觉细节——顺序反了容易什么都想弄结果什么都毛糙。希望这篇对你起步有帮助。本文还有配套的精品资源点击获取
返回列表