Android Compose 新闻AppRoom复杂数据AlertDialog弹窗页面导航

Posted 初学者-Study

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Compose 新闻AppRoom复杂数据AlertDialog弹窗页面导航相关的知识,希望对你有一定的参考价值。

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

前言

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

正文

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

一、使用 Room 引用复杂数据

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

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

class NewslistItemConverter 

    @TypeConverter
    fun stringToObject(value: String): List<NewslistItem> 
        val listType = object : TypeToken<List<NewslistItem>>() .type
        return Gson().fromJson(value, listType)
    

    @TypeConverter
    fun 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 = navController
    mViewModel = viewModel

    mViewModel.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.colorsAndroid Compose 新闻App抽屉布局动态权限拍照返回

Android Compose 新闻App网络框架搭建

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

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

Android Compose 新闻App抽屉布局动态权限拍照返回

Android Compose 新闻App抽屉布局动态权限拍照返回