Как правильно расширить существующий компонент MVVM UI? - PullRequest
0 голосов
/ 07 сентября 2018

В моем настольном приложении Kotlin, использующем TornadoFX, я создал макет AudioCard (подкласс VBox), который имеет несколько меток и основные элементы управления аудиопроигрывателем. Это AudioCard имеет AudioCardViewModel, который обрабатывает события из пользовательского интерфейса, и 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()
        }

    }
}

До этого момента я пытался сохранить код как можно более общим, позволяя себе или другим повторно использовать этот компонент пользовательского интерфейса в будущих приложениях, которые должны воспроизводить звук. Однако для моего текущего приложения наиболее целесообразно иметь более специализированную версию этого компонента пользовательского интерфейса, которая инициализируется непосредственно из моего класса модели данных и может расширять некоторые действия. Я пробовал что-то вроде этого (обязательные поля и классы из предыдущего блока кода были переключены на 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)
}

К сожалению, это не так просто. Переопределив viewModel в CustomAudioCard, свойство viewModel перестает быть окончательным, вызывая NullPointerException, когда функция инициализации суперкласса AudioCard пытается использовать модель представления для установки метки заголовка до того, как дочерний класс имеет инициализировал модель представления.

Я подозреваю, что может быть выход из этого путем определения интерфейса AudioCardViewModel и / или использования возможности Kotlin делегировать с ключевым словом by, но у меня сложилось впечатление, что определение интерфейса (как в MVP ) не должен быть необходим для MVVM.

Подводя итог: Как правильно расширить существующий элемент управления MVVM, особенно в контексте библиотеки Kotlin TornadoFX?

1 Ответ

0 голосов
/ 07 сентября 2018

Вот решение, с которым я столкнулся Пол Стовелл . Вместо того, чтобы создавать модель представления в представлении (вариант 1 в статье Стовелла), я переключился на внедрение модели представления в представление (вариант 2). Я также провел рефакторинг для лучшего соответствия MVVM с помощью документации TornadoFX и этого ответа относительно того, куда должна идти бизнес-логика . Мой код 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
       }
    }
}

Я вижу несколько способов исправить это, особенно в отношении моделей, но этот вариант, похоже, хорошо работает в моем сценарии.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...