3

我正在尝试学习 Arrow 库并通过将我的一些 Android Kotlin 代码从更命令式风格转换为函数式风格来改进我的函数式编程。我一直在应用程序中进行一种 MVI 编程,以使测试更简单。

“传统”方法

视图模型

我的视图模型有一个LiveData视图的状态加上一个公共方法将用户交互从视图传递到视图模型,因此视图模型可以以任何合适的方式更新状态。

class MyViewModel: ViewModel() {
    val state = MutableLiveData(MyViewState()) // MyViewState is a data class with relevant data

    fun instruct(intent: MyIntent) { // MyIntent is a sealed class of data classes representing user interactions
        return when(intent) {
            is FirstIntent -> return viewModelScope.launch(Dispatchers.IO) {
                val result = myRoomRepository.suspendFunctionManipulatingDatabase(intent.myVal)
                updateStateWithResult(result)
            }.run { Unit }
            is SecondIntent -> return updateStateWithResult(intent.myVal)
        }
    }
}

活动

Activity 订阅LiveData并且在状态更改时,它使用状态运行渲染函数。该活动还将用户交互作为意图传递给视图模型(不要与 Android 的Intent类混淆)。

class MyActivity: AppCompatActivity() {
   private val viewModel = MyViewModel()

   override fun onCreateView() {
      viewModel.state.observe(this, Observer { render(it) })
      myWidget.onClickObserver = {
         viewModel.instruct(someIntent)
      }
   }

   private fun render(state: MyViewState) { /* update view with state */ }
}

Arrow.IO 函数式编程

我很难找到使用 Arrow 的 IO monad 使副作用明显且可进行单元测试的不纯函数的示例。

查看模型

到目前为止,我已经把我的视图模型变成了:

class MyViewModel: ViewModel() {
    // ...

    fun instruct(intent: MyIntent): IO<Unit> {
        return when(intent) {
            is FirstIntent -> IO.fx {
                val (result) = effect { myRoomRepository.suspendFunctionManipulatingDatabase(intent.myVal) }
                updateStateWithResult(result)
            }
            is SecondIntent -> IO { updateStateWithResult(intent.myVal) }
        }
    }
}

我不知道我应该如何让这些 IO 东西Dispatcher.IO像我一直在做的那样运行viewModelScope.launch。我找不到如何使用 Arrow 执行此操作的示例。进行 API 调用的似乎都不是 Android 应用程序,因此没有关于 Android UI 与 IO 线程的指导。

查看模型单元测试

现在,因为我看到的一个好处是,当我编写视图模型的单元测试时,我可以进行测试。如果我模拟存储库以检查是否suspendFunctionManipulatingDatabase使用预期参数调用。

@Test
fun myTest() {
    val result: IO<Unit> = viewModel.instruct(someIntent)
    result.unsafeRunSync()
    // verify suspendFunctionManipulatingDatabase argument was as expected
}

活动

我不知道如何将上述内容合并到我的活动中。

class MyActivity: AppCompatActivity() {
   private val viewModel = MyViewModel()

   override fun onCreateView() {
      viewModel.state.observe(this, Observer { render(it) })
      myWidget.onClickObserver = {
         viewModel.instruct(someIntent).unsafeRunSync() // Is this how I should do it?
      }
   } 

   // ...
}

我的理解是 IO 块中的任何内容都不会立即运行(即它是惰性的)。您必须调用 attempt() 或 unsafeRunSync() 来获取要评估的内容。

  1. 从 Activity调用viewModel.instruct意味着我需要创建一些范围并调用Dispatchers.IO对吗?这是 Bad(TM) 吗?我能够使用“传统”方法将协程完全限制在视图模型中。

  2. 我在哪里合并Dispatchers.IO来复制我所做的viewModelScope.launch(Dispatchers.IO)

  3. 这是您在使用 Arrow 时应该构建单元测试的方式IO吗?

4

1 回答 1

5

这确实是一篇非常值得阅读的帖子。我还建议深入研究我编写的这个示例应用程序,它也使用了 ArrowFx。

https://github.com/JorgeCastilloPrz/ArrowAndroidSamples

  • 请注意我们如何使用 fx 构建完整的程序并在架构中的所有级别返回 Kind。这使得代码对 F 类型具有多态性,因此您可以根据环境随意使用 F 的不同运行时数据类型来运行它。在这种情况下,我们最终在边缘使用 IO 运行它。这就是本例中的活动,但也可以是应用程序类或片段。将此视为您的应用程序的入口点。如果我们谈论的是 jvm 程序,那么等价的就是 main()。这只是一个如何编写多态程序的示例,但如果您想保持更简单,您可以使用 IO.fx 并在任何地方返回 IO。
  • 请注意我们如何在 fx 块内的数据源中使用 continueOn() 离开并返回到主线程。协程上下文更改在 ArrowFx 中是显式的,因此计算会在 continueOn 之后立即跳转到传递的线程,直到您故意再次切换到不同的线程。这故意使线程更改显式。
  • 您可以注入这些调度程序以在测试中使用不同的调度程序。希望我能很快在 repo 中提供这方面的例子,但你可以想象这会是什么样子。
  • 对于如何编写测试的语法,请注意您的程序将返回 Kind(如果您采用多态)或 IO,因此您将从测试中 unsafeRunSync 它(与生产代码中的 unsafeRunAsync 或 unsafeRunAsyncCancellable 相比,因为 Android 需要它是异步的)。那是因为我们希望我们的测试是同步的并且也是阻塞的(对于后者,我们需要注入适当的调度程序)。
  • 当前警告:回购中提出的解决方案仍然不关心取消、生命周期或幸存的配置更改。这是我想尽快解决的问题。使用具有混合样式的 ViewModel 可能会有机会。这是 Android,所以如果能带来更好的生产力,我不会害怕混合风格。我想到的另一种选择可能是功能更强大的东西。ViewModel 最终通过使用 ViewModelStore 使用保留配置状态现有 API 来保留自己。这最终听起来像是一个简单的缓存,这绝对是一个副作用,可以包装到 IO 中实现。我想考虑一下。

我肯定还建议阅读完整的 ArrowFx 文档以更好地理解:https ://arrow-kt.io/docs/fx/我认为这会有所帮助。

有关使用函数式编程和 Arrow to Android 的方法的更多想法,您可以查看我的博客https://jorgecastillo.dev/我的计划是从 2020 年开始围绕此编写深入的内容,因为有很多人感兴趣。

另一方面,您可以在 Kotlinlang JetBrains Slack 中找到我或任何其他 Arrow 团队维护人员,我们可以在那里进行更详细的对话或尝试解决您可能遇到的任何疑问 https://kotlinlang.slack.com/

最后澄清一下:函数式编程只是一种解决通用问题的范式,例如异步、线程、并发、依赖注入、错误处理等。这些问题可以在任何程序上找到,无论平台如何。即使在 Android 应用程序中。这就是为什么 FP 与其他任何选项一样适用于移动设备,但我们仍在探索以更符合人体工程学的方式提供最佳 API 来满足通常的 Android 需求。从这个意义上说,我们正在探索中,2020年将是非常有希望的一年。

希望这有帮助!您的想法似乎与这种方法的整体运作方式完全一致。

于 2019-12-30T09:13:00.857 回答