Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android Compose 新闻App(三)网络数据Compose UI显示加载、Room和DataStore使用

Android Compose 新闻App(三)网络数据Compose UI显示加载、Room和DataStore使用

作者头像
晨曦_LLW
发布于 2022-04-15 09:33:15
发布于 2022-04-15 09:33:15
3.5K01
代码可运行
举报
运行总次数:1
代码可运行

Compose 新闻App(三)网络数据Compose UI显示加载、DataStore和Room使用

前言

  现在数据已经有了,现在主要就是Compose UI的设计。完成本篇文章,效果图如下:

正文

  后面的内容涉及到样式布局组件,内容比较多。

一、样式

在这里我们先进行样式的配置,打开ui.theme文件夹。 首先是修改Color.kt文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
val Blue200 = Color(0xFF979FF2)
val Blue300 = Color(0xFF6D7DEA)
val Blue700 = Color(0xFF0068C2)
val Blue800 = Color(0xFF0059A5)
val Blue900 = Color(0xFF004076)

然后是Shape.kt文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(4.dp),
    large = RoundedCornerShape(8.dp)
)

再是Theme.kt文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private val LightColorPalette = lightColors(
    primary = Blue700,
    primaryVariant = Blue900,
    onPrimary = Color.White,
    secondary = Blue700,
    secondaryVariant = Blue900,
    onSecondary = Color.White,
    error = Blue800,
    onBackground = Color.Black
)

private val DarkColorPalette = darkColors(
    primary = Blue300,
    primaryVariant = Blue700,
    onPrimary = Color.Black,
    secondary = Blue300,
    onSecondary = Color.Black,
    error = Blue200,
    onBackground = Color.White
)

@Composable
fun GoodNewsTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
    MaterialTheme(
        colors = if (darkTheme) DarkColorPalette else LightColorPalette,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

最后是Type.kt文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

val JetnewsTypography = Typography(
    defaultFontFamily = Montserrat,
    h4 = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 30.sp,
        letterSpacing = 0.sp
    ),
    h5 = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 24.sp,
        letterSpacing = 0.sp
    ),
    h6 = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 20.sp,
        letterSpacing = 0.sp
    ),
    subtitle1 = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 16.sp,
        letterSpacing = 0.15.sp
    ),
    subtitle2 = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        letterSpacing = 0.1.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        letterSpacing = 0.5.sp
    ),
    body2 = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        letterSpacing = 0.25.sp
    ),
    button = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 14.sp,
        letterSpacing = 1.25.sp
    ),
    caption = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 12.sp,
        letterSpacing = 0.4.sp
    ),
    overline = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 12.sp,
        letterSpacing = 1.sp
    )
)

这个文件中有一些字体文件,在我项目的res下。

当然你也可以不用这些字体。

下面我们再res文件夹下创建一个values-night文件夹,在里面创建一个colors.xml。里面的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<resources>
    <color name="blue700">#0068C2color>
    <color name="blue900">#004076color>
    <color name="status_bar">#0E0E0Ecolor>
resources>

再去修改values下的colors.xml。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<resources>
    <color name="blue700">#0068C2color>
    <color name="blue900">#004076color>
    <color name="status_bar">#0068C2color>
resources>

最后修改values.xml下的theme.xml,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<resources>
    <style name="Theme.GoodNews" parent="android:Theme.Material.Light.NoActionBar">
        "android:colorPrimary">@color/blue700
        "android:colorPrimaryDark">@color/blue900
        "android:colorAccent">@color/blue700

        "android:statusBarColor">@color/status_bar
        "android:background">@color/status_bar
    style>
resources>

二、Scaffold(脚手架)

  你可能是第一次看到这个玩意。Compose 附带内置的 Material 组件可组合项,您可以用他们创建应用。最高级别的可组合项是 Scaffold。Scaffold 可让您实现具有基本 Material Design 布局结构的界面。Scaffold 可以为最常见的顶层 Material 组件(例如 TopAppBar、BottomAppBar、FloatingActionButton 和 Drawer)提供槽位。使用 Scaffold 时,您可以确保这些组件能够正确放置并协同工作。这是它里面提供的一些参数

  你或许听说过Compose是声明式UI,但是更多的是插槽 API,插槽 API 是 Compose 引入的一种模式,它在可组合项的基础上提供了一层自定义设置。那么什么是插槽API呢?比如一个Button中有图标和文字,对应的就是Icon和Text,你可以认为这就是插槽。

理论的东西说了很多了,下面来实践一下。在MainActivity.kt中增加一个MainScreen函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Composable
private fun MainScreen() {
    Scaffold {
        
    }
}

然后在setContent和DefaultPreview中调用,下面我们预览一下:

一篇空白,我们可以把这个Scaffold当成是一个布局。下面我们新增一个TopAppBar

三、TopAppBar(顶部应用栏)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
			//顶部应用栏
            TopAppBar(
                title = {
                    Text(
                        text = stringResource(id = R.string.app_name),
                        modifier = Modifier.fillMaxWidth(),
                        textAlign = TextAlign.Center,
                        color = MaterialTheme.colors.onSecondary
                    )
                }
            )

这里的TopAppBar中设置title参数,然后写一个Text的插槽,设置文字、控件宽度、文字摆放位置、颜色。 下面预览一下:

预览的时候看不到状态栏,我们可以通过真机或者虚拟机来看一下效果。

① 属性值

这里的属性有几个是可以传入插槽的,就是有@Composable注解的,比如我们设置一下navigationIcon和action。

这里我们看到navigationIcon和actions的里面都有一个IconButton,这表示这个图标是可以点击的,然后我们设置点击事件,弹一个Toast,这里是一个扩展函数,我们在utils包下新建一个ToastUtils类,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fun String.showToast() = Toast.makeText(App.context, this, Toast.LENGTH_SHORT).show()

fun String.showLongToast() = Toast.makeText(App.context, this, Toast.LENGTH_LONG).show()

fun Int.showToast() = Toast.makeText(App.context, this, Toast.LENGTH_SHORT).show()

fun Int.showLongToast() = Toast.makeText(App.context, this, Toast.LENGTH_LONG).show()

然后我们来解释一下找个Icon里面的内容,Icons.Filled.Person表示的是一个填充的Person图标,它里面是通过Path去绘制的,Icons是androidx.compose.material.icons依赖库里面的,因此不需要我们自己去写,都是material风格的图标。contentDescription就是一个描述,就是说明这个内容是什么意思,不是很重要。下面我们运行一下:

四、列表

我们现在有标题栏了,下面我们写页面主要内容,下面我们在MainActivity.kt中新增一个BodyContent()函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(8.dp)) {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

① 显示列表

这个函数需要在MainScreen()函数中调用。

下面运行一下:

② 滑动列表

你会发现你滑动不了,我们只需要加一行代码就可以滑动了,如下图所示:

通过modifier的链式调用verticalScroll()函数,再传进去rememberScrollState()。你可能又会问了,那横向滚动呢?为了区分一下,我再改了这个BodyContent函数。

下面我们运行一下:

好了,现在我们已经掌握了列表的基本使用了,下面我们加上网络请求返回的数据来看。

这里我们就显示这个news的数组数据。

③ 加载网络数据

之前在initData中进行数据请求的返回处理,拿到了返回值,如下图所示:

这里层层传值到BodyContent函数中,在这个函数中我们就来显示数据,函数的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Composable
fun BodyContent(news: List<NewsItem>, modifier: Modifier = Modifier) {
    LazyColumn(
        state = rememberLazyListState(),
        modifier = modifier.padding(8.dp)
    ) {
        items(news) { new ->
            Column(modifier = Modifier.padding(8.dp)) {
                Text(
                    text = new.title,
                    fontWeight = FontWeight.ExtraBold,
                    fontSize = 16.sp,
                    modifier = Modifier.padding(0.dp, 10.dp)
                )
                Text(text = new.summary, fontSize = 12.sp)
                Row(modifier = Modifier.padding(0.dp, 10.dp)) {
                    Text(text = new.infoSource, fontSize = 12.sp)
                    Text(
                        text = new.pubDateStr,
                        fontSize = 12.sp,
                        modifier = Modifier.padding(8.dp, 0.dp)
                    )
                }
            }
            Divider(
                modifier = Modifier.padding(horizontal = 8.dp),
                color = colorResource(id = R.color.black).copy(alpha = 0.08f)
            )
        }
    }
}

看起来内容比较多啊,说明一下:

首先是这个LazyColumn,LazyColumn,它只会渲染界面上的可见项,因而有助于提升性能,而且无需使用 scroll 修饰符。Jetpack Compose 中的 LazyColumn 等同于 Android 视图中的 RecyclerView。这里的state就使用rememberLazyListState()。 那么这里就说完了。

  这个items里面就是显示数据,然后我们构建item的布局,常规的属性值就没啥好说的,这里就说一下这个Divider,这就是一个分隔线。我们增加一个左右填充,然后设置分隔线的颜色,这里用了一个black色值,就是#000000,在colors.xml中添加即可,然后设置这个颜色值的透明度,太亮了不好看。

然后你需要在setContent中添加initData()的调用

下面我们运行一下:

这样写代码是不是很简单呢?

五、Room使用

  现在数据有了,那么为了减少接口API的访问次数,我们需要将数据存储到本地数据库中,我们可以在每天访问两次或一次接口,然后其余的访问都从数据库中去获取数据。这样是不是很好呢?这里我们使用的是Room数据库,它在Java和Kotlin中使用的方式有点点变化,总体区别不大。

① 添加依赖

要使用Room,首先是添加依赖,现在项目的build.gradle中定义好Room数据库的依赖版本:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
room_version = '2.3.0'

然后到app模块下的build.gradle中的dependencies{}闭包中去添加依赖:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//Room数据库
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

如下图所示:

然后Sync Now即可,相比于Hilt来说,你会觉得Room的引入更简单了,这里的room-ktx库是是对Kotlin协程的支持。Java使用时没有这个库。

② 基础配置

下面我们来使用它,首先是实体Bean,在com.llw.goodnews包下新建db包,然后将bean包移动到db包下,打开EpidemicNews类,

添加两个注解,然后我们添加接口,在db包下新建一个dao包,dao包下新建一个NewsItemDao接口,里面的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Dao
interface NewsItemDao {

    @Query("SELECT * FROM newsitem")
    fun getAll(): List<NewsItem>

    @Insert
    fun insertAll(newsItem: List<NewsItem>?)

    @Query("DELETE FROM newsitem")
    fun delete()
}

最后在db包下创建一个AppDatabase用于处理数据库,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Database(entities = [NewsItem::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {

    abstract fun newsItemDao(): NewsItemDao

    companion object {

        @Volatile
        private var instance: AppDatabase? = null

        private const val DATABASE_NAME = "good_news.db"

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build().also { instance = it }
            }
        }
    }
}

这里很简单的代码,也没啥好说的,就是初始化,然后单例。下面进入到App中,如下所示配置

③ 使用

  这里我们存储的数据表是NewsItem,但是网络请求返回的是EpidemicNews,因此我们要改一下返回的数据,改的话就在EpidemicNewsRepository中,这里我们请求成功之后返回的是epidemicNews,如下图所示:

然后我们增加两行代码:

这里就是拿到数据之后保存到本地数据库中,为什么要先删除呢?因为我要保证每次拿到的数据都是当前最新的并且和网络返回的数据一样。然后我们回到MainActivity.kt中,先运行一次,保证我们的数据库中有数据保存之后,再按照如下图所示的代码去改动。

这就是说当我的数据库中有数据了,那么就从本地数据库中去获取数据显示在UI上,运行一下:

你会发现报错了,报错的原因就是我标注的这里,大意就是无法在主线程中访问数据库,那么也好解决,在Room上加一个配置就可以了。打开AppDatabase,如下图所示修改一下即可。

下面再运行一下就可以了。不过我们依然要去解决在主线程中访问数据库的问题,这个后面再说,现在你会觉得这样切换太麻烦了,先请求一次网络,然后改一下代码再去请求数据库,这也太low了,不行,绝对不行。下面我们改一下,通过代码来解决这个问题。

六、DataStore使用

  刚才的问题可以通过什么方式去解决呢?本地缓存,在Android中提到缓存,你最开始想到的就是SP(SharedPreferences),然后是腾讯的MMKV,再是DataStore,这三者是先后顺序出现的,也许你还不知道DataStore是什么,没关系,我这里也不会讲的,哈哈哈。是不是很意外。当然了你不了解可以去看看Android Jetpack组件 DataStore的使用和简单封装,看完了你就知道怎么用了,当然你也可以不用看,因为实际上我们的用法和SP差不多,都是封装成工具类来使用,在那篇文章中就是这样封装,在这里就直接拿来用。

① 添加依赖

  DataStore也是Jetpack的组件,因此我们使用的话也是需要添加依赖的。首先依然是在项目的build.gradle中添加依赖版本

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
datastore_version = '1.0.0'

然后是在app的build.gradle中的dependencies{}闭包中添加如下依赖:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//DataStore
    implementation "androidx.datastore:datastore-preferences:$datastore_version"
    implementation "androidx.datastore:datastore-preferences-core:$datastore_version"

位置如下图所示:

然后Sync Now。

② 封装

首先在App中增加如下代码

我们在utils包下新建一个EasyDataStore.kt,里面的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
object EasyDataStore {

    // 创建DataStore
    private val App.dataStore: DataStore<Preferences> by preferencesDataStore(name = "GoodNews")

    // DataStore变量
    private val dataStore = App.instance.dataStore

    /**
     * 存数据
     */
    fun <T> putData(key: String, value: T) {
        runBlocking {
            when (value) {
                is Int -> putIntData(key, value)
                is Long -> putLongData(key, value)
                is String -> putStringData(key, value)
                is Boolean -> putBooleanData(key, value)
                is Float -> putFloatData(key, value)
                is Double -> putDoubleData(key, value)
                else -> throw IllegalArgumentException("This type cannot be saved to the Data Store")
            }
        }
    }

    /**
     * 取数据
     */
    fun <T> getData(key: String, defaultValue: T): T {
        val data = when (defaultValue) {
            is Int -> getIntData(key, defaultValue)
            is Long -> getLongData(key, defaultValue)
            is String -> getStringData(key, defaultValue)
            is Boolean -> getBooleanData(key, defaultValue)
            is Float -> getFloatData(key, defaultValue)
            is Double -> getDoubleData(key, defaultValue)
            else -> throw IllegalArgumentException("This type cannot be saved to the Data Store")
        }
        return data as T
    }



    /**
     * 存放Int数据
     */
    private suspend fun putIntData(key: String, value: Int) = dataStore.edit {
        it[intPreferencesKey(key)] = value
    }

    /**
     * 存放Long数据
     */
    private suspend fun putLongData(key: String, value: Long) = dataStore.edit {
        it[longPreferencesKey(key)] = value
    }

    /**
     * 存放String数据
     */
    private suspend fun putStringData(key: String, value: String) = dataStore.edit {
        it[stringPreferencesKey(key)] = value
    }

    /**
     * 存放Boolean数据
     */
    private suspend fun putBooleanData(key: String, value: Boolean) = dataStore.edit {
        it[booleanPreferencesKey(key)] = value
    }

    /**
     * 存放Float数据
     */
    private suspend fun putFloatData(key: String, value: Float) = dataStore.edit {
        it[floatPreferencesKey(key)] = value
    }

    /**
     * 存放Double数据
     */
    private suspend fun putDoubleData(key: String, value: Double) = dataStore.edit {
        it[doublePreferencesKey(key)] = value
    }

    /**
     * 取出Int数据
     */
    private fun getIntData(key: String, default: Int = 0): Int = runBlocking {
        return@runBlocking dataStore.data.map {
            it[intPreferencesKey(key)] ?: default
        }.first()
    }

    /**
     * 取出Long数据
     */
    private fun getLongData(key: String, default: Long = 0): Long = runBlocking {
        return@runBlocking dataStore.data.map {
            it[longPreferencesKey(key)] ?: default
        }.first()
    }

    /**
     * 取出String数据
     */
    private fun getStringData(key: String, default: String? = null): String = runBlocking {
        return@runBlocking dataStore.data.map {
            it[stringPreferencesKey(key)] ?: default
        }.first()!!
    }

    /**
     * 取出Boolean数据
     */
    private fun getBooleanData(key: String, default: Boolean = false): Boolean = runBlocking {
        return@runBlocking dataStore.data.map {
            it[booleanPreferencesKey(key)] ?: default
        }.first()
    }

    /**
     * 取出Float数据
     */
    private fun getFloatData(key: String, default: Float = 0.0f): Float = runBlocking {
        return@runBlocking dataStore.data.map {
            it[floatPreferencesKey(key)] ?: default
        }.first()
    }

    /**
     * 取出Double数据
     */
    private fun getDoubleData(key: String, default: Double = 0.00): Double = runBlocking {
        return@runBlocking dataStore.data.map {
            it[doublePreferencesKey(key)] ?: default
        }.first()
    }
}

这个工具类我就不多解释了,代码也不难,你可能只是不了解而已,也就是协程和DataStore的配合使用。下面我们怎么把这个用到刚才所说的问题中呢?

③ 使用

  首先先说一下业务逻辑,通过一个缓存值记录当天是否有请求网络API接口,没有请求就从网络中返回数据,然后保存到数据库中,第二次请求这个缓存值就有效果了,那么就从本地数据库中返回数据。这样就可以了,好了下面我们来使用吧。

这里我们需要在定义常量,在Constant中增加如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	/**
     * 今日请求接口返回数据的时间戳
     */
    const val REQUEST_TIMESTAMP = "requestTimestamp_news"

然后我们回到EpidemicNewsRepository中

因为我们要在这里判断数据是从本地来还是网络来,这里我们通过时间戳来处理。如果当前时间小于缓存中的时间,则从本地数据库获取,反之从网络中获取,这里我们创建一个工具类,在utils包下,新建一个EasyDate.kt,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
object EasyDate {
    private const val STANDARD_TIME = "yyyy-MM-dd HH:mm:ss"
    private const val FULL_TIME = "yyyy-MM-dd HH:mm:ss.SSS"
    private const val YEAR_MONTH_DAY = "yyyy-MM-dd"
    private const val YEAR_MONTH_DAY_CN = "yyyy年MM月dd号"
    private const val HOUR_MINUTE_SECOND = "HH:mm:ss"
    private const val HOUR_MINUTE_SECOND_CN = "HH时mm分ss秒"
    private const val YEAR = "yyyy"
    private const val MONTH = "MM"
    private const val DAY = "dd"
    private const val HOUR = "HH"
    private const val MINUTE = "mm"
    private const val SECOND = "ss"
    private const val MILLISECOND = "SSS"
    private const val YESTERDAY = "昨天"
    private const val TODAY = "今天"
    private const val TOMORROW = "明天"
    private const val SUNDAY = "星期日"
    private const val MONDAY = "星期一"
    private const val TUESDAY = "星期二"
    private const val WEDNESDAY = "星期三"
    private const val THURSDAY = "星期四"
    private const val FRIDAY = "星期五"
    private const val SATURDAY = "星期六"
    private val weekDays = arrayOf(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY)

    /**
     * 获取标准时间
     *
     * @return 例如 2021-07-01 10:35:53
     */
    val dateTime: String get() = SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(Date())

    /**
     * 获取完整时间
     *
     * @return 例如 2021-07-01 10:37:00.748
     */
    val fullDateTime: String get() = SimpleDateFormat(FULL_TIME, Locale.CHINESE).format(Date())

    /**
     * 获取年月日(今天)
     *
     * @return 例如 2021-07-01
     */
    val theYearMonthAndDay: String
        get() = SimpleDateFormat(YEAR_MONTH_DAY, Locale.CHINESE).format(Date())

    /**
     * 获取年月日
     *
     * @return 例如 2021年07月01号
     */
    val theYearMonthAndDayCn: String
        get() = SimpleDateFormat(YEAR_MONTH_DAY_CN, Locale.CHINESE).format(Date())

    /**
     * 获取年月日
     * @param delimiter 分隔符
     * @return 例如 2021年07月01号
     */
    fun getTheYearMonthAndDayDelimiter(delimiter: CharSequence): String =
        SimpleDateFormat(YEAR + delimiter + MONTH + delimiter + DAY, Locale.CHINESE).format(Date())

    /**
     * 获取时分秒
     *
     * @return 例如 10:38:25
     */
    val hoursMinutesAndSeconds: String get() = SimpleDateFormat(HOUR_MINUTE_SECOND, Locale.CHINESE).format(Date())

    /**
     * 获取时分秒
     *
     * @return 例如 10时38分50秒
     */
    val hoursMinutesAndSecondsCn: String get() = SimpleDateFormat(HOUR_MINUTE_SECOND_CN, Locale.CHINESE).format(Date())

    /**
     * 获取时分秒
     * @param delimiter 分隔符
     * @return 例如 2021/07/01
     */
    fun getHoursMinutesAndSecondsDelimiter(delimiter: CharSequence): String =
        SimpleDateFormat(HOUR + delimiter + MINUTE + delimiter + SECOND, Locale.CHINESE).format(Date())

    /**
     * 获取年
     *
     * @return 例如 2021
     */
    val year: String get() = SimpleDateFormat(YEAR, Locale.CHINESE).format(Date())

    /**
     * 获取月
     *
     * @return 例如 07
     */
    val month: String get() = SimpleDateFormat(MONTH, Locale.CHINESE).format(Date())

    /**
     * 获取天
     *
     * @return 例如 01
     */
    val day: String get() = SimpleDateFormat(DAY, Locale.CHINESE).format(Date())

    /**
     * 获取小时
     *
     * @return 例如 10
     */
    val hour: String get() = SimpleDateFormat(HOUR, Locale.CHINESE).format(Date())

    /**
     * 获取分钟
     *
     * @return 例如 40
     */
    val minute: String get() = SimpleDateFormat(MINUTE, Locale.CHINESE).format(Date())

    /**
     * 获取秒
     *
     * @return 例如 58
     */
    val second: String get() = SimpleDateFormat(SECOND, Locale.CHINESE).format(Date())

    /**
     * 获取毫秒
     *
     * @return 例如 666
     */
    val milliSecond: String get() = SimpleDateFormat(MILLISECOND, Locale.CHINESE).format(Date())

    /**
     * 获取时间戳
     *
     * @return 例如 1625107306051
     */
    val timestamp: Long get() = System.currentTimeMillis()

    /**
     * 将时间转换为时间戳
     *
     * @param time 例如 2021-07-01 10:44:11
     * @return 1625107451000
     */
    fun dateToStamp(time: String?): Long {
        val simpleDateFormat = SimpleDateFormat(STANDARD_TIME, Locale.CHINESE)
        var date: Date? = null
        try {
            date = simpleDateFormat.parse(time)
        } catch (e: ParseException) {
            e.printStackTrace()
        }
        return Objects.requireNonNull(date)!!.time
    }

    /**
     * 将时间戳转换为时间
     *
     * @param timeMillis 例如 1625107637084
     * @return 例如 2021-07-01 10:47:17
     */
    fun stampToDate(timeMillis: Long): String = SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(Date(timeMillis))

    /**
     * 获取第二天凌晨0点时间戳
     * @return
     */
    fun getMillisNextEarlyMorning(): Long {
        val cal = Calendar.getInstance()
        //日期加1
        cal.add(Calendar.DAY_OF_YEAR, 1)
        //时间设定到0点整
        cal[Calendar.HOUR_OF_DAY] = 0
        cal[Calendar.SECOND] = 0
        cal[Calendar.MINUTE] = 0
        cal[Calendar.MILLISECOND] = 0
        return cal.timeInMillis
    }

    /**
     * 获取今天是星期几
     *
     * @return 例如 星期四
     */
    val todayOfWeek: String
        get() {
            val cal = Calendar.getInstance()
            cal.time = Date()
            var index = cal[Calendar.DAY_OF_WEEK] - 1
            if (index < 0) {
                index = 0
            }
            return weekDays[index]
        }

    /**
     * 根据输入的日期时间计算是星期几
     *
     * @param dateTime 例如 2021-06-20
     * @return 例如 星期日
     */
    fun getWeek(dateTime: String): String {
        val cal = Calendar.getInstance()
        if ("" == dateTime) {
            cal.time = Date(System.currentTimeMillis())
        } else {
            val sdf = SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault())
            var date: Date?
            try {
                date = sdf.parse(dateTime)
            } catch (e: ParseException) {
                date = null
                e.printStackTrace()
            }
            if (date != null) {
                cal.time = Date(date.time)
            }
        }
        return weekDays[cal[Calendar.DAY_OF_WEEK] - 1]
    }

    /**
     * 获取输入日期的昨天
     *
     * @param date 例如 2021-07-01
     * @return 例如 2021-06-30
     */
    fun getYesterday(date: Date?): String {
        var date = date
        val calendar: Calendar = GregorianCalendar()
        calendar.time = date
        calendar.add(Calendar.DATE, -1)
        date = calendar.time
        return SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault()).format(date)
    }

    /**
     * 获取输入日期的明天
     *
     * @param date 例如 2021-07-01
     * @return 例如 2021-07-02
     */
    fun getTomorrow(date: Date?): String {
        var date = date
        val calendar: Calendar = GregorianCalendar()
        calendar.time = date
        calendar.add(Calendar.DATE, +1)
        date = calendar.time
        return SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault()).format(date)
    }

    /**
     * 根据年月日计算是星期几并与当前日期判断  非昨天、今天、明天 则以星期显示
     *
     * @param dateTime 例如 2021-07-03
     * @return 例如 星期六
     */
    fun getDayInfo(dateTime: String): String {
        val dayInfo: String
        val yesterday = getYesterday(Date())
        val today = theYearMonthAndDay
        val tomorrow = getTomorrow(Date())
        dayInfo = if (dateTime == yesterday) {
            YESTERDAY
        } else if (dateTime == today) {
            TODAY
        } else if (dateTime == tomorrow) {
            TOMORROW
        } else {
            getWeek(dateTime)
        }
        return dayInfo
    }

    //把日期设置为当月第一天
    //日期回滚一天,也就是最后一天
    /**
     * 获取本月天数
     *
     * @return 例如 31
     */
    val currentMonthDays: Int
        get() {
            val calendar = Calendar.getInstance()
            //把日期设置为当月第一天
            calendar[Calendar.DATE] = 1
            //日期回滚一天,也就是最后一天
            calendar.roll(Calendar.DATE, -1)
            return calendar[Calendar.DATE]
        }

    /**
     * 获得指定月的天数
     *
     * @param year  例如 2021
     * @param month 例如 7
     * @return 例如 31
     */
    fun getMonthDays(year: Int, month: Int): Int {
        val calendar = Calendar.getInstance()
        calendar[Calendar.YEAR] = year
        calendar[Calendar.MONTH] = month - 1
        //把日期设置为当月第一天
        calendar[Calendar.DATE] = 1
        //日期回滚一天,也就是最后一天
        calendar.roll(Calendar.DATE, -1)
        return calendar[Calendar.DATE]
    }
}

然后回到EpidemicNewsRepository中,我们新增两个函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	/**
     * 保存到本地数据库
     */
    private fun saveNews(epidemicNews: EpidemicNews) {
        Log.d(TAG, "saveNews: 保存到本地数据库")
        EasyDataStore.putData(REQUEST_TIMESTAMP, EasyDate.getMillisNextEarlyMorning())
        App.db.newsItemDao().deleteAll()
        App.db.newsItemDao().insertAll(epidemicNews.newslist?.get(0)?.news)
    }

    /**
     * 从本地数据库中加载
     */
    private fun getLocalForNews() = EpidemicNews(SUCCESS, CODE, listOf(NewslistItem(App.db.newsItemDao().getAll(), null, null)))

一个是保存到数据库,一个是从数据库中获取数据,这里是相当于构建了的EpidemicNews对象,因此我们要改一下EpidemicNews中的变量修饰符,还有就是可空类型,如下图所示:

改好之后,再回到EpidemicNewsRepository中去改造getEpidemicNews()函数,代码如下:

然后我们再回到MainActivity.kt中

下面运行一下,我们看看日志:

你会发现从网络中获取了数据,那么再运行一次看看:

从数据库中获取了数据。你可以看到其实我们现在已经避免了从主线程中访问数据库了,下面我们去掉AppDatabase中的allowMainThreadQueries()

然后你可以再运行确认一下,其实也不用去确认,因为我们现在就是在协程中访问数据库。 但是为了更规范的使用,我们看一下在NewsItemDao接口的方法前面加一个suspend,如下图所示:

再回到EpidemicNewsRepository中,如下图所示加一个suspend即可。

下面你再运行还是一样的。

好了,本文章就到这里了。

七、源码

GitHub:GoodNews CSDN:GoodNews_3.rar

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/04/13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
EF Core如何处理多对多关系
EF Core在处理多对多关系时并不像一对一和一对多关系那样好处理,下面我们利用一个简单的电子商城购物车来讲解一下吧。
喵叔
2021/11/24
2.5K0
如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计。现在把剩余的“卖”这个动作给做了。这里提醒一下,正常情况下,我们的每一步业务设计都需要和领域专家进行沟通,尽可能的符合通用语言的表述。这里的领域专家包括但不限于当前开发团队中对这块业务最了解的开发人员、系统实际的使用人等。
Zachary_ZF
2018/09/10
9740
如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
C#/.NET/.NET Core开发实战
在本文中,我们将通过一个实战项目来展示如何使用C#、.NET和.NET Core来构建一个简单的电子商务平台。这个项目将涵盖后端API的开发、数据库设计、以及前端页面的基本呈现。
Michel_Rolle
2024/10/09
3.4K0
实战丨云开发商城小程序(附源码)
* 本文包含较多代码片段,PC端浏览推荐前往:https://cloud.tencent.com/document/product/876/70253
腾讯云开发TCB
2022/03/10
7K0
实战丨云开发商城小程序(附源码)
初探领域驱动设计(1)为复杂业务而生
概述   领域驱动设计也就是3D(Domain-Driven Design)已经有了10年的历史,我相信很多人或多或少都听说过这个名词,但是有多少人真正懂得如何去运用它,或者把它运用好呢?于是有人说,DDD和TDD这些玩意是一些形而上的东西,只是一茶余饭后的谈资,又或是放到简历上提升逼格而已。前面这句话我写完之后犹豫了,犹豫要不要把它删掉,因为它让我看起来像个喷子,我确实感到不解,为什么别人10年前创造总结出来的东西,我们在10年之后对它的理解还处于这么低的一个层次。开篇就说远了,我也是最近才开始认真学习领
用户1153966
2018/03/14
1.1K0
初探领域驱动设计(1)为复杂业务而生
Java 20 的新功能入门
Java是一种广泛应用于软件开发的编程语言,自诞生以来不断演进和改进。每个新版本都引入了一些新的功能和改进,帮助开发者更轻松地构建可靠的和高效的应用程序。 在本篇博客文章中,我们将重点介绍Java 20中引入的新功能,帮助读者了解并入门这些新特性。
大盘鸡拌面
2023/11/02
2990
如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能。本篇准备把剩下的购物车的基本概念一次处理完。
Zachary_ZF
2018/09/10
9110
如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
【愚公系列】2022年01月 Django商城项目 30-购物车功能实现
文章目录 一、添加购物车 1.后端逻辑代码 2.前台请求接口代码 3.实际效果 二、获取购物车 1.后端逻辑代码 2.前台页面代码 3.实际效果 三、更新购物车 1.后端逻辑代码 2.前台页面代码 3.实际效果 四、删除购物车 1.后端逻辑代码 2.前台页面代码 五、合并购物车 一、添加购物车 1.后端逻辑代码 """ 一 前后端需求分析需求 前端需要收集: 商品id,商品数量, 选中是可选的(默认就是选中) 如果用户登陆了则请求携带session id
愚公搬代码
2022/02/04
6380
【愚公系列】2022年01月 Django商城项目 30-购物车功能实现
【Vue H5项目实战】从0到1的肯德基点餐系统—— 商品与购物车逻辑设计(Vue3.2 + Vite + TS + Vant + Pinia + Nodejs
文章链接:https://cloud.tencent.com/developer/article/2473693
中杯可乐多加冰
2024/12/04
4270
Java Web 项目在线商城开发教程
在线商城是电子商务的重要载体,基于Java Web技术开发的在线商城,具有跨平台性、稳定性和强大的生态系统支持等优势。它能为用户提供便捷的购物体验,同时为商家提供高效的商品管理和订单处理平台。
啦啦啦191
2025/08/03
1030
Java Web 项目在线商城开发教程
Web 小案例 -- 网上书城(三)
内容有点乱,有兴趣的同伙可依照后面的案例结构结合文章进行阅读    和网上购买东西一样,你可以在不登录的状态下去浏览商品,但是当你想把自己中意的东西加入购物车或是收藏起来就需要你拥有自己的账号然后登录后才可以进一步操作,上次我们的翻页操作也就是可以供大家进行商品的浏览,所以这次我们先完成登录操作(具体登录所需我们直接插入数据表,不再对注册做处理)。 关于登录操作的具体操作步骤 向数据表 userinfo 中插入数据(user_id,user_name,Account_id) 在翻页操作页面我们已经将登录的超
bgZyy
2018/05/16
2.5K0
Go 语言代码简单的在线购物平台:
这个代码示例实现了一个简单的在线购物平台,包括用户、产品、购物车和订单的结构体,以及添加用户、产品、创建购物车、添加产品到购物车、提交订单和显示订单的方法。示例中展示了用户在购物平台上选购产品、添加到购物车、提交订单,并打印出订单的详情。希望这个示例能够对您提供一些参考!如果您还有其他问题,请随时提问。
青灯古酒
2023/10/16
4130
Web-第二十二天 Web商城实战二【悟空教程】
`cname` varchar(20) DEFAULT NULL, #分类名称
Java帮帮
2018/07/27
1.3K0
Web-第二十二天 Web商城实战二【悟空教程】
jsp电子商务购物车之四 数据库存储篇
为了方便用户下次登录,仍然可以看到自己的购物车内容,所以,需要在数据库存储相应的购物车项目,本处增加购物车项表;uid和bid是复合主键。
张哥编程
2024/12/17
3360
jsp电子商务购物车之四 数据库存储篇
VUE实现一个购物车
想象每个组件都分别为家中的成员:爸爸、妈妈、孩子们。但是,作为一个家庭,他们需要共享状态。在这个家庭中,充当看家狗的Vuex就是来帮助我们解决问题的。
HelloWorldZ
2024/03/20
2680
VUE实现一个购物车
购物车设计与实现
购物车是电商项目常用的功能,传统的做法可以使用关系型数据库,比如mysql来处理。但在实际使用中,由于购物车的数据量太大,而且修改频繁,会导致数据库的压力增加,所以一般不会直接使用关系型数据库来存储购物车信息。
用户3467126
2022/04/28
1.9K0
购物车设计与实现
深入解析Java中如何用Redis存储购物车信息:原理与实战案例
咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~
bug菌
2024/10/24
4640
深入解析Java中如何用Redis存储购物车信息:原理与实战案例
学习PetShop3.0(4)购物车
终于到购物车了,在看这个之前应该已经明白了第三篇的那个模型,这样购物车基本也就明白了。 来看一下ShoppingCart.aspx这个页。 当你看好了一个宠物,比如可爱的Golden Retriever,嘿嘿,那就点add to cart按钮,这时就会跳到ShoppingCart.aspx,url里带了这个宠物的id号,根据该id号程序将该宠物放到cart里面。然后你可以再去挑别的宠物,比如一只猫(……),虽然这不是什么好主意。然后该宠物的id号又会被传到ShoppingCart.aspx,并添加到cart里面。在ShoppingCart.aspx里,你可以更改想要领养的宠物的数量,然后程序会根据你要求的数量来计算所需的钱以及该宠物是否还有剩余。在你做出决定后可以点proceed to checkout进入定单生成的环节。 上面是大体的流程。下面来看.net petshop是怎么实现这个cart的 基本的实现主要是BLL里的Cart和Model里的CartItemInfo,而Web.ProcessFlow的CartControler则负责具体的实现。想一想第三篇里的那个模型,具体到这里,每挑选一个宠物,就有一个CartItemInfo通过CartControler添加到了保存在Session里的Cart里面,最后生成定单的时候就从Session里把Cart的值取出来(CartControler有生成定单的方法,下一篇再说)。 来看一下ShoppingCart.aspx.cs里向Cart添加CartItemInfo的代码 // Create an instance of the cart controller ProcessFlow.CartController cartController = new ProcessFlow.CartController();
全栈程序员站长
2022/11/03
2750
Spring高级技术应用——百战商城实现(下)
需要用到pojo,但是我们可以通过依赖Mapper项目来简介添加Pojo项目 需要用到Spring Data整合Solr的坐标
时间静止不是简史
2020/07/27
1.3K0
Spring高级技术应用——百战商城实现(下)
商城业务:购物车
- 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
一个风轻云淡
2023/10/15
4090
商城业务:购物车
推荐阅读
相关推荐
EF Core如何处理多对多关系
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验