general improvements added according to latest library usage statistics

This commit is contained in:
Gavriil Sitnikov 2017-04-16 22:56:55 +03:00
parent 08fb147e54
commit 19f77a1bdd
27 changed files with 1065 additions and 1294 deletions

View File

@ -11,7 +11,7 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 16
targetSdkVersion 25 targetSdkVersion 25
} }
} }
@ -19,4 +19,5 @@ android {
dependencies { dependencies {
provided 'com.android.support:support-annotations:25.3.1' provided 'com.android.support:support-annotations:25.3.1'
provided 'io.reactivex:rxandroid:1.2.1' provided 'io.reactivex:rxandroid:1.2.1'
provided 'io.reactivex:rxjava:1.2.9'
} }

View File

@ -0,0 +1,112 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Observable;
import rx.subjects.BehaviorSubject;
/**
* Created by Gavriil Sitnikov on 24/03/2016.
* Wrapper over {@link BehaviorSubject} which could be serialized.
* Such object is useful as view model and also as value in Android that could be passed into {@link android.os.Bundle}.
*
* @param <TValue> Type of Changeable value;
* @param <TReturnValue> Type of actual value operating by Changeable. Could be same as {@link TValue}.
*/
public abstract class BaseChangeable<TValue, TReturnValue> implements Serializable {
private static final long serialVersionUID = 1L;
private transient BehaviorSubject<Optional<TValue>> valueSubject;
public BaseChangeable(@Nullable final TValue defaultValue) {
valueSubject = BehaviorSubject.create(new Optional<>(defaultValue));
}
@NonNull
protected Observable<Optional<TValue>> observeOptionalValue() {
return valueSubject.distinctUntilChanged();
}
/**
* Sets current value.
*
* @param value Value to set.
*/
public void set(@Nullable final TValue value) {
valueSubject.onNext(new Optional<>(value));
}
/**
* Returns current value.
*
* @return Current value.
*/
@Nullable
public TValue get() {
return valueSubject.getValue().getValue();
}
/**
* Returns {@link Observable} which is emits current value and then emitting changes of current value.
*
* @return Current value {@link Observable}.
*/
@NonNull
public abstract Observable<TReturnValue> observe();
private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException {
outputStream.writeObject(valueSubject.getValue());
}
@SuppressWarnings("unchecked")
private void readObject(@NonNull final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
valueSubject = BehaviorSubject.create((Optional<TValue>) inputStream.readObject());
}
@Override
public boolean equals(@Nullable final Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
final BaseChangeable<?, ?> that = (BaseChangeable<?, ?>) object;
return ObjectUtils.equals(valueSubject.getValue(), that.valueSubject.getValue());
}
@Override
public int hashCode() {
return valueSubject.getValue() != null ? valueSubject.getValue().hashCode() : 0;
}
}

View File

@ -22,47 +22,19 @@ package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.io.IOException; import ru.touchin.roboswag.core.utils.Optional;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import rx.Observable; import rx.Observable;
import rx.subjects.BehaviorSubject;
/** /**
* Created by Gavriil Sitnikov on 24/03/2016. * Created by Gavriil Sitnikov on 24/03/2016.
* Wrapper over {@link BehaviorSubject} which could be serialized. * Variant of {@link BaseChangeable} which is allows to set nullable values.
* Such object is useful as view model and also as value in Android that could be passed into {@link android.os.Bundle}. * Needed to separate non-null Changeable from nullable Changeable.
*/ */
public class Changeable<T> implements Serializable { //COMPATIBILITY NOTE: in RxJava2 it should extends BaseChangeable<T, Optional<T>>
public class Changeable<T> extends BaseChangeable<T, T> {
private static final long serialVersionUID = 1L;
private transient BehaviorSubject<T> subject;
public Changeable(@Nullable final T defaultValue) { public Changeable(@Nullable final T defaultValue) {
subject = BehaviorSubject.create(defaultValue); super(defaultValue);
}
/**
* Sets current value.
*
* @param value Value to set.
*/
public void set(@Nullable final T value) {
subject.onNext(value);
}
/**
* Returns current value.
*
* @return Current value.
*/
@Nullable
public T get() {
return subject.getValue();
} }
/** /**
@ -71,35 +43,10 @@ public class Changeable<T> implements Serializable {
* @return Current value {@link Observable}. * @return Current value {@link Observable}.
*/ */
@NonNull @NonNull
@Override
//COMPATIBILITY NOTE: in RxJava2 it should be Observable<Optional<T>>
public Observable<T> observe() { public Observable<T> observe() {
return subject.distinctUntilChanged(); return observeOptionalValue().map(Optional::getValue);
}
private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException {
outputStream.writeObject(subject.getValue());
}
@SuppressWarnings("unchecked")
private void readObject(@NonNull final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
subject = BehaviorSubject.create((T) inputStream.readObject());
}
@Override
public boolean equals(@Nullable final Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
final Changeable<?> that = (Changeable<?>) object;
return ObjectUtils.equals(subject.getValue(), that.subject.getValue());
}
@Override
public int hashCode() {
return subject.getValue() != null ? subject.getValue().hashCode() : 0;
} }
} }

View File

@ -21,18 +21,22 @@ package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException; import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
/** /**
* Created by Gavriil Sitnikov on 24/03/2016. * Created by Gavriil Sitnikov on 24/03/2016.
* Variant of {@link Changeable} which is allows to set only non-null values. * Variant of {@link BaseChangeable} which is allows to set only non-null values.
* Needed to separate non-null Changeable from nullable Changeable.
*/ */
public class NonNullChangeable<T> extends Changeable<T> { public class NonNullChangeable<T> extends BaseChangeable<T, T> {
private static final long serialVersionUID = 1L;
public NonNullChangeable(@NonNull final T defaultValue) { public NonNullChangeable(@NonNull final T defaultValue) {
super(defaultValue); super(defaultValue);
if (defaultValue == null) {
throw new ShouldNotHappenException();
}
} }
@NonNull @NonNull
@ -45,11 +49,30 @@ public class NonNullChangeable<T> extends Changeable<T> {
return value; return value;
} }
@SuppressWarnings("PMD.UselessOverridingMethod")
// UselessOverridingMethod: we need only annotation change
@Override @Override
public void set(@NonNull final T value) { public void set(@NonNull final T value) {
if (value == null) {
Lc.assertion("value is null");
return;
}
super.set(value); super.set(value);
} }
/**
* Returns {@link Observable} which is emits current value and then emitting changes of current value.
*
* @return Current value {@link Observable}.
*/
@NonNull
@Override
public Observable<T> observe() {
return observeOptionalValue()
.map(optional -> {
if (optional.getValue() == null) {
throw new ShouldNotHappenException();
}
return optional.getValue();
});
}
} }

View File

@ -1,59 +0,0 @@
package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Created by Gavriil Sitnikov on 21/05/2016.
* Object represents Observable's execution result. Contains all items and errors emitted by Observable during subscription.
*/
public class ObservableResult<T> {
@NonNull
private final List<T> items = new LinkedList<>();
@Nullable
private Throwable error;
/**
* Passes item to collect.
*
* @param item Emitted item.
*/
public void onNext(@Nullable final T item) {
items.add(item);
}
/**
* Passes error to collect.
*
* @param error Emitted error.
*/
public void onError(@NonNull final Throwable error) {
this.error = error;
}
/**
* Returns list of collected items.
*
* @return Items.
*/
@NonNull
public List<T> getItems() {
return new ArrayList<>(items);
}
/**
* Returns collected error.
*
* @return Error.
*/
@Nullable
public Throwable getError() {
return error;
}
}

View File

@ -28,7 +28,6 @@ import rx.Observable.OnSubscribe;
import rx.Scheduler; import rx.Scheduler;
import rx.Subscriber; import rx.Subscriber;
import rx.Subscription; import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1; import rx.functions.Action1;
import rx.observables.ConnectableObservable; import rx.observables.ConnectableObservable;
import rx.schedulers.Schedulers; import rx.schedulers.Schedulers;
@ -114,19 +113,15 @@ public final class OnSubscribeRefCountWithCacheTime<T> implements OnSubscribe<T>
@NonNull @NonNull
private Action1<Subscription> onSubscribe(@NonNull final Subscriber<? super T> subscriber, private Action1<Subscription> onSubscribe(@NonNull final Subscriber<? super T> subscriber,
@NonNull final AtomicBoolean writeLocked) { @NonNull final AtomicBoolean writeLocked) {
return new Action1<Subscription>() { return subscription -> {
@Override try {
public void call(@NonNull final Subscription subscription) { baseSubscription.add(subscription);
// ready to subscribe to source so do it
try { doSubscribe(subscriber, baseSubscription);
baseSubscription.add(subscription); } finally {
// ready to subscribe to source so do it // release the write lock
doSubscribe(subscriber, baseSubscription); lock.unlock();
} finally { writeLocked.set(false);
// release the write lock
lock.unlock();
writeLocked.set(false);
}
} }
}; };
} }
@ -152,14 +147,14 @@ public final class OnSubscribeRefCountWithCacheTime<T> implements OnSubscribe<T>
} }
private void cleanup() { private void cleanup() {
// on error or completion we need to unsubscribe the base subscription // on error or completion we need to unsubscribe the base subscription and set the subscriptionCount to 0
// and set the subscriptionCount to 0
lock.lock(); lock.lock();
try { try {
if (baseSubscription == currentBase) { if (baseSubscription == currentBase) {
if (worker != null) { cleanupWorker();
worker.unsubscribe(); // backdoor into the ConnectableObservable to cleanup and reset its state
worker = null; if (source instanceof Subscription) {
((Subscription) source).unsubscribe();
} }
baseSubscription.unsubscribe(); baseSubscription.unsubscribe();
baseSubscription = new CompositeSubscription(); baseSubscription = new CompositeSubscription();
@ -174,41 +169,45 @@ public final class OnSubscribeRefCountWithCacheTime<T> implements OnSubscribe<T>
@NonNull @NonNull
private Subscription disconnect(@NonNull final CompositeSubscription current) { private Subscription disconnect(@NonNull final CompositeSubscription current) {
return Subscriptions.create(new Action0() { return Subscriptions.create(() -> {
lock.lock();
@Override try {
public void call() { if (baseSubscription == current && subscriptionCount.decrementAndGet() == 0) {
lock.lock(); if (worker != null) {
try { worker.unsubscribe();
if (baseSubscription == current && subscriptionCount.decrementAndGet() == 0) { } else {
if (worker != null) { worker = scheduler.createWorker();
worker.unsubscribe();
} else {
worker = scheduler.createWorker();
}
worker.schedule(new Action0() {
@Override
public void call() {
lock.lock();
try {
if (subscriptionCount.get() == 0) {
baseSubscription.unsubscribe();
// need a new baseSubscription because once
// unsubscribed stays that way
worker.unsubscribe();
worker = null;
baseSubscription = new CompositeSubscription();
}
} finally {
lock.unlock();
}
}
}, cacheTime, cacheTimeUnit);
} }
} finally { worker.schedule(() -> {
lock.unlock(); lock.lock();
try {
if (subscriptionCount.get() == 0) {
cleanupWorker();
// backdoor into the ConnectableObservable to cleanup and reset its state
if (source instanceof Subscription) {
((Subscription) source).unsubscribe();
}
baseSubscription.unsubscribe();
// need a new baseSubscription because once
// unsubscribed stays that way
baseSubscription = new CompositeSubscription();
}
} finally {
lock.unlock();
}
}, cacheTime, cacheTimeUnit);
} }
} finally {
lock.unlock();
} }
}); });
} }
private void cleanupWorker() {
if (worker != null) {
worker.unsubscribe();
worker = null;
}
}
} }

View File

@ -20,9 +20,11 @@
package ru.touchin.roboswag.core.observables; package ru.touchin.roboswag.core.observables;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
@ -33,9 +35,9 @@ import java.util.concurrent.CountDownLatch;
import ru.touchin.roboswag.core.log.Lc; import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.utils.ServiceBinder; import ru.touchin.roboswag.core.utils.ServiceBinder;
import rx.Emitter;
import rx.Observable; import rx.Observable;
import rx.Scheduler; import rx.Scheduler;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
/** /**
@ -55,23 +57,50 @@ public final class RxAndroidUtils {
@NonNull @NonNull
public static <T extends Service> Observable<T> observeService(@NonNull final Context context, @NonNull final Class<T> serviceClass) { public static <T extends Service> Observable<T> observeService(@NonNull final Context context, @NonNull final Class<T> serviceClass) {
return Observable return Observable
.just(new SubscribeServiceConnection<T>()) .just(new OnSubscribeServiceConnection<T>())
.switchMap(serviceConnection -> Observable .switchMap(onSubscribeServiceConnection -> Observable
.<T>create(subscriber -> { .<T>create(emitter -> {
serviceConnection.subscriber = subscriber; onSubscribeServiceConnection.emitter = emitter;
context.bindService(new Intent(context, serviceClass), serviceConnection, Context.BIND_AUTO_CREATE); context.bindService(new Intent(context, serviceClass), onSubscribeServiceConnection, Context.BIND_AUTO_CREATE);
}) }, Emitter.BackpressureMode.LATEST)
.doOnUnsubscribe(() -> context.unbindService(serviceConnection))) .doOnUnsubscribe(() -> {
context.unbindService(onSubscribeServiceConnection);
onSubscribeServiceConnection.emitter = null;
}))
.distinctUntilChanged() .distinctUntilChanged()
.replay(1) .replay(1)
.refCount(); .refCount();
} }
/**
* Observes classic Android broadcast with {@link BroadcastReceiver} as source of Observable items and Intent as items.
*
* @param context Context to register {@link BroadcastReceiver};
* @param intentFilter {@link IntentFilter} to register {@link BroadcastReceiver};
* @return Observable that observes Android broadcasts.
*/
@NonNull
public static Observable<Intent> observeBroadcastEvent(@NonNull final Context context, @NonNull final IntentFilter intentFilter) {
return Observable
.just(new OnSubscribeBroadcastReceiver())
.switchMap(onOnSubscribeBroadcastReceiver -> Observable
.<Intent>create(emitter -> {
onOnSubscribeBroadcastReceiver.emitter = emitter;
context.registerReceiver(onOnSubscribeBroadcastReceiver, intentFilter);
}, Emitter.BackpressureMode.LATEST)
.doOnUnsubscribe(() -> {
context.unregisterReceiver(onOnSubscribeBroadcastReceiver);
onOnSubscribeBroadcastReceiver.emitter = null;
}))
.share();
}
/** /**
* Creating {@link Scheduler} that is scheduling work on specific thread with {@link Looper}. * Creating {@link Scheduler} that is scheduling work on specific thread with {@link Looper}.
* Do not use it much times - it is creating endless thread every call. * Do not use it much times - it is creating endless thread every call.
* It's good to use it only like a constant like: * It's good to use it only like a constant like:
* private static final Scheduler SCHEDULER = RxAndroidUtils.createLooperScheduler(); * private static final Scheduler SCHEDULER = RxAndroidUtils.createLooperScheduler();
* IMPORTANT NOTE: looper thread will live forever! Do not create a lot of such Schedulers.
* *
* @return Looper thread based {@link Scheduler}. * @return Looper thread based {@link Scheduler}.
*/ */
@ -91,20 +120,19 @@ public final class RxAndroidUtils {
private RxAndroidUtils() { private RxAndroidUtils() {
} }
private static class SubscribeServiceConnection<T> implements ServiceConnection { private static class OnSubscribeServiceConnection<TService extends Service> implements ServiceConnection {
@Nullable @Nullable
private Subscriber<? super T> subscriber; private Emitter<? super TService> emitter;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public void onServiceConnected(@NonNull final ComponentName name, @Nullable final IBinder service) { public void onServiceConnected(@NonNull final ComponentName name, @Nullable final IBinder service) {
if (subscriber == null) { if (emitter == null) {
return; return;
} }
if (service instanceof ServiceBinder) { if (service instanceof ServiceBinder) {
subscriber.onNext((T) ((ServiceBinder) service).getService()); emitter.onNext((TService) ((ServiceBinder) service).getService());
} else { } else {
Lc.assertion("IBinder should be instance of ServiceBinder."); Lc.assertion("IBinder should be instance of ServiceBinder.");
} }
@ -112,8 +140,21 @@ public final class RxAndroidUtils {
@Override @Override
public void onServiceDisconnected(@NonNull final ComponentName name) { public void onServiceDisconnected(@NonNull final ComponentName name) {
if (subscriber != null) { // service have been killed/crashed and destroyed. instead of emit null just wait service reconnection.
subscriber.onNext(null); // even if someone keeps reference to dead service it is problem of service object to work correctly after destroy.
}
}
private static class OnSubscribeBroadcastReceiver extends BroadcastReceiver {
@Nullable
private Emitter<? super Intent> emitter;
@Override
public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
if (emitter != null) {
emitter.onNext(intent);
} }
} }

View File

@ -25,25 +25,21 @@ import android.support.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import rx.Emitter;
import rx.Observable; import rx.Observable;
import rx.Subscriber;
/** /**
* Created by Gavriil Sitnikov on 23/05/16. * Created by Gavriil Sitnikov on 23/05/16.
* Class to represent collection which is providing it's inner changes in Rx observable way. * Class to represent collection which is providing it's inner changes in Rx observable way.
* Use {@link #observeChanges()} and {@link #observeItems()} to observe collection changes. * Use {@link #observeChanges()} and {@link #observeItems()} to observe collection changes.
* Use {@link #loadItem(int)} to load item asynchronously.
* Methods {@link #size()} and {@link #get(int)} will return only already loaded items info. * Methods {@link #size()} and {@link #get(int)} will return only already loaded items info.
* *
* @param <TItem> Type of collection's items. * @param <TItem> Type of collection's items.
*/ */
public abstract class ObservableCollection<TItem> implements Serializable { public abstract class ObservableCollection<TItem> {
private int changesCount; private int changesCount;
@NonNull @NonNull
@ -51,7 +47,7 @@ public abstract class ObservableCollection<TItem> implements Serializable {
@NonNull @NonNull
private transient Observable<Collection<TItem>> itemsObservable; private transient Observable<Collection<TItem>> itemsObservable;
@Nullable @Nullable
private transient Subscriber<? super CollectionChange<TItem>> changesSubscriber; private transient Emitter<? super CollectionChange<TItem>> changesEmitter;
public ObservableCollection() { public ObservableCollection() {
this.changesObservable = createChangesObservable(); this.changesObservable = createChangesObservable();
@ -61,19 +57,16 @@ public abstract class ObservableCollection<TItem> implements Serializable {
@NonNull @NonNull
private Observable<CollectionChange<TItem>> createChangesObservable() { private Observable<CollectionChange<TItem>> createChangesObservable() {
return Observable return Observable
.<CollectionChange<TItem>>create(subscriber -> this.changesSubscriber = subscriber) .<CollectionChange<TItem>>create(emitter -> this.changesEmitter = emitter, Emitter.BackpressureMode.BUFFER)
.doOnUnsubscribe(() -> this.changesSubscriber = null) .doOnUnsubscribe(() -> this.changesEmitter = null)
.replay(0) .share();
.refCount();
} }
@NonNull @NonNull
private Observable<Collection<TItem>> createItemsObservable() { private Observable<Collection<TItem>> createItemsObservable() {
return Observable return Observable
.<Collection<TItem>>switchOnNext(Observable.create(subscriber -> { //switchOnNext to calculate getItems() on subscription but not on that method calling moment
subscriber.onNext(observeChanges().map(changes -> getItems()).startWith(getItems())); .switchOnNext(Observable.fromCallable(() -> observeChanges().map(changes -> getItems()).startWith(getItems())))
subscriber.onCompleted();
}))
.replay(1) .replay(1)
.refCount(); .refCount();
} }
@ -102,9 +95,12 @@ public abstract class ObservableCollection<TItem> implements Serializable {
* @param changes Changes of collection. * @param changes Changes of collection.
*/ */
protected void notifyAboutChanges(@NonNull final Collection<Change<TItem>> changes) { protected void notifyAboutChanges(@NonNull final Collection<Change<TItem>> changes) {
if (changes.isEmpty()) {
return;
}
changesCount++; changesCount++;
if (changesSubscriber != null) { if (changesEmitter != null) {
changesSubscriber.onNext(new CollectionChange<>(changesCount, Collections.unmodifiableCollection(changes))); changesEmitter.onNext(new CollectionChange<>(changesCount, Collections.unmodifiableCollection(changes)));
} }
} }
@ -164,36 +160,6 @@ public abstract class ObservableCollection<TItem> implements Serializable {
return size() == 0; return size() == 0;
} }
/**
* Returns {@link Observable} which is loading item by position.
* It could return null in onNext callback if there is no item to load for such position.
*
* @param position Position to load item;
* @return {@link Observable} to load item.
*/
@NonNull
public abstract Observable<TItem> loadItem(int position);
/**
* Returns {@link Observable} which is loading item by range.
* It will return collection of loaded items in onNext callback.
*
* @param first First position of item to load;
* @param last Last position of item to load;
* @return {@link Observable} to load items.
*/
@NonNull
public Observable<Collection<TItem>> loadRange(final int first, final int last) {
final List<Observable<TItem>> itemsRequests = new ArrayList<>();
for (int i = first; i <= last; i++) {
itemsRequests.add(loadItem(i));
}
return Observable.concatEager(itemsRequests)
.filter(loadedItem -> loadedItem != null)
.toList()
.map(Collections::unmodifiableCollection);
}
private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException { private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException {
outputStream.writeInt(changesCount); outputStream.writeInt(changesCount);
} }

View File

@ -8,9 +8,9 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException; import rx.Subscription;
import rx.Observable;
import rx.functions.Func1; import rx.functions.Func1;
import rx.schedulers.Schedulers;
/** /**
* Created by Gavriil Sitnikov on 02/06/2016. * Created by Gavriil Sitnikov on 02/06/2016.
@ -23,7 +23,10 @@ public class ObservableFilteredList<TItem> extends ObservableCollection<TItem> {
@NonNull @NonNull
private static <TItem> List<TItem> filterCollection(@NonNull final Collection<TItem> sourceCollection, private static <TItem> List<TItem> filterCollection(@NonNull final Collection<TItem> sourceCollection,
@NonNull final Func1<TItem, Boolean> filter) { @Nullable final Func1<TItem, Boolean> filter) {
if (filter == null) {
return new ArrayList<>(sourceCollection);
}
final List<TItem> result = new ArrayList<>(sourceCollection.size()); final List<TItem> result = new ArrayList<>(sourceCollection.size());
for (final TItem item : sourceCollection) { for (final TItem item : sourceCollection) {
if (filter.call(item)) { if (filter.call(item)) {
@ -33,35 +36,39 @@ public class ObservableFilteredList<TItem> extends ObservableCollection<TItem> {
return result; return result;
} }
@Nullable @NonNull
private List<TItem> filteredList; private List<TItem> filteredList;
@Nullable @NonNull
private Collection<TItem> sourceCollection; private ObservableCollection<TItem> sourceCollection;
@Nullable @Nullable
private Func1<TItem, Boolean> filter; private Func1<TItem, Boolean> filter;
@Nullable
public ObservableFilteredList() { private Subscription sourceCollectionSubscription;
super();
//do nothing
}
public ObservableFilteredList(@NonNull final Collection<TItem> sourceCollection) {
super();
this.sourceCollection = new ArrayList<>(sourceCollection);
this.filteredList = new ArrayList<>(sourceCollection);
}
public ObservableFilteredList(@NonNull final Func1<TItem, Boolean> filter) { public ObservableFilteredList(@NonNull final Func1<TItem, Boolean> filter) {
super(); this(new ArrayList<>(), filter);
this.filter = filter;
} }
public ObservableFilteredList(@NonNull final Collection<TItem> sourceCollection, public ObservableFilteredList(@NonNull final Collection<TItem> sourceCollection, @Nullable final Func1<TItem, Boolean> filter) {
@NonNull final Func1<TItem, Boolean> filter) { this(new ObservableList<>(sourceCollection), filter);
}
public ObservableFilteredList(@NonNull final ObservableCollection<TItem> sourceCollection, @Nullable final Func1<TItem, Boolean> filter) {
super(); super();
this.sourceCollection = new ArrayList<>(sourceCollection);
this.filter = filter; this.filter = filter;
filteredList = filterCollection(this.sourceCollection, this.filter); this.sourceCollection = sourceCollection;
this.filteredList = filterCollection(this.sourceCollection.getItems(), this.filter);
update();
}
/**
* Sets collection of items to filter.
*
* @param sourceCollection Collection with items.
*/
public void setSourceCollection(@Nullable final ObservableCollection<TItem> sourceCollection) {
this.sourceCollection = sourceCollection != null ? sourceCollection : new ObservableList<>();
update();
} }
/** /**
@ -70,8 +77,8 @@ public class ObservableFilteredList<TItem> extends ObservableCollection<TItem> {
* @param sourceCollection Collection with items. * @param sourceCollection Collection with items.
*/ */
public void setSourceCollection(@Nullable final Collection<TItem> sourceCollection) { public void setSourceCollection(@Nullable final Collection<TItem> sourceCollection) {
this.sourceCollection = sourceCollection != null ? new ArrayList<>(sourceCollection) : null; this.sourceCollection = sourceCollection != null ? new ObservableList<>(sourceCollection) : new ObservableList<>();
updateCollections(); update();
} }
/** /**
@ -81,73 +88,51 @@ public class ObservableFilteredList<TItem> extends ObservableCollection<TItem> {
*/ */
public void setFilter(@Nullable final Func1<TItem, Boolean> filter) { public void setFilter(@Nullable final Func1<TItem, Boolean> filter) {
this.filter = filter; this.filter = filter;
updateCollections(); update();
} }
/** /**
* Updates collection by current filter. Use it if some item's parameter which is important for filtering have changing. * Updates collection by current filter. Use it if some item's parameter which is important for filtering have changing.
*/ */
public void updateCollections() { private void update() {
if (sourceCollection == null) { if (sourceCollectionSubscription != null) {
if (filteredList != null) { sourceCollectionSubscription.unsubscribe();
final Change<TItem> change = new Change<>(Change.Type.REMOVED, filteredList, 0); sourceCollectionSubscription = null;
filteredList = null;
notifyAboutChange(change);
}
return;
}
final List<TItem> oldFilteredList = filteredList;
if (filter != null) {
filteredList = filterCollection(sourceCollection, filter);
} else {
filteredList = new ArrayList<>(sourceCollection);
}
if (oldFilteredList != null) {
final Collection<Change<TItem>> changes = Change.calculateCollectionChanges(oldFilteredList, filteredList, false);
if (!changes.isEmpty()) {
notifyAboutChanges(changes);
}
} else {
notifyAboutChange(new Change<>(Change.Type.INSERTED, filteredList, 0));
} }
sourceCollectionSubscription = sourceCollection.observeItems()
.observeOn(Schedulers.computation())
.subscribe(items -> {
final List<TItem> oldFilteredList = filteredList;
filteredList = filterCollection(items, filter);
notifyAboutChanges(Change.calculateCollectionChanges(oldFilteredList, filteredList, false));
});
} }
@Override @Override
public int size() { public int size() {
return filteredList != null ? filteredList.size() : 0; return filteredList.size();
} }
@NonNull @NonNull
@Override @Override
public TItem get(final int position) { public TItem get(final int position) {
if (filteredList == null) {
throw new ShouldNotHappenException();
}
return filteredList.get(position); return filteredList.get(position);
} }
@NonNull @NonNull
@Override @Override
public Collection<TItem> getItems() { public Collection<TItem> getItems() {
return filteredList != null ? Collections.unmodifiableCollection(filteredList) : Collections.emptyList(); return Collections.unmodifiableCollection(filteredList);
} }
/** /**
* Returns source non-filtered collection of items. * Returns source non-filtered observable collection of items.
* *
* @return Non-filtered collection of items. * @return Non-filtered collection of items.
*/ */
@NonNull @NonNull
public Collection<TItem> getSourceItems() { public ObservableCollection<TItem> getSourceCollection() {
return sourceCollection != null ? Collections.unmodifiableCollection(sourceCollection) : Collections.emptyList(); return sourceCollection;
}
@NonNull
@Override
public Observable<TItem> loadItem(final int position) {
return filteredList != null && filteredList.size() > position
? Observable.just(filteredList.get(position))
: Observable.just(null);
} }
} }

View File

@ -31,7 +31,6 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import ru.touchin.roboswag.core.log.Lc; import ru.touchin.roboswag.core.log.Lc;
import rx.Observable;
/** /**
* Created by Gavriil Sitnikov on 23/05/16. * Created by Gavriil Sitnikov on 23/05/16.
@ -218,9 +217,7 @@ public class ObservableList<TItem> extends ObservableCollection<TItem> implement
final Collection<Change<TItem>> changes = Change.calculateCollectionChanges(items, newItems, false); final Collection<Change<TItem>> changes = Change.calculateCollectionChanges(items, newItems, false);
items.clear(); items.clear();
items.addAll(newItems); items.addAll(newItems);
if (!changes.isEmpty()) { notifyAboutChanges(changes);
notifyAboutChanges(changes);
}
} }
} }
@ -243,14 +240,6 @@ public class ObservableList<TItem> extends ObservableCollection<TItem> implement
} }
} }
@NonNull
@Override
public Observable<TItem> loadItem(final int position) {
synchronized (this) {
return position < items.size() ? Observable.just(items.get(position)) : Observable.just(null);
}
}
private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException { private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException {
outputStream.writeObject(items); outputStream.writeObject(items);
} }

View File

@ -1,49 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.Nullable;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* Object represents loaded items with reference to load other parts and info of are there more items to load or not.
*
* @param <TItem> Type of items to load;
* @param <TReference> Type of reference to load other parts of items;
* @param <TNewerReference> Type of reference to load newer parts of items.
*/
public interface LoadedRenewableItems<TItem, TReference, TNewerReference> extends LoadedItems<TItem, TReference> {
/**
* Returns count of new items other than loaded.
*
* @return Count of new items other than loaded.
*/
int getNewerItemsCount();
/**
* Returns reference to load newer items from this loaded part.
*
* @return Reference to load newer items.
*/
@Nullable
TNewerReference getNewerReference();
}

View File

@ -24,6 +24,7 @@ import android.support.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -46,6 +47,7 @@ import rx.subjects.BehaviorSubject;
* {@link ObservableCollection} which is loading items more and more by paging/limit-offset/reference-based mechanisms. * {@link ObservableCollection} which is loading items more and more by paging/limit-offset/reference-based mechanisms.
* To use this collection {@link MoreItemsLoader} should be created. * To use this collection {@link MoreItemsLoader} should be created.
* {@link MoreItemsLoader} is an object to load next block of items by info from previous loaded block (last loaded item/reference etc.). * {@link MoreItemsLoader} is an object to load next block of items by info from previous loaded block (last loaded item/reference etc.).
* Use {@link #loadItem(int)} and {@link #loadRange(int, int)} to load items asynchronously.
* *
* @param <TItem> Type of collection's items; * @param <TItem> Type of collection's items;
* @param <TMoreReference> Type of reference object to help rightly loading next block of items; * @param <TMoreReference> Type of reference object to help rightly loading next block of items;
@ -82,10 +84,7 @@ public class LoadingMoreList<TItem, TMoreReference, TLoadedItems extends LoadedI
@Nullable final LoadedItems<TItem, TMoreReference> initialItems) { @Nullable final LoadedItems<TItem, TMoreReference> initialItems) {
super(); super();
this.loadingMoreObservable = Observable this.loadingMoreObservable = Observable
.switchOnNext(Observable.<Observable<TLoadedItems>>create(subscriber -> { .switchOnNext(Observable.fromCallable(() -> createLoadRequestBasedObservable(this::createActualRequest, moreMoreItemsLoader::load)))
subscriber.onNext(createLoadRequestBasedObservable(this::createActualRequest, moreMoreItemsLoader::load));
subscriber.onCompleted();
}))
.single() .single()
.doOnError(throwable -> { .doOnError(throwable -> {
if (throwable instanceof IllegalArgumentException || throwable instanceof NoSuchElementException) { if (throwable instanceof IllegalArgumentException || throwable instanceof NoSuchElementException) {
@ -287,25 +286,50 @@ public class LoadingMoreList<TItem, TMoreReference, TLoadedItems extends LoadedI
return loadingMoreObservable; return loadingMoreObservable;
} }
/**
* Returns {@link Observable} which is loading item by position.
* It could return null in onNext callback if there is no item to load for such position.
*
* @param position Position to load item;
* @return {@link Observable} to load item.
*/
@NonNull @NonNull
@Override
public Observable<TItem> loadItem(final int position) { public Observable<TItem> loadItem(final int position) {
return Observable return Observable
.switchOnNext(Observable .switchOnNext(Observable
.<Observable<TItem>>create(subscriber -> { .fromCallable(() -> {
if (position < size()) { if (position < size()) {
subscriber.onNext(Observable.just(get(position))); return Observable.just(get(position));
} else if (moreItemsCount.getValue() == 0) { } else if (moreItemsCount.getValue() == 0) {
subscriber.onNext(Observable.just((TItem) null)); return Observable.just((TItem) null);
} else { } else {
subscriber.onNext(loadingMoreObservable.switchMap(ignored -> Observable.<TItem>error(new NotLoadedYetException()))); return loadingMoreObservable.switchMap(ignored -> Observable.<TItem>error(new NotLoadedYetException()));
} }
subscriber.onCompleted();
}) })
.subscribeOn(loaderScheduler)) .subscribeOn(loaderScheduler))
.retry((number, throwable) -> throwable instanceof NotLoadedYetException); .retry((number, throwable) -> throwable instanceof NotLoadedYetException);
} }
/**
* Returns {@link Observable} which is loading item by range.
* It will return collection of loaded items in onNext callback.
*
* @param first First position of item to load;
* @param last Last position of item to load;
* @return {@link Observable} to load items.
*/
@NonNull
public Observable<Collection<TItem>> loadRange(final int first, final int last) {
final List<Observable<TItem>> itemsRequests = new ArrayList<>();
for (int i = first; i <= last; i++) {
itemsRequests.add(loadItem(i));
}
return Observable.concatEager(itemsRequests)
.filter(loadedItem -> loadedItem != null)
.toList()
.map(Collections::unmodifiableCollection);
}
/** /**
* Remove all loaded items and resets collection's state. * Remove all loaded items and resets collection's state.
*/ */
@ -335,7 +359,7 @@ public class LoadingMoreList<TItem, TMoreReference, TLoadedItems extends LoadedI
DO_NOTHING, DO_NOTHING,
REMOVE_FROM_COLLECTION, REMOVE_FROM_COLLECTION,
REMOVE_FROM_LOADED_ITEMS, REMOVE_FROM_LOADED_ITEMS,
REPLACE_SOURCE_ITEM_WITH_LOADED, REPLACE_SOURCE_ITEM_WITH_LOADED
} }
/** /**
@ -354,7 +378,7 @@ public class LoadingMoreList<TItem, TMoreReference, TLoadedItems extends LoadedI
* @return Action to do with items. * @return Action to do with items.
*/ */
@NonNull @NonNull
FilterAction decideFilterAction(@NonNull final TItem collectionObject, @NonNull final TItem loadedItemsObject); FilterAction decideFilterAction(@NonNull TItem collectionObject, @NonNull TItem loadedItemsObject);
} }

View File

@ -1,241 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.NoSuchElementException;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.observables.collections.ObservableCollection;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* {@link ObservableCollection} which is loading items more and more by paging/limit-offset/reference-based mechanisms but also it is providing
* interface to load newer items and info about it's loading availability.
* To use this collection {@link MoreItemsLoader} and {@link NewerItemsLoader} should be created.
*
* @param <TItem> Type of collection's items;
* @param <TReference> Type of reference object to help rightly loading next block of items;
* @param <TNewerReference> Type of reference object to help rightly loading block of newer items;
* @param <TLoadedItems> Type of loading block of items.
*/
public class LoadingRenewableList<TItem, TReference, TNewerReference,
TLoadedItems extends LoadedRenewableItems<TItem, TReference, TNewerReference>>
extends LoadingMoreList<TItem, TReference, TLoadedItems> {
@Nullable
private TNewerReference newerReference;
@NonNull
private final BehaviorSubject<Integer> newerItemsCount = BehaviorSubject.create(LoadedItems.UNKNOWN_ITEMS_COUNT);
@NonNull
private final Observable<TLoadedItems> loadingNewerObservable;
@NonNull
private final Observable<TLoadedItems> loadingNewestObservable;
public LoadingRenewableList(@NonNull final MoreItemsLoader<TItem, TReference, TLoadedItems> moreMoreItemsLoader,
@NonNull final NewerItemsLoader<TItem, TReference, TNewerReference, TLoadedItems> newerItemsLoader) {
super(moreMoreItemsLoader);
this.loadingNewerObservable = createLoadingNewerObservable(newerItemsLoader, false);
this.loadingNewestObservable = createLoadingNewerObservable(newerItemsLoader, true);
}
public LoadingRenewableList(@NonNull final MoreItemsLoader<TItem, TReference, TLoadedItems> moreMoreItemsLoader,
@NonNull final NewerItemsLoader<TItem, TReference, TNewerReference, TLoadedItems> newerItemsLoader,
@Nullable final TLoadedItems initialItems) {
super(moreMoreItemsLoader, initialItems);
this.loadingNewerObservable = createLoadingNewerObservable(newerItemsLoader, false);
this.loadingNewestObservable = createLoadingNewerObservable(newerItemsLoader, true);
if (initialItems != null) {
updateNewerReference(initialItems);
}
}
@NonNull
private Observable<TLoadedItems> waitForInitialLoading(@NonNull final Observable<TLoadedItems> observable) {
return getLoadingMoreObservable().ignoreElements().concatWith(observable);
}
@NonNull
private NewerLoadRequest<TNewerReference> createActualRequest() {
return new NewerLoadRequest<>(newerReference, newerItemsCount.getValue());
}
@NonNull
private Observable<TLoadedItems> createLoadingNewerObservable(
@NonNull final NewerItemsLoader<TItem, TReference, TNewerReference, TLoadedItems> newerItemsLoader,
final boolean renew) {
return Observable
.switchOnNext(Observable.<Observable<TLoadedItems>>create(subscriber -> {
if (!renew) {
subscriber.onNext(Observable.concat(
//we need non-empty list to start loading newer items or we need to wait any change (should be insertion)
isEmpty() ? observeChanges().first().switchMap(ignored -> Observable.empty()) : Observable.empty(),
createLoadRequestBasedObservable(this::createActualRequest,
loadRequest -> loadRequest.getNewerReference() == null && isEmpty()
? waitForInitialLoading(newerItemsLoader.load(loadRequest))
: newerItemsLoader.load(loadRequest))));
} else {
subscriber.onNext(newerItemsLoader.load(new NewerLoadRequest<>(null, LoadedItems.UNKNOWN_ITEMS_COUNT))
.subscribeOn(Schedulers.io())
.observeOn(getLoaderScheduler()));
}
subscriber.onCompleted();
}))
.single()
.doOnError(throwable -> {
if (throwable instanceof IllegalArgumentException || throwable instanceof NoSuchElementException) {
Lc.assertion(new ShouldNotHappenException("Updates during loading not supported."
+ " NewerItemsLoader should emit only one result.", throwable));
}
})
.doOnNext(loadedItems -> onNewerItemsLoaded(loadedItems, renew))
.replay(1)
.refCount();
}
/**
* Returns if there are new items to load.
*
* @return True if there are more items to load.
*/
public boolean hasNewerItems() {
return newerItemsCount.getValue() != 0;
}
/**
* Update a new items count.
*
* @param count new items count
*/
public void updateNewerItemsCount(final int count) {
newerItemsCount.onNext(count);
}
/**
* Returns {@link Observable} which is providing status of if is there are new items to load or not.
*
* @return {@link Observable} of more items availability status.
*/
@NonNull
public Observable<Boolean> observeHasNewerItems() {
return newerItemsCount.map(count -> count != 0).distinctUntilChanged();
}
/**
* Returns {@link Observable} which is providing count of new items to load.
*
* @return {@link Observable} of new items availability status.
*/
@NonNull
public Observable<Integer> observeNewerItemsCount() {
return newerItemsCount.distinctUntilChanged();
}
@Override
protected void onItemsLoaded(@NonNull final TLoadedItems loadedItems, final int insertPosition, final boolean reset) {
super.onItemsLoaded(loadedItems, insertPosition, reset);
if (newerReference == null) {
updateNewerReference(loadedItems);
}
}
/**
* Calls when newer items part loaded.
*
* @param loadedItems Loaded items;
* @param renew Flag indicates is it loading just to load some new items (false) or to load totally new items (true).
*/
protected void onNewerItemsLoaded(@NonNull final TLoadedItems loadedItems, final boolean renew) {
onItemsLoaded(loadedItems, 0, renew);
updateNewerReference(loadedItems);
}
@Override
protected void resetState() {
super.resetState();
newerReference = null;
newerItemsCount.onNext(LoadedItems.UNKNOWN_ITEMS_COUNT);
}
/**
* Returns {@link Observable} that will load newer items by count returned by last loaded items part.
*
* @return {@link Observable} to load newer items.
*/
@NonNull
public Observable<TLoadedItems> loadNewer() {
return loadingNewerObservable.first();
}
/**
* Returns {@link Observable} that will load all newer items.
*
* @return Returns {@link Observable} to limited load newer items.
*/
@NonNull
public Observable<?> loadToNewest() {
return loadToNewest(Integer.MAX_VALUE);
}
/**
* Returns {@link Observable} that will load some newer itemslimited by maximum pages loading results.
*
* @param maxPageDeep Limit to load pages;
* @return Returns {@link Observable} to limited load newer items.
*/
@NonNull
public Observable<?> loadToNewest(final int maxPageDeep) {
return Observable
.switchOnNext(Observable
.<Observable<?>>create(subscriber -> {
subscriber.onNext(newerItemsCount.getValue() == 0
? Observable.empty()
: loadingNewerObservable.switchMap(ignored -> Observable.error(new NotLoadedYetException())));
subscriber.onCompleted();
})
.subscribeOn(getLoaderScheduler()))
.retry((number, throwable) -> number <= maxPageDeep && throwable instanceof NotLoadedYetException);
}
/**
* Returns {@link Observable} that tries to load some newer items even if there are no info about count of them.
*
* @return {@link Observable} to load newer items.
*/
@NonNull
public Observable<TLoadedItems> renew() {
return loadingNewestObservable.first();
}
private void updateNewerReference(@NonNull final TLoadedItems loadedItems) {
if (loadedItems.getNewerReference() != null) {
newerReference = loadedItems.getNewerReference();
}
newerItemsCount.onNext(loadedItems.getNewerItemsCount());
}
}

View File

@ -1,40 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.NonNull;
import rx.Observable;
/**
* Created by Gavriil Sitnikov on 02/06/2016.
* Object that is loading new part of items by reference.
*
* @param <TItem> Type of items to be loaded;
* @param <TNewerReference> Type of reference to be used to load new part of items;
* @param <TLoadedItems> Type of loaded items part.
*/
public interface NewerItemsLoader<TItem, TReference, TNewerReference,
TLoadedItems extends LoadedRenewableItems<TItem, TReference, TNewerReference>> {
@NonNull
Observable<TLoadedItems> load(@NonNull final NewerLoadRequest<TNewerReference> newerLoadRequest);
}

View File

@ -1,74 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.Nullable;
import ru.touchin.roboswag.core.utils.ObjectUtils;
/**
* Created by Gavriil Sitnikov on 02/06/2016.
* Request represents request to load new part of items.
*
* @param <TNewerReference> Type of reference to load new part of items.
*/
public class NewerLoadRequest<TNewerReference> {
@Nullable
private final TNewerReference newerReference;
private final int newerItemsCount;
public NewerLoadRequest(@Nullable final TNewerReference newerReference, final int newerItemsCount) {
this.newerReference = newerReference;
this.newerItemsCount = newerItemsCount;
}
/**
* Returns reference to be used to load new part of items.
*
* @return Reference object.
*/
@Nullable
public TNewerReference getNewerReference() {
return newerReference;
}
/**
* Count of newer items to load.
*
* @return Count of newer items to load.
*/
public int getNewerItemsCount() {
return newerItemsCount;
}
@Override
public boolean equals(@Nullable final Object object) {
return object instanceof NewerLoadRequest
&& ObjectUtils.equals(((NewerLoadRequest) object).newerReference, newerReference)
&& ((NewerLoadRequest) object).newerItemsCount == newerItemsCount;
}
@Override
public int hashCode() {
return newerItemsCount + (newerReference != null ? newerReference.hashCode() : 0);
}
}

View File

@ -0,0 +1,482 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.storable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.lang.reflect.Type;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.log.LcGroup;
import ru.touchin.roboswag.core.observables.OnSubscribeRefCountWithCacheTime;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Completable;
import rx.Observable;
import rx.Scheduler;
import rx.Single;
import rx.exceptions.OnErrorThrowable;
import rx.functions.Actions;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* Base class allows to async access to some store.
* Supports conversion between store and actual value. If it is not needed then use {@link SameTypesConverter}
* Supports migration from specific version to latest by {@link Migration} object.
* Allows to set default value which will be returned if actual value is null.
* Allows to declare specific {@link ObserveStrategy}.
* Also specific {@link Scheduler} could be specified to not create new scheduler per storable.
*
* @param <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject};
* @param <TReturnObject> Type of actual value operating by Storable. Could be same as {@link TObject}.
*/
public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE");
private static final long DEFAULT_CACHE_TIME_MILLIS = TimeUnit.SECONDS.toMillis(5);
@NonNull
private static ObserveStrategy getDefaultObserveStrategyFor(@NonNull final Type objectType, @NonNull final Type storeObjectType) {
if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) objectType)) {
return ObserveStrategy.CACHE_ACTUAL_VALUE;
}
if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) storeObjectType)) {
return ObserveStrategy.CACHE_STORE_VALUE;
}
return ObserveStrategy.NO_CACHE;
}
@NonNull
private final TKey key;
@NonNull
private final Type objectType;
@NonNull
private final Type storeObjectType;
@NonNull
private final Store<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
@NonNull
private final PublishSubject<Optional<TStoreObject>> newStoreValueEvent = PublishSubject.create();
@NonNull
private final Observable<Optional<TStoreObject>> storeValueObservable;
@NonNull
private final Observable<Optional<TObject>> valueObservable;
@NonNull
private final Scheduler scheduler;
public BaseStorable(@NonNull final BuilderCore<TKey, TObject, TStoreObject> builderCore) {
this(builderCore.key, builderCore.objectType, builderCore.storeObjectType,
builderCore.store, builderCore.converter, builderCore.observeStrategy,
builderCore.migration, builderCore.defaultValue, builderCore.storeScheduler, builderCore.cacheTimeMillis);
}
@SuppressWarnings("PMD.ExcessiveParameterList")
//ExcessiveParameterList: that's why we are using builder to create it
private BaseStorable(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler,
final long cacheTimeMillis) {
this.key = key;
this.objectType = objectType;
this.storeObjectType = storeObjectType;
this.store = store;
this.converter = converter;
final ObserveStrategy nonNullObserveStrategy
= observeStrategy != null ? observeStrategy : getDefaultObserveStrategyFor(objectType, storeObjectType);
scheduler = storeScheduler != null ? storeScheduler : Schedulers.from(Executors.newSingleThreadExecutor());
storeValueObservable
= createStoreValueObservable(nonNullObserveStrategy, migration, defaultValue, cacheTimeMillis);
valueObservable = createValueObservable(storeValueObservable, nonNullObserveStrategy, cacheTimeMillis);
}
@Nullable
private Optional<TStoreObject> returnDefaultValueIfNull(@NonNull final Optional<TStoreObject> storeObject, @Nullable final TObject defaultValue) {
if (storeObject.getValue() != null || defaultValue == null) {
return storeObject;
}
try {
return new Optional<>(converter.toStoreObject(objectType, storeObjectType, defaultValue));
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while converting default value of '%s' from '%s' from store %s",
key, defaultValue, store);
throw OnErrorThrowable.from(exception);
}
}
@NonNull
private Observable<Optional<TStoreObject>> createStoreInitialLoadingObservable(@Nullable final Migration<TKey> migration) {
final Single<Optional<TStoreObject>> loadObservable = store.loadObject(storeObjectType, key)
.doOnError(throwable -> STORABLE_LC_GROUP.w(throwable, "Exception while trying to load value of '%s' from store %s", key, store));
return (migration != null ? migration.migrateToLatestVersion(key).andThen(loadObservable) : loadObservable)
.subscribeOn(scheduler)
.observeOn(scheduler)
.toObservable()
.replay(1)
.refCount()
.take(1);
}
@NonNull
private Observable<Optional<TStoreObject>> createStoreValueObservable(@NonNull final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
final long cacheTimeMillis) {
final Observable<Optional<TStoreObject>> storeInitialLoadingObservable = createStoreInitialLoadingObservable(migration);
final Observable<Optional<TStoreObject>> result = storeInitialLoadingObservable
.concatWith(newStoreValueEvent)
.map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue));
return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? Observable.unsafeCreate(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
@NonNull
private Observable<Optional<TObject>> createValueObservable(@NonNull final Observable<Optional<TStoreObject>> storeValueObservable,
@NonNull final ObserveStrategy observeStrategy,
final long cacheTimeMillis) {
final Observable<Optional<TObject>> result = storeValueObservable
.map(storeObject -> {
try {
return new Optional<>(converter.toObject(objectType, storeObjectType, storeObject.getValue()));
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while trying to converting value of '%s' from store %s by %s",
key, storeObject, store, converter);
throw OnErrorThrowable.from(exception);
}
});
return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? Observable.unsafeCreate(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
/**
* Returns key of value.
*
* @return Unique key.
*/
@NonNull
public TKey getKey() {
return key;
}
/**
* Returns type of actual object.
*
* @return Type of actual object.
*/
@NonNull
public Type getObjectType() {
return objectType;
}
/**
* Returns type of store object.
*
* @return Type of store object.
*/
@NonNull
public Type getStoreObjectType() {
return storeObjectType;
}
/**
* Returns {@link Store} where store class representation of object is storing.
*
* @return Store.
*/
@NonNull
public Store<TKey, TStoreObject> getStore() {
return store;
}
/**
* Returns {@link Converter} to convert values from store class to actual and back.
*
* @return Converter.
*/
@NonNull
public Converter<TObject, TStoreObject> getConverter() {
return converter;
}
@NonNull
private Completable internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) {
return (checkForEqualityBeforeSet ? storeValueObservable.take(1).toSingle() : Single.just(new Optional<>(null)))
.observeOn(scheduler)
.flatMapCompletable(oldStoreValue -> {
final TStoreObject newStoreValue;
try {
newStoreValue = converter.toStoreObject(objectType, storeObjectType, newValue);
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while trying to store value of '%s' from store %s by %s",
key, newValue, store, converter);
return Completable.error(exception);
}
if (checkForEqualityBeforeSet && ObjectUtils.equals(newStoreValue, oldStoreValue.getValue())) {
return Completable.complete();
}
return store.storeObject(storeObjectType, key, newStoreValue)
.doOnError(throwable -> STORABLE_LC_GROUP.w(throwable,
"Exception while trying to store value of '%s' from store %s by %s",
key, newValue, store, converter))
.observeOn(scheduler)
.andThen(Completable.fromAction(() -> {
newStoreValueEvent.onNext(new Optional<>(newStoreValue));
if (checkForEqualityBeforeSet) {
STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, oldStoreValue, newStoreValue);
} else {
STORABLE_LC_GROUP.i("Value of '%s' force changed to '%s'", key, newStoreValue);
}
}));
});
}
/**
* Creates observable which is async setting value to store.
* It is not checking if stored value equals new value.
* In result it will be faster to not get value from store and compare but it will emit item to {@link #observe()} subscribers.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event!
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
//COMPATIBILITY NOTE: it is not Completable to prevent migration of old code
@NonNull
public Observable<?> forceSet(@Nullable final TObject newValue) {
return internalSet(newValue, false).toObservable();
}
/**
* Creates observable which is async setting value to store.
* It is checking if stored value equals new value.
* In result it will take time to get value from store and compare
* but it won't emit item to {@link #observe()} subscribers if stored value equals new value.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event!
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
//COMPATIBILITY NOTE: it is not Completable to prevent migration of old code
@NonNull
public Observable<?> set(@Nullable final TObject newValue) {
return internalSet(newValue, true).toObservable();
}
@Deprecated
//COMPATIBILITY NOTE: it is deprecated as it's execution not bound to Android lifecycle objects
public void setCalm(@Nullable final TObject newValue) {
set(newValue).subscribe(Actions.empty(), Lc::assertion);
}
/**
* Sets value synchronously. You should NOT use this method normally. Use {@link #set(Object)} asynchronously instead.
*
* @param newValue Value to set;
*/
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
public void setSync(@Nullable final TObject newValue) {
set(newValue).toBlocking().subscribe();
}
@NonNull
protected Observable<Optional<TObject>> observeOptionalValue() {
return valueObservable;
}
/**
* Returns Observable which is emitting item on subscribe and every time when someone have changed value.
* It could emit next and error events but not completed.
*
* @return Returns observable of value.
*/
@NonNull
public abstract Observable<TReturnObject> observe();
/**
* Returns Observable which is emitting only one item on subscribe.
* It could emit next and error events but not completed.
*
* @return Returns observable of value.
*/
@NonNull
//COMPATIBILITY NOTE: it is not Single to prevent migration of old code
public Observable<TReturnObject> get() {
return observe().take(1);
}
/**
* Gets value synchronously. You should NOT use this method normally. Use {@link #get()} or {@link #observe()} asynchronously instead.
*
* @return Returns value;
*/
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
@Nullable
public TReturnObject getSync() {
return get().toBlocking().first();
}
/**
* Enum that is representing strategy of observing item from store.
*/
public enum ObserveStrategy {
/**
* Not caching value so on every {@link #get()} emit it will get value from {@link #getStore()} and converts it with {@link #getConverter()}.
*/
NO_CACHE,
/**
* Caching only store value so on every {@link #get()} emit it will converts it with {@link #getConverter()}.
* Do not use such strategy if store object could be big (like byte-array of file).
*/
CACHE_STORE_VALUE,
/**
* Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}.
* But it will take time for getting value from {@link #getStore()} to set value.
* Do not use such strategy if object could be big (like Bitmap or long string).
* Do not use such strategy if object is mutable because multiple subscribers could then change it's state.
*/
CACHE_ACTUAL_VALUE,
/**
* Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}.
* It won't take time or getting value from {@link #getStore()} to set value.
* Do not use such strategy if store object could be big (like byte-array of file).
* Do not use such strategy if object could be big (like Bitmap or long string).
* Do not use such strategy if object is mutable because multiple subscribers could then change it's state.
*/
CACHE_STORE_AND_ACTUAL_VALUE
}
/**
* Helper class to create various builders.
*
* @param <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
public static class BuilderCore<TKey, TObject, TStoreObject> {
@NonNull
protected final TKey key;
@NonNull
protected final Type objectType;
@NonNull
private final Type storeObjectType;
@NonNull
private final Store<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
@Nullable
private ObserveStrategy observeStrategy;
@Nullable
private Migration<TKey> migration;
@Nullable
private TObject defaultValue;
@Nullable
private Scheduler storeScheduler;
private long cacheTimeMillis;
protected BuilderCore(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter) {
this(key, objectType, storeObjectType, store, converter, null, null, null, null, DEFAULT_CACHE_TIME_MILLIS);
}
protected BuilderCore(@NonNull final BuilderCore<TKey, TObject, TStoreObject> sourceBuilder) {
this(sourceBuilder.key, sourceBuilder.objectType, sourceBuilder.storeObjectType,
sourceBuilder.store, sourceBuilder.converter, sourceBuilder.observeStrategy,
sourceBuilder.migration, sourceBuilder.defaultValue, sourceBuilder.storeScheduler, sourceBuilder.cacheTimeMillis);
}
@SuppressWarnings({"PMD.ExcessiveParameterList", "CPD-START"})
//CPD: it is same code as constructor of Storable
//ExcessiveParameterList: that's why we are using builder to create it
private BuilderCore(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler,
final long cacheTimeMillis) {
this.key = key;
this.objectType = objectType;
this.storeObjectType = storeObjectType;
this.store = store;
this.converter = converter;
this.observeStrategy = observeStrategy;
this.migration = migration;
this.defaultValue = defaultValue;
this.storeScheduler = storeScheduler;
this.cacheTimeMillis = cacheTimeMillis;
}
@SuppressWarnings("CPD-END")
protected void setStoreSchedulerInternal(@Nullable final Scheduler storeScheduler) {
this.storeScheduler = storeScheduler;
}
protected void setObserveStrategyInternal(@Nullable final ObserveStrategy observeStrategy) {
this.observeStrategy = observeStrategy;
}
protected void setMigrationInternal(@NonNull final Migration<TKey> migration) {
this.migration = migration;
}
protected void setCacheTimeInternal(final long cacheTime, @NonNull final TimeUnit timeUnit) {
this.cacheTimeMillis = timeUnit.toMillis(cacheTime);
}
@Nullable
protected TObject getDefaultValue() {
return defaultValue;
}
protected void setDefaultValueInternal(@NonNull final TObject defaultValue) {
this.defaultValue = defaultValue;
}
}
}

View File

@ -58,7 +58,7 @@ public class Migration<TKey> {
@NonNull @NonNull
private Single<Long> loadCurrentVersion(@NonNull final TKey key) { private Single<Long> loadCurrentVersion(@NonNull final TKey key) {
return versionsStore.loadObject(Long.class, key) return versionsStore.loadObject(Long.class, key)
.map(version -> version != null ? version : DEFAULT_VERSION) .map(version -> version.getValue() != null ? version.getValue() : DEFAULT_VERSION)
.onErrorResumeNext(throwable .onErrorResumeNext(throwable
-> Single.error(new MigrationException(String.format("Can't get version of '%s' from %s", key, versionsStore), throwable))); -> Single.error(new MigrationException(String.format("Can't get version of '%s' from %s", key, versionsStore), throwable)));
} }

View File

@ -23,20 +23,12 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.log.LcGroup; import ru.touchin.roboswag.core.observables.storable.concrete.NonNullStorable;
import ru.touchin.roboswag.core.observables.OnSubscribeRefCountWithCacheTime; import ru.touchin.roboswag.core.utils.Optional;
import ru.touchin.roboswag.core.observables.storable.builders.NonNullStorableBuilder;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import rx.Completable;
import rx.Observable; import rx.Observable;
import rx.Scheduler; import rx.Scheduler;
import rx.Single;
import rx.exceptions.OnErrorThrowable;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
/** /**
* Created by Gavriil Sitnikov on 04/10/2015. * Created by Gavriil Sitnikov on 04/10/2015.
@ -51,419 +43,17 @@ import rx.subjects.PublishSubject;
* @param <TObject> Type of actual object; * @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}. * @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/ */
public class Storable<TKey, TObject, TStoreObject> { //COMPATIBILITY NOTE: in RxJava2 it should extends BaseStorable<TKey, TObject, TStoreObject, Optional<TObject>>
public class Storable<TKey, TObject, TStoreObject> extends BaseStorable<TKey, TObject, TStoreObject, TObject> {
public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE");
private static final long DEFAULT_CACHE_TIME_MILLIS = TimeUnit.SECONDS.toMillis(5);
@NonNull
private static ObserveStrategy getDefaultObserveStrategyFor(@NonNull final Type objectType, @NonNull final Type storeObjectType) {
if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) objectType)) {
return ObserveStrategy.CACHE_ACTUAL_VALUE;
}
if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) storeObjectType)) {
return ObserveStrategy.CACHE_STORE_VALUE;
}
return ObserveStrategy.NO_CACHE;
}
@NonNull
private final TKey key;
@NonNull
private final Type objectType;
@NonNull
private final Type storeObjectType;
@NonNull
private final Store<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
@NonNull
private final PublishSubject<TStoreObject> newStoreValueEvent = PublishSubject.create();
@NonNull
private final Observable<TStoreObject> storeValueObservable;
@NonNull
private final Observable<TObject> valueObservable;
@NonNull
private final Scheduler scheduler;
public Storable(@NonNull final BuilderCore<TKey, TObject, TStoreObject> builderCore) { public Storable(@NonNull final BuilderCore<TKey, TObject, TStoreObject> builderCore) {
this(builderCore.key, builderCore.objectType, builderCore.storeObjectType, super(builderCore);
builderCore.store, builderCore.converter, builderCore.observeStrategy,
builderCore.migration, builderCore.defaultValue, builderCore.storeScheduler, builderCore.cacheTimeMillis);
}
@SuppressWarnings("PMD.ExcessiveParameterList")
//ExcessiveParameterList: that's why we are using builder to create it
private Storable(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler,
final long cacheTimeMillis) {
this.key = key;
this.objectType = objectType;
this.storeObjectType = storeObjectType;
this.store = store;
this.converter = converter;
final ObserveStrategy nonNullObserveStrategy
= observeStrategy != null ? observeStrategy : getDefaultObserveStrategyFor(objectType, storeObjectType);
scheduler = storeScheduler != null ? storeScheduler : Schedulers.from(Executors.newSingleThreadExecutor());
storeValueObservable
= createStoreValueObservable(nonNullObserveStrategy, migration, defaultValue, cacheTimeMillis);
valueObservable = createValueObservable(storeValueObservable, nonNullObserveStrategy, cacheTimeMillis);
}
@Nullable
private TStoreObject returnDefaultValueIfNull(@Nullable final TStoreObject storeObject, @Nullable final TObject defaultValue) {
if (storeObject != null || defaultValue == null) {
return storeObject;
}
try {
return converter.toStoreObject(objectType, storeObjectType, defaultValue);
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while converting default value of '%s' from '%s' from store %s",
key, defaultValue, store);
throw OnErrorThrowable.from(exception);
}
} }
@NonNull @NonNull
private Observable<TStoreObject> createStoreInitialLoadingObservable(@Nullable final Migration<TKey> migration) { @Override
final Single<TStoreObject> loadObservable = store.loadObject(storeObjectType, key)
.doOnError(throwable -> STORABLE_LC_GROUP.w(throwable, "Exception while trying to load value of '%s' from store %s", key, store));
return (migration != null ? migration.migrateToLatestVersion(key).andThen(loadObservable) : loadObservable)
.subscribeOn(scheduler)
.observeOn(scheduler)
.toObservable()
.replay(1)
.refCount()
.take(1);
}
@NonNull
private Observable<TStoreObject> createStoreValueObservable(@NonNull final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
final long cacheTimeMillis) {
final Observable<TStoreObject> storeInitialLoadingObservable = createStoreInitialLoadingObservable(migration);
final Observable<TStoreObject> result = storeInitialLoadingObservable
.concatWith(newStoreValueEvent)
.map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue));
return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? Observable.create(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
@NonNull
private Observable<TObject> createValueObservable(@NonNull final Observable<TStoreObject> storeValueObservable,
@NonNull final ObserveStrategy observeStrategy,
final long cacheTimeMillis) {
final Observable<TObject> result = storeValueObservable
.map(storeObject -> {
try {
return converter.toObject(objectType, storeObjectType, storeObject);
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while trying to converting value of '%s' from store %s by %s",
key, storeObject, store, converter);
throw OnErrorThrowable.from(exception);
}
});
return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? Observable.create(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
/**
* Returns key of value.
*
* @return Unique key.
*/
@NonNull
public TKey getKey() {
return key;
}
/**
* Returns type of actual object.
*
* @return Type of actual object.
*/
@NonNull
public Type getObjectType() {
return objectType;
}
/**
* Returns type of store object.
*
* @return Type of store object.
*/
@NonNull
public Type getStoreObjectType() {
return storeObjectType;
}
/**
* Returns {@link Store} where store class representation of object is storing.
*
* @return Store.
*/
@NonNull
public Store<TKey, TStoreObject> getStore() {
return store;
}
/**
* Returns {@link Converter} to convert values from store class to actual and back.
*
* @return Converter.
*/
@NonNull
public Converter<TObject, TStoreObject> getConverter() {
return converter;
}
@NonNull
private Completable internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) {
return (checkForEqualityBeforeSet ? storeValueObservable.take(1) : Observable.just(null))
.observeOn(scheduler)
.switchMap(oldStoreValue -> {
final TStoreObject newStoreValue;
try {
newStoreValue = converter.toStoreObject(objectType, storeObjectType, newValue);
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while trying to store value of '%s' from store %s by %s",
key, newValue, store, converter);
return Observable.error(exception);
}
if (checkForEqualityBeforeSet && ObjectUtils.equals(newStoreValue, oldStoreValue)) {
return Observable.empty();
}
return store.storeObject(storeObjectType, key, newStoreValue)
.doOnError(throwable -> STORABLE_LC_GROUP.w(throwable,
"Exception while trying to store value of '%s' from store %s by %s",
key, newValue, store, converter))
.observeOn(scheduler)
.andThen(Completable.fromAction(() -> {
newStoreValueEvent.onNext(newStoreValue);
if (checkForEqualityBeforeSet) {
STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, oldStoreValue, newStoreValue);
} else {
STORABLE_LC_GROUP.i("Value of '%s' force changed to '%s'", key, newStoreValue);
}
}))
.toObservable();
})
.toCompletable();
}
/**
* Creates observable which is async setting value to store.
* It is not checking if stored value equals new value.
* In result it will be faster to not get value from store and compare but it will emit item to {@link #observe()} subscribers.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! //TODO: it's Completable :(
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
@NonNull
public Observable<?> forceSet(@Nullable final TObject newValue) {
return internalSet(newValue, false).toObservable();
}
/**
* Creates observable which is async setting value to store.
* It is checking if stored value equals new value.
* In result it will take time to get value from store and compare
* but it won't emit item to {@link #observe()} subscribers if stored value equals new value.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! //TODO: it's Completable :(
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
@NonNull
public Observable<?> set(@Nullable final TObject newValue) {
return internalSet(newValue, true).toObservable();
}
/**
* Sets value synchronously. You should NOT use this method normally. Use {@link #set(Object)} asynchronously instead.
*
* @param newValue Value to set;
*/
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
public void setSync(@Nullable final TObject newValue) {
set(newValue).toBlocking().subscribe();
}
/**
* Returns Observable which is emitting item on subscribe and every time when someone have changed value.
* It could emit next and error events but not completed.
*
* @return Returns observable of value.
*/
@NonNull
public Observable<TObject> observe() { public Observable<TObject> observe() {
return valueObservable; return observeOptionalValue().map(Optional::getValue);
}
/**
* Returns Observable which is emitting only one item on subscribe. //TODO: it's Single :(
* It could emit next and error events but not completed.
*
* @return Returns observable of value.
*/
@NonNull
public Observable<TObject> get() {
return valueObservable.take(1);
}
/**
* Gets value synchronously. You should NOT use this method normally. Use {@link #get()} or {@link #observe()} asynchronously instead.
*
* @return Returns value;
*/
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
@Nullable
public TObject getSync() {
return get().toBlocking().first();
}
/**
* Enum that is representing strategy of observing item from store.
*/
public enum ObserveStrategy {
/**
* Not caching value so on every {@link #get()} emit it will get value from {@link #getStore()} and converts it with {@link #getConverter()}.
*/
NO_CACHE,
/**
* Caching only store value so on every {@link #get()} emit it will converts it with {@link #getConverter()}.
* Do not use such strategy if store object could be big (like byte-array of file).
*/
CACHE_STORE_VALUE,
/**
* Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}.
* But it will take time for getting value from {@link #getStore()} to set value.
* Do not use such strategy if object could be big (like Bitmap or long string).
* Do not use such strategy if object is mutable because multiple subscribers could then change it's state.
*/
CACHE_ACTUAL_VALUE,
/**
* Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}.
* It won't take time or getting value from {@link #getStore()} to set value.
* Do not use such strategy if store object could be big (like byte-array of file).
* Do not use such strategy if object could be big (like Bitmap or long string).
* Do not use such strategy if object is mutable because multiple subscribers could then change it's state.
*/
CACHE_STORE_AND_ACTUAL_VALUE
}
/**
* Helper class to create various builders.
*
* @param <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
public static class BuilderCore<TKey, TObject, TStoreObject> {
@NonNull
protected final TKey key;
@NonNull
protected final Type objectType;
@NonNull
private final Type storeObjectType;
@NonNull
private final Store<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
@Nullable
private ObserveStrategy observeStrategy;
@Nullable
private Migration<TKey> migration;
@Nullable
private TObject defaultValue;
@Nullable
private Scheduler storeScheduler;
private long cacheTimeMillis;
protected BuilderCore(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter) {
this(key, objectType, storeObjectType, store, converter, null, null, null, null, DEFAULT_CACHE_TIME_MILLIS);
}
protected BuilderCore(@NonNull final BuilderCore<TKey, TObject, TStoreObject> sourceBuilder) {
this(sourceBuilder.key, sourceBuilder.objectType, sourceBuilder.storeObjectType,
sourceBuilder.store, sourceBuilder.converter, sourceBuilder.observeStrategy,
sourceBuilder.migration, sourceBuilder.defaultValue, sourceBuilder.storeScheduler, sourceBuilder.cacheTimeMillis);
}
@SuppressWarnings({"PMD.ExcessiveParameterList", "CPD-START"})
//CPD: it is same code as constructor of Storable
//ExcessiveParameterList: that's why we are using builder to create it
private BuilderCore(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler,
final long cacheTimeMillis) {
this.key = key;
this.objectType = objectType;
this.storeObjectType = storeObjectType;
this.store = store;
this.converter = converter;
this.observeStrategy = observeStrategy;
this.migration = migration;
this.defaultValue = defaultValue;
this.storeScheduler = storeScheduler;
this.cacheTimeMillis = cacheTimeMillis;
}
@SuppressWarnings("CPD-END")
protected void setStoreSchedulerInternal(@Nullable final Scheduler storeScheduler) {
this.storeScheduler = storeScheduler;
}
protected void setObserveStrategyInternal(@Nullable final ObserveStrategy observeStrategy) {
this.observeStrategy = observeStrategy;
}
protected void setMigrationInternal(@NonNull final Migration<TKey> migration) {
this.migration = migration;
}
protected void setCacheTimeInternal(final long cacheTime, @NonNull final TimeUnit timeUnit) {
this.cacheTimeMillis = timeUnit.toMillis(cacheTime);
}
@Nullable
protected TObject getDefaultValue() {
return defaultValue;
}
protected void setDefaultValueInternal(@NonNull final TObject defaultValue) {
this.defaultValue = defaultValue;
}
} }
/** /**
@ -540,8 +130,8 @@ public class Storable<TKey, TObject, TStoreObject> {
* @return Builder that allows to specify other fields. * @return Builder that allows to specify other fields.
*/ */
@NonNull @NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setDefaultValue(@NonNull final TObject defaultValue) { public NonNullStorable.Builder<TKey, TObject, TStoreObject> setDefaultValue(@NonNull final TObject defaultValue) {
return new NonNullStorableBuilder<>(this, defaultValue); return new NonNullStorable.Builder<>(this, defaultValue);
} }
/** /**

View File

@ -24,6 +24,7 @@ import android.support.annotation.Nullable;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Completable; import rx.Completable;
import rx.Single; import rx.Single;
@ -64,6 +65,6 @@ public interface Store<TKey, TStoreObject> {
* @return Object from store found by key; * @return Object from store found by key;
*/ */
@NonNull @NonNull
Single<TStoreObject> loadObject(@NonNull Type storeObjectType, @NonNull TKey key); Single<Optional<TStoreObject>> loadObject(@NonNull Type storeObjectType, @NonNull TKey key);
} }

View File

@ -1,113 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.storable.builders;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.observables.storable.Migration;
import ru.touchin.roboswag.core.observables.storable.Storable;
import ru.touchin.roboswag.core.observables.storable.concrete.NonNullStorable;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Scheduler;
/**
* Created by Gavriil Sitnikov on 15/05/2016.
* Builder that is already contains not null default value.
*
* @param <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
public class NonNullStorableBuilder<TKey, TObject, TStoreObject> extends Storable.BuilderCore<TKey, TObject, TStoreObject> {
public NonNullStorableBuilder(@NonNull final Storable.Builder<TKey, TObject, TStoreObject> sourceBuilder,
@NonNull final TObject defaultValue) {
super(sourceBuilder);
setDefaultValueInternal(defaultValue);
}
/**
* Sets specific {@link Scheduler} to store/load/convert values on it.
*
* @param storeScheduler Scheduler;
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setStoreScheduler(@Nullable final Scheduler storeScheduler) {
setStoreSchedulerInternal(storeScheduler);
return this;
}
/**
* Sets specific {@link Storable.ObserveStrategy} to cache value in memory in specific way.
*
* @param observeStrategy ObserveStrategy;
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setObserveStrategy(@Nullable final Storable.ObserveStrategy observeStrategy) {
setObserveStrategyInternal(observeStrategy);
return this;
}
/**
* Sets cache time for while value that cached by {@link #setObserveStrategy(Storable.ObserveStrategy)}
* will be in memory after everyone unsubscribe.
* It is important for example for cases when user switches between screens and hide/open app very fast.
*
* @param cacheTime Cache time value;
* @param timeUnit Cache time units.
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) {
setCacheTimeInternal(cacheTime, timeUnit);
return this;
}
/**
* Sets specific {@link Migration} to migrate values from specific version to latest version.
*
* @param migration Migration;
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setMigration(@NonNull final Migration<TKey> migration) {
setMigrationInternal(migration);
return this;
}
/**
* Building {@link NonNullStorable} object.
*
* @return New {@link NonNullStorable}.
*/
@NonNull
public NonNullStorable<TKey, TObject, TStoreObject> build() {
if (getDefaultValue() == null) {
throw new ShouldNotHappenException();
}
return new NonNullStorable<>(this);
}
}

View File

@ -20,10 +20,16 @@
package ru.touchin.roboswag.core.observables.storable.concrete; package ru.touchin.roboswag.core.observables.storable.concrete;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.observables.storable.BaseStorable;
import ru.touchin.roboswag.core.observables.storable.Migration;
import ru.touchin.roboswag.core.observables.storable.Storable; import ru.touchin.roboswag.core.observables.storable.Storable;
import ru.touchin.roboswag.core.observables.storable.builders.NonNullStorableBuilder;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException; import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
import rx.Scheduler;
/** /**
* Created by Gavriil Sitnikov on 04/10/2015. * Created by Gavriil Sitnikov on 04/10/2015.
@ -34,20 +40,109 @@ import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
* @param <TObject> Type of actual object; * @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}. * @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/ */
public class NonNullStorable<TKey, TObject, TStoreObject> extends Storable<TKey, TObject, TStoreObject> { public class NonNullStorable<TKey, TObject, TStoreObject> extends BaseStorable<TKey, TObject, TStoreObject, TObject> {
public NonNullStorable(@NonNull final NonNullStorableBuilder<TKey, TObject, TStoreObject> builderCore) { public NonNullStorable(@NonNull final Builder<TKey, TObject, TStoreObject> builderCore) {
super(builderCore); super(builderCore);
} }
@NonNull @NonNull
@Override @Override
public TObject getSync() { public Observable<TObject> observe() {
final TObject result = super.getSync(); return observeOptionalValue()
if (result == null) { .map(optional -> {
throw new ShouldNotHappenException(); if (optional.getValue() == null) {
} throw new ShouldNotHappenException();
return result; }
return optional.getValue();
});
} }
/**
* Created by Gavriil Sitnikov on 15/05/2016.
* Builder that is already contains not null default value.
*
* @param <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
@SuppressWarnings("CPD-START")
//CPD: it is same code as Builder of Storable because it's methods returning this and can't be inherited
public static class Builder<TKey, TObject, TStoreObject> extends BuilderCore<TKey, TObject, TStoreObject> {
public Builder(@NonNull final Storable.Builder<TKey, TObject, TStoreObject> sourceBuilder,
@NonNull final TObject defaultValue) {
super(sourceBuilder);
if (defaultValue == null) {
throw new ShouldNotHappenException();
}
setDefaultValueInternal(defaultValue);
}
/**
* Sets specific {@link Scheduler} to store/load/convert values on it.
*
* @param storeScheduler Scheduler;
* @return Builder that allows to specify other fields.
*/
@NonNull
public Builder<TKey, TObject, TStoreObject> setStoreScheduler(@Nullable final Scheduler storeScheduler) {
setStoreSchedulerInternal(storeScheduler);
return this;
}
/**
* Sets specific {@link ObserveStrategy} to cache value in memory in specific way.
*
* @param observeStrategy ObserveStrategy;
* @return Builder that allows to specify other fields.
*/
@NonNull
public Builder<TKey, TObject, TStoreObject> setObserveStrategy(@Nullable final ObserveStrategy observeStrategy) {
setObserveStrategyInternal(observeStrategy);
return this;
}
/**
* Sets cache time for while value that cached by {@link #setObserveStrategy(ObserveStrategy)}
* will be in memory after everyone unsubscribe.
* It is important for example for cases when user switches between screens and hide/open app very fast.
*
* @param cacheTime Cache time value;
* @param timeUnit Cache time units.
* @return Builder that allows to specify other fields.
*/
@NonNull
public Builder<TKey, TObject, TStoreObject> setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) {
setCacheTimeInternal(cacheTime, timeUnit);
return this;
}
/**
* Sets specific {@link Migration} to migrate values from specific version to latest version.
*
* @param migration Migration;
* @return Builder that allows to specify other fields.
*/
@NonNull
public Builder<TKey, TObject, TStoreObject> setMigration(@NonNull final Migration<TKey> migration) {
setMigrationInternal(migration);
return this;
}
/**
* Building {@link NonNullStorable} object.
*
* @return New {@link NonNullStorable}.
*/
@NonNull
@SuppressWarnings("CPD-END")
public NonNullStorable<TKey, TObject, TStoreObject> build() {
if (getDefaultValue() == null) {
throw new ShouldNotHappenException();
}
return new NonNullStorable<>(this);
}
}
} }

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.utils;
import android.support.annotation.Nullable;
import java.io.Serializable;
/**
* Created by Gavriil Sitnikov on 16/04/2017.
* Holds nullable objects inside. It is needed to implement RxJava2 non-null emitting logic.
*
* @param <T> Type of object.
*/
public class Optional<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Nullable
private final T value;
public Optional(@Nullable final T value) {
this.value = value;
}
/**
* Returns holding nullable object.
*
* @return Holding object.
*/
@Nullable
public T getValue() {
return value;
}
@Override
public boolean equals(@Nullable final Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
final Optional<?> that = (Optional<?>) object;
return ObjectUtils.equals(value, that.value);
}
@Override
public int hashCode() {
return value != null ? value.hashCode() : 0;
}
}

View File

@ -22,6 +22,7 @@ package ru.touchin.roboswag.core.utils;
import android.app.Service; import android.app.Service;
import android.os.Binder; import android.os.Binder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/** /**
* Created by Gavriil Sitnikov on 03/10/2015. * Created by Gavriil Sitnikov on 03/10/2015.
@ -47,4 +48,23 @@ public class ServiceBinder<TService extends Service> extends Binder {
return service; return service;
} }
@Override
public boolean equals(@Nullable final Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
final ServiceBinder that = (ServiceBinder) object;
return ObjectUtils.equals(service, that.service);
}
@Override
public int hashCode() {
return service.hashCode();
}
} }

View File

@ -21,8 +21,6 @@ package ru.touchin.roboswag.core.utils;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import rx.functions.Func0;
/** /**
* Created by Gavriil Sitnikov on 13/11/2015. * Created by Gavriil Sitnikov on 13/11/2015.
* Thread local value with specified creator of value per thread. * Thread local value with specified creator of value per thread.
@ -30,24 +28,33 @@ import rx.functions.Func0;
public class ThreadLocalValue<T> extends ThreadLocal<T> { public class ThreadLocalValue<T> extends ThreadLocal<T> {
@NonNull @NonNull
private final Func0<T> creator; private final Fabric<T> fabric;
public ThreadLocalValue(@NonNull final NonNullFunc<T> creator) { public ThreadLocalValue(@NonNull final Fabric<T> fabric) {
super(); super();
this.creator = creator; this.fabric = fabric;
} }
@NonNull @NonNull
@Override @Override
protected T initialValue() { protected T initialValue() {
return creator.call(); return fabric.create();
} }
public interface NonNullFunc<T> extends Func0<T> { /**
* Fabric of thread-local objects.
*
* @param <T> Type of objects.
*/
public interface Fabric<T> {
/**
* Creates object.
*
* @return new instance of object.
*/
@NonNull @NonNull
@Override T create();
T call();
} }

View File

@ -81,9 +81,7 @@ public class HalfNullablePair<TFirst, TSecond> implements Serializable {
@Override @Override
public int hashCode() { public int hashCode() {
int result = first.hashCode(); return ObjectUtils.hashCode(first, second);
result = 31 * result + (second != null ? second.hashCode() : 0);
return result;
} }
} }

View File

@ -81,9 +81,7 @@ public class NonNullPair<TFirst, TSecond> implements Serializable {
@Override @Override
public int hashCode() { public int hashCode() {
int result = first.hashCode(); return ObjectUtils.hashCode(first, second);
result = 31 * result + second.hashCode();
return result;
} }
} }

View File

@ -85,9 +85,7 @@ public class NullablePair<TFirst, TSecond> implements Serializable { //todo: mb
@Override @Override
public int hashCode() { public int hashCode() {
int result = first.hashCode(); return ObjectUtils.hashCode(first, second);
result = 31 * result + second.hashCode();
return result;
} }
} }