I'm trying to make random password generator based on user input and everything is fine until i use .toCharArray().shuffle() function, but without shuffling it's too predictable beacuse it puts letters in pre-determined positions. Is there any way this code would work? Any workaround? I already tried stringbuilder but it bypasses user input so I don't know what to do now.
val chars= "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~##$%^&*()!"
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser)
{
when(seekBar)
{
sbNumberOfLetters ->
{
tvLetterCount.text = progress.toString()
smallLetters = progress
}
sbNumberOfCapitalLetters ->
{
tvCapitalsCount.text = progress.toString()
capitalLetterNumber = progress
}
sbNumberOfNumerals ->
{
tvNumeralsCount.text = progress.toString()
numeralsNumber = progress
}
sbNumberOfSpecialChars ->
{
tvSpecialCharsCount.text = progress.toString()
specialCharNumber = progress
}
}
}
}
private fun generatePassword() {
for (y in 1..numeralsNumber)
{
var randomLetter = Random.nextInt(0, 9)
listOfLetters.add(chars[randomLetter].toString())
}
for (w in 1..smallLetters)
{
var randomLetter = Random.nextInt(10, 36)
listOfLetters.add(chars[randomLetter].toString())
}
for (x in 1..capitalLetterNumber)
{
var randomLetter = Random.nextInt(36, 62)
listOfLetters.add(chars[randomLetter].toString())
}
for (z in 1..specialCharNumber)
{
var randomLetter = Random.nextInt(63, 73)
listOfLetters.add(chars[randomLetter].toString())
}
password = (listOfLetters.joinToString(separator = "",)).toCharArray().shuffle().toString()
tvGeneratedPassword.text = password
listOfLetters.clear()
}
shuffle returns Unit, so calling toString() on Unit will return kotlin.Unit which is defined in Unit. Try this:
listOfLetters.shuffle()
val password = listOfLetters.joinToString(separator = "")
tvGeneratedPassword.text = password
Might as well use the tools that Kotlin provides to write expressive and maintainable code ( are the indexes for Random.nextInt in the code you provided correct? ) . Here is one alternative (here lists and not arrays are used and shuffled() returns a new list ) :
abstract sealed class RandomChars {
open val chars: CharArray = charArrayOf()
fun get(count: Int) = (1..count).map { chars[Random.nextInt(0, chars.size)] }
}
object RandomDigits : RandomChars() {
override val chars = "0123456789".toCharArray()
}
object RandomLowerCase : RandomChars() {
override val chars = "abcdefghijklmnopqrstuvwxyz".toCharArray()
}
object RandomUpperCase : RandomChars() {
override val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
}
object RandomSpecial : RandomChars() {
override val chars = "~##$%^&*()!".toCharArray()
}
fun main() {
val password =
(RandomDigits.get(1) + RandomLowerCase.get(2) + RandomUpperCase.get(2) + RandomSpecial.get(1))
.shuffled()
.joinToString(separator = "")
println(password) // e.g. %Oa7Mt
}
Here is another, more functional approach:
val charGenerator =
{ alphabet: CharArray -> { count: Int -> (1..count).map { alphabet[Random.nextInt(0, alphabet.size)] } } }
val randomDigits = charGenerator("0123456789".toCharArray())
val randomLowecase = charGenerator("abcdefghijklmnopqrstuvwxyz".toCharArray())
val randomUppercase = charGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray())
val randomSpecial = charGenerator("~##$%^&*()!".toCharArray())
fun main() {
val password =
(randomDigits(1) + randomLowecase(2) + randomUppercase(2) + randomSpecial(1))
.shuffled()
.joinToString(separator = "")
println(password) // e.g. %Oa7Mt
}
Related
I have a TextField in which there cannot be more than 10 characters, and the user is required to enter date in the format "mm/dd/yyyy". Whenever user types first 2 characters I append "/", when the user types next 2 characters I append "/" again.
I did the following to achieve this:
var maxCharDate = 10
TextField(
value = query2,
onValueChange = {
if (it.text.length <= maxCharDate) {
if (it.text.length == 2 || it.text.length == 5)
query2 = TextFieldValue(it.text + "/", selection = TextRange(it.text.length+1))
else
query2 = it
}
emailErrorVisible.value = false
},
label = {
Text(
"Date of Birth (mm/dd/yyyy)",
color = colorResource(id = R.color.bright_green),
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = with(LocalDensity.current) { dimensionResource(id = R.dimen._12ssp).toSp() })
},
.
.
.
It's working except that the appended "/" doesn't get deleted on pressing backspace, while other characters do get deleted.
How do I make it such that "/" is deleted too on pressing backspace?
You can do something different using the onValueChange to define a max number of characters and using visualTransformation to display your favorite format without changing the value in TextField.
val maxChar = 8
TextField(
singleLine = true,
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
},
visualTransformation = DateTransformation()
)
where:
class DateTransformation() : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return dateFilter(text)
}
}
fun dateFilter(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 8) text.text.substring(0..7) else text.text
var out = ""
for (i in trimmed.indices) {
out += trimmed[i]
if (i % 2 == 1 && i < 4) out += "/"
}
val numberOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 1) return offset
if (offset <= 3) return offset +1
if (offset <= 8) return offset +2
return 10
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <=2) return offset
if (offset <=5) return offset -1
if (offset <=10) return offset -2
return 8
}
}
return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}
The / is being deleted but as soon as you delete, the length of the text becomes 2 or 5. So it checks the condition,
if (it.text.length == 2 || it.text.length == 5)
Since the condition is true now, the / appends again into the text. So it seems like it is not at all being deleted.
One way to solve this is by storing the previous text length and checking if the text length now is greater than the previous text length.
To achieve this, declare a variable below maxCharDate as
var previousTextLength = 0
And change the nested if condition to,
if ((it.text.length == 2 || it.text.length == 5) && it.text.length > previousTextLength)
And at last update the previousTextLength variable. Below the emailErrorVisible.value = false add
previousTextLength = it.text.length;
Implementation of VisualTranformation that accepts any type of mask for Jetpack Compose TextField:
class MaskVisualTransformation(private val mask: String) : VisualTransformation {
private val specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
override fun filter(text: AnnotatedString): TransformedText {
var out = ""
var maskIndex = 0
text.forEach { char ->
while (specialSymbolsIndices.contains(maskIndex)) {
out += mask[maskIndex]
maskIndex++
}
out += char
maskIndex++
}
return TransformedText(AnnotatedString(out), offsetTranslator())
}
private fun offsetTranslator() = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
val offsetValue = offset.absoluteValue
if (offsetValue == 0) return 0
var numberOfHashtags = 0
val masked = mask.takeWhile {
if (it == '#') numberOfHashtags++
numberOfHashtags < offsetValue
}
return masked.length + 1
}
override fun transformedToOriginal(offset: Int): Int {
return mask.take(offset.absoluteValue).count { it == '#' }
}
}
}
How to use it:
#Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.length <= DATE_LENGTH) {
date = it
}
},
visualTransformation = MaskVisualTransformation(DATE_MASK)
)
}
object DateDefaults {
const val DATE_MASK = "##/##/####"
const val DATE_LENGTH = 8 // Equals to "##/##/####".count { it == '#' }
}
I would suggest not only a date mask, but a simpler and generic solution for inputs masking.
A general formatter interface in order to implement any kind of mask.
interface MaskFormatter {
fun format(textToFormat: String): String
}
Implement our own formatters.
object DateFormatter : MaskFormatter {
override fun format(textToFormat: String): String {
TODO("Format '01212022' into '01/21/2022'")
}
}
object CreditCardFormatter : MaskFormatter {
override fun format(textToFormat: String): String {
TODO("Format '1234567890123456' into '1234 5678 9012 3456'")
}
}
And finally use this generic extension function for transforming your text field inputs and you won't need to care about the offsets at all.
internal fun MaskFormatter.toVisualTransformation(): VisualTransformation =
VisualTransformation {
val output = format(it.text)
TransformedText(
AnnotatedString(output),
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int = output.length
override fun transformedToOriginal(offset: Int): Int = it.text.length
}
)
}
Some example usages:
// Date Example
private const val MAX_DATE_LENGTH = 8
#Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.matches("^\\d{0,$MAX_DATE_LENGTH}\$".toRegex())) {
date = it
}
},
visualTransformation = DateFormatter.toVisualTransformation()
)
}
// Credit Card Example
private const val MAX_CREDIT_CARD_LENGTH = 16
#Composable
fun CreditCardTextField() {
var creditCard by remember { mutableStateOf("") }
TextField(
value = creditCard,
onValueChange = {
if (it.matches("^\\d{0,$MAX_CREDIT_CARD_LENGTH}\$".toRegex())) {
creditCard = it
}
},
visualTransformation = CreditCardFormatter.toVisualTransformation()
)
}
It is because you are checking for the length of the string. Whenever the length is two, you insert a slash. Hence the slash gets deleted, and re-inserted.
Why don't you just create three TextFields and insert Slashes as Texts in between. Such logic can be very hard to perfect. Keen users can use it to crash your app, and also devs can insert malicious stuff, and exploit this flaw because the handling logic can have loopholes as well, so... It is better in my opinion to just go with the simplest (and what I think is more elegant) way of constructing.
Here is the code:
private fun setData(playerData: TreeMap<String, String>) {
val values = ArrayList<Entry>()
val graphData = java.util.ArrayList<Float>()
for (value in playerData.values) {
graphData.add(value.toInt().toFloat()) # <-- converting here
}
graphData.reverse()
for (i in 0 until graphData.size) {
val itemi = i.toFloat()
values.add(
Entry(
itemi, #<-- entering here directly
graphData[i],
resources.getDrawable(R.drawable.ic_cricket)
)
)
}
val set1: LineDataSet
set1 = LineDataSet(values, "Player Form Graph")
val dataSets = ArrayList<ILineDataSet>()
// Possible Solution?
val valueFormatter = IValueFormatter { value, _, _, _ ->
value.toInt().toString()
}
set1.valueFormatter = valueFormatter as ValueFormatter?
dataSets.add(set1)
val data = LineData(dataSets)
// set data
binding.chart1.data = data
}
Error: java.lang.ClassCastException: <id>DemoFragment$setData$valueFormatter$1 cannot be cast to com.github.mikephil.charting.formatter.ValueFormatter
The thing is some values are working without .0 and some are with .0
this is what I have used:
set1.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return value.toInt().toString()
}
}
In case if there is a chance of value coming in decimal as well, say [1, 1.2, 3, 3.5] is your set, then go for:
set1.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return DecimalFormat("#.#").format(value)
}
}
in above case #.# can be replaced with #.## if you want to show UPTO 2 decimal places and so on.
Use:
DecimalFormat df = new DecimalFormat( "0" );
I am trying to return a list from inside firestore function based on if a condition is true.I want to return different lists when different categories are selected.
I tried:
putting the return statement out of firestore function which did not work and returned empty list due to firestore async behaviour.
creating my own callback to wait for Firestore to return the data using interface as I saw in some other questions but in that case how am i supposed to access it as my function has a Int value(i.e.private fun getRandomPeople(num: Int): List<String>)?
What could be the way of returning different lists for different categories based on firestore conditions?
My code(Non Activity class):
class Board// Create new game
(private val context: Context, private val board: GridLayout) {
fun newBoard(size: Int) {
val squares = size * size
val people = getRandomPeople(squares)
createBoard(context, board, size, people)
}
fun createBoard(context: Context, board: GridLayout, size: Int, people: List<String>) {
destroyBoard()
board.columnCount = size
board.rowCount = size
var iterator = 0
for(col in 1..size) {
for (row in 1..size) {
cell = RelativeLayout(context)
val cellSpec = { GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f) }
val params = GridLayout.LayoutParams(cellSpec(), cellSpec())
params.width = 0
cell.layoutParams = params
cell.setBackgroundResource(R.drawable.bordered_rectangle)
cell.gravity = Gravity.CENTER
cell.setPadding(5, 0, 5, 0)
text = TextView(context)
text.text = people[iterator++]
words.add(text.text as String)
text.maxLines = 5
text.setSingleLine(false)
text.gravity = Gravity.CENTER
text.setTextColor(0xFF000000.toInt())
cell.addView(text)
board.addView(cell)
cells.add(GameCell(cell, text, false, row, col) { })
}
}
}
private fun getRandomPeople(num: Int): List<String> {
val mFirestore: FirebaseFirestore=FirebaseFirestore.getInstance()
val mAuth: FirebaseAuth=FirebaseAuth.getInstance()
val currentUser: FirebaseUser=mAuth.currentUser!!
var validIndexes :MutableList<Int>
var chosenIndexes = mutableListOf<Int>()
var randomPeople = mutableListOf<String>()
mFirestore.collection("Names").document(gName).get().addOnSuccessListener(OnSuccessListener<DocumentSnapshot>(){ queryDocumentSnapshot->
var categorySelected:String=""
if (queryDocumentSnapshot.exists()) {
categorySelected= queryDocumentSnapshot.getString("selectedCategory")!!
print("categoryselected is:$categorySelected")
Toast.makeText(context, "Got sel category from gameroom:$categorySelected", Toast.LENGTH_LONG).show()
when(categorySelected){
"CardWords"->{
for (i in 1..num) {
validIndexes=(0..CardWords.squares.lastIndex).toMutableList()
val validIndexIndex = (0..validIndexes.lastIndex).random()
val peopleIndex = validIndexes[validIndexIndex]
chosenIndexes.add(peopleIndex)
val person = CardWords.squares[peopleIndex]
randomPeople.add(person)
validIndexes.remove(peopleIndex)
peopleIndexes = chosenIndexes.toList()
}
}
else->{}
}
}
else {
Toast.makeText(context, "Sel category does not exist", Toast.LENGTH_LONG).show()
}
}).addOnFailureListener(OnFailureListener { e->
val error=e.message
Toast.makeText(context,"Error:"+error, Toast.LENGTH_LONG).show()
})
return randomPeople.toList()
}
}
Activity A:
Board(this, gridLay!!)
I wanna sort some strings that contain numbers but after a sort, it becomes like this ["s1", "s10", "s11", ... ,"s2", "s21", "s22"]. after i search i fount this question with same problem. but in my example, I have mutableList<myModel>, and I must put all string in myModel.title for example into a mutable list and place into under code:
val sortData = reversedData.sortedBy {
//pattern.matcher(it.title).matches()
Collections.sort(it.title, object : Comparator<String> {
override fun compare(o1: String, o2: String): Int {
return extractInt(o1) - extractInt(o2)
}
fun extractInt(s: String): Int {
val num = s.replace("\\D".toRegex(), "")
// return 0 if no digits found
return if (num.isEmpty()) 0 else Integer.parseInt(num)
}
})
}
I have an error in .sortedBy and Collections.sort(it.title), may please help me to fix this.
you can use sortWith instead of sortBy
for example:
class Test(val title:String) {
override fun toString(): String {
return "$title"
}
}
val list = listOf<Test>(Test("s1"), Test("s101"),
Test("s131"), Test("s321"), Test("s23"), Test("s21"), Test("s22"))
val sortData = list.sortedWith( object : Comparator<Test> {
override fun compare(o1: Test, o2: Test): Int {
return extractInt(o1) - extractInt(o2)
}
fun extractInt(s: Test): Int {
val num = s.title.replace("\\D".toRegex(), "")
// return 0 if no digits found
return if (num.isEmpty()) 0 else Integer.parseInt(num)
}
})
will give output:
[s1, s21, s22, s23, s101, s131, s321]
A possible solution based on the data you posted:
sortedBy { "s(\\d+)".toRegex().matchEntire(it)?.groups?.get(1)?.value?.toInt() }
Of course I would move the regex out of the lambda, but it is a more concise answer this way.
A possible solution can be this:
reversedData.toObservable()
.sorted { o1, o2 ->
val pattern = Pattern.compile("\\d+")
val matcher = pattern.matcher(o1.title)
val matcher2 = pattern.matcher(o2.title)
if (matcher.find()) {
matcher2.find()
val o1Num = matcher.group(0).toInt()
val o2Num = matcher2.group(0).toInt()
return#sorted o1Num - o2Num
} else {
return#sorted o1.title?.compareTo(o2.title ?: "") ?: 0
}
}
.toList()
.subscribeBy(
onError = {
it
},
onSuccess = {
reversedData = it
}
)
As you state that you need a MutableList, but don't have one yet, you should use sortedBy or sortedWith (in case you want to work with a comparator) instead and you get just a (new) list out of your current one, e.g.:
val yourMutableSortedList = reversedData.sortedBy {
pattern.find(it)?.value?.toInt() ?: 0
}.toMutableList() // now calling toMutableList only because you said you require one... so why don't just sorting it into a new list and returning a mutable list afterwards?
You may want to take advantage of compareBy (or Javas Comparator.comparing) for sortedWith.
If you just want to sort an existing mutable list use sortWith (or Collections.sort):
reversedData.sortWith(compareBy {
pattern.find(it)?.value?.toInt() ?: 0
})
// or using Java imports:
Collections.sort(reversedData, Compatarator.comparingInt {
pattern.find(it)?.value?.toInt() ?: 0 // what would be the default for non-matching ones?
})
Of course you can also play around with other comparator helpers (e.g. mixing nulls last, or similar), e.g.:
reversedData.sortWith(nullsLast(compareBy {
pattern.find(it)?.value
}))
For the samples above I used the following Regex:
val pattern = """\d+""".toRegex()
I wrote a custom comparator for my JSON sorting. It can be adapted from bare String/Number/Null
fun getComparator(sortBy: String, desc: Boolean = false): Comparator<SearchResource.SearchResult> {
return Comparator { o1, o2 ->
val v1 = getCompValue(o1, sortBy)
val v2 = getCompValue(o2, sortBy)
(if (v1 is Float && v2 is Float) {
v1 - v2
} else if (v1 is String && v2 is String) {
v1.compareTo(v2).toFloat()
} else {
getCompDefault(v1) - getCompDefault(v2)
}).sign.toInt() * (if (desc) -1 else 1)
}
}
private fun getCompValue(o: SearchResource.SearchResult, sortBy: String): Any? {
val sorter = gson.fromJson<JsonObject>(gson.toJson(o))[sortBy]
try {
return sorter.asFloat
} catch (e: ClassCastException) {
try {
return sorter.asString
} catch (e: ClassCastException) {
return null
}
}
}
private fun getCompDefault(v: Any?): Float {
return if (v is Float) v else if (v is String) Float.POSITIVE_INFINITY else Float.NEGATIVE_INFINITY
}
I have a value in the UI that it's value depends on two LiveData objects. Imagine a shop where you need a subtotal = sum of all items price and a total = subtotal + shipment price. Using Transformations we can do the following for the subtotal LiveData object (as it only depends on itemsLiveData):
val itemsLiveData: LiveData<List<Items>> = ...
val subtotalLiveData = Transformations.map(itemsLiveData) {
items ->
getSubtotalPrice(items)
}
In the case of the total it would be great to be able to do something like this:
val shipPriceLiveData: LiveData<Int> = ...
val totalLiveData = Transformations.map(itemsLiveData, shipPriceLiveData) {
items, price ->
getSubtotalPrice(items) + price
}
But, unfortunately, that's not possible because we cannot put more than one argument in the map function. Anyone knows a good way of achieving this?
I come up with another solution.
class PairLiveData<A, B>(first: LiveData<A>, second: LiveData<B>) : MediatorLiveData<Pair<A?, B?>>() {
init {
addSource(first) { value = it to second.value }
addSource(second) { value = first.value to it }
}
}
class TripleLiveData<A, B, C>(first: LiveData<A>, second: LiveData<B>, third: LiveData<C>) : MediatorLiveData<Triple<A?, B?, C?>>() {
init {
addSource(first) { value = Triple(it, second.value, third.value) }
addSource(second) { value = Triple(first.value, it, third.value) }
addSource(third) { value = Triple(first.value, second.value, it) }
}
}
fun <A, B> LiveData<A>.combine(other: LiveData<B>): PairLiveData<A, B> {
return PairLiveData(this, other)
}
fun <A, B, C> LiveData<A>.combine(second: LiveData<B>, third: LiveData<C>): TripleLiveData<A, B, C> {
return TripleLiveData(this, second, third)
}
Then, you can combine multiple source.
val totalLiveData = Transformations.map(itemsLiveData.combine(shipPriceLiveData)) {
// Do your stuff
}
If you want to have 4 or more sources, you need to create you own data class because Kotlin only has Pair and Triple.
In my opinion, there is no reason to run with uiThread in Damia's solution.
You can use switchMap() for such case, because it returns LiveData object which can be Transformations.map()
In below code I am getting sum of final amount of two objects onwardSelectQuote and returnSelectQuote
finalAmount = Transformations.switchMap(onwardSelectQuote) { data1 ->
Transformations.map(returnSelectQuote) { data2 -> ViewUtils.formatRupee((data1.finalAmount!!.toFloat() + data2.finalAmount!!.toFloat()).toString())
}
}
UPDATE
Based on my previous answer, I created a generic way where we can add as many live datas as we want.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
/**
* CombinedLiveData is a helper class to combine results from multiple LiveData sources.
* #param liveDatas Variable number of LiveData arguments.
* #param combine Function reference that will be used to combine all LiveData data.
* #param R The type of data returned after combining all LiveData data.
* Usage:
* CombinedLiveData<SomeType>(
* getLiveData1(),
* getLiveData2(),
* ... ,
* getLiveDataN()
* ) { datas: List<Any?> ->
* // Use datas[0], datas[1], ..., datas[N] to return a SomeType value
* }
*/
class CombinedLiveData<R>(vararg liveDatas: LiveData<*>,
private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() {
private val datas: MutableList<Any?> = MutableList(liveDatas.size) { null }
init {
for(i in liveDatas.indices){
super.addSource(liveDatas[i]) {
datas[i] = it
value = combine(datas)
}
}
}
}
OLD
At the end I used MediatorLiveData to achieve the same objective.
fun mapBasketTotal(source1: LiveData<List<Item>>, source2: LiveData<ShipPrice>): LiveData<String> {
val result = MediatorLiveData<String>()
uiThread {
var subtotal: Int = 0
var shipPrice: Int = 0
fun sumAndFormat(){ result.value = format(subtotal + shipPrice)}
result.addSource(source1, { items ->
if (items != null) {
subtotal = getSubtotalPrice(items)
sumAndFormat()
}
})
result.addSource(source2, { price ->
if (price != null) {
shipPrice = price
sumAndFormat()
}
})
}
return result
}
I use following classes to transform many live data with different types
class MultiMapLiveData<T>(
private val liveDataSources: Array<LiveData<*>>,
private val waitFirstValues: Boolean = true,
private val transform: (signalledLiveData: LiveData<*>) -> T
): LiveData<T>() {
private val mObservers = ArrayList<Observer<Any>>()
private var mInitializedSources = mutableSetOf<LiveData<*>>()
override fun onActive() {
super.onActive()
if (mObservers.isNotEmpty()) throw InternalError(REACTIVATION_ERROR_MESSAGE)
if (mInitializedSources.isNotEmpty()) throw InternalError(REACTIVATION_ERROR_MESSAGE)
for (t in liveDataSources.indices) {
val liveDataSource = liveDataSources[t]
val observer = Observer<Any> {
if (waitFirstValues) {
if (mInitializedSources.size < liveDataSources.size) {
mInitializedSources.add(liveDataSource)
}
if (mInitializedSources.size == liveDataSources.size) {
value = transform(liveDataSource)
}
} else {
value = transform(liveDataSource)
}
}
liveDataSource.observeForever(observer)
mObservers.add(observer)
}
}
override fun onInactive() {
super.onInactive()
for (t in liveDataSources.indices) {
val liveDataSource = liveDataSources[t]
val observer = mObservers[t]
liveDataSource.removeObserver(observer)
}
mObservers.clear()
mInitializedSources.clear()
}
companion object {
private const val REACTIVATION_ERROR_MESSAGE = "Reactivation of active LiveData"
}
}
class MyTransformations {
companion object {
fun <T> multiMap(
liveDataSources: Array<LiveData<*>>,
waitFirstValues: Boolean = true,
transform: (signalledLiveData: LiveData<*>) -> T
): LiveData<T> {
return MultiMapLiveData(liveDataSources, waitFirstValues, transform)
}
fun <T> multiSwitch(
liveDataSources: Array<LiveData<*>>,
waitFirstValues: Boolean = true,
transform: (signalledLiveData: LiveData<*>) -> LiveData<T>
): LiveData<T> {
return Transformations.switchMap(
multiMap(liveDataSources, waitFirstValues) {
transform(it)
}) {
it
}
}
}
}
Usage:
Note that the logic of the work is slightly different. The LiveData that caused the update (signalledLiveData) is passed to the Tranformation Listener as parameter, NOT the values of all LiveData. You get the current LiveData values yourself in the usual way via value property.
examples:
class SequenceLiveData(
scope: CoroutineScope,
start: Int,
step: Int,
times: Int
): LiveData<Int>(start) {
private var current = start
init {
scope.launch {
repeat (times) {
value = current
current += step
delay(1000)
}
}
}
}
suspend fun testMultiMap(lifecycleOwner: LifecycleOwner, scope: CoroutineScope) {
val liveS = MutableLiveData<String>("aaa")
val liveI = MutableLiveData<Int>()
val liveB = MutableLiveData<Boolean>()
val multiLiveWait: LiveData<String> = MyTransformations.multiMap(arrayOf(liveS, liveI, liveB)) {
when (it) {
liveS -> log("liveS changed")
liveI -> log("liveI changed")
liveB -> log("liveB changed")
}
"multiLiveWait: S = ${liveS.value}, I = ${liveI.value}, B = ${liveB.value}"
}
val multiLiveNoWait: LiveData<String> = MyTransformations.multiMap(arrayOf(liveS, liveI, liveB), false) {
when (it) {
liveS -> log("liveS changed")
liveI -> log("liveI changed")
liveB -> log("liveB changed")
}
"multiLiveNoWait: S = ${liveS.value}, I = ${liveI.value}, B = ${liveB.value}"
}
multiLiveWait.observe(lifecycleOwner) {
log(it)
}
multiLiveNoWait.observe(lifecycleOwner) {
log(it)
}
scope.launch {
delay(1000)
liveS.value = "bbb"
delay(1000)
liveI.value = 2222
delay(1000)
liveB.value = true // ***
delay(1000)
liveI.value = 3333
// multiLiveWait generates:
//
// <-- waits until all sources get first values (***)
//
// liveB changed: S = bbb, I = 2222, B = true
// liveI changed: S = bbb, I = 3333, B = true
// multiLiveNoWait generates:
// liveS changed: S = aaa, I = null, B = null
// liveS changed: S = bbb, I = null, B = null
// liveI changed: S = bbb, I = 2222, B = null
// liveB changed: S = bbb, I = 2222, B = true <-- ***
// liveI changed: S = bbb, I = 3333, B = true
}
}
suspend fun testMultiMapSwitch(lifecycleOwner: LifecycleOwner, scope: CoroutineScope) {
scope.launch {
val start1 = MutableLiveData(0)
val step1 = MutableLiveData(1)
val multiLiveData = MyTransformations.multiSwitch(arrayOf(start1, step1)) {
SequenceLiveData(scope, start1.value!!, step1.value!!, 5)
}
multiLiveData.observe(lifecycleOwner) {
log("$it")
}
delay(7000)
start1.value = 100
step1.value = 2
delay(7000)
start1.value = 200
step1.value = 3
delay(7000)
// generates:
// 0
// 1
// 2
// 3
// 4
// 100 <-- start.value = 100
// 100 <-- step.value = 2
// 102
// 104
// 106
// 108
// 200 <-- start.value = 200
// 200 <-- step.value = 3
// 203
// 206
// 209
// 212
}
}