blob: df6dfea53b7ed3d58b19f4df8efb64f553a5967f [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.baku.toolkit.bind;
import android.view.View;
import android.widget.TextView;
import org.joda.time.Duration;
import java.util.concurrent.TimeUnit;
import io.v.baku.toolkit.BakuActivityTrait;
import io.v.rx.syncbase.SingleWatchEvent;
import lombok.RequiredArgsConstructor;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.ReplaySubject;
/**
* This coordinator defers the read binding until a specified delay after the latest write, then
* taking the latest read. Write/watch latency can cause reflexive watch changes from Syncbase to
* arrive after subsequent changes to the UI state have already been made, causing a stuttering
* revert.
*
* The default delay is 500 ms.
*
* A simple debounce on the read link or write link doesn't solve the problem because it effectively
* just adds a delay to the boundary condition. To prevent this, any update from the model must be
* throttled if there was a recent update from the view.
*
* Unfortunately for rapid concurrent updates this can result in divergence which should be handled
* via conflict resolution or CRDT.
*
* This coordinator is included in the default coordinator chain for
* {@linkplain ScalarBindingBuilder#bindTo(TextView) two-way
* <code>TextView</code> bindings}.
*
* ## Usage
*The following example shows how you would use this coordinator explicitly while creating a custom
* binding within a {@link io.v.baku.toolkit.BakuActivityTrait}:
*
* ```java
* {@link BakuActivityTrait#dataBinder() dataBinder()}.{@link BindingBuilder#forKey(java.lang.String)
* forKey}("foo")
* .{@link ScalarBindingBuilder#coordinators(CoordinatorChain[])
* coordinators}({@link
* DeferReadOnWriteCoordinator#DeferReadOnWriteCoordinator(TwoWayBinding)
* DeferReadOnWriteCoordinator::new})
* .{@link ScalarBindingBuilder#bindTo(View) bindTo}(myView);
* ```
*/
@RequiredArgsConstructor
public class DeferReadOnWriteCoordinator<T> implements TwoWayBinding<T> {
public static final Duration DEFAULT_IO_DEBOUNCE = Duration.millis(500);
private final TwoWayBinding<T> mChild;
private final Duration mIoDebounce;
private final ReplaySubject<Observable<?>> mRxDebounce = ReplaySubject.createWithSize(1);
{
mRxDebounce.onNext(Observable.just(0)
.observeOn(AndroidSchedulers.mainThread()));
//We expect these timeouts to be on the main thread; see putDebounceWindow
}
/**
* A reference to this constructor can be used as a {@link CoordinatorChain}.
*/
public DeferReadOnWriteCoordinator(final TwoWayBinding<T> child) {
this(child, DEFAULT_IO_DEBOUNCE);
}
private Observable<?> getDebounceWindow() {
return Observable.switchOnNext(mRxDebounce).first();
}
private void putDebounceWindow() {
mRxDebounce.onNext(Observable.timer(mIoDebounce.getMillis(), TimeUnit.MILLISECONDS,
AndroidSchedulers.mainThread()));
//Do timeouts on the main thread to ensure that timeouts don't clear while an input update
//is in progress.
}
@Override
public Observable<SingleWatchEvent<T>> linkRead() {
return mChild.linkRead().debounce(s -> getDebounceWindow());
}
@Override
public Subscription linkWrite(final Observable<T> rxData) {
return mChild.linkWrite(rxData.doOnNext(d -> putDebounceWindow()));
}
}