73

I'm struggling to implement something I assumed would be fairly simple in Rx.

I have a list of items, and I want to have each item emitted with a delay.

It seems the Rx delay() operator just shifts the emission of all items by the specified delay, not each individual item.

Here's some testing code. It groups items in a list. Each group should then have a delay applied before being emitted.

Observable.range(1, 5)
    .groupBy(n -> n % 5)
    .flatMap(g -> g.toList())
    .delay(50, TimeUnit.MILLISECONDS)
    .doOnNext(item -> {
        System.out.println(System.currentTimeMillis() - timeNow);
        System.out.println(item);
        System.out.println(" ");
    }).toList().toBlocking().first();

The result is:

154ms
[5]

155ms
[2]

155ms
[1]

155ms
[3]

155ms
[4]

But what I would expect to see is something like this:

174ms
[5]

230ms
[2]

285ms
[1]

345ms
[3]

399ms
[4]

What am I doing wrong?

Magnus
  • 7,952
  • 2
  • 26
  • 52
athor
  • 6,848
  • 2
  • 34
  • 37
  • wonder why isn't any of the answers actually answering the question. Why isn't this working, what's wrong with it? – eis Mar 15 '18 at 06:46
  • 8
    _"I'm struggling to implement something I assumed would be fairly simple in Rx"_ seems to be the introduction to every Rx question. :) – Steven Jeuris Mar 05 '19 at 14:54

17 Answers17

75

The simplest way to do this seems to be just using concatMap and wrapping each item in a delayed Obserable.

long startTime = System.currentTimeMillis();
Observable.range(1, 5)
        .concatMap(i-> Observable.just(i).delay(50, TimeUnit.MILLISECONDS))
        .doOnNext(i-> System.out.println(
                "Item: " + i + ", Time: " + (System.currentTimeMillis() - startTime) +"ms"))
        .toCompletable().await();

Prints:

Item: 1, Time: 51ms
Item: 2, Time: 101ms
Item: 3, Time: 151ms
Item: 4, Time: 202ms
Item: 5, Time: 252ms
Magnus
  • 7,952
  • 2
  • 26
  • 52
66

One way to do it is to use zip to combine your observable with an Interval observable to delay the output.

Observable.zip(Observable.range(1, 5)
        .groupBy(n -> n % 5)
        .flatMap(g -> g.toList()),
    Observable.interval(50, TimeUnit.MILLISECONDS),
    (obs, timer) -> obs)
    .doOnNext(item -> {
      System.out.println(System.currentTimeMillis() - timeNow);
      System.out.println(item);
      System.out.println(" ");
    }).toList().toBlocking().first();
iagreen
  • 31,470
  • 8
  • 76
  • 90
  • 2
    Thanks! I think the delay operator is not intended to be used the way I wanted to. This solution works :) – athor Oct 26 '15 at 19:52
  • 15
    This won't work if you source observable emits items at a rate slower than 50 ms. The `Observable.interval()` will emit an item every 50 ms. the `zip()` operator will buffer these if there is no matching items from your grouped list. Then, when a group is emitted, zip will immediately combine that with an item from the interval observable and send it to your `doOnNext()` without delay. – kjones Oct 26 '15 at 21:21
  • @kjones Agreed -- this will not work for a slow producer, only to delay one where the items are available at the same time. But the original question was - "I have a list of items, and I want to have each item emitted with a delay." Which sounds like `from` with a delay between each emission. – iagreen Oct 27 '15 at 00:25
  • 1
    @iagreen - You're right. I just assumed the Observable.range(1,5) was a hypothetical source observable. Since pretty much everything I work on can produce items at any random time, I tend to project this requirement onto others as well. – kjones Oct 27 '15 at 15:26
  • Observable messageObservable = Observable .interval(1, TimeUnit.SECONDS) .map(i -> others.get(i.intValue())) .take(others.size()); Thats its more simple. – tonilopezmr Aug 05 '16 at 18:07
  • If you'd rather not wrap your initial observable, you can in-line `zipWith` as in Mina Samy 's answer: https://stackoverflow.com/a/41718758/1650674 – tir38 Sep 04 '17 at 21:47
  • There's nothing cool about RxJava when you need to resort to these kinds of obscure solutions to do something that should be ridiculously simple. – Johann Jun 11 '19 at 08:31
  • @AndroidDev I agree – iagreen Jun 12 '19 at 22:28
48

Just sharing a simple approach to emit each item in a collection with an interval:

Observable.just(1,2,3,4,5)
    .zipWith(Observable.interval(500, TimeUnit.MILLISECONDS), (item, interval) -> item)
    .subscribe(System.out::println);

Each item will be emitted every 500 milliseconds

wypieprz
  • 7,981
  • 4
  • 43
  • 46
Mina Wissa
  • 10,923
  • 13
  • 90
  • 158
22

For kotlin users, I wrote an extension function for the 'zip with interval' approach

import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit

fun <T> Observable<T>.delayEach(interval: Long, timeUnit: TimeUnit): Observable<T> =
    Observable.zip(
        this, 
        Observable.interval(interval, timeUnit), 
        BiFunction { item, _ -> item }
    )

It works the same way, but this makes it reusable. Example:

Observable.range(1, 5)
    .delayEach(1, TimeUnit.SECONDS)
MatPag
  • 41,742
  • 14
  • 105
  • 114
Tim
  • 41,901
  • 18
  • 127
  • 145
5

I think it's exactly what you need. Take look:

long startTime = System.currentTimeMillis();
Observable.intervalRange(1, 5, 0, 50, TimeUnit.MILLISECONDS)
                .timestamp(TimeUnit.MILLISECONDS)
                .subscribe(emitTime -> {
                    System.out.println(emitTime.time() - startTime);
                });
Vlad
  • 497
  • 7
  • 12
4

To introduce delay between each item emitted is useful:

List<String> letters = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));

Observable.fromIterable(letters)
                .concatMap(item -> Observable.interval(1, TimeUnit.SECONDS)
                        .take(1)
                        .map(second -> item))
                .subscribe(System.out::println);

More good options at https://github.com/ReactiveX/RxJava/issues/3505

yaircarreno
  • 4,002
  • 1
  • 34
  • 32
2

You can implement a custom rx operator such as MinRegularIntervalDelayOperator and then use this with the lift function

Observable.range(1, 5)
    .groupBy(n -> n % 5)
    .flatMap(g -> g.toList())
    .lift(new MinRegularIntervalDelayOperator<Integer>(50L))
    .doOnNext(item -> {
      System.out.println(System.currentTimeMillis() - timeNow);
      System.out.println(item);
      System.out.println(" ");
    }).toList().toBlocking().first();
0

To delay each group you can change your flatMap() to return an Observable that delays emitting the group.

Observable
        .range(1, 5)
        .groupBy(n -> n % 5)
        .flatMap(g ->
                Observable
                        .timer(50, TimeUnit.MILLISECONDS)
                        .flatMap(t -> g.toList())
        )
        .doOnNext(item -> {
            System.out.println(System.currentTimeMillis() - timeNow);
            System.out.println(item);
            System.out.println(" ");
        }).toList().toBlocking().first();
kjones
  • 5,783
  • 2
  • 27
  • 27
  • 1
    Unfortunately this doesn't work. The timestamp printed is the same for all items. – athor Oct 26 '15 at 19:52
  • That's because you source observable `range(1, 5)` emits all items almost simultaneously. If your source was emitting items at random times you would see each group delay by 50 ms. The answer you have selected as the right answer will not do what you want if say your source emits 1 item, then delays for say 500 ms, then emits 4 more items. In that case the last 4 items will all have pretty much the same timestamp. – kjones Oct 26 '15 at 21:12
  • yeah all executed at same time in doOnNext – Jemshit Aug 20 '16 at 09:51
0

A not so clean way is to make the delay change with the iteration using the .delay(Func1) operator.

Observable.range(1, 5)
            .delay(n -> n*50)
            .groupBy(n -> n % 5)
            .flatMap(g -> g.toList())
            .doOnNext(item -> {
                System.out.println(System.currentTimeMillis() - timeNow);
                System.out.println(item);
                System.out.println(" ");
            }).toList().toBlocking().first();
Tushar Nallan
  • 784
  • 6
  • 16
0

There is other way to do it using concatMap as concatMap returns observable of source items. so we can add delay on that observable.

here what i have tried.

Observable.range(1, 5)
          .groupBy(n -> n % 5)
          .concatMap(integerIntegerGroupedObservable ->
          integerIntegerGroupedObservable.delay(2000, TimeUnit.MILLISECONDS))
          .doOnNext(item -> {
                    System.out.println(System.currentTimeMillis() - timeNow);
                    System.out.println(item);
                    System.out.println(" ");
                }).toList().toBlocking().first(); 
Sanket Kachhela
  • 10,861
  • 8
  • 50
  • 75
0

You can use

   Observable.interval(1, TimeUnit.SECONDS)
            .map(new Function<Long, Integer>() {
                @Override
                public Integer apply(Long aLong) throws Exception {
                    return aLong.intValue() + 1;
                }
            })
            .startWith(0)
            .take(listInput.size())
            .subscribe(new Consumer<Integer>() {
                @Override
                public void accept(Integer index) throws Exception {
                    Log.d(TAG, "---index of your list --" + index);
                }
            });

This code above not duplicate value(index). "I'm sure"

Duy Phan
  • 783
  • 6
  • 5
0
Observable.just("A", "B", "C", "D", "E", "F")
    .flatMap { item -> Thread.sleep(2000)
        Observable.just( item ) }
    .subscribe { println( it ) }
Vairavan
  • 1,246
  • 1
  • 15
  • 19
0

A Swift extension for both approaches suggested in this post.

Concat

import RxSwift

extension Observable {
    public func delayEach(_ dueTime: RxSwift.RxTimeInterval, scheduler: RxSwift.SchedulerType) -> RxSwift.Observable<Element> {
        return self.concatMap { Observable.just($0).delay(dueTime, scheduler: scheduler) }
    }
}

Zip

import RxSwift

extension Observable {
    public func delayEach(_ period: RxSwift.RxTimeInterval, scheduler: RxSwift.SchedulerType) -> RxSwift.Observable<Element> {
        return Observable.zip(
            Observable<Int>.interval(period, scheduler: scheduler),
            self
        ) { $1 }
    }
}

Usage

Observable.range(start: 1, count: 5)
    .delayEach(.seconds(1), scheduler: MainScheduler.instance)

My personal preference goes out to the concat approach since it will also work as expected when the upstream emits items at a slower rate than the delay interval.

And yes the original post is RxJava specific, but Google also brings you here for RxSwift queries.

Houwert
  • 190
  • 12
0

Regarding eis' comment "wonder why isn't any of the answers actually answering the question. Why isn't this working, what's wrong with it?":

It's behaving differently than expected because delaying an item means its emission time is delayed relative to the time the item would otherwise be emitted - not relative to the previous item.

Imagine the OP's observable without any delay: All items are emitted in quick succession (in the same millisecond). With delay, each item is emitted later. But since the same delay is applied to each item, their relative emission times do not change. They are still emitted in one millisecond.

Think of a person entering the room at 14:00. Another person enters at 14:01. If you apply a delay of one hour to both, they enter at 15:00 and 15:01. There is still just one minute between them.

Dabbler
  • 9,733
  • 5
  • 41
  • 64
-1

I think you want this:

Observable.range(1, 5)
            .delay(50, TimeUnit.MILLISECONDS)
            .groupBy(n -> n % 5)
            .flatMap(g -> g.toList())
            .doOnNext(item -> {
                System.out.println(System.currentTimeMillis() - timeNow);
                System.out.println(item);
                System.out.println(" ");
            }).toList().toBlocking().first();

This way it will delay the numbers going into the group rather than delaying the reduced list by 5 seconds.

FriendlyMikhail
  • 2,857
  • 23
  • 39
-1

You can add a delay between emitted items by using flatMap, maxConcurrent and delay()

Here is an example - emit 0..4 with delay

@Test
fun testEmitWithDelays() {
    val DELAY = 500L
    val COUNT = 5

    val latch = CountDownLatch(1)
    val startMoment = System.currentTimeMillis()
    var endMoment : Long = 0

    Observable
        .range(0, COUNT)
        .flatMap( { Observable.just(it).delay(DELAY, TimeUnit.MILLISECONDS) }, 1) // maxConcurrent = 1
        .subscribe(
                { println("... value: $it, ${System.currentTimeMillis() - startMoment}") },
                {},
                {
                    endMoment = System.currentTimeMillis()
                    latch.countDown()
                })

    latch.await()

    assertTrue { endMoment - startMoment >= DELAY * COUNT }
}

... value: 0, 540
... value: 1, 1042
... value: 2, 1544
... value: 3, 2045
... value: 4, 2547
cVoronin
  • 1,341
  • 15
  • 21
-1

you should be able to achieve this by using Timer operator. I tried with delay but couldn't achieve the desired output. Note nested operations done in flatmap operator.

    Observable.range(1,5)
            .flatMap(x -> Observable.timer(50 * x, TimeUnit.MILLISECONDS)
                        .map(y -> x))
            // attach timestamp
            .timestamp()
            .subscribe(timedIntegers ->
                    Log.i(TAG, "Timed String: "
                            + timedIntegers.value()
                            + " "
                            + timedIntegers.time()));
Abhishek Bansal
  • 5,197
  • 4
  • 40
  • 69