Android Compose 新闻App(五)Room复杂数据、AlertDialog弹窗、页面导航

  • 前言
  • 正文
    • 一、使用 Room 引用复杂数据
    • 二、疫情风险区展示
    • 三、AlertDialog弹窗
      • ① 显示弹窗
      • ③ 弹窗加载数据
    • 四、页面导航
      • ① 创建Activity
      • ② 创建页面
      • ③ 添加Compose导航依赖
      • ④ NavController和NavHost
    • 五、数据展示
      • ① 导航时传递参数
      • ② 列表数据展示
    • 六、源码

前言

  在上篇文章中,我们进一步对EpidemicNews的Desc数据进行处理,本文章中,要解决根本问题,那就是把EpidemicNews直接保存到数据库中。本篇文章运行效果图

正文

  在这一次改动之前,再最后一次卸载这个App。

一、使用 Room 引用复杂数据

Room提供了基本类型和装箱类型之间的转换功能,但不允许实体之间的对象引用。如果要在对象中增加一个List,比如下图这样

Room中不支持对象中直接存储集合,如果需要存储则需要一个转换器,下面在db包中新增一个converter包,在包下新增一个NewslistItemConverter.kt,里面的代码如下:

class NewslistItemConverter {@TypeConverterfun stringToObject(value: String): List<NewslistItem> {val listType = object : TypeToken<List<NewslistItem>>() {}.typereturn Gson().fromJson(value, listType)}@TypeConverterfun objectToString(list: List<Any>): String = Gson().toJson(list)}

然后把EpidemicNews的代码重新改动一下:

@TypeConverters(NewslistItemConverter::class)
@Entity
data class EpidemicNews(@PrimaryKey var id: Int = 0,var msg: String = "",var code: Int = 0,var newslist: List<NewslistItem>
)data class NewslistItem(val news: List<NewsItem>,val desc: Desc,val riskarea: Riskarea
)data class NewsItem(val summary: String = "",val sourceUrl: String = "",val id: Int = 0,val title: String = "",val pubDate: Long = 0,val pubDateStr: String = "",val infoSource: String = ""
)data class Desc(val curedCount: Int = 0,val seriousCount: Int = 0,val currentConfirmedIncr: Int = 0,val midDangerCount: Int = 0,val suspectedIncr: Int = 0,val seriousIncr: Int = 0,val confirmedIncr: Int = 0,val globalStatistics: GlobalStatistics,val deadIncr: Int = 0,val suspectedCount: Int = 0,val currentConfirmedCount: Int = 0,val confirmedCount: Int = 0,val modifyTime: Long = 0,val createTime: Long = 0,val curedIncr: Int = 0,val yesterdaySuspectedCountIncr: Int = 0,val foreignStatistics: ForeignStatistics,val highDangerCount: Int = 0,val id: Int = 0,val deadCount: Int = 0,val yesterdayConfirmedCountIncr: Int = 0
)data class Riskarea(val high: List<String>?,val mid: List<String>?
)data class GlobalStatistics(val currentConfirmedCount: Int = 0,val confirmedCount: Int = 0,val curedCount: Int = 0,val currentConfirmedIncr: Int = 0,val confirmedIncr: Int = 0,val curedIncr: Int = 0,val deadCount: Int = 0,val deadIncr: Int = 0,val yesterdayConfirmedCountIncr: Int = 0
)data class ForeignStatistics(val currentConfirmedCount: Int = 0,val confirmedCount: Int = 0,val curedCount: Int = 0,val currentConfirmedIncr: Int = 0,val suspectedIncr: Int = 0,val confirmedIncr: Int = 0,val curedIncr: Int = 0,val deadCount: Int = 0,val deadIncr: Int = 0,val suspectedCount: Int = 0
)

注意看这个转换器添加的位置:

在dao包下新建一个EpidemicNewsDao.kt,里面的代码如下:

@Dao
interface EpidemicNewsDao {@Query("SELECT * FROM `epidemicnews` WHERE id LIKE :id LIMIT 1")suspend fun getNews(id: Int = 1): EpidemicNews@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insert(desc: EpidemicNews?)
}

最后我们修改AppDatabase,内容如下图所示:

最后我们改一下EpidemicNewsRepository.kt中的代码:

然后再回到MainActivity中检查一下,这里去掉了?

现在就比之前要简洁了,下面再运行一下效果和之前一样。

二、疫情风险区展示

  又到了愉快的Compose UI环节了,这里我们将要展示目前的高风险区和中风险区的个数。首先是数据来源,打开MainActivity.kt,然后如下图所示修改一下:

riskarea就是风险区的数据类,然后我们同样要在列表中展示,那么可以再创建一个riskareaItem函数,代码如下:

private fun LazyListScope.riskareaItem(riskarea: Riskarea) {item {Card(modifier = Modifier.fillMaxWidth().padding(8.dp),elevation = 2.dp,backgroundColor = Color.White) {Row {Column(modifier = Modifier.weight(1f).clickable { "高风险区".showToast() }.padding(0.dp, 12.dp),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "高风险区", fontSize = 12.sp)Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.red))) {append("${riskarea.high?.size}")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append("个")}})}Column(modifier = Modifier.weight(1f).clickable { "中风险区".showToast() }.padding(0.dp, 12.dp),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "中风险区", fontSize = 12.sp)Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.dark_red))) {append("${riskarea.mid?.size}")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append("个")}})}}}}
}

函数的内容比较简单,都是前面讲过的,这里有一个小细节要注意

就是clickable和padding的顺序问题,padding如果在clickable之前设置那么点击时不会包含内填充,反之会包含,建议亲自试一下感受更明显。下面我们运行一下:

嗯,效果喜人,不过我们这里只显示了有多少个风险区,那么具体是哪些风险区呢?我们用另一种方式来查看。

三、AlertDialog弹窗

  我会尽可能的用到Compose中的控件,下面我们来用一下弹窗,首先我们要点击这个区域显示一个弹窗。

① 显示弹窗

  首先我们在riskareaItem函数中增加一个变量

         //定义一个变量 mutableStateOf 需要导包var showDialog by remember { mutableStateOf(false) }

然后我们在点击的时候对这个变量赋值

然后在这个Row的下面写一个弹窗

         if (showDialog) {AlertDialog(onDismissRequest = { showDialog = false },title = {Text(text = "标题")},text = {Text(text = "初学者-Study")},confirmButton = {TextButton(onClick = { showDialog = false }) {Text(text = "确定")}})}

这个代码很好理解,设置弹窗点击消失时的值和点击确定按钮的值都一样,将变量赋值为false,然后就是弹窗的一些基本参数,注意添加代码的位置,如下图所示:

下面运行一下:

效果还是不错的,下面要显示数据了。

③ 弹窗加载数据

这里修改一下代码:

这里修改了一下之前的那个变量,然后又增加了两个变量,同时写了一个showDialog函数,这样我们就把dialog抽离出去了。函数代码如下:

@Composable
private fun showDialog(openDialog: MutableState<Boolean>,dialogTitle: MutableState<String>,dialogList: MutableState<List<String>>
) {if (openDialog.value) {AlertDialog(onDismissRequest = { openDialog.value = false },title = { Text(text = dialogTitle.value) },text = { Text(text = "${dialogList.value.size}个") },confirmButton = {TextButton(onClick = { openDialog.value = false }) {Text(text = "确定")}})}
}

这里我们就可以在弹窗中知道当前的风险区个数了,然后还需要对那几个值进行赋值:


下面运行一下:


嗯,数据就这样有了,作为AlertDialog不推荐在这里显示很多数据,那么如果针对之前的逻辑,我想要查询风险区的具体信息要怎么办呢?

四、页面导航

  你可能听过Compose页面导航,也见过很多人写导航,但很少有像我这样,现在才来弄导航的,为什么这么说呢?因为导航最好是在项目搭建的初期就构建好,而不是现在再来弄,这很耗时间,但是又不能不做,因为要符合Compose的使用,先来说一下现在是什么业务场景,我们在一个页面中显示了列表,当要查看详情时,进入另一个页面,这就要导航到另一个页面,你可以理解为单个Activity和多个Fragment的关系,那么在Compose上怎么做呢?为了不让读者一脸懵逼,我这里会从头开始,怎么一个从头开始呢?

① 创建Activity

  从头开始当然是从创建Activity开始了,总所周知,Android项目创建之后会有一个默认的MainActivity,因为我们在这里面写了很多东西,我要是一个一个来拆除又显得很笨拙,所以我们创建一个HomeActivity,将它作为启动Activity。为了显得专业一点,我们在ui包下新建一个activity包。然后在activity包下新建一个HomeActivity。HomeActivity的代码如下:

class HomeActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {GoodNewsTheme {Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colors.background) {}}}}
}

然后修改一下AndroidManifest.xml


运行一下:

嗯,第一步完成。

② 创建页面

在ui包下新建一个pages包,包下新建EpidemicNewsListPage.kt和RiskZoneDetailsPage.kt文件,两个文件里面一无所有。这就表示两个页面,一个是疫情新闻列表页面,一个是风险区详情页面。

好了,页面也创建好了,我们该使用导航了,也就是Navigation,Android的Jetpack的Navigation组件是支持Compose使用的,因此我们需要添加一个依赖库。

③ 添加Compose导航依赖

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

 //Compose 导航implementation 'androidx.navigation:navigation-compose:2.4.1'


然后Sync Now,同步即可使用了。

④ NavController和NavHost

  导航中必不可少的一个API,那就是NavController,通俗一点说就是控制器,用于控制页面跳转并且会保存页面的状态。而NavHost就是导航页面要显示的内容,两者组合使用。

下面我们在HomeActivity中创建两者。

这里可以看到NavHost有三个参数,一个是navController,一个是开始目的地,也就是页面中第一个要显示的内容,最后是一个构造,改成Kotlin就是下面这样。

这里因为一个页面对应一个String值的话,我们可以写一个Page描述的常量类。在pages包下新建一个PageConstant.kt,里面的代码如下:

object PageConstant {const val EPIDEMIC_NEWS_LIST_PAGE = "EpidemicNewsListPage"const val RISK_ZONE_DETAILS_PAGE= "RiskZoneDetailsPage"
}

回到HomeActivity,用上去。

然后我们来写这个NavGraph的构造,也就是我们的导航里面有多少个页面。

我们通过composable装载当前的页面描述,然后方法体要是一个可组合函数,但是现在我们两个Page中都没有,因此我们去创建可组合函数。

在EpidemicNewsListPage.kt中创建EpidemicNewsListPage可组合函数

@Composable
fun EpidemicNewsListPage(){}

在RiskZoneDetailsPage.kt中创建RiskZoneDetailsPage可组合函数

@Composable
fun RiskZoneDetailsPage(){}

这里要注意一点,如果函数被@Composable注解,那么此函数首字母要大写。

下面我们再回到HomeActivity中,设置一下,如下图所示:

你现在就可以运行了,可以检查一下这样会不会报错,当然了由于我们在两个可组合函数中什么都没有写,因此你运行成功了,也什么都看不到。那么我们在EpidemicNewsListPage可组合函数中增加一些内容,然后运行一下,如下图所示:

同样的我们在RiskZoneDetailsPage中也增加一个Text,

然后在HomeActivity中改一下开始目的地。


再运行一下:

运行效果说明这个导航没有问题,想看那个页面就看那个页面,为所欲为。不过,,,但是好像和我想的不太一样,这样改无疑很蠢,那么如果要在EpidemicNewsListPage中导航到RiskZoneDetailsPage呢?也很简单,我们修改一下HomeActivity中的代码。

这里将开始目的地改回EPIDEMIC_NEWS_LIST_PAGE,并且在EpidemicNewsListPage()函数中增加一个navController参数,然后我们修改一下EpidemicNewsListPage函数,代码如下:

这里我们在EpidemicNewsListPage()函数中接收这个参数,然后在页面的点击事件中进行导航,导航到详情页面。下面我们运行一下:

我们成功导航到了详情页面,并且我们点击系统的返回按钮是可以返回到之前的页面的,这说明navController进行了返回栈的管理,这无疑是很舒服的。那么你可能想自己去返回上一个页面,这也很好处理,下面我们修改RiskZoneDetailsPage()函数代码:如下图所示:

这里最重要的就是navController,其次是navController.popBackStack(),这个就是回退栈,触发后显示当前栈顶的页面。最后我们在HomeActivity中将所需要的navController传入到RiskZoneDetailsPage()函数当中。


下面运行一下:

相信到目前为止你已经了解了导航基本要怎么做了,那么下面我们结合当前的实际情况去更改代码就好了。

五、数据展示

下面我们将之前写在MainActivity中的代码要改到EpidemicNewsListPage.kt中,代码如下:

@SuppressLint("StaticFieldLeak")
lateinit var mNavController: NavHostController
lateinit var mViewModel: MainViewModel/*** 疫情新闻列表页面*/
@Composable
fun EpidemicNewsListPage(navController: NavHostController,viewModel: MainViewModel
) {mNavController = navControllermViewModel = viewModelmViewModel.getNews()mViewModel.result.observeAsState().value?.let { result ->result.getOrNull()?.newslist?.get(0)?.let { MainScreen(it) }}
}@Composable
private fun MainScreen(newslistItem: NewslistItem) {Scaffold(topBar = {//顶部应用栏TopAppBar(title = {Text(text = "疫情新闻",textAlign = TextAlign.Center,modifier = Modifier.fillMaxWidth(),color = MaterialTheme.colors.onSecondary)},navigationIcon = {IconButton(onClick = { "Person".showToast() }) {Icon(imageVector = Icons.Filled.Person,contentDescription = "Person")}},actions = {IconButton(onClick = { "Settings".showToast() }) {Icon(imageVector = Icons.Filled.Settings,contentDescription = "Settings",)}},elevation = 4.dp)}) { innerPadding ->BodyContent(newslistItem.desc,newslistItem.riskarea,newslistItem.news,Modifier.padding(innerPadding))}
}@Composable
fun BodyContent(desc: Desc,riskarea: Riskarea,news: List<NewsItem>,modifier: Modifier = Modifier
) {SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing = false),onRefresh = { mViewModel.getNews(true) },indicator = { state, trigger ->SwipeRefreshIndicator(state = state,refreshTriggerDistance = trigger,scale = true,backgroundColor = MaterialTheme.colors.primary,shape = MaterialTheme.shapes.small,)}) {LazyColumn(state = rememberLazyListState(),modifier = modifier.padding(8.dp)) {descItem(desc)riskareaItem(riskarea)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 = com.llw.goodnews.R.color.black).copy(alpha = 0.08f))}}}
}private fun LazyListScope.descItem(desc: Desc) {item {Card(modifier = Modifier.fillMaxWidth().padding(8.dp),elevation = 2.dp,backgroundColor = Color.White) {Column {Row(modifier = Modifier.padding(12.dp)) {Column(modifier = Modifier.fillMaxSize().weight(1f),verticalArrangement = Arrangement.Center,//设置垂直居中对齐horizontalAlignment = Alignment.CenterHorizontally//设置水平居中对齐) {Text(text = "现存确诊人数", fontSize = 12.sp)Text(text = desc.currentConfirmedCount.toString(),fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.red),modifier = Modifier.padding(0.dp, 4.dp))Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 12.sp,color = colorResource(id = R.color.gray))) {append("较昨日 ")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append(desc.currentConfirmedIncr.addSymbols())}})}Column(modifier = Modifier.fillMaxSize().weight(1f),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "累计确诊人数", fontSize = 12.sp)Text(text = desc.confirmedCount.toString(),fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.dark_red),modifier = Modifier.padding(0.dp, 4.dp))Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 12.sp,color = colorResource(id = R.color.gray))) {append("较昨日 ")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append(desc.confirmedIncr.addSymbols())}})}}Row(modifier = Modifier.padding(12.dp)) {Column(modifier = Modifier.fillMaxSize().weight(1f),verticalArrangement = Arrangement.Center,//设置垂直居中对齐horizontalAlignment = Alignment.CenterHorizontally//设置水平居中对齐) {Text(text = "累计治愈人数", fontSize = 12.sp)Text(text = desc.curedCount.toString(),fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.green),modifier = Modifier.padding(0.dp, 4.dp))Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 12.sp,color = colorResource(id = R.color.gray))) {append("较昨日 ")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append(desc.curedIncr.addSymbols())}})}Column(modifier = Modifier.fillMaxSize().weight(1f),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "累计死亡人数", fontSize = 12.sp)Text(text = desc.deadCount.toString(),fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.gray_black),modifier = Modifier.padding(0.dp, 4.dp))Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 12.sp,color = colorResource(id = R.color.gray))) {append("较昨日 ")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append(desc.deadIncr.addSymbols())}})}}}}}
}private fun LazyListScope.riskareaItem(riskarea: Riskarea) {item {Card(modifier = Modifier.fillMaxWidth().padding(8.dp),elevation = 2.dp,backgroundColor = Color.White) {Row {Column(modifier = Modifier.weight(1f).clickable {mNavController.navigate(RISK_ZONE_DETAILS_PAGE)}.padding(0.dp, 12.dp),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "高风险区", fontSize = 12.sp)Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.red))) {append("${riskarea.high?.size}")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append("个")}})}Column(modifier = Modifier.weight(1f).clickable {mNavController.navigate(RISK_ZONE_DETAILS_PAGE)}.padding(0.dp, 12.dp),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "中风险区", fontSize = 12.sp)Text(buildAnnotatedString {withStyle(style = SpanStyle(fontSize = 28.sp,fontWeight = FontWeight.Bold,color = colorResource(id = R.color.dark_red))) {append("${riskarea.mid?.size}")}withStyle(style = SpanStyle(fontSize = 12.sp,fontWeight = FontWeight.Bold)) {append("个")}})}}}}
}fun Int.addSymbols(): String = if (this > 0.0 && this != 0) "+$this" else "$this"

这里唯一要说明的就是变量的问题,注意看我这里的变量更改。

其他的基本上差不多,对了还有导包要正确,

在点击高风险区和低风险区时导航到详情页面详情页面,下面去修改HomeActivity中的代码:

主要的变化就是viewmodel传递过去。下面可以运行一下了:

① 导航时传递参数

现在我们已经可以导航到详情页面了,那么我们需要传递两个参数,一个是标题,一个是列表。从易到难,我们先传递标题过去。回到HomeActivity中,修改代码如下所示:

然后在点击导航时将数据传递进去。

再进入到RiskZoneDetailsPage.kt,修改代码如下所示:

@Composable
fun RiskZoneDetailsPage(navController: NavHostController, title: String) {Scaffold(topBar = {//顶部应用栏TopAppBar(title = {Text(text = title,textAlign = TextAlign.Center,modifier = Modifier.fillMaxWidth(),color = MaterialTheme.colors.onSecondary)},navigationIcon = {IconButton(onClick = { navController.popBackStack() }) {Icon(imageVector = Icons.Filled.ArrowBack,contentDescription = "ArrowBack")}},actions = {IconButton(onClick = { "Settings".showToast() }, modifier = Modifier.alpha(0f)) {Icon(imageVector = Icons.Filled.Settings,contentDescription = "Settings",)}},elevation = 4.dp)}){}
}

增加了一个参数,然后设置了一个TopAppBar,下面我们运行一下:

现在我们再传递一个参数过去。这里我选择将列表转化为String再传递。这样我们又可以用到Gson的知识了,回到HomeActivity中,修改代码如下所示:

这里就是多增加一个参数而已,然后我们回到点击事件的地方,将参数传递进去。


注意不要漏掉这个 / ,最后我们回到RiskZoneDetailsPage中,首先增加一个参数。

② 列表数据展示

因为是由String转Lit< String >,因此我们可以写一个关于Gson的拓展函数在

inline fun <reified T : Any> Gson.fromJson(json: String?): T {return Gson().fromJson(json, T::class.java)
}


注意这个拓展函数的位置,它并不在RiskZoneDetailsPage可组合函数中。这里会用到也些颜色值,因此在colors.xml中增加这两个色值

 <color name="white">#FFFFFF</color><color name="gray_white">#F8F8F8</color><!--灰白-->

然后回到RiskZoneDetailsPage()可组合函数中,修改代码如下所示:

这里主要就是一个列表,然后我们使用了itemsIndexed,这个会带一个index,也就是item索引,然后我们可以通过索引设置当前item中的Text的序号,下标从0开始,因此 + 1,而后我们希望在点击的时候显示当前item的内容,那么因为这是一个List < String >,content就是String类型的,所以我们可以通过String的拓展函数showToast直接弹出一个Toast。后面的Spacer就是增加一个分隔线,下面我们运行一下:

你现在可以将MainActivity给删除掉了,记得也要把AndroidManifest.xml中的相关内容删掉,本篇文章到此结束,后面的将会有更多
关于Compose的内容,说起来这篇文章也写了蛮多内容的,我也是陆陆续续的写,有时间就写一下,山高水长,后会有期~

六、源码

如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~
GitHub:GoodNews
CSDN:GoodNews_5.rar

Android Compose 新闻App(五)Room复杂数据、AlertDialog弹窗、页面导航相关推荐

  1. Android Compose 新闻App(四)下拉刷新、复杂数据、网格布局、文字样式

    Compose 新闻App(四)下拉刷新.复杂数据.网格布局.文字样式 前言 正文 一.下拉刷新 ① 添加依赖 ② 使用 ③ 样式更改 二.刷新数据 三.复杂数据 四.复杂列表 ① 更改返回数据 ② ...

  2. Android Compose 新闻App(二)ViewModel、Hlit、数据流

    Compose 新闻App(二)ViewModel.Hlit.数据流 前言 正文 一.添加依赖 ① 添加Hilt依赖 ②添加ViewModel依赖 二.Hilt使用 三.ViewModel使用 四.数 ...

  3. Android Compose 新闻App(一)网络框架搭建

    Compose 新闻App(一)网络框架搭建 前言 正文 一.项目创建 二.依赖配置 三.数据API 四.网络框架构建 五.项目配置 六.网络请求 七.源码 前言   要去学习新的知识,光是简单的使用 ...

  4. 简单的Android端新闻App的实现。

    1. 更新记录: 2021/11/14: 1.更新了数据来源的 api 使用了聚合数据的 新闻 api 2.使用了 TabLayout 代替原来的 textview 组. 2021/11/13: 1. ...

  5. android模拟机新闻APP,DavidTGNewsProject

    DavidTGNewsProject ##[Android]最新主流新闻app功能实现.仿网易,搜狐等新闻客户端 (原创作品,转载请说明出处)先给大家看一下效果图: 这个项目总体来说虽然不是特别难,但 ...

  6. 简单的Android端新闻App的实现

    先上效果图: 图一  :     图二:    总体思路概述:        如图本app界面简单,图一的最顶端是安卓原生的标题栏,图二的最顶端是我自己定义的标题栏,具体代码后面再说.图一标题栏下面是 ...

  7. 仿网易新闻APP(五)——无限横向滑动菜单(自定义HorizontalScrollView+ViewPager)

    自从Gallery被谷歌废弃以后,Google推荐使用ViewPager和HorizontalScrollView来实现Gallery的效果.的确HorizontalScrollView可以实现Gal ...

  8. 凤凰新闻 android,凤凰新闻app正式版

    凤凰新闻app是一款由凤凰网推出的新闻资讯手机客户端,用户可以在凤凰新闻app中获得第一手新闻资讯,资讯涵盖面甚广,用户可以寻找到自己感兴趣的资讯,精美的界面,简单易上手的操作,感兴趣的朋友不要错过! ...

  9. cnbc for android,cnbc新闻app下载 安卓

    介绍(2021-03-09) 作为*好的商业新闻网站之一,NBC Un... **NEW & IMPROVED, REDESIGNED, MORE POWERFUL CNBC APP** CN ...

  10. Jsoup抓取网页数据完成一个简易的Android新闻APP

    前言:作为一个篮球迷,每天必刷NBA新闻.用了那么多新闻APP,就想自己能不能也做个简易的新闻APP.于是便使用Jsoup抓取了虎扑NBA新闻的数据,完成了一个简易的新闻APP.虽然没什么技术含量,但 ...

最新文章

  1. “智源论坛Live”报名 | 清华大学高天宇:实体关系抽取的现状和未来
  2. 7、斐波那契数列、跳台阶、变态跳台阶、矩形覆盖------------剑指offer系列
  3. usg2130 虚拟服务器,usg2130防火墙怎么样设置
  4. 【NLP】AAAI21最佳论文Runners Up!Transformer的归因探索!
  5. 11.4 iftop:动态显示网络接口流量信息
  6. 使用pytz模块进行时区转换及时间计算
  7. sql 触发器的常用语句(转)
  8. RedisRDB持久化机制
  9. Z表数据EXCEL导入
  10. 解决浏览器 Provisional headers are shown 无法向后台发送请求问题
  11. 使用cloudbase-init初始化windows虚拟机
  12. 被马云逼上绝路,睡了12年宾馆!中国最狠会计,拿下4600亿
  13. 實驗項目wordcount
  14. 搭建云计算机win10,win10家庭版连接云主机
  15. Tool-X:在AndroidUbuntu平台安装Kali的各种小工具
  16. 简单python程序代码_几个简单的python程序分享
  17. 联合密度函数求期望_已知(X,Y)的联合概率密度,分别求X,Y的期望、方差
  18. sqluldr2 用法简述
  19. linux创建文件夹共享文件夹,Linux创建文件夹共享
  20. MATLAB频谱图绘制

热门文章

  1. 图像处理系列——图像融合之加权平均(WA)
  2. 信息安全专业面试知识点整理(密码学与信数基础)
  3. 致信息安全专业同学的一封信
  4. 60秒倒计时实现的两种方式
  5. python,检测代理ip是否有效
  6. 前端开发工程师,CSS盒子模型居中方法
  7. 判断合法标识符(c语言或c++)
  8. Aladdin and the Flying Carpet LightOJ - 1341
  9. FFmpeg的音频处理详解
  10. 机器人庄园作文_关于周庄一日游作文六年级汇总5篇