Android Compose news App web data Compose UI display loading, Room and DataStore usage

preface

  now that the data is available, the main thing is the design of Compose UI. After completing this article, the renderings are as follows:

text

  the following content involves style layout components, and there are many contents.

1, Style

Here, we first configure the style and open the UI The theme folder.
The first is to modify the color KT file

val Blue200 = Color(0xFF979FF2)
val Blue300 = Color(0xFF6D7DEA)
val Blue700 = Color(0xFF0068C2)
val Blue800 = Color(0xFF0059A5)
val Blue900 = Color(0xFF004076)

Then shape KT file

val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(4.dp),
    large = RoundedCornerShape(8.dp)
)

Then there is theme KT file

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
    )
}

Finally, type KT file

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
    )
)

There are some font files in this file, under res of my project.

Of course, you can also use these fonts.

Next, we will create a values night folder under the res folder and create a colors xml. The code inside is as follows:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="blue700">#0068C2</color>
    <color name="blue900">#004076</color>
    <color name="status_bar">#0E0E0E</color>
</resources>

Then modify the colors under values xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="blue700">#0068C2</color>
    <color name="blue900">#004076</color>
    <color name="status_bar">#0068C2</color>
</resources>

Finally, modify values Theme XML, the code is as follows:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.GoodNews" parent="android:Theme.Material.Light.NoActionBar">
        <item name="android:colorPrimary">@color/blue700</item>
        <item name="android:colorPrimaryDark">@color/blue900</item>
        <item name="android:colorAccent">@color/blue700</item>

        <item name="android:statusBarColor">@color/status_bar</item>
        <item name="android:background">@color/status_bar</item>
    </style>
</resources>

2, Scaffold

  you may have seen this for the first time. Compose comes with built-in Material components to combine items, which you can use to create applications. The highest level composable item is scaffold. Scaffold allows you to implement an interface with a basic Material Design layout structure. Scaffold can provide slots for the most common top-level Material components, such as TopAppBar, BottomAppBar, FloatingActionButton, and Drawer. When using scaffold, you can ensure that these components are properly placed and work together. These are some parameters provided in it

  you may have heard that Compose is a declarative UI, but more is the slot API. The slot API is a mode introduced by Compose, which provides a layer of custom settings on the basis of composable items. So what is the slot API? For example, you can think that Text corresponds to Icon in Icon.

I've said a lot about theory. Let's practice it. In mainactivity KT adds a MainScreen function

@Composable
private fun MainScreen() {
    Scaffold {
        
    }
}

Then we call it in setContent and DefaultPreview.

For a blank article, we can regard this Scaffold as a layout. Let's add a TopAppBar

3, TopAppBar (top application bar)

			//Top application bar
            TopAppBar(
                title = {
                    Text(
                        text = stringResource(id = R.string.app_name),
                        modifier = Modifier.fillMaxWidth(),
                        textAlign = TextAlign.Center,
                        color = MaterialTheme.colors.onSecondary
                    )
                }
            )


Here, set the title Parameter in the TopAppBar, and then write a Text slot to set the Text, control width, Text placement position and color.
Here's a preview:

We can't see the status bar when previewing. We can see the effect through the real machine or virtual machine.

① Attribute value


Several of the properties here can be passed into the slot, which are annotated with @ Composable. For example, let's set navigationIcon and action.

Here we see that there is an IconButton in both navigationIcon and actions, which means that the icon can be clicked. Then we set the click event and pop up a Toast. Here is an extension function. We create a Toast utils class under the utils package. The code is as follows:

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()

Then let's explain what's in Icon, icons Filled. Person represents a filled person Icon, which is drawn through Path, and icons is Android X compose. material. Icons depend on the icons in the library, so we don't need to write them ourselves. They are all material style icons. contentDescription is a description that explains what the content means, which is not very important. Let's run:

4, List

Now we have a title bar. Let's write the main content of the page. Let's go to mainactivity KT adds a BodyContent() function

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(8.dp)) {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

① Display list

This function needs to be called in the MainScreen() function.

Run the following:

② Sliding list

You will find that you can't slide. We just need to add a line of code to slide, as shown in the figure below:

Call the verticalScroll() function through the chain of modifier, and then pass it in remeberscrollstate(). You may ask again, what about horizontal scrolling? In order to distinguish, I changed this BodyContent function again.

Let's run:

Well, now that we have mastered the basic use of the list, let's add the data returned by the network request.

Here we will display the array data of this news.

③ Load network data

Previously, the data request was returned in initData, and the return value was obtained, as shown in the following figure:

Here, the values are transferred to the BodyContent function layer by layer. In this function, we will display the data. The code of the function is as follows:

@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)
            )
        }
    }
}

It seems that there are many contents. Please explain:

The first is the LazyColumn, LazyColumn, which only renders visible items on the interface, which helps to improve performance without using the scroll modifier. LazyColumn in Jetpack Compose is equivalent to RecyclerView in Android view. The state here uses rememberLazyListState().
So that's it.

   the items display data, and then we build the layout of the item. There is nothing to say about the general attribute values. Here's the Divider, which is a separation line. We add a left and right fill, and then set the color of the separation line. Here we use a black color value, which is #000000, in colors XML, and then set the transparency of this color value. It's too bright to look good.

Then you need to add the call of initData() in setContent

Let's run:

Is it easy to write code like this?

5, Room usage

   now that the data is available, in order to reduce the number of accesses to the interface API, we need to store the data in the local database. We can access the interface twice or once a day, and then obtain the data from the database for other accesses. Isn't that good? Here we use the Room database. The way it is used in Java and Kotlin has changed a little, and the overall difference is not big.

① Add dependency

To use Room, first add dependencies, and now the project's build The dependent version of the Room database is defined in gradle:

room_version = '2.3.0'


Then go to build.com under the app module Add dependencies to the dependencies {} closure in gradle:

	//Room database
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

As shown in the figure below:

Then Sync Now. Compared with Hilt, you will find the introduction of Room easier. The Room KTX library here supports Kotlin collaboration. This library is not available when using Java.

② Basic configuration

Let's use it. The first is the entity bean, which is on COM llw. Create a new db package under goodnews package, then move the bean package to db package, and open the EpidemicNews class,

Add two annotations, and then we add interfaces. Create a dao package under db package and a NewsItemDao interface under dao package. The code inside is as follows:

@Dao
interface NewsItemDao {

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

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

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

Finally, create an AppDatabase under db package to process the database. The code is as follows:

@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 }
            }
        }
    }
}

Here is a very simple code, there is nothing to say, that is, initialization, and then singleton. Now enter the App and configure it as shown below

③ Use

   the data table we store here is NewsItem, but the network request returns epidemicnews, so we need to change the returned data. If we change it, it is in the epidemicnews repository. Here, after our request is successful, we return epidemicnews, as shown in the following figure:

Then we add two lines of code:

Here is to save the data to the local database after getting it. Why delete it first? Because I want to ensure that the data I get every time is the latest and the same as the data returned by the network. Then we go back to mainactivity KT, run it once to ensure that there is data saved in our database, and then change it according to the code shown in the figure below.

That is to say, when there is data in my database, get the data from the local database and display it on the UI. Run:

You will find that the error is reported. The reason for the error is that I marked here to the effect that the database cannot be accessed in the main thread, so it's good to solve it. Just add a configuration on the Room. Open AppDatabase and modify it as shown in the figure below.

Just run it again. However, we still need to solve the problem of accessing the database in the main thread. We'll talk about this later. Now you'll find it too troublesome to switch like this. First request the network, and then change the code to request the database. It's too low. No, absolutely not. Let's change it and solve this problem through code.

6, DataStore usage

  how can we solve the problem just now? Local cache. When it comes to caching in Android, you first think of SP (shared preferences), then Tencent's MMKV, and then DataStore. These three appear in sequence. Maybe you don't know what DataStore is. It doesn't matter. I won't talk about it here, ha ha ha. Isn't it a surprise. Of course, you don't understand. You can go and have a look Use and simple encapsulation of Android Jetpack component DataStore , after reading it, you will know how to use it. Of course, you don't need to read it, because in fact, our usage is similar to that of SP, which is encapsulated into tool classes. In that article, it is encapsulated in this way, and it will be used directly here.

① Add dependency

  DataStore is also a component of Jetpack, so we need to add dependencies if we use it. The first is still in the project build Add dependent versions to gradle

datastore_version = '1.0.0'

And then it's in the app's build Add the following dependencies to the dependencies {} closure in gradle:

	//DataStore
    implementation "androidx.datastore:datastore-preferences:$datastore_version"
    implementation "androidx.datastore:datastore-preferences-core:$datastore_version"

The location is shown in the figure below:

Then Sync Now.

② Encapsulation

First, add the following code in the App

We will create a new easydatastore under the utils package KT, the code inside is as follows:

object EasyDataStore {

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

    // DataStore variable
    private val dataStore = App.instance.dataStore

    /**
     * Save data
     */
    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")
            }
        }
    }

    /**
     * Fetch data
     */
    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
    }



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

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

    /**
     * Store String data
     */
    private suspend fun putStringData(key: String, value: String) = dataStore.edit {
        it[stringPreferencesKey(key)] = value
    }

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

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

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

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

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

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

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

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

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

I won't explain this tool class much, and the code is not difficult. You may just don't understand it, that is, the cooperative use of collaborative process and DataStore. Next, how can we apply this to the problem just mentioned?

③ Use

   first, let's talk about the business logic. Record whether there is a request for the network API interface on the same day through a cache value. If there is no request, return the data from the network, and then save it to the database. If it is effective to request the cache value for the second time, then return the data from the local database. That's all right. Now let's use it.

Here, we need to define constants and add the following code in Constant:

	/**
     * Timestamp of data returned by today's request interface
     */
    const val REQUEST_TIMESTAMP = "requestTimestamp_news"

Then we return to the epidemic news repository

Because we need to judge whether the data comes from the local or the network. Here we process it through timestamp. If the current time is less than the time in the cache, it will be obtained from the local database. On the contrary, it will be obtained from the network. Here we create a tool class and create an EasyDate under the utils package KT, the code is as follows:

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 year MM month dd number"
    private const val HOUR_MINUTE_SECOND = "HH:mm:ss"
    private const val HOUR_MINUTE_SECOND_CN = "HH Time mm branch ss second"
    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 = "yesterday"
    private const val TODAY = "today"
    private const val TOMORROW = "tomorrow"
    private const val SUNDAY = "Sunday"
    private const val MONDAY = "Monday"
    private const val TUESDAY = "Tuesday"
    private const val WEDNESDAY = "Wednesday"
    private const val THURSDAY = "Thursday"
    private const val FRIDAY = "Friday"
    private const val SATURDAY = "Saturday"
    private val weekDays = arrayOf(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY)

    /**
     * Get standard time
     *
     * @return For example, 2021-07-01 10:35:53
     */
    val dateTime: String get() = SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(Date())

    /**
     * Get full time
     *
     * @return For example, 2021-07-01 10:37:00.748
     */
    val fullDateTime: String get() = SimpleDateFormat(FULL_TIME, Locale.CHINESE).format(Date())

    /**
     * Get date (today)
     *
     * @return For example, 2021-07-01
     */
    val theYearMonthAndDay: String
        get() = SimpleDateFormat(YEAR_MONTH_DAY, Locale.CHINESE).format(Date())

    /**
     * Get date
     *
     * @return For example, July 1, 2021
     */
    val theYearMonthAndDayCn: String
        get() = SimpleDateFormat(YEAR_MONTH_DAY_CN, Locale.CHINESE).format(Date())

    /**
     * Get date
     * @param delimiter Separator
     * @return For example, July 1, 2021
     */
    fun getTheYearMonthAndDayDelimiter(delimiter: CharSequence): String =
        SimpleDateFormat(YEAR + delimiter + MONTH + delimiter + DAY, Locale.CHINESE).format(Date())

    /**
     * Get hours, minutes and seconds
     *
     * @return For example, 10:38:25
     */
    val hoursMinutesAndSeconds: String get() = SimpleDateFormat(HOUR_MINUTE_SECOND, Locale.CHINESE).format(Date())

    /**
     * Get hours, minutes and seconds
     *
     * @return For example, 10:38:50
     */
    val hoursMinutesAndSecondsCn: String get() = SimpleDateFormat(HOUR_MINUTE_SECOND_CN, Locale.CHINESE).format(Date())

    /**
     * Get hours, minutes and seconds
     * @param delimiter Separator
     * @return For example, 2021 / 07 / 01
     */
    fun getHoursMinutesAndSecondsDelimiter(delimiter: CharSequence): String =
        SimpleDateFormat(HOUR + delimiter + MINUTE + delimiter + SECOND, Locale.CHINESE).format(Date())

    /**
     * Acquisition year
     *
     * @return E.g. 2021
     */
    val year: String get() = SimpleDateFormat(YEAR, Locale.CHINESE).format(Date())

    /**
     * Get month
     *
     * @return For example 07
     */
    val month: String get() = SimpleDateFormat(MONTH, Locale.CHINESE).format(Date())

    /**
     * Get days
     *
     * @return For example 01
     */
    val day: String get() = SimpleDateFormat(DAY, Locale.CHINESE).format(Date())

    /**
     * Get hours
     *
     * @return Example 10
     */
    val hour: String get() = SimpleDateFormat(HOUR, Locale.CHINESE).format(Date())

    /**
     * Get minutes
     *
     * @return For example 40
     */
    val minute: String get() = SimpleDateFormat(MINUTE, Locale.CHINESE).format(Date())

    /**
     * Get seconds
     *
     * @return For example 58
     */
    val second: String get() = SimpleDateFormat(SECOND, Locale.CHINESE).format(Date())

    /**
     * Get milliseconds
     *
     * @return E.g. 666
     */
    val milliSecond: String get() = SimpleDateFormat(MILLISECOND, Locale.CHINESE).format(Date())

    /**
     * Get timestamp
     *
     * @return For example, 1625107306051
     */
    val timestamp: Long get() = System.currentTimeMillis()

    /**
     * Convert time to timestamp
     *
     * @param time For example, 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
    }

    /**
     * Convert timestamp to time
     *
     * @param timeMillis For example, 1625107637084
     * @return For example, 2021-07-01 10:47:17
     */
    fun stampToDate(timeMillis: Long): String = SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(Date(timeMillis))

    /**
     * Get the 0:00 am timestamp of the next day
     * @return
     */
    fun getMillisNextEarlyMorning(): Long {
        val cal = Calendar.getInstance()
        //Date plus 1
        cal.add(Calendar.DAY_OF_YEAR, 1)
        //The time is set to 0:00 sharp
        cal[Calendar.HOUR_OF_DAY] = 0
        cal[Calendar.SECOND] = 0
        cal[Calendar.MINUTE] = 0
        cal[Calendar.MILLISECOND] = 0
        return cal.timeInMillis
    }

    /**
     * What day is today
     *
     * @return For example, Thursday
     */
    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]
        }

    /**
     * The day of the week is calculated according to the entered date and time
     *
     * @param dateTime For example, June 20, 2021
     * @return For example, Sunday
     */
    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]
    }

    /**
     * Get yesterday of the input date
     *
     * @param date For example, 2021-07-01
     * @return For example, June 30, 2021
     */
    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)
    }

    /**
     * Get tomorrow of the input date
     *
     * @param date For example, 2021-07-01
     * @return For example, 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)
    }

    /**
     * The day of the week is calculated according to the month, year and day, and judged according to the current date. If it is not yesterday, today and tomorrow, it will be displayed in weeks
     *
     * @param dateTime For example, 2021-07-03
     * @return For example, Saturday
     */
    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
    }

    //Set the date to the first day of the month
    //The date is the last day
    /**
     * Get the number of days in this month
     *
     * @return Example 31
     */
    val currentMonthDays: Int
        get() {
            val calendar = Calendar.getInstance()
            //Set the date to the first day of the month
            calendar[Calendar.DATE] = 1
            //The date is the last day
            calendar.roll(Calendar.DATE, -1)
            return calendar[Calendar.DATE]
        }

    /**
     * Gets the number of days in the specified month
     *
     * @param year  For example, 2021
     * @param month Example 7
     * @return Example 31
     */
    fun getMonthDays(year: Int, month: Int): Int {
        val calendar = Calendar.getInstance()
        calendar[Calendar.YEAR] = year
        calendar[Calendar.MONTH] = month - 1
        //Set the date to the first day of the month
        calendar[Calendar.DATE] = 1
        //The date is the last day
        calendar.roll(Calendar.DATE, -1)
        return calendar[Calendar.DATE]
    }
}

Then back to the epidemic news repository, we add two new functions

	/**
     * Save to local database
     */
    private fun saveNews(epidemicNews: EpidemicNews) {
        Log.d(TAG, "saveNews: Save to local database")
        EasyDataStore.putData(REQUEST_TIMESTAMP, EasyDate.getMillisNextEarlyMorning())
        App.db.newsItemDao().deleteAll()
        App.db.newsItemDao().insertAll(epidemicNews.newslist?.get(0)?.news)
    }

    /**
     * Load from local database
     */
    private fun getLocalForNews() = EpidemicNews(SUCCESS, CODE, listOf(NewslistItem(App.db.newsItemDao().getAll(), null, null)))

One is to save to the database and the other is to obtain data from the database. This is equivalent to the constructed EpidemicNews object. Therefore, we need to change the variable modifier in EpidemicNews and the nullable type, as shown in the following figure:
After the modification, go back to the epidemicnews repository to modify the getEpidemicNews() function. The code is as follows:

Then we return to mainactivity KT medium

Let's run it and take a look at the log:


You will find that you have obtained data from the network, so run it again to see:

Data was obtained from the database. You can see that we have avoided accessing the database from the main thread. Let's remove allowMainThreadQueries() in AppDatabase

Then you can run the confirmation again. In fact, you don't need to confirm, because we now access the database in the cooperation process.
However, for more standardized use, let's add a suspend in front of the method of the NewsItemDao interface, as shown in the following figure:

Go back to the epidemic news repository and add a suspend as shown in the figure below.

Next you run the same again.

Well, that's all for this article.

7, Source code

GitHub: GoodNews
CSDN: GoodNews_3.rar

Tags: compose room

Posted by Katmando on Wed, 13 Apr 2022 23:46:59 +0930