资讯详情

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

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

  • 前言
  • 正文
    • 一、样式
    • 二、Scaffold(脚手架)
    • 三、TopAppBar(顶部应用栏)
      • ① 属性值
    • 四、列表
      • ① 显示列表
      • ② 滑动列表
      • ③ 加载网络数据
    • 五、Room使用
      • ① 添加依赖
      • ② 基础配置
      • ③ 使用
    • 六、DataStore使用
      • ① 添加依赖
      • ② 封装
      • ③ 使用
    • 七、源码

前言

??现在数据已经存在了,现在主要是Compose UI设计。本文完成,效果图如下: 在这里插入图片描述

正文

??以下内容涉及样式布局组件,内容较多。

一、样式

这里我们先配置风格,打开ui.theme文件夹。 首先是修改Color.kt文件

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

然后是Shape.kt文件

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

再是Theme.kt文件

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文件

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。里面的代码如下:

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

再去修改values下的colors.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>

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

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

二、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函数

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

然后在setContent和DefaultPreview中调用,下面我们预览一下: 一篇空白,我们可以把这个Scaffold当成是一个布局。下面我们新增一个TopAppBar

三、TopAppBar(顶部应用栏)

			//顶部应用栏
            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类,代码如下:

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

@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函数中,在这个函数中我们就来显示数据,函数的代码如下:

@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数据库的依赖版本:

room_version = '2.3.0'

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

	//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接口,里面的代码如下:

@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用于处理数据库,代码如下:

@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中添加依赖版本

datastore_version = '1.0.0'

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

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

位置如下图所示: 然后Sync Now。

② 封装

首先在App中增加如下代码

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

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 

标签: 高压电阻器5w500m

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台