I've a code:
val d = Single
.zip<List<X>, Optional<Y>, DataContent>(
xSingle,
YSingle,
BiFunction { x, y ->
val b = if (y.isPresent()) {
y.get()
} else {
null
}
return#BiFunction DataContent(x, b)
})
.subscribe({ data ->
...
}, { t ->
...
})
I've heard, that using Optional to check null value as shown in an example, is bad practice. Is that true? Why? Can someone show an alternative using RxJava2?
In general, Optional has a restricted set of use cases and is in danger of being overrused. You can refer to this answer by Java author Brian Goetz to understand these (emphasis added):
But we did have a clear intention when adding this [java.util.Optional] feature, and it was not to be a general purpose Maybe or Some type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors.
For example, you probably should never use it for something that returns an array of results, or a list of results; instead return an empty array or list. You should almost never use it as a field of something or a method parameter.
In the original example posted, Optional<Y> is used as a method parameter so this is against Java best practices. Besides, Maybe is idiomatic in RxJava.
Assuming you have something like the following:
class X
class Y
data class DataContent constructor(val listOfX: List<X>, val y: Y)
You could write a function like this that would seem to fit your use case:
fun zipToDataContent(maybeListOfX: Maybe<List<X>>, maybeY: Maybe<Y>): Maybe<DataContent> =
Maybe.zip<List<X>, Y, DataContent>(
maybeListOfX,
maybeY,
BiFunction { listOfX, y -> DataContent(listOfX, y) })
Tests:
#Test
fun testZipToDataContentWithRealY() {
val x = X()
val y = Y()
val maybeListOfX = Maybe.just(listOf(x))
val maybeY = Maybe.just(y)
zipToDataContent(maybeListOfX, maybeY).test()
.assertOf {
Maybe.just(DataContent(listOf(x), y))
}
}
#Test
fun testZipToDataContentWithEmptyY() {
val x = X()
val maybeListOfX = Maybe.just(listOf(x))
val maybeY = Maybe.empty<Y>()
zipToDataContent(maybeListOfX, maybeY).test()
.assertOf {
Maybe.empty<DataContent>()
}
}
Related
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)
In the code below, i'd like to generalize it so I instead of viewBinding.editText.text and viewModel.property.price can use the same method for e.g viewBinding.secondEditText.text and viewModel.property.income.
I'm thinking exchanging viewBinding.editText.text for a variable defined in the primary constructor, but then I'd need the variable to contain a reference to viewBinding.editText.text/viewBinding.secondEditText.text etc. instead of containing a value.
Is this possible? I've looked at lengths for this but can't find anything useful.
fun updateProperty() {
//... other irrelevant code
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
//... other irrelevant code
}
You can pass parameters into a function, yeah!
This is the easy one:
fun updateProperty(editText: EditText) {
val contents = editText.text.toString()
}
simple enough, you just pass in whatever instance of an EditText and the function does something with it.
If you're just using objects with setters and getters, you can just define the type you're going to be using and pass them in. Depending on what viewmodel.property is, you might be able to pass that in as well, and access price and income on it. Maybe use an interface or a sealed class if there are other types you want to use - they need some commonality if you're going to be using a generalised function that works with them all.
Properties are a bit tricker - assuming viewmodel.property contains a var price: Double, and you didn't want to pass in property itself, just a Double that exists somewhere, you can do it like this:
import kotlin.reflect.KMutableProperty0
var wow: Double = 1.2
fun main() {
println(wow)
setVar(::wow, 6.9)
println(wow)
}
fun setVar(variable: KMutableProperty0<Double>, value: Double) {
variable.set(value)
}
>> 1.2
>> 6.9
(see Property references if you're not familiar with the :: syntax)
KMutableProperty0 represents a reference to a mutable property (a var) which doesn't have any receivers - just a basic var. And don't worry about the reflect import, this is basic reflection stuff like function references, it's part of the base Kotlin install
Yes, method parameters can also be references to classes or interfaces. And method parameters can also be references to other methods/functions/lambdas.
If you are dealing with cases that are hard to generalize, consider using some kind of inversion of control (function as parameter or lambda).
You add a lambda parameter to your updateProperty function
fun updateProperty(onUpdate: (viewBinding: YourViewBindingType, viewModel: YourViewModelType) -> Unit) {
//... other irrelevant code
// here you just call the lambda, with any parameters that might be useful 'on the other side'
onUpdate(viewBinding, viewModel)
//... other irrelevant code
}
Elsewhere in code - case 1:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
Elsewhere in code - case 2:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Elsewhere in code - case 3:
updateProperty() { viewBinding, viewModel ->
// I am a totally different case, because I have to update two properties at once!
viewModel.property.somethingElse1 = viewBinding.thirdEditText.text.toString().toBoolean()
viewModel.property.somethingElse2 = viewBinding.fourthEditText.text
.toString().replaceAll("[- ]*", "").toInt()
}
You could then go even further and define a function for the first 2 cases, since those 2 can be generalized, and then call it inside the lambda (or even pass it as the lambda), which would save you some amount of code, if you call updateProperty() in many places in your code or simply define a simple function for each of them, and call that instead, like this
fun updatePrice() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
fun updateIncome() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Then elsewhere in code you just call it in a really simple way
updatePrice()
updateIncome()
Trying to sort the custom components on defined order, and other subjects should be after that desired order: Like if there is any other subject except the defined List ie "GK" then it should be on last position etc.
However I am getting Null Pointer Exception due to subject is not defined in the requireList if scheduleCommandList have the subject which is not in requiredList. How can I overcome this?
Desired Order List is Below:
private val requiredList: HashMap<String, Int> = hashMapOf(
"Maths" to 0,
"Physics" to 1,
"Science" to 2,
)
Sorting function to sort the List:
private fun sortCommandList(scheduleCommandList: ArrayList<BaseComponent>): ArrayList<BaseComponent> {
val comparator = Comparator { o1: BaseComponent, o2: BaseComponent ->
return#Comparator requiredList[o1.name]!! - requiredList[o2.name]!!
}
val copy = arrayListOf<BaseComponent>().apply { addAll(scheduleCommandList) }
copy.sortWith(comparator)
return copy
}
It seems you understand the problem correctly. If an item is not present in requiredList then you still try to compare their required positions and this causes NullPointerException. Remember that you should use !! only in cases when you are sure there can't be a null. In this case null is possible and we have to handle it somehow. The easiest is to replace it with Int.MAX_VALUE which places the item at the end. Also, this code can be really much simpler:
private fun sortCommandList(scheduleCommandList: List<BaseComponent>): List<BaseComponent> {
return scheduleCommandList.sortedBy { requiredList[it.name] ?: Int.MAX_VALUE }
}
It can be even better to create this utility as extension function:
private fun List<BaseComponent>.mySort(): List<BaseComponent> {
return sortedBy { requiredList[it.name] ?: Int.MAX_VALUE }
}
Then we can simplify the name of the function, because it is implicit that it is used to sort BaseComponent objects.
So in this case I have three array that I want to map into a list of object (The objects has three parameters as well).
I have three arrays allProductCodeList, allProductNameList, and allProductQtyList (Content of this array is from a Retrofit Client response)
allProductCodeList = response.body()?.data?.map { it?.stkProdcode }!!
allProductNameList = response.body()?.data?.map { it?.proName }!!
allProductQtyList = response.body()?.data?.map { it?.stkAllqty }!!
//I printed these arrays to LogCat so it is easier to see
Log.i("Order", allProductCodeList.toString())
Log.i("Order", allProductNameList.toString())
Log.i("Order", allProductQtyList.toString())
This is the content of the array I printed into the LogCat:
This is the Data class which I want to parse these arrays into:
data class ProcodeRecommendationListDataClass(
val procode: String?,
val productName: String?,
val qty: Int?
)
What I want to do is parse these three array into a list that will looks like:
[ProcodeRecommendationListDataClass("0100009","", 2),ProcodeRecommendationListDataClass("0100061","", 1),ProcodeRecommendationListDataClasslass("0100062","", 6)]
I've done it when I only have two arrays to map (I use this solution for it). But now it I have three arrays, I confused.
If there's any detail I miss to point out, Just let me know !
1. This is you three arrays
allProductCodeList = response.body()?.data?.map { it?.stkProdcode }!!
allProductNameList = response.body()?.data?.map { it?.proName }!!
allProductQtyList = response.body()?.data?.map { it?.stkAllqty }!!
2. Make A New List
List<ProcodeRecommendationListDataClass> finalList = List()
3. Run a for loop with any of three array size with indices;
for(pos in allProductCodeList.indices){
finalList.add(ProcodeRecommendationListDataClass(allProductCodeList[pos],
allProductNameList[pos],
allProductQtyList[pos] ))
}
Now finalList is your result.
One forward straight way is to use one more zip - someone once said all problems are solved with one more level of inderection:
allProductCodeList
.zip(allProductNameList)
.zip(allProductQtyList)
.map { (codeAndName, qt) ->
ProcodeRecommendationListDataClass(
codeAndName.first,
codeAndName.second,
qt
)
}
It doesn't look super pretty, but it should be ok.
Another way is to create your own zip that takes 2 lists:
fun <X, Y, Z, R> List<X>.zipWith(l1: List<Y>, l2: List<Z>, transform: (X, Y, Z) -> R): List<R> {
val length = min(min(size, l1.size), l2.size)
val result = mutableListOf<R>()
for (i in 0 until length) {
result.add(transform(get(i), l1[i], l2[i]))
}
return result
}
fun main() {
val k = allProductCodeList.zipWith(allProductNameList, allProductQtyList) { code, name, qt ->
ProcodeRecommendationListDataClass(
code,
name,
qt
)
}
println(k)
}
Basically extends a list of X that takes 2 other lists. It iterates through them applying the transform method (this is so you can map the elements as you go).
This will iterate always the smallest amount of elements - in other words, you won't get more elements than the smallest list. I can't be sure, but I assume the default implementation does something similar.
Why not just create objects in place?
val allProducts = response.body()?.data?.map {
ProcodeRecommendationListDataClass(it?.stkProdcode, it?.proName, it?.stkAllqty)
} ?: emptyList()
You can use mapIndexed instead of map. use index to get third data.
val list = allProductCodeList.zip(allProductNameList)
.mapIndexed { index, pair -> SomeClass(pair.first, pair.second,allProductQtyList[index]) }
Given the following setup:
I have 2 repositories: Repository A and Repository B both of them return live data.
I have a ViewModel that uses both of these repositories.
I want to extract something from Repository A and depending on the result I want to grab something from Repository B and then transform the result before returning to UI.
For this I have been looking at the LiveData Transformation classes. The examples show a single transformation of the result however I want something along the lines of chaining two transformations. How can I accomplish this?
I have tried setting something up like this but get a type mismatch on the second transformation block:
internal val launchStatus: LiveData<String> = Transformations
.map(respositoryA.getData(), { data ->
if (data.isValid){
"stringA"
} else {
//This gives a type mismatch for the entire block
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
(Also please let me know if there is an alternate/recommended approach for grabbing something for chaining these call i.e. grab something from A and then grab something from B depending on result of A and so on)
Your lambda sometimes returns the String "stringA", and sometimes returns the LiveData<String> given by:
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
This means that your lambda doesn't make sense - it returns different things in different branches.
As others have mentioned, you could write your own MediatorLiveData instead of using the one given by Transformations. However, I think it's easier to do the following:
internal val launchStatus: LiveData<String> = Transformations
.switchMap(respositoryA.getData(), { data ->
if (data.isValid) {
MutableLiveData().apply { setValue("stringA") }
} else {
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
All I've done is made the first code branch also return a LiveData<String>, so now your lambda makes sense - it's a (String) -> LiveData<String>. I had to make one other change: use switchMap instead of map. This is because map takes a lambda (X) -> Y, but switchMap takes a lambda (X) -> LiveData<Y>.
I used MediatorLiveData to solve this problem.
MediatorLiveData can observer other LiveData objects and react to them.
Instead of observing either of the repositories. I created myData (instance of MediatorLiveData) in my ViewModel class and have my view observe this object. Then I add Repository A as the initial source and observe that and only add Repository B if the result of A requires it. This allows me to keep the transformations associated with the live data of each of the repo and still process each result in the correct order. See below for implementation:
internal val myData: MediatorLiveData<String> = MediatorLiveData()
private val repoA: LiveData<String> = Transformations.map(
respositoryA.getData(), { data ->
if (data.isValid) "stringA" else ""
})
private val repoB: LiveData<String> = Transformations.map(
repositoryB.getData(), { data -> "stringB"
})
fun start() {
myData.addSource(repoA, {
if (it == "stringA") {
myData.value = it
} else {
myData.addSource(repoB, {
myData.value = it
})
}
})
}
Note: The solution does not cover the case where repoB might be added multiple times but it should be simple enough to handle.
I would try using switchMap instead of map:
Similar to map(), applies a function to the value stored in the LiveData object and unwraps and dispatches the result downstream. The function passed to switchMap() must return a LiveData object.
You can nest transformations.
val finalLiveData = Transformations.switchMap(liveData1){
val search = it
Transformations.switchMap(liveData2) {
db(context).dao().all(search, it)
}
}
You can transform the data by using switchmap. Here's an example documentation.