资讯动态

购物商城界面源码详解:Android Studio工程配置与RecyclerView实现

发布时间:2026/10/8 22:59:31 来源:尧图企业网站定制
简介一份面向Android初学者的购物商城界面项目源码演示如何在Android Studio中借助ListView与自定义Adapter搭建商品列表页面覆盖商品条目布局、图片与价格展示、数据绑定、列表滚动加载等核心知识点适合课程设计、毕业设计预研或入门练手。资源为zip压缩包共518个文件大小9.05MB以xml布局和配置文件、java源码、json数据、png图片及gradle构建脚本为主同时附带可安装的apk和项目依赖描述目录结构清晰便于导入和对照学习。当前已有13542人学习下载源码中关键位置附有注释可直接导入Android Studio运行便于读者快速掌握ListView与Adapter的配合原理并在此基础上进一步实现下拉刷新、异步加载图片、商品详情页等进阶功能是理解Android UI开发与列表适配机制的实用参考。1. 一个「能跑起来的购物商城界面源码」到底长什么样Android Studio 实现购物商城界面的源码工程拿到手通常长这样Gradle 能同步、编译不报错、点运行就能看到一个带底部导航的商品展示 App。它没有登录、没有支付、甚至没有后端接口所有商品数据都是本地集合这恰恰是它敢叫「简单」的原因。很多新手卡在「会看代码、不会串起来」这一步而这份源码解决的是从零到能跑、能改、能答辩的完整链路。适合三类人——刚学完四大组件想练手的学生、要做课程设计界面交付的开发者以及刚按安装教程装好 Android Studio 想找第一个练手工程的人。下面这套目录拆分方法是我自己拿到任何商城界面源码后最常用的阅读和移植套路。2. Android Studio 工程准备SDK版本、Gradle国内源和依赖选型2.1 新建工程时选 Empty Views Activity别一上来就 Compose拿到这类界面源码第一步不是急着看代码而是确认你本机的 Android Studio 能不能把它原样打开。最常见的翻车场景是别人用的是 View 体系XML 布局 Activity/Fragment你按默认模板建的是 Compose 工程字段名、生命周期、依赖全对不上。所以我一般会先新建一个测试工程语言选 Kotlin模板选 Empty Views ActivityMinimum SDK 选 API 24。这个选择的原因很实在网上能找到的购物商城界面源码绝大多数还是 View 体系RecyclerView 的适配器写法、BottomNavigationView 的菜单配置、Glide 的加载调用都是同一套 API遇到问题直接搜得到答案。项目名也有讲究。工程名和包名里不要出现中文、空格和首字符数字否则后面生成 R 类、配置签名、改包名都会多出一堆莫名其妙的问题。新建完成之后先别急着写代码看一眼gradle-wrapper.properties里的 distributionUrl确认 Gradle 版本和你当前 Android Studio 匹配。版本不匹配的典型症状是 Sync 卡很久然后报一个 Unsupported class file major version 的错这个在第 5 章会细说。提示如果你拿到手的源码是用旧版 Android Studio 建的先不要直接升级 AGP 和 Gradle。旧工程默认的依赖写法可能不兼容新版本优先让工程按原样跑通再谈升级。2.2 改 settings.gradle把仓库源切到国内镜像仓库同步慢是这类源码工程第一个拦路虎。Android Studio 新建工程默认走google()和mavenCentral()网络条件一般的时候 Sync 能卡住好几分钟看着像死机。我处理任何新工程的第一件事就是把settings.gradle里的仓库源加上阿里云镜像并且放在默认仓库前面。pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/gradle-plugin } maven { url https://maven.aliyun.com/repository/public } google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } google() mavenCentral() } }这里一个容易忽略的参数是repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)。它的作用是禁止各模块自己在 build.gradle 里再声明仓库统一走根配置。旧源码里常有模块级repositories如果保留Sync 时会出现两套仓库互相覆盖的告警。项目结构越大的源码越容易出现这种重复配置直接删掉模块级的仓库声明是更省事的做法。镜像地址里三个库的优先级按我写的这个顺序排即可google 镜像管 AndroidX 和 Google 服务gradle-plugin 镜像管 AGP 插件public 镜像聚合了 Maven Central 和 JCenter 的大部分包。全部配置完成后点 Sync Now正常 13 分钟能拉完。如果还是慢检查是不是开了代理导致 localhost 访问异常关掉代理再试一次。2.3 依赖清单RecyclerView、Material 和 Glide 各干各的活购物商城界面源码的依赖不会太复杂核心就四五个库。我在模块级 build.gradle 里通常保留下面这份清单每加一个依赖都注释清楚它管什么dependencies { // View 体系的基座不用 support 包那套老写法 implementation androidx.appcompat:appcompat:1.6.1 // BottomNavigationView、FloatingActionButton 等 Material 组件 implementation com.google.android.material:material:1.11.0 // 首页商品列表和购物车列表都靠它 implementation androidx.recyclerview:recyclerview:1.3.2 // 商品卡片圆角背景 implementation androidx.cardview:cardview:1.0.0 // 网络图和本地图加载占位图处理 implementation com.github.bumptech.glide:glide:4.16.0 }说明几个选型细节。第一appcompat必须和material版本配合Material 库内部依赖了 appcompat版本差太多会报找不到Theme.AppCompat主题的错误。第二RecyclerView 和 CardView 都来自 AndroidX不要和旧 support 包混用源码里如果同时出现android.support.v7.widget.RecyclerView和androidx.recyclerview编译必炸需要全局替换。第三Glide 版本我固定用自己验证过的版本不盲目升新它的 API 在不同大版本之间变化不小比如Glide.with(context).load(url).into(imageView)这种写法从 4.x 沿用至今但占位图方法名在 4.15 之后就有差异。compileSdk 和 targetSdk 我一般跟着 Android Studio 向导默认走当前新工程默认是 compileSdk 34。如果源码工程的 compileSdk 比你的 SDK Manager 里装的版本高Sync 会提示安装对应 Platform反之如果源码用的是更高版本而你本机 SDK 低优先在 SDK Manager 里补齐版本而不是把 compileSdk 强行改小——有些 API 在旧 compileSdk 下直接编译不过。3. 首页商品列表用 RecyclerView 把一个数据模型变成两列卡片3.1 商品模型 Goods 只放四个字段列表图片先用本地资源首页是整个商城界面源码里信息量最大的一块。先定义商品的数据模型这个类决定了后续列表、搜索、购物车共用的字段结构。我做这类界面时字段宁少勿多四个够用就行// 商品数据模型id 用于购物车定位name 用于搜索过滤 data class Goods( val id: Int, val name: String, val description: String, val price: Double, val imageRes: Int // 图片资源 ID先接本地 mipmap后期可换 URL )字段类型有讲究。imageRes用Int存资源 ID好处是不需要处理图片下载的异步问题列表滚动流畅适合把界面逻辑先跑通。等你要接真实接口了把imageRes改成String类型的图片 URL适配器里Glide.load()的入参相应换掉即可其他地方不用动。price用Double而不是String因为后面购物车要算总价字符串做四舍五入容易出玄学 bug。模拟数据的组织方式我习惯写在一个DataSource对象里而不是散落在 Activityobject DataSource { val goodsList listOf( Goods(1, 无线蓝牙耳机, 入耳式 主动降噪 续航30小时, 199.0, R.mipmap.ic_headphone), Goods(2, 机械键盘, 87键 茶轴 背光, 299.0, R.mipmap.ic_keyboard), Goods(3, 电竞鼠标, 1600DPI 人体工学, 99.0, R.mipmap.ic_mouse) // 模拟数据 6~8 条即可界面效果比条数重要 ) }把数据集中放一个文件里后面接接口时只用改这一个地方。3.2 item_goods.xml 用 CardView 包住图片和价格商品列表的每一行是一个卡片布局我用 CardView 作为根容器里面竖着排图片、名称、价格三行。这里要注意 RecyclerView 的 item 布局根节点高度不能是match_parent必须用wrap_content否则会出现列表只显示一项或者每项占满屏幕的诡异问题。?xml version1.0 encodingutf-8? androidx.cardview.widget.CardView xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightwrap_content app:cardCornerRadius8dp app:cardElevation2dp LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical ImageView android:idid/iv_goods android:layout_widthmatch_parent android:layout_height120dp android:scaleTypecenterCrop / TextView android:idid/tv_name android:layout_widthwrap_content android:layout_heightwrap_content android:textSize15sp android:textColor#222222 / TextView android:idid/tv_price android:layout_widthwrap_content android:layout_heightwrap_content android:textSize16sp android:textColor#FF3B30 / /LinearLayout /androidx.cardview.widget.CardViewcardCornerRadius和cardElevation是决定卡片质感的两个参数一个控制圆角大小一个控制阴影高度。8dp 圆角加 2dp 阴影是比较克制的效果阴影太大会显得卡片悬浮感过强不适合密集商品列表。ImageView的scaleTypecenterCrop保证不同尺寸的图在 120dp 高度里都能裁切居中显示不会出现图片被拉伸变形。3.3 GoodsAdapter 的三个必写方法与点击回调RecyclerView 的适配器是商城界面的灵魂很多人第一次写会用错onBindViewHolder里创建绑定的时机。记住一个原则创建 ViewHolder 才做 inflate绑定数据只做赋值。class GoodsAdapter( private val goodsList: ListGoods, private val onItemClick: (Goods) - Unit // 回调接口点击商品时通知外部 ) : RecyclerView.AdapterGoodsAdapter.ViewHolder() { // ViewHolder 持有 item 根布局引用避免每次绑定都 findViewByid class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val ivGoods: ImageView itemView.findViewById(R.id.iv_goods) val tvName: TextView itemView.findViewById(R.id.tv_name) val tvPrice: TextView itemView.findViewById(R.id.tv_price) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemView LayoutInflater.from(parent.context) .inflate(R.layout.item_goods, parent, false) return ViewHolder(itemView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val goods goodsList[position] holder.tvName.text goods.name holder.tvPrice.text ¥ String.format(%.2f, goods.price) Glide.with(holder.itemView) .load(goods.imageRes) .placeholder(R.drawable.ic_placeholder) .into(holder.ivGoods) // 点击事件在绑定位置设置因为要拿着 position 对应的数据 holder.itemView.setOnClickListener { onItemClick(goods) } } override fun getItemCount(): Int goodsList.size }逻辑说明onCreateViewHolder只负责用 LayoutInflater 把布局文件变成 View 并包进 ViewHolderonBindViewHolder按 position 取数据填充控件getItemCount告诉 RecyclerView 一共多少条。这里点击事件放在绑定方法里是标准做法因为每个位置的数据不同但要注意不要在这里写setOnClickListener之外的重逻辑列表滑动时会频繁触发绑定重操作会导致掉帧。String.format(%.2f, goods.price)是价格显示的关键直接把 Double 转字符串会出现199.0这种不专业的写法格式化后是199.00。占位图placeholder参数在网络图加载失败时尤其有用不然页面会出现一整块空白看起来像崩了。3.4 GridLayoutManager 与 spanSizeLookup第一行做成横条首页商品列表最常用的布局是两列网格但第一行往往会放一个横条 Banner。这个效果不需要嵌套两个 RecyclerView一个spanSizeLookup就能解决val layoutManager GridLayoutManager(this, 2) layoutManager.spanSizeLookup object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { // 第 0 个 item 独占两列其余 item 占一列 return if (position 0) 2 else 1 } } recyclerView.layoutManager layoutManagerGridLayoutManager的第二个参数是列数spanSizeLookup动态控制每个 item 占据的列数。第 0 位占 2 列视觉上就是一次横幅后面每项占 1 列形成两列商品网格。注意你的适配器数据列表里第 0 项要放 Banner 对应的数据否则显示出来是「第一张图被拉宽」的错觉而且点击时 position 的对应关系要对上。RecyclerView 自己还要配合一句优化recyclerView.setHasFixedSize(true)数据量固定且条目高度不会因为内容变化时这句让 RecyclerView 跳过重新测量布局的过程滚动更跟手。购物车列表如果支持删除和增减数量就不要加这句否则列表高度变化时会出现 item 显示错乱的坑。4. 底部导航和购物车Fragment 骨架怎么搭才不散架4.1 底部导航菜单与 activity_main 布局购物商城界面源码基本都长一个模式底部四个 Tab首页、分类、购物车、我的点击切换中间的页面区域。底部导航用 Material 库的BottomNavigationView菜单在res/menu/下用 XML 声明?xml version1.0 encodingutf-8? menu xmlns:androidhttp://schemas.android.com/apk/res/android item android:idid/nav_home android:icondrawable/ic_home android:title首页 / item android:idid/nav_category android:icondrawable/ic_category android:title分类 / item android:idid/nav_cart android:icondrawable/ic_cart android:title购物车 / item android:idid/nav_profile android:icondrawable/ic_profile android:title我的 / /menuBottomNavigationView的菜单项建议最多 4 个规范文档里也只保证到 4 项。超过 4 个会自动变成「更多」折叠样式源码里如果看到 5 个 Tab实际效果往往是最后一个被收起来用户找不到入口。图标资源用drawable下的 selector 做选中变色更合适商城界面的底部导航选中态一般是主题色。activity_main 布局的结构是上下两部分LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical androidx.fragment.app.FragmentContainerView android:idid/fragment_container android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottom_nav android:layout_widthmatch_parent android:layout_heightwrap_content android:background#FFFFFF / /LinearLayoutFragmentContainerView是上面那块内容的容器layout_height0dp加layout_weight1让它占满除导航栏以外的所有空间。这里有一个容易踩的布局坑如果把容器高度写成match_parent底部导航会被顶到屏幕外面因为 LinearLayout 垂直方向的空间被两个控件抢。4.2 Fragment 切换用 replace 还是 add/hide主 Activity 的 Fragment 切换新手最常用的写法是replace()但它每次都会销毁旧 Fragment 再创建新 Fragment购物车滑到一半切走再切回来状态全没了。商城界面的正确做法是add()加hide()让 Fragment 实例常驻内存class MainActivity : AppCompatActivity() { private val homeFragment by lazy { HomeFragment() } private val cartFragment by lazy { CartFragment() } private val profileFragment by lazy { ProfileFragment() } private fun switchFragment(target: Fragment) { val transaction supportFragmentManager.beginTransaction() // 把所有 Fragment 先隐藏再显示目标 Fragment listOf(homeFragment, cartFragment, profileFragment).forEach { if (it.isAdded) transaction.hide(it) } if (!target.isAdded) { transaction.add(R.id.fragment_container, target, target.javaClass.simpleName) } else { transaction.show(target) } transaction.commit() } }这段代码的逻辑是先隐藏已添加的 Fragment再判断目标 Fragment 是否第一次添加。首次进来add到容器此后切 Tab 只做show和hide购物车里滚动的位置、输入框的内容全部保留。by lazy声明三个 Fragment 实例保证它们是单例不会每次切换重新创建。javaClass.simpleName作为 tag 传给 add 方法是为了后续用findFragmentByTag找回对应实例。一旦 Fragment 多了这个 tag 的命名规范就重要起来建议统一用带页面语义的名字比如TAG_CART不要用数字编号。4.3 购物车数量加减Adapter 回调接口的正确姿势购物车列表和首页列表的差别在于数据可变且界面要响应数量变化。数量加减按钮如果写在 item 布局里适配器内部处理点击事件会导致购物车 Activity/Fragment 拿不到数据变化算不了总价。我的做法是让购物车 Adapter 暴露一个回调接口class CartAdapter( private val itemList: MutableListGoods, private val onPlusClick: (Goods) - Unit // 加号回调参数是当前条目 ) : RecyclerView.AdapterCartAdapter.ViewHolder() { override fun onBindViewHolder(holder: ViewHolder, position: Int) { val goods itemList[position] holder.tvCount.text goods.count.toString() holder.btnPlus.setOnClickListener { onPlusClick(goods) } } }调用方在 Fragment 里实现回调负责更新数据并刷新总价cartAdapter CartAdapter(cartList) { goods - goods.count 1 tvTotal.text 合计¥ cartList.sumOf { it.price * it.count } cartAdapter.notifyItemChanged(cartList.indexOf(goods)) }notifyItemChanged比notifyDataSetChanged更高效它只刷新被点击的那一行不会让整个列表闪烁。indexOf定位 position 时要求 Goods 类重写过equals或直接用不可变的 id 匹配否则对象是同一个引用时没问题从接口拉取的新对象就定位不到了。购物车界面这个环节最容易写着写着变成「点一下加号整个列表重排」这种体验一进答辩现场就会被问住。5. 真机与模拟器调试避坑五个高频翻车点5.1 图片加载不出来INTERNET 权限和 HTTP 明文流量现象首页列表能滚价格文字正常图片位置全是空白Logcat 里偶尔有 Glide 的加载失败日志。原因分两种一是 AndroidManifest 里没声明INTERNET权限网络请求直接静默失败二是图片地址是http://开头Android 9 以后默认禁止明文流量Glide 拿不到数据。解决权限声明加在 manifest 根节点下明文流量在 application 节点里开启uses-permission android:nameandroid.permission.INTERNET / application android:usesCleartextTraffictrue ... 这个usesCleartextTraffic只建议在调试期开上线前尽可能换成 https 图片源。还有一种是 Glide 占位图正常显示、加载完成后仍然空白那就要检查图片 URL 本身是否能直接访问很多源码里给的图片接口早就失效了换一张有效图即可。5.2 资源重复错误同名 drawable 与 mipmap 的冲突现象编译报AAPT: error: resource drawable/ic_home already exists但翻目录没看到两个相同文件。原因源码工程里drawable和mipmap目录同时存在同名资源或者drawable下存在ic_home.png和ic_home.xml两个文件。Android 打包时会把所有资源目录合并到同一个命名空间同名必冲突。解决统一命名规范图标带前缀ic_背景带前缀bg_不同用途的资源不要共用名字改完执行一次 Build - Clean Project 再重新编译。这个错误在从 GitHub 移植项目时特别常见因为作者经常自己改了一版图标但旧文件忘了删。5.3 Gradle 同步卡死JDK 版本与国内源的组合问题现象Sync 长时间停留在进度条或者报Unsupported class file major version 61.0。原因旧源码用 AGP 7.x 配 JDK 11你本机 Android Studio 用的是 JDK 17或者 Gradle 发行版正在从国外服务器下载大版本文件上百 MB 卡在半路。解决镜像源按第 2 章配置好之后看distributionUrl里的 Gradle 版本。AGP 7.x 配 Gradle 7.x 配 JDK 11AGP 8.x 配 Gradle 8.x 配 JDK 17对应关系以工程能编译为准不要追新。如果确认是 Gradle 下载卡住把gradle-wrapper.properties里的 distributionUrl 域名手动改成腾讯或阿里镜像地址下载完成后改回原地址即可。5.4 模拟器创建失败系统镜像没装、虚拟化没开现象AVD 列表里没有可用设备或者启动模拟器后黑屏一直不加载。原因创建模拟器时只选了设备型号没下载对应 API 级别的 System Image电脑的虚拟化技术在 BIOS 里关闭模拟器起不来。解决在 SDK Manager 的 SDK Platforms 里勾选你要用的 Android 版本和对应的 Google APIs 系统镜像等下载完再创建 AVD。如果公司电脑 BIOS 不让开虚拟化与其跟模拟器较劲不如直接用真机调试Android Studio 识别 USB 设备后点 Run 一样能出效果。模拟器里验证商城界面够用但测购物车滑动流畅度建议上真机。5.5 布局预览黑屏主题和 AppCompatActivity 不匹配现象布局文件在 Design 视图里一片黑Activity 运行起来倒正常。原因Android Studio 的布局预览默认用的主题和你的 Activity 实际主题不一致或者 XML 里根布局引用了 Theme.AppCompat 类主题但依赖里没有对应实现。解决在 Preview 窗口右上角把主题切换为Theme.Material3.DayNight.NoActionBar或你 manifest 里给 Activity 指定的主题。如果切换后仍黑屏检查 values/themes.xml 里应用的 parent 是不是Theme.AppCompat.DayNightMaterial 组件库会覆盖旧的 AppCompat 主题声明在新建工程开始就选 Material 主题能少很多这样的小毛病。6. 让源码更值钱搜索过滤、下拉刷新和一次重构6.1 给列表加搜索重写 Filterable 里的两个方法商品列表接一个搜索框是界面源码从「演示」变「能用」的关键一步。RecyclerView 适配器实现Filterable接口只需要重写getFilter和performFiltering两个方法。核心逻辑是performFiltering里过滤原始数据publishResults里把过滤结果交给适配器并刷新。class GoodsAdapter( private var goodsList: ListGoods ) : RecyclerView.AdapterGoodsAdapter.ViewHolder(), Filterable { private var fullList goodsList // 保存全量数据过滤后再恢复 override fun getFilter(): Filter object : Filter() { override fun performFiltering(constraint: CharSequence?): FilterResults { val keyword constraint.toString().trim() val filtered if (keyword.isEmpty()) { fullList } else { fullList.filter { it.name.contains(keyword) || it.description.contains(keyword) } } val results FilterResults() results.values filtered return results } Suppress(UNCHECKED_CAST) override fun publishResults(constraint: CharSequence?, results: FilterResults?) { goodsList results?.values as ListGoods notifyDataSetChanged() } } }搜索接口上EditText 的addTextChangedListener里调用adapter.filter.filter(keyword)即可。注意过滤最好在description字段也做一次匹配否则搜「降噪」找不到对应耳机会被用户认为功能是坏的。6.2 SwipeRefreshLayout 一分钟接入下拉刷新下拉刷新是商城界面另一个高频诉求用 Material 库的SwipeRefreshLayout包住 RecyclerViewswipeRefreshLayout.setOnRefreshListener { // 模拟刷新恢复全量数据延迟 1 秒收起刷新动画 Handler(Looper.getMainLooper()).postDelayed({ adapter.restoreData() swipeRefreshLayout.isRefreshing false }, 1000) }isRefreshing false一定要在刷新完成后手动关闭否则圆圈转个不停这是下拉刷新最常见的小翻车点。真实的刷新动作应该放在restoreData()里比如重新从 DataSource 赋值并调用notifyDataSetChanged()。6.3 重构别手抄抽取方法快捷键把 Activity 拆瘦Activity 里逻辑一旦超过 200 行就该顺手做一次重构。Android Studio 的 Extract Method 快捷键是我最常用的一个光标选中一段代码macOS 上是Cmd Option MWindows/Linux 上是Ctrl Alt M一键把选中代码抽成独立方法。比如把 MainActivity 里的 Fragment 切换逻辑和底部导航监听各抽成一个方法Activity 立刻变得能一眼看完。View 绑定和回调接口不要手动敲用菜单里的 Generate 或 Alt Insert 自动生成比手抄靠谱得多。最后说一个我自己踩过的坑最早写商城界面源码时我图省事把所有逻辑堆在 MainActivity 里商品列表、购物车、搜索全写在同一个页面结果改一个需求几百行代码来回翻。后来我强迫自己养成了一个习惯——一个页面一个 Fragment一个列表一个 Adapter数据先写死再想接口。这套骨架真正值钱的地方不是图片多好看而是它的拆分方式让每个功能都能独立修改而不牵连其他部分。你照着这份源码跑通一遍再把它的目录结构和自己的项目比对一下就会明白我说的「界面源码的价值在组织方式而不在页面数量」是什么意思。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑