How to switch flow based on condition? - kotlin

There is query and two filters. When set search filter and query is not empty, need to one flow. When set checked filter need to other flow.
According debug, onClickSearchFilter or onClickCheckedFilter calls with query or filter changed - return new flow. But in UI no changes, collector dont work second time.
How to switch flow based on condition?
When I debugging with breakpoints in flows, app crash every time A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x14 in tid 23174 (JDWP), pid 23167 . Rebuild, clear cache, reload device - dont' help.
repeatOnStarted(viewModel.itemsFlow) {
// it doesn't work when flow is switched
pagingAdapter.submitData(it)
}
val itemsFlow = queryFlow
.debounce(1000)
.combine(filterFlow) { query, filter ->
when (filter) {
R.id.search_result_chip -> onClickSearchFilter(query)
R.id.checked_chip -> onClickCheckedFilter()
else -> throw Exception("")
}
}.flatMapMerge { // in order to switch from Flow<Flow<*>> to Flow<*>
it
}
private fun onClickSearchFilter(query: String): Flow<PagingData<ItemEntity>> {
return if (query.length < 2)
emptyFlow()
else Pager(BasePagingSource.getConfig()) {
SearchPagingSource(query, client)
}.flow.cachedIn(viewModelScope)
}
private fun onClickCheckedFilter(): Flow<PagingData<ItemEntity>> {
return Pager(
config = BasePagingSource.getConfig(),
remoteMediator = RemoteMediator(checkedIds, cacheDatabase, client)
) {
cacheDatabase.itemDao.getPagingSource(type, checkedIds)
}.flow
}

Related

Flow<T>.distinctUntilChanged() is not working

I'm trying to have a coroutine run while a SharedFlow is active (subscriptionCount is greater than zero) and be cancelled when the count drops. But somehow even something as simple as distinctUntilChanged() is not working as it should and I'm baffled.
For this I'm making a "onActive" extension like this:
fun <T : Any> MutableSharedFlow<T>.onActive(
block: suspend CoroutineScope.() -> Unit
): Flow<T> {
val original = this
val isActiveFlow: Flow<Boolean> = subscriptionCount
.map {
println("Class: Count is $it")
it > 0
}
.distinctUntilChanged()
return isActiveFlow.flatMapLatest { isActive ->
println("Class: isActive is $isActive")
// here would be the code that calls `block`
// but just this exactly as is, already triggers the error
original // still emits the original flow,
// that is needed or else subscriptionCount never changes
}
}
This initially this seems to work, but running a test on it that adds several subscribers, will print "isActive is true" several times in a row. Why is distinctUntilChanged() not working? This repeated call messes-up with the rest of the logic in the redacted area.
The test is like this:
#Test
fun `onActive is called only once with multiple subscribers`() = runBlocking {
val flow = MutableSharedFlow<Int>(
replay = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST
).apply {
repeat(5) { tryEmit(it) }
}.onActive {
}
val jobs = mutableListOf<Job>()
repeat(3) { count ->
jobs.add(flow.onEach {
println("Test: Listener $count received $it")
}.launchIn(this))
}
delay(100)
jobs.forEach { it.cancel() }
jobs.forEach { it.join() }
}
running this the output is:
Class: Count is 0
Class: isActive is false
Class: Count is 1
Class: Count is 1
Class: isActive is true
Class: Count is 2
Class: Count is 2
Class: isActive is true
Class: Count is 3
Test: Listener 0 received 3
Test: Listener 0 received 4
Test: Listener 1 received 3
Test: Listener 1 received 4
Test: Listener 2 received 3
Test: Listener 2 received 4
Class: Count is 2
Class: isActive is true
Class: Count is 3
Class: Count is 3
Class: Count is 3
Test: Listener 0 received 3
Test: Listener 0 received 4
So the question, why is distinctUntilChanged() not working and how can I fix it?
It seems the behaviour you're seeing is actually correct as far as distinctUntilChanged is concerned:
the first registered subscriber collects the original 2 replayed elements with the starting isActive=false value
then isActive becomes true because of that first susbcription, so that first subscriber recollects the original flow due to flatMapLatest, and thus gets again the replayed elements
the other 2 subscribers arrive when the subscriptionCount is already non-0 so isActive stays true for them until they are cancelled
If the coroutine you launch "while there are subscribers" is meant to produce elements in the SharedFlow, I would rather define the flow like a channelFlow/callbackFlow initially, and then use shareIn with SharingStarted.WhileSubscribed to have this "run when there are susbcribers" behaviour.
If it's "just on the side", you probably want an external scope and just launch a coroutine separately to listen to sharedFlow.subscribersCount and start/stop the "sidecar" coroutine.

Efficient way to update progress of long task while processing a task with Mono&Flux

I tried to make a method for long task which sends events of progress.
The method implemented by using Flux and Mono.
I figured out two ways to do it.
But I can't decide which approach is more efficient and elegant, or the other one.
Way 1.
var progressEvents = Sinks.Many<Progress> = Sinks.many().multicast()
fun doLongTask(taskFile : Mono<File>) : Mono<Another> {
return taskFile.map{ file ->
progressEvents.tryEmit(Progress("1"))
doSomething(file)
}.map{ something ->
progressEvents.tryEmit(Progress("2"))
doAnother(something)
}
}
Way2.
var progressEvents = Sinks.Many<Progress> = Sinks.many().multicast()
fun doLongTask(taskFile : Mono<File>) : Mono<Another> {
var doSomeMap = taskFile.map{ file ->
doSomething(file)
}.share()
doSomeMap.map{ something -> Progress("1")}.subscribe{progressEvents::tryEmit}
var doAnotherMap = doSomeMap.map{ something ->
doAnother(something)
}.share()
doAnotherMap.map{ another -> Progress("2")}.subscribe{progressEvents::tryEmit}
return doAnotherMap
}

Spring Reactor not working on handle operation while DB fetch

I'm using spring reactor. The code below is :
public Mono<ResponseEntity<SignUpResponse>> createSignUpForUser(SignUpRequest signUpRequest) {
return Mono.just(signUpRequest)
.map(sign -> {
Mono<UserDetailsEntity> userDetailsEntityMono = userDetailsRepository.findByPhoneNumber(sign.getMobileNumber());
userDetailsEntityMono.handle((user, sink) -> {
if (user != null) {
sink.error(new RuntimeException("Phone number already registered"));
}
});
return functionUserDetails.apply(sign);
})
.flatMap(userDetailsRepository::save)
.map(functionUser)
.flatMap(userRepository::save)
.map(usr -> ResponseEntity.ok(functionSignUpRes.apply(usr)))
.defaultIfEmpty(ResponseEntity.notFound().build())
.log();
}
Here the findByPhoneNumber(sign.getMobileNumber()) DB call is not working (the error is not throwing). The Rest of the operations are working and returning the response. Am i doing anything wrongly ? help me to fix this issue.
I think you need to rewrite your code a bit
public Mono<ResponseEntity<SignUpResponse>> createSignUpForUser(SignUpRequest signUpRequest) {
return userDetailsRepository.findByPhoneNumber(sign.getMobileNumber())
.flatMap(__ -> Mono.error(new RuntimeException("Phone number already registered")))
.switchIfEmpty(userDetailsRepository.save(functionUserDetails.apply(sign)))
.map(functionUser)
.flatMap(userRepository::save)
.map(usr -> ResponseEntity.ok(functionSignUpRes.apply(usr)))
.defaultIfEmpty(ResponseEntity.notFound().build())
.log()
;
}
Idea is that if you find that number then it should error, if result is empty then you would need to insert it so we need to use switch if empty

Mono.zip with null

My code:
Mono.zip(
credentialService.getCredentials(connect.getACredentialsId()),
credentialService.getCredentials(connect.getBCredentialsId())
)
.flatMap(...
From the frontend we get connect object with 2 fields:
connect{
aCredentialsId : UUID //required
bCredentialsId : UUID //optional
}
So sometimes the second line credentialService.getCredentials(connect.getBCredentialsId())) can return Mono.empty
How to write code to be prepared for this empty Mono when my second field bCredentialsId is null?
What should I do? In case of empty values return Mono.just(new Object) and then check if obj.getValue != null??? I need to fetch data from DB for 2 different values
The strategy I prefer here is to declare an optional() utility method like so:
public class Utils {
public static <T> Mono<Optional<T>> optional(Mono<T> in) {
return in.map(Optional::of).switchIfEmpty(Mono.just(Optional.empty()));
}
}
...which then allows you to transform your second Mono to one that will always return an optional, and thus do something like:
Mono.zip(
credentialService.getCredentials(connect.getACredentialsId()),
credentialService.getCredentials(connect.getBCredentialsId()).transform(Utils::optional)
).map(e -> new Connect(e.getT1(), e.getT2()))
(...assuming you have a Connect object that takes an Optional as the second parameter of course.)
An easier way is using mono's defaultIfEmpty method.
Mono<String> m1 = credentialService.getCredentials(connect.getACredentialsId());
Mono<String> m2 = credentialService.getCredentials(connect.getBCredentialsId()).defaultIfEmpty("");
Mono.zip(m1, m2).map(t -> connectService.connect(t.getT1(), t.getT2()));
Explanation: if m2 is null then get empty string as a default value instead of null.
Instead of using .zip here, I would work with a nullable property of Connect and use .flatMap in combination with .switchIfEmpty for it.
Kotlin-Version:
val aCredentials = credentialService.getCredentials(connect.getACredentialsId())
credentialService.getCredentials(connect.getBCredentialsId())
.flatMap { bCredentials -> aCredentials
.map { Connect(it, bCredentials)}
.switchIfEmpty(Connect(null, bCredentials))
}
.switchIfEmpty { aCredentials.map { Connect(it, null) } }

Registering plugin on quick find in Dynamics CRM 2013

I have to register a plugin on Quick Find search on "Artilce" entity. When user enter any thing in quick find text box on Article entity at that time my plugin execute and return filter the data based on our business logic.
1.What event is fired when we find using quick find.
2.What message passes when this event is fired.
I have tried registering the plugin on RetrieveMultiple message but this is not triggered when we click on search in quick find.
Please help.
We have a Plugin registered on the RetrieveMultiple. We had a business requirement to search for the records, using WildCard by default.
Plugin Registration Details:
Message: RetrieveMultiple
Primary Entity:None
Secondary Entity:None
Pre-Operation
Code:
public const String QueryLiteral = "Query";
public const String LIKE = "%";
public void Execute(IServiceProvider serviceProvider)
{
String ParentEntity = String.Empty;
String OriginalSearch = String.Empty;
// Obtain the execution context from the service provider.
var ContextInstance = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Get a reference to the Organization service.
IOrganizationService ServiceInstance =
((IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory))).
CreateOrganizationService(ContextInstance.InitiatingUserId);
// Critical Point here - NOTICE that the InputParameters Contains the word Query
if (ContextInstance.Depth < 2 && ContextInstance.InputParameters.Contains(QueryLiteral) &&
ContextInstance.InputParameters[QueryLiteral] is QueryExpression)
{
QueryExpression QueryPointer = (ContextInstance.InputParameters[QueryLiteral] as QueryExpression);
//Verify the conversion worked as expected - if not, everything else is useless
if (null != QueryPointer)
{
// Check if the request is coming from any Search View
// We know this b/c Criteria isn't null and the Filters Count > 1
if (null != QueryPointer.Criteria && QueryPointer.Criteria.Filters.Count > 1)
{
ParentEntity = ContextInstance.PrimaryEntityName;
OriginalSearch = QueryPointer.Criteria.Filters[1].Conditions[0].Values[0].ToString();
OriginalSearch = String.Format(CultureInfo.CurrentCulture,
"{0}{1}", LIKE, OriginalSearch);
}
ConditionExpression NewCondition = null;
FilterExpression NewFilter = null;
if (null != QueryPointer.Criteria)
{
//Change the default 'BeginsWith'Operator to 'Contains/Like' operator in the basic search query
foreach (FilterExpression FilterSet in QueryPointer.Criteria.Filters)
{
foreach (ConditionExpression ConditionSet in FilterSet.Conditions)
{
if (ConditionSet.Operator == ConditionOperator.Like)
{
if (OriginalSearch != "")
ConditionSet.Values[0] = OriginalSearch;
else
{
OriginalSearch = QueryPointer.Criteria.Filters[0].Conditions[0].Values[0].ToString();
OriginalSearch = String.Format(CultureInfo.CurrentCulture,
"{0}{1}", LIKE, OriginalSearch);
ConditionSet.Values[0] = OriginalSearch;
}
}
}
}
}
}
ContextInstance.InputParameters[QueryLiteral] = QueryPointer;
}
}
Check details on this Post: http://www.williamgryan.mobi/?p=596
We have raised a ticket with Microsoft to address this situaation.
The solution they provided was to modify the Database to make the message
SearchByTitleKbArticleRequest available in the plugin registration tool.
I currently dont remember the table that we updated a flag against these messages.
After updating the table we were able to register the plugin against the message
SearchByTitleKbArticleRequest
Then the plugin triggered and we modified the entity collection returned from there.