如何正确扩展现有的MVVM UI组件?

时间:2018-09-06 21:30:24

标签: mvvm kotlin tornadofx

在使用TornadoFX的Kotlin桌面应用程序中,我创建了一个{ bar: 1337, foo: 'Hi', } 布局(AudioCard的子类),该布局具有一些标签和基本的音频播放器控件。该VBox具有一个AudioCard(用于处理来自UI的事件)和一个AudioCardViewModel(用于存储标题,字幕,音频文件路径等信息)。简化版本如下所示。

AudioCardModel

到目前为止,我一直尝试使代码尽可能通用,以允许自己或其他人在将来需要播放音频的应用程序中重用此UI组件。但是,对于我当前的应用程序,最有意义的是拥有一个更专业的UI组件版本,该组件可以直接从我的数据模型类初始化自身并可以扩展某些操作。我已经尝试过类似的操作(先前代码块中的必填字段和类已切换为data class AudioCardModel( var title: String, var audioFile: File ) class AudioCardViewModel(title: String, audioFile: File) { val model = AudioCardModel(title, audioFile) var titleProperty = SimpleStringProperty(model.title) fun playButtonPressed() { // play the audio file from the model } } class AudioCard(title: String, audioFile: File) : VBox() { val viewModel = AudioCardViewModel(title, audioFile) init { // create the UI label(title) { bind(viewModel.titleProperty) } button("Play") { viewModel.playButtonPressed() } } } ):

open

不幸的是,这并不是那么简单。通过覆盖data class CustomAudioCardModel( var customData: CustomData ) class CustomAudioCardViewModel(customData: CustomData) : AudioCardViewModel(customData.name, customData.file) { val model = CustomAudioCardModel(customData) override fun playButtonPressed() { super.playButtonPressed() // do secondary things only needed by CustomAudioCardViewModel } } class CustomAudioCard(customData: CustomData): AudioCard(customData.name, customData.file) { override val viewModel = CustomAudioCardViewModel(customData) } 中的viewModelCustomAudioCard属性不再是最终的,当viewModel超类的init函数尝试使用视图模型进行设置时,导致NullPointerException子类初始化视图模型之前的标题标签。

我怀疑可以通过定义AudioCard接口和/或使用Kotlin的AudioCardViewModel委托功能来解决这个问题,但我的印象是定义接口(例如在MVP中)对于MVVM来说不是必需的。

总结:扩展现有MVVM控件的正确方法是什么,特别是在Kotlin TornadoFX库的上下文中?

1 个答案:

答案 0 :(得分:1)

这是我从Paul Stovell遇到的解决方案。我没有在视图内创建视图模型(Stovell文章中的选项1),而是切换到将视图模型注入视图(选项2)中。在TornadoFX documentationthis answer regarding where business logic should go的帮助下,我还进行了重构以更好地实现MVVM。我的AudioCard代码现在看起来像这样:

open class AudioCardModel(title: String, audioFile: File) {
    var title: String by property(title)
    val titleProperty = getProperty(AudioCardModel::title)

    var audioFile: File by property(audioFile)
    val audioFileProperty = getProperty(AudioCardModel::audioFile)

    open fun play() {
        // play the audio file
    }
}

open class AudioCardViewModel(private val model: AudioCardModel) {
    var titleProperty = bind { model.titleProperty }

    fun playButtonPressed() {
        model.play()
    }
}

open class AudioCard(private val viewModel: AudioCardViewModel) : VBox() {
    init {
        // create the UI
        label(viewModel.titleProperty.get()) {
            bind(viewModel.titleProperty)
        }
        button("Play") {
            viewModel.playButtonPressed()
        }
    }
}

扩展视图现在看起来像:

class CustomAudioCardModel(
    var customData: CustomData
) : AudioCardModel(customData.name, customData.file) {
    var didPlay by property(false)
    val didPlayProperty = getProperty(CustomAudioCardModel::didPlay)

    override fun play() {
        super.play()
        // do extra business logic
        didPlay = true
    }
}

class CustomAudioCardViewModel(
    private val model: CustomAudioCardModel
) : AudioCardViewModel(model) {
    val didPlayProperty = bind { model.didPlayProperty }
}

class CustomAudioCard(
    private val viewModel: CustomAudioCardViewModel 
) : AudioCard(customViewModel) {
    init {
       model.didPlayProperty.onChange { newValue ->
           // change UI when audio has been played
       }
    }
}

我看到了一些清理方法,尤其是在模型方面,但是此选项在我的情况下似乎很好用。