├── lives ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── snakydesign │ │ │ └── livedataextensions │ │ │ ├── livedata │ │ │ └── SingleLiveData.kt │ │ │ ├── Creating.kt │ │ │ ├── operators │ │ │ └── SingleLiveDataConcat.kt │ │ │ ├── Filtering.kt │ │ │ ├── Transforming.kt │ │ │ └── Combining.kt │ └── test │ │ └── java │ │ └── com │ │ └── snakydesign │ │ └── livedataextensions │ │ ├── Utils.kt │ │ ├── livedata │ │ ├── NonNullLiveDataTest.kt │ │ └── SingleLiveDataTest.kt │ │ ├── CreatingTest.kt │ │ ├── operators │ │ └── SingleLiveDataConcatTest.kt │ │ ├── TransformingTest.kt │ │ ├── CombiningTest.kt │ │ └── FilteringTest.kt ├── proguard-rules.pro ├── build.gradle └── pom.xml ├── settings.gradle ├── travis_build.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .travis.yml ├── publish.gradle ├── testapplicaiton └── src │ └── main │ └── java │ └── com │ └── snakydesign │ └── lives │ └── testapplicaiton │ └── MainActivity.java ├── .gitlab-ci.yml ├── gradle.properties ├── versions.gradle ├── CHANGELOG.md ├── gradlew.bat ├── gradlew ├── README.md └── gradle-mvn-push.gradle /lives/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lives', ':testapplicaiton' -------------------------------------------------------------------------------- /travis_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./gradlew check 3 | ./gradlew test 4 | ./gradlew build -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adibfara/Lives/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /lives/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | .testapplication 12 | .idea -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions 2 | 3 | open class TestOnNextAction : OnNextAction { 4 | override fun invoke(p1: T?) { 5 | } 6 | 7 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 28 07:38:07 IRDT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | licenses: 7 | - 'android-sdk-preview-license-.+' 8 | - 'android-sdk-license-.+' 9 | - 'google-gdk-license-.+' 10 | 11 | before_install: 12 | - yes | sdkmanager "platforms;android-28" 13 | android: 14 | components: 15 | - tools 16 | - platform-tools 17 | - build-tools-28.0.3 18 | - android-28 19 | - extra-android-m2repository 20 | sudo: false 21 | 22 | before_script: 23 | # Create and start an emulator for instrumentation tests. 24 | - chmod +x gradlew 25 | - chmod +x ./travis_build.sh 26 | # script for build and release via Travis to Bintray 27 | script: ./travis_build.sh 28 | 29 | notifications: 30 | email: false 31 | 32 | # cache between builds 33 | cache: 34 | directories: 35 | - "$HOME/.m2" 36 | - "$HOME/.gradle" 37 | - "$HOME/gcloud-sdk/" 38 | -------------------------------------------------------------------------------- /lives/src/main/java/com/snakydesign/livedataextensions/livedata/SingleLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions.livedata 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MediatorLiveData 5 | import androidx.lifecycle.Observer 6 | 7 | /** 8 | * Created by Adib Faramarzi 9 | * Emits at most one item 10 | */ 11 | class SingleLiveData(liveData: LiveData) : MediatorLiveData() { 12 | private var hasSetValue = false 13 | private val mediatorObserver = Observer { 14 | if(!hasSetValue){ 15 | hasSetValue=true 16 | this@SingleLiveData.value = it 17 | } 18 | } 19 | init { 20 | if(liveData.value!=null){ 21 | hasSetValue=true 22 | this.value = liveData.value 23 | }else { 24 | addSource(liveData, mediatorObserver) 25 | } 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /lives/src/main/java/com/snakydesign/livedataextensions/Creating.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Adib Faramarzi 3 | */ 4 | 5 | @file:JvmName("Lives") 6 | @file:JvmMultifileClass 7 | package com.snakydesign.livedataextensions 8 | 9 | import androidx.lifecycle.LiveData 10 | import androidx.lifecycle.MutableLiveData 11 | 12 | /** 13 | * Creates a LiveData that emits the initialValue immediately. 14 | */ 15 | fun liveDataOf(initialValue: T): MutableLiveData { 16 | return emptyLiveData().apply { value = initialValue } 17 | } 18 | 19 | 20 | /** 21 | * Creates a LiveData that emits the value that the `callable` function produces, immediately. 22 | */ 23 | fun liveDataOf(callable: () -> T): LiveData { 24 | val returnedLiveData = MutableLiveData() 25 | returnedLiveData.value = callable.invoke() 26 | return returnedLiveData 27 | } 28 | 29 | /** 30 | * Creates an empty LiveData. 31 | */ 32 | fun emptyLiveData(): MutableLiveData { 33 | return MutableLiveData() 34 | } -------------------------------------------------------------------------------- /publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.jfrog.artifactory" 2 | apply plugin: 'maven-publish' 3 | def artifactoryUrl = "https://api.bintray.com/maven/adibfara/lives/" 4 | apply from: rootProject.file('versions.gradle') 5 | 6 | publishing.publications { 7 | aar(MavenPublication) { 8 | groupId "com.snakydesign.livedataextensions" 9 | version = versionNumbers.versionName 10 | artifactId = 'lives' 11 | artifact("$buildDir/outputs/aar/$archivesBaseName-release.aar") 12 | } 13 | } 14 | 15 | artifactory { 16 | contextUrl = artifactoryUrl 17 | publish { 18 | repository { 19 | Properties properties = new Properties() 20 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 21 | repoKey = 'lives/' 22 | username = properties.getProperty('bintray.user') 23 | password = properties.getProperty('bintray.apiKey') 24 | } 25 | 26 | defaults { 27 | publishArtifacts = true 28 | publications('aar') 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /testapplicaiton/src/main/java/com/snakydesign/lives/testapplicaiton/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.snakydesign.lives.testapplicaiton; 2 | 3 | import android.os.Bundle; 4 | import android.widget.Toast; 5 | 6 | import com.snakydesign.livedataextensions.Lives; 7 | 8 | import androidx.annotation.Nullable; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | import androidx.lifecycle.MutableLiveData; 11 | import androidx.lifecycle.Observer; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | MutableLiveData mutableLiveData = new MutableLiveData<>(); 20 | Lives.first(mutableLiveData).observe(this, new Observer() { 21 | @Override 22 | public void onChanged(@Nullable Integer anInt) { 23 | Toast.makeText(MainActivity.this, "HELLO" + String.valueOf(anInt), Toast.LENGTH_SHORT).show(); 24 | } 25 | }); 26 | mutableLiveData.setValue(2); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: openjdk:8-jdk 2 | 3 | cache: 4 | paths: 5 | - .m2/ 6 | - .gradle/ 7 | 8 | variables: 9 | ANDROID_COMPILE_SDK: "26" 10 | ANDROID_BUILD_TOOLS: "25.0.2" 11 | ANDROID_SDK_TOOLS: "27.0.1" 12 | 13 | before_script: 14 | - apt-get --quiet update --yes 15 | - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 16 | 17 | - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip 18 | - unzip -q android-sdk.zip -d android-sdk-linux 19 | 20 | - mkdir android-sdk-linux/licenses 21 | - printf "8933bad161af4178b1185d1a37fbf41ea5269c55\nd56f5187479451eabf01fb78af6dfcb131a6481e" > android-sdk-linux/licenses/android-sdk-license 22 | - printf "84831b9409646a918e30573bab4c9c91346d8abd" > android-sdk-linux/licenses/android-sdk-preview-license 23 | - android-sdk-linux/tools/bin/sdkmanager --update > update.log 24 | - android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" "build-tools;${ANDROID_BUILD_TOOLS}" "extras;google;m2repository" "extras;android;m2repository" > installPlatform.log 25 | 26 | - export ANDROID_HOME=$PWD/android-sdk-linux 27 | - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ 28 | - chmod +x ./gradlew 29 | 30 | stages: 31 | - build 32 | - test 33 | 34 | build: 35 | stage: build 36 | script: 37 | - ./gradlew assembleDebug 38 | artifacts: 39 | paths: 40 | - app/build/outputs/ 41 | 42 | unitTests: 43 | stage: test 44 | script: 45 | - ./gradlew test 46 | -------------------------------------------------------------------------------- /lives/src/main/java/com/snakydesign/livedataextensions/operators/SingleLiveDataConcat.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions.operators 2 | 3 | import androidx.lifecycle.MediatorLiveData 4 | import com.snakydesign.livedataextensions.livedata.SingleLiveData 5 | 6 | /** 7 | * Created by Adib Faramarzi (adibfara@gmail.com) 8 | * Can be used for concating multiple LiveData objects 9 | */ 10 | class SingleLiveDataConcat(liveDataList:List>): MediatorLiveData() { 11 | constructor(vararg liveData:SingleLiveData):this(liveData.toList()) 12 | 13 | private val emittedValues = mutableListOf() 14 | private val hasEmittedValues = mutableListOf() 15 | private var lastEmittedLiveDataIndex = -1 16 | init { 17 | (0 until liveDataList.size).forEach { 18 | index-> 19 | emittedValues.add(null) 20 | hasEmittedValues.add(false) 21 | addSource(liveDataList[index]) { 22 | emittedValues[index] = it 23 | hasEmittedValues[index] = true 24 | removeSource(this) 25 | checkEmit() 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Emits the item that are in the `queue` 32 | */ 33 | private fun checkEmit(){ 34 | while (lastEmittedLiveDataIndex < emittedValues.size-1 && hasEmittedValues[lastEmittedLiveDataIndex+1]){ 35 | value = emittedValues[lastEmittedLiveDataIndex+1] 36 | lastEmittedLiveDataIndex += 1 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | GROUP=com.snakydesign.livedataextensions 15 | POM_ARTIFACT_ID=lives 16 | BINTRAY_REPO_NAME=lives 17 | VERSION_NAME=2.0.0 18 | 19 | POM_DESCRIPTION=LiveData Extensions For Android (Java and Kotlin) 20 | 21 | POM_URL=https://github.com/adibfara/Lives 22 | POM_SCM_URL=https://github.com/adibfara/Lives 23 | POM_SCM_CONNECTION=scm:git:git://github.com/adibfara/Lives.git 24 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com:adibfara/Lives.git 25 | POM_ISSUE_URL=https://github.com/adibfara/Lives/issues 26 | 27 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 28 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 29 | POM_ALL_LICENCES=['Apache-2.0'] 30 | POM_LICENCE_DIST=repo 31 | POM_DEVELOPER_ID=adibfara 32 | POM_DEVELOPER_NAME=Adib Faramarzi 33 | POM_NAME=Lives 34 | POM_PACKAGING=aar 35 | android.useAndroidX=true -------------------------------------------------------------------------------- /versions.gradle: -------------------------------------------------------------------------------- 1 | ext.versionNumbers = [ 2 | gradle : '3.4.1', 3 | jfrog : '4.7.5', 4 | bintray : '1.8.4', 5 | androidMaven: '2.1', 6 | 7 | kotlin : '1.3.72', 8 | mockito : '2.21.0', 9 | arch : '2.2.0', 10 | junit : '4.12', 11 | 12 | minSdk : 16, 13 | targetSdk : 28, 14 | versionCode : 4, 15 | versionName : '2.0.0', 16 | ] 17 | 18 | ext.libraries = [ 19 | gradle : "com.android.tools.build:gradle:$versionNumbers.gradle", 20 | kotlinGradle : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versionNumbers.kotlin", 21 | jfrog : "org.jfrog.buildinfo:build-info-extractor-gradle:$versionNumbers.jfrog", 22 | bintray : "com.jfrog.bintray.gradle:gradle-bintray-plugin:$versionNumbers.bintray", 23 | androidMaven : "com.github.dcendents:android-maven-gradle-plugin:$versionNumbers.androidMaven", 24 | 25 | kotlinSTDLib : "org.jetbrains.kotlin:kotlin-stdlib:$versionNumbers.kotlin", 26 | kotlinTestJunit: "org.jetbrains.kotlin:kotlin-test-junit:$versionNumbers.kotlin", 27 | mockito : "org.mockito:mockito-core:$versionNumbers.mockito", 28 | archLifeCycle : "androidx.lifecycle:lifecycle-extensions:$versionNumbers.arch", 29 | archLiveData : "androidx.lifecycle:lifecycle-livedata:$versionNumbers.arch", 30 | archCoreTesting: "android.arch.core:core-testing:$versionNumbers.arch", 31 | liveDataKtx: "androidx.lifecycle:lifecycle-livedata-ktx:$versionNumbers.arch", 32 | junit : "junit:junit:$versionNumbers.junit", 33 | ] -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/livedata/NonNullLiveDataTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions.livedata 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import com.snakydesign.livedataextensions.nonNull 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.mockito.Mockito 10 | 11 | /** 12 | * Created by Adib Faramarzi (adibfara@gmail.com) - 06/04/2018 13 | */ 14 | @Suppress("UNCHECKED_CAST") 15 | class NonNullLiveDataTest { 16 | 17 | @Rule 18 | @JvmField 19 | val rule = InstantTaskExecutorRule() 20 | @Test 21 | fun `test LiveData nonNull operator should not emit null`(){ 22 | val observer= Mockito.mock(Observer::class.java) as Observer 23 | val sourceLiveData1 = MutableLiveData() 24 | val testingLiveData = sourceLiveData1.nonNull() 25 | testingLiveData.observeForever(observer) 26 | sourceLiveData1.value = null 27 | sourceLiveData1.value = null 28 | sourceLiveData1.value = null 29 | Mockito.verifyZeroInteractions(observer) 30 | } 31 | 32 | @Test 33 | fun `test LiveData nonNull operator should emit non nulls`(){ 34 | val observer= Mockito.mock(Observer::class.java) as Observer 35 | val sourceLiveData1 = MutableLiveData() 36 | val testingLiveData = sourceLiveData1.nonNull() 37 | testingLiveData.observeForever(observer) 38 | sourceLiveData1.value = null 39 | sourceLiveData1.value = 2 40 | Mockito.verify(observer).onChanged(2) 41 | sourceLiveData1.value = null 42 | Mockito.verifyNoMoreInteractions(observer) 43 | } 44 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.0] 2 | ### Changed 3 | - [_BREAKING_] `map` and `switchMap` operators are now using the implementation of [Android LiveData KTX](https://developer.android.com/kotlin/ktx#livedata) . 4 | - [_BREAKING_] NonNullLiveData class has been removed. .nonNull() is now returning a regular live data. 5 | - [_BREAKING_] `zip` and `combineLatest` functions are now nullable. 6 | 7 | ## [1.3.0] 8 | ### Changed 9 | - [_BREAKING_] Update the dependencies to AndroidX 10 | - Removed synchronized blocks, since all LiveData operation is already handled on the main thread 11 | - [_BREAKING_] change the name of `just(value)` to `liveDataOf(value)`, since just was too broad. 12 | - [_BREAKING_] change the name of `from { }` to `liveDataOf { }`, since just was too broad. 13 | - [_BREAKING_] change the name of `empty()` to `emptyLiveData()`, since empty was too broad. 14 | 15 | ## [1.2.0] 16 | ### Added 17 | - `LiveData.sampleWith(otherLiveData)`: Samples the current live data with other live data, resulting in a live data that emits the last value emitted by the original live data (if any) whenever the other live data emits 18 | - `scan(accumulator)` : Applies the accumulator function to each emitted item, starting with the second emitted item. Initial value of the accumulator is the first item. 19 | - `scan(seed, accumulator)` : Applies the accumulator function to each emitted item, starting with the initial seed. 20 | - `combineLatest(firstLiveData, secondLiveData, combineFunction)`: combines both of the LiveDatas using the combineFunction and emits a value after any of them have emitted a value. 21 | 22 | ### Changed 23 | - change `api` to `implementation` when importing the architecture components libraries to make them compile-only 24 | 25 | ## [1.0.1] 26 | ### Added 27 | - Add support for android v15. 28 | - Add the operator empty() to easily create MutableLiveData objects. 29 | -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/livedata/SingleLiveDataTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions.livedata 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.mockito.Mockito 9 | 10 | /** 11 | * Created by Adib Faramarzi 12 | */ 13 | @Suppress("UNCHECKED_CAST") 14 | class SingleLiveDataTest { 15 | @Rule 16 | @JvmField 17 | val rule = InstantTaskExecutorRule() 18 | 19 | @Test 20 | fun `test SingleLiveDataTest without initial value`(){ 21 | val observer= Mockito.mock(Observer::class.java) as Observer 22 | val sourceLiveData = MutableLiveData() 23 | val testingLiveData = SingleLiveData(sourceLiveData) 24 | testingLiveData.observeForever(observer) 25 | 26 | sourceLiveData.value = 2 27 | kotlin.test.assertEquals(2, testingLiveData.value) 28 | Mockito.verify(observer).onChanged(2) 29 | 30 | sourceLiveData.value = 3 31 | kotlin.test.assertEquals(2, testingLiveData.value) 32 | 33 | Mockito.verifyNoMoreInteractions(observer) 34 | } 35 | 36 | @Test 37 | fun `test SingleLiveDataTest with initial value`(){ 38 | val observer= Mockito.mock(Observer::class.java) as Observer 39 | val sourceLiveData = MutableLiveData() 40 | sourceLiveData.value = 2 41 | val testingLiveData = SingleLiveData(sourceLiveData) 42 | testingLiveData.observeForever(observer) 43 | 44 | kotlin.test.assertEquals(2, testingLiveData.value) 45 | Mockito.verify(observer).onChanged(2) 46 | 47 | sourceLiveData.value = 3 48 | kotlin.test.assertEquals(2, testingLiveData.value) 49 | 50 | Mockito.verifyNoMoreInteractions(observer) 51 | } 52 | } -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/CreatingTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import org.junit.After 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Before 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.mockito.Mockito 11 | 12 | /** 13 | * Created by Adib Faramarzi 14 | */ 15 | @Suppress("UNCHECKED_CAST") 16 | class CreatingTest { 17 | 18 | @Rule 19 | @JvmField 20 | val rule = InstantTaskExecutorRule() 21 | 22 | @Before 23 | fun setUp() { 24 | } 25 | 26 | @After 27 | fun tearDown() { 28 | } 29 | 30 | @Test 31 | fun `test LiveData creation from value`(){ 32 | val testingLiveData = liveDataOf(2) 33 | val observer= Mockito.mock(Observer::class.java) as Observer 34 | assertEquals(2,testingLiveData.value) 35 | 36 | testingLiveData.observeForever(observer) 37 | Mockito.verify(observer).onChanged(2) 38 | Mockito.verifyNoMoreInteractions(observer) 39 | } 40 | 41 | @Test 42 | fun `test LiveData creation from function`(){ 43 | val testingLiveData = liveDataOf { 2 } 44 | val observer= Mockito.mock(Observer::class.java) as Observer 45 | assertEquals(2,testingLiveData.value) 46 | 47 | testingLiveData.observeForever(observer) 48 | Mockito.verify(observer).onChanged(2) 49 | Mockito.verifyNoMoreInteractions(observer) 50 | } 51 | 52 | @Test 53 | fun `test LiveData creation from empty`(){ 54 | val testingLiveData = emptyLiveData() 55 | val observer= Mockito.mock(Observer::class.java) as Observer 56 | testingLiveData.observeForever(observer) 57 | Mockito.verifyZeroInteractions(observer) 58 | assertEquals(null, testingLiveData.value) 59 | } 60 | } -------------------------------------------------------------------------------- /lives/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | ## Android architecture components: Lifecycle 23 | # LifecycleObserver's empty constructor is considered to be unused by proguard 24 | -keepclassmembers class * implements android.arch.lifecycle.LifecycleObserver { 25 | (...); 26 | } 27 | # ViewModel's empty constructor is considered to be unused by proguard 28 | -keepclassmembers class * extends android.arch.lifecycle.ViewModel { 29 | (...); 30 | } 31 | # keep Lifecycle State and Event enums values 32 | -keepclassmembers class android.arch.lifecycle.Lifecycle$State { *; } 33 | -keepclassmembers class android.arch.lifecycle.Lifecycle$Event { *; } 34 | # keep methods annotated with @OnLifecycleEvent even if they seem to be unused 35 | # (Mostly for LiveData.LifecycleBoundObserver.onStateChange(), but who knows) 36 | -keepclassmembers class * { 37 | @android.arch.lifecycle.OnLifecycleEvent *; 38 | } 39 | 40 | -keepclassmembers class * implements android.arch.lifecycle.LifecycleObserver { 41 | (...); 42 | } 43 | 44 | -keep class * implements android.arch.lifecycle.LifecycleObserver { 45 | (...); 46 | } 47 | -keepclassmembers class android.arch.** { *; } 48 | -keep class android.arch.** { *; } 49 | -dontwarn android.arch.** -------------------------------------------------------------------------------- /lives/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | minSdkVersion 15 7 | targetSdkVersion 28 8 | versionCode 2 9 | versionName "1.2.1" 10 | 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | testApplicationId "com.snakydesign.livedataextensions.test" 13 | // Specifies the fully-qualified class name of the test instrumentation runner. 14 | testInstrumentationRunner "android.test.InstrumentationTestRunner" 15 | 16 | 17 | } 18 | 19 | sourceSets { 20 | test.java.srcDirs += 'src/test/kotlin' 21 | test.java.srcDirs += 'src/test/java' 22 | } 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | consumerProguardFiles 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_7 32 | targetCompatibility JavaVersion.VERSION_1_7 33 | } 34 | } 35 | 36 | afterEvaluate { 37 | android.sourceSets.all { sourceSet -> 38 | if (!sourceSet.name.startsWith("test")) 39 | { 40 | sourceSet.kotlin.setSrcDirs([]) 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation fileTree(include: ['*.jar'], dir: 'libs') 47 | testImplementation 'junit:junit:4.12' 48 | implementation libraries.kotlinSTDLib 49 | testImplementation libraries.kotlinTestJunit 50 | implementation libraries.archLifeCycle 51 | implementation libraries.archLiveData 52 | implementation libraries.liveDataKtx 53 | testImplementation libraries.mockito 54 | testImplementation('androidx.arch.core:core-testing:2.0.0', { 55 | exclude group: 'com.android.support', module: 'support-compat' 56 | exclude group: 'com.android.support', module: 'support-annotations' 57 | exclude group: 'com.android.support', module: 'support-core-utils' 58 | }) 59 | } 60 | 61 | apply from: rootProject.file('./gradle-mvn-push.gradle') 62 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ######################################################################### 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ######################################################################### 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lives/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.snakydesign 6 | lives 7 | 1.0.0 8 | aar 9 | Lives 10 | LiveData Extensions For Android (Java and Kotlin) 11 | https://github.com/adibfara/Lives 12 | 13 | 14 | The Apache Software License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | repo 17 | 18 | 19 | 20 | 21 | adibfara 22 | Adib Faramarzi 23 | 24 | 25 | 26 | scm:git:git://github.com/adibfara/Lives.git 27 | scm:git:ssh://git@github.com:adibfara/Lives.git 28 | https://github.com/adibfara/Lives 29 | 30 | 31 | 32 | org.jetbrains.kotlin 33 | kotlin-stdlib 34 | 1.2.31 35 | runtime 36 | 37 | 38 | android.arch.lifecycle 39 | extensions 40 | 1.1.1 41 | runtime 42 | 43 | 44 | junit 45 | junit 46 | 4.12 47 | test 48 | 49 | 50 | org.jetbrains.kotlin 51 | kotlin-test-junit 52 | 1.2.31 53 | test 54 | 55 | 56 | org.mockito 57 | mockito-core 58 | 2.8.9 59 | test 60 | 61 | 62 | android.arch.core 63 | core-testing 64 | 1.1.1 65 | test 66 | 67 | 68 | support-core-utils 69 | com.android.support 70 | 71 | 72 | support-annotations 73 | com.android.support 74 | 75 | 76 | support-compat 77 | com.android.support 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/operators/SingleLiveDataConcatTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions.operators 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MediatorLiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.Observer 7 | import com.snakydesign.livedataextensions.concat 8 | import org.junit.Assert 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.mockito.Mockito.* 13 | 14 | /** 15 | * Created by Adib Faramarzi 16 | */ 17 | @Suppress("UNCHECKED_CAST") 18 | class SingleLiveDataConcatTest : MediatorLiveData(){ 19 | @Rule 20 | @JvmField 21 | val rule = InstantTaskExecutorRule() 22 | 23 | @Test 24 | fun `test LiveData concat multiple LiveData with wrong initial data`(){ 25 | val observer= mock(Observer::class.java) as Observer 26 | val sourceLiveData1 = MutableLiveData() 27 | val sourceLiveData2 = MutableLiveData() 28 | val sourceLiveData3 = MutableLiveData() 29 | sourceLiveData2.value = 3 30 | val testingLiveData = concat(sourceLiveData1,sourceLiveData2,sourceLiveData3) 31 | testingLiveData.observeForever(observer) 32 | Assert.assertEquals(null, testingLiveData.value) 33 | verifyZeroInteractions(observer) 34 | } 35 | 36 | @Test 37 | fun `test LiveData concat multiple LiveData with proper initial data`(){ 38 | val observer= mock(Observer::class.java) as Observer 39 | val sourceLiveData1 = MutableLiveData() 40 | val sourceLiveData2 = MutableLiveData() 41 | val sourceLiveData3 = MutableLiveData() 42 | sourceLiveData1.value = 2 43 | val testingLiveData = concat(sourceLiveData1,sourceLiveData2,sourceLiveData3) 44 | testingLiveData.observeForever(observer) 45 | Assert.assertEquals(2, testingLiveData.value) 46 | verify(observer).onChanged(2) 47 | verifyZeroInteractions(observer) 48 | } 49 | 50 | @Test 51 | fun `test LiveData concat multiple LiveData with initial and on going data`(){ 52 | val observer= mock(Observer::class.java) as Observer 53 | val sourceLiveData1 = MutableLiveData() 54 | val sourceLiveData2 = MutableLiveData() 55 | val sourceLiveData3 = MutableLiveData() 56 | sourceLiveData2.value = 2 57 | val testingLiveData = concat(sourceLiveData1,sourceLiveData2,sourceLiveData3) 58 | testingLiveData.observeForever(observer) 59 | Assert.assertEquals(null, testingLiveData.value) 60 | sourceLiveData1.value = 1 61 | 62 | val inOrder = inOrder(observer) 63 | inOrder.verify(observer).onChanged(1) 64 | inOrder.verify(observer).onChanged(2) 65 | sourceLiveData3.value = 3 66 | inOrder.verify(observer).onChanged(3) 67 | assertEquals(3, testingLiveData.value) 68 | inOrder.verifyNoMoreInteractions() 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /lives/src/main/java/com/snakydesign/livedataextensions/Filtering.kt: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Created by Adib Faramarzi 4 | */ 5 | 6 | @file:JvmName("Lives") 7 | @file:JvmMultifileClass 8 | package com.snakydesign.livedataextensions 9 | 10 | import androidx.lifecycle.LiveData 11 | import androidx.lifecycle.MediatorLiveData 12 | import androidx.lifecycle.Transformations 13 | import com.snakydesign.livedataextensions.livedata.SingleLiveData 14 | 15 | 16 | /** 17 | * Emits the items that are different from all the values that have been emitted so far 18 | */ 19 | fun LiveData.distinct(): LiveData { 20 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 21 | val dispatchedValues = mutableSetOf() 22 | mutableLiveData.addSource(this) { 23 | if(!dispatchedValues.contains(it)) { 24 | mutableLiveData.value = it 25 | dispatchedValues.add(it) 26 | } 27 | } 28 | return mutableLiveData 29 | } 30 | 31 | /** 32 | * Emits the items that are different from the last item 33 | */ 34 | fun LiveData.distinctUntilChanged(): LiveData { 35 | return Transformations.distinctUntilChanged(this) 36 | } 37 | 38 | /** 39 | * Emits the items that pass through the predicate 40 | */ 41 | inline fun LiveData.filter(crossinline predicate : (T?)->Boolean): LiveData { 42 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 43 | mutableLiveData.addSource(this) { 44 | if(predicate(it)) 45 | mutableLiveData.value = it 46 | } 47 | return mutableLiveData 48 | } 49 | 50 | /** 51 | * Emits at most 1 item and returns a SingleLiveData 52 | */ 53 | fun LiveData.first(): SingleLiveData { 54 | return SingleLiveData(this) 55 | } 56 | 57 | /** 58 | * Emits the first n valueus 59 | */ 60 | fun LiveData.take(count:Int): LiveData { 61 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 62 | var takenCount = 0 63 | mutableLiveData.addSource(this) { 64 | if(takenCount LiveData.takeUntil(crossinline predicate : (T?)->Boolean): LiveData { 78 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 79 | var metPredicate = predicate(value) 80 | mutableLiveData.addSource(this) { 81 | if(predicate(it)) metPredicate = true 82 | if(!metPredicate) { 83 | mutableLiveData.value = it 84 | } else { 85 | mutableLiveData.removeSource(this) 86 | } 87 | } 88 | return mutableLiveData 89 | } 90 | 91 | /** 92 | * Skips the first n values 93 | */ 94 | fun LiveData.skip(count:Int): LiveData { 95 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 96 | var skippedCount = 0 97 | mutableLiveData.addSource(this) { 98 | if(skippedCount>=count) { 99 | mutableLiveData.value = it 100 | } 101 | skippedCount++ 102 | } 103 | return mutableLiveData 104 | } 105 | 106 | /** 107 | * Skips all values until a certain predicate is met (the item that actives the predicate is also emitted) 108 | */ 109 | inline fun LiveData.skipUntil(crossinline predicate : (T?)->Boolean): LiveData { 110 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 111 | var metPredicate = false 112 | mutableLiveData.addSource(this) { 113 | if(metPredicate || predicate(it)) { 114 | metPredicate=true 115 | mutableLiveData.value = it 116 | } 117 | } 118 | return mutableLiveData 119 | } 120 | 121 | /** 122 | * emits the item that was emitted at `index` position 123 | * Note: This only works for elements that were emitted `after` the `elementAt` is applied. 124 | */ 125 | fun LiveData.elementAt(index:Int): SingleLiveData { 126 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 127 | var currentIndex = 0 128 | if(this.value != null) 129 | currentIndex = -1 130 | mutableLiveData.addSource(this) { 131 | if(currentIndex==index) { 132 | mutableLiveData.value = it 133 | mutableLiveData.removeSource(this) 134 | } 135 | currentIndex++ 136 | } 137 | return SingleLiveData(mutableLiveData) 138 | } 139 | 140 | /** 141 | * Emits only the values that are not null 142 | */ 143 | fun LiveData.nonNull(): MediatorLiveData { 144 | return MediatorLiveData().also { mediatorLiveData -> 145 | this.value?.let { 146 | mediatorLiveData.value = it 147 | } 148 | mediatorLiveData.addSource(this@nonNull) { 149 | if (it != null) { 150 | mediatorLiveData.value = it 151 | } 152 | } 153 | 154 | } 155 | } 156 | 157 | /** 158 | * Emits the default value when a null value is emitted 159 | */ 160 | fun LiveData.defaultIfNull(default:T):LiveData{ 161 | val mutableLiveData:MediatorLiveData = MediatorLiveData() 162 | mutableLiveData.addSource(this) { 163 | mutableLiveData.value = it ?: default 164 | } 165 | return mutableLiveData 166 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################# 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################# 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /lives/src/main/java/com/snakydesign/livedataextensions/Transforming.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Adib Faramarzi 3 | */ 4 | 5 | @file:JvmName("Lives") 6 | @file:JvmMultifileClass 7 | 8 | package com.snakydesign.livedataextensions 9 | 10 | import androidx.lifecycle.LiveData 11 | import androidx.lifecycle.MediatorLiveData 12 | import androidx.lifecycle.MutableLiveData 13 | import androidx.lifecycle.Transformations 14 | import com.snakydesign.livedataextensions.livedata.SingleLiveData 15 | import java.util.concurrent.atomic.AtomicBoolean 16 | 17 | /** 18 | * Maps any values that were emitted by the LiveData to the given function 19 | */ 20 | fun LiveData.map(function: MapperFunction): LiveData { 21 | return Transformations.map(this, function) 22 | } 23 | 24 | /** 25 | * Maps any values that were emitted by the LiveData to the given function that produces another LiveData 26 | */ 27 | fun LiveData.switchMap(function: MapperFunction>): LiveData { 28 | return Transformations.switchMap(this, function) 29 | } 30 | 31 | /** 32 | * Does the `onNext` function before everything actually emitting the item to the observers 33 | */ 34 | fun LiveData.doBeforeNext(onNext: OnNextAction): MutableLiveData { 35 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 36 | mutableLiveData.addSource(this) { 37 | onNext(it) 38 | mutableLiveData.value = it 39 | } 40 | return mutableLiveData 41 | } 42 | 43 | /** 44 | * Does the `onNext` function after emitting the item to the observers 45 | */ 46 | fun LiveData.doAfterNext(onNext: OnNextAction): MutableLiveData { 47 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 48 | mutableLiveData.addSource(this) { 49 | mutableLiveData.value = it 50 | onNext(it) 51 | } 52 | return mutableLiveData 53 | } 54 | 55 | /** 56 | * Buffers the items emitted by the LiveData, and emits them when they reach the `count` as a List. 57 | */ 58 | fun LiveData.buffer(count: Int): MutableLiveData> { 59 | val mutableLiveData: MediatorLiveData> = MediatorLiveData() 60 | val latestBuffer = mutableListOf() 61 | mutableLiveData.addSource(this) { value -> 62 | latestBuffer.add(value) 63 | if (latestBuffer.size == count) { 64 | mutableLiveData.value = latestBuffer.toList() 65 | latestBuffer.clear() 66 | } 67 | 68 | } 69 | return mutableLiveData 70 | } 71 | 72 | /** 73 | * Returns a LiveData that applies a specified accumulator function to each item that is emitted 74 | * after the first item has been emitted. 75 | * Note: The LiveData should not emit nulls. Add .nonNull() to your LiveData if you want to ensure this. 76 | * 77 | * @param accumulator the function that is applied to each item 78 | */ 79 | fun LiveData.scan(accumulator: (accumulatedValue: T, currentValue: T) -> T): MutableLiveData { 80 | var accumulatedValue: T? = null 81 | val hasEmittedFirst = AtomicBoolean(false) 82 | return MediatorLiveData().apply { 83 | addSource(this@scan) { emittedValue -> 84 | if (hasEmittedFirst.compareAndSet(false, true)) { 85 | accumulatedValue = emittedValue 86 | } else { 87 | accumulatedValue = accumulator(accumulatedValue!!, emittedValue!!) 88 | value = accumulatedValue 89 | } 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Returns a LiveData that applies a specified accumulator function to the first item emitted by a source LiveData, 96 | * then feeds the result of that function along with the second item emitted by the source LiveData into the same function, 97 | * and so on, emitting the result of each of these iterations. 98 | * Note: Your LiveData should not emit nulls. Add .nonNull() to your LiveData if you want to ensure this. 99 | * 100 | * @param initialSeed the initial value of the accumulator 101 | * @param accumulator the function that is applied to each item 102 | */ 103 | fun LiveData.scan(initialSeed: R, accumulator: (accumulated: R, currentValue: T) -> R): MutableLiveData { 104 | var accumulatedValue = initialSeed 105 | return MediatorLiveData().apply { 106 | value = initialSeed 107 | addSource(this@scan) { emittedValue -> 108 | accumulatedValue = accumulator(accumulatedValue, emittedValue!!) 109 | value = accumulatedValue 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Emits the items of the first LiveData that emits the item. Items of other LiveDatas will never be emitted and are not considered. 116 | */ 117 | fun amb(vararg inputLiveData: LiveData, considerNulls: Boolean = true): MutableLiveData { 118 | val mutableLiveData: MediatorLiveData = MediatorLiveData() 119 | 120 | var activeLiveDataIndex = inputLiveData.indexOfFirst { it.value != null } 121 | if (activeLiveDataIndex >= 0) { 122 | mutableLiveData.value = inputLiveData[activeLiveDataIndex].value 123 | } 124 | inputLiveData.forEachIndexed { index, liveData -> 125 | mutableLiveData.addSource(liveData) { value -> 126 | if (considerNulls || value != null) { 127 | activeLiveDataIndex = index 128 | inputLiveData.forEachIndexed { index, liveData -> 129 | if (index != activeLiveDataIndex) { 130 | mutableLiveData.removeSource(liveData) 131 | } 132 | } 133 | 134 | if (index == activeLiveDataIndex) { 135 | mutableLiveData.value = value 136 | } 137 | } 138 | 139 | 140 | } 141 | } 142 | return mutableLiveData 143 | } 144 | 145 | /** 146 | * Converts a LiveData to a SingleLiveData (exactly similar to LiveData.first() 147 | */ 148 | fun LiveData.toSingleLiveData(): SingleLiveData = first() 149 | 150 | /** 151 | * Converts a LiveData to a MutableLiveData with the initial value set by this LiveData's value 152 | */ 153 | fun LiveData.toMutableLiveData(): MutableLiveData { 154 | val liveData = MutableLiveData() 155 | liveData.value = this.value 156 | return liveData 157 | } 158 | 159 | /** 160 | * Mapper function used in the operators that need mapping 161 | */ 162 | typealias MapperFunction = (T) -> O 163 | 164 | /** 165 | * Mapper function used in the operators that need mapping 166 | */ 167 | typealias OnNextAction = (T?) -> Unit -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lives - Android LiveData Extensions for Kotlin and Java 2 | ------------------------------------------------------- 3 | [![Build Status](https://travis-ci.org/adibfara/Lives.svg?branch=master)](https://travis-ci.org/adibfara/Lives) [![Latest Version](https://img.shields.io/bintray/v/adibfara/lives/lives.svg?label=version)](https://github.com/adibfara/Lives) 4 | 5 | Add RxJava-like operators to your LiveData objects with ease, usable in Kotlin (as extension functions) and Java (as static functions to a class called Lives) 6 | 7 | Download 8 | -------- 9 | Add the dependencies to your project: 10 | 11 | __AndroidX Version__ 12 | 13 | ```groovy 14 | implementation 'com.snakydesign.livedataextensions:lives:1.3.0' 15 | implementation 'android.arch.lifecycle:extensions:x.x.x' // If you are using the AndroidX version, that's fine too, as the Jetifier will take care of the conversion. 16 | ``` 17 | 18 | __Non AndroidX Version__ 19 | 20 | ```groovy 21 | implementation 'com.snakydesign.livedataextensions:lives:1.2.1' 22 | implementation 'androidx.lifecycle:lifecycle-livedata:x.x.x' 23 | ``` 24 | 25 | 26 | If you want to use this library on a Java project, add the following dependency: 27 | ```groovy 28 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:x.x.x' 29 | ``` 30 | 31 | Usage 32 | -------- 33 | 34 | #### Kotlin 35 | **Import the functions** 36 | ```kotlin 37 | import com.snakydesign.livedataextensions.* 38 | ``` 39 | **Creating LiveData** 40 | 41 | - `liveDataOf` : Create a LiveData object from a value (like `just` in RxJava, although it immediately emits the value) 42 | ```kotlin 43 | val liveData = liveDataOf(2) //liveData will produce 2 (as Int) when observed 44 | ``` 45 | 46 | - `from` : Creates a LiveData that emits the value that the `callable` function produces, and immediately emits it. 47 | ```kotlin 48 | val liveData = liveDataOf {computePI()} 49 | ``` 50 | 51 | - `emptyLiveData` : Creates an empty LiveData 52 | ```kotlin 53 | val liveData = emptyLiveData() 54 | ``` 55 | 56 | 57 | **Filtering** 58 | 59 | - `distinct` : Emits the items that are different from all the values that have been emitted so far 60 | ```kotlin 61 | val originalLiveData = MutableLiveData() 62 | val newLiveData = originalLiveData.distinct() 63 | originalLiveData.value = 2 64 | originalLiveData.value = 2 // newLiveData will not produce this 65 | originalLiveData.value = 3 // newLiveData will produce 66 | originalLiveData.value = 2 // newLiveData will not produce this 67 | ``` 68 | 69 | - `distinctUntilChanged` : Emits the items that are different from the last item 70 | ```kotlin 71 | val originalLiveData = MutableLiveData() 72 | val newLiveData = originalLiveData.distinctUntilChanged() 73 | originalLiveData.value = 2 74 | originalLiveData.value = 2 // newLiveData will not produce this 75 | originalLiveData.value = 3 // newLiveData will produce 76 | originalLiveData.value = 2 // newLiveData will produce 77 | ``` 78 | 79 | - `filter` :Emits the items that pass through the predicate 80 | ```kotlin 81 | val originalLiveData = MutableLiveData() 82 | val newLiveData = originalLiveData.filter { it > 2 } 83 | originalLiveData.value = 3 // newLiveData will produce 84 | originalLiveData.value = 2 // newLiveData will not produce this 85 | ``` 86 | 87 | - `first()` : produces a SingleLiveData that produces only one Item. 88 | - `take(n:Int)` : produces a LiveData that produces only the first n Items. 89 | - `takeUntil(predicate)` : Takes until a certain predicate is met, and does not emit anything after that, whatever the value. 90 | - `skip(n)` : Skips the first n values. 91 | - `skipUntil(predicate)` : Skips all values until a certain predicate is met (the item that actives the predicate is also emitted). 92 | - `elementAt(index)` : emits the item that was emitted at `index` position 93 | - `nonNull()` : Will never emit the nulls to the observers. 94 | - `defaultIfNull(value)`: Will produce the `value` when `null` is received. 95 | 96 | **Combining** 97 | 98 | - `merge(List)` : Merges multiple LiveData, and emits any item that was emitted by any of them 99 | - `LiveData.merge(LiveData)` : Merges this LiveData with another one, and emits any item that was emitted by any of them 100 | - `concat(LiveData...)` : Concats multiple LiveData objects (and converts them to `SingleLiveData` if necessary, and emits their first item in order. (Please check the note below.) 101 | - `LiveData.then(LiveData)` : Concats the first LiveData with the given one. (Please check the note below.) 102 | - `startWith(startingValue)`: Emits the `startingValue` before any other value. 103 | - `zip(firstLiveData, secondLiveData, zipFunction)`: zips both of the LiveDatas using the zipFunction and emits a value after both of them have emitted their values, after that, emits values whenever any of them emits a value. 104 | - `combineLatest(firstLiveData, secondLiveData, combineFunction)`: combines both of the LiveDatas using the combineFunction and emits a value after any of them have emitted a value. 105 | - `LiveData.sampleWith(otherLiveData)`: Samples the current live data with other live data, resulting in a live data that emits the last value emitted by the original live data (if any) whenever the other live data emits 106 | 107 | **Transforming** 108 | 109 | - `map(mapperFunction)` : Map each value emitted to another value (and type) with the given function 110 | - `switchMap(mapperFunction)` : Maps any values that were emitted by the LiveData to the given function that produces another LiveData 111 | - `doBeforeNext(OnNextAction)` : Does the `onNext` function before everything actually emitting the item to the observers 112 | - `doAfterNext(OnNextAction)` : Does the `onNext` function after emitting the item to the observers(function) : Does the `onNext` function before everything actually emitting the item to the observers 113 | - `buffer(count)` : Buffers the items emitted by the LiveData, and emits them when they reach the `count` as a List. 114 | - `scan(accumulator)` : Applies the accumulator function to each emitted item, starting with the second emitted item. Initial value of the accumulator is the first item. 115 | - `scan(seed, accumulator)` : Applies the accumulator function to each emitted item, starting with the initial seed. 116 | - `amb(LiveData...)` : Emits the items of the first LiveData that emits the item. Items of other LiveDatas will never be emitted and are not considered. 117 | - `toMutableLiveData()` : Converts a LiveData to a MutableLiveData with the initial value set by this LiveData's value 118 | 119 | #### Java 120 | 121 | You can call any function prefixed with `Lives` keyword. 122 | 123 | import com.snakydesign.livedataextensions.Lives; 124 | 125 | - Example (create a LiveData with the initial value of 2 and map each value to its String type 126 | ```kotlin 127 | LiveData liveData = Lives.map(Lives.just(2), new Function1() { 128 | @Override 129 | public Integer invoke(Integer integer) { 130 | return String.valueOf(integer); 131 | } 132 | }) ; 133 | ``` 134 | 135 | #### Notes 136 | 137 | Please note that because of design of `LiveData`, after a value is emitted to an observer, and then another value is emitted, the old value is destroyed in any LiveData object. So unlike RxJava, if a new Observer is attached, It will only receive the most recent value. 138 | 139 | So If you want to use operators like `concat`, you have to consider allowing only one observer to the LiveData. 140 | 141 | PRs are more than welcome, and please file an issue If you encounter something 🍻. 142 | 143 | You can also ping me on twitter [@TheSNAKY](http://twitter.com/TheSNAKY). 144 | 145 | 146 | License 147 | ======= 148 | 149 | Copyright 2018 Adib Faramarzi. 150 | 151 | Licensed under the Apache License, Version 2.0 (the "License"); 152 | you may not use this file except in compliance with the License. 153 | You may obtain a copy of the License at 154 | 155 | http://www.apache.org/licenses/LICENSE-2.0 156 | 157 | Unless required by applicable law or agreed to in writing, software 158 | distributed under the License is distributed on an "AS IS" BASIS, 159 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 160 | See the License for the specific language governing permissions and 161 | limitations under the License. 162 | -------------------------------------------------------------------------------- /gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | if (project.rootProject.file('local.properties').exists()) { 17 | apply plugin: 'com.jfrog.bintray' 18 | apply plugin: 'com.github.dcendents.android-maven' 19 | apply plugin: 'maven-publish' 20 | 21 | version = VERSION_NAME 22 | group = GROUP 23 | 24 | // Bintray 25 | bintray { 26 | Properties properties = new Properties() 27 | println("starting bintray") 28 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 29 | user = properties.getProperty("bintray.user") 30 | key = properties.getProperty("bintray.apikey") 31 | 32 | configurations = ['archives'] 33 | dryRun = false 34 | pkg { 35 | repo = BINTRAY_REPO_NAME 36 | name = POM_ARTIFACT_ID 37 | desc = POM_DESCRIPTION 38 | websiteUrl = POM_URL 39 | issueTrackerUrl = POM_ISSUE_URL 40 | vcsUrl = POM_SCM_URL 41 | licenses = ["Apache-2.0"] 42 | publish = true 43 | 44 | publicDownloadNumbers = true 45 | version { 46 | desc = POM_DESCRIPTION 47 | gpg { 48 | sign = true //Determines whether to GPG sign the files. The default is false 49 | passphrase = properties.getProperty("bintray.gpg.password") 50 | //Optional. The passphrase for GPG signing' 51 | } 52 | 53 | mavenCentralSync { 54 | sync = true 55 | user = properties.getProperty("bintray.oss.user") 56 | password = properties.getProperty("bintray.oss.password") 57 | close = '1' 58 | } 59 | } 60 | } 61 | 62 | publications= ['mavenAar'] 63 | } 64 | 65 | install { 66 | repositories.mavenInstaller { 67 | customizePom(pom, project) 68 | configuration = configurations.archives 69 | 70 | } 71 | } 72 | 73 | task androidJavadocs(type: Javadoc) { 74 | source = android.sourceSets.main.java.source 75 | options.addStringOption('Xdoclint:none', '-quiet') 76 | options.addStringOption('encoding', 'UTF-8') 77 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 78 | } 79 | 80 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 81 | classifier = 'javadoc' 82 | from androidJavadocs.destinationDir 83 | } 84 | 85 | task androidSourcesJar(type: Jar) { 86 | classifier = 'sources' 87 | from android.sourceSets.main.java.source 88 | } 89 | 90 | 91 | if (JavaVersion.current().isJava8Compatible()) { 92 | allprojects { 93 | tasks.withType(Javadoc) { 94 | options.addStringOption('Xdoclint:none', '-quiet') 95 | } 96 | } 97 | } 98 | 99 | artifacts { 100 | if (project.getPlugins().hasPlugin('com.android.application') || 101 | project.getPlugins().hasPlugin('com.android.library')) { 102 | archives androidSourcesJar 103 | } 104 | } 105 | 106 | 107 | } 108 | def customizePom(pom, gradleProject) { 109 | publishing { 110 | publications { 111 | mavenAar(MavenPublication) { 112 | artifact("$buildDir/outputs/aar/app-release.aar") 113 | groupId = GROUP 114 | artifactId = POM_ARTIFACT_ID 115 | version = VERSION_NAME 116 | pom.withXml { 117 | def dependenciesNode = asNode().appendNode('dependencies') 118 | 119 | // Iterate over the implementation dependencies (we don't want the test ones), adding a node for each 120 | configurations.implementation.allDependencies.each { 121 | // Ensure dependencies such as fileTree are not included in the pom. 122 | if (it.name != 'unspecified') { 123 | def dependencyNode = dependenciesNode.appendNode('dependency') 124 | dependencyNode.appendNode('groupId', GROUP) 125 | dependencyNode.appendNode('artifactId', POM_ARTIFACT_ID) 126 | dependencyNode.appendNode('version', VERSION_NAME) 127 | } 128 | } 129 | groupId = GROUP 130 | artifactId = POM_ARTIFACT_ID 131 | version = VERSION_NAME 132 | gradleProject { 133 | name POM_NAME 134 | packaging POM_PACKAGING 135 | description POM_DESCRIPTION 136 | url POM_URL 137 | 138 | scm { 139 | url POM_SCM_URL 140 | connection POM_SCM_CONNECTION 141 | developerConnection POM_SCM_DEV_CONNECTION 142 | } 143 | 144 | licenses { 145 | license { 146 | name POM_LICENCE_NAME 147 | url POM_LICENCE_URL 148 | distribution POM_LICENCE_DIST 149 | } 150 | } 151 | 152 | developers { 153 | developer { 154 | id POM_DEVELOPER_ID 155 | name POM_DEVELOPER_NAME 156 | } 157 | } 158 | } 159 | } 160 | 161 | } 162 | } 163 | } 164 | 165 | pom.whenConfigured { generatedPom -> 166 | // eliminate test-scoped dependencies (no need in maven central poms) 167 | generatedPom.dependencies.removeAll { dep -> 168 | dep.scope == "test" 169 | } 170 | 171 | // sort to make pom dependencies order consistent to ease comparison of older poms 172 | generatedPom.dependencies = generatedPom.dependencies.sort { dep -> 173 | "$dep.scope:$dep.groupId:$dep.artifactId" 174 | } 175 | 176 | // add all items necessary for maven central publication 177 | generatedPom.project { 178 | groupId = GROUP 179 | artifactId = POM_ARTIFACT_ID 180 | version = VERSION_NAME 181 | gradleProject { 182 | name POM_NAME 183 | packaging POM_PACKAGING 184 | description POM_DESCRIPTION 185 | url POM_URL 186 | 187 | scm { 188 | url POM_SCM_URL 189 | connection POM_SCM_CONNECTION 190 | developerConnection POM_SCM_DEV_CONNECTION 191 | } 192 | 193 | licenses { 194 | license { 195 | name POM_LICENCE_NAME 196 | url POM_LICENCE_URL 197 | distribution POM_LICENCE_DIST 198 | } 199 | } 200 | 201 | developers { 202 | developer { 203 | id POM_DEVELOPER_ID 204 | name POM_DEVELOPER_NAME 205 | } 206 | } 207 | } 208 | } 209 | 210 | 211 | } 212 | } 213 | 214 | 215 | task copyPom(type: Copy) { 216 | from rootProject.file("/lives/build/publications/mavenAar/pom-default.xml") 217 | into rootProject.file("/lives/build/poms/") 218 | } 219 | 220 | task releaseOnAdibBintray(type: GradleBuild) { 221 | tasks = ['assembleRelease', 'androidSourcesJar', 'generatePomFileForMavenAarPublication', 'copyPom', 'bintrayUpload'] 222 | } -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/TransformingTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import org.junit.After 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Before 9 | import org.junit.Rule 10 | import org.junit.Test 11 | import org.mockito.Mockito 12 | import org.mockito.Mockito.* 13 | import kotlin.test.assertNull 14 | 15 | /** 16 | * Created by Adib Faramarzi (adibfara@gmail.com) 17 | */ 18 | @Suppress("UNCHECKED_CAST") 19 | class TransformingTest { 20 | 21 | 22 | @Rule 23 | @JvmField 24 | val rule = InstantTaskExecutorRule() 25 | 26 | @Before 27 | fun setUp() { 28 | } 29 | 30 | @After 31 | fun tearDown() { 32 | } 33 | 34 | @Test 35 | fun `test LiveData filter initial item`(){ 36 | val observer= Mockito.mock(Observer::class.java) as Observer 37 | val testingLiveData = liveDataOf(2).filter { 38 | it!=null && it > 1 39 | } 40 | testingLiveData.observeForever(observer) 41 | 42 | assertEquals(2,testingLiveData.value) 43 | Mockito.verify(observer).onChanged(2) 44 | Mockito.verifyNoMoreInteractions(observer) 45 | } 46 | 47 | @Test 48 | fun `test LiveData filter multiple items`(){ 49 | val observer= Mockito.mock(Observer::class.java) as Observer 50 | val originalLiveData = liveDataOf(2) 51 | val testingLiveData = originalLiveData.filter { 52 | it!=null && it > 10 53 | } 54 | testingLiveData.observeForever(observer) 55 | 56 | originalLiveData.value = 11 57 | assertEquals(11,testingLiveData.value) 58 | 59 | originalLiveData.value = 100 60 | assertEquals(100,testingLiveData.value) 61 | 62 | originalLiveData.value = 5 63 | assertEquals(100,testingLiveData.value) 64 | } 65 | 66 | @Test 67 | fun `test LiveData doBeforeNext`(){ 68 | val observer= Mockito.mock(Observer::class.java) as Observer 69 | val mockedBeforeNext = mock(TestOnNextAction::class.java) as OnNextAction 70 | val sourceLiveData = MutableLiveData() 71 | val testingLiveData = sourceLiveData.doBeforeNext(mockedBeforeNext) 72 | val expectedValue = 3 73 | testingLiveData.observeForever(observer) 74 | 75 | sourceLiveData.value = expectedValue 76 | val inOrder = inOrder(mockedBeforeNext,observer) 77 | inOrder.verify(mockedBeforeNext, times(1))(expectedValue) 78 | assertEquals(expectedValue,testingLiveData.value) 79 | inOrder.verify(observer).onChanged(expectedValue) 80 | inOrder.verifyNoMoreInteractions() 81 | } 82 | 83 | @Test 84 | fun `test LiveData doAterNext`(){ 85 | val observer= Mockito.mock(Observer::class.java) as Observer 86 | val mockedBeforeNext = spy(TestOnNextAction::class.java) as OnNextAction 87 | val sourceLiveData = MutableLiveData() 88 | val testingLiveData = sourceLiveData.doAfterNext(mockedBeforeNext) 89 | val expectedValue = 3 90 | testingLiveData.observeForever(observer) 91 | 92 | sourceLiveData.value = expectedValue 93 | val inOrder = inOrder(mockedBeforeNext,observer) 94 | inOrder.verify(observer).onChanged(expectedValue) 95 | inOrder.verify(mockedBeforeNext, times(1))(expectedValue) 96 | assertEquals(expectedValue,testingLiveData.value) 97 | inOrder.verifyNoMoreInteractions() 98 | } 99 | 100 | @Test 101 | fun `test LiveData buffer`(){ 102 | val observer= Mockito.mock(Observer::class.java) as Observer> 103 | val sourceLiveData = MutableLiveData() 104 | val testingLiveData = sourceLiveData.buffer(3) 105 | val expectedValue = listOf(1,2,3) 106 | val secondExpectedValue = listOf(4,5,6) 107 | testingLiveData.observeForever(observer) 108 | 109 | sourceLiveData.value = 1 110 | assertEquals(null,testingLiveData.value) 111 | sourceLiveData.value = 2 112 | assertEquals(null,testingLiveData.value) 113 | 114 | sourceLiveData.value = 3 115 | assertEquals(expectedValue,testingLiveData.value) 116 | verify(observer).onChanged(expectedValue) 117 | 118 | sourceLiveData.value = 4 119 | assertEquals(expectedValue,testingLiveData.value) 120 | sourceLiveData.value = 5 121 | assertEquals(expectedValue,testingLiveData.value) 122 | 123 | sourceLiveData.value = 6 124 | assertEquals(secondExpectedValue,testingLiveData.value) 125 | verify(observer).onChanged(secondExpectedValue) 126 | 127 | verifyNoMoreInteractions(observer) 128 | } 129 | 130 | @Test 131 | fun `test LiveData amb and they fire after being amb-ed`(){ 132 | val observer= Mockito.mock(Observer::class.java) as Observer 133 | val sourceLiveData = MutableLiveData() 134 | val secondSourceLiveData = MutableLiveData() 135 | val testingLiveData = amb(sourceLiveData,secondSourceLiveData) 136 | testingLiveData.observeForever(observer) 137 | 138 | //choose the second observer to win the race 139 | secondSourceLiveData.value = 1 140 | sourceLiveData.value = 2 141 | assertEquals(1,testingLiveData.value) 142 | verify(observer).onChanged(1) 143 | sourceLiveData.value = 5 144 | assertEquals(1,testingLiveData.value) 145 | secondSourceLiveData.value = 10 146 | assertEquals(10,testingLiveData.value) 147 | verify(observer).onChanged(10) 148 | 149 | verifyNoMoreInteractions(observer) 150 | } 151 | @Test 152 | fun `test LiveData amb and one has value before amb`(){ 153 | val observer= Mockito.mock(Observer::class.java) as Observer 154 | val sourceLiveData = MutableLiveData() 155 | val secondSourceLiveData = MutableLiveData() 156 | sourceLiveData.value = 2 157 | val testingLiveData = amb(sourceLiveData,secondSourceLiveData) 158 | testingLiveData.observeForever(observer) 159 | verify(observer).onChanged(2) 160 | 161 | //choose the second observer to win the race 162 | secondSourceLiveData.value = 1 163 | assertEquals(2,testingLiveData.value) 164 | sourceLiveData.value = 5 165 | verify(observer).onChanged(5) 166 | assertEquals(5,testingLiveData.value) 167 | 168 | secondSourceLiveData.value = 10 169 | assertEquals(5,testingLiveData.value) 170 | 171 | sourceLiveData.value = 20 172 | assertEquals(20,testingLiveData.value) 173 | verify(observer).onChanged(20) 174 | verify(observer).onChanged(20) 175 | 176 | verifyNoMoreInteractions(observer) 177 | } 178 | 179 | @Test 180 | fun `test LiveData scan with without initial seed, does not emit first item`() { 181 | val observer = Mockito.mock(Observer::class.java) as Observer 182 | val sourceLiveData = MutableLiveData() 183 | val testingLiveData = sourceLiveData.scan { acc, value -> 184 | value?.let { acc?.plus(it) } 185 | } 186 | testingLiveData.observeForever(observer) 187 | assertNull(testingLiveData.value) 188 | verifyZeroInteractions(observer) 189 | } 190 | 191 | @Test 192 | fun `test LiveData scan without initial seed`() { 193 | val observer = Mockito.mock(Observer::class.java) as Observer 194 | val sourceLiveData = MutableLiveData() 195 | val testingLiveData = sourceLiveData.scan { acc, value -> 196 | value?.let { acc?.plus(it) } 197 | } 198 | testingLiveData.observeForever(observer) 199 | assertNull(testingLiveData.value) 200 | 201 | sourceLiveData.value = 1 202 | assertEquals(null, testingLiveData.value) 203 | 204 | sourceLiveData.value = 2 205 | assertEquals(3, testingLiveData.value) 206 | verify(observer).onChanged(3) 207 | 208 | sourceLiveData.value = 5 209 | assertEquals(8, testingLiveData.value) 210 | verify(observer).onChanged(8) 211 | 212 | verifyNoMoreInteractions(observer) 213 | } 214 | 215 | @Test 216 | fun `test LiveData scan with initial seed`() { 217 | val observer = Mockito.mock(Observer::class.java) as Observer 218 | val sourceLiveData = MutableLiveData() 219 | val testingLiveData = sourceLiveData.scan("W") { acc: String, value: Int -> 220 | acc + "X$value" 221 | } 222 | testingLiveData.observeForever(observer) 223 | 224 | assertEquals("W", testingLiveData.value) 225 | verify(observer).onChanged("W") 226 | 227 | sourceLiveData.value = 1 228 | assertEquals("WX1", testingLiveData.value) 229 | verify(observer).onChanged("WX1") 230 | 231 | sourceLiveData.value = 2 232 | assertEquals("WX1X2", testingLiveData.value) 233 | verify(observer).onChanged("WX1X2") 234 | 235 | verifyNoMoreInteractions(observer) 236 | } 237 | } -------------------------------------------------------------------------------- /lives/src/main/java/com/snakydesign/livedataextensions/Combining.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Adib Faramarzi 3 | */ 4 | 5 | @file:JvmName("Lives") 6 | @file:JvmMultifileClass 7 | 8 | package com.snakydesign.livedataextensions 9 | 10 | import androidx.lifecycle.LiveData 11 | import androidx.lifecycle.MediatorLiveData 12 | import androidx.lifecycle.MutableLiveData 13 | import com.snakydesign.livedataextensions.livedata.SingleLiveData 14 | import com.snakydesign.livedataextensions.operators.SingleLiveDataConcat 15 | import java.util.concurrent.atomic.AtomicBoolean 16 | 17 | /** 18 | * Merges this LiveData with another one, and emits any item that was emitted by any of them 19 | */ 20 | fun LiveData.mergeWith(vararg liveDatas: LiveData): LiveData { 21 | val mergeWithArray = mutableListOf>() 22 | mergeWithArray.add(this) 23 | mergeWithArray.addAll(liveDatas) 24 | return merge(mergeWithArray) 25 | } 26 | 27 | /** 28 | * Merges multiple LiveData, and emits any item that was emitted by any of them 29 | */ 30 | fun merge(liveDataList: List>): LiveData { 31 | val finalLiveData: MediatorLiveData = MediatorLiveData() 32 | liveDataList.forEach { liveData -> 33 | 34 | liveData.value?.let { 35 | finalLiveData.value = it 36 | } 37 | 38 | finalLiveData.addSource(liveData) { source -> 39 | finalLiveData.value = source 40 | } 41 | } 42 | return finalLiveData 43 | } 44 | 45 | /** 46 | * Emits the `startingValue` before any other value. 47 | */ 48 | fun LiveData.startWith(startingValue: T?): LiveData { 49 | val finalLiveData = MediatorLiveData() 50 | var startingData: LiveData? = MutableLiveData(startingValue) 51 | finalLiveData.addSource(this) { 52 | if (null != startingData) { 53 | finalLiveData.removeSource(startingData!!) 54 | startingData = null 55 | } 56 | finalLiveData.value = it 57 | } 58 | finalLiveData.addSource(startingData!!) { 59 | finalLiveData.value = it 60 | finalLiveData.removeSource(startingData!!) 61 | startingData = null 62 | } 63 | return finalLiveData 64 | } 65 | 66 | /** 67 | * zips both of the LiveData and emits a value after both of them have emitted their values, 68 | * after that, emits values whenever both of them emit another value. 69 | * 70 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 71 | * objects have a new value, but combineLatest will emit after any of them has a new value. 72 | */ 73 | fun zip(first: LiveData, second: LiveData): LiveData> { 74 | return zip(first, second) { x, y -> Pair(x, y) } 75 | } 76 | 77 | /** 78 | * zips both of the LiveData and emits a value after both of them have emitted their values, 79 | * after that, emits values whenever both of them emit another value. 80 | * 81 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 82 | * objects have a new value, but combineLatest will emit after any of them has a new value. 83 | */ 84 | fun zip(first: LiveData, second: LiveData, zipFunction: (X?, Y?) -> R): LiveData { 85 | val finalLiveData: MediatorLiveData = MediatorLiveData() 86 | 87 | val firstEmit: Emit = Emit() 88 | val secondEmit: Emit = Emit() 89 | 90 | val combine: () -> Unit = { 91 | if (firstEmit.emitted && secondEmit.emitted) { 92 | val combined = zipFunction(firstEmit.value, secondEmit.value) 93 | firstEmit.reset() 94 | secondEmit.reset() 95 | finalLiveData.value = combined 96 | } 97 | } 98 | 99 | finalLiveData.addSource(first) { value -> 100 | firstEmit.value = value 101 | combine() 102 | } 103 | finalLiveData.addSource(second) { value -> 104 | secondEmit.value = value 105 | combine() 106 | } 107 | return finalLiveData 108 | } 109 | 110 | /** 111 | * zips three LiveData and emits a value after all of them have emitted their values, 112 | * after that, emits values whenever all of them emit another value. 113 | * 114 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 115 | * objects have a new value, but combineLatest will emit after any of them has a new value. 116 | */ 117 | fun zip( 118 | first: LiveData, 119 | second: LiveData, 120 | third: LiveData, 121 | zipFunction: (X?, Y?, Z?) -> R 122 | ): LiveData { 123 | val finalLiveData: MediatorLiveData = MediatorLiveData() 124 | 125 | val firstEmit: Emit = Emit() 126 | val secondEmit: Emit = Emit() 127 | val thirdEmit: Emit = Emit() 128 | 129 | val combine: () -> Unit = { 130 | if (firstEmit.emitted && secondEmit.emitted && thirdEmit.emitted) { 131 | val combined = zipFunction(firstEmit.value, secondEmit.value, thirdEmit.value) 132 | firstEmit.reset() 133 | secondEmit.reset() 134 | thirdEmit.reset() 135 | finalLiveData.value = combined 136 | } 137 | } 138 | 139 | finalLiveData.addSource(first) { value -> 140 | firstEmit.value = value 141 | combine() 142 | } 143 | finalLiveData.addSource(second) { value -> 144 | secondEmit.value = value 145 | combine() 146 | } 147 | finalLiveData.addSource(third) { value -> 148 | thirdEmit.value = value 149 | combine() 150 | } 151 | return finalLiveData 152 | } 153 | 154 | /** 155 | * zips three LiveData and emits a value after all of them have emitted their values, 156 | * after that, emits values whenever all of them emit another value. 157 | * 158 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 159 | * objects have a new value, but combineLatest will emit after any of them has a new value. 160 | */ 161 | fun zip(first: LiveData, second: LiveData, third: LiveData): LiveData> { 162 | return zip(first, second, third) { x, y, z -> Triple(x, y, z) } 163 | } 164 | 165 | /** 166 | * Combines the latest values from multiple LiveData objects. 167 | * First emits after all LiveData objects have emitted a value, and will emit afterwards after any 168 | * of them emits a new value. 169 | * 170 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 171 | * objects have a new value, but combineLatest will emit after any of them has a new value. 172 | */ 173 | fun combineLatest(first: LiveData, second: LiveData, combineFunction: (X?, Y?) -> R): LiveData { 174 | val finalLiveData: MediatorLiveData = MediatorLiveData() 175 | 176 | val firstEmit: Emit = Emit() 177 | val secondEmit: Emit = Emit() 178 | 179 | val combine: () -> Unit = { 180 | if (firstEmit.emitted && secondEmit.emitted) { 181 | val combined = combineFunction(firstEmit.value, secondEmit.value) 182 | finalLiveData.value = combined 183 | } 184 | } 185 | 186 | finalLiveData.addSource(first) { value -> 187 | firstEmit.value = value 188 | combine() 189 | } 190 | finalLiveData.addSource(second) { value -> 191 | secondEmit.value = value 192 | combine() 193 | } 194 | return finalLiveData 195 | } 196 | 197 | /** 198 | * Combines the latest values from multiple LiveData objects. 199 | * First emits after all LiveData objects have emitted a value, and will emit afterwards after any 200 | * of them emits a new value. 201 | * 202 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 203 | * objects have a new value, but combineLatest will emit after any of them has a new value. 204 | */ 205 | fun combineLatest(first: LiveData, second: LiveData): LiveData> = 206 | combineLatest(first, second) { x, y -> Pair(x, y) } 207 | 208 | /** 209 | * Combines the latest values from multiple LiveData objects. 210 | * First emits after all LiveData objects have emitted a value, and will emit afterwards after any 211 | * of them emits a new value. 212 | * 213 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 214 | * objects have a new value, but combineLatest will emit after any of them has a new value. 215 | */ 216 | fun combineLatest( 217 | first: LiveData, 218 | second: LiveData, 219 | third: LiveData, 220 | combineFunction: (X?, Y?, Z?) -> R 221 | ): LiveData { 222 | val finalLiveData: MediatorLiveData = MediatorLiveData() 223 | 224 | val firstEmit: Emit = Emit() 225 | val secondEmit: Emit = Emit() 226 | val thirdEmit: Emit = Emit() 227 | 228 | val combine: () -> Unit = { 229 | if (firstEmit.emitted && secondEmit.emitted && thirdEmit.emitted) { 230 | val combined = combineFunction(firstEmit.value, secondEmit.value, thirdEmit.value) 231 | finalLiveData.value = combined 232 | } 233 | } 234 | 235 | finalLiveData.addSource(first) { value -> 236 | firstEmit.value = value 237 | combine() 238 | } 239 | finalLiveData.addSource(second) { value -> 240 | secondEmit.value = value 241 | combine() 242 | } 243 | finalLiveData.addSource(third) { value -> 244 | thirdEmit.value = value 245 | combine() 246 | } 247 | return finalLiveData 248 | } 249 | 250 | /** 251 | * Combines the latest values from multiple LiveData objects. 252 | * First emits after all LiveData objects have emitted a value, and will emit afterwards after any 253 | * of them emits a new value. 254 | * 255 | * The difference between combineLatest and zip is that the zip only emits after all LiveData 256 | * objects have a new value, but combineLatest will emit after any of them has a new value. 257 | */ 258 | fun combineLatest(first: LiveData, second: LiveData, third: LiveData): LiveData> = 259 | combineLatest(first, second, third) { x, y, z -> Triple(x, y, z) } 260 | 261 | /** 262 | * Converts the LiveData to `SingleLiveData` and concats it with the `otherLiveData` and emits their 263 | * values one by one 264 | */ 265 | fun LiveData.then(otherLiveData:LiveData):LiveData{ 266 | return if (this is SingleLiveData){ 267 | when (otherLiveData) { 268 | is SingleLiveData -> SingleLiveDataConcat(this,otherLiveData) 269 | else -> SingleLiveDataConcat(this,otherLiveData.toSingleLiveData()) 270 | } 271 | }else{ 272 | when (otherLiveData) { 273 | is SingleLiveData -> SingleLiveDataConcat(this.toSingleLiveData(),otherLiveData) 274 | else -> SingleLiveDataConcat(this.toSingleLiveData(),otherLiveData.toSingleLiveData()) 275 | } 276 | } 277 | } 278 | fun LiveData.concatWith(otherLiveData:LiveData) = then(otherLiveData) 279 | 280 | /** 281 | * Concats the given LiveData together and emits their values one by one in order 282 | */ 283 | fun concat(vararg liveData:LiveData):LiveData{ 284 | val liveDataList = mutableListOf>() 285 | liveData.forEach { 286 | if( it is SingleLiveData) 287 | liveDataList.add(it) 288 | else 289 | liveDataList.add(it.toSingleLiveData()) 290 | } 291 | return SingleLiveDataConcat(liveDataList) 292 | } 293 | 294 | /** 295 | * Samples the current live data with other live data, resulting in a live data that emits the last 296 | * value emitted by the original live data (if there were any values emitted) whenever the other live 297 | * data emits 298 | */ 299 | fun LiveData.sampleWith(other: LiveData<*>): LiveData { 300 | val finalLiveData: MediatorLiveData = MediatorLiveData() 301 | val hasValueToConsume = AtomicBoolean(false) 302 | var latestValue: T? = null 303 | finalLiveData.addSource(this) { 304 | hasValueToConsume.set(true) 305 | latestValue = it 306 | } 307 | finalLiveData.addSource(other) { 308 | if (hasValueToConsume.compareAndSet(true, false)) { 309 | finalLiveData.value = latestValue 310 | } 311 | } 312 | return finalLiveData 313 | } 314 | 315 | /** 316 | * Wrapper that wraps an emitted value. 317 | */ 318 | private class Emit { 319 | 320 | internal var emitted: Boolean = false 321 | 322 | internal var value: T? = null 323 | set(value) { 324 | field = value 325 | emitted = true 326 | } 327 | 328 | fun reset() { 329 | value = null 330 | emitted = false 331 | } 332 | } -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/CombiningTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import org.junit.After 7 | import org.junit.Assert 8 | import org.junit.Before 9 | import org.junit.Rule 10 | import org.junit.Test 11 | import org.mockito.ArgumentMatchers.any 12 | import org.mockito.Mockito 13 | import org.mockito.Mockito.never 14 | import org.mockito.Mockito.verify 15 | import org.mockito.Mockito.verifyNoMoreInteractions 16 | import org.mockito.Mockito.verifyZeroInteractions 17 | 18 | /** 19 | * Created by Adib Faramarzi 20 | */ 21 | @Suppress("UNCHECKED_CAST") 22 | class CombiningTest { 23 | @Rule 24 | @JvmField 25 | val rule = InstantTaskExecutorRule() 26 | 27 | @Before 28 | fun setUp() { 29 | } 30 | 31 | @After 32 | fun tearDown() { 33 | } 34 | 35 | @Test 36 | fun `test LiveData merge multiple LiveData`() { 37 | val observer = Mockito.mock(Observer::class.java) as Observer 38 | val sourceLiveData1 = MutableLiveData() 39 | val sourceLiveData2 = MutableLiveData() 40 | sourceLiveData1.value = 2 41 | sourceLiveData2.value = 3 42 | val testingLiveData = merge(listOf(sourceLiveData1, sourceLiveData2)) 43 | testingLiveData.observeForever(observer) 44 | Assert.assertEquals(3, testingLiveData.value) 45 | verify(observer).onChanged(2) 46 | verify(observer).onChanged(3) 47 | 48 | 49 | sourceLiveData1.value = 5 50 | Assert.assertEquals(5, testingLiveData.value) 51 | Mockito.verify(observer).onChanged(5) 52 | 53 | sourceLiveData2.value = 8 54 | Assert.assertEquals(8, testingLiveData.value) 55 | verify(observer).onChanged(8) 56 | verifyNoMoreInteractions(observer) 57 | } 58 | 59 | @Test 60 | fun `test LiveData merge with another LiveData`() { 61 | val observer = Mockito.mock(Observer::class.java) as Observer 62 | val sourceLiveData1 = MutableLiveData() 63 | val sourceLiveData2 = MutableLiveData() 64 | sourceLiveData1.value = 2 65 | sourceLiveData2.value = 3 66 | val testingLiveData = sourceLiveData1.mergeWith(sourceLiveData2) 67 | testingLiveData.observeForever(observer) 68 | Assert.assertEquals(3, testingLiveData.value) 69 | verify(observer).onChanged(2) 70 | verify(observer).onChanged(3) 71 | 72 | 73 | sourceLiveData1.value = 5 74 | Assert.assertEquals(5, testingLiveData.value) 75 | Mockito.verify(observer).onChanged(5) 76 | 77 | sourceLiveData2.value = 8 78 | Assert.assertEquals(8, testingLiveData.value) 79 | verify(observer).onChanged(8) 80 | verifyNoMoreInteractions(observer) 81 | } 82 | 83 | @Test 84 | fun `test LiveData startWith`() { 85 | val observer = Mockito.mock(Observer::class.java) as Observer 86 | val sourceLiveData1 = MutableLiveData() 87 | 88 | val testingLiveData = sourceLiveData1.startWith(2) 89 | testingLiveData.observeForever(observer) 90 | Assert.assertEquals(2, testingLiveData.value) 91 | verify(observer).onChanged(2) 92 | 93 | sourceLiveData1.value = 3 94 | verify(observer).onChanged(3) 95 | Assert.assertEquals(3, testingLiveData.value) 96 | 97 | verifyNoMoreInteractions(observer) 98 | } 99 | 100 | @Test 101 | fun `test LiveData zip with another LiveData`() { 102 | val observer = Mockito.mock(Observer::class.java) as Observer> 103 | val sourceLiveData1 = MutableLiveData() 104 | val sourceLiveData2 = MutableLiveData() 105 | val expectedResult = Pair(true, 3) 106 | val testingLiveData = zip(sourceLiveData1, sourceLiveData2) { b, i -> Pair(b, i) } 107 | testingLiveData.observeForever(observer) 108 | 109 | 110 | sourceLiveData1.value = true 111 | Assert.assertEquals(null, testingLiveData.value) 112 | sourceLiveData2.value = 3 113 | Assert.assertEquals(expectedResult, testingLiveData.value) 114 | verify(observer).onChanged(expectedResult) 115 | sourceLiveData2.value = 4 // should not trigger the live data 116 | 117 | verifyNoMoreInteractions(observer) 118 | } 119 | 120 | @Test 121 | fun `test LiveData zip with another LiveData for second emission`() { 122 | val observer = Mockito.mock(Observer::class.java) as Observer> 123 | val sourceLiveData1 = MutableLiveData() 124 | val sourceLiveData2 = MutableLiveData() 125 | val expectedResult = Pair(true, 3) 126 | val expectedResult2 = Pair(false, 4) 127 | val testingLiveData = zip(sourceLiveData1, sourceLiveData2) { b, i -> Pair(b, i) } 128 | testingLiveData.observeForever(observer) 129 | 130 | 131 | sourceLiveData1.value = true 132 | Assert.assertEquals(null, testingLiveData.value) 133 | sourceLiveData2.value = 3 134 | Assert.assertEquals(expectedResult, testingLiveData.value) 135 | verify(observer).onChanged(expectedResult) 136 | sourceLiveData2.value = 4 137 | sourceLiveData1.value = false // should not trigger the live data 138 | 139 | verify(observer).onChanged(expectedResult2) 140 | verifyNoMoreInteractions(observer) 141 | } 142 | 143 | @Test 144 | fun `test LiveData zip with 2 other LiveData`() { 145 | val observer = Mockito.mock(Observer::class.java) as Observer> 146 | val sourceLiveData1 = MutableLiveData() 147 | val sourceLiveData2 = MutableLiveData() 148 | val sourceLiveData3 = MutableLiveData() 149 | val expectedResult = Triple(true, 3, "hello") 150 | val testingLiveData = zip(sourceLiveData1, sourceLiveData2, sourceLiveData3) { b, i, s -> Triple(b, i, s) } 151 | testingLiveData.observeForever(observer) 152 | 153 | 154 | sourceLiveData1.value = true 155 | Assert.assertEquals(null, testingLiveData.value) 156 | sourceLiveData2.value = 3 157 | Assert.assertEquals(null, testingLiveData.value) 158 | sourceLiveData3.value = "hello" 159 | Assert.assertEquals(expectedResult, testingLiveData.value) 160 | verify(observer).onChanged(expectedResult) 161 | 162 | verifyNoMoreInteractions(observer) 163 | } 164 | 165 | @Test 166 | fun `test LiveData zip with null values`() { 167 | val observer = Mockito.mock(Observer::class.java) as Observer> 168 | val sourceLiveData1 = MutableLiveData() 169 | val sourceLiveData2 = MutableLiveData() 170 | val sourceLiveData3 = MutableLiveData() 171 | val testingLiveData = zip(sourceLiveData1, sourceLiveData2, sourceLiveData3) 172 | testingLiveData.observeForever(observer) 173 | 174 | // Ensure there is no emit until all sources have emitted 175 | sourceLiveData1.value = null 176 | sourceLiveData2.value = null 177 | Assert.assertEquals(null, testingLiveData.value) 178 | verify(observer, never()).onChanged(any()) 179 | 180 | // After all emitted null, we expect an emit with null values 181 | sourceLiveData3.value = null 182 | val expectedResult = Triple(null, null, null) 183 | Assert.assertEquals(expectedResult, testingLiveData.value) 184 | verify(observer).onChanged(expectedResult) 185 | 186 | // Ensure there is no emit until all sources have emitted a new value 187 | sourceLiveData2.value = 42 188 | sourceLiveData3.value = "42" 189 | verifyZeroInteractions(observer) 190 | 191 | // After all emitted new value, we expect another emit 192 | sourceLiveData1.value = true 193 | val expectedResult2 = Triple(true, 42, "42") 194 | Assert.assertEquals(expectedResult2, testingLiveData.value) 195 | verify(observer).onChanged(expectedResult2) 196 | } 197 | 198 | @Test 199 | fun `test LiveData sample with another LiveData`() { 200 | val observer = Mockito.mock(Observer::class.java) as Observer> 201 | val sourceLiveData1 = MutableLiveData() 202 | val sourceLiveData2 = MutableLiveData() 203 | val sourceLiveData3 = MutableLiveData() 204 | val expectedResult = Triple(true, 3, "hello") 205 | val testingLiveData = zip(sourceLiveData1, sourceLiveData2, sourceLiveData3) { b, i, s -> Triple(b, i, s) } 206 | testingLiveData.observeForever(observer) 207 | 208 | 209 | sourceLiveData1.value = true 210 | Assert.assertEquals(null, testingLiveData.value) 211 | sourceLiveData2.value = 3 212 | Assert.assertEquals(null, testingLiveData.value) 213 | sourceLiveData3.value = "hello" 214 | Assert.assertEquals(expectedResult, testingLiveData.value) 215 | verify(observer).onChanged(expectedResult) 216 | 217 | verifyNoMoreInteractions(observer) 218 | } 219 | 220 | @Test 221 | fun `test LiveData combineLatest with another LiveData`() { 222 | val observer = Mockito.mock(Observer::class.java) as Observer> 223 | val sourceLiveData1 = MutableLiveData() 224 | val sourceLiveData2 = MutableLiveData() 225 | val expectedResult = Pair(true, 3) 226 | val expectedResult2 = Pair(true, 4) 227 | val expectedResult3 = Pair(false, 4) 228 | val testingLiveData = combineLatest(sourceLiveData1, sourceLiveData2) { boolean, int -> Pair(boolean, int) } 229 | testingLiveData.observeForever(observer) 230 | 231 | 232 | sourceLiveData1.value = true 233 | Assert.assertEquals(null, testingLiveData.value) 234 | sourceLiveData2.value = 3 235 | 236 | Assert.assertEquals(expectedResult, testingLiveData.value) 237 | verify(observer).onChanged(expectedResult) 238 | 239 | sourceLiveData2.value = 4 240 | 241 | Assert.assertEquals(expectedResult2, testingLiveData.value) 242 | verify(observer).onChanged(expectedResult2) 243 | 244 | sourceLiveData1.value = false 245 | 246 | 247 | Assert.assertEquals(expectedResult3, testingLiveData.value) 248 | verify(observer).onChanged(expectedResult3) 249 | verifyNoMoreInteractions(observer) 250 | } 251 | 252 | @Test 253 | fun `test LiveData combineLatest with null values`() { 254 | val observer = Mockito.mock(Observer::class.java) as Observer> 255 | val sourceLiveData1 = MutableLiveData() 256 | val sourceLiveData2 = MutableLiveData() 257 | val testingLiveData = combineLatest(sourceLiveData1, sourceLiveData2) { boolean, int -> Pair(boolean, int) } 258 | testingLiveData.observeForever(observer) 259 | 260 | // Ensure there is no emit until all sources have emitted 261 | sourceLiveData1.value = null 262 | Assert.assertEquals(null, testingLiveData.value) 263 | verify(observer, never()).onChanged(any()) 264 | 265 | // After all emitted null, we expect an emit with null values 266 | sourceLiveData2.value = null 267 | val expectedResult = Pair(null, null) 268 | Assert.assertEquals(expectedResult, testingLiveData.value) 269 | verify(observer).onChanged(expectedResult) 270 | 271 | // One emitted a non-null value 272 | sourceLiveData2.value = 4 273 | val expectedResult2 = Pair(null, 4) 274 | Assert.assertEquals(expectedResult2, testingLiveData.value) 275 | verify(observer).onChanged(expectedResult2) 276 | 277 | // Both emitted a non-null value 278 | sourceLiveData1.value = false 279 | val expectedResult3 = Pair(false, 4) 280 | Assert.assertEquals(expectedResult3, testingLiveData.value) 281 | verify(observer).onChanged(expectedResult3) 282 | verifyNoMoreInteractions(observer) 283 | } 284 | 285 | @Test 286 | fun `test LiveData combineLatest three with null values`() { 287 | val observer = Mockito.mock(Observer::class.java) as Observer> 288 | val sourceLiveData1 = MutableLiveData() 289 | val sourceLiveData2 = MutableLiveData() 290 | val sourceLiveData3 = MutableLiveData() 291 | val testingLiveData = combineLatest(sourceLiveData1, sourceLiveData2, sourceLiveData3) { boolean, int, long -> 292 | Triple(boolean, int, long) 293 | } 294 | testingLiveData.observeForever(observer) 295 | 296 | // Ensure there is no emit until all sources have emitted 297 | sourceLiveData1.value = null 298 | sourceLiveData2.value = null 299 | Assert.assertEquals(null, testingLiveData.value) 300 | verify(observer, never()).onChanged(any()) 301 | 302 | // After all emitted null, we expect an emit with null values 303 | sourceLiveData3.value = null 304 | val expectedResult = Triple(null, null, null) 305 | Assert.assertEquals(expectedResult, testingLiveData.value) 306 | verify(observer).onChanged(expectedResult) 307 | } 308 | 309 | @Test 310 | fun `test LiveData sampleWith another live data`() { 311 | val observer = Mockito.mock(Observer::class.java) as Observer 312 | val sourceLiveData = MutableLiveData() 313 | val samplerLiveData = MutableLiveData() 314 | val testingLiveData = sourceLiveData.sampleWith(samplerLiveData) 315 | testingLiveData.observeForever(observer) 316 | 317 | 318 | sourceLiveData.value = 1 319 | Assert.assertNull(testingLiveData.value) 320 | 321 | sourceLiveData.value = 2 322 | Assert.assertNull(testingLiveData.value) 323 | samplerLiveData.value = true 324 | Assert.assertEquals(2, testingLiveData.value) 325 | verify(observer).onChanged(2) 326 | 327 | sourceLiveData.value = 3 328 | Assert.assertEquals(2, testingLiveData.value) 329 | samplerLiveData.value = true 330 | Assert.assertEquals(3, testingLiveData.value) 331 | verify(observer).onChanged(3) 332 | 333 | samplerLiveData.value = true 334 | sourceLiveData.value = 5 335 | 336 | verifyNoMoreInteractions(observer) 337 | } 338 | 339 | @Test 340 | fun `test LiveData sampleWith another live data to not emit when there are no new values`() { 341 | val observer = Mockito.mock(Observer::class.java) as Observer 342 | val sourceLiveData = MutableLiveData() 343 | val samplerLiveData = MutableLiveData() 344 | val testingLiveData = sourceLiveData.sampleWith(samplerLiveData) 345 | testingLiveData.observeForever(observer) 346 | 347 | samplerLiveData.value = true 348 | samplerLiveData.value = true 349 | samplerLiveData.value = true 350 | Assert.assertNull(testingLiveData.value) 351 | sourceLiveData.value = 5 352 | samplerLiveData.value = true 353 | Assert.assertEquals(5, testingLiveData.value) 354 | verify(observer).onChanged(5) 355 | 356 | verifyNoMoreInteractions(observer) 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /lives/src/test/java/com/snakydesign/livedataextensions/FilteringTest.kt: -------------------------------------------------------------------------------- 1 | package com.snakydesign.livedataextensions 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import org.junit.* 7 | import org.mockito.Mockito 8 | import org.mockito.Mockito.times 9 | import kotlin.test.assertEquals 10 | 11 | /** 12 | * Created by Adib Faramarzi 13 | */ 14 | @Suppress("UNCHECKED_CAST") 15 | class FilteringTest { 16 | 17 | 18 | @Rule 19 | @JvmField 20 | val rule = InstantTaskExecutorRule() 21 | 22 | @Before 23 | fun setUp() { 24 | } 25 | 26 | @After 27 | fun tearDown() { 28 | } 29 | 30 | @Test 31 | fun `test LiveData filter`(){ 32 | val observer= Mockito.mock(Observer::class.java) as Observer 33 | val testingLiveData = liveDataOf(2).filter { 34 | it!=null && it > 1 35 | } 36 | testingLiveData.observeForever(observer) 37 | 38 | Assert.assertEquals(2, testingLiveData.value) 39 | Mockito.verify(observer).onChanged(2) 40 | Mockito.verifyNoMoreInteractions(observer) 41 | } 42 | 43 | @Test 44 | fun `test LiveData distinct`(){ 45 | val observer= Mockito.mock(Observer::class.java) as Observer 46 | val sourceLiveData = MutableLiveData() 47 | val testingLiveData = sourceLiveData.distinct() 48 | testingLiveData.observeForever(observer) 49 | 50 | sourceLiveData.value = 2 51 | assertEquals(2,testingLiveData.value) 52 | Mockito.verify(observer).onChanged(2) 53 | 54 | sourceLiveData.value = 2 55 | assertEquals(2,testingLiveData.value) 56 | 57 | Mockito.verifyNoMoreInteractions(observer) 58 | } 59 | 60 | @Test 61 | fun `test LiveData distinct when new value inserted`(){ 62 | val observer= Mockito.mock(Observer::class.java) as Observer 63 | val sourceLiveData = MutableLiveData() 64 | val testingLiveData = sourceLiveData.distinct() 65 | testingLiveData.observeForever(observer) 66 | 67 | sourceLiveData.value = 2 68 | assertEquals(2,testingLiveData.value) 69 | Mockito.verify(observer).onChanged(2) 70 | 71 | sourceLiveData.value = 2 72 | assertEquals(2,testingLiveData.value) 73 | 74 | sourceLiveData.value = 3 75 | assertEquals(3,testingLiveData.value) 76 | Mockito.verify(observer).onChanged(3) 77 | 78 | sourceLiveData.value = 2 79 | assertEquals(3,testingLiveData.value) 80 | 81 | Mockito.verifyNoMoreInteractions(observer) 82 | } 83 | 84 | @Test 85 | fun `test LiveData distinctUntilChanged same value given`(){ 86 | val observer= Mockito.mock(Observer::class.java) as Observer 87 | val sourceLiveData = MutableLiveData() 88 | val testingLiveData = sourceLiveData.distinctUntilChanged() 89 | testingLiveData.observeForever(observer) 90 | 91 | sourceLiveData.value = 2 92 | assertEquals(2, testingLiveData.value) 93 | Mockito.verify(observer).onChanged(2) 94 | 95 | sourceLiveData.value = 2 96 | assertEquals(2, testingLiveData.value) 97 | 98 | Mockito.verifyNoMoreInteractions(observer) 99 | } 100 | 101 | @Test 102 | fun `test LiveData distinctUntilChanged initial null value`() { 103 | val observer = Mockito.mock(Observer::class.java) as Observer 104 | val sourceLiveData = MutableLiveData() 105 | val testingLiveData = sourceLiveData.distinctUntilChanged() 106 | testingLiveData.observeForever(observer) 107 | 108 | sourceLiveData.value = null 109 | assertEquals(null, testingLiveData.value) 110 | 111 | } 112 | 113 | @Test 114 | fun `test LiveData distinctUntilChanged updating source from observer doesn't trigger change`() { 115 | val sourceLiveData = MutableLiveData() 116 | var timesCalled = 0 117 | val observer = Observer { t -> 118 | timesCalled++ 119 | // set same value from observer 120 | sourceLiveData.value = t 121 | } 122 | val testingLiveData = sourceLiveData.distinctUntilChanged() 123 | testingLiveData.observeForever(observer) 124 | 125 | sourceLiveData.value = 2 126 | 127 | if (timesCalled > 1) { 128 | throw IllegalStateException() 129 | } 130 | } 131 | 132 | @Test 133 | fun `test LiveData distinctUntilChanged`(){ 134 | val observer= Mockito.mock(Observer::class.java) as Observer 135 | val sourceLiveData = MutableLiveData() 136 | val testingLiveData = sourceLiveData.distinctUntilChanged() 137 | testingLiveData.observeForever(observer) 138 | 139 | sourceLiveData.value = 2 140 | assertEquals(2,testingLiveData.value) 141 | Mockito.verify(observer).onChanged(2) 142 | 143 | sourceLiveData.value = 2 144 | assertEquals(2,testingLiveData.value) 145 | 146 | sourceLiveData.value = 3 147 | assertEquals(3,testingLiveData.value) 148 | Mockito.verify(observer).onChanged(3) 149 | 150 | Mockito.verifyNoMoreInteractions(observer) 151 | } 152 | 153 | @Test 154 | fun `test LiveData first without initial value`(){ 155 | val observer= Mockito.mock(Observer::class.java) as Observer 156 | val sourceLiveData = MutableLiveData() 157 | val testingLiveData = sourceLiveData.first() 158 | testingLiveData.observeForever(observer) 159 | 160 | sourceLiveData.value = 2 161 | assertEquals(2,testingLiveData.value) 162 | Mockito.verify(observer).onChanged(2) 163 | 164 | sourceLiveData.value = 3 165 | assertEquals(2,testingLiveData.value) 166 | 167 | Mockito.verifyNoMoreInteractions(observer) 168 | } 169 | 170 | @Test 171 | fun `test LiveData first with initial value`(){ 172 | val observer= Mockito.mock(Observer::class.java) as Observer 173 | val sourceLiveData = MutableLiveData() 174 | sourceLiveData.value = 2 175 | val testingLiveData = sourceLiveData.first() 176 | testingLiveData.observeForever(observer) 177 | 178 | assertEquals(2,testingLiveData.value) 179 | Mockito.verify(observer).onChanged(2) 180 | 181 | sourceLiveData.value = 3 182 | assertEquals(2,testingLiveData.value) 183 | 184 | Mockito.verifyNoMoreInteractions(observer) 185 | } 186 | 187 | @Test 188 | fun `test LiveData take(2) without initial value`(){ 189 | val observer= Mockito.mock(Observer::class.java) as Observer 190 | val sourceLiveData = MutableLiveData() 191 | val testingLiveData = sourceLiveData.take(2) 192 | testingLiveData.observeForever(observer) 193 | 194 | sourceLiveData.value = 2 195 | assertEquals(2,testingLiveData.value) 196 | Mockito.verify(observer).onChanged(2) 197 | 198 | sourceLiveData.value = 3 199 | assertEquals(3,testingLiveData.value) 200 | Mockito.verify(observer).onChanged(3) 201 | 202 | sourceLiveData.value = 4 203 | 204 | Mockito.verifyNoMoreInteractions(observer) 205 | } 206 | 207 | @Test 208 | fun `test LiveData take(2) with initial value`(){ 209 | val observer= Mockito.mock(Observer::class.java) as Observer 210 | val sourceLiveData = MutableLiveData() 211 | sourceLiveData.value = 2 212 | val testingLiveData = sourceLiveData.take(2) 213 | testingLiveData.observeForever(observer) 214 | 215 | assertEquals(2,testingLiveData.value) 216 | Mockito.verify(observer).onChanged(2) 217 | 218 | sourceLiveData.value = 3 219 | assertEquals(3,testingLiveData.value) 220 | Mockito.verify(observer).onChanged(3) 221 | 222 | sourceLiveData.value = 4 223 | 224 | Mockito.verifyNoMoreInteractions(observer) 225 | } 226 | 227 | @Test 228 | fun `test LiveData takeUntil without initial predicate met`(){ 229 | val observer= Mockito.mock(Observer::class.java) as Observer 230 | val sourceLiveData = MutableLiveData() 231 | val testingLiveData = sourceLiveData.takeUntil { it!=null && it >= 3 } 232 | testingLiveData.observeForever(observer) 233 | 234 | sourceLiveData.value = 1 235 | assertEquals(1,testingLiveData.value) 236 | Mockito.verify(observer).onChanged(1) 237 | 238 | sourceLiveData.value = 2 239 | assertEquals(2,testingLiveData.value) 240 | Mockito.verify(observer).onChanged(2) 241 | 242 | sourceLiveData.value = 3 243 | 244 | sourceLiveData.value = 2 245 | 246 | Mockito.verifyNoMoreInteractions(observer) 247 | } 248 | 249 | @Test 250 | fun `test LiveData takeUntil with initial predicate met`(){ 251 | val observer= Mockito.mock(Observer::class.java) as Observer 252 | val sourceLiveData = MutableLiveData() 253 | sourceLiveData.value = 5 254 | val testingLiveData = sourceLiveData.takeUntil { it!=null && it >= 3 } 255 | testingLiveData.observeForever(observer) 256 | 257 | assertEquals(null,testingLiveData.value) 258 | 259 | sourceLiveData.value = 3 260 | 261 | sourceLiveData.value = 2 262 | 263 | Mockito.verifyNoMoreInteractions(observer) 264 | } 265 | 266 | @Test 267 | fun `test LiveData skip(1) without initial value`(){ 268 | val observer= Mockito.mock(Observer::class.java) as Observer 269 | val sourceLiveData = MutableLiveData() 270 | val testingLiveData = sourceLiveData.skip(1) 271 | testingLiveData.observeForever(observer) 272 | 273 | sourceLiveData.value = 2 274 | assertEquals(null,testingLiveData.value) 275 | 276 | sourceLiveData.value = 3 277 | assertEquals(3,testingLiveData.value) 278 | Mockito.verify(observer).onChanged(3) 279 | 280 | sourceLiveData.value = 4 281 | assertEquals(4,testingLiveData.value) 282 | Mockito.verify(observer).onChanged(4) 283 | 284 | Mockito.verifyNoMoreInteractions(observer) 285 | } 286 | 287 | @Test 288 | fun `test LiveData skip(1) with initial value`(){ 289 | val observer= Mockito.mock(Observer::class.java) as Observer 290 | val sourceLiveData = MutableLiveData() 291 | sourceLiveData.value = 2 292 | val testingLiveData = sourceLiveData.skip(1) 293 | testingLiveData.observeForever(observer) 294 | 295 | assertEquals(null,testingLiveData.value) 296 | 297 | sourceLiveData.value = 3 298 | assertEquals(3,testingLiveData.value) 299 | Mockito.verify(observer).onChanged(3) 300 | 301 | sourceLiveData.value = 4 302 | assertEquals(4,testingLiveData.value) 303 | Mockito.verify(observer).onChanged(4) 304 | 305 | Mockito.verifyNoMoreInteractions(observer) 306 | } 307 | 308 | @Test 309 | fun `test LiveData skipUntil without initial predicate met`(){ 310 | val observer= Mockito.mock(Observer::class.java) as Observer 311 | val sourceLiveData = MutableLiveData() 312 | val testingLiveData = sourceLiveData.skipUntil { it!=null && it >= 3 } 313 | testingLiveData.observeForever(observer) 314 | 315 | sourceLiveData.value = 1 316 | sourceLiveData.value = 2 317 | 318 | sourceLiveData.value = 3 319 | assertEquals(3,testingLiveData.value) 320 | Mockito.verify(observer).onChanged(3) 321 | 322 | sourceLiveData.value = 1 323 | assertEquals(1,testingLiveData.value) 324 | Mockito.verify(observer).onChanged(1) 325 | 326 | sourceLiveData.value = 7 327 | assertEquals(7,testingLiveData.value) 328 | Mockito.verify(observer).onChanged(7) 329 | 330 | Mockito.verifyNoMoreInteractions(observer) 331 | } 332 | 333 | @Test 334 | fun `test LiveData skipUntil with initial predicate met`(){ 335 | val observer= Mockito.mock(Observer::class.java) as Observer 336 | val sourceLiveData = MutableLiveData() 337 | sourceLiveData.value = 3 338 | val testingLiveData = sourceLiveData.skipUntil { it!=null && it >= 3 } 339 | testingLiveData.observeForever(observer) 340 | 341 | assertEquals(3,testingLiveData.value) 342 | Mockito.verify(observer).onChanged(3) 343 | 344 | sourceLiveData.value = 1 345 | assertEquals(1,testingLiveData.value) 346 | Mockito.verify(observer).onChanged(1) 347 | 348 | sourceLiveData.value = 7 349 | assertEquals(7,testingLiveData.value) 350 | Mockito.verify(observer).onChanged(7) 351 | 352 | Mockito.verifyNoMoreInteractions(observer) 353 | } 354 | 355 | @Test 356 | fun `test LiveData elementAt(1) without initial values`(){ 357 | val observer= Mockito.mock(Observer::class.java) as Observer 358 | val sourceLiveData = MutableLiveData() 359 | val testingLiveData = sourceLiveData.elementAt(1) 360 | testingLiveData.observeForever(observer) 361 | 362 | sourceLiveData.value = 1 363 | assertEquals(null,testingLiveData.value) 364 | 365 | sourceLiveData.value = 2 366 | assertEquals(2,testingLiveData.value) 367 | Mockito.verify(observer).onChanged(2) 368 | 369 | sourceLiveData.value = 3 370 | assertEquals(2,testingLiveData.value) 371 | 372 | Mockito.verifyNoMoreInteractions(observer) 373 | } 374 | 375 | @Test 376 | fun `test LiveData elementAt(1) with initial values that should be ignored`(){ 377 | val observer= Mockito.mock(Observer::class.java) as Observer 378 | val sourceLiveData = MutableLiveData() 379 | sourceLiveData.value = 1 380 | sourceLiveData.value = 2 381 | val testingLiveData = sourceLiveData.elementAt(1) 382 | testingLiveData.observeForever(observer) 383 | 384 | sourceLiveData.value = 3 385 | assertEquals(null,testingLiveData.value) 386 | 387 | sourceLiveData.value = 4 388 | assertEquals(4,testingLiveData.value) 389 | Mockito.verify(observer).onChanged(4) 390 | 391 | sourceLiveData.value = 5 392 | assertEquals(4,testingLiveData.value) 393 | 394 | Mockito.verifyNoMoreInteractions(observer) 395 | } 396 | 397 | @Test 398 | fun `test LiveData nonNull when initial is emitted a null`(){ 399 | val observer= Mockito.mock(Observer::class.java) as Observer 400 | val sourceLiveData = MutableLiveData() 401 | sourceLiveData.value = null 402 | val testingLiveData = sourceLiveData.nonNull() 403 | testingLiveData.observeForever(observer) 404 | Mockito.verifyNoMoreInteractions(observer) 405 | } 406 | 407 | @Test 408 | fun `test LiveData nonNull when initial is emitted a null and emits non null values after`(){ 409 | val observer= Mockito.mock(Observer::class.java) as Observer 410 | val sourceLiveData = MutableLiveData() 411 | sourceLiveData.value = null 412 | val testingLiveData = sourceLiveData.nonNull() 413 | testingLiveData.observeForever(observer) 414 | 415 | sourceLiveData.value = 2 416 | assertEquals(2,testingLiveData.value) 417 | Mockito.verify(observer).onChanged(2) 418 | 419 | sourceLiveData.value = 3 420 | assertEquals(3,testingLiveData.value) 421 | Mockito.verify(observer).onChanged(3) 422 | 423 | sourceLiveData.value = null 424 | 425 | Mockito.verifyNoMoreInteractions(observer) 426 | } 427 | 428 | @Test 429 | fun `test LiveData defaultIfNull when initial is emitted a null`(){ 430 | val observer= Mockito.mock(Observer::class.java) as Observer 431 | val sourceLiveData = MutableLiveData() 432 | sourceLiveData.value = null 433 | val testingLiveData = sourceLiveData.defaultIfNull(2) 434 | testingLiveData.observeForever(observer) 435 | 436 | assertEquals(2,testingLiveData.value) 437 | Mockito.verify(observer).onChanged(2) 438 | 439 | Mockito.verifyNoMoreInteractions(observer) 440 | } 441 | 442 | @Test 443 | fun `test LiveData defaultIfNull when initial is emitted a null and emits non null values after`(){ 444 | val observer= Mockito.mock(Observer::class.java) as Observer 445 | val sourceLiveData = MutableLiveData() 446 | sourceLiveData.value = null 447 | val testingLiveData = sourceLiveData.defaultIfNull(5) 448 | testingLiveData.observeForever(observer) 449 | 450 | assertEquals(5,testingLiveData.value) 451 | Mockito.verify(observer).onChanged(5) 452 | 453 | sourceLiveData.value = 2 454 | assertEquals(2,testingLiveData.value) 455 | Mockito.verify(observer).onChanged(2) 456 | 457 | sourceLiveData.value = 3 458 | assertEquals(3,testingLiveData.value) 459 | Mockito.verify(observer).onChanged(3) 460 | 461 | sourceLiveData.value = null 462 | assertEquals(5,testingLiveData.value) 463 | Mockito.verify(observer,times(2)).onChanged(5) 464 | 465 | Mockito.verifyNoMoreInteractions(observer) 466 | } 467 | } --------------------------------------------------------------------------------