I'm trying to genericise the boilerplate around a very common pattern, and Kotlin brings me tantalisingly close.
I've built a class that serves as a listener manager, as follows:
class GenericListenerSupport <EventArgumentType, ListenerFunction: (EventArgumentType) -> Unit> {
private val listeners = mutableListOf<ListenerFunction>()
fun addListener(listener: ListenerFunction) {
listeners.add(listener)
}
fun removeListener(listener: ListenerFunction) {
listeners.remove(listener)
}
fun fireListeners(argument: EventArgumentType) {
listeners.forEach { it.invoke(argument) }
}
}
and it can be used as follows:
class ExampleWithArgument {
private val listenerSupport = GenericListenerSupport<String, (String)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ value -> System.out.println("My string: "+value)})
}
fun exampleFire() {
listenerSupport.fireListeners("Hello")
}
}
So far, so good. But what if the listener has no arguments? Or stretching even further, multiple parameters.
I can scrape through with this:
class ExampleWithNoArgument {
private val listenerSupport = GenericListenerSupport<Nothing?, (Nothing?)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ System.out.println("I've got no argument")})
}
fun exampleFiring() {
listenerSupport.fireListeners(null)
}
}
but it smells, and obviously it's no use for multiple parameters.
Is there a better way to pull this off? e.g. something supporting this concept:
private val listenerSupport = GenericListenerSupport<???, (String, Double)->Unit>()
Since your GenericListenerSupport declares a type parameter EventArgumentType and expects an instance of it in fun fireListeners(argument: EventArgumentType), I doubt you can support multiple arguments in a clean way. Instead, I'd suggest using a data class (which is not so much extra code), as a clean and type-safe way to wrap multiple values:
data class MyEvent(val id: String, val value: Double)
private val listenerSupport = GenericListenerSupport<MyEvent, (MyEvent) -> Unit>()
As to passing no value, you can also use Unit, the type that has exactly one value Unit:
listenerSupport.fireListeners(Unit)
The type system and resolution won't allow you to pass no argument where a single one is expected, but, as #Ruckus T-Boom suggested, you can make an extension to fire listeners with no value where Unit is expected:
fun GenericListenerSupport<Unit>.fireListeners() = fireListeners(Unit)
A bit off-topic, but I think you can simplify the type if you don't need custom function types and (EventArgumentType) -> Unit is sufficient:
class GenericListenerSupport<EventArgumentType> {
/* Just use `(EventArgumentType) -> Unit` inside. */
}
Related
We can write functional interfaces in Kotlin like this - function-interfaces
fun interface Sum {
fun add(a: Int, b: Int): Int
}
val sumImpl = Sum { a, b ->
return#Sum a + b
}
val testSum = sumImpl.add(4, 5)
How can we write Jetpack Composable function in same way? Below code is not working.
`
fun interface SampleText {
#Composable
fun text(data : String)
}
val textImpl = SampleText { data ->
return#SampleText #Composable { Text(data) }
}
#Composable
fun testText() = textImpl.text("Data")
I have tried this as well, but this also didn't work.
fun interface SampleText {
fun text(data : String) : #Composable () -> Unit
}
val textImpl = SampleText { data ->
#Composable { Text(data) }
}
#Composable
fun testText() = textImpl.text("Data")
The first version is not compiling in its lambda form because your interface function returns a Unit and your'e actually having a Type mismatch error, its just weird the compiler reports Internal Error when you try to return a #Composable annotated function, but the issue becomes clear if you simply return something like a String.
vs
To solve your first version, either you fully declare an object of the class like this (though its useless since you want a lambda version of your SAM interface not an actual object in the first place)
val textImpl = object: SampleText {
#Composable
override fun text(data: String) {
Text(data)
}
}
, but it will work just by simply calling the testText() function like this.
testText()
Or change it to your second version.
Now for your second version, since your interface returns a #Composable lambda, you have to invoke it as well in the call-site, making two function invocations to make it work,
testText()() // two invocations
first call invokes your testText() function, second pair of parenthesis invokes the #Composable lambda from your interface.
Or simply call .invoke()
testText().invoke() // just call the .invoke() of the returned composable lambda
Either of the implementations and calls display the text "Data"
I have the below working code which uses a dropdown to update the satusFilterFlow to allow for the filtering of characters through the getCharacterList call. The getCharacterList call uses the jetpack paging and returns Flow<PagerData<Character>>.
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
// private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.flatMapLatest{ statusFilter ->
characterRepository.getCharacterList(null, statusFilter.status)
.cachedIn(viewModelScope)
.flowOn(Dispatchers.IO)
}.asLiveData()
Given the above working solution, what is the correct flow extension to allow for me to add multiple StateFlows as I build out additional filters (e.g. SearchFilter).
I have tried combineTransorm as follows:
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.combineTransform(searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
However, this gives me a "Not enough information to infer type variable R" error.
The usual way to understand and/or fix those errors is to specify types explicitly in the function call:
statusFilterFlow.combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(searchFilterFlow) { ... }
This is orthogonal to the problem at hand, but I'd also suggest using the top-level combineTransform overload that takes all source flows as argument (instead of having the first one as receiver), so there is a better symmetry. Since I believe there is no reason one of the filters is more special than the other.
All in all, this gives:
val listData: LiveData<PagingData<Character>> =
combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(statusFilterFlow, searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
For anymore else, this is too complex or doesn't work out for you ... Use Combine then flatMap latest on the top of that.
private val _selectionLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
val searchKeyword: MutableStateFlow<String> = MutableStateFlow("")
val unassignedJobs: LiveData<List<Job>> =
combine(_selectionLocation, searchKeyword) { location: Location?, keyword: String ->
Log.e("HomeViewModel", "$location -- $keyword")
location to keyword
}.flatMapLatest { pair ->
_repo.getJob(Status.UNASSIGNED, pair.first).map {
Log.e("HomeViewModel", "size ${it.size}")
it.filter { it.desc.contains(pair.second) }
}
}.flowOn(Dispatchers.IO).asLiveData(Dispatchers.Main)
There is function collectAsState() applicable to a StateFlow property in order to observe it in a Composable.
A composable requires a StateFlow because StateFlow guarantees an initial value. A Flow doesn't come with that guarantee.
Now, what is the way to go if I have a StateFlow property but I want to apply an operator (like map) before collecting the Flow in the Composable?
Here an example:
Let's say a repository exposes a StateFlow<MyClass>
val myClassStateFlow: StateFlow<MyClass>
data class MyClass(val a: String)
... and a view model has a dependency on the repository and wants to expose only the property a to its Composable...
val aFlow = myClassState.Flow.map { it.a } // <- this is of type Flow<String>
The map operator changes the type from StateFlow<MyClass> to Flow<String>.
Is it semantically justified that aFlow has no initial value anymore? After all its first emission is derived from the initial value of myClassStateFlow.
It's required to convert Flow back into StateFlow at some point. Which is the more idiomatic place for this?
In the view model using stateIn()? How would the code look like?
In the composable using collectAsState(initial: MyClass) and come up with an initial value (although myClassStateFlow had an initial value)?
See this issue on GitHub
Currently there is no built-in way to transform StateFlows, only Flows. But you can write your own.
Way I ended up solving was to use the example in that post.
First create a notion of a DerivedStateFlow.
class DerivedStateFlow<T>(
private val getValue: () -> T,
private val flow: Flow<T>
) : StateFlow<T> {
override val replayCache: List<T>
get () = listOf(value)
override val value: T
get () = getValue()
#InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) {
flow.collect(collector)
}
}
Then have an extension on StateFlow like the current map extension on Flow
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
return DerivedStateFlow(
getValue = { transform(this.value) },
flow = this.map { a -> transform(a) }
)
}
Now in your Repository or ViewModel, you can use it as below.
class MyViewModel( ... ) {
private val originalStateFlow:StateFlow<SomeT> = ...
val someStateFlowtoExposeToCompose =
originalStateFlow
.mapState { item ->
yourTransform(item)
}
}
Now you can consume it as you expect in Compose without any special work, since it returns a StateFlow.
I have two different data class, eg:
Guitar & Piano.
I wanna create a list to store both data class, eg: instruments,
so that I can add both dataclass into list by:
instruments.add(Guitar())
instruments.add(Piano())
I thinking about using:
val instruments = arrayListOf<Any>()
My question is it any better way to achieve this?
Kotlin does not support multi-typing. However, you can apply workarounds.
First, to help modeling your problem, you could create a super type (interface or abstract class) as suggested in comments, to extract common properties, or just have a "marker" interface. It allows to narrow accepted objects to a certain category, and improve control.
Anyhow, you can filter any list to get back only values of wanted type using filterIsInstance :
enum class InstrumentFamily {
Strings, Keyboards, Winds, Percussions
}
abstract class Instrument(val family : InstrumentFamily)
data class Guitar(val stringCount : Int) : Instrument(InstrumentFamily.Strings)
data class Piano(val year: Int) : Instrument(InstrumentFamily.Keyboards)
fun main() {
val mix = listOf(Guitar(6), Piano(1960), null, Guitar(7), Piano(2010))
val guitars: List<Guitar> = mix.filterIsInstance<Guitar>()
guitars.forEach { println(it) }
val pianos : List<Piano> = mix.filterIsInstance<Piano>()
pianos.forEach { println(it) }
}
However, beware that this operator will scan all list, so it can become slow if used with large lists or many times. So, don't rely on it too much.
Another workaround would be to create an index per type, and use sealed classes to ensure full control over possible types (but therefore, you'll lose extensibility capabilities).
Exemple :
import kotlin.reflect.KClass
enum class InstrumentFamily {
Strings, Keyboards, Winds, Percussions
}
sealed class Instrument(val family : InstrumentFamily)
data class Guitar(val stringCount : Int) : Instrument(InstrumentFamily.Strings)
data class Piano(val year: Int) : Instrument(InstrumentFamily.Keyboards)
/** Custom mapping by value type */
class InstrumentContainer(private val valuesByType : MutableMap<KClass<out Instrument>, List<Instrument>> = mutableMapOf()) : Map<KClass<out Instrument>, List<Instrument>> by valuesByType {
/** When receiving an instrument, store it in a sublist specialized for its type */
fun add(instrument: Instrument) {
valuesByType.merge(instrument::class, listOf(instrument)) { l1, l2 -> l1 + l2}
}
/** Retrieve all objects stored for a given subtype */
inline fun <reified I :Instrument> get() = get(I::class) as List<out I>
}
fun main() {
val mix = listOf(Guitar(6), Piano(1960), null, Guitar(7), Piano(2010))
val container = InstrumentContainer()
mix.forEach { if (it != null) container.add(it) }
container.get<Guitar>().forEach { println(it) }
}
I am looking at using sealed class to represent a finite set of possible values.
This is part of a codegeneration project that will write a very large number of such classes, which can each have a lot of cases. I am therefore concerned about app size. As it is very likely that several cases can have the same attributes, I am looking at using wrappers such as:
data class Foo(val title: String, ...lot of other attributes)
data class Bar(val id: Int, ...lot of other attributes)
sealed class ContentType {
class Case1(val value: Foo) : ContentType()
class Case2(val value: Bar) : ContentType()
// try to reduce app size by reusing the existing type,
// while preserving the semantic of a different case
class Case3(val value: Bar) : ContentType()
}
fun main() {
val content: ContentType = ContentType.Case1(Foo("hello"))
when(content) {
is ContentType.Case1 -> println(content.value.title)
is ContentType.Case2 -> println(content.value.id)
is ContentType.Case3 -> println(content.value.id)
}
}
Is this how I should approach this problem?
If so, how can I best make the properties of the associated value accessible from the sealed class? So that
is ContentType.Case2 -> println(content.value.id)
becomes
is ContentType.Case2 -> println(content.id)
Is this how I should approach this problem?
IMHO, yes, but with some semantic changes, listed after.
How can I best make the properties of the associated value accessible from the sealed class?
You can generate extensions or instance functions for each subclass.
e.g.
val ContentType.Case2.id: String get() = value.id
In this way, you can successfully call:
is ContentType.Case2 -> println(content.id)
How can I reduce the app size while preserving the semantic of another case?
You can do it generating only one class for all the cases which need the same types as parameters and using Kotlin contracts to handle them.
Taking your example, you could generate:
sealed class ContentType {
class Case1(val value: Foo) : ContentType()
class Case2_3(val value: Bar, val caseSuffix: Int) : ContentType()
}
As you can see, the classes Case2 and Case3 are now only one class and caseSuffix identifies which one of them it is.
You can now generate the following extensions (one for each case):
#OptIn(ExperimentalContracts::class)
fun ContentType.isCase1(): Boolean {
contract {
returns(true) implies (this#isCase1 is ContentType.Case1)
}
return this is ContentType.Case1
}
#OptIn(ExperimentalContracts::class)
fun ContentType.isCase2(): Boolean {
contract {
returns(true) implies (this#isCase2 is ContentType.Case2_3)
}
return this is ContentType.Case2_3 && caseSuffix == 2
}
#OptIn(ExperimentalContracts::class)
fun ContentType.isCase3(): Boolean {
contract {
returns(true) implies (this#isCase3 is ContentType.Case2_3)
}
return this is ContentType.Case2_3 && caseSuffix == 3
}
Since you are using contracts the client can now use them with:
when {
content.isCase1() -> println(content.title)
content.isCase2() -> println(content.id)
content.isCase3() -> println(content.id)
}
As you can see, a further optimization could be removing the property caseSuffix for cases with only one suffix to avoid unnecessary properties.