从零开始: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,高度撑满父布局 -->
    <ImageView
        android: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" />  <!-- 图标资源 -->

    <!-- 居中标题文本 -->
    <TextView
        android: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,高度撑满父布局 -->
    <ImageView
        android: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" />  <!-- 图标资源 -->

    <!-- 购物车商品数量角标(红色圆形背景+白色数字) -->
    <TextView
        android: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(解决内容超出屏幕时的滚动问题) -->
    <ScrollView
        android:layout_width="match_parent"  <!-- 宽度匹配父容器 -->
        android:layout_height="wrap_content"> <!-- 高度根据内容自适应 -->

        <!-- 网格布局:GridLayout(用于实现2列的网格排列) -->
        <GridLayout
            android: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" />

    <!-- 可滚动视图:用于支持内容超出屏幕时的滚动 -->
    <ScrollView
        android:layout_width="match_parent"  <!-- 宽度匹配父容器 -->
        android:layout_height="wrap_content"> <!-- 高度根据内容自适应 -->

        <!-- 内容容器:垂直方向的LinearLayout,包含商品详情各个元素 -->
        <LinearLayout
            android:layout_width="match_parent"  <!-- 宽度匹配父容器(ScrollView) -->
            android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->
            android:orientation="vertical">      <!-- 子元素垂直排列 -->

            <!-- 商品图片展示区域 -->
            <ImageView
                android:id="@+id/iv_goods_pic"  <!-- 控件ID(用于代码中访问) -->
                android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->
                android:layout_height="350dp"    <!-- 固定高度350dp -->
                android:scaleType="fitCenter" /> <!-- 图片缩放模式:居中适应 -->

            <!-- 商品价格显示 -->
            <TextView
                android: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 -->

            <!-- 商品描述文本 -->
            <TextView
                android: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 -->

            <!-- 加入购物车按钮 -->
            <Button
                android: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" />

    <!-- 可滚动视图:支持内容超出屏幕时的滚动 -->
    <ScrollView
        android:layout_width="match_parent"  <!-- 宽度匹配父容器 -->
        android:layout_height="wrap_content"> <!-- 高度根据内容自适应 -->

        <!-- 相对布局容器:用于切换显示购物车内容/空状态 -->
        <RelativeLayout
            android:layout_width="match_parent"  <!-- 宽度匹配父容器 -->
            android:layout_height="wrap_content"> <!-- 高度根据内容自适应 -->

            <!-- 购物车内容区域(默认显示) -->
            <LinearLayout
                android:id="@+id/ll_content"
                android:layout_width="match_parent"  <!-- 宽度撑满父容器 -->
                android:layout_height="wrap_content"  <!-- 高度根据内容自适应 -->
                android:orientation="vertical"      <!-- 子元素垂直排列 -->
                android:visibility="visible">      <!-- 初始可见 -->

                <!-- 表头布局:水平排列的商品信息标题 -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"> <!-- 子元素水平排列 -->

                    <!-- 图片标题(固定宽度85dp) -->
                    <TextView
                        android:layout_width="85dp"
                        android:layout_height="wrap_content"
                        android:gravity="center"  <!-- 文本居中 -->
                        android:text="图片"
                        android:textColor="@color/black"
                        android:textSize="15sp" />

                    <!-- 商品名称标题(权重3,占比最大) -->
                    <TextView
                        android: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" />

                    <!-- 数量标题 -->
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:gravity="center"
                        android:text="数量"
                        android:textColor="@color/black"
                        android:textSize="15sp" />

                    <!-- 单价标题 -->
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:gravity="center"
                        android:text="单价"
                        android:textColor="@color/black"
                        android:textSize="15sp" />

                    <!-- 总价标题 -->
                    <TextView
                        android: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>

                <!-- 动态内容容器:用于代码中添加购物车商品条目 -->
                <LinearLayout
                    android:id="@+id/ll_cart"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical" />

                <!-- 底部操作栏:水平排列 -->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:padding="0dp">  <!-- 去除内边距 -->

                    <!-- 清空按钮 -->
                    <Button
                        android: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" />

                    <!-- 占位文本(自动扩展剩余空间) -->
                    <TextView
                        android: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" />

                    <!-- 总金额显示(红色突出) -->
                    <TextView
                        android: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" />          <!-- 大号字体 -->

                    <!-- 结算按钮 -->
                    <Button
                        android: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>

            <!-- 空状态提示区域(默认隐藏) -->
            <LinearLayout
                android:id="@+id/ll_empty"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:visibility="gone">  <!-- 初始不可见 -->

                <!-- 提示文本(上下外边距各100dp) -->
                <TextView
                    android: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" />

                <!-- 跳转按钮 -->
                <Button
                    android: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">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="@color/black"
        android:textSize="17sp" />

    <ImageView
        android:id="@+id/iv_thumb"
        android:layout_width="180dp"
        android:layout_height="150dp"
        android:scaleType="fitCenter" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:orientation="horizontal">

        <TextView
            android: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" />

        <Button
            android: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">

    <ImageView
        android:id="@+id/iv_thumb"
        android:layout_width="85dp"
        android:layout_height="85dp"
        android:scaleType="fitCenter" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"
        android:orientation="vertical">

        <TextView
            android: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" />

        <TextView
            android: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>

    <TextView
        android: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" />

    <TextView
        android: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" />

    <TextView
        android: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-preferences
    implementation 'androidx.datastore:datastore-preferences:1.0.0'
    // datastore库各版本见 https://mvnrepository.com/artifact/androidx.datastore/datastore-rxjava2
    implementation 'androidx.datastore:datastore-preferences-rxjava2:1.0.0'
    def room_version = "2.5.0" // 请使用最新版本
    // room库各版本见 https://mvnrepository.com/artifact/androidx.room/room-runtime
    implementation "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); // 插入一条购物车信息

    @Insert
    void insertCartList(List<CartInfo> cartList); // 插入多条购物车信息

    @Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录
    int updateCart(CartInfo cart); // 更新购物车信息

    @Delete
    void 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); // 插入一条商品信息

    @Insert
    void insertGoodsList(List<GoodsInfo> goodsList); // 插入多条商品信息

    @Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录
    int updateGoods(GoodsInfo goods); // 更新商品信息

    @Delete
    void 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; // 声明一个商品的持久化对象

    @Override
    protected 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);
        }
    }

    @Override
    protected 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;        // 商品数据库访问对象(用于操作商品数据)

    @Override
    protected 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之上的所有Activity
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 
            startActivity(intent);
        });
        
        // 显示当前购物车商品总数
        tv_count.setText("" + MainApplication.goodsCount);
        
        // 从全局Application中获取数据库访问对象
        cartDao = MainApplication.getInstance().getCartDB().cartDao();  // 购物车DAO
        goodsDao = 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 + "到购物车");
    }

    @Override
    protected 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());  // 传递商品ID
                startActivity(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; // 声明一个商品的持久化对象

    @Override
    protected 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, "成功添加至购物车");
    }

    @Override
    protected 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; // 声明一个商品的持久化对象

    @Override
    protected 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);
        }
    }

    @Override
    protected 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开发的基础知识,还深入探讨了性能优化的实用方案。无论是技术实现还是实战经验,都为后续开发提供了有价值的参考。

🙌 求点赞、收藏、关注! 

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二七有头发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值