https://kotlinlang.org logo
#android
Title
# android
a

Abhimanyu

04/01/2024, 3:48 AM
Hi all, How to check if a method is empty? Example,
Copy code
interface TestInterface {
  fun emptyDefaultMethod() {}
}

fun TestComponent(
  impl: TestInterface,
) {
  if (impl == {} ) { // This doesn't work
    // Do something
  } else {
    // Do something else
  }
}
Usage without override for
emptyDefaultMethod
Copy code
TestComponent(object: TestInterface {})
Usage with override for
emptyDefaultMethod
Copy code
TestComponent(object: TestInterface {
  override fun emptyDefaultMethod {
    // Non empty override logic
  }
})
😂 4
m

Matthew Feinberg

04/01/2024, 10:46 AM
I don't think there's any good safe way to do that. You could possibly (though I would NOT recommend it) make it work with something like:
Copy code
val EMPTY_METHOD : ()->Unit = {}
interface TestInterface {
    val myMethod: ()->Unit = EMPTY_METHOD
}
fun TestComponent(
  impl: TestInterface,
) {
  if (impl.myMethod === EMPTY_METHOD ) {
    // Do something
  } else {
    // Do something else
  }
}
TestComponent(object: TestInterface {})
TestComponent(object: TestInterface {
    override val myMethod: ()->Unit get = {
        // Do something
    }
})
But.... there are SO many possible pitfalls to doing it this way, I'd be extremely wary of using it in production code. You might also be able to do something with reflection to check if the method has been overridden or not (you probably won't be able to tell if it's empty or not though, just if it's been overridden). I'm not familiar enough with reflection on default implementations in interfaces... But even that, I'd be super cautious about that in production code. It might be better if you give some context about why you want do it and maybe someone might have a better (more idiomatic?) solution to suggest that doesn't involve checking if the method is empty or not. (Also: Not sure this belongs in #android... it seems more like a general Kotlin question...)
a

Abhimanyu

04/01/2024, 11:11 AM
Thanks @Matthew Feinberg for sharing. I also ended up doing something very similar. (Changed the method to lambda variable, with default as null)
Copy code
interface TestInterface {
  val customSlot: @Composable (() -> Unit)? 
    get() = null
}

fun TestComponent(
  impl: TestInterface,
) {
  if (impl.customSlot == null ) {
    // Do something
  } else {
    // Do something else
  }
}
Usage without override for
emptyDefaultMethod
Copy code
TestComponent(object: TestInterface {})
Usage with override for
emptyDefaultMethod
Copy code
TestComponent(object: TestInterface {
  override val customSlot = @Composable {
    // Slot code
  }
})
To share context, I want to get some composables passed on from an app to a component (in a separate library). As the component evolves, we may need to add additional composable slots. First approach would be to deprecate the existing component and create new ones to maintain backward compatibility. But, this would eventually lead to multiple versions of a component. So, I was exploring ways in which I can add new slots to the component in the library without breaking the clients usages. (In a backwards compatible way)
I have been testing for last few hours, and did not find any major issues so far. If you could share any possible issues that can happen, I would really appreciate it. thank you color
m

Matthew Feinberg

04/01/2024, 11:18 AM
I don't think there's any guarantee that the compiler won't optimize it in some way where the comparison fails on some platforms... (I don't know enough about that to be sure, but it's a behavior I'd be hesitant to rely on).
Are you looking for backwards compatibility at a source level or binary level? At a source level, you could just use default arguments...
a

Abhimanyu

04/01/2024, 11:19 AM
The app and library are all specific to android context.
fails on some platforms
Does this refer to platforms other than android?
Both, we are looking for API and ABI stability.
m

Matthew Feinberg

04/01/2024, 11:22 AM
It would probably be stable in the JVM, I'd imagine, but it doesn't feel very idiomatic to me. Why not just use function overloads? Something like this... First version...
Copy code
@Composable
fun MyComponent(
    firstSlot: @Composable ()->Unit = {} 
) {
   // Single common implementation here
}
Added slot:
Copy code
@Composable
fun MyComponent(
    firstSlot: @Composable ()->Unit = {},
    secondSlot: @Composable ()->Unit = {} 
) {
   // Single common implementation here
}

// For backwards compatibility
@Composable
fun MyComponent(
    firstSlot: @Composable ()->Unit = {} 
) = MyComponent( firstSlot = firstSlot, secondSlot = {} )
You wouldn't need multiple versions; you'd just delegate simpler overloads to a common implementation.
(edited sample code to clarify that)
If you need to detect if a slot is used or not, you could make them nullable...
Copy code
@Composable
fun MyComponent(
    firstSlot: (@Composable ()->Unit)? = null,
    secondSlot: (@Composable ()->Unit)? = null  
) {
   // Single common implementation here
   if( firstSlot == null ) {
   } else {
   }
}

// For backwards compatibility
@Composable
fun MyComponent(
    firstSlot: (@Composable ()->Unit)? = null 
) = MyComponent( firstSlot = firstSlot, secondSlot = null )
a

Abhimanyu

04/01/2024, 11:30 AM
In our case, The composable slots are not directly passed as parameters to the components. Custom slot
Copy code
interface ComponentSlots {
  // Some slots
}
Usage
Copy code
Component(
  data = ComponentDataClass(),
  eventHandler = {},
  customSlots = object: ComponentCustomSlots {},
)
Adding new slot
Copy code
interface ComponentSlots {
  // Some slots
  val slot1: @Composable (() -> Unit)? 
    get() = null
}
usage
Copy code
Component(
  data = ComponentDataClass(),
  eventHandler = {},
  customSlots = object: ComponentCustomSlots {
    override val slot1 = @Composable {
      // Slot 1 code
    }
  },
)
So, function overload might not help us. Reasoning for such complex logic. Client can have trailing lambdas. So adding parameter in between the list of parameters breaks either the API or the ABI.
m

Matthew Feinberg

04/01/2024, 11:34 AM
Hmm... I might be wrong, but I don't think adding methods to interfaces is ABI compatible on the JVM target, even with default method implementations.
🤔 1
a

Abhimanyu

04/01/2024, 11:36 AM
Assuming I am now working with the variable using a lambda instead of a method directly, is that still the same?
m

Matthew Feinberg

04/01/2024, 11:39 AM
I think that's still the same, yes.
So if I understand correctly, the reason for not using parameters is because of the risk of the interpretation of a trailing lambda changing?
👌 1
You can prevent trailing lambda by putting a
vararg
parameter at the end. Use a class with a private constructor for the type.
Copy code
fun MyComponent(slotOne: ()->Unit = {}, slotTwo: ()->Unit = {}, vararg unused: NO_TRAILING_LAMBDA) {

}
😮 1
Copy code
class NO_TRAILING_LAMBDA private constructor()
Anything after the
vararg
must be a named parameter. A trailing lambda is a compile error.
a

Abhimanyu

04/01/2024, 11:42 AM
Example
Copy code
fun Component1(
  slot1: @Composable: () -> Unit
) {}
Usage
Copy code
Component1 {
}
Adding new Slot
Copy code
fun Component1(
  slot1: @Composable: () -> Unit
  slot2: @Composable: () -> Unit
) {}
breaks component1 existing usage.
The
vararg
things looks like a nice idea. Thanks, let me explore more on that. gratitude thank you
m

Matthew Feinberg

04/01/2024, 11:46 AM
No problem 🙂 I wish I could take credit, but learned it from looking at the Compose Multiplatform source code.