How to create a list from scanned BLE results - android

as invers to the question asked here How to convert Flow<List<Object>> to Flow<Object> I want to convert my Flow<Object> to Flow<List<Object>>.
At least I think I want that, so I try to explain what I want to achieve and give some background. I am working on an Android application that uses bluetooth to scan and connect to BLE devices. I'm fairly new to the Android platform and kotlin so I haven't quite grasped all the details despite all the many things I've already learnt.
My repository has a method which returns a Flow of ScanResults from the bluetooth adapter:
fun bluetoothScan(): Flow<ScanResult> {
return bluetoothStack.bluetoothScan()
}
My ViewModel consumes that function, maps the data to my BleScanResult and returns it as LiveData.
val scanResults: LiveData<BleScanResult> =
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.asLiveData()
In my activity I want to observer on that data and display it in a RecyclerView:
val adapter = ScanResultListAdapter()
binding.rcBleScanResults.adapter = adapter
viewModel.scanResults.observe(this) { result ->
//result.let { adapter.submitList(it) }
}
The problem is that scanResults is from type Flow<BleScanResult> and not Flow<List<BleScanResult>>, so the call to adapter.submitList(it) throws an error as it is expected to be a list.
So, how do I convert Flow to Flow<List> (with additional filtering of duplicates)? Or is there something I miss about the conception of Flow/LiveData?

You can try to use a MutableList and fill it with the data you get form a Flow, something like the following:
val results: MutableList<BleScanResult> = mutableListOf()
val scanResults: LiveData<List<BleScanResult>> =
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map {
results.apply {
add(BleScanResult(it.device.name, it.device.address))
}
}
} else {
emptyFlow()
}
}.asLiveData()
You can also use a MutableSet instead of MutableList if you want to have a unique list of items (assuming BleScanResult is a data class).

You could use the liveData builder to collect the Flow's values into a MutableList.
Here I copy the MutableList using toList() before emitting it since RecyclerView Adapters don't play well with mutable data sources.
val scanResults: LiveData<List<BleScanResult>> = liveData {
val cumulativeResults = mutableListOf<BleScanResult>()
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.collect {
cumulativeResults += it
emit(cumulativeResults.toList())
}
}
If you want to avoid duplicate entries and reordering of entries, you can use a set like this:
val scanResults: LiveData<List<BleScanResult>> = liveData {
val cumulativeResults = mutableSetOf<BleScanResult>()
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.collect {
if (it !in cumulativeResults) {
cumulativeResults += it
emit(cumulativeResults.toList())
}
}
}

Related

How to use RecyclerView or other ui elements that depend on multiple livedata and may change on interaction android kotlin

I have a rather difficult task for me here. I have a RecyclerView that depends on 3-4 livedata objects. For this I use MediatorLiveData:
class TripleMediatorLiveData<F, S, T>(
firstLiveData: LiveData<F>,
secondLiveData: LiveData<S>,
thirdLiveData: LiveData<T>
) : MediatorLiveData<Triple<F?, S?, T?>>() {
init {
addSource(firstLiveData) {
firstLiveDataValue: F -> value =
Triple(firstLiveDataValue, secondLiveData.value, thirdLiveData.value)
}
addSource(secondLiveData) {
secondLiveDataValue: S -> value =
Triple(firstLiveData.value, secondLiveDataValue, thirdLiveData.value)
}
addSource(thirdLiveData) {
thirdLiveDataValue: T -> value =
Triple(firstLiveData.value, secondLiveData.value, thirdLiveDataValue)
}
}
}
I get data from ROOM Database. As I said earlier, I have a RecyclerView, the logic of which looks like this:
viewModel.tripleMediatorLiveData.observe(viewLifecycleOwner) { triple ->
val settings = triple.first
val availability = triple.second
val chapters = triple.third
when(availability) {
0 -> {
createCourseNotAvailableItems()
binding.btnBegin.setOnClickListener {
viewModel.setTimersForChapters(chapters!!)
viewModel.updateCourseAvailability(1)
val courseEndTime = TimeManager().currentDatePlusMinutes(55)
viewModel.setCourseEndTime(courseEndTime)
}
}
1 -> {
createCourseAvailableItems(chapters!!)
binding.btnBegin.setOnClickListener {
viewModel.resetTimersForChapters(chapters)
viewModel.updateCourseAvailability(0)
viewModel.setCourseEndTime()
}
viewModel.courseCompleted.observe(viewLifecycleOwner) { endDate ->
val currentDate = TimeManager().getCurrentTimeInSeconds()
endDate?.let {
if (currentDate > it && it != 0L) {
CourseCompletedDialog(
onClick = {
viewModel.resetTimersForChapters(chapters)
viewModel.updateCourseAvailability(0)
viewModel.setCourseEndTime()
}
).show(childFragmentManager, CourseCompletedDialog.TAG)
}
}
}
}
}
}
As you can see, the RecyclerView handles a large number of LiveData objects ​​and is displayed differently depending on the state. The problem arises in the appearance of the dialog.
When OnClick triggered, the data is updated and the Observer is triggered, which is why the Observer does not work quite predictably, because of which the dialog may appear several times in a row, BUT IT SHOULD NOT APPEAR AT ALL AFTER THE ONCLICK TRIGGERED.
How can I improve the code so that the dialog does not appear multiple times in a row?

How Filter a Flow<List<Item> by propertis of Item Object and return a new Flow<List<Item>>

I am new to kotlin and reactive programming.
I have a repository that runs a query to a room database. I want to filter the list of Items according to some parameters before sending it to the viewmodel.
I don't know what I'm doing wrong, but I think it has to do with not understanding something about Flow. It gives me an error that I return a unit and not a list.
suspend fun getFilterList(flowList: Flow<List<Item>>): Flow<List<Item>>{
val filterList: MutableList<Item> = mutableListOf()
flowList.collectLatest { list ->
list.toList().forEach { item ->
if (item.owner1 != 100){
filterList.add(item)
}
}
}
return filterList.asFlow()
}
A function that returns a flow does not need to suspend. I assume this should meet your requirements:
#OptIn(ExperimentalCoroutinesApi::class)
fun getFilterList(flowList: Flow<List<Item>>): Flow<List<Item>>{
return flowList.mapLatest { list ->
list.filter { it.owner1 != 100 }
}
}

RxJava filtering with inside object

For start I must say I am begginer in RxJava.
Data class:
#Entity(tableName = "google_book")
data class GoogleBook (
#PrimaryKey(autoGenerate = true) val id: Int=0,
val items: ArrayList<VolumeInfo>)
data class VolumeInfo(val volumeInfo: BookInfo){
data class BookInfo(val title: String, val publisher: String, val description: String, val imageLinks: ImageLinks?)
data class ImageLinks(val smallThumbnail: String?)
}
Function which helps me save data to database:
fun searchBooks(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
bookRepository.getBooksFromApi(query)
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { x ->
x?.let { googleBook ->
searchJob?.cancel()
searchJob = viewModelScope.launch {
bookRepository.deleteGoogleBook()
bookRepository.insertGoogleBook(googleBook)
}
} ?: kotlin.run {
Log.d(TAG, "observeTasks: Error")
}
}
}
}
}
As seen I want to filter list within GoogleBook object by image parameter but It doesnt work. I cannot add filtering for data class ImageLinks so I have no Idea how can I make it right
I am asking mostly about this part:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
Thanks for reading
welcome to RxJava, you gonna love it.
As far as I can tell the issue with your filtering simply relies here:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null })
} // this returns you a new list filtered list here, but does not modify the original one
t // but you return the same data object here, it is not modified at all
}
// also consider naming it bookInfo if it is actually a bookInfo
What you should do is make a copy of your object with the filtered elements, something like this:
fun filterGoogleBookBySmallThumbNail(googleBook: GoogleBook): GoogleBook {
val filteredItems = googleBook.items.filter { it.volumeInfo.imageLinks?.smallThumbnail == null }
return googleBook.copy(items = ArrayList(filteredItems)) // now a new googleBook item is created with the filtered elements
}
// snippet to adjust then
bookRepository.getBooksFromApi(query)
.map { googleBook -> filterGoogleBookBySmallThumbNail(googleBook) }
//...
Some additional notes / suggestions I have:
I don't see you actually disposing of the subscription of the Observable.
bookRepository.getBooksFromApi(query) If this line returns an Observable, even if you cancel the job, you will be still observing that Observable. If it returns a Single then you are in luck, because after one element it is disposed.
To properly dispose, in cancellation you would have to do something like this(still i would recommend the other two rather, just wanted to note the not disposing):
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
val disposable = bookRepository.getBooksFromApi(query)
//...
.subscribe { x ->
//...
}
try {
awaitCancellation() // this actually suspends the coroutine until it is cancelled
} catch (cancellableException: CancellationException) {
disposable.dispose() // this disposes the observable subscription
// that way the coroutine stays alive as long as it's not cancelled, and at that point it actually cleans up the Rx Subscription
}
Seems wasteful that you start a new coroutine job just to do actions
If you want to go the Rx way, you could make the
bookRepository.deleteGoogleBook() and bookRepository.insertGoogleBook(googleBook) Completable, and setup the observable as:
bookRepository.getBooksFromApi(query)
//..
.flatMap {
bookRepository.deleteGoogleBook().andThen(bookRepository.insertGoogleBook(it)).andThen(Observable.just(it))
}
//..subscribeOn
.subscribe()
Seems weird you are mixing coroutine and RX this way
if you don't want to go full Rx, you may consider converting your Observable into a kotlin coroutine Flow, that would be easier to handle with coroutine cancellations and calling suspend functions.
I hope it's helpful

Android RxJava sort object by its parameter which is list of another object

I have an object
data class Question(
var publishDate: DateTime?,
var tags: List<QuestionTags>
etc.
Now it's sorted by publishDate
val sortedQuestions = questions.sortedBy { it.publishDate }
But I would like to sort it by one of its tags chapterId. QuestionTags looks like this
data class QuestionTags(
val id: Long,
val chapterId: Long?
etc.
I was trying to do some RxJava magic
sortedQuestions =
questions.flatMap { it.tags }
.filter { it.chapterId != null }
.map { sortedQuestions[0] }
.sortedBy { it.blablabla }
But it isn't working in any case.
How to .map it or .flatMap it to make it happen?
I was trying to flatMap it to QuestionTags, than map it to Question
sortedQuestions =
questions.flatMap { it.tags }
.filter { it.chapterId != null }
.map { Question() }
But in .map { Question() } it force me to pass values to all constructor parameters, which I don't want to do.
It should be sorted by first chapterId from list. And if both have same chapterId, they should be sorted by publishDate, like it's already done
Then you don't need flatMap (or RxJava) at all:
questions.sortedBy { Pair(it.tags.firstOrNull(), it.publishDate) }

Unable to update mutableList object model values Kotlin

all wanted to update the values in the mutable list, but loop jumping out here is my code snippet.
for ((i, dataObject) in data.withIndex()) {
channelsObject.getChannel(
dataObject.remoteChannelId,
ChatCallbackListener<Channel>() {
Log.e("last friend", it.friendlyName)
it!!.messages?.getLastMessages(1, ChatCallbackListener<List<Message>>() {
if (it.size > 0)
if (it.get(0).hasMedia()) {
dataObject.isAttachement = true
dataObject.lastMessage = "attachment"
} else {
dataObject.lastMessage = it.get(0).messageBody
dataObject.lastMessageDataAndTime = it.get(0).dateCreated
})
})
setThreadAdapter(data)
}
I am unable to update two values of the data class, looks like the loop jumping out before getting messages, any help highly appreciated.
Thanks
Looks like your adapter is getting called before the data retrieved by the callbacks.
there are multiple approached to do that.
But one is below:
fun jhaman() {
val data = mutableListOf<Any>()
// to set the adapter with empty message so you can update
setThreadAdapter(data)
val remoteChannelId = 0// todo change this
data.forEachIndexed { index, any ->
channelsObject.getChannel(
any.remoteChannelId,
ChatCallbackListener<Channel>() { channel ->
Log.e("last friend", any.friendlyName)
channel.messages?.getLastMessages(1, ChatCallbackListener<List<Message>>() {
//update your data what you wana to do in the data object
any.apply {
// update the data
}
adapter.updateThreadAdapter(index, any)
})
})
}
}
// in adapter
fun updateThreadAdapter(index: Int, any: Any) {
list.get(index) = any
notifyItemChanged(list)
}
Change any to your object.
Disclaimer: I did not run this code because I don't have that chat api in my project. thanks

Categories

Resources