我对Android开发非常陌生.

我有一个房间结果数据库:

@Entity(tableName = "results")
data class Result(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val result : Float,
    val date : String
)

@Database(
    entities = [Result::class],
    version = 1,
    exportSchema = false
)
abstract class ResultDatabase : RoomDatabase() {
    abstract fun resultDao() : ResultDao

    companion object {
        @Volatile
        private var Instance : ResultDatabase? = null

        fun getDatabase(context : Context) : ResultDatabase {
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, ResultDatabase::class.java, "result_database")
                    .fallbackToDestructiveMigration()
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

有一个ASO

@Dao
interface ResultDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(result : Result)

    @Update
    suspend fun update(result : Result)

    @Delete
    suspend fun delete(result : Result)

    @Query("SELECT * FROM results WHERE id = :id")
    fun getResult(id : Int) : Flow<Result>

    @Query("SELECT * FROM results ORDER BY date ASC")
    fun getAllResults() : Flow<List<Result>>
}

以及存储库

class ResultRepository(private val resultDao : ResultDao) {

    fun getAllResults(): Flow<List<Result>> = resultDao.getAllResults()

    suspend fun getResultsSmoothed(numSteps : Int) : List<Result> {

        var allResults : List<Result> = listOf()
        getAllResults().collect { result -> allResults = allResults + result }

        val minDate = LocalDateTime.parse(allResults.first().date)
        val maxDate = LocalDateTime.parse(allResults.last().date)

        val period = Duration.between(minDate, maxDate)

        var step = period.seconds / numSteps

        var hundredResults = listOf<Result>()

        var nextDate = minDate

        for (i in 0..numSteps) {

            val resultPrior = allResults.fold(allResults.first()) { acc: Result, r ->
                val rd = LocalDateTime.parse(r.date)
                if (rd <= nextDate && (rd > LocalDateTime.parse(acc.date))) r
                else acc
            }

            val resultAfter = allResults.fold(allResults.last()) { acc: Result, r ->
                val rd = LocalDateTime.parse(r.date)
                if (rd > nextDate && (rd <= LocalDateTime.parse(acc.date))) r
                else acc
            }

            if (resultAfter == resultPrior) {
                val newResult = Result(date = nextDate.toString(), result = resultPrior.result)
                hundredResults = hundredResults + newResult
            } else {

                val resultPriorDate = LocalDateTime.parse(resultPrior.date)
                val resultAfterDate = LocalDateTime.parse(resultAfter.date)
                val periodBetween = Duration.between(resultPriorDate, resultAfterDate)

                val periodOver = Duration.between(resultPriorDate, nextDate)

                val percentageOver =
                    periodOver.seconds.toDouble() / periodBetween.seconds.toDouble();

                val resultDifference = resultAfter.result - resultPrior.result;
                val increaseToPriorAmount = resultDifference.toDouble() * percentageOver;

                val stepResult = resultPrior.result + increaseToPriorAmount;

                val newResult = Result(date = nextDate.toString(), result = stepResult.toFloat())
                hundredResults = hundredResults + newResult
            }
            nextDate = nextDate.plusSeconds(step)
        }

        return hundredResults
    }

    suspend fun getResultLabels(numLabels : Int) : List<String> {

        var allResults : List<Result> = listOf()
        getAllResults().collect { result -> allResults = allResults + result }

        val minDate = LocalDateTime.parse(allResults.first().date)
        val maxDate = LocalDateTime.parse(allResults.last().date)

        val period = Duration.between(minDate, maxDate)

        val step = period.seconds / numLabels

        var dateLabels = listOf<String>()

        var nextDate = minDate

        for (i in 0..numLabels) {
            val formatter = DateTimeFormatter.ofPattern("d MMM yy")
            dateLabels = dateLabels + nextDate.format(formatter)
            nextDate = nextDate.plusSeconds(step)
        }

        return dateLabels
    }

    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun insertResult(result : Result) {
        resultDao.insert(result)
    }

    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun updateResult(result : Result) {
        resultDao.update(result)
    }

    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun deleteResult(result : Result) {
        resultDao.delete(result)
    }
}

两个函数getResultLabelsgetResultsSmoothed正在处理数据库中的一些值并返回内容.我将其放在这里的原因是将其提供给视图模型,因为我已经明白我不应该在我的组合中进行这种处理.

然后我的视图模型

class ResultViewModel(private val repository: ResultRepository) : ViewModel() {

    val allResults = repository.getAllResults()

    fun getSmoothedResults(num : Int) : Flow<List<Result>> {
        return flow {
            val results = repository.getResultsSmoothed(num)
            Log.d("ME vm", results.toString())
            emit(results)
        }
    }

    fun getResultLabels(num : Int) : Flow<List<String>> {
        return flow { emit(repository.getResultLabels(num)) }
    }

    fun update(result : Result) = viewModelScope.launch {
        repository.updateResult(result)
    }

    fun delete(result : Result) = viewModelScope.launch {
        repository.deleteResult(result)
    }

    fun insert(result : Result) = viewModelScope.launch {
        repository.insertResult(result)
    }
}

class ResultViewModelFactory(private val repository: ResultRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass : Class<T>) : T {
        if(modelClass.isAssignableFrom(ResultViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return ResultViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

我试图做的是将这些List放入Flow中,以便使用它们的组合将自动更新.

例如:

@Composable
fun Metrics(resultViewModel: ResultViewModel,
            resultList : List<Result>?) {

    if(resultList.isNullOrEmpty())
        return

    var dateLabels = resultViewModel.getResultLabels(5).collectAsState(initial = listOf())

    var hundredResults = resultViewModel.getSmoothedResults(100).collectAsState(
        initial = listOf()
    )

    Log.d("memememe", hundredResults.value.toString())

    Column()
    {
        LineChart(
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
                .padding(top = 12.dp, end = 12.dp),
            linesChartData = listOf(LineChartData(
                lineDrawer = SolidLineDrawer(color = MaterialTheme.colorScheme.onBackground),
                points = hundredResults.value.map {
                    LineChartData.Point(it.result, it.id.toString())
                })),
            animation = simpleChartAnimation(),
            pointDrawer = com.github.tehras.charts.line.renderer.point.NoPointDrawer,
            labels = dateLabels.value,
            xAxisDrawer = SimpleXAxisDrawer(
                axisLineThickness = 1.dp,
                axisLineColor = MaterialTheme.colorScheme.onBackground,
                labelTextColor = MaterialTheme.colorScheme.onBackground
            ),
            yAxisDrawer = SimpleYAxisDrawer(
                axisLineThickness = 1.dp,
                axisLineColor = MaterialTheme.colorScheme.onBackground,
                labelTextColor = MaterialTheme.colorScheme.onBackground
            )
        )
        DataTable(
            modifier = Modifier.fillMaxWidth(),
            columns = listOf(
                DataColumn(
                    width = TableColumnWidth.Fixed(60.dp)
                ) {
                    Text("")
                },
                DataColumn {
                    Text("Date")
                },
                DataColumn {
                    Text("Result")
                }
            )
        ) {
            resultList.forEach { r ->
                row {
                    cell {
                        IconButton(onClick = { resultViewModel.delete(r) }) {
                            Icon(Icons.TwoTone.Delete, "Delete result")
                        }
                    }
                    cell {
                        Text(
                            text = LocalDateTime.parse(r.date)
                                .format(DateTimeFormatter.ofPattern("dd MMM yyyy"))
                        )
                    }
                    cell {
                        Text("${r.result}")
                    }
                }
            }
        }
    }
}

然而,我的日志(log)呼叫有一个空列表.我可能完全错误地处理这个问题,但我不确定.我对协程和状态的使用以及它们如何相互作用有点困惑.

推荐答案

你是对的,getResultLabelsgetResultsSmoothed所做的处理不属于组合.不过,它也不属于存储库.

这种处理称为business logic,应该在存储库所属的数据层和构成代码的UI表示之间完成.甚至还有一个专门的层,即Domain layer层,尽管我只建议在业务逻辑比这两个功能广泛得多的情况下使用该层.如果没有专用的域层,业务逻辑将在视图模型中完成.从技术上讲,视图模型属于UI层,但由于它是将原始数据转换为UI状态的地方,因此它非常适合这里.

但这并不能解决您的问题.

在干净的体系 struct 中,理想情况下,视图模型仅通过公开类型StateFlow的属性将状态传递给组合,同时通过不带返回值的函数从UI接收事件(后者在您的代码中已经看起来很好).此外,一般的 idea 是让您的数据源(在您的情况下是Room数据库)返回一个流,该流在通过层的过程中仅为transformed,直到它被收集到UI中.然而,在您的代码中,存储库已经收集了数据库流,并且返回了简单的列表.然后,在您的视图模型中,您try 再次将列表包裹在流中,但这不会起作用,因为视图模型的流现在已与数据库分离.

目标是UI接收一个流,当数据库中发生任何变化时,该流会自动使用数据库中的当前值更新,而不必通过再次调用一些函数来民意调查新结果.为了实现这一点,应该在compose中收集only个流.不在存储库中,不在视图模型中.

要重构代码以符合这一点,您首先需要从存储库中提取业务逻辑.目前,功能getResultsSmoothedgetResultLabels与流一起工作.但业务逻辑应该与流量无关.它应该只是将数据(在这种情况下是结果列表)转换为其他列表.最好将转换作为文件级别的顶级函数移动到单独的文件中:

fun getResultsSmoothed(
    allResults: List<Result>,
    numSteps: Int,
): List<Result> {
    val minDate = LocalDateTime.parse(allResults.first().date)
    val maxDate = LocalDateTime.parse(allResults.last().date)

    val period = Duration.between(minDate, maxDate)

    val step = period.seconds / numSteps

    var hundredResults = listOf<Result>()

    var nextDate = minDate

    for (i in 0..numSteps) {
        val resultPrior = allResults.fold(allResults.first()) { acc: Result, r ->
            val rd = LocalDateTime.parse(r.date)
            if (rd <= nextDate && (rd > LocalDateTime.parse(acc.date))) r
            else acc
        }

        val resultAfter = allResults.fold(allResults.last()) { acc: Result, r ->
            val rd = LocalDateTime.parse(r.date)
            if (rd > nextDate && (rd <= LocalDateTime.parse(acc.date))) r
            else acc
        }

        if (resultAfter == resultPrior) {
            val newResult = Result(date = nextDate.toString(), result = resultPrior.result)
            hundredResults = hundredResults + newResult
        } else {
            val resultPriorDate = LocalDateTime.parse(resultPrior.date)
            val resultAfterDate = LocalDateTime.parse(resultAfter.date)
            val periodBetween = Duration.between(resultPriorDate, resultAfterDate)

            val periodOver = Duration.between(resultPriorDate, nextDate)

            val percentageOver =
                periodOver.seconds.toDouble() / periodBetween.seconds.toDouble()

            val resultDifference = resultAfter.result - resultPrior.result
            val increaseToPriorAmount = resultDifference.toDouble() * percentageOver

            val stepResult = resultPrior.result + increaseToPriorAmount

            val newResult = Result(date = nextDate.toString(), result = stepResult.toFloat())
            hundredResults = hundredResults + newResult
        }
        nextDate = nextDate.plusSeconds(step)
    }

    return hundredResults
}

fun getResultLabels(
    allResults: List<Result>,
    numLabels: Int,
): List<String> {
    val minDate = LocalDateTime.parse(allResults.first().date)
    val maxDate = LocalDateTime.parse(allResults.last().date)

    val period = Duration.between(minDate, maxDate)

    val step = period.seconds / numLabels

    var dateLabels = listOf<String>()

    var nextDate = minDate

    for (i in 0..numLabels) {
        val formatter = DateTimeFormatter.ofPattern("d MMM yy")
        dateLabels = dateLabels + nextDate.format(formatter)
        nextDate = nextDate.plusSeconds(step)
    }

    return dateLabels
}

现在,它们独立于代码的其余部分,并且无论当前上下文如何,只要需要转换List<Result>就可以调用.

现在可以删除存储库中的两个功能,因此存储库现在已经清除了任何流集合,这是应该的.它公开的唯一数据是从getAllResults函数返回的所有结果的流.

解决了这个问题,视图模型现在只需要使用我们刚刚从存储库中解放的两个函数来获取该流并仅转换其content个:

val smoothedResults: StateFlow<List<Result>> = repository.getAllResults()
    .mapLatest { results ->
        getResultsSmoothed(results, 100)
    }
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = emptyList(),
    )

val resultLabels = repository.getAllResults()
    .mapLatest { results ->
        getResultLabels(results, 5)
    }
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = emptyList(),
    ) 

mapLatest在流上的工作方式与map在列表上的工作方式非常相似:对于每个新的流emits ,流的内容都会被Lambda的结果替换.返回值仍然是Flow,只是内容发生了变化,我们不需要收集任何东西.

之后,流量被转换为StateFlow x stateIn.StateFlow是一个特殊配置的流,它不保留历史记录,仅为其收集器提供最新的值.此外,它是一个hot流,这意味着它可以独立工作,因此可以与多个收集器共享.这也是为什么我们需要提供viewModelScope,这样它就可以在内部启动协程来完成所有这些工作.由于它自行运行,我们还需要提供一个初始值,该值将被使用,直到upstream 流产生其第一个值.Android文档有一个nice primer on StateFlows(有点过时,他们会在视图模型中收集流),但由于流是Kotlin的一部分,为了全面概述,您应该看看Kotlin documentation.

从上面的代码中可以看到,smoothedResultsresultLabels现在是properties.由于它们没有参数,我将之前用于调用函数getSmoothedResultsgetResultLabels1005移动到视图模型本身中.这些功能不再需要,现在可以删除.

现在这一切都应该按预期进行.您的编写代码只需要从新属性收集流:

var dateLabels = resultViewModel.resultLabels.collectAsState()
var hundredResults = resultViewModel.smoothedResults.collectAsState()

我在这里删除了初始值,因为它已经是视图模型中StateFlow的一部分.实际上,您应该用Gradle依赖项androidx.lifecycle:lifecycle-runtime-compose中的collectAsStateWithLifecycle替换collectAsState.这会更高效一点,因为当可组合物不在屏幕上时,通过尊重活动的生命周期,流收集就会停止.此外,变量可以声明为val而不是var,因为它们包含流.流程的values可能会改变,但流程本身永远不会改变.为了使其完美,您可以使用Kotlin的by delegates来解开州的价值属性.那么看起来就像这样:

val dateLabels by resultViewModel.resultLabels.collectAsStateWithLifecycle()
val hundredResults by resultViewModel.smoothedResults.collectAsStateWithLifecycle()

无论您之前使用hundredResults.valuedateLabels.value,现在都可以分别使用hundredResultsdateLabels.


只剩下两个问题需要解决:

  1. 正如您可能已经意识到的那样,您无法再从UI参数化getResultLabels(5)getSmoothedResults(100).这些值现在在视图模型中硬编码.要改变这一点,价值观需要来自流量本身.这一开始听起来很复杂,但使用Kotlin可以非常优雅地完成.以smoothedResults为例,看起来像这样:

    private val smoothedResultsNum = MutableStateFlow(100)
    
    val smoothedResults: StateFlow<List<Result>> = repository.getAllResults()
        .combine(smoothedResultsNum) { results, num ->
            getResultsSmoothed(results, num)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )
    
    fun updateSmoothedResultsNum(num: Int) {
        smoothedResultsNum.value = num
    }
    

    MutableStateFlow只是您可以通过value属性访问的值的容器.它也是一个成熟的流程,因此可以与上面所示的其他流程结合.它的值一开始为100,但您可以随时通过从您的组合中调用updateSmoothedResultsNum来更改该值.当任何基础流具有新值时,smoothedResults StateFlow将始终更新.也就是说,要么数据库包含新值,要么num已更改.

    同样的情况也可以应用于其他StateFlow resultLabels.

  2. 这在您的原始代码中已经是一个问题.您的存储库的函数getAllResults()被多次调用(准确地说是两次).每次调用它时,都会在数据库上执行新的SQL查询并返回新的流.不过,内容相同,所以这实际上没有必要.当数据库中没有太多数据时,这并不重要,但随着数据的增加或功能的额外调用者,这可能很快就会成为瓶颈.

    解决方案是在存储库中将流转换为SharedFlow.这是StateFlow的通用版本.它也是一个hot流,这是必要的,因此它可以独立运行基础流,因此它可以为订阅SharedFlow的每个收集器提供相同的值.因此得名:此流,与从Room f.e.返回的cold个流相反,可以与多个Collection 家共享.这也意味着,您需要一个协程范围,就像您在视图模型中使用StateFlow所做的那样:

    class ResultRepository(
        private val resultDao: ResultDao,
        scope: CoroutineScope,
    ) {
        val allResults: Flow<List<Result>> = resultDao.getAllResults()
            .shareIn(
                scope = scope,
                started = SharingStarted.WhileSubscribed(),
            )
    
        // ...
    }
    

    除了视图模型之外,您的存储库没有内置的协程范围,因此必须将其作为控制器的参数提供.当您在活动中创建存储库时,您可以使用该活动的lifecycleScope来进行此操作.

Android相关问答推荐

超过Unity+Android磁贴内存限制,部分内容可能无法绘制

ArrayList.remove()(Kotlin中的removeAt())导致奇怪的IndexOutOfBoundsResponse异常

Kotlin Gzip字符串未按预期工作

处理Room数据库中的嵌套实体

在内部创建匿名对象的繁忙循环调用函数会产生开销吗?

如何使用react native下载android中/data/data/[应用程序包名称文件夹]/files中的文件

无法插入 LayoutNode@cc72396 子级,因为它已有父级

为什么第二个代码可以安全地在 map 中进行网络调用,因为它已被缓存?

如何在每次显示可组合项时执行代码(并且只执行一次)

当父布局的背景为浅色时,Android ProgressBar 背景 colored颜色 变为灰色

在移动设备上看到时如何增加 PasswordField 文本?

Jetpack Compose 动画性能问题

如何在 Android 应用程序未激活/未聚焦时显示视图?

如何关闭可组合对话框?

如何限制键盘输入键不允许在下一行输入(Android Jetpack Compose 中的 TextField)

无法 HEAD 'https://jcenter.bintray.com/com/facebook/react/react-native/maven-metadata.xml'

Android Studio 错误要求依赖它的库和应用程序针对 Android API 的 33 版或更高版本进行编译.

为什么我在 Jetpack Compose 中被警告可选修饰符参数应该具有默认值修饰符?

Android Studio:如何添加应用程序质量洞察窗口以查看 Android Studio 中的 Crashlytics 数据?

可组合的 fillMaxSize 和旋转不起作用