Prevent mutableSet order change in Kotlin - kotlin

I am using a Mutable set of Strings to store some data whose order I dont want to change, by using preferenceEditObject.putStringSet() method. Although I add and remove data from the set, but the order should remain the same.
private fun getStrings(): MutableSet<String>? {
return preferenceObject.getStringSet("initList", mutableSetOf())
}
private fun setStrings(list: MutableSet<String>?) {
preferenceEditObject.putStringSet("initList",list).apply()
}
But when I use the getStrings() method, the order of elements gets changed. What should I do?

When writing, store the index together with the value, and when reading, sort by this index
private fun getStrings(): Set<String> {
return preferenceObject.getStringSet("initList", setOf())!!
.map { it.split(":", limit = 2).let { (index, value) -> index.toInt() to value } }
.sortedBy { it.first }
.mapTo(mutableSetOf()) { it.second }
}
private fun setStrings(list: Set<String>) {
preferenceEditObject.putStringSet("initList", list.mapIndexedTo(mutableSetOf()) { index, s -> "$index:$s" }).apply()
}

Related

Combine search and sort with kotlin flow

I need to search and sort data simultaneously. I did it for search but it wont trigger for sort. I'm also using pagination.
User can type in searchView and flow will trigger, but problem is when i change sortState (ascending or descending) it wont trigger flow for searching articles on api endpoint.
ViewModel:
private val currentQuery = MutableStateFlow(DEFAULT_QUERY)
private val sortState = MutableStateFlow<SortOrderState>(SortOrderState.Ascending)
val flow = currentQuery
.debounce(2300)
.filter {
it.trim().isNotEmpty()
}
.distinctUntilChanged()
.flatMapLatest { query ->
articleRepository.getSearchResult(query.lowercase(Locale.ROOT),sortState.value)
}
Fragment:
lifecycleScope.launch {
viewModel.flow.collectLatest { articles ->
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
adapter.submitData(articles)
}
}
In fragment I have function: viewModel.searchNews(newText)
And in Main activity: viewModel.setSortState(SortOrderState.Ascending) (one menu item clicked) to see if MutableStateFlow.value is changed. I can see that in ViewModel i can change these values but if I do:
val flow=currentQuery.combine(sortState){
query,state ->
}
I never changes if I click on sort menu item, only if I type something to search.
Edit: sortState is not updating in flow variable, I checked setSortState and I can clearly see that state is changed but in flow I only send ascending all the time.
Main activity:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_sortAsc -> {
viewModel.setSortState(SortOrderState.Ascending)
}
R.id.menu_sortDesc -> {
viewModel.setSortState(SortOrderState.Descening)
}
}
return super.onOptionsItemSelected(item)
}
ViewModel:
fun setSortState(sortOrderState: SortOrderState) {
sortState.value = sortOrderState
}
SortOrderState:
sealed interface SortOrderState{
object Ascending : SortOrderState
object Descening : SortOrderState
}
Edit 2: Collecting in HomeFragment it always gives me Ascending value even if i click on menu item for descending sort
lifecycleScope.launch {
viewModel.sortState.collectLatest {
Log.d(TAG, "onCreateViewSort: $it")
}
In ViewModel I can see sortState is changed:
fun setSortState(sortOrderState: SortOrderState) {
sortState.value = sortOrderState
Log.d(TAG, "setSortState: ${sortState.value}")
}
You aren't using your sort state as a Flow. You're only passively using its value, so your output flow won't automatically update when the value changes.
Instead, you need to combine your flows.
Here, I also moved your lowercase transformation before the distinctUntilChanged because I think that makes more logical sense. Also, it makes sense to include the trim in the transformation and not just in the filter.
val flow = currentQuery
.debounce(2300)
.map { it.trim().lowercase(Locale.ROOT) }
.filter { it.isNotEmpty() }
.distinctUntilChanged()
.combine(sortState) { query, sort -> query to sort }
.flatMapLatest { (query, sort) ->
articleRepository.getSearchResult(query, sort)
}
You might also consider tagging this with shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 1) so the search doesn't have to restart on a screen rotation.

Should I get rid of big switch case?

I have a factory which includes many HTML attribute generators which returns one of them based on the type of attribute, so I wanted to see if there is a better way of doing this.
class AttributeHtmlGeneratorFactory {
fun create(property: String): AttributeHtmlGenerator {
when (property) {
"animation" -> {
return AnimationHtmlGenerator()
}
...
"left", "top" -> {
return PositionHtmlGenerator()
}
...
"scaleX" , "scaleY", ... , "direction" -> {
return UnusedAttributesHtmlGenerator()
}
this when switch has like 20 switch cases in it.
this is the interface which all these classes are using
interface AttributeHtmlGenerator {
fun generateHtml(member: KProperty1<HtmlComponentDataModel, *>, component: HtmlComponentDataModel ): String
}
and this is where and how I'm using all of these:
var result = ""
HtmlComponentDataModel::class.memberProperties.forEach { member ->
val generator = AttributeHtmlGeneratorFactory().create(member.name)
result = result.plus(generator.generateHtml(member, component))
}
return result
also, this is a simple implementation of the interface:
class ButtonFillHtmlGenerator : AttributeHtmlGenerator {
override fun generateHtml(member: KProperty1<HtmlComponentDataModel, *>, component: HtmlComponentDataModel): String {
var result = ""
member.get(component)?.let {
result = result.plus("background-color:${it};")
}
return result
}
}
is there anyway to make this better?
If you just want to reformat the when statement, I suggest you you do like this:
fun create(property: String): AttributeHtmlGenerator = when (property)
{
"animation" -> AnimationHtmlGenerator()
"left", "top" -> PositionHtmlGenerator()
"scaleX", "scaleY", "direction" -> UnusedAttributesHtmlGenerator()
else -> error("No generator found for property $property")
}
If you want to split this logic across modules, you would use a Map.
class AttributeHtmlGeneratorFactory {
private val generatorMap = mutableMapOf<String, () -> AttributeHtmlGenerator>()
init {
assignGeneratorToProperties("animation") { AnimationHtmlGenerator() }
assignGeneratorToProperties("left", "top") { PositionHtmlGenerator() }
}
fun create(property: String): AttributeHtmlGenerator {
return generatorMap[property]?.invoke() ?: error("No generator found for property $property")
}
fun assignGeneratorToProperties(vararg properties: String, provider: () -> AttributeHtmlGenerator) {
properties.forEach {
generatorMap[it] = provider
}
}
}
This way you can call assignGeneratorToProperties in parts of the code and thus split the initialization logic.
Performance-wise, when/if-else statements are really performant when you have a few cases but a HashMap outperforms them for a lot of elements. You decide what to use depending on your case.

Reduce/Collect `List<Map<String, Set<String>` to `Map<String, Set<String>>`

After performing a parallelStream() on a List, I end up with a List<Map<String, Set<String>. I want to unify this into a Map<String, Set<String>> (which will only keep uniques across the List of Maps).
I am unfamiliar with the collect and reduce functions, so don't have anything to go ahead with.
Existing code:
private val TYPES = listOf("string", "integer")
private fun getLinesOfEachTypeAcrossMultipleFiles(files: List<File>): Map<String, Set<String>> {
return files
.parallelStream()
.map { file ->
TYPES.associate {
it to getRelevantTypeLinesFromFile(file)
}
}
// Converted into a Stream<String, Set<String>>
// .reduce() / collect() ?
}
private fun getRelevantTypeLinesFromFile(it: File): Set<String> {
// Sample code
return setOf()
}
If you're looking for an equivalent Java code, you can stream all the entries using flatMap and then collect them as a Map with a merge function as :
Map<String, Set<String>> some(List<Map<String, Set<String>>> listOfMap) {
return listOfMap.stream()
.flatMap(a -> a.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(s1, s2) -> {
s1.addAll(s2);
return s1;
}));
}
I figured out and implemented a Kotlin-specific solution of using the fold operator (instead of reduce or collect):
private val TYPES = listOf("string", "integer")
private fun getLinesOfEachTypeAcrossMultipleFiles(files: List<File>): Map<String, Set<String>> {
return files
.map { file ->
TYPES.associate { it to getRelevantTypeLinesFromFile(file) }
}
.fold(mutableMapOf<String, MutableSet<String>>()) { acc, map ->
acc.apply {
map.forEach { key, value ->
acc.getOrPut(key) { mutableSetOf() }.addAll(value)
}
}
}
}
private fun getRelevantTypeLinesFromFile(it: File): Set<String> {
// Sample code
return setOf()
}
A benefit of using fold is that we don't need to change the type of the data from Map to MutableMap and Set to MutableSet.

RxJava Filter on Error

This question is loosely related to this question, but there were no answers. The answer from Bob Dalgleish is close, but doesn't support the potential error coming from a Single (which I think that OP actually wanted as well).
I'm basically looking for a way to "filter on error" - but don't think this exists when the lookup is RX based. I am trying to take a list of values, run them through a lookup, and skip any result that returns a lookup failure (throwable). I'm having trouble figuring out how to accomplish this in a reactive fashion.
I've tried various forms of error handling operators combined with mapping. Filter only works for raw values - or at least I couldn't figure out how to use it to support what I'd like to do.
In my use case, I iterate a list of IDs, requesting data for each from a remote service. If the service returns 404, then the item doesn't exist anymore. I should remove non-existing items from the local database and continue processing IDs. The stream should return the list of looked up values.
Here is a loose example. How do I write getStream() so that canFilterOnError passes?
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import org.junit.Test
class SkipExceptionTest {
private val data: Map<Int, String> = mapOf(
Pair(1, "one"),
Pair(2, "two"),
Pair(4, "four"),
Pair(5, "five")
)
#Test
fun canFilterOnError() {
getStream(listOf(1, 2, 3, 4, 5))
.subscribeOn(Schedulers.trampoline())
.observeOn(Schedulers.trampoline())
.test()
.assertComplete()
.assertNoErrors()
.assertValueCount(1)
.assertValue {
it == listOf(
"one", "two", "four", "five"
)
}
}
fun getStream(list: List<Int>): Single<List<String>> {
// for each item in the list
// get it's value via getValue()
// if a call to getValue() results in a NotFoundException, skip that value and continue
// mutate the results using mutate()
TODO("not implemented")
}
fun getValue(id: Int): Single<String> {
return Single.fromCallable {
val value: String? = data[id]
if (value != null) {
data[id]
} else {
throw NotFoundException("dat with id $id does not exist")
}
}
}
class NotFoundException(message: String) : Exception(message)
}
First .materialize(), then .filter() on non-error events, then .dematerialize():
getStream(/* ... */)
.materialize()
.filter(notification -> { return !notification.isOnError(); })
.dematerialize()
I ended up mapping getValue() to Optional<String>, then calling onErrorResumeNext() on that and either returning Single.error() or Single.just(Optional.empty()). From there, the main stream could filter out the empty Optional.
private fun getStream(list: List<Int>): Single<List<String>> {
return Observable.fromIterable(list)
.flatMapSingle {
getValue(it)
.map {
Optional.of(it)
}
.onErrorResumeNext {
when (it) {
is NotFoundException -> Single.just(Optional.empty())
else -> Single.error(it)
}
}
}
.filter { it.isPresent }
.map { it.get() }
.toList()
}

Using condition to select the sorting property in Kotlin

I am using sortedBy() to perform sorting on the collection of objects.
Since the order may change depending on the user choice, I've ended up with the following code
val sortedList = if (sortingOrder == WordSortingOrder.BY_ALPHA) {
list.sortedBy { it.word.value }
} else {
list.sortedBy { it.createdAt }
}
Then I perform further actions on the sorted collection.
I realize that sortedBy() method expects a property to be returned.
I wonder if there is a way to embed the sorting condition in one chain of collection methods.
If your properties are of different types you won't be able to select one of them based on some condition as a result for sortedBy, as their common supertype would be inferred as Any and it is not a subtype of Comparable<R> as sortedBy expects.
Instead you can utilize sortedWith method, which takes a Comparator, and provide a comparator depending on the condition:
list.sortedWith(
if (sortingOrder == WordSortingOrder.BY_ALPHA)
compareBy { it.word.value }
else
compareBy { it.createdAt }
)
Comparators for different properties are created here with the kotlin.comparisons.compareBy function.
You can then extract the logic which selects comparator based on sorting order to a function:
list.sortedWith(comparatorFor(sortingOrder))
fun comparatorFor(sortingOrder: WordSortingOrder): Comparator<MyType> = ...
The sortedBy expects any function of type (T) -> R as its parameter. A property is a corner case of that.
Which means you can do this:
val sortedList = list
.sortedBy { if (sortingOrder == WordSortingOrder.BY_ALPHA) it.word.value else it.createdAt}
Or, if you need something more OOP-ish:
enum class WordSortingOrder(val transform: (MyObject) -> Int) {
BY_ALPHA({it.word.value}),
BY_ALPHA_REVERSED({-1 * it.word.value}),
DEFAULT({it.createdAt})
}
val sortedList = list.sortedBy { sortingOrder.transform(it)}
You can do something like:
list.sortedBy { item ->
when(sortingOrder) {
WordSortingOrder.BY_ALPHA -> item.word.value
else -> item.createdAt
}
}
You can make the lambda argument passed to sortedBy conditional:
list.sortedBy(if (sortingOrder == WordSortingOrder.BY_ALPHA) {
{ it: MyType -> it.word.value }
} else {
{ it: MyType -> it.createdAt }
})
You may find using when instead of if more readable in this scenario:
list.sortedBy(when (sortingOrder) {
WordSortingOrder.BY_ALPHA -> { it: MyType -> it.word.value }
else -> { it: MyType -> it.createdAt }
})
If your selectors have different return types then you can simply wrap your existing code within list.let { list -> ... } or use run:
list.run {
if (sortingOrder == WordSortingOrder.BY_ALPHA) {
sortedBy { it.word.value }
} else {
sortedBy { it.createdAt }
}
}
You can then continue chainging calls after the let/run.