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

如何正确扩展现有的MVVMUI组件?,mvvm,kotlin,tornadofx,Mvvm,Kotlin,Tornadofx,在使用TornadFX的Kotlin桌面应用程序中,我创建了VBox的声卡布局子类,它有一些标签和基本的音频播放器控件。此声卡有一个AudioCardViewModel,用于处理来自UI的事件,还有一个AudioCardModel,用于保存标题、字幕、音频文件路径等信息。简化版本如下所示 data class AudioCardModel( var title: String, var audioFile: File ) class AudioCardViewModel(tit

在使用TornadFX的Kotlin桌面应用程序中,我创建了VBox的声卡布局子类,它有一些标签和基本的音频播放器控件。此声卡有一个AudioCardViewModel,用于处理来自UI的事件,还有一个AudioCardModel,用于保存标题、字幕、音频文件路径等信息。简化版本如下所示

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()
        }

    }
}
到目前为止,我一直试图使代码尽可能通用,允许我自己或其他人在将来需要播放音频的应用程序中重用此UI组件。然而,对于我当前的应用程序来说,最有意义的是拥有一个更专业的UI组件版本,它直接从我的数据模型类初始化自己,并可以扩展一些操作。我尝试过类似的方法,前一个代码块中的必填字段和类被切换为打开:

不幸的是,这并不是那么简单。通过覆盖CustomAudioCard中的viewModel,viewModel属性不再是最终属性,当AudioCard超类的init函数在子类初始化视图模型之前尝试使用视图模型设置标题标签时,会导致NullPointerException

我怀疑有一种方法可以解决这个问题,即定义AudioCardViewModel接口和/或使用Kotlin的by关键字进行委托的能力,但我的印象是,MVVM不需要像MVP中那样定义接口


总而言之:扩展现有MVVM控件的正确方法是什么,特别是在Kotlin TornadoFX库中?

以下是我遇到的解决方案。我没有在Stovell的文章中的视图选项1中创建视图模型,而是将视图模型注入到视图选项2中。在和的帮助下,我还进行了重构,以更好地遵守MVVM。我的声卡代码现在如下所示:

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
       }
    }
}
我看到了一些方法来解决这个问题,特别是关于模型,但是这个选项在我的场景中似乎工作得很好

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
       }
    }
}