diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/CHANGELOG.MD b/CHANGELOG.MD new file mode 100644 index 0000000..fe05126 --- /dev/null +++ b/CHANGELOG.MD @@ -0,0 +1,6 @@ +# Changelog + +## [Unreleased] +- PublishLiveData and ReplayLiveData implemented +- Initial commit + diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..ac8494d --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2019] [IBRAHIM YILMAZ cs.ibrahimyilmaz@gmail.com] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/arch_data.iml b/arch_data.iml new file mode 100644 index 0000000..ca1d597 --- /dev/null +++ b/arch_data.iml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2a0b9a6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 28 + + + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + } +} + +dependencies { + def lifecycle_version = "2.0.0" + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" + annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" + testImplementation "org.mockito:mockito-core:$versionMockito" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$versionMockitoKotlin" + kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + testImplementation "androidx.arch.core:core-testing:$lifecycle_version" +} +repositories { + mavenCentral() +} diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/src/androidTest/java/me/ibrahimyilmaz/archrelay/ExampleInstrumentedTest.kt b/src/androidTest/java/me/ibrahimyilmaz/archrelay/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b5759e4 --- /dev/null +++ b/src/androidTest/java/me/ibrahimyilmaz/archrelay/ExampleInstrumentedTest.kt @@ -0,0 +1,27 @@ +package me.ibrahimyilmaz.archrelay + +import android.content.Context + +import org.junit.Test +import org.junit.runner.RunWith + +import androidx.test.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 + +import org.junit.Assert.assertEquals + +/** + * Instrumented test, which will execute on an Android device. + * + * @see [Testing documentation](http://d.android.com/tools/testing) + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + + assertEquals("me.ibrahimyilmaz.archrelay.test", appContext.packageName) + } +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2e40329 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/src/main/java/me/ibrahimyilmaz/arch_data/LiveDataExtensions.kt b/src/main/java/me/ibrahimyilmaz/arch_data/LiveDataExtensions.kt new file mode 100644 index 0000000..e7cc579 --- /dev/null +++ b/src/main/java/me/ibrahimyilmaz/arch_data/LiveDataExtensions.kt @@ -0,0 +1,13 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.lifecycle.MutableLiveData + + +inline fun mutableLiveDataOf(): MutableLiveData = MutableLiveData() + +inline fun publishLiveDataOf(): PublishLiveData = PublishLiveData() + +inline fun replayLiveDataOf(): ReplayLiveData = ReplayLiveData() + + + diff --git a/src/main/java/me/ibrahimyilmaz/arch_data/MemoryLessLiveData.kt b/src/main/java/me/ibrahimyilmaz/arch_data/MemoryLessLiveData.kt new file mode 100644 index 0000000..a6a3c34 --- /dev/null +++ b/src/main/java/me/ibrahimyilmaz/arch_data/MemoryLessLiveData.kt @@ -0,0 +1,46 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer + +internal class MemoryLessLiveData : MutableLiveData() { + + private val singleDataEvent = mutableLiveDataOf>() + + private val observerMap = mutableMapOf, SingleLiveEventObserver>() + + private val observers: MutableCollection> get() = observerMap.keys + + override fun observe(owner: LifecycleOwner, observer: Observer) { + val singleEventObserver = singleEventObserverOf(observer) + observerMap[observer] = singleEventObserver + singleDataEvent.observe(owner, singleEventObserver) + } + + override fun observeForever(observer: Observer) { + val singleEventObserver = singleEventObserverOf(observer) + observerMap[observer] = singleEventObserver + singleDataEvent.observeForever(singleEventObserver) + } + + override fun removeObserver(observer: Observer) { + observerMap.remove(observer)?.let { + singleDataEvent.removeObserver(it) + } + } + + override fun removeObservers(owner: LifecycleOwner) { + observerMap.clear() + singleDataEvent.removeObservers(owner) + } + + override fun postValue(value: T) = + singleDataEvent.postValue(SingleLiveDataEvent(observers, value)) + + override fun setValue(value: T) { + singleDataEvent.value = SingleLiveDataEvent(observers, value) + } + +} + diff --git a/src/main/java/me/ibrahimyilmaz/arch_data/PublishLiveData.kt b/src/main/java/me/ibrahimyilmaz/arch_data/PublishLiveData.kt new file mode 100644 index 0000000..c569e1a --- /dev/null +++ b/src/main/java/me/ibrahimyilmaz/arch_data/PublishLiveData.kt @@ -0,0 +1,38 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer + +class PublishLiveData : MutableLiveData() { + + private val liveData = MemoryLessLiveData() + + override fun observe(owner: LifecycleOwner, observer: Observer) { + liveData.observe(owner, observer) + } + + override fun observeForever(observer: Observer) { + liveData.observeForever(observer) + } + + override fun removeObserver(observer: Observer) { + liveData.removeObserver(observer) + } + + override fun removeObservers(owner: LifecycleOwner) { + liveData.removeObservers(owner) + } + + override fun postValue(value: T) { + liveData.postValue(value) + } + + override fun setValue(value: T) { + liveData.setValue(value) + } + + override fun getValue(): T? { + return liveData.value + } +} \ No newline at end of file diff --git a/src/main/java/me/ibrahimyilmaz/arch_data/ReplayLiveData.kt b/src/main/java/me/ibrahimyilmaz/arch_data/ReplayLiveData.kt new file mode 100644 index 0000000..3e9a615 --- /dev/null +++ b/src/main/java/me/ibrahimyilmaz/arch_data/ReplayLiveData.kt @@ -0,0 +1,50 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer + +class ReplayLiveData : MutableLiveData() { + + private val liveData = MemoryLessLiveData() + + private val values = mutableListOf() + + private val mutableDataEvent = mutableLiveDataOf>() + + private val eventObserver: (Observer) -> Unit = { observer -> + values.forEach { observer.onChanged(it) } + } + + init { + mutableDataEvent.observeForever(eventObserver) + } + + override fun observe(owner: LifecycleOwner, observer: Observer) { + liveData.observe(owner, observer) + mutableDataEvent.postValue(observer) + } + + override fun observeForever(observer: Observer) { + liveData.observeForever(observer) + mutableDataEvent.postValue(observer) + } + + override fun postValue(value: T) { + values.add(value) + liveData.postValue(value) + } + + override fun setValue(value: T) { + values.add(value) + liveData.setValue(value) + } + + override fun getValue(): T? { + return liveData.value + } + + protected fun finalize() { + mutableDataEvent.removeObserver(eventObserver) + } +} \ No newline at end of file diff --git a/src/main/java/me/ibrahimyilmaz/arch_data/SingleEventObserver.kt b/src/main/java/me/ibrahimyilmaz/arch_data/SingleEventObserver.kt new file mode 100644 index 0000000..35d89c2 --- /dev/null +++ b/src/main/java/me/ibrahimyilmaz/arch_data/SingleEventObserver.kt @@ -0,0 +1,15 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.lifecycle.Observer + + +internal inline fun singleEventObserverOf(observer: Observer): SingleLiveEventObserver = SingleLiveEventObserver(observer) + +internal class SingleLiveEventObserver(private val observer: Observer) : Observer> { + override fun onChanged(t: SingleLiveDataEvent?) { + t?.getContentIfNotHandled(observer)?.let { + observer.onChanged(it) + } + } +} + diff --git a/src/main/java/me/ibrahimyilmaz/arch_data/SingleLiveDataEvent.kt b/src/main/java/me/ibrahimyilmaz/arch_data/SingleLiveDataEvent.kt new file mode 100644 index 0000000..c8a45b6 --- /dev/null +++ b/src/main/java/me/ibrahimyilmaz/arch_data/SingleLiveDataEvent.kt @@ -0,0 +1,17 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.lifecycle.Observer + + +internal class SingleLiveDataEvent(observers: MutableCollection>, private val content: T) { + + private val receiverObservers = ArrayList(observers) + + fun getContentIfNotHandled(observer: Observer): T? = + receiverObservers.firstOrNull { it == observer }?.let { + receiverObservers.remove(it) + content + } +} + + diff --git a/src/test/java/me/ibrahimyilmaz/arch_data/PublishLiveDataTest.kt b/src/test/java/me/ibrahimyilmaz/arch_data/PublishLiveDataTest.kt new file mode 100644 index 0000000..fd0e660 --- /dev/null +++ b/src/test/java/me/ibrahimyilmaz/arch_data/PublishLiveDataTest.kt @@ -0,0 +1,146 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import com.nhaarman.mockitokotlin2.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class PublishLiveDataTest { + + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() + + private lateinit var liveData: PublishLiveData + + @Before + fun setUp() { + liveData = publishLiveDataOf() + } + + @Test + fun should_not_notify_when_there_is_no_event() { + //GIVEN + val observer = mock>() + + //WHEN + liveData.observeForever(observer) + + //THEN + verify(observer, never()).onChanged(any()) + } + + @Test + fun should_not_notify_when_there_is_event_before_observation() { + //GIVEN + val observer = mock>() + + //WHEN + liveData.postValue(5) + liveData.observeForever(observer) + + //THEN + verify(observer, never()).onChanged(any()) + } + + @Test + fun should_notify_when_there_is_event() { + //GIVEN + val expectedValue = 5 + val observer = mock>() + + //WHEN + liveData.observeForever(observer) + liveData.postValue(expectedValue) + + //THEN + verify(observer).onChanged(expectedValue) + } + + @Test + fun should_not_notify_new_observer_for_previous_events() { + //GIVEN + val observerOld = mock>() + val observerNew = mock>() + + //WHEN + liveData.observeForever(observerOld) + + liveData.postValue(1) + liveData.postValue(2) + liveData.postValue(3) + + liveData.observeForever(observerNew) + + //THEN + verify(observerNew, never()).onChanged(1) + verify(observerNew, never()).onChanged(2) + verify(observerNew, never()).onChanged(3) + } + + @Test + fun should_notify_new_observers_for_only_new_events() { + //GIVEN + val observerOld = mock>() + val observerNew = mock>() + + //WHEN + liveData.observeForever(observerOld) + liveData.postValue(1) + liveData.postValue(2) + liveData.postValue(3) + + liveData.observeForever(observerNew) + liveData.postValue(4) + liveData.postValue(5) + + //THEN + verify(observerNew, never()).onChanged(1) + verify(observerNew, never()).onChanged(2) + verify(observerNew, never()).onChanged(3) + val inOrderVerifier = inOrder(observerNew) + inOrderVerifier.verify(observerNew).onChanged(4) + inOrderVerifier.verify(observerNew).onChanged(5) + inOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun should_receive_events_happening_after_subscription_for_re_subscription() { + //GIVEN + val observer = mock>() + + //WHEN + liveData.observeForever(observer) + + liveData.postValue(1) + liveData.postValue(2) + + + liveData.removeObserver(observer) + + liveData.postValue(3) + liveData.postValue(4) + + liveData.observeForever(observer) + + liveData.postValue(5) + liveData.postValue(6) + + //THEN + verify(observer, never()).onChanged(3) + verify(observer, never()).onChanged(4) + + val inOrderVerifier = inOrder(observer) + inOrderVerifier.verify(observer).onChanged(1) + inOrderVerifier.verify(observer).onChanged(2) + inOrderVerifier.verify(observer).onChanged(5) + inOrderVerifier.verify(observer).onChanged(6) + inOrderVerifier.verifyNoMoreInteractions() + } + +} \ No newline at end of file diff --git a/src/test/java/me/ibrahimyilmaz/arch_data/ReplayLiveDataTest.kt b/src/test/java/me/ibrahimyilmaz/arch_data/ReplayLiveDataTest.kt new file mode 100644 index 0000000..82c200b --- /dev/null +++ b/src/test/java/me/ibrahimyilmaz/arch_data/ReplayLiveDataTest.kt @@ -0,0 +1,99 @@ +package me.ibrahimyilmaz.arch_data + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import com.nhaarman.mockitokotlin2.inOrder +import com.nhaarman.mockitokotlin2.mock +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule + +class ReplayLiveDataTest { + + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() + + private lateinit var liveData: ReplayLiveData + + @Before + fun setUp() { + liveData = replayLiveDataOf() + } + + @Test + fun should_notify_new_observers_for_past_events() { + //GIVEN + val observerOld = mock>() + val observerNew = mock>() + + //WHEN + liveData.observeForever(observerOld) + liveData.postValue(1) + liveData.postValue(2) + liveData.postValue(3) + + liveData.observeForever(observerNew) + val inOrderVerifier = inOrder(observerNew) + inOrderVerifier.verify(observerNew).onChanged(1) + inOrderVerifier.verify(observerNew).onChanged(2) + inOrderVerifier.verify(observerNew).onChanged(3) + inOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun should_notify_observer_for_past_events_for_re_subscription() { + //GIVEN + val observer = mock>() + + //WHEN + liveData.observeForever(observer) + liveData.postValue(1) + liveData.postValue(2) + liveData.postValue(3) + + liveData.removeObserver(observer) + liveData.observeForever(observer) + + val inOrderVerifier = inOrder(observer) + inOrderVerifier.verify(observer).onChanged(1) + inOrderVerifier.verify(observer).onChanged(2) + inOrderVerifier.verify(observer).onChanged(3) + inOrderVerifier.verify(observer).onChanged(1) + inOrderVerifier.verify(observer).onChanged(2) + inOrderVerifier.verify(observer).onChanged(3) + inOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun should_receive_events_happened_when_observer_unsubscribed_for_re_subscription() { + //GIVEN + val observer = mock>() + + //WHEN + liveData.observeForever(observer) + liveData.postValue(1) + liveData.postValue(2) + liveData.postValue(3) + + liveData.removeObserver(observer) + liveData.postValue(4) + liveData.postValue(5) + liveData.observeForever(observer) + + liveData.postValue(6) + + val inOrderVerifier = inOrder(observer) + inOrderVerifier.verify(observer).onChanged(1) + inOrderVerifier.verify(observer).onChanged(2) + inOrderVerifier.verify(observer).onChanged(3) + inOrderVerifier.verify(observer).onChanged(1) + inOrderVerifier.verify(observer).onChanged(2) + inOrderVerifier.verify(observer).onChanged(3) + inOrderVerifier.verify(observer).onChanged(4) + inOrderVerifier.verify(observer).onChanged(5) + inOrderVerifier.verify(observer).onChanged(6) + inOrderVerifier.verifyNoMoreInteractions() + } + +} \ No newline at end of file