In my Kotlin desktop application using TornadoFX, I have created an AudioCard layout (subclass of VBox) which has a few labels and basic audio player controls. This AudioCard has an AudioCardViewModel which handles events from the UI and an AudioCardModel which holds information like the title, subtitle, audio file path, etc. A simplified version is shown below.
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()
}
}
}
Up until this point, I have tried to keep the code as general as possible, allowing myself or others to reuse this UI component in future applications that need to play audio. However, for my current application, it makes the most sense to have a more specialized version of this UI component that initializes itself directly from my data model class and can extend some of the actions. I've tried something like this (the required fields and classes from the previous code block were switched to 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)
}
Unfortunately, this isn't so straightforward. By overriding viewModel in CustomAudioCard, the viewModel property ceases to be final, causing a NullPointerException when the init function of the AudioCard superclass tries to use the view model to set up the title label before the child class has initialized the view model.
I suspect there might be a way out of this by defining an AudioCardViewModel interface and/or using Kotlin's ability to delegate with the by keyword, but I'm under the impression that defining the interface (like in MVP) shouldn't be necessary for MVVM.
To summarize: What is the correct way to extend an existing MVVM control, specifically in the context of the Kotlin TornadoFX library?
Here is the solution I came across from Paul Stovell. Instead of creating the view model within the view (Option 1 in Stovell's article), I switched to injecting the view model into the view (Option 2). I also refactored for better MVVM adherence with help from the TornadoFX documentation and this answer regarding where business logic should go. My AudioCard code now looks like this:
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()
}
}
}
The extension view now looks like:
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
}
}
}
I see a few ways to clean this up, especially regarding the models, but this option seems to work well in my scenario.
Related
How can I create objects within a Vapor-Applicaton that are instantiated when the app starts and are acessible from within my controllers?
I would like to use Dictionary to store some of my models across multiple requests and assign them to a user via hidden fields.
Unfortunately SessionData only accepts Strings.
Many thanks in advance
Michael
The obvious way is to extend Application. I used the documentation for Repositories as inspiration. First, create a structure to hold your properties:
struct AppConfig {
var emailStatus: EmailStatus
static var environment: AppConfig {
return .init(emailStatus: .unknown)
}
}
Then extend:
extension Application {
struct AppConfigKey: StorageKey {
typealias Value = AppConfig
}
var config: AppConfig {
get {
storage[AppConfigKey.self] ?? .environment
}
set {
storage[AppConfigKey.self] = newValue
}
}
}
Finally, initialise in configure.swift:
app.config.emailStatus = .unknown
I’ve used an enumeration as an example but it can be whatever you want.
Edit: Addressing OP's issues and concerns
I put the above code in a separate source file, so you need:
import Vapor
To gain access to StorageKey, etc.
Accessing the running application instance in a controller is easy, it's available from the request as in request.application.
I am a beginner in Kotlin and trying to implement MVVM design pattern on android development. I have to implement a Recyclerview in a fragment.
How we can set adapter with value to a recyclerview from the viewmodel class since the api call is observed within the viewmodel.
My fragment class is look like as below
class NotesFragment : Fragment() {
lateinit var binding:FragmentNotesBinding
lateinit var viewModel:NoteListViewModel
companion object {
fun newInstance(param1: String): NotesFragment {
val fragment = NotesFragment()
val args = Bundle()
fragment.arguments = args
return fragment
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_notes,container,false)
viewModel = NoteListViewModel(binding)
return binding.root
}
is it good practice that we passing the binding object to our viewmodel class and updating the viewModel object again from ViewModel class as below
private fun onSuccess(success: NoteResponse?) {
dataVisibility.value=View.VISIBLE
success.let {
noteAdapter= noteAdapter(documentResponse?.result,mContext)
binding.viewModel=this
}
}
The core about MVVM is seperation of concerns. ViewModel should not hold any reference to the View(Activity/Fragment). LikeWise your Data/Repository layer should not hold ViewModel reference.
So to achieve data flow you can use either Reactive Observables(Rx)/ LiveData from android architecture components to pass back the data.
1) Create MutableLiveData in your Viewmodel.
2) Set the MutableLiveData with api response model.
3) Observe the MutableLiveData in your Fragment for the response data.
4) Use the data to set your adapter inside your fragment.
Please check ViewModel - Developer document to understand better.
I am trying to pass data from Activity A to Activity B but without using Intent putExtra nor using SharePreferences, I'm using a MVVM pattern in kotlin, so right now I'm using an object declaration like this
object SharedData{ var myMovies: ArrayList<Movie>? = null }
So later on in Activity A i'm assigning a value like this
val movieList = ArrayList<Movie>()
movieList.add(Movie("The Purge"))
SharedData.myMovies = movieList
And then in Activity B i retrieve this value by:
val movieList = ArrayList<Movie>()
SharedData.myMovies.let {
movieList = it
}
But I'm new in kotlin and now I know this is not the correct approach. because the singleton object allocates memory and it never gets collected by the GC. So now I'm stucked here.
Any guidance or help would be appreciated
So, if you're using MVVM pattern it's very straight forward. Use the basic ViewModel implementation with Android Architecture Components. See more of this in https://developer.android.com/topic/libraries/architecture/
class MyActivity : AppCompatActivity() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_layout)
myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
myViewModel.myObject.observe(this, Observer { mySharedObject ->
//TODO whatever you want to do with your data goes here
Log.i("SomeTag", mySharedObject.anyData)
})
}
}
class MyCoachFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let {
myViewModel = ViewModelProviders.of(it).get(MyViewModel::class.java)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val myObject = MyObject() // what ever object you need to share between Activities and Fragments, it could be a data class or any object
myViewModel.myObject.postValue(myObject)
}
}
class MyViewModel : ViewModel() {
var myObject = MutableLiveData<MyObject>()
}
My suggestion would be - If you want to share the data just between two activities, you should use intent and send the content as parcelable Object( parcelableArray for your Movielist scenario ) to the next activity. This would be clean implementation.
But I'm new in kotlin and now I know this is not the correct approach.
It is not wrong approach either, can be used depends on your use case. If it meets all the below scenarios, you can go for static variable approach. But Static object will be cleared when the app is killed (either by user or by system)
1.If the stored data size is less.
2.Data does not need to be persisted on App kill and relaunch.
3.Data is shared across many activities.
the singleton object allocates memory and it never gets collected by
the GC
Yes. It's true. Static variables are not eligible for garbage collection. But as long as the memory print is very less, it's okay to use static variable provided it meets above mentioned scenarios.
In Swift we usually use extensions as a way to organize methods in separate blocks and even files. This makes code much cleaner but it also allows us to do some tricks such as:
class API {}
extension API {
class Lists{}
}
extension Lists {
class Posts {
func latest() -> [Post] {
// get latest posts from a REST api
}
}
}
We can put any of the extension blocks in a separate file and it works perfectly in Swift.
Now one do the following to get the latest posts from the API in a clean way
let posts = API.Lists.Posts.latest()
Trying to convert that code into Kotlin I used SwiftKotlin converter tool that I thought that might work but it doesn't compile as It seems to be invalid:
class API {}
class API.Lists {}
class Lists.Posts {
companion object {
fun latest() {
// get posts
}
}
}
So I came up with the following that works fine and also compiles but it's not suitable for my case as methods can be quite long and I can't afford to have them all in one class in one file and I don't know how I can split them in multiple files.
class API {
class Lists {
class Posts {
companion object {
fun latest() {
}
}
}
}
}
Any suggestion is appreciated.
To put an extension on a companion object, you can write
fun API.Lists.Posts.Companion.latest() ...
You still need
class API {
class Lists {
class Posts {
companion object {
}
}
}
}
in a single file, but extensions can be defined elsewhere.
If you just want to mimic usage of these calls, you can use objects, like this for example:
object ApiPosts {
fun latest() {}
}
object ApiLists {
val Posts = ApiPosts
}
object API {
val Lists = ApiLists
}
API.Lists.Posts.latest()
But this is really not a Kotlin way, and in common case it's a bad practice to write in a language the way it's not supposed to.
One possible solution is to just extend the inner classes with normal functions not static ones (companion):
class ParentClass {
class InnerClass {
}
}
And in any other file you could do:
fun ParentClass.InnerClass.instanceMember() {
}
And for usage:
ParentClass.InnerClass().instanceMember()
Of course this isn't exactly like the Swift version but it's close enough.
I have the following class which is my view model (this is very simple right now, but it will contain a chunk more logic eventually):
public class IndicoTalk : ITalk
{
private Talk _talk;
public IndicoTalk(Talk t)
{
this._talk = t;
}
public string Title
{
get { return _talk.Title; }
}
}
Now, I have a reactive ui view for this guy:
public sealed partial class TalkView : UserControl, IViewFor<ITalk>
{
public TalkView()
{
this.InitializeComponent();
this.Bind(ViewModel, x => x.Title, y => y.TalkTitle.Text);
}
Note that the IViewFor is for ITalk, not IndicoTalk. This is because I can have other types of talk, and they will all fit into the same view.
And I register this ViewModel in my App start up:
Locator.CurrentMutable.Register(() => new TalkView(),
typeof(IViewFor<IWalker.DataModel.Inidco.IndicoMeetingRef.IndicoTalk>));
Finally, in another viewmodel I have a ReactiveList which contains a bunch of these IndicoTalks's. Of course, when I bind this to a ListBox, ReactiveUI fails to find the view. If I switch to IViewFor then everything works just fine.
What is the proper way to gently redirect the view resolution in this case?
A half-way solution: leave all code above the same, but put in the IViewFor ITalk instead of IndicoTalk. This works, but means I will have to register with Splat (the CurrentMutable call above) every ViewModel that inherrits from ITalk. I'd love to avoid that if possible!
Many thanks!
So, why not just do:
Locator.CurrentMutable.Register(() => new TalkView(), typeof(IViewFor<ITalk>));