MVIKotlin学习笔记(5):时间旅行
时间旅行
时间旅行是一个强大的调试工具,它允许你记录所有来自活跃的Stores
的事件和状态。当事件被记录后你可以浏览、重演和调试它。它的核心功能是多平台,被所有支持目标实现。然而,一些特定的功能只能在特定的平台上使用。
时间旅行是一种调试工具,它可能会影响性能。理想情况下它不应该在生产环境中使用。
启动并使用时间旅行有三个主要步骤:
- 向所有
Store
的工厂提供一个time-travel-aware,这是StoreFactory
的变体。 - 在app上运行时间旅行服务端。
- 使用客户端连接服务端并使用它。
提供一个StoreFactory用于时间旅行
TimeTravelStoreFactory
用于创建Store
的实例,它能够记录和重演事件。在调试构建中,StoreFactory
的变体能够通过DI传递给所有Store
工厂,而不是DefaultStoreFactory
。
本节介绍的功能可用于所有受支持的目标。
假设有以下Store
工厂:
internal class CalculatorStoreFactory(private val storeFactory: StoreFactory) {
fun create(): CalculatorStore =
object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
name = "CounterStore",
// ...
) {
}
// 其他代码
}
它接受一个StoreFactory
并用它来创建CalculatorStore
的实例。现在可以传递任何StoreFactory
。如果你想启用时间旅行,只要传递TimeTravelStoreFactory
的实例即可:
val storeFactory = TimeTravelStoreFactory()
CalculatorStoreFactory(storeFactory).create()
也可以组合它和LoggingStoreFactory
:
val storeFactory = LoggingStoreFactory(TimeTravelStoreFactory())
CalculatorStoreFactory(storeFactory).create()
通常需要在主程序的某处定义一个全局的StoreFactory
,并将它传递给所有依赖项。
运行时间旅行服务端
每个启用了时间旅行的app都有一个TimeTravelController的全局实例。每个Store
自动连接到这个控制器。这个控制器接受来自外部的各种指令、来自以注册的Stores
的记录事件、替换状态和重新触发事件用于调试。
为了允许远程控制,app应该运行一个时间旅行服务端。服务端绑定了TimeTravelController
和外部世界。服务端的实现在不同的平台是不同的。目前,以下的目标受支持:
- 基于JVM的目标:
android
和jvm
- Darwin/Apple目标:
ios
,tvos
,watchos
和macos
- JavaScript(
js
),只支持谷歌浏览器
为其他平台实现服务端应该没有技术限制。作者欢迎提issue。
所有的服务端实现(除了JavaScript)是基于TCP的。默认端口是6379
,端口可以在初始化期间显式更改。
通信协议是开放的,但被认为是内部的。不同版本之间没有兼容性保证。
在安卓app上运行
首先,在app模块中导入时间旅行依赖项,替换<version>
为最后一个发行版本。
implementation("com.arkivanov.mvikotlin:mvikotlin-timetravel:<version>")
在Application
类的onCreate()
中开始时间旅行。
class App : Application() {
private val timeTravelServer = TimeTravelServer()
override fun onCreate() {
super.onCreate()
timeTravelServer.start()
}
}
由于时间服务服务端确实使用了设备上的互联网与开发机器进行通信,因此即使应用程序不使用互联网,您也需要在AndroidManifest.xml中声明使用互联网权限。
在JVM app上运行
首先,在app模块中导入时间旅行依赖项,替换<version>
为最后一个发行版本。
implementation("com.arkivanov.mvikotlin:mvikotlin-timetravel:<version>")
在应用的main
函数中创建一个TimeTravelServer
的实例并提供runOnMainThread
参数,这可以是SwingUtilities.invokeLater {}
或其他被使用的coroutines/Reaktive。
fun main() {
TimeTravelServer(runOnMainThread = { SwingUtilities.invokeLater(it) })
.start()
}
在Drawin/Apple app上运行
为了在Drawin/Apple设备上设置TimeTravelServer
,mvikotlin-timetravel
依赖项必须在build.gradle.kts
的共享模块中导入。与此同时,mvikotlin-timetravel
模块必须作为api
依赖项添加。这可以在commonMain
源集或只在Dawwin源集中完成。
kotlin {
ios {
binaries {
framework {
export("com.arkivanov.mvikotlin:mvikotlin-timetravel:<version>")
}
}
}
sourceSets {
named("commonMain") {
dependencies {
api("com.arkivanov.mvikotlin:mvikotlin-timetravel:<version>")
}
}
}
}
然后在AppDelegate
中启动TimeTravelServer
的实例。
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
private let s = TimeTravelServer()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 在应用启动后重写定制点。
s.start()
return true
}
}
在谷歌浏览器(JavaScript)上运行
首先,在app模块中导入时间旅行依赖项,替换<version>
为最后一个发行版本。
implementation("com.arkivanov.mvikotlin:mvikotlin-timetravel:<version>")
在应用的main
函数中启动TimeTravelServer
。
fun main() {
TimeTravelServer().start()
// 重置代码
}
使用时间旅行客户端
时间旅行客户端与服务端进行通信,并提供UI用于控制功能和显示数据。目前提供了三种客户端变体。
- Intellij IDEA插件 - 目前仅适用于安卓应用
- 独立java应用 - 适用于安卓,JVM和Drawin/Apple应用
- 谷歌浏览器插件 - 适用于JavaScript网页应用
使用Intellij IDEA插件
Intellij IDEA插件可以在IDE中直接使用。目前只能连接安卓应用。
如何安装
可以从Intellij IDEA Marketplace找到该插件,可以在Intellij IDEA和Android Studio中直接安装。在Settings -> Plugins -> Marketplace标签的搜索栏中查找“MVIKotlin”。
如何使用
该插件使用ADB转发TCP端口6379。
首先确保TimeTravelServer
在应用中运行,然后运行安卓应用并打开IDE中的时间旅行插件,可以单击“连接”并开始记录状态更改。插件首次运行时会询问adb
路径。
在桌面上使用独立客户端应用程序
桌面客户端应用程序提供了与IntelliJ IDEA插件类似的功能。但它也可以连接到JVM和Darwin/Apple应用程序。
如何安装
桌面时间旅行客户端应用程序尚未发布,因此您需要从源代码构建并运行它。可以运行以下命令(至少需要JDK11):
./gradlew :mvikotlin-timetravel-client:app-desktop:run
如何使用
客户端通过TCP连接服务端。
连接到已经运行了TimeTravelServer
的安卓应用的简单方法是打开设置并选择 “Connect via ADB”。然后点击 “Connect” 按钮,客户端将提示您使用 adb 可执行路径,然后应建立连接。客户端使用ADB转发TCP端口。
连接到非安卓应用(或连接到安卓应用但不使用ADB),打开设置并取消选择 “Connect via ADB”,输入设备的主机地址。如果是本地运行的设备,主机地址通常是localhost
。对于远程设备,主机地址应该明确指定。请参阅设备的设置以找到它的TCP地址。在任何情况下,设备的端口应该是可连接的(例如拥有权限、端口在白名单中)。
构建发行版
时间旅行客户端是使用Compose for Desktop实现的,因此组装一个发行版是可行的。请参阅 documentation page。
使用谷歌浏览器插件(实验性)
谷歌浏览器扩展提供了与其他时间旅行客户端类似的功能,但专门为Web应用程序设计。
谷歌浏览器扩展程序目前是实验性的。最终它将被提升为稳定或删除。
如何安装
可以从Chrome Web Store安装插件。
如何使用
这个插件增加了开发者工具面板,它的外观和工作原理与其他时间旅行客户类似。确保在Web应用程序中已经启动TimeTravelServer
。当web页面被加载时,右键并选择 “Inspect” 目录项。导航到“MVIKotlin”工具面板并单击“Connect”按钮。该插件会在web页面中注入特殊脚本,它在TimeTravelServer
和插件中代理消息。
记录事件
每当时间旅行客户端连接到应用时,它可以开始记录事件。按下“Start recording”按钮开始记录。所有记录的事件将会出现在左侧的列表中。按下“Stop recording”停止记录。
检查事件
当记录结束后,应用会进入检查状态。在这个状态下所有的Stores
会从输入和输出断开连接,所有的事件会累积并推迟到检查结束后执行。
每一个记录事件都能被检查。在列表中选择一个事件,细节会出现在右侧。事件细节的精确表示依赖于时间旅行服务器的实现,在不同的平台上有所不同。
安卓和JVM目标的时间旅行服务器使用反射以精确解析对象属性。
Darwin/Apple目标的时间旅行服务器只使用了toString
函数。建议定义states,intents,actions和messages为数据类。
JavaScript目标的时间旅行服务器使用JSON.stringify
函数。类似于Darwin/Apple,也建议使用数据类。
时间旅行
在检查状态期间,Store
的状态可以回滚和重演,会用到下面这些按钮:
- “Move to start” - 移动到第一个记录状态
- “Step backward” - 移动到前一个记录状态
- “Step forward” - 移动到后一个记录状态
- “Move to end” - 移动到最后一个记录状态
UI总是会展示当前选择的状态。
调试记录事件
在检查状态时,每个记录事件都可以被再一次触发。一个典型的使用情况是记录app一个不正确的行为,然后在代码中设置断点并触发一个记录事件。要触发一个事件,在列表中选择它,然后单击“Debug the selected event”按钮。
如果触发的事件是一个Intent
或一个Action
,每个事件的相应Store
的Executor
的新调试实例会被创建。Executor
相应的调试实例用相同的State
,与记录时的State
相同。在调试会话期间调度的所有Messages
都通过 Reducer
传递,并且Executor
的调试实例的State
会相应更新。任何在事件调试会话期间被发布的Label
会被忽略。
如果触发的事件是一个Message
,Reducer
只会带着Message
和对应的State
被调用,它的结果会被忽略。
导出和导入事件
这个功能目前只支持JVM和安卓应用。这个功能只有在所有相关类(Intents
, Actions
, Messages
, States
, 和Labels
)必须实现Serializable
接口。MVIKotlin
模块提供了方便的 JvmSerializable
接口,它可以在common源集中使用。
如果要导出记录事件,按下“Export events”按钮。选择一个文件夹并键入文件名。所有的事件会被序列化并保存在文件中。
如果要导入之间导出的事件,按下“Import events”按钮并选择一个文件。所有的事件会从文件被反序列化并应用于当前Stores
。
在导入文件时应用程序的代码应该和导出时相同。反序列化的类要符合序列化时的类。否则它的行为是不确定的。
结束检查
如果要结束检查,按下“Cancel”按钮,之后在检查期间等待队列中的Intents
和Labels
都会自动处理。