├── 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 | [](https://travis-ci.org/adibfara/Lives) [](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