From 1ca1bbe42c4908bfd6a3f6a78e69d128a9d1023d Mon Sep 17 00:00:00 2001 From: M66B Date: Thu, 9 Mar 2023 07:43:39 +0100 Subject: [PATCH] Updated AndroidX lifecycle --- app/build.gradle | 2 +- .../lifecycle/ComputableLiveData.java | 163 --------------- .../androidx/lifecycle/ComputableLiveData.kt | 131 ++++++++++++ .../java/androidx/lifecycle/LiveData.java | 17 +- .../androidx/lifecycle/MediatorLiveData.java | 34 ++- .../lifecycle/{Observer.java => Observer.kt} | 19 +- .../androidx/lifecycle/Transformations.java | 194 ----------------- .../androidx/lifecycle/Transformations.kt | 196 ++++++++++++++++++ patches/ComputableLiveData.patch | 60 +++--- 9 files changed, 408 insertions(+), 408 deletions(-) delete mode 100644 app/src/main/java/androidx/lifecycle/ComputableLiveData.java create mode 100644 app/src/main/java/androidx/lifecycle/ComputableLiveData.kt rename app/src/main/java/androidx/lifecycle/{Observer.java => Observer.kt} (72%) delete mode 100644 app/src/main/java/androidx/lifecycle/Transformations.java create mode 100644 app/src/main/java/androidx/lifecycle/Transformations.kt diff --git a/app/build.gradle b/app/build.gradle index 4c8a9f6bb6..ef92c4eeab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -427,7 +427,7 @@ dependencies { def lbm_version = "1.1.0" def swiperefresh_version = "1.2.0-alpha01" def documentfile_version = "1.1.0-alpha01" - def lifecycle_version = "2.5.1" // 2.6.0 + def lifecycle_version = "2.6.0" def lifecycle_extensions_version = "2.2.0" def room_version = "2.4.3" // 2.5.0 def sqlite_version = "2.3.0" diff --git a/app/src/main/java/androidx/lifecycle/ComputableLiveData.java b/app/src/main/java/androidx/lifecycle/ComputableLiveData.java deleted file mode 100644 index 290d9bad8b..0000000000 --- a/app/src/main/java/androidx/lifecycle/ComputableLiveData.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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 androidx.lifecycle; - -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; -import androidx.arch.core.executor.ArchTaskExecutor; - -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A LiveData class that can be invalidated & computed when there are active observers. - *

- * It can be invalidated via {@link #invalidate()}, which will result in a call to - * {@link #compute()} if there are active observers (or when they start observing) - *

- * This is an internal class for now, might be public if we see the necessity. - * - * @param The type of the live data - * @hide internal - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -public abstract class ComputableLiveData { - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final Executor mExecutor; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final LiveData mLiveData; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final AtomicBoolean mInvalid = new AtomicBoolean(true); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final AtomicBoolean mComputing = new AtomicBoolean(false); - - /** - * Creates a computable live data that computes values on the arch IO thread executor. - */ - @SuppressWarnings("WeakerAccess") - public ComputableLiveData() { - this(ArchTaskExecutor.getIOThreadExecutor()); - } - - /** - * Creates a computable live data that computes values on the specified executor. - * - * @param executor Executor that is used to compute new LiveData values. - */ - @SuppressWarnings("WeakerAccess") - public ComputableLiveData(@NonNull Executor executor) { - mExecutor = executor; - mLiveData = new LiveData() { - @Override - protected void onActive() { - mExecutor.execute(mRefreshRunnable); - } - }; - } - - /** - * Returns the LiveData managed by this class. - * - * @return A LiveData that is controlled by ComputableLiveData. - */ - @SuppressWarnings("WeakerAccess") - @NonNull - public LiveData getLiveData() { - return mLiveData; - } - - @VisibleForTesting - final Runnable mRefreshRunnable = new Runnable() { - @WorkerThread - @Override - public void run() { - boolean computed; - do { - computed = false; - // compute can happen only in 1 thread but no reason to lock others. - if (mComputing.compareAndSet(false, true)) { - // as long as it is invalid, keep computing. - try { - T value = null; - boolean once = true; - long last = android.os.SystemClock.elapsedRealtime(); - while (mInvalid.compareAndSet(true, false)) { - long now = android.os.SystemClock.elapsedRealtime(); - if (value != null && (once || last + 2500 < now)) { - eu.faircode.email.Log.i(mLiveData + " post once=" + once + " age=" + (now - last) + " ms"); - once = false; - last = now; - mLiveData.postValue(value); - } - computed = true; - value = compute(); - } - if (computed) { - mLiveData.postValue(value); - } - } catch (Throwable ex) { - // java.lang.IllegalStateException: Couldn't read row xxx column yyy - eu.faircode.email.Log.e(ex); - mInvalid.set(true); - } finally { - // release compute lock - mComputing.set(false); - } - } - // check invalid after releasing compute lock to avoid the following scenario. - // Thread A runs compute() - // Thread A checks invalid, it is false - // Main thread sets invalid to true - // Thread B runs, fails to acquire compute lock and skips - // Thread A releases compute lock - // We've left invalid in set state. The check below recovers. - } while (computed && mInvalid.get()); - } - }; - - // invalidation check always happens on the main thread - @VisibleForTesting - final Runnable mInvalidationRunnable = new Runnable() { - @MainThread - @Override - public void run() { - boolean isActive = mLiveData.hasActiveObservers(); - if (mInvalid.compareAndSet(false, true)) { - if (isActive) { - mExecutor.execute(mRefreshRunnable); - } - } - } - }; - - /** - * Invalidates the LiveData. - *

- * When there are active observers, this will trigger a call to {@link #compute()}. - */ - public void invalidate() { - ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); - } - - // TODO https://issuetracker.google.com/issues/112197238 - @SuppressWarnings({"WeakerAccess", "UnknownNullness"}) - @WorkerThread - protected abstract T compute(); -} diff --git a/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt b/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt new file mode 100644 index 0000000000..0278c2cae0 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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 androidx.lifecycle + +import androidx.annotation.RestrictTo +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import androidx.arch.core.executor.ArchTaskExecutor +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A LiveData class that can be invalidated & computed when there are active observers. + * + * It can be invalidated via [invalidate], which will result in a call to + * [compute] if there are active observers (or when they start observing) + * + * This is an internal class for now, might be public if we see the necessity. + * + * @param The type of the live data + * @hide internal +*/ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +abstract class ComputableLiveData @JvmOverloads +/** + * Creates a computable live data that computes values on the specified executor + * or the arch IO thread executor by default. + * + * @param executor Executor that is used to compute new LiveData values. + */ +constructor( + internal val executor: Executor = ArchTaskExecutor.getIOThreadExecutor() +) { + + private val _liveData: LiveData = + object : LiveData() { + override fun onActive() { + executor.execute(refreshRunnable) + } + } + /** + * The LiveData managed by this class. + */ + open val liveData: LiveData = _liveData + internal val invalid = AtomicBoolean(true) + internal val computing = AtomicBoolean(false) + + @JvmField + @VisibleForTesting + internal val refreshRunnable = Runnable { + var computed: Boolean + do { + computed = false + // compute can happen only in 1 thread but no reason to lock others. + if (computing.compareAndSet(false, true)) { + // as long as it is invalid, keep computing. + try { + var value: T? = null + var once = true; + var last = android.os.SystemClock.elapsedRealtime() + while (invalid.compareAndSet(true, false)) { + var now = android.os.SystemClock.elapsedRealtime() + if (value != null && (once || last + 2500 < now)) { + eu.faircode.email.Log.i(liveData.toString() + " post once=" + once + " age=" + (now - last) + " ms") + once = false; + last = now; + liveData.postValue(value); + } + computed = true + value = compute() + } + if (computed) { + liveData.postValue(value) + } + } catch (ex: Throwable) { + // java.lang.IllegalStateException: Couldn't read row xxx column yyy + eu.faircode.email.Log.e(ex); + invalid.set(true); + } finally { + // release compute lock + computing.set(false) + } + } + // check invalid after releasing compute lock to avoid the following scenario. + // Thread A runs compute() + // Thread A checks invalid, it is false + // Main thread sets invalid to true + // Thread B runs, fails to acquire compute lock and skips + // Thread A releases compute lock + // We've left invalid in set state. The check below recovers. + } while (computed && invalid.get()) + } + + // invalidation check always happens on the main thread + @JvmField + @VisibleForTesting + internal val invalidationRunnable = Runnable { + val isActive = liveData.hasActiveObservers() + if (invalid.compareAndSet(false, true)) { + if (isActive) { + executor.execute(refreshRunnable) + } + } + } + + /** + * Invalidates the LiveData. + * + * When there are active observers, this will trigger a call to [.compute]. + */ + open fun invalidate() { + ArchTaskExecutor.getInstance().executeOnMainThread(invalidationRunnable) + } + + // TODO https://issuetracker.google.com/issues/112197238 + @WorkerThread + protected abstract fun compute(): T +} \ No newline at end of file diff --git a/app/src/main/java/androidx/lifecycle/LiveData.java b/app/src/main/java/androidx/lifecycle/LiveData.java index a694fcd9c8..10b9fac733 100644 --- a/app/src/main/java/androidx/lifecycle/LiveData.java +++ b/app/src/main/java/androidx/lifecycle/LiveData.java @@ -311,10 +311,11 @@ public abstract class LiveData { /** * Returns the current value. + *

* Note that calling this method on a background thread does not guarantee that the latest * value set will be received. * - * @return the current value + * @return the current value or null if {@link #isInitialized()} is false */ @SuppressWarnings("unchecked") @Nullable @@ -326,6 +327,20 @@ public abstract class LiveData { return null; } + /** + * Returns whether an explicit value has been set on this LiveData. If this returns + * true, then the current value can be retrieved from {@link #getValue()}. + *

+ * Note that calling this method on a background thread may still result in this method + * returning false even if a call to {@link #postValue(Object)} is being + * processed. + * + * @return whether an explicit value has been set on this LiveData + */ + public boolean isInitialized() { + return mData != NOT_SET; + } + int getVersion() { return mVersion; } diff --git a/app/src/main/java/androidx/lifecycle/MediatorLiveData.java b/app/src/main/java/androidx/lifecycle/MediatorLiveData.java index f9fc4f2ecf..1fe6ced11c 100644 --- a/app/src/main/java/androidx/lifecycle/MediatorLiveData.java +++ b/app/src/main/java/androidx/lifecycle/MediatorLiveData.java @@ -38,25 +38,25 @@ import java.util.Map; * is called for either of them, we set a new value in {@code liveDataMerger}. * *

- * LiveData liveData1 = ...;
- * LiveData liveData2 = ...;
+ * LiveData<Integer> liveData1 = ...;
+ * LiveData<Integer> liveData2 = ...;
  *
- * MediatorLiveData liveDataMerger = new MediatorLiveData<>();
- * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
- * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
+ * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
+ * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
+ * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
  * 
*

* Let's consider that we only want 10 values emitted by {@code liveData1}, to be * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code * liveData1} and remove it as a source. *

- * liveDataMerger.addSource(liveData1, new Observer() {
+ * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
  *      private int count = 1;
  *
  *      {@literal @}Override public void onChanged(@Nullable Integer s) {
  *          count++;
  *          liveDataMerger.setValue(s);
- *          if (count > 10) {
+ *          if (count > 10) {
  *              liveDataMerger.removeSource(liveData1);
  *          }
  *      }
@@ -70,8 +70,24 @@ public class MediatorLiveData extends MutableLiveData {
     private SafeIterableMap, Source> mSources = new SafeIterableMap<>();
 
     /**
-     * Starts to listen the given {@code source} LiveData, {@code onChanged} observer will be called
-     * when {@code source} value was changed.
+     * Creates a MediatorLiveData with no value assigned to it.
+     */
+    public MediatorLiveData() {
+        super();
+    }
+
+    /**
+     * Creates a MediatorLiveData initialized with the given {@code value}.
+     *
+     * @param value initial value
+     */
+    public MediatorLiveData(T value) {
+        super(value);
+    }
+
+    /**
+     * Starts to listen to the given {@code source} LiveData, {@code onChanged} observer will be
+     * called when {@code source} value was changed.
      * 

* {@code onChanged} callback will be called only when this {@code MediatorLiveData} is active. *

If the given LiveData is already added as a source but with a different Observer, diff --git a/app/src/main/java/androidx/lifecycle/Observer.java b/app/src/main/java/androidx/lifecycle/Observer.kt similarity index 72% rename from app/src/main/java/androidx/lifecycle/Observer.java rename to app/src/main/java/androidx/lifecycle/Observer.kt index 30afeedf07..2df6295d87 100644 --- a/app/src/main/java/androidx/lifecycle/Observer.java +++ b/app/src/main/java/androidx/lifecycle/Observer.kt @@ -13,20 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package androidx.lifecycle; +package androidx.lifecycle /** - * A simple callback that can receive from {@link LiveData}. - * - * @param The type of the parameter + * A simple callback that can receive from [LiveData]. * * @see LiveData LiveData - for a usage description. - */ -public interface Observer { +*/ +fun interface Observer { + /** - * Called when the data is changed. - * @param t The new data + * Called when the data is changed is changed to [value]. */ - void onChanged(T t); -} + fun onChanged(value: T) +} \ No newline at end of file diff --git a/app/src/main/java/androidx/lifecycle/Transformations.java b/app/src/main/java/androidx/lifecycle/Transformations.java deleted file mode 100644 index f687e65c8d..0000000000 --- a/app/src/main/java/androidx/lifecycle/Transformations.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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 androidx.lifecycle; - -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.arch.core.util.Function; - -/** - * Transformation methods for {@link LiveData}. - *

- * These methods permit functional composition and delegation of {@link LiveData} instances. The - * transformations are calculated lazily, and will run only when the returned {@link LiveData} is - * observed. Lifecycle behavior is propagated from the input {@code source} {@link LiveData} to the - * returned one. - */ -@SuppressWarnings("WeakerAccess") -public class Transformations { - - private Transformations() { - } - - /** - * Returns a {@code LiveData} mapped from the input {@code source} {@code LiveData} by applying - * {@code mapFunction} to each value set on {@code source}. - *

- * This method is analogous to {@link io.reactivex.Observable#map}. - *

- * {@code transform} will be executed on the main thread. - *

- * Here is an example mapping a simple {@code User} struct in a {@code LiveData} to a - * {@code LiveData} containing their full name as a {@code String}. - * - *

-     * LiveData<User> userLiveData = ...;
-     * LiveData<String> userFullNameLiveData =
-     *     Transformations.map(
-     *         userLiveData,
-     *         user -> user.firstName + user.lastName);
-     * });
-     * 
- * - * @param source the {@code LiveData} to map from - * @param mapFunction a function to apply to each value set on {@code source} in order to set - * it - * on the output {@code LiveData} - * @param the generic type parameter of {@code source} - * @param the generic type parameter of the returned {@code LiveData} - * @return a LiveData mapped from {@code source} to type {@code } by applying - * {@code mapFunction} to each value set. - */ - @MainThread - @NonNull - public static LiveData map( - @NonNull LiveData source, - @NonNull final Function mapFunction) { - final MediatorLiveData result = new MediatorLiveData<>(); - result.addSource(source, new Observer() { - @Override - public void onChanged(@Nullable X x) { - result.setValue(mapFunction.apply(x)); - } - }); - return result; - } - - /** - * Returns a {@code LiveData} mapped from the input {@code source} {@code LiveData} by applying - * {@code switchMapFunction} to each value set on {@code source}. - *

- * The returned {@code LiveData} delegates to the most recent {@code LiveData} created by - * calling {@code switchMapFunction} with the most recent value set to {@code source}, without - * changing the reference. In this way, {@code switchMapFunction} can change the 'backing' - * {@code LiveData} transparently to any observer registered to the {@code LiveData} returned - * by {@code switchMap()}. - *

- * Note that when the backing {@code LiveData} is switched, no further values from the older - * {@code LiveData} will be set to the output {@code LiveData}. In this way, the method is - * analogous to {@link io.reactivex.Observable#switchMap}. - *

- * {@code switchMapFunction} will be executed on the main thread. - *

- * Here is an example class that holds a typed-in name of a user - * {@code String} (such as from an {@code EditText}) in a {@link MutableLiveData} and - * returns a {@code LiveData} containing a List of {@code User} objects for users that have - * that name. It populates that {@code LiveData} by requerying a repository-pattern object - * each time the typed name changes. - *

- * This {@code ViewModel} would permit the observing UI to update "live" as the user ID text - * changes. - * - *

-     * class UserViewModel extends AndroidViewModel {
-     *     MutableLiveData<String> nameQueryLiveData = ...
-     *
-     *     LiveData<List<String>> getUsersWithNameLiveData() {
-     *         return Transformations.switchMap(
-     *             nameQueryLiveData,
-     *                 name -> myDataSource.getUsersWithNameLiveData(name));
-     *     }
-     *
-     *     void setNameQuery(String name) {
-     *         this.nameQueryLiveData.setValue(name);
-     *     }
-     * }
-     * 
- * - * @param source the {@code LiveData} to map from - * @param switchMapFunction a function to apply to each value set on {@code source} to create a - * new delegate {@code LiveData} for the returned one - * @param the generic type parameter of {@code source} - * @param the generic type parameter of the returned {@code LiveData} - * @return a LiveData mapped from {@code source} to type {@code } by delegating - * to the LiveData returned by applying {@code switchMapFunction} to each - * value set - */ - @MainThread - @NonNull - public static LiveData switchMap( - @NonNull LiveData source, - @NonNull final Function> switchMapFunction) { - final MediatorLiveData result = new MediatorLiveData<>(); - result.addSource(source, new Observer() { - LiveData mSource; - - @Override - public void onChanged(@Nullable X x) { - LiveData newLiveData = switchMapFunction.apply(x); - if (mSource == newLiveData) { - return; - } - if (mSource != null) { - result.removeSource(mSource); - } - mSource = newLiveData; - if (mSource != null) { - result.addSource(mSource, new Observer() { - @Override - public void onChanged(@Nullable Y y) { - result.setValue(y); - } - }); - } - } - }); - return result; - } - - /** - * Creates a new {@link LiveData} object that does not emit a value until the source LiveData - * value has been changed. The value is considered changed if {@code equals()} yields - * {@code false}. - * - * @param source the input {@link LiveData} - * @param the generic type parameter of {@code source} - * @return a new {@link LiveData} of type {@code X} - */ - @MainThread - @NonNull - public static LiveData distinctUntilChanged(@NonNull LiveData source) { - final MediatorLiveData outputLiveData = new MediatorLiveData<>(); - outputLiveData.addSource(source, new Observer() { - - boolean mFirstTime = true; - - @Override - public void onChanged(X currentValue) { - final X previousValue = outputLiveData.getValue(); - if (mFirstTime - || (previousValue == null && currentValue != null) - || (previousValue != null && !previousValue.equals(currentValue))) { - mFirstTime = false; - outputLiveData.setValue(currentValue); - } - } - }); - return outputLiveData; - } -} diff --git a/app/src/main/java/androidx/lifecycle/Transformations.kt b/app/src/main/java/androidx/lifecycle/Transformations.kt new file mode 100644 index 0000000000..c3b9ed6894 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/Transformations.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ +@file:JvmName("Transformations") + +package androidx.lifecycle + +import androidx.annotation.CheckResult +import androidx.annotation.MainThread +import androidx.arch.core.util.Function + +/** + * Returns a [LiveData] mapped from `this` LiveData by applying [transform] to each value set on + * `this` LiveData. + * + * This method is analogous to [io.reactivex.Observable.map]. + * + * [transform] will be executed on the main thread. + * + * Here is an example mapping a simple `User` struct in a `LiveData` to a + * `LiveData` containing their full name as a `String`. + * + * ``` + * val userLD : LiveData = ...; + * val userFullNameLD: LiveData = userLD.map { user -> user.firstName + user.lastName } + * ``` + * + * @param transform a function to apply to each value set on `source` in order to set + * it on the output `LiveData` + * @return a LiveData mapped from `source` to type `` by applying + * `mapFunction` to each value set. + */ +@JvmName("map") +@MainThread +@CheckResult +fun LiveData.map( + transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards Y) +): LiveData { + val result = MediatorLiveData() + result.addSource(this) { x -> result.value = transform(x) } + return result +} + +@Deprecated( + "Use kotlin functions, instead of outdated arch core Functions", + level = DeprecationLevel.HIDDEN +) +@JvmName("map") +@MainThread +@CheckResult +fun LiveData.map(mapFunction: Function): LiveData { + val result = MediatorLiveData() + result.addSource(this) { x -> result.value = mapFunction.apply(x) } + return result +} + +/** + * Returns a [LiveData] mapped from the input `this` `LiveData` by applying + * [transform] to each value set on `this`. + *

+ * The returned `LiveData` delegates to the most recent `LiveData` created by + * [transform] with the most recent value set to `this`, without + * changing the reference. In this way [transform] can change the 'backing' + * `LiveData` transparently to any observer registered to the `LiveData` returned + * by `switchMap()`. + * + * Note that when the backing `LiveData` is switched, no further values from the older + * `LiveData` will be set to the output `LiveData`. In this way, the method is + * analogous to [io.reactivex.Observable.switchMap]. + * + * [transform] will be executed on the main thread. + * + * Here is an example class that holds a typed-in name of a user + * `String` (such as from an `EditText`) in a [MutableLiveData] and + * returns a `LiveData` containing a List of `User` objects for users that have + * that name. It populates that `LiveData` by requerying a repository-pattern object + * each time the typed name changes. + *

+ * This `ViewModel` would permit the observing UI to update "live" as the user ID text + * changes. + * + * ``` + * class UserViewModel: AndroidViewModel { + * val nameQueryLiveData : MutableLiveData = ... + * + * fun usersWithNameLiveData(): LiveData> = nameQueryLiveData.switchMap { + * name -> myDataSource.usersWithNameLiveData(name) + * } + * + * fun setNameQuery(val name: String) { + * this.nameQueryLiveData.value = name; + * } + * } + * ``` + * + * @param transform a function to apply to each value set on `source` to create a + * new delegate `LiveData` for the returned one + * @return a LiveData mapped from `source` to type `` by delegating to the LiveData + * returned by applying `switchMapFunction` to each value set + */ +@JvmName("switchMap") +@MainThread +@CheckResult +fun LiveData.switchMap( + transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards LiveData)? +): LiveData { + val result = MediatorLiveData() + result.addSource(this, object : Observer { + var liveData: LiveData? = null + + override fun onChanged(value: X) { + val newLiveData = transform(value) + if (liveData === newLiveData) { + return + } + if (liveData != null) { + result.removeSource(liveData!!) + } + liveData = newLiveData + if (liveData != null) { + result.addSource(liveData!!) { y -> result.setValue(y) } + } + } + }) + return result +} + +@Deprecated( + "Use kotlin functions, instead of outdated arch core Functions", + level = DeprecationLevel.HIDDEN +) +@JvmName("switchMap") +@MainThread +@CheckResult +fun LiveData.switchMap(switchMapFunction: Function>): LiveData { + val result = MediatorLiveData() + result.addSource(this, object : Observer { + var liveData: LiveData? = null + + override fun onChanged(value: X) { + val newLiveData = switchMapFunction.apply(value) + if (liveData === newLiveData) { + return + } + if (liveData != null) { + result.removeSource(liveData!!) + } + liveData = newLiveData + if (liveData != null) { + result.addSource(liveData!!) { y -> result.setValue(y) } + } + } + }) + return result +} + +/** + * Creates a new [LiveData] object does not emit a value until the source `this` LiveData value + * has been changed. The value is considered changed if `equals()` yields `false`. + * + * @return a new [LiveData] of type `X` + */ +@JvmName("distinctUntilChanged") +@MainThread +@CheckResult +fun LiveData.distinctUntilChanged(): LiveData { + val outputLiveData = MediatorLiveData() + var firstTime = true + if (isInitialized) { + outputLiveData.value = value + firstTime = false + } + outputLiveData.addSource(this) { value -> + val previousValue = outputLiveData.value + if (firstTime || + previousValue == null && value != null || + previousValue != null && previousValue != value + ) { + firstTime = false + outputLiveData.value = value + } + } + return outputLiveData +} \ No newline at end of file diff --git a/patches/ComputableLiveData.patch b/patches/ComputableLiveData.patch index 1ca6c324c4..8d56d192e9 100644 --- a/patches/ComputableLiveData.patch +++ b/patches/ComputableLiveData.patch @@ -1,29 +1,31 @@ ---- /home/marcel/support/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/ComputableLiveData.java 2020-03-23 17:03:45.426122318 +0100 -+++ /home/marcel/email/app/src/main/java/androidx/lifecycle/ComputableLiveData.java 2020-06-14 11:48:36.977868184 +0200 -@@ -96,13 +96,26 @@ public abstract class ComputableLiveData - // as long as it is invalid, keep computing. - try { - T value = null; -+ boolean once = true; -+ long last = android.os.SystemClock.elapsedRealtime(); - while (mInvalid.compareAndSet(true, false)) { -+ long now = android.os.SystemClock.elapsedRealtime(); -+ if (value != null && (once || last + 2500 < now)) { -+ eu.faircode.email.Log.i(mLiveData + " post once=" + once + " age=" + (now - last) + " ms"); -+ once = false; -+ last = now; -+ mLiveData.postValue(value); -+ } - computed = true; - value = compute(); - } - if (computed) { - mLiveData.postValue(value); - } -+ } catch (Throwable ex) { -+ // java.lang.IllegalStateException: Couldn't read row xxx column yyy -+ eu.faircode.email.Log.e(ex); -+ mInvalid.set(true); - } finally { - // release compute lock - mComputing.set(false); +diff --git a/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt b/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt +index 94aa8d7f72..ebdb5de278 100644 +--- a/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt ++++ b/app/src/main/java/androidx/lifecycle/ComputableLiveData.kt +@@ -69,13 +69,26 @@ constructor( + // as long as it is invalid, keep computing. + try { + var value: T? = null ++ var once = true; ++ var last = android.os.SystemClock.elapsedRealtime() + while (invalid.compareAndSet(true, false)) { ++ var now = android.os.SystemClock.elapsedRealtime() ++ if (value != null && (once || last + 2500 < now)) { ++ eu.faircode.email.Log.i(liveData.toString() + " post once=" + once + " age=" + (now - last) + " ms") ++ once = false; ++ last = now; ++ liveData.postValue(value); ++ } + computed = true + value = compute() + } + if (computed) { + liveData.postValue(value) + } ++ } catch (ex: Throwable) { ++ // java.lang.IllegalStateException: Couldn't read row xxx column yyy ++ eu.faircode.email.Log.e(ex); ++ invalid.set(true); + } finally { + // release compute lock + computing.set(false)