当前位置: 首页 > news >正文

从零开始:Android Studio开发购物车(第二个实战项目)

一年经验的全栈程序员,目前头发健在,但不知道能撑多久。

文章目录

前言

 一、页面编写

1. 顶部标签栏title_shopping.xml

2. 商品展现列表activity_shopping_channel.xml

 3. 商品详情页面activity_shopping_detail.xml

4. 购物车页面activity_shopping_cart.xml

5. 创建商品展示单元格item_goods.xml

 6. 创建购物车商品展示单元格

 二、Room基础配置

1. 添加依赖

2. 添加购物车实体

3. 添加商品实体

4. 创建数据库DAO层

 5. 创建数据库实例方法

 三、Application全局化

1.引导入相关依赖

2.相关代码编写

3. 修改AndroidManifest.xml

四、业务逻辑代码

1. 商品展示页面

2.  商品详情页面

3.  购物车页面

 五、项目结构示意图 

六、成果展示 

总结

🙌 求点赞、收藏、关注! 


前言

经过前几天的Android速成学习,我决定需要用一个实战项目来巩固知识。所以选择了购物车是刚刚好的。

购物车功能作为电商应用的核心组件之一,其实现方式和性能表现直接影响用户体验。传统的购物车实现往往只存储商品ID和数量等基本信息,当用户离线查看购物车时,商品图片需要重新从网络加载,这不仅增加了流量消耗,也降低了用户体验的连贯性。

本文将带你深入探索如何利用Android官方推荐的Room持久化库,构建一个功能完善且性能优异的购物车模块。与常规实现不同,我们的方案将重点解决以下技术难点:

  1. 本地图片存储:直接将商品图片以Blob形式存入Room数据库,确保用户离线状态下仍能完整查看购物车内容

  2. 数据关系建模:使用Room的关系型数据库特性,建立商品与购物车项之间的关联

  3. 性能优化:针对图片存储可能带来的性能问题,提供切实可行的解决方案

  4. UI与数据同步:实现RecyclerView与数据库的实时联动更新

通过本实战项目,你不仅能掌握Room数据库的高级用法,还能学习到如何在实际项目中平衡功能需求与技术实现。无论你是Android开发新手还是有一定经验的开发者,相信这篇实战指南都能为你带来有价值的参考。


 一、页面编写

1. 顶部标签栏title_shopping.xml

<!-- 相对布局:作为标题栏容器,高度固定为50dp,背景为浅蓝色 -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->android:layout_height="50dp"        <!-- 固定高度50dp -->android:background="#aaaaff" >      <!-- 背景色(浅蓝色) --><!-- 返回按钮图标 --><!-- 左对齐父布局,固定宽度50dp,高度撑满父布局 --><ImageViewandroid:id="@+id/iv_back"      <!-- 控件ID(代码中可通过findViewById操作) -->android:layout_width="50dp"     <!-- 固定宽度 -->android:layout_height="match_parent"  <!-- 高度撑满父布局 -->android:layout_alignParentLeft="true"  <!-- 对齐父布局左侧 -->android:padding="10dp"          <!-- 内边距(让图标看起来更协调) -->android:scaleType="fitCenter"    <!-- 图片缩放模式:居中适应 -->android:src="@drawable/ic_back" />  <!-- 图标资源 --><!-- 居中标题文本 --><TextViewandroid:id="@+id/tv_title"      <!-- 控件ID -->android:layout_width="wrap_content"  <!-- 宽度根据文本内容自适应 -->android:layout_height="match_parent"  <!-- 高度撑满父布局 -->android:layout_centerInParent="true"  <!-- 在父布局中居中 -->android:gravity="center"        <!-- 文本内容居中显示 -->android:textColor="@color/black"  <!-- 文本颜色 -->android:textSize="20sp" />      <!-- 文本大小 --><!-- 购物车图标 --><!-- 右对齐父布局,固定宽度50dp,高度撑满父布局 --><ImageViewandroid:id="@+id/iv_cart"       <!-- 控件ID -->android:layout_width="50dp"     <!-- 固定宽度 -->android:layout_height="match_parent"  <!-- 高度撑满父布局 -->android:layout_alignParentRight="true"  <!-- 对齐父布局右侧 -->android:scaleType="fitCenter"    <!-- 图片缩放模式:居中适应 -->android:src="@drawable/cart" />  <!-- 图标资源 --><!-- 购物车商品数量角标(红色圆形背景+白色数字) --><TextViewandroid:id="@+id/tv_count"      <!-- 控件ID -->android:layout_width="20dp"    <!-- 固定宽度 -->android:layout_height="20dp"   <!-- 固定高度 -->android:layout_alignParentTop="true"  <!-- 对齐父布局顶部 -->android:layout_toRightOf="@+id/iv_cart"  <!-- 位于购物车图标右侧 -->android:layout_marginLeft="-20dp"  <!-- 负边距实现与购物车图标重叠 -->android:gravity="center"       <!-- 文本居中 -->android:background="@drawable/shape_oval_red"  <!-- 红色圆形背景(需自定义shape) -->android:text="0"               <!-- 默认显示数量0 -->android:textColor="@color/white"  <!-- 文本颜色(白色) -->android:textSize="15sp" />      <!-- 文本大小 --></RelativeLayout>

2. 商品展现列表activity_shopping_channel.xml

<!-- 根布局:垂直方向的LinearLayout,占满整个屏幕,背景为橙色 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"  <!-- 宽度匹配父容器 -->android:layout_height="match_parent" <!-- 高度匹配父容器 -->android:orientation="vertical" >    <!-- 子元素垂直排列 --><!-- 引入标题栏布局(复用公共标题栏) --><!-- 说明:此处通过include标签复用预先定义的标题栏布局文件title_shopping.xml --><include layout="@layout/title_shopping" /><!-- 可滚动的容器:ScrollView(解决内容超出屏幕时的滚动问题) --><ScrollViewandroid:layout_width="match_parent"  <!-- 宽度匹配父容器 -->android:layout_height="wrap_content"> <!-- 高度根据内容自适应 --><!-- 网格布局:GridLayout(用于实现2列的网格排列) --><GridLayoutandroid:id="@+id/gl_channel"     <!-- 设置ID便于代码中动态操作 -->android:layout_width="match_parent"  <!-- 宽度匹配父容器(ScrollView) -->android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->android:columnCount="2" />      <!-- 指定网格列数为2(关键属性) --></ScrollView></LinearLayout>

 3. 商品详情页面activity_shopping_detail.xml

<!-- 主布局:垂直方向的LinearLayout,占满整个屏幕,背景为橙色 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"  <!-- 宽度匹配父容器(全屏宽度) -->android:layout_height="match_parent" <!-- 高度匹配父容器(全屏高度) -->android:background="@color/orange"   <!-- 设置背景颜色为橙色 -->android:orientation="vertical">     <!-- 子元素垂直排列 --><!-- 引入标题栏布局 --><!-- 通过include标签复用定义好的标题栏布局文件(title_shopping.xml) --><include layout="@layout/title_shopping" /><!-- 可滚动视图:用于支持内容超出屏幕时的滚动 --><ScrollViewandroid:layout_width="match_parent"  <!-- 宽度匹配父容器 -->android:layout_height="wrap_content"> <!-- 高度根据内容自适应 --><!-- 内容容器:垂直方向的LinearLayout,包含商品详情各个元素 --><LinearLayoutandroid:layout_width="match_parent"  <!-- 宽度匹配父容器(ScrollView) -->android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->android:orientation="vertical">      <!-- 子元素垂直排列 --><!-- 商品图片展示区域 --><ImageViewandroid:id="@+id/iv_goods_pic"  <!-- 控件ID(用于代码中访问) -->android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->android:layout_height="350dp"    <!-- 固定高度350dp -->android:scaleType="fitCenter" /> <!-- 图片缩放模式:居中适应 --><!-- 商品价格显示 --><TextViewandroid:id="@+id/tv_goods_price"  <!-- 控件ID -->android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->android:paddingLeft="5dp"        <!-- 左侧内边距5dp -->android:textColor="@color/red"  <!-- 文本颜色为红色 -->android:textSize="22sp" />      <!-- 文本大小22sp --><!-- 商品描述文本 --><TextViewandroid:id="@+id/tv_goods_desc"  <!-- 控件ID -->android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->android:paddingLeft="5dp"        <!-- 左侧内边距5dp -->android:textColor="@color/black"  <!-- 文本颜色为黑色 -->android:textSize="15sp" />       <!-- 文本大小15sp --><!-- 加入购物车按钮 --><Buttonandroid:id="@+id/btn_add_cart"  <!-- 控件ID -->android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->android:text="加入购物车"        <!-- 按钮文本 -->android:textColor="@color/black"  <!-- 文本颜色为黑色 -->android:textSize="17sp" />       <!-- 文本大小17sp --></LinearLayout></ScrollView>
</LinearLayout>

4. 购物车页面activity_shopping_cart.xml

<!-- 主布局:垂直方向的LinearLayout,占满整个屏幕 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"  <!-- 宽度匹配父容器(全屏宽度) -->android:layout_height="match_parent" <!-- 高度匹配父容器(全屏高度) -->android:orientation="vertical">     <!-- 子元素垂直排列 --><!-- 引入标题栏布局 --><!-- 复用定义好的标题栏布局文件(title_shopping.xml) --><include layout="@layout/title_shopping" /><!-- 可滚动视图:支持内容超出屏幕时的滚动 --><ScrollViewandroid:layout_width="match_parent"  <!-- 宽度匹配父容器 -->android:layout_height="wrap_content"> <!-- 高度根据内容自适应 --><!-- 相对布局容器:用于切换显示购物车内容/空状态 --><RelativeLayoutandroid:layout_width="match_parent"  <!-- 宽度匹配父容器 -->android:layout_height="wrap_content"> <!-- 高度根据内容自适应 --><!-- 购物车内容区域(默认显示) --><LinearLayoutandroid:id="@+id/ll_content"android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->android:orientation="vertical"      <!-- 子元素垂直排列 -->android:visibility="visible">      <!-- 初始可见 --><!-- 表头布局:水平排列的商品信息标题 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"> <!-- 子元素水平排列 --><!-- 图片标题(固定宽度85dp) --><TextViewandroid:layout_width="85dp"android:layout_height="wrap_content"android:gravity="center"  <!-- 文本居中 -->android:text="图片"android:textColor="@color/black"android:textSize="15sp" /><!-- 商品名称标题(权重3,占比最大) --><TextViewandroid:layout_width="0dp"  <!-- 权重布局必须设为0dp -->android:layout_height="wrap_content"android:layout_weight="3"   <!-- 宽度权重占比 -->android:gravity="center"android:text="名称"android:textColor="@color/black"android:textSize="15sp" /><!-- 数量标题 --><TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:gravity="center"android:text="数量"android:textColor="@color/black"android:textSize="15sp" /><!-- 单价标题 --><TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:gravity="center"android:text="单价"android:textColor="@color/black"android:textSize="15sp" /><!-- 总价标题 --><TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:gravity="center"android:text="总价"android:textColor="@color/black"android:textSize="15sp" /></LinearLayout><!-- 动态内容容器:用于代码中添加购物车商品条目 --><LinearLayoutandroid:id="@+id/ll_cart"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical" /><!-- 底部操作栏:水平排列 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:padding="0dp">  <!-- 去除内边距 --><!-- 清空按钮 --><Buttonandroid:id="@+id/btn_clear"android:layout_width="wrap_content"  <!-- 宽度根据文本自适应 -->android:layout_height="wrap_content"android:gravity="center"android:text="清空"android:textColor="@color/black"android:textSize="17sp" /><!-- 占位文本(自动扩展剩余空间) --><TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"  <!-- 占据剩余空间 -->android:gravity="center|right"  <!-- 右对齐且垂直居中 -->android:text="总金额:"android:textColor="@color/black"android:textSize="17sp" /><!-- 总金额显示(红色突出) --><TextViewandroid:id="@+id/tv_total_price"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginRight="10dp"  <!-- 右侧外边距 -->android:gravity="center|left"       <!-- 左对齐且垂直居中 -->android:textColor="@color/red"      <!-- 红色文本 -->android:textSize="25sp" />          <!-- 大号字体 --><!-- 结算按钮 --><Buttonandroid:id="@+id/btn_settle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center"android:text="结算"android:textColor="@color/black"android:textSize="17sp" /></LinearLayout></LinearLayout><!-- 空状态提示区域(默认隐藏) --><LinearLayoutandroid:id="@+id/ll_empty"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:visibility="gone">  <!-- 初始不可见 --><!-- 提示文本(上下外边距各100dp) --><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="100dp"android:layout_marginTop="100dp"android:gravity="center"android:text="哎呀,购物车空空如也,快去选购商品吧"android:textColor="@color/black"android:textSize="17sp" /><!-- 跳转按钮 --><Buttonandroid:id="@+id/btn_shopping_channel"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center"android:text="逛逛手机商场"android:textColor="@color/black"android:textSize="17sp" /></LinearLayout></RelativeLayout></ScrollView>
</LinearLayout>

5. 创建商品展示单元格item_goods.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/ll_item"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:gravity="center"android:background="@color/white"android:orientation="vertical"><TextViewandroid:id="@+id/tv_name"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center"android:textColor="@color/black"android:textSize="17sp" /><ImageViewandroid:id="@+id/iv_thumb"android:layout_width="180dp"android:layout_height="150dp"android:scaleType="fitCenter" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="45dp"android:orientation="horizontal"><TextViewandroid:id="@+id/tv_price"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="2"android:gravity="center"android:textColor="@color/red"android:textSize="15sp" /><Buttonandroid:id="@+id/btn_add"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="3"android:gravity="center"android:text="加入购物车"android:textColor="@color/black"android:textSize="15sp" /></LinearLayout></LinearLayout>

 6. 创建购物车商品展示单元格

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@color/white"android:orientation="horizontal"><ImageViewandroid:id="@+id/iv_thumb"android:layout_width="85dp"android:layout_height="85dp"android:scaleType="fitCenter" /><LinearLayoutandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="3"android:orientation="vertical"><TextViewandroid:id="@+id/tv_name"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="2"android:gravity="left|center"android:textColor="@color/black"android:textSize="17sp" /><TextViewandroid:id="@+id/tv_desc"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="3"android:gravity="left|center"android:textColor="@color/black"android:textSize="12sp" /></LinearLayout><TextViewandroid:id="@+id/tv_count"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:gravity="center"android:textColor="@color/black"android:textSize="17sp" /><TextViewandroid:id="@+id/tv_price"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:gravity="right|center"android:textColor="@color/black"android:textSize="15sp" /><TextViewandroid:id="@+id/tv_sum"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1.2"android:gravity="right|center"android:textColor="@color/red"android:textSize="17sp" /></LinearLayout>


 二Room基础配置

1. 添加依赖

首先在app模块的build.gradle中添加依赖:

dependencies {    ........
// datastore库各版本见 https://mvnrepository.com/artifact/androidx.datastore/datastore-preferencesimplementation 'androidx.datastore:datastore-preferences:1.0.0'// datastore库各版本见 https://mvnrepository.com/artifact/androidx.datastore/datastore-rxjava2implementation 'androidx.datastore:datastore-preferences-rxjava2:1.0.0'def room_version = "2.5.0" // 请使用最新版本// room库各版本见 https://mvnrepository.com/artifact/androidx.room/room-runtimeimplementation "androidx.room:room-runtime:$room_version"annotationProcessor "androidx.room:room-compiler:$room_version"
}

2. 添加购物车实体

在java/com/example/shopping/entity/CartInfo.java添加购物车实体信息

package com.example.shopping.entity;import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;//购物车信息
@Entity
public class CartInfo {@PrimaryKey(autoGenerate = true) // 该字段是自增主键private long id; // 序号private long goodsId; // 商品编号private int count; // 商品数量private String updateTime; // 更新时间public void setId(long id) {this.id = id;}public long getId() {return this.id;}public void setGoodsId(long goodsId) {this.goodsId = goodsId;}public long getGoodsId() {return this.goodsId;}public void setCount(int count) {this.count = count;}public int getCount() {return this.count;}public void setUpdateTime(String updateTime) {this.updateTime = updateTime;}public String getUpdateTime() {return this.updateTime;}}

3. 添加商品实体

在java/com/example/shopping/entity/GoodsInfo.java添加商品实体

package com.example.shopping.entity;import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;import com.example.shopping.R;import java.util.ArrayList;//商品信息
@Entity
public class GoodsInfo {@PrimaryKey(autoGenerate = true) // 该字段是自增主键private long id; // 序号private String name; // 名称private String desc; // 描述private double price; // 价格private String picPath; // 大图的保存路径private int picRes; // 大图的资源编号public void setId(long id) {this.id = id;}public long getId() {return this.id;}public void setName(String name) {this.name = name;}public String getName() {return this.name;}public void setDesc(String desc) {this.desc = desc;}public String getDesc() {return this.desc;}public void setPrice(double price) {this.price = price;}public double getPrice() {return this.price;}public void setPicPath(String picPath) {this.picPath = picPath;}public String getPicPath() {return this.picPath;}public void setPicRes(int picRes) {this.picRes = picRes;}public int getPicRes() {return this.picRes;}// 声明一个手机商品的名称数组private static String[] mNameArray = {"iPhone11", "Mate30", "小米10", "OPPO Reno3", "vivo X30", "荣耀30S"};// 声明一个手机商品的描述数组private static String[] mDescArray = {"Apple iPhone11 256GB 绿色 4G全网通手机","华为 HUAWEI Mate30 8GB+256GB 丹霞橙 5G全网通 全面屏手机","小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机","OPPO Reno3 8GB+128GB 蓝色星夜 双模5G 拍照游戏智能手机","vivo X30 8GB+128GB 绯云 5G全网通 美颜拍照手机","荣耀30S 8GB+128GB 蝶羽红 5G芯片 自拍全面屏手机"};// 声明一个手机商品的价格数组private static float[] mPriceArray = {6299, 4999, 3999, 2999, 2998, 2399};// 声明一个手机商品的大图数组private static int[] mPicArray = {R.drawable.iphone, R.drawable.huawei, R.drawable.xiaomi,R.drawable.oppo, R.drawable.vivo, R.drawable.rongyao};// 获取默认的手机信息列表public static ArrayList<GoodsInfo> getDefaultList() {ArrayList<GoodsInfo> goodsList = new ArrayList<GoodsInfo>();for (int i = 0; i < mNameArray.length; i++) {GoodsInfo info = new GoodsInfo();info.name = mNameArray[i];info.desc = mDescArray[i];info.price = mPriceArray[i];info.picRes = mPicArray[i];goodsList.add(info);}return goodsList;}}

商品数据先进行写死后期在结合后端进行改进。

4. 创建数据库DAO层

专门对数据库进行操作的层级,在java/com/example/shopping/dao/CartDao.java编写购物车数据库操作

package com.example.shopping.dao;import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;import com.example.shopping.util.DateUtil;
import com.example.shopping.entity.CartInfo;import java.util.List;@Dao
public interface CartDao {@Query("SELECT * FROM CartInfo") // 设置查询语句List<CartInfo> queryAllCart(); // 加载所有购物车信息@Query("SELECT * FROM CartInfo WHERE goodsId = :goodsId") // 设置带条件的查询语句CartInfo queryCartByGoodsId(long goodsId); // 根据名字加载购物车@Insert(onConflict = OnConflictStrategy.REPLACE) // 记录重复时替换原记录void insertOneCart(CartInfo cart); // 插入一条购物车信息@Insertvoid insertCartList(List<CartInfo> cartList); // 插入多条购物车信息@Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录int updateCart(CartInfo cart); // 更新购物车信息@Deletevoid deleteCart(CartInfo cart); // 删除购物车信息@Query("DELETE FROM CartInfo WHERE goodsId = :goodsId") // 设置删除语句void deleteOneCart(long goodsId); // 删除一条购物车信息@Query("DELETE FROM CartInfo WHERE 1=1") // 设置删除语句void deleteAllCart(); // 删除所有购物车信息default void save(long goodsId) {CartInfo cartInfo = queryCartByGoodsId(goodsId);if (cartInfo == null) {cartInfo = new CartInfo();cartInfo.setGoodsId(goodsId);cartInfo.setCount(1);cartInfo.setUpdateTime(DateUtil.getNowDateTime(""));insertOneCart(cartInfo);} else {cartInfo.setCount(cartInfo.getCount()+1);cartInfo.setUpdateTime(DateUtil.getNowDateTime(""));updateCart(cartInfo);}}
}

在java/com/example/shopping/dao/GoodsDao.java编写商品数据库操作

package com.example.shopping.dao;import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;import com.example.shopping.entity.GoodsInfo;import java.util.List;@Dao
public interface GoodsDao {@Query("SELECT * FROM GoodsInfo") // 设置查询语句List<GoodsInfo> queryAllGoods(); // 加载所有商品信息@Query("SELECT * FROM GoodsInfo WHERE id = :id") // 设置带条件的查询语句GoodsInfo queryGoodsById(long id); // 根据名字加载商品@Insert(onConflict = OnConflictStrategy.REPLACE) // 记录重复时替换原记录long insertOneGoods(GoodsInfo goods); // 插入一条商品信息@Insertvoid insertGoodsList(List<GoodsInfo> goodsList); // 插入多条商品信息@Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录int updateGoods(GoodsInfo goods); // 更新商品信息@Deletevoid deleteGoods(GoodsInfo goods); // 删除商品信息@Query("DELETE FROM GoodsInfo WHERE 1=1") // 设置删除语句void deleteAllGoods(); // 删除所有商品信息
}

 5. 创建数据库实例方法

在java/com/example/shopping/database/CartDatabase.java创建购物车数据库实例 

package com.example.shopping.database;import androidx.room.Database;
import androidx.room.RoomDatabase;import com.example.shopping.dao.CartDao;
import com.example.shopping.entity.CartInfo;//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保存路径
@Database(entities = {CartInfo.class},version = 1, exportSchema = false)
public abstract class CartDatabase extends RoomDatabase {// 获取该数据库中某张表的持久化对象public abstract CartDao cartDao();
}

在java/com/example/shopping/database/GoodsDatabase.java创建商品数据库实例

package com.example.shopping.database;import androidx.room.Database;
import androidx.room.RoomDatabase;import com.example.shopping.dao.GoodsDao;
import com.example.shopping.entity.GoodsInfo;//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保存路径
@Database(entities = {GoodsInfo.class},version = 1, exportSchema = false)
public abstract class GoodsDatabase extends RoomDatabase {// 获取该数据库中某张表的持久化对象public abstract GoodsDao goodsDao();
}

 三、Application全局化

由于购物车存储信息不只一个页面需要进行获取数据,所以需要把这些数据库实例变成全局实例。

1.引导入相关依赖

在app模块的build.gradle中添加依赖:

    implementation 'androidx.multidex:multidex:2.0.1'

这次编写的应用类(MainApplication),它继承自MultiDexApplication,主要用于全局初始化和管理应用级别的资源和状态。

2.相关代码编写

package com.example.shopping;import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;import androidx.appcompat.app.AppCompatActivity;import com.example.shopping.dao.CartDao;
import com.example.shopping.dao.GoodsDao;
import com.example.shopping.entity.CartInfo;
import com.example.shopping.entity.GoodsInfo;
import com.example.shopping.util.FileUtil;
import com.example.shopping.util.SharedUtil;
import com.example.shopping.util.ToastUtil;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;@SuppressLint("SetTextI18n")
public class ShoppingCartActivity extends AppCompatActivity {private final static String TAG = "ShoppingCartActivity";private TextView tv_count; // 声明一个文本视图对象private TextView tv_total_price; // 声明一个文本视图对象private LinearLayout ll_content; // 声明一个线性布局对象private LinearLayout ll_cart; // 声明一个购物车列表的线性布局对象private LinearLayout ll_empty; // 声明一个线性布局对象private CartDao cartDao; // 声明一个购物车的持久化对象private GoodsDao goodsDao; // 声明一个商品的持久化对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_shopping_cart);TextView tv_title = findViewById(R.id.tv_title);tv_title.setText("购物车");tv_count = findViewById(R.id.tv_count);tv_total_price = findViewById(R.id.tv_total_price);ll_content = findViewById(R.id.ll_content);ll_cart = findViewById(R.id.ll_cart);ll_empty = findViewById(R.id.ll_empty);findViewById(R.id.iv_back).setOnClickListener(v -> finish());findViewById(R.id.btn_shopping_channel).setOnClickListener(v -> {// 从购物车页面跳到商场页面Intent intent = new Intent(this, ShoppingChannelActivity.class);intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 设置启动标志startActivity(intent); // 跳转到手机商场页面});findViewById(R.id.btn_clear).setOnClickListener(v -> {cartDao.deleteAllCart(); // 清空购物车数据库MainApplication.goodsCount = 0;showCount(); // 显示最新的商品数量ToastUtil.show(this, "购物车已清空");});findViewById(R.id.btn_settle).setOnClickListener(v -> {AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("结算商品");builder.setMessage("客官抱歉,支付功能尚未开通,请下次再来");builder.setPositiveButton("我知道了", null);builder.create().show(); // 显示提醒对话框});// 从App实例中获取唯一的购物车持久化对象cartDao = MainApplication.getInstance().getCartDB().cartDao();// 从App实例中获取唯一的商品持久化对象goodsDao = MainApplication.getInstance().getGoodsDB().goodsDao();MainApplication.goodsCount = cartDao.queryAllCart().size();}// 显示购物车图标中的商品数量private void showCount() {tv_count.setText("" + MainApplication.goodsCount);if (MainApplication.goodsCount == 0) {ll_content.setVisibility(View.GONE);ll_cart.removeAllViews(); // 移除下面的所有子视图mGoodsMap.clear();ll_empty.setVisibility(View.VISIBLE);} else {ll_content.setVisibility(View.VISIBLE);ll_empty.setVisibility(View.GONE);}}@Overrideprotected void onResume() {super.onResume();showCount(); // 显示购物车的商品数量downloadGoods(); // 模拟从网络下载商品图片showCart(); // 展示购物车中的商品列表}// 声明一个购物车中的商品信息列表private List<CartInfo> mCartList = new ArrayList<CartInfo>();// 声明一个根据商品编号查找商品信息的映射private final HashMap<Long, GoodsInfo> mGoodsMap = new HashMap<Long, GoodsInfo>();private void deleteGoods(CartInfo info) {MainApplication.goodsCount -= info.getCount();// 从购物车的数据库中删除商品cartDao.deleteOneCart(info.getGoodsId());// 从购物车的列表中删除商品for (int i = 0; i < mCartList.size(); i++) {if (info.getGoodsId() == mCartList.get(i).getGoodsId()) {mCartList.remove(i);break;}}showCount(); // 显示最新的商品数量ToastUtil.show(this, "已从购物车删除" + mGoodsMap.get(info.getGoodsId()).getName());mGoodsMap.remove(info.getGoodsId());refreshTotalPrice(); // 刷新购物车中所有商品的总金额}// 展示购物车中的商品列表private void showCart() {ll_cart.removeAllViews(); // 移除下面的所有子视图mCartList = cartDao.queryAllCart(); // 查询购物车数据库中所有的商品记录Log.d(TAG, "mCartList.size()=" + mCartList.size());if (mCartList == null || mCartList.size() <= 0) {return;}for (int i = 0; i < mCartList.size(); i++) {final CartInfo info = mCartList.get(i);// 根据商品编号查询商品数据库中的商品记录final GoodsInfo goods = goodsDao.queryGoodsById(info.getGoodsId());Log.d(TAG, "name=" + goods.getName() + ",price=" + goods.getPrice() + ",desc=" + goods.getDesc());mGoodsMap.put(info.getGoodsId(), goods);// 获取布局文件item_goods.xml的根视图View view = LayoutInflater.from(this).inflate(R.layout.item_cart, null);ImageView iv_thumb = view.findViewById(R.id.iv_thumb);TextView tv_name = view.findViewById(R.id.tv_name);TextView tv_desc = view.findViewById(R.id.tv_desc);TextView tv_count = view.findViewById(R.id.tv_count);TextView tv_price = view.findViewById(R.id.tv_price);TextView tv_sum = view.findViewById(R.id.tv_sum);// 给商品行添加点击事件。点击商品行跳到商品的详情页view.setOnClickListener(v -> {Intent intent = new Intent(this, ShoppingDetailActivity.class);intent.putExtra("goods_id", info.getGoodsId());startActivity(intent); // 跳到商品详情页面});// 给商品行添加长按事件。长按商品行就删除该商品view.setOnLongClickListener(v -> {AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setMessage("是否从购物车删除"+goods.getName()+"?");builder.setPositiveButton("是", (dialog, which) -> {ll_cart.removeView(v); // 移除当前视图deleteGoods(info); // 删除该商品});builder.setNegativeButton("否", null);builder.create().show(); // 显示提醒对话框return true;});iv_thumb.setImageURI(Uri.parse(goods.getPicPath())); // 设置商品图片tv_name.setText(goods.getName()); // 设置商品名称tv_desc.setText(goods.getDesc()); // 设置商品描述tv_count.setText("" + info.getCount()); // 设置商品数量tv_price.setText("" + (int)goods.getPrice()); // 设置商品单价tv_sum.setText("" + (int)(info.getCount() * goods.getPrice())); // 设置商品总价ll_cart.addView(view); // 往购物车列表添加该商品行}refreshTotalPrice(); // 重新计算购物车中的商品总金额}// 重新计算购物车中的商品总金额private void refreshTotalPrice() {int total_price = 0;for (CartInfo info : mCartList) {GoodsInfo goods = mGoodsMap.get(info.getGoodsId());total_price += goods.getPrice() * info.getCount();}tv_total_price.setText("" + total_price);}private String mFirst = "true"; // 是否首次打开// 模拟网络数据,初始化数据库中的商品信息private void downloadGoods() {// 获取共享参数保存的是否首次打开参数mFirst = SharedUtil.getIntance(this).readString("first", "true");// 获取当前App的私有下载路径String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/";if (mFirst.equals("true")) { // 如果是首次打开ArrayList<GoodsInfo> goodsList = GoodsInfo.getDefaultList(); // 模拟网络图片下载for (int i = 0; i < goodsList.size(); i++) {GoodsInfo info = goodsList.get(i);long id = goodsDao.insertOneGoods(info); // 往商品数据库插入一条该商品的记录info.setId(id);Bitmap pic = BitmapFactory.decodeResource(getResources(), info.getPicRes());String pic_path = path + id + ".jpg";FileUtil.saveImage(pic_path, pic); // 往存储卡保存商品图片info.setPicPath(pic_path);goodsDao.updateGoods(info); // 更新商品数据库中该商品记录的图片路径}}// 把是否首次打开写入共享参数SharedUtil.getIntance(this).writeString("first", "false");}}

3. 修改AndroidManifest.xml

主要是添加这一行


四、业务逻辑代码

1. 商品展示页面

在java/com/example/shopping/ShoppingChannelActivity.java

// 使用@SuppressLint注解忽略"SetTextI18n"警告(直接设置文本时可能缺少国际化处理的警告)
@SuppressLint("SetTextI18n")
public class ShoppingChannelActivity extends AppCompatActivity {// 声明控件成员变量private TextView tv_count;         // 显示购物车商品数量的文本视图private GridLayout gl_channel;    // 商品展示区域的网格布局private CartDao cartDao;          // 购物车数据库访问对象(用于操作购物车数据)private GoodsDao goodsDao;        // 商品数据库访问对象(用于操作商品数据)@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 设置当前Activity的布局文件setContentView(R.layout.activity_shopping_channel);// 初始化标题栏TextView tv_title = findViewById(R.id.tv_title);tv_title.setText("手机商场");  // 设置标题文本// 初始化控件tv_count = findViewById(R.id.tv_count);       // 购物车数量显示框gl_channel = findViewById(R.id.gl_channel);   // 商品网格布局容器// 返回按钮点击事件findViewById(R.id.iv_back).setOnClickListener(v -> finish());// 购物车图标点击事件findViewById(R.id.iv_cart).setOnClickListener(v -> {// 跳转到购物车页面Intent intent = new Intent(this, ShoppingCartActivity.class);// 清除Activity栈中位于目标Activity之上的所有Activityintent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent);});// 显示当前购物车商品总数tv_count.setText("" + MainApplication.goodsCount);// 从全局Application中获取数据库访问对象cartDao = MainApplication.getInstance().getCartDB().cartDao();  // 购物车DAOgoodsDao = MainApplication.getInstance().getGoodsDB().goodsDao(); // 商品DAO}/*** 将指定商品添加到购物车* @param goods_id 商品ID* @param goods_name 商品名称(用于Toast提示)*/private void addToCart(long goods_id, String goods_name) {// 增加全局商品计数MainApplication.goodsCount++;// 更新界面显示tv_count.setText("" + MainApplication.goodsCount);// 将商品ID保存到购物车数据库cartDao.save(goods_id);// 显示添加成功的提示ToastUtil.show(this, "已添加一部" + goods_name + "到购物车");}@Overrideprotected void onResume() {super.onResume();// 每次返回Activity时更新购物车数量显示tv_count.setText("" + MainApplication.goodsCount);// 刷新商品列表showGoods(); }/*** 展示商品列表*/private void showGoods() {// 获取屏幕宽度用于计算商品项宽度int screenWidth = Utils.getScreenWidth(this);// 设置网格布局中子项的布局参数(宽度为屏幕一半,高度自适应)LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(screenWidth/2, LinearLayout.LayoutParams.WRAP_CONTENT);// 清空现有商品视图gl_channel.removeAllViews(); // 从数据库获取所有商品数据List<GoodsInfo> goodsList = goodsDao.queryAllGoods();// 遍历商品列表for (final GoodsInfo info : goodsList) {// 加载单个商品项的布局View view = LayoutInflater.from(this).inflate(R.layout.item_goods, null);// 初始化商品项中的控件ImageView iv_thumb = view.findViewById(R.id.iv_thumb);  // 商品图片TextView tv_name = view.findViewById(R.id.tv_name);     // 商品名称TextView tv_price = view.findViewById(R.id.tv_price);   // 商品价格Button btn_add = view.findViewById(R.id.btn_add);      // 加入购物车按钮// 设置商品信息tv_name.setText(info.getName());  // 设置商品名称iv_thumb.setImageURI(Uri.parse(info.getPicPath())); // 加载商品图片// 商品图片点击事件(跳转到详情页)iv_thumb.setOnClickListener(v -> {Intent intent = new Intent(this, ShoppingDetailActivity.class);intent.putExtra("goods_id", info.getId());  // 传递商品IDstartActivity(intent);});// 设置商品价格(去掉小数部分)tv_price.setText("" + (int)info.getPrice());// 加入购物车按钮点击事件btn_add.setOnClickListener(v -> addToCart(info.getId(), info.getName()));// 将商品项添加到网格布局gl_channel.addView(view, params);}}
}

2.  商品详情页面

在java/com/example/shopping/ShoppingDetailActivity.java

@SuppressLint("SetTextI18n")
public class ShoppingDetailActivity extends AppCompatActivity {private TextView tv_title; // 声明一个文本视图对象private TextView tv_count; // 声明一个文本视图对象private TextView tv_goods_price; // 声明一个文本视图对象private TextView tv_goods_desc; // 声明一个文本视图对象private ImageView iv_goods_pic; // 声明一个图像视图对象private long mGoodsId; // 当前商品的商品编号private CartDao cartDao; // 声明一个购物车的持久化对象private GoodsDao goodsDao; // 声明一个商品的持久化对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_shopping_detail);tv_title = findViewById(R.id.tv_title);tv_count = findViewById(R.id.tv_count);tv_goods_price = findViewById(R.id.tv_goods_price);tv_goods_desc = findViewById(R.id.tv_goods_desc);iv_goods_pic = findViewById(R.id.iv_goods_pic);findViewById(R.id.iv_back).setOnClickListener(v -> finish());findViewById(R.id.iv_cart).setOnClickListener(v -> {startActivity(new Intent(this, ShoppingCartActivity.class)); // 跳转到购物车页面});findViewById(R.id.btn_add_cart).setOnClickListener(v -> addToCart(mGoodsId));tv_count.setText("" + MainApplication.goodsCount);// 从App实例中获取唯一的购物车持久化对象cartDao = MainApplication.getInstance().getCartDB().cartDao();// 从App实例中获取唯一的商品持久化对象goodsDao = MainApplication.getInstance().getGoodsDB().goodsDao();}// 把指定编号的商品添加到购物车private void addToCart(long goods_id) {MainApplication.goodsCount++;tv_count.setText("" + MainApplication.goodsCount);cartDao.save(goods_id); // 把该商品填入购物车数据库ToastUtil.show(this, "成功添加至购物车");}@Overrideprotected void onResume() {super.onResume();showDetail(); // 展示商品详情}private void showDetail() {// 获取上一个页面传来的商品编号mGoodsId = getIntent().getLongExtra("goods_id", 0L);if (mGoodsId > 0) {// 根据商品编号查询商品数据库中的商品记录GoodsInfo info = goodsDao.queryGoodsById(mGoodsId);tv_title.setText(info.getName()); // 设置商品名称tv_goods_desc.setText(info.getDesc()); // 设置商品描述tv_goods_price.setText("" + (int)info.getPrice()); // 设置商品价格iv_goods_pic.setImageURI(Uri.parse(info.getPicPath())); // 设置商品图片}}}

3.  购物车页面

@SuppressLint("SetTextI18n")
public class ShoppingCartActivity extends AppCompatActivity {private final static String TAG = "ShoppingCartActivity";private TextView tv_count; // 声明一个文本视图对象private TextView tv_total_price; // 声明一个文本视图对象private LinearLayout ll_content; // 声明一个线性布局对象private LinearLayout ll_cart; // 声明一个购物车列表的线性布局对象private LinearLayout ll_empty; // 声明一个线性布局对象private CartDao cartDao; // 声明一个购物车的持久化对象private GoodsDao goodsDao; // 声明一个商品的持久化对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_shopping_cart);TextView tv_title = findViewById(R.id.tv_title);tv_title.setText("购物车");tv_count = findViewById(R.id.tv_count);tv_total_price = findViewById(R.id.tv_total_price);ll_content = findViewById(R.id.ll_content);ll_cart = findViewById(R.id.ll_cart);ll_empty = findViewById(R.id.ll_empty);findViewById(R.id.iv_back).setOnClickListener(v -> finish());findViewById(R.id.btn_shopping_channel).setOnClickListener(v -> {// 从购物车页面跳到商场页面Intent intent = new Intent(this, ShoppingChannelActivity.class);intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 设置启动标志startActivity(intent); // 跳转到手机商场页面});findViewById(R.id.btn_clear).setOnClickListener(v -> {cartDao.deleteAllCart(); // 清空购物车数据库MainApplication.goodsCount = 0;showCount(); // 显示最新的商品数量ToastUtil.show(this, "购物车已清空");});findViewById(R.id.btn_settle).setOnClickListener(v -> {AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("结算商品");builder.setMessage("客官抱歉,支付功能尚未开通,请下次再来");builder.setPositiveButton("我知道了", null);builder.create().show(); // 显示提醒对话框});// 从App实例中获取唯一的购物车持久化对象cartDao = MainApplication.getInstance().getCartDB().cartDao();// 从App实例中获取唯一的商品持久化对象goodsDao = MainApplication.getInstance().getGoodsDB().goodsDao();MainApplication.goodsCount = cartDao.queryAllCart().size();}// 显示购物车图标中的商品数量private void showCount() {tv_count.setText("" + MainApplication.goodsCount);if (MainApplication.goodsCount == 0) {ll_content.setVisibility(View.GONE);ll_cart.removeAllViews(); // 移除下面的所有子视图mGoodsMap.clear();ll_empty.setVisibility(View.VISIBLE);} else {ll_content.setVisibility(View.VISIBLE);ll_empty.setVisibility(View.GONE);}}@Overrideprotected void onResume() {super.onResume();showCount(); // 显示购物车的商品数量downloadGoods(); // 模拟从网络下载商品图片showCart(); // 展示购物车中的商品列表}// 声明一个购物车中的商品信息列表private List<CartInfo> mCartList = new ArrayList<CartInfo>();// 声明一个根据商品编号查找商品信息的映射private final HashMap<Long, GoodsInfo> mGoodsMap = new HashMap<Long, GoodsInfo>();private void deleteGoods(CartInfo info) {MainApplication.goodsCount -= info.getCount();// 从购物车的数据库中删除商品cartDao.deleteOneCart(info.getGoodsId());// 从购物车的列表中删除商品for (int i = 0; i < mCartList.size(); i++) {if (info.getGoodsId() == mCartList.get(i).getGoodsId()) {mCartList.remove(i);break;}}showCount(); // 显示最新的商品数量ToastUtil.show(this, "已从购物车删除" + mGoodsMap.get(info.getGoodsId()).getName());mGoodsMap.remove(info.getGoodsId());refreshTotalPrice(); // 刷新购物车中所有商品的总金额}// 展示购物车中的商品列表private void showCart() {ll_cart.removeAllViews(); // 移除下面的所有子视图mCartList = cartDao.queryAllCart(); // 查询购物车数据库中所有的商品记录Log.d(TAG, "mCartList.size()=" + mCartList.size());if (mCartList == null || mCartList.size() <= 0) {return;}for (int i = 0; i < mCartList.size(); i++) {final CartInfo info = mCartList.get(i);// 根据商品编号查询商品数据库中的商品记录final GoodsInfo goods = goodsDao.queryGoodsById(info.getGoodsId());Log.d(TAG, "name=" + goods.getName() + ",price=" + goods.getPrice() + ",desc=" + goods.getDesc());mGoodsMap.put(info.getGoodsId(), goods);// 获取布局文件item_goods.xml的根视图View view = LayoutInflater.from(this).inflate(R.layout.item_cart, null);ImageView iv_thumb = view.findViewById(R.id.iv_thumb);TextView tv_name = view.findViewById(R.id.tv_name);TextView tv_desc = view.findViewById(R.id.tv_desc);TextView tv_count = view.findViewById(R.id.tv_count);TextView tv_price = view.findViewById(R.id.tv_price);TextView tv_sum = view.findViewById(R.id.tv_sum);// 给商品行添加点击事件。点击商品行跳到商品的详情页view.setOnClickListener(v -> {Intent intent = new Intent(this, ShoppingDetailActivity.class);intent.putExtra("goods_id", info.getGoodsId());startActivity(intent); // 跳到商品详情页面});// 给商品行添加长按事件。长按商品行就删除该商品view.setOnLongClickListener(v -> {AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setMessage("是否从购物车删除"+goods.getName()+"?");builder.setPositiveButton("是", (dialog, which) -> {ll_cart.removeView(v); // 移除当前视图deleteGoods(info); // 删除该商品});builder.setNegativeButton("否", null);builder.create().show(); // 显示提醒对话框return true;});iv_thumb.setImageURI(Uri.parse(goods.getPicPath())); // 设置商品图片tv_name.setText(goods.getName()); // 设置商品名称tv_desc.setText(goods.getDesc()); // 设置商品描述tv_count.setText("" + info.getCount()); // 设置商品数量tv_price.setText("" + (int)goods.getPrice()); // 设置商品单价tv_sum.setText("" + (int)(info.getCount() * goods.getPrice())); // 设置商品总价ll_cart.addView(view); // 往购物车列表添加该商品行}refreshTotalPrice(); // 重新计算购物车中的商品总金额}// 重新计算购物车中的商品总金额private void refreshTotalPrice() {int total_price = 0;for (CartInfo info : mCartList) {GoodsInfo goods = mGoodsMap.get(info.getGoodsId());total_price += goods.getPrice() * info.getCount();}tv_total_price.setText("" + total_price);}private String mFirst = "true"; // 是否首次打开// 模拟网络数据,初始化数据库中的商品信息private void downloadGoods() {// 获取共享参数保存的是否首次打开参数mFirst = SharedUtil.getIntance(this).readString("first", "true");// 获取当前App的私有下载路径String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/";if (mFirst.equals("true")) { // 如果是首次打开ArrayList<GoodsInfo> goodsList = GoodsInfo.getDefaultList(); // 模拟网络图片下载for (int i = 0; i < goodsList.size(); i++) {GoodsInfo info = goodsList.get(i);long id = goodsDao.insertOneGoods(info); // 往商品数据库插入一条该商品的记录info.setId(id);Bitmap pic = BitmapFactory.decodeResource(getResources(), info.getPicRes());String pic_path = path + id + ".jpg";FileUtil.saveImage(pic_path, pic); // 往存储卡保存商品图片info.setPicPath(pic_path);goodsDao.updateGoods(info); // 更新商品数据库中该商品记录的图片路径}}// 把是否首次打开写入共享参数SharedUtil.getIntance(this).writeString("first", "false");}}

 五、项目结构示意图 


六、成果展示 

总结

本次实战项目通过Room数据库实现了购物车功能,重点解决了本地图片存储、数据关系建模等核心问题,提升了离线状态下的用户体验。借助Room的关系型特性与RecyclerView联动,确保了数据与UI的高效同步。项目不仅巩固了Android开发的基础知识,还深入探讨了性能优化的实用方案。无论是技术实现还是实战经验,都为后续开发提供了有价值的参考。

🙌 求点赞、收藏、关注! 

如果这篇文章对你有帮助,不妨:
👍 点个赞 → 让更多人看到这篇干货!
⭐ 收藏一下 → 方便以后随时查阅!
🔔 加关注 → 获取更多 前端/后端/全栈技术深度解析

你的支持,是我持续创作的最大动力! 🚀

相关文章:

从零开始:Android Studio开发购物车(第二个实战项目)

一年经验的全栈程序员&#xff0c;目前头发健在&#xff0c;但不知道能撑多久。 文章目录 前言 一、页面编写 1. 顶部标签栏title_shopping.xml 2. 商品展现列表activity_shopping_channel.xml 3. 商品详情页面activity_shopping_detail.xml 4. 购物车页面activity_shopping…...

2. python协程/异步编程详解

目录 1. 简单的异步程序 2. 协程函数和协程对象 3. 事件循环 4. 任务对象Task及Future对象 4.1 Task与Future的关系 4.2 Future对象 4.3 全局对象和循环事件对象 5. await关键字 6. 异步上下文管理 7.异步迭代器 8. asyncio的常用函数 8.1 asyncio.run 8.2 asyncio.get…...

XSS靶场实战(工作wuwuwu)

knoxss knoxss Single Reflection Using QUERY of URL ——01 测试标签 <script>alert(666666)</script>——02: " <h1>test</h1>没有反应&#xff0c;查看源码 现在需要闭合双引号&#xff0c;我计划还是先搞标签 "><h1>tes…...

DNA复制过程3D动画教学工具

DNA复制过程3D动画教学工具 访问工具页面: DNA复制动画演示 工具介绍 我开发了一个交互式的DNA复制过程3D动画演示工具&#xff0c;用于分子生物学教学。这个工具直观展示了&#xff1a; DNA双螺旋结构的解旋过程碱基互补配对原理半保留复制机制完整的复制周期动画 主要特点…...

在Mybatis中写sql的常量应用

下面示例把原来写死的 1、2、3 都替换成了绑定好的常量&#xff0c;同时额外演示了如何把第五个状态也一起统计&#xff08;如果你的 DTO 没有对应字段&#xff0c;也可删掉相应那一行&#xff09;。 <!-- 1. 定义可复用的常量绑定 --> <sql id"DeviceStatusCon…...

一次讲明白SaaS、PaaS、IaaS、aPaaS、iPaaS、RaaS、RPAaaS

在数字化浪潮与5G技术的强势驱动下&#xff0c;各行业对云服务的需求正呈现出井喷式增长态势&#xff0c;众多企业纷纷投身云服务的怀抱&#xff0c;以期在激烈的市场竞争中抢占先机。而谷云科技作为iPaaS领域的佼佼者&#xff0c;也在这股浪潮中大放异彩&#xff0c;助力企业实…...

RTDETRv2 pytorch训练

RTDETRv2 pytorch训练 1. 代码获取2. 数据集制作3. 环境配置4. 代码修改1&#xff09;configs/dataset/coco_detection.yml2) configs/src/data/coco_dataset.py3&#xff09;configs/src/core/yaml_utils.py4&#xff09;configs/rtdeterv2/include/optimizer.yml 5. 代码训练…...

Unity3D仿星露谷物语开发39之非基于网格的光标

1、目标 当鼠标移动到reapable scenary&#xff08;可收割庄稼&#xff09;上方时&#xff0c;光标会变成十字架。 之前章节中&#xff0c;Grid有Dug/Watered属性&#xff0c;光标移动上方时会显示方框。 而这次的功能并非基于Grid的属性&#xff0c;而是基于scenary&#x…...

什么是 MCP?AI 应用的“USB-C”标准接口详解

目录 &#x1f9e9; 什么是 MCP&#xff1f;AI 应用的“USB-C”标准接口详解 &#x1f4cc; 背景与动机 &#x1f9e0; 核心概念 &#x1f3d7;️ 技术架构 &#x1f680; 应用场景 &#x1f9e9; 什么是 MCP&#xff1f;AI 应用的“USB-C”标准接口详解 &#x1f4cc; 背…...

狼人杀中的智能策略:解析AI如何理解复杂社交游戏

想要掌握如何将大模型的力量发挥到极致吗&#xff1f;叶梓老师带您深入了解 Llama Factory —— 一款革命性的大模型微调工具&#xff08;限时免费&#xff09;。 1小时实战课程&#xff0c;您将学习到如何轻松上手并有效利用 Llama Factory 来微调您的模型&#xff0c;以发挥其…...

10 基于Gazebo和Rviz实现导航仿真,包括SLAM建图,地图服务,机器人定位,路径规划

在9中我们已经实现了机器人的模块仿真&#xff0c;现在要在这个基础上实现SLAM建图&#xff0c;地图服务&#xff0c;机器人定位&#xff0c;路径规划 1. 还是在上述机器人的工作空间下&#xff0c;新建功能包&#xff08;nav&#xff09;&#xff0c;导入依赖 gmapping ma…...

jmeter-Beashell获取请求body data

在使用JMeter的BeanShell处理器或BeanShell断言中获取HTTP请求的body数据&#xff0c;可以通过几种方式实现。下面是一些常用的方法&#xff1a; 方法1&#xff1a;使用prev变量 在BeanShell处理器或断言中&#xff0c;prev变量可以用来访问最近的sampler&#xff08;采样器&…...

区块链密码学核心

文章目录 概要1. 基础密码学哈希函数&#xff08;Hash Function&#xff09;对称加密与非对称加密数字签名&#xff08;Digital Signature&#xff09;密钥管理 2. 区块链专用密码学技术零知识证明&#xff08;Zero-Knowledge Proof, ZKP&#xff09;同态加密&#xff08;Homom…...

Git 多账号切换及全局用户名设置不生效问,GIT进行上传无权限问题

解决 Git 多账号切换及全局用户名设置不生效问题 在软件开发过程中&#xff0c;我们经常会使用 Git 进行版本控制。有时&#xff0c;我们需要在同一台机器上管理多个 Git 账号&#xff0c;最近我在进行使用git的时候因为项目要进行上传的不同的git账号&#xff0c;但是通过本地…...

阿里云服务迁移实战: 04-IP 迁移

普通过户 如资料过户按量付费EIP所述&#xff0c;如果原账号是个人账号&#xff0c;则目标账号无限制&#xff0c;如果原账号是企业账号&#xff0c;则目标账号必须为相同认证主体的企业账号。 其主要操作就是&#xff0c;在原账号发起过户&#xff0c;在新账号接收过户。具体…...

探索PyTorch中的空间与通道双重注意力机制:实现concise的scSE模块

探索PyTorch中的空间与通道双重注意力机制&#xff1a;实现concise的scSE模块 在深度学习领域&#xff0c;尤其是在计算机视觉任务中&#xff0c;特征图的注意力机制变得越来越重要。近期&#xff0c;我在研究一种结合了通道和空间两种注意力机制的模块——Concise Spatial an…...

关闭正点原子atk-qtapp-start.service

# 查找相关服务 systemctl list-units --typeservice --staterunning # 查看详细信息 systemctl status atk-qtapp-start.service >> ● atk-qtapp-start.service - Qt App Start …...

[按键安卓ios脚本辅助插件开发]数组排序函数例子

按键安卓ios工具辅助脚本插件开发教程&#xff0c;教程目的是让大家掌握Lua基本语法与按键精灵手机版的插件开发制作。 在按键精灵中排序需要我们自己写算法实现&#xff0c;例如快速排序&#xff0c;冒泡排序等&#xff0c;而在Lua中有内置的table.sort()排序命令。 这个命令…...

【BotSharp框架示例 ——实现聊天机器人,并通过 DeepSeek V3实现 function calling】

BotSharp框架示例 ——实现聊天机器人&#xff0c;并通过 DeepSeek V3实现 function calling 一、一点点感悟二、创建项目1、创建项目2、添加引用3、MyWeatherPlugin项目代码编写4、WeatherApiDefaultService项目代码编写5、WebAPI MyWeatherAPI 的项目代码编写6、data文件夹中…...

记录一个单独读取evt.bdf的方法

问题描述 之前只能使用eeglab的工具&#xff0c;读取博瑞康达的data.bdf和evt.bdf时&#xff0c;使用的是eeglab的下面这个读取文件的插件。 evt.bdf使用记事本文件查看是乱码的形式。 实现方法 事实上&#xff0c;我们可以直接对这个文件的16进制进行解析。 对文件的位和…...

是否想要一个桌面哆啦A梦的宠物

是否想拥有一个在指定时间喊你的桌面宠物呢&#xff08;手动狗头&#xff09; 如果你有更好的想法&#xff0c;欢迎提出你的想法。 是否考虑过跟开发者一对一&#xff0c;提出你的建议&#xff08;狗头&#xff09;。 https://wwxc.lanzouo.com/idKnJ2uvq11c 密码:bbkm...

防爆风扇储能轴流风机风量风压如何保障通风安全?

在化工车间、煤矿巷道等高危环境中&#xff0c;通风安全是保障生产与人员生命安全的关键防线。防爆风扇储能轴流风机凭借独特的风量风压设计与性能优势&#xff0c;成为守护通风安全的可靠屏障。那么&#xff0c;它究竟是如何发挥作用的呢?​ 从风量设计来看&#xff0c;防爆风…...

Centos 7系统 宝塔部署Tomcat项目(保姆级教程)

再看文章之前默认已经安装好系统&#xff0c;可能是云系统&#xff0c;或者是虚拟机。 宝塔安装 这个比较简单&#xff0c;参考这个老哥的即可&#xff1a; https://blog.csdn.net/weixin_42753193/article/details/125959289 环境配置 进入宝塔面板之后会出现环境安装&…...

Electron读取本地文件

在 Electron 应用中&#xff0c;可以使用 Node.js 的 fs 模块来读取本地文件。以下是如何实现这一功能的详细步骤。 1. 设置项目结构 假设你的项目目录如下&#xff1a; my-electron-app/ ├── main.js ├── index.html └── renderer.js2. 使用 fs 模块读取文件 在主…...

Plesk 下的 IP 地址管理

Plesk是一个方便管理的控制面板&#xff0c;可以简化网站主机和服务器数据中心的自动化管理。它专为提供Windows和Linux服务器的供应商设计。Plesk面板适用于虚拟主机和独立服务器 服务器管理员可以使用Plesk来配置新网站、电子邮件系统和转售商账户&#xff0c;也可以通过Ple…...

基于STM32、HAL库的DS28E15P安全验证及加密芯片驱动程序设计

一、简介: DS28E15P是Maxim Integrated (现为Analog Devices)生产的一款1-Wire EEPROM芯片,具有以下特点: 1-Wire接口通信,仅需单根数据线加地线 1024位(128字节)EEPROM存储器 每个器件具有唯一的64位ROM ID 工作电压范围:2.8V至5.25V 内置CRC16校验功能 可编程写保护功能…...

浅析localhost、127.0.0.1 和 0.0.0.0的区别

文章目录 三者的解释三者的核心区别总结使用场景示例什么是回环地址常见问题开发工具中的地址使用为什么开发工具同时支持localhost和127.0.0.1&#xff1f;实际应用示例VSCode中的Live Server插件VSCode中的VUE项目IDEA中的Spring Boot应用 最佳实践建议 localhost、 127.0.0…...

antd+react实现html图片预览效果

import { Image } from ‘antd’; import { useEffect, useRef, useState } from ‘react’; import styles from ‘./index.module.less’; interface PreviewHtmlWithImagesProps { htmlContent: string; } const PreviewHtmlWithImages: React.FC ({ htmlContent }) >…...

【React】轻松掌握 React 中 useEffect的使用

你有没有想过&#xff0c;为什么你的 React 组件能够轻松应对周围发生的变化&#xff0c;比如每当有新数据到来时自动更新&#xff0c;或者处理可以动态响应实时事件的组件&#xff1f;这就是 useEffect 的用武之地&#xff01;这个强大的钩子&#xff08;Hook&#xff09;就像…...

请简述一下什么是 Kotlin?它有哪些特性?

1 JVM 语言的共性&#xff1a;编译成字节码文件 Kotlin 和 Java 同属于 JVM&#xff08;Java Virtual Machine&#xff09;语言&#xff0c;它们的代码最终都会被编译成 JVM 字节码&#xff08;.class&#xff09;文件。 编译流程&#xff1a; Kotlin 编译&#xff1a;Kotli…...

Post与Get以及@Requestbody和@Pathvariable标签的应用

Post的使用场景&#xff1a;简单来讲适用于有安全性限制的&#xff0c;因为post请求的内容会被存在某个封装内容中&#xff08;比如表单、jason格式等&#xff09;&#xff0c;这部分内容是不会被浏览器的cache所捕捉&#xff0c;安全性较强。 Get的使用场景&#xff1a;与pos…...

基于tabula对pdf中的excel进行识别并转换成word(三)

上一节中是基于PaddleOCR对图片中的excel进行识别并转换成word优化&#xff0c;本节改变思路&#xff0c;直接从pdf中读取表格的信息&#xff0c;具体思路如下所述。 PDF中的表格数据如下截图所示&#xff1a; 一、基于tabula从PDF中提取表格 df_list tabula.read_pdf("…...

k8s集群环境部署业务系统

k8s集群环境部署业务系统&#xff0c;通过shell脚本整合部署过程&#xff0c;简化部署流程。操作流程如下&#xff1a; A&#xff0c;B为业务系统服务名。 一.部署前准备。在k8s集群各节点执行该脚本&#xff0c;完成业务系统镜像加载。 #!/bin/bash # 1.删除deployment ech…...

MySQL 8.4.4 安全升级指南:从漏洞修复到版本升级全流程解析

目录 二、升级前关键注意事项 1. 数据安全与备份 2. 版本兼容性与路径规划 三、分步升级操作流程 1. 环境预检与准备 2. 安装包部署 3. 强制升级组件 4. 验证与启动 一、背景与必要性 近期安全扫描发现生产环境的 MySQL 数据库存在多个高危漏洞(CVE 详情参见Oracle 官…...

“假读“操作在I2C接收流程中的原因

在I2C接收流程中&#xff0c;"假读"操作是NXP I2C控制器工作特性要求的必要操作&#xff0c;具体原因如下&#xff1a; // 接收函数关键代码 void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size) {// ...dummy base->I2DR; /* 假读 *…...

TA学习之路——2.3图形的HLSL常用函数详解

1.基本数学运算 函数作用max(a,b)返回a,b值中较大的那个min(a,b)返回a,b值中较小的那个mul(a,b两变量相乘,常用于矩阵abs(a)返回a的绝对值sqrt(x)返回x的平方根rsqrt(x)返回x的平方根的倒数degrees(x)将弧度转成角度radians(x)将角度转成弧度noise(x)噪声函数1.1 创建一个测试…...

Python数据容器:数据容器的分类、数据容器特点总结、各数据容器遍历的特点、数据容器通用操作(统计,转换,排序)

目录 数据容器的分类 数据容器特点总结 数据容器遍历的特点 通用操作 通用统计len()、max()、min() 通用转换list()、tuple()、str()、set() 通用排序sorted 数据容器的分类 分类&#xff1a; 是否支持下标索引 支持&#xff1a;列表、元组、字符串-序列类型不支持&…...

FastAPI的发展历史

参考&#xff1a;https://zhuanlan.zhihu.com/p/710831974 FastAPI 于 2019 年发布&#xff0c;由 Sebastian Ramirez 创建。他是 Pydantic 框架的创建者&#xff0c;也是多个开源项目的贡献者。 FastAPI 的设计初衷是为了解决 Python Web 框架在数据类型验证和文档生成方面的问…...

本地大模型编程实战(28)查询图数据库NEO4J(1)

本文将基于langchain 框架&#xff0c;用LLM(大语言模型)查询图数据库NEO4J。 使用 qwen2.5 做实验&#xff0c;用 llama3.1 查不出内容。 文章目录 安装 NEO4J准备图数据查询图数据总结代码 安装 NEO4J 参见&#xff1a;在windows系统中安装图数据库NEO4J 。 准备图数据 我…...

从厨房到云端:从预制菜到云原生

小美&#xff1a;小猿&#xff0c;你最近在忙什么呢&#xff1f;看你总是加班。 小猿&#xff1a;唉&#xff0c;公司在搞什么“云原生”改造&#xff0c;说是要把我们的应用搬到云上&#xff0c;搞得我头都大了。 小美&#xff1a;云原生&#xff1f;听起来很高大上啊&#…...

单片机-89C51部分:9、串行口通讯

飞书文档https://x509p6c8to.feishu.cn/wiki/WSh3wnADkixHspk7kc8c5esRnad 一、什么是串口&#xff1f;它的作用&#xff1f; 串行口&#xff0c;简称为串口&#xff0c;什么是串口&#xff1f;它的作用是什么&#xff1f; 两个人交流&#xff0c;一般通过在说话在空气中产生…...

C++程序退出时的对象析构陷阱:深度解析与避坑指南

C++程序退出时的对象析构陷阱:深度解析与避坑指南​ 一、从诡异案例说起:局部对象为何"神秘消失"?二、全局对象 vs 局部对象1. 全局对象生命周期2. 局部对象生命周期三、程序终止的两种姿势:exit() vs return四、atexit():最后的救命稻草1.基础用法2. 核心特性3…...

为什么 Vite 速度比 Webpack 快?

一、webpack会先进行编译&#xff0c;再运行&#xff0c;vite会直接启动&#xff0c;再按需编译文件。 首先看两张图&#xff0c;可以清晰的看到&#xff0c;上面的图是webpack编译过的&#xff0c;而下面的图是vite直接使用工程内文件。 二、区别于Webpack先打包的方式&am…...

指针变量存放在哪?

指针的存储位置取决于其声明方式和作用域&#xff0c;以下是详细分析&#xff1a; 1. 栈&#xff08;Stack&#xff09; 局部指针变量&#xff1a;在函数内部声明的指针&#xff08;非静态&#xff09;&#xff0c;作为局部变量存储在栈中。void func() {int *p; // p本身存储…...

Kafka 消息可靠性深度解析:大流量与小流量场景下的设计哲学

在分布式消息系统的设计中&#xff0c;消息可靠性保障本质上是系统在一致性、可用性、吞吐量三者之间动态博弈的结果。Kafka作为现代流式架构的核心组件&#xff0c;其消息可靠性机制在不同流量场景下呈现出截然不同的设计哲学。本文将从系统设计原理层面&#xff0c;解构大流量…...

python数据分析(六):Pandas 多数据操作全面指南

Pandas 多数据操作全面指南&#xff1a;Merge, Join, Concatenate 与 Compare 1. 引言 在数据分析工作中&#xff0c;我们经常需要处理多个数据集并将它们以各种方式组合起来。Pandas 提供了多种强大的多数据操作方法&#xff0c;包括合并(merge)、连接(join)、连接(concaten…...

在K8S迁移节点kubelet数据存储目录

默认k8s节点kubelet数据目录在 /var/lib/kubelet&#xff0c;如果在部署前没有做好规划&#xff0c;其实默认就存储在系统盘/分区下了&#xff0c;这样会导致一个问题&#xff0c;如果数据量过大会导致kubelet服务异常&#xff0c;其次&#xff0c;系统盘下有一些系统服务引用&…...

k8s 学习记录 (六)_Pod 污点和容忍性详解

一、前言 在 Kubernetes 集群中&#xff0c;我们已经了解了节点亲和性和 Pod 亲和性&#xff0c;它们在 Pod 调度方面提供了很大的灵活性。今天&#xff0c;我们来探讨另外两个重要的概念 ——Pod 污点&#xff08;Taints&#xff09;和容忍性&#xff08;Tolerations&#xf…...

hutools工具类中isNotEmpty与isNotBlank区分

基于以下两种情况。在判断的变量是String类型时&#xff0c; 判断是否为空&#xff0c;推荐使用isNotBlank(). 1. isNotEmpty 不会验证str中是否含有空字符串&#xff0c;而 isNotBlank方法会验证 public static boolean isNotEmpty(CharSequence str) {return false isEmpty…...

C#解析USB - HID手柄上摇杆按键数据

1. 了解相关知识 ​​HID设备通信原理​​&#xff1a;HID设备通过端点报告其状态和数据&#xff0c;设备通过报告描述符来描述数据用途&#xff0c;操作系统通过这个描述符了解设备发送数据的结构。通常一个完整的报告以特定的格式从设备传输至主机。​​Windows API函数​​…...