Sealed class with existing classes? - kotlin

I am implementing a custom keyboard and I am representing the keys 0-9 and the decimal separator as Button objects. Then I have one final key which is the backspace and is being represented as an ImageButton.
When I handle the click events I know that if the user clicked a Button they are adding an element to the text field and if they clicked an ImageButton they are removing the last element from the text field.
Since the keyboard only has two possible type of buttons I wanted to implement this logic with a when block without using an else branch. Is it possible? Looking at the sealed class documentation I don't think it might be but just asking to make sure.
I'd like to do something like this:
sealed class KeyboardButton {
class Button
class ImageButton
}
fun handleKeyPress(button: View) {
when(button) {
is KeyboardButton.Button -> // append element to text
is KeyboardButton.ImageButton -> // remove last element from text
}
}

You can't do this with a sealed class because you can't get the views to inherit from your sealed class. You can use an else branch that throws an exception:
fun handleKeyPress(button: View) {
when(button) {
is Button -> // append element to text
is ImageButton -> // remove last element from text
else -> error("unsupported view type")
}
}

You can wrap the existing types like this:
sealed class KeyboardButton {
class KButton(val x: Button)
class KImageButton(val x: ImageButton)
}
fun handleKeyPress(button: KeyboardButton) {
when(button) {
is KeyboardButton.KButton -> // use button.x to access the underlying Button
is KeyboardButton.KImageButton -> // similarly use button.x
}
}

Related

Access fragment view in parent activity

I have an activity which displays multiple fragments depending on which one is selected.
I also have a button in this activity and I want to obtain a value from a specific fragment when this button is clicked.
How can I obtain this value?
I tried to get the view I wanted from the fragment from the activity as the code shows below but I can understand that it doesn't work since the fragment is still to be created.
onOffButton.setOnClickListener {
if (onOffButton.text.contains("ON")) {
onOffButton.text = "TURN OFF"
var hoursPicker = findViewById<NumberPicker>(R.id.hoursPicker)
}
}
The short version is you shouldn't do this, there are all kinds of complications (especially when you're trying to access the Fragment's Views).
It's even more complicated if the Fragment might not even be added to the UI at all! If it's not there, what value are you supposed to use? If you want to somehow create the Fragment just so it exists, and so you can read the value from its text box, then that's a sign the value really needs to be stored somewhere else, so you don't need the Fragment if you want to access it.
The easiest, recommended, and modern way to share data like this is with a ViewModel:
class MyViewModel : ViewModel() {
// setting a default value here!
var currentHour: Int = 0
}
class MyActivity : AppCompatActivity() {
val model: MyViewModel by viewModels()
fun onCreate(...) {
...
onOffButton.setOnClickListener {
// access the data in the ViewModel
val currentHour = model.currentHour
}
}
}
class MyFragment : Fragment() {
// using activityViewModels so we get the parent Activity's copy of the VM,
// so we're all sharing the same object and seeing the same data
val model: MyViewModel by activityViewModels()
fun onViewCreated(...) {
...
hoursPicker.setOnValueChangeListener { _, _, newValue ->
// update the VM
model.currentHour = newValue
}
}
}
So basically, you have this ViewModel object owned by the Activity and visible to its Fragments. The VM outlives all of those components, so you don't lose data while an Activity is being destroyed on rotation, or when a Fragment isn't added to the UI, etc.
The VM is the source of data, everything else just reads from it, or updates it when something changes (like when the Fragment updates the variable when its number picker's value changes). This way, the Activity doesn't need to go "ask" the Fragment for info - it's stored in a central location, in the VM
This is the most basic way to use a ViewModel - you can start using LiveData and Flow objects to make different UI components observe data and react to changes too. For example, your button in your Activity could change some enabled state in the VM, and the Fragment (if it's added) will see that change and can do things like make the number picker visible or invisible.
It's way easier to coordinate this stuff with a ViewModel, so if you don't already know how to use them, I'd recommend learning it!

Is it possible to have a field with a generic type that refers to the actual runtime type of the containing class?

I'm fiddling around with this code where I have a base class Node which can be extended:
open class Node
class SubNode : Node()
Now, I have a Behavior class that can be attached to a node, and when this attachment happens, the behavior object is invoked:
open class Behavior {
fun attach(node: Node) {
println("Behavior was attached to a node")
}
}
open class Node {
var behavior: Behavior? = null
set(value) {
field = value
value.attach(this)
}
}
This works, but could this be generified in such way that the type of the attach method would always refer to the actual type of the attached Node? For instance, if the Behavior class was extended like this:
open class Behavior<NodeType: Node> {
open fun attach(node: NodeType) {
}
}
class SubBehavior : Behavior<SubNode>() {
override fun attach(node: SubNode) {
}
}
I've tried various ways of setting up the types in Node class, but can't figure any other way than passing the actual subclass type to the base class (which seems rather cumbersome):
open class Node<SubType: Node> {
var behavior: Behavior<SubType>? = null
}
class SubNode : Node<SubNode>()
Is there a way to do this in any other way?
I think what you need are self types, which don't exist in Kotlin (at least, not yet).
Using recursive generics like you did is the most common way around the problem.
That said, I have trouble understanding your use case here for intertwining these 2 classes together this way. Like how is behaviour used inside your node, etc.

Abstract fun invoke() not implemented

I've created a View and I encountered a problem with my interface for Buttons ClickListener.
Interface looks like this
interface CustomButtonsClickListener : () -> Unit {
fun onPlusClick(view: View, button: ImageButton)
fun onMinusClick(view: View, button: ImageButton)
}
It is implemented by the method:
fun setCustomButtonsClickListeners(clickListener: CustomButtonsClickListener) {
binding.addButton.setOnClickListener {
clickListener.onPlusClick(this, binding.addButton)
}
binding.minusButton.setOnClickListener {
clickListener.onMinusClick(this, binding.minusButton)
}
}
This is how it look like "outside"
view.setCustomButtonsClickListeners(object : CustomButtonsClickListener {
override fun onPlusClick(view: View, button: ImageButton) {
}
override fun onMinusClick(view: View, button: ImageButton) {
}
})
The Problem:
I am getting error on object saying:
Object is not abstract and does not implement abstract member public abstract fun invoke()...
What is the method invoke() and how should I implement it? I would rather do it inside the View class so I don't have to do it while using the View somewhere in the app.
This looks like confusion over syntax.  The line:
interface CustomButtonsClickListener : () -> Unit {
creates an interface which extends () -> Unit (and goes on to add two methods to it).
() -> Unit is the type of a function which takes no parameters and returns nothing useful.  You don't often need to know that function's name (as the lambda syntax hides it), but this is one of those corner cases where you find out that it's called… invoke()!
(It's described in the documentation.)
So you're defining an interface with three methods: one inherited from the parent interface, and the other two declared here.
So of course, when you try to implement the interface but implement only two of those three methods, the compiler complains that you've forgotten the third.
I guess you just want an interface with only your two new methods, so you can simply declare it with:
interface CustomButtonsClickListener {

Dealing with immutability in a GUI-based application

I have a GUI based application (Kotlin) that consists of a list of Notes to the left (it shows the note's title) and the selected Note's edition screen to the right.
When the users modify the title in the edition screen, the list item must show the new title.
In a mutable world, I'd do something like this:
interface Note {
fun title(): String
fun content(): String
fun setTitle(newTitle: String)
fun setContent(newTitle: String)
fun addListener(l: Listener)
}
class NoteListItem(n: Note) : Listener {
init {
n.addListener(this)
}
override fun onChange() { //from Listener
repaint();
}
fun repaint() {
//code
}
}
class NoteEditionScreen(n: Note) {
fun onTitleTextChanged(newTitle: String) {
n.setTitle(newTitle)
}
//...
}
Inside Note.setTitle method, listeners are notified.
Both "screens" have the same instance of Note, so changes are propagated.
However, with immutability:
interface Note {
fun title()
fun content()
fun title(newTitle: String) : Note
fun content(newContent: String) : Note
}
the method Note.title(String) returns a new instance of Note instead of changing the state.
In that case, how can I "notify" to the NoteListItem the title has changed?
Obviously, those two concepts don't go together nicely here.
The essence of the elements on your UI is: they allow for changes. The user doesn't know (or care) if the underlying object is immutable or not. He wants to change that title.
Thus, there are two options for you:
give up on your Node being immutable
introduce an abstraction layer that is used by your UI
In other words: you can add a mutable NodeContainer that is used for displaying immutable node objects on your UI.
Now you have to balance between having the advantages of keeping your Node class immutable and the disadvantages of adding mutable wrapper thingy around the Node class. But that is your decision; depending on your context.

Create an arbitrary view

So, I have a case in which I need to have N rows in form of: Label TextView/Checkbox. Maybe I will have to have more than those two views, so I want to be able to support anything that is TornadoFx View.
I've created an interface that has one method that returns TornadoFx View and it looks like this:
interface ValueContainer {
fun getView() : View
}
One of the implementations looks like this:
class BooleanValueContainer(val checked: Boolean) : ValueContainer {
val valueProperty = SimpleBooleanProperty(checked)
override fun getView(): View {
return (object : View() {
override val root = checkbox {
bind(valueProperty)
}
})
}
}
Now, when I try to use it inside init block, it doesn't show in the layout. root is GridPane and parameters is a list of objects that have name and reference to ValueContainer implementation (BooleanValueContainer or other one which I haven't shown):
init {
with(root) {
parameters.map {
row(it.name) {
it.parameterContainer.getView()
}
}
}
}
I'm stuck here for quite a while and I've tried anything I could find but nothing really worked except putting textview or checkbox block instead of getView() call, but then I would have to have logic on what view should I show inside this class which represents a view and I don't want that.
The reason this is not working for you is that you simply call parameterContainer.getView() but you don't add the View to the row. I think what's confusing you is that for builders you can just say label() for example, and it's added to the current Node in the builder tree. In your case, you just say Label() (just create an instance of Label, not call the label builder), which would create a new Label, but not add it to the children list of the current Node. To solve your problem, do:
this += it.parameterContainer.getView()
This will add the View to the row.
Apart from this, I don't quite see the point of the ValueContainer. What does it solve to put a View inside this container object? I suspect this as well might be due to a misunderstanding and I'd like to understand why you feel that you need this construct.