├── livecache ├── .gitignore ├── proguard-rules.pro ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── mastercard │ │ │ └── labs │ │ │ └── android │ │ │ └── livecache │ │ │ ├── concurrent │ │ │ ├── IAppExecutors.kt │ │ │ └── AppExecutors.kt │ │ │ ├── api │ │ │ ├── adapter │ │ │ │ ├── ServerResponse.kt │ │ │ │ └── LiveRetrofitCall.kt │ │ │ ├── ApiResponse.kt │ │ │ └── ResponseHandler.kt │ │ │ ├── lifecycle │ │ │ ├── AbsentLiveData.kt │ │ │ ├── LifecycleAndroidViewModel.kt │ │ │ └── SingleLiveEvent.kt │ │ │ ├── utils │ │ │ └── Resource.kt │ │ │ ├── cache │ │ │ └── CachePolicy.kt │ │ │ ├── core │ │ │ ├── base │ │ │ │ └── ILiveResource.kt │ │ │ └── LiveDataResource.kt │ │ │ ├── RepoRequest.kt │ │ │ └── LiveResource.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mastercard │ │ │ └── labs │ │ │ └── android │ │ │ └── livecache │ │ │ ├── utils │ │ │ ├── InstantAppExecutors.kt │ │ │ ├── KMockito.kt │ │ │ └── CountingAppExecutors.kt │ │ │ ├── BaseLiveCacheTest.kt │ │ │ ├── RepoRequestTest.kt │ │ │ ├── LiveResourceTest.kt │ │ │ └── LiveDataResourceTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mastercard │ │ └── labs │ │ └── android │ │ └── livecache │ │ └── ExampleInstrumentedTest.kt └── build.gradle ├── art ├── vm.png ├── repo.png ├── design.png └── decision_tree.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── artifactory.gradle ├── RELEASING.md ├── gradle.properties ├── .gitignore ├── settings.gradle ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /livecache/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /art/vm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/live-cache/HEAD/art/vm.png -------------------------------------------------------------------------------- /art/repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/live-cache/HEAD/art/repo.png -------------------------------------------------------------------------------- /art/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/live-cache/HEAD/art/design.png -------------------------------------------------------------------------------- /art/decision_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/live-cache/HEAD/art/decision_tree.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/live-cache/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## Releasing 2 | 3 | 1. Set in your local *gradle.properties*: 4 | * `artifactory_user` to your jFrog user 5 | * `artifactory_password` to your jFrom password 6 | * `artifactory_contextUrl` to your jFrog URL 7 | 2. Bump the library version in artifactory.gradle 8 | 3. Make your changes 9 | 4. Commit and push your changes 10 | 4. Run `./gradlew clean assembleRelease artifactoryPublish` 11 | 6. If needed, promote your artifact to release -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /livecache/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | .idea/* 45 | .idea/ 46 | 47 | # Keystore files 48 | # Uncomment the following line if you do not want to check your keystore files in. 49 | #*.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | 57 | # Freeline 58 | freeline.py 59 | freeline/ 60 | freeline_project_description.json 61 | 62 | # fastlane 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | fastlane/readme.md 68 | -------------------------------------------------------------------------------- /livecache/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | livecache 29 | 30 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | include ':livecache' 28 | 29 | rootProject.name = "livecache-root" 30 | -------------------------------------------------------------------------------- /livecache/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/concurrent/IAppExecutors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.concurrent 29 | 30 | import java.util.concurrent.Executor 31 | 32 | /** 33 | * @author ech0s7r 34 | */ 35 | interface IAppExecutors { 36 | 37 | val diskIO: Executor 38 | 39 | val networkIO: Executor 40 | 41 | val mainThread: Executor 42 | 43 | } 44 | -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/api/adapter/ServerResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.api.adapter 29 | 30 | import com.mastercard.labs.android.livecache.api.ApiResponse 31 | import retrofit2.Response 32 | 33 | /** 34 | * @author ech0s7r 35 | */ 36 | class ServerResponse(val response: Response) : ApiResponse by ApiResponse.wrap(response.isSuccessful, response.body()) -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/api/ApiResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.api 29 | 30 | /** 31 | * @author ech0s7r 32 | */ 33 | interface ApiResponse { 34 | val isSuccessful: Boolean 35 | fun body(): T? 36 | 37 | companion object { 38 | fun wrap(success: Boolean, body: T?): ApiResponse { 39 | return object : ApiResponse { 40 | override val isSuccessful: Boolean = success 41 | override fun body(): T? = body 42 | } 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/lifecycle/AbsentLiveData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * Copyright 2018 MasterCard International. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are 6 | * permitted provided that the following conditions are met: 7 | * 8 | * Redistributions of source code must retain the above copyright notice, this list of 9 | * conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of 11 | * conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * Neither the name of the MasterCard International Incorporated nor the names of its 14 | * contributors may be used to endorse or promote products derived from this software 15 | * without specific prior written permission. 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 19 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 21 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 23 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 24 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | * SUCH DAMAGE. 26 | * 27 | */ 28 | 29 | package com.mastercard.labs.android.livecache.lifecycle 30 | 31 | import android.arch.lifecycle.LiveData 32 | 33 | /** 34 | * A LiveData class that has null value 35 | * 36 | * @author ech0s7r 37 | */ 38 | class AbsentLiveData private constructor() : LiveData() { 39 | 40 | init { 41 | postValue(null) 42 | } 43 | 44 | companion object { 45 | fun new(): LiveData { 46 | return AbsentLiveData() 47 | } 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/utils/InstantAppExecutors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.utils 29 | 30 | import com.mastercard.labs.android.livecache.concurrent.IAppExecutors 31 | import java.util.concurrent.Executor 32 | 33 | 34 | class InstantAppExecutors(override val diskIO: Executor = instant, 35 | override val networkIO: Executor = instant, 36 | override val mainThread: Executor = instant) : IAppExecutors { 37 | private companion object { 38 | val instant = Executor { command -> command.run() } 39 | } 40 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/api/ResponseHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.api 29 | 30 | import com.mastercard.labs.android.livecache.utils.Resource 31 | 32 | /** 33 | * @author ech0s7r 34 | */ 35 | interface ResponseHandler { 36 | 37 | /** 38 | * Handle successful web service response 39 | */ 40 | fun onSuccess(response: ApiResponse) 41 | 42 | /** 43 | * Handle failed web services call 44 | */ 45 | fun onFailure(throwable: Throwable?): Resource.Error? 46 | 47 | /** 48 | * Handle cache loaded success event 49 | */ 50 | fun onCacheSuccess(data: M?) 51 | 52 | } -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/utils/KMockito.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.utils 29 | 30 | import org.mockito.ArgumentCaptor 31 | import org.mockito.Mockito 32 | 33 | /** 34 | * 35 | * @author ech0s7r 36 | */ 37 | object KMockito { 38 | inline fun mock() = Mockito.mock(T::class.java) 39 | 40 | inline fun isA() = Mockito.isA(T::class.java) 41 | 42 | inline fun argumentCaptor() = ArgumentCaptor.forClass(T::class.java) 43 | 44 | fun any(): T { 45 | Mockito.any() 46 | return uninitialized() 47 | } 48 | 49 | private fun uninitialized(): T = null as T 50 | 51 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/utils/Resource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.utils 29 | 30 | 31 | /** 32 | * A generic class that describes a data with a status, provided by ViewModel to UI 33 | * 34 | * @param T data type 35 | * 36 | * @author ech0s7r 37 | */ 38 | sealed class Resource { 39 | data class Loading(val data: T?) : Resource() 40 | data class Preload(val data: T) : Resource() 41 | data class Success(val data: T) : Resource() 42 | open class Error(val throwable: Throwable?) : Resource() 43 | } 44 | typealias Loading = Resource.Loading 45 | typealias Preload = Resource.Preload 46 | typealias Success = Resource.Success 47 | typealias Error = Resource.Error -------------------------------------------------------------------------------- /livecache/src/androidTest/java/com/mastercard/labs/android/livecache/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | package com.mastercard.labs.android.livecache 28 | 29 | import android.support.test.InstrumentationRegistry 30 | import android.support.test.runner.AndroidJUnit4 31 | import org.junit.Assert.assertEquals 32 | import org.junit.Test 33 | import org.junit.runner.RunWith 34 | 35 | 36 | /** 37 | * Instrumented test, which will execute on an Android device. 38 | */ 39 | @RunWith(AndroidJUnit4::class) 40 | class ExampleInstrumentedTest { 41 | 42 | @Test 43 | fun useAppContext() { 44 | // Context of the app under test. 45 | val appContext = InstrumentationRegistry.getTargetContext() 46 | assertEquals("com.mastercard.labs.android.livecache.test", appContext.packageName) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/cache/CachePolicy.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.cache 29 | 30 | import android.arch.lifecycle.LiveData 31 | 32 | /** 33 | * @author ech0s7r 34 | */ 35 | class CachePolicy { 36 | internal var saveCbk: ((T) -> Unit)? = null 37 | internal var isValidCbk: ((T?) -> Boolean)? = null 38 | internal var loadCbk: (() -> LiveData)? = null 39 | 40 | fun isValid(validImpl: (T?) -> Boolean): CachePolicy { 41 | isValidCbk = validImpl 42 | return this 43 | } 44 | 45 | fun save(storeImpl: (T) -> Unit): CachePolicy { 46 | saveCbk = storeImpl 47 | return this 48 | } 49 | 50 | fun load(loadImpl: () -> LiveData): CachePolicy { 51 | loadCbk = loadImpl 52 | return this 53 | } 54 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/lifecycle/LifecycleAndroidViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.lifecycle 29 | 30 | import android.app.Application 31 | import android.arch.lifecycle.AndroidViewModel 32 | import android.arch.lifecycle.Lifecycle 33 | import android.arch.lifecycle.LifecycleOwner 34 | import android.arch.lifecycle.LifecycleRegistry 35 | 36 | /** 37 | * @author ech0s7r 38 | */ 39 | abstract class LifecycleAndroidViewModel(app: Application) : AndroidViewModel(app), LifecycleOwner { 40 | 41 | private val lifecycleRegistry = LifecycleRegistry(this).apply { 42 | markState(Lifecycle.State.STARTED) 43 | } 44 | 45 | override fun getLifecycle() = lifecycleRegistry 46 | 47 | 48 | override fun onCleared() { 49 | super.onCleared() 50 | lifecycleRegistry.markState(Lifecycle.State.DESTROYED) 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/BaseLiveCacheTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache 29 | 30 | import android.arch.core.executor.testing.InstantTaskExecutorRule 31 | import com.mastercard.labs.android.livecache.concurrent.IAppExecutors 32 | import com.mastercard.labs.android.livecache.utils.CountingAppExecutors 33 | import org.junit.Rule 34 | import java.util.concurrent.TimeUnit 35 | 36 | /** 37 | * @author ech0s7r 38 | */ 39 | abstract class BaseLiveCacheTest { 40 | 41 | 42 | @Rule 43 | @JvmField 44 | var instantExecutorRule = InstantTaskExecutorRule() 45 | 46 | private val countingAppExecutors = CountingAppExecutors() 47 | 48 | protected var appExecutors: IAppExecutors = countingAppExecutors.appExecutors 49 | 50 | protected fun drain() { 51 | try { 52 | countingAppExecutors.drainTasks(1, TimeUnit.SECONDS) 53 | } catch (t: Throwable) { 54 | throw AssertionError(t) 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/concurrent/AppExecutors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.concurrent 29 | 30 | import android.os.Handler 31 | import android.os.Looper 32 | import java.util.concurrent.Executor 33 | import java.util.concurrent.Executors 34 | 35 | 36 | /** 37 | * Global executor pools for the whole application, if not provided a different one this will be used as default. 38 | * 39 | * 40 | * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind 41 | * webservice requests). 42 | * 43 | * @author ech0s7r 44 | */ 45 | object AppExecutors : IAppExecutors { 46 | 47 | /** 48 | * Executor used for disk IO operation 49 | */ 50 | override val diskIO: Executor by lazy { Executors.newSingleThreadExecutor() } 51 | 52 | /** 53 | * Executor used for network operation 54 | */ 55 | override val networkIO: Executor by lazy { Executors.newFixedThreadPool(3) } 56 | 57 | /** 58 | * Executor used for executing task on UI main thread 59 | */ 60 | override val mainThread: Executor by lazy { MainThreadExecutor() } 61 | 62 | 63 | private class MainThreadExecutor : Executor { 64 | private val mainThreadHandler = Handler(Looper.getMainLooper()) 65 | override fun execute(command: Runnable) { 66 | mainThreadHandler.post(command) 67 | } 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/api/adapter/LiveRetrofitCall.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.api.adapter 29 | 30 | import com.mastercard.labs.android.livecache.api.ApiResponse 31 | import retrofit2.Call 32 | import retrofit2.Callback 33 | import retrofit2.Response 34 | import java.util.concurrent.CountDownLatch 35 | 36 | /** 37 | * 38 | * @author ech0s7r 39 | */ 40 | class LiveRetrofitCall(private val retrofitCall: LiveRetrofitCall.() -> Unit) { 41 | 42 | private val latch = CountDownLatch(1) 43 | private var throwable: Throwable? = null 44 | private var response: Response? = null 45 | 46 | val callback: Callback = object : Callback { 47 | override fun onFailure(call: Call?, t: Throwable) { 48 | throwable = t 49 | latch.countDown() 50 | } 51 | 52 | override fun onResponse(call: Call?, res: Response) { 53 | response = res 54 | latch.countDown() 55 | } 56 | 57 | } 58 | 59 | fun createCall(): ApiResponse { 60 | retrofitCall.invoke(this) 61 | latch.await() 62 | if (response != null) { 63 | return response!! as ApiResponse 64 | } else { 65 | throw throwable!! 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /gradle/artifactory.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | apply plugin: 'com.jfrog.artifactory' 29 | apply plugin: 'maven-publish' 30 | 31 | /** 32 | * Please read RELEASE.md, on how to release and publish 33 | */ 34 | 35 | def libraryGroupId = 'com.mastercard.labs.android' 36 | def libraryArtifactId = 'livecache' 37 | def libraryVersion = '1.0.2' 38 | 39 | version = libraryVersion 40 | 41 | publishing { 42 | publications { 43 | aar(MavenPublication) { 44 | groupId libraryGroupId 45 | version = libraryVersion 46 | artifactId libraryArtifactId 47 | artifact("$buildDir/outputs/aar/${libraryArtifactId}-release.aar") 48 | } 49 | } 50 | } 51 | 52 | 53 | artifactory { 54 | contextUrl = "${artifactory_contextUrl}" 55 | publish { 56 | repository { 57 | repoKey = 'libs-release-local' 58 | username = "${artifactory_user}" 59 | password = "${artifactory_password}" 60 | maven = true 61 | } 62 | defaults { 63 | publications('aar') 64 | publishArtifacts = true 65 | properties = ['qa.level': 'basic', 'dev.team': 'core'] 66 | publishPom = true 67 | } 68 | } 69 | resolve { 70 | repository { 71 | repoKey = 'libs-release' 72 | username = "${artifactory_user}" 73 | password = "${artifactory_password}" 74 | maven = true 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/core/base/ILiveResource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.core.base 29 | 30 | import android.arch.lifecycle.LiveData 31 | import android.support.annotation.MainThread 32 | import android.support.annotation.WorkerThread 33 | import com.mastercard.labs.android.livecache.api.ApiResponse 34 | import com.mastercard.labs.android.livecache.lifecycle.AbsentLiveData 35 | import com.mastercard.labs.android.livecache.utils.Resource 36 | 37 | interface ILiveResource { 38 | 39 | @WorkerThread 40 | fun handleResponse(call: ApiResponse) 41 | 42 | @WorkerThread 43 | fun handleCacheResponse(result: ResultType?) { 44 | } 45 | 46 | @WorkerThread 47 | fun createCall(): ApiResponse 48 | 49 | @WorkerThread 50 | fun convertResponse(call: ApiResponse): ResultType? = call.body() as ResultType 51 | 52 | @WorkerThread 53 | fun processResult(result: ResultType) 54 | 55 | @WorkerThread 56 | fun processFailure(e: Throwable? = null): Resource.Error = Resource.Error(e) 57 | 58 | fun asLiveData(): LiveData> 59 | 60 | interface IStorable { 61 | @WorkerThread 62 | fun storeData(data: ResultType) { 63 | } 64 | 65 | @MainThread 66 | fun isCacheValid(data: ResultType?): Boolean = false // in the simple implementation fetch always from the network 67 | 68 | @MainThread 69 | fun loadFromDb(): LiveData = AbsentLiveData.new() // in the simple implementation load an empty value from DB 70 | } 71 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/RepoRequest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache 29 | 30 | import android.arch.lifecycle.Lifecycle 31 | import android.arch.lifecycle.LiveData 32 | import com.mastercard.labs.android.livecache.utils.* 33 | 34 | /** 35 | * Helper class used to request repository resources 36 | * 37 | * @param T Data type 38 | * @param E Error type 39 | * 40 | * @author ech0s7r 41 | */ 42 | open class RepoRequest>(private val lifecycle: Lifecycle, 43 | private val request: () -> LiveData>) { 44 | 45 | private var loadingCbk: (() -> Unit)? = null 46 | private var preloadCbk: ((T) -> Unit)? = null 47 | private var successCbk: ((T) -> Unit)? = null 48 | private var errorCbk: ((E) -> Unit)? = null 49 | 50 | fun onLoading(onLoading: () -> Unit): RepoRequest { 51 | loadingCbk = onLoading 52 | return this 53 | } 54 | 55 | fun onPreload(onPreload: (T) -> Unit): RepoRequest { 56 | preloadCbk = onPreload 57 | return this 58 | } 59 | 60 | fun onSuccess(onSuccess: (T) -> Unit): RepoRequest { 61 | successCbk = onSuccess 62 | return this 63 | } 64 | 65 | fun onError(onError: (E) -> Unit): RepoRequest { 66 | errorCbk = onError 67 | return this 68 | } 69 | 70 | fun execute() { 71 | val result = request() 72 | result.observe({ lifecycle }) { 73 | when (it) { 74 | is Loading -> loadingCbk?.invoke() 75 | is Preload -> preloadCbk?.invoke(it.data) 76 | is Success -> successCbk?.invoke(it.data) 77 | is Error -> errorCbk?.invoke((it as E)) 78 | } 79 | } 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/lifecycle/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * Copyright 2018 MasterCard International. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are 6 | * permitted provided that the following conditions are met: 7 | * 8 | * Redistributions of source code must retain the above copyright notice, this list of 9 | * conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of 11 | * conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * Neither the name of the MasterCard International Incorporated nor the names of its 14 | * contributors may be used to endorse or promote products derived from this software 15 | * without specific prior written permission. 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 19 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 21 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 23 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 24 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | * SUCH DAMAGE. 26 | * 27 | */ 28 | 29 | package com.mastercard.labs.android.livecache.lifecycle 30 | 31 | import android.arch.lifecycle.LifecycleOwner 32 | import android.arch.lifecycle.MutableLiveData 33 | import android.arch.lifecycle.Observer 34 | import android.support.annotation.MainThread 35 | import android.support.annotation.WorkerThread 36 | import com.ech0s7r.android.log.Logger 37 | import java.util.concurrent.atomic.AtomicBoolean 38 | 39 | /** 40 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like 41 | * navigation and Snackbar messages. 42 | * 43 | * 44 | * This avoids a common problem with events: on configuration change (like rotation) an update 45 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an 46 | * explicit call to setValue() or call(). 47 | * 48 | * 49 | * Note that only one observer is going to be notified of changes. 50 | */ 51 | class SingleLiveEvent : MutableLiveData() { 52 | 53 | private val mPending = AtomicBoolean(false) 54 | 55 | @MainThread 56 | override fun observe(owner: LifecycleOwner, observer: Observer) { 57 | 58 | if (hasActiveObservers()) { 59 | Logger.d("Multiple observers registered but only one will be notified of changes.") 60 | } 61 | 62 | // Observe the internal MutableLiveData 63 | super.observe(owner, Observer { t -> 64 | if (mPending.compareAndSet(true, false)) { 65 | observer.onChanged(t) 66 | } 67 | }) 68 | } 69 | 70 | @MainThread 71 | override fun setValue(t: T?) { 72 | mPending.set(true) 73 | super.setValue(t) 74 | } 75 | 76 | @WorkerThread 77 | override fun postValue(t: T?) { 78 | mPending.set(true) 79 | super.postValue(t) 80 | } 81 | 82 | /** 83 | * Used for cases where T is Void, to make calls cleaner. 84 | */ 85 | @MainThread 86 | fun call() { 87 | value = null 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /livecache/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | apply plugin: 'com.android.library' 29 | apply plugin: 'kotlin-android' 30 | apply plugin: 'kotlin-kapt' 31 | 32 | android { 33 | compileSdkVersion 27 34 | 35 | defaultConfig { 36 | minSdkVersion 16 37 | targetSdkVersion 27 38 | versionCode 1 39 | versionName "1.0" 40 | 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | minifyEnabled false 47 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 48 | } 49 | } 50 | 51 | } 52 | 53 | dependencies { 54 | implementation fileTree(dir: 'libs', include: ['*.jar']) 55 | 56 | /* Kotlin */ 57 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 58 | 59 | /* ViewModel and LiveData */ 60 | implementation "android.arch.lifecycle:runtime:1.1.1" 61 | implementation "android.arch.lifecycle:extensions:1.1.1" 62 | kapt "android.arch.lifecycle:compiler:1.1.1" 63 | 64 | /* Logger */ 65 | implementation "com.ech0s7r.android:loggerlib:1.2.12" 66 | 67 | /* Retrofit 2 */ 68 | implementation "com.squareup.retrofit2:retrofit:2.3.0" 69 | implementation "com.squareup.retrofit2:converter-gson:2.3.0" 70 | implementation("com.squareup.retrofit2:converter-simplexml:2.3.0") { 71 | exclude group: 'stax', module: 'stax-api' 72 | exclude group: 'stax', module: 'stax' 73 | exclude group: 'xpp3', module: 'xpp3' 74 | } 75 | implementation "com.squareup.retrofit2:retrofit-mock:2.3.0" 76 | 77 | /* jUnit4 testing */ 78 | testImplementation 'junit:junit:4.12' 79 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 80 | 81 | /* Espresso */ 82 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 83 | 84 | /* Mockito */ 85 | testImplementation "org.mockito:mockito-core:2.8.9", { 86 | exclude group: 'net.bytebuddy' 87 | } 88 | testImplementation "org.mockito:mockito-inline:2.8.9" 89 | 90 | /* Test helpers for LiveData */ 91 | testImplementation "android.arch.core:core-testing:1.1.1" 92 | } 93 | 94 | repositories { 95 | mavenCentral() 96 | } 97 | 98 | apply from: rootProject.file('gradle/artifactory.gradle') 99 | -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/utils/CountingAppExecutors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * Copyright 2018 MasterCard International. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are 6 | * permitted provided that the following conditions are met: 7 | * 8 | * Redistributions of source code must retain the above copyright notice, this list of 9 | * conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of 11 | * conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * Neither the name of the MasterCard International Incorporated nor the names of its 14 | * contributors may be used to endorse or promote products derived from this software 15 | * without specific prior written permission. 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 19 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 21 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 23 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 24 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | * SUCH DAMAGE. 26 | * 27 | */ 28 | 29 | package com.mastercard.labs.android.livecache.utils 30 | 31 | import com.mastercard.labs.android.livecache.concurrent.IAppExecutors 32 | import java.util.concurrent.Executor 33 | import java.util.concurrent.Executors 34 | import java.util.concurrent.TimeUnit 35 | import java.util.concurrent.TimeoutException 36 | 37 | class CountingAppExecutors { 38 | 39 | private val LOCK = Object() 40 | 41 | private var taskCount = 0 42 | 43 | val appExecutors: IAppExecutors 44 | 45 | init { 46 | val increment = Runnable { 47 | synchronized(LOCK) { 48 | taskCount-- 49 | //println("taskCount=$taskCount (inc)") 50 | if (taskCount == 0) { 51 | LOCK.notifyAll() 52 | } 53 | } 54 | } 55 | val decrement = Runnable { 56 | synchronized(LOCK) { 57 | taskCount++ 58 | //println("taskCount=$taskCount (dec)") 59 | } 60 | } 61 | appExecutors = object : IAppExecutors { 62 | override val networkIO: Executor = CountingExecutor(increment, decrement) 63 | override val mainThread: Executor = CountingExecutor(increment, decrement) 64 | override val diskIO: Executor = CountingExecutor(increment, decrement) 65 | } 66 | } 67 | 68 | @Throws(InterruptedException::class, TimeoutException::class) 69 | fun drainTasks(time: Int, timeUnit: TimeUnit) { 70 | val end = System.currentTimeMillis() + timeUnit.toMillis(time.toLong()) 71 | while (true) { 72 | synchronized(LOCK) { 73 | if (taskCount == 0) { 74 | return 75 | } 76 | val now = System.currentTimeMillis() 77 | val remaining = end - now 78 | if (remaining > 0) { 79 | LOCK.wait(remaining) 80 | } else { 81 | throw TimeoutException("could not drain tasks") 82 | } 83 | } 84 | } 85 | } 86 | 87 | private class CountingExecutor(private val increment: Runnable, private val decrement: Runnable) : Executor { 88 | 89 | private val delegate = Executors.newSingleThreadExecutor() 90 | 91 | override fun execute(command: Runnable) { 92 | increment.run() 93 | delegate.execute { 94 | try { 95 | command.run() 96 | } finally { 97 | decrement.run() 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveCache 2 | 3 | > version 1.2 4 | 5 | #### Table of contents 6 | - [Overview](#overview) 7 | * [Features](#features) 8 | - [Usage](#usage) 9 | * [LiveResource: fetch from the network or cache](#from-viewmodel-class) 10 | * [RepoRequest: request a resource from the repository](#from-repository-class-liveresource) 11 | - [Design](#design) 12 | * [LiveResource decision tree](#liveresource-decision-tree) 13 | - [Extra](#extra) 14 | * [Android ViewModel lifecycle](#android-viewmodel-lifecycle) 15 | - [Install](#install) 16 | * [Compatibility](#compatibility) 17 | - [License](#license) 18 | 19 | ## Overview 20 | 21 | LiveCache is a pure Kotlin Android and extensible library that make it easier to define caching policies for the data you would like to fetch from the network (from the application data layer) and provide reactive callbacks to handle async data loading considering android components lifecycle. 22 | 23 | ### Features 24 | - Lifecycle-aware: benefits from [LiveData](https://developer.android.com/reference/android/arch/lifecycle/LiveData) for async events 25 | - Async data loading from network or cache 26 | - Keep fetching, parsing, caching logic in one place 27 | - Preload from cache during network calls 28 | - Simplifies to declare caching strategies 29 | - Repository as single source of truth 30 | - No manual lifecycle managment: no need to register/unregister or subscribe/unsubscribe 31 | - Use [Room](https://developer.android.com/topic/libraries/architecture/room) or whatever persistence library you prefer 32 | 33 | ## Usage 34 | 35 | ### From ViewModel class 36 | 37 | Fetch a repository resource using `RepoRequest` and react on callback events, notifying the UI: 38 | 39 | ``` kotlin 40 | fun getUser(id: Long) { 41 | RepoRequest(lifecycle) { repo.getUser(id) } 42 | .onLoading { /* on loading event */ } 43 | .onPreload { /* on preload event */ } 44 | .onSuccess { /* on success event*/ } 45 | .onError { /* on error */ } 46 | .execute() 47 | ``` 48 | 49 | 50 | ### From repository class: LiveResource 51 | 52 | Fetch a resource from the network or from the cache, declaring your cache policy and map a server response into a model object. 53 | 54 | ``` kotlin 55 | fun getUser(id: Long): LiveData> = 56 | LiveResource.fetch(appExecutors, 57 | isStorable = true, 58 | preload = true) { remote.getUser(id) /*remote call*/ } 59 | .mapper { /* map response */ } 60 | .onResult { /* to execute after mapping */ } 61 | /* Set caching policy */ 62 | .setCachePolicy(CachePolicy() 63 | /* define here, is cache valid ? */ 64 | .isValid { /* is cache valid ? */ } 65 | .load { /* load from cache */ } 66 | .save { /* save into cache */ }) 67 | /* return resource as LiveData */ 68 | .asLiveData() 69 | ``` 70 | 71 | 72 | 73 | ## Design 74 | 75 | ![design](art/design.png) 76 | 77 | 78 | ### LiveResource decision tree 79 | 80 | ![decision tree](art/decision_tree.png) 81 | 82 | 83 | ## Extra 84 | 85 | ### Android ViewModel Lifecycle 86 | 87 | 88 | 89 | ## Install 90 | In your build.gradle, add: 91 | 92 | ``` 93 | implementation 'com.mastercard.labs.android:livecache:$live_cache_version' 94 | ``` 95 | 96 | ### Compatibility 97 | 98 | Android >= *4.0.1* (API 14). 99 | 100 | 101 | ## License 102 | ``` 103 | Copyright 2018 MasterCard International 104 | 105 | Licensed under the Apache License, Version 2.0 (the "License"); 106 | you may not use this file except in compliance with the License. 107 | You may obtain a copy of the License at 108 | 109 | http://www.apache.org/licenses/LICENSE-2.0 110 | 111 | Unless required by applicable law or agreed to in writing, software 112 | distributed under the License is distributed on an "AS IS" BASIS, 113 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 114 | See the License for the specific language governing permissions and 115 | limitations under the License. 116 | ``` 117 | -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/RepoRequestTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache 29 | 30 | import android.arch.lifecycle.Lifecycle 31 | import android.arch.lifecycle.LifecycleOwner 32 | import android.arch.lifecycle.LifecycleRegistry 33 | import android.arch.lifecycle.MutableLiveData 34 | import com.mastercard.labs.android.livecache.utils.KMockito 35 | import com.mastercard.labs.android.livecache.utils.Resource 36 | import junit.framework.Assert 37 | import org.junit.Before 38 | import org.junit.Test 39 | import org.junit.runner.RunWith 40 | import org.junit.runners.JUnit4 41 | import org.mockito.Mockito 42 | 43 | /** 44 | * @author ech0s7r 45 | */ 46 | @RunWith(JUnit4::class) 47 | class RepoRequestTest : BaseLiveCacheTest() { 48 | 49 | private data class Foo(val value: String) 50 | 51 | 52 | private lateinit var lifecycleOwner: LifecycleOwner 53 | private lateinit var lifecycle: LifecycleRegistry 54 | private lateinit var liveData: MutableLiveData> 55 | 56 | private lateinit var repoRequest: RepoRequest> 57 | 58 | private var loadingCalled = false 59 | private var preloadCalled: Foo? = null 60 | private var successCalled: Foo? = null 61 | private var errorCalled: Resource.Error? = null 62 | 63 | 64 | @Before 65 | fun setup() { 66 | lifecycleOwner = KMockito.mock() 67 | lifecycle = LifecycleRegistry(lifecycleOwner) 68 | liveData = MutableLiveData() 69 | 70 | Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle) 71 | 72 | repoRequest = RepoRequest(lifecycle) { liveData } 73 | .onLoading { loadingCalled = true } 74 | .onPreload { preloadCalled = it } 75 | .onSuccess { successCalled = it } 76 | .onError { errorCalled = it } 77 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 78 | repoRequest.execute() 79 | } 80 | 81 | 82 | @Test 83 | fun `loading and success`() { 84 | liveData.value = Resource.Loading(null) 85 | liveData.value = Resource.Success(Foo("ok")) 86 | Assert.assertTrue(loadingCalled) 87 | Assert.assertEquals(Foo("ok"), successCalled) 88 | Assert.assertNull(preloadCalled) 89 | Assert.assertNull(errorCalled) 90 | } 91 | 92 | @Test 93 | fun `just loading`() { 94 | liveData.value = Resource.Loading(null) 95 | Assert.assertTrue(loadingCalled) 96 | Assert.assertNull(successCalled) 97 | Assert.assertNull(preloadCalled) 98 | Assert.assertNull(errorCalled) 99 | } 100 | 101 | @Test 102 | fun `loading, preload, success`() { 103 | liveData.value = Resource.Loading(null) 104 | liveData.value = Resource.Preload(Foo("preload")) 105 | liveData.value = Resource.Success(Foo("ok")) 106 | Assert.assertTrue(loadingCalled) 107 | Assert.assertEquals(Foo("preload"), preloadCalled) 108 | Assert.assertEquals(Foo("ok"), successCalled) 109 | Assert.assertNull(errorCalled) 110 | } 111 | 112 | @Test 113 | fun `loading, preload, error`() { 114 | val error = RuntimeException("error") 115 | liveData.value = Resource.Loading(null) 116 | liveData.value = Resource.Preload(Foo("preload")) 117 | liveData.value = Resource.Error(error) 118 | Assert.assertTrue(loadingCalled) 119 | Assert.assertEquals(Foo("preload"), preloadCalled) 120 | Assert.assertEquals(error, errorCalled?.throwable) 121 | Assert.assertNull(successCalled) 122 | } 123 | 124 | @Test 125 | fun `loading, error`() { 126 | val error = RuntimeException("error") 127 | liveData.value = Resource.Loading(null) 128 | liveData.value = Resource.Error(error) 129 | Assert.assertTrue(loadingCalled) 130 | Assert.assertNull(successCalled) 131 | Assert.assertNull(preloadCalled) 132 | Assert.assertEquals(error, errorCalled?.throwable) 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/core/LiveDataResource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache.core 29 | 30 | import android.annotation.SuppressLint 31 | import android.arch.lifecycle.LiveData 32 | import android.arch.lifecycle.MediatorLiveData 33 | import android.arch.lifecycle.MutableLiveData 34 | import android.support.annotation.MainThread 35 | import com.mastercard.labs.android.livecache.concurrent.IAppExecutors 36 | import com.mastercard.labs.android.livecache.core.base.ILiveResource 37 | import com.mastercard.labs.android.livecache.lifecycle.AbsentLiveData 38 | import com.mastercard.labs.android.livecache.utils.Resource 39 | import java.util.concurrent.atomic.AtomicBoolean 40 | 41 | /** 42 | * A generic class that can provide a resource backed by both the database and/or the network. 43 | * 44 | * @param ResultType Type for the Resource data 45 | * @param ApiResultType Type for the API response 46 | * @property isStorable True if the data can be stored, false otherwise (default is false) 47 | * @property appExecutors executor pools to use 48 | * @constructor Create a new Request Resource 49 | * 50 | * @author ech0s7r 51 | */ 52 | @Suppress("LeakingThis") 53 | @SuppressLint("NoLoggedException") 54 | internal abstract class LiveDataResource 55 | @MainThread 56 | internal constructor(private val appExecutors: IAppExecutors, 57 | private val isStorable: Boolean = false, 58 | private val preload: Boolean = false) 59 | : ILiveResource, ILiveResource.IStorable { 60 | 61 | private inner class CallResult(val result: ResultType? = null, val throwable: Throwable? = null) 62 | 63 | 64 | private var preloaded = AtomicBoolean(false) 65 | private val result = MediatorLiveData>() 66 | 67 | init { 68 | result.value = Resource.Loading(null) 69 | if (isStorable) { 70 | val dbSource = try { 71 | loadFromDb() 72 | } catch (t: Throwable) { 73 | AbsentLiveData.new() 74 | } 75 | result.addSource(dbSource) { data -> 76 | result.removeSource(dbSource) 77 | if (isCacheValid(data)) { 78 | result.addSource(dbSource) { newData -> 79 | appExecutors.diskIO.execute { handleCacheResponse(newData) } 80 | result.value = newData?.let { 81 | Resource.Success(it) 82 | } ?: processFailure() 83 | 84 | } 85 | } else { 86 | if (preload) { 87 | result.addSource(dbSource) { newData -> 88 | newData?.let { result.value = Resource.Preload(it) } 89 | if (preloaded.compareAndSet(false, true)) { 90 | fetchData() // only once 91 | } 92 | } 93 | } else { 94 | fetchData() 95 | } 96 | } 97 | } 98 | } else { 99 | fetchData() 100 | } 101 | } 102 | 103 | private fun fetchData() { 104 | val remoteData = MutableLiveData() 105 | executeCall(remoteData) 106 | dispatchResult(remoteData) 107 | } 108 | 109 | private fun executeCall(remoteData: MutableLiveData) { 110 | appExecutors.networkIO.execute { 111 | try { 112 | val call = createCall() 113 | if (!call.isSuccessful) { 114 | result.postValue(processFailure()) 115 | } else { 116 | val converted = convertResponse(call) 117 | if (converted != null) { 118 | processResult(converted) 119 | } 120 | if (converted != null) { 121 | remoteData.postValue(CallResult(converted)) 122 | } else { 123 | if (!preload) { 124 | remoteData.postValue(null) 125 | } 126 | } 127 | handleResponse(call) 128 | } 129 | } catch (t: Throwable) { 130 | remoteData.postValue(CallResult(throwable = t)) 131 | } 132 | } 133 | } 134 | 135 | 136 | private fun dispatchResult(remoteData: MutableLiveData) { 137 | result.addSource(remoteData) { data -> 138 | result.removeSource(remoteData) 139 | if (data?.result != null) { 140 | if (isStorable) { 141 | appExecutors.diskIO.execute { 142 | storeData(data.result) 143 | result.postValue(Resource.Success(data.result)) 144 | } 145 | } else { 146 | result.value = Resource.Success(data.result) 147 | } 148 | } else { 149 | result.value = processFailure(data?.throwable) 150 | } 151 | } 152 | } 153 | 154 | override fun asLiveData(): LiveData> = result 155 | } -------------------------------------------------------------------------------- /livecache/src/main/java/com/mastercard/labs/android/livecache/LiveResource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache 29 | 30 | import android.arch.lifecycle.LiveData 31 | import com.mastercard.labs.android.livecache.api.ApiResponse 32 | import com.mastercard.labs.android.livecache.api.ResponseHandler 33 | import com.mastercard.labs.android.livecache.api.adapter.LiveRetrofitCall 34 | import com.mastercard.labs.android.livecache.cache.CachePolicy 35 | import com.mastercard.labs.android.livecache.concurrent.IAppExecutors 36 | import com.mastercard.labs.android.livecache.core.LiveDataResource 37 | import com.mastercard.labs.android.livecache.lifecycle.AbsentLiveData 38 | import com.mastercard.labs.android.livecache.utils.Resource 39 | 40 | /** 41 | * 42 | * @author ech0s7r 43 | */ 44 | open class LiveResource internal constructor(private val isStorable: Boolean = false) { 45 | 46 | constructor(appExecutors: IAppExecutors, isStorable: Boolean, preload: Boolean, 47 | remoteCall: () -> ApiResponse, responseClass: Class) : this(isStorable) { 48 | this.appExecutors = appExecutors 49 | this.remoteCall = remoteCall 50 | this.responseClass = responseClass 51 | this.preload = preload 52 | } 53 | 54 | protected constructor(appExecutors: IAppExecutors, 55 | isStorable: Boolean, 56 | preload: Boolean, 57 | remoteCall: () -> ApiResponse, 58 | responseClass: Class, 59 | apiResponseHandler: ResponseHandler?) : this(appExecutors, isStorable, preload, remoteCall, responseClass) { 60 | this.apiResponseHandler = apiResponseHandler 61 | } 62 | 63 | 64 | constructor(isStorable: Boolean, 65 | requestRetrofit: LiveRetrofitCall.() -> Unit, responseClass: Class) : this(isStorable) { 66 | this.retrofitCall = requestRetrofit 67 | this.responseClass = responseClass 68 | } 69 | 70 | private var mapper: ((t: T) -> M)? = null 71 | 72 | private var onResult: ((M) -> Unit)? = null 73 | 74 | private var remoteCall: (() -> ApiResponse)? = null 75 | 76 | private var retrofitCall: (LiveRetrofitCall.() -> Unit)? = null 77 | 78 | private var cachePolicy: CachePolicy? = null 79 | 80 | private var responseClass: Class? = null 81 | 82 | private var preload: Boolean = false 83 | 84 | private var appExecutors: IAppExecutors? = null 85 | 86 | private var apiResponseHandler: ResponseHandler? = null 87 | 88 | private val liveResource by lazy { 89 | object : LiveDataResource(appExecutors!!, isStorable, preload) { 90 | override fun createCall(): ApiResponse { 91 | return if (remoteCall != null) { 92 | remoteCall?.invoke()!! 93 | } else { 94 | LiveRetrofitCall(retrofitCall!!).createCall() 95 | } 96 | } 97 | 98 | override fun storeData(data: M) { 99 | cachePolicy?.saveCbk?.invoke(data) 100 | } 101 | 102 | override fun isCacheValid(data: M?): Boolean { 103 | return cachePolicy?.isValidCbk?.invoke(data) ?: false 104 | } 105 | 106 | override fun loadFromDb(): LiveData { 107 | return if (cachePolicy?.loadCbk != null) { 108 | cachePolicy?.loadCbk?.invoke() ?: AbsentLiveData.new() 109 | } else { 110 | super.loadFromDb() 111 | } 112 | } 113 | 114 | override fun processResult(result: M) { 115 | onResult?.invoke(result) 116 | } 117 | 118 | override fun processFailure(e: Throwable?): Resource.Error { 119 | return apiResponseHandler?.onFailure(e) ?: Resource.Error(e) 120 | } 121 | 122 | override fun handleResponse(call: ApiResponse) { 123 | apiResponseHandler?.onSuccess(call) 124 | } 125 | 126 | @Suppress("UNCHECKED_CAST") 127 | override fun convertResponse(call: ApiResponse): M? { 128 | return mapper?.invoke(call.body()!!) ?: call.body() as M 129 | } 130 | 131 | override fun handleCacheResponse(result: M?) { 132 | if (cachePolicy?.isValidCbk?.invoke(result) == true) { 133 | apiResponseHandler?.onCacheSuccess(result) 134 | } else { 135 | super.handleCacheResponse(result) 136 | } 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Set a mapper for the web services data 143 | */ 144 | fun mapper(mapper: (t: T) -> M): LiveResource { 145 | this.mapper = mapper 146 | return this 147 | } 148 | 149 | /** 150 | * Handle result data, after mapping 151 | */ 152 | fun onResult(onResult: (M) -> Unit): LiveResource { 153 | this.onResult = onResult 154 | return this 155 | } 156 | 157 | /** 158 | * Define a cache policy for the resource 159 | */ 160 | fun setCachePolicy(cachePolicy: CachePolicy): LiveResource { 161 | this.cachePolicy = cachePolicy 162 | return this 163 | } 164 | 165 | /** 166 | * Return the stream as Repository Resource 167 | */ 168 | fun asLiveData(): LiveData> = liveResource.asLiveData() 169 | 170 | companion object { 171 | 172 | /** 173 | * Use to fetch a resource remotely (or from the cache), this function should to be used from Repository classes 174 | * 175 | * @param isStorable true if the resource can be stored 176 | * @param preload true if the observer would be notified on preload event 177 | * @param remoteCall Remote call to perform. The remote call is executed in a worker thread. 178 | */ 179 | inline fun fetch(appExecutors: IAppExecutors, 180 | isStorable: Boolean = false, 181 | preload: Boolean = false, 182 | noinline remoteCall: () -> ApiResponse): LiveResource { 183 | return LiveResource(appExecutors, 184 | isStorable = isStorable, 185 | preload = preload, 186 | remoteCall = remoteCall, 187 | responseClass = T::class.java) 188 | } 189 | 190 | inline fun , M> fetchLive(isStorable: Boolean = false, 191 | noinline requestRetrofit: LiveRetrofitCall.() -> Unit): LiveResource { 192 | return LiveResource(isStorable = isStorable, 193 | requestRetrofit = requestRetrofit, 194 | responseClass = T::class.java) 195 | } 196 | 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/LiveResourceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache 29 | 30 | import android.arch.lifecycle.MutableLiveData 31 | import android.arch.lifecycle.Observer 32 | import com.mastercard.labs.android.livecache.api.ApiResponse 33 | import com.mastercard.labs.android.livecache.cache.CachePolicy 34 | import com.mastercard.labs.android.livecache.utils.KMockito 35 | import com.mastercard.labs.android.livecache.utils.Resource 36 | import junit.framework.Assert 37 | import org.junit.Before 38 | import org.junit.Test 39 | import org.junit.runner.RunWith 40 | import org.junit.runners.JUnit4 41 | import org.mockito.Mockito 42 | import org.mockito.Mockito.* 43 | 44 | /** 45 | * @author ech0s7r 46 | */ 47 | @RunWith(JUnit4::class) 48 | class LiveResourceTest : BaseLiveCacheTest() { 49 | 50 | data class ServerResponse(val value: String) : ApiResponse { 51 | override val isSuccessful: Boolean = false 52 | override fun body(): ServerResponse = ServerResponse(value) 53 | } 54 | 55 | 56 | private lateinit var remoteCall: () -> ApiResponse 57 | private lateinit var onResult: (ServerResponse) -> Unit 58 | private lateinit var mapper: ((t: ServerResponse) -> ServerResponse) 59 | private lateinit var cachePolicy: CachePolicy 60 | private lateinit var liveData: LiveResource 61 | 62 | private lateinit var serverResponseFailure: ApiResponse 63 | private lateinit var serverResponseOK: ApiResponse 64 | 65 | @Before 66 | fun setup() { 67 | remoteCall = KMockito.mock() 68 | onResult = KMockito.mock() 69 | mapper = KMockito.mock() 70 | cachePolicy = KMockito.mock() 71 | serverResponseOK = KMockito.mock() 72 | serverResponseFailure = KMockito.mock() 73 | 74 | `when`(serverResponseFailure.isSuccessful).thenReturn(false) 75 | 76 | `when`(serverResponseOK.isSuccessful).thenReturn(true) 77 | //`when`(serverResponseOK.code()).thenReturn(200) 78 | `when`(serverResponseOK.body()).thenReturn(ServerResponse("ok from server")) 79 | } 80 | 81 | @Test 82 | fun `not storable, no pre-loading, server fails, no cache, no mapper`() { 83 | `when`(remoteCall.invoke()).thenReturn(serverResponseFailure) // return false from server 84 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 85 | 86 | LiveResource.fetch(appExecutors, false, false, remoteCall) 87 | 88 | liveData = LiveResource.fetch(appExecutors, isStorable = false, preload = false, remoteCall = remoteCall) 89 | .setCachePolicy(cachePolicy) 90 | .onResult { onResult } 91 | val observer: Observer> = KMockito.mock() 92 | liveData.asLiveData().observeForever(observer) 93 | 94 | drain() 95 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 96 | Mockito.verify(observer, times(1)).onChanged(KMockito.isA>()) 97 | verifyNoMoreInteractions(observer) 98 | } 99 | 100 | 101 | @Test 102 | fun `not storable, no pre-loading, server ok, no cache, no mapper`() { 103 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 104 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return OK from server 105 | 106 | liveData = LiveResource.fetch(appExecutors, isStorable = false, preload = false, remoteCall = remoteCall) 107 | .setCachePolicy(cachePolicy) 108 | .onResult { onResult } 109 | 110 | val observer: Observer> = KMockito.mock() 111 | liveData.asLiveData().observeForever(observer) 112 | 113 | drain() 114 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 115 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse(value = "ok from server"))) 116 | verifyNoMoreInteractions(observer) 117 | } 118 | 119 | @Test 120 | fun `not storable, no pre-loading, server ok, no cache, with mapper ok`() { 121 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 122 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return OK from server 123 | `when`(mapper.invoke(KMockito.any())).thenReturn(ServerResponse("mapper changed")) 124 | 125 | liveData = LiveResource.fetch(appExecutors, isStorable = false, preload = false, remoteCall = remoteCall) 126 | .setCachePolicy(cachePolicy) 127 | .mapper(mapper) 128 | .onResult { onResult } 129 | 130 | val observer: Observer> = KMockito.mock() 131 | liveData.asLiveData().observeForever(observer) 132 | 133 | drain() 134 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 135 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse(value = "mapper changed"))) 136 | verifyNoMoreInteractions(observer) 137 | } 138 | 139 | @Test 140 | fun `not storable, no pre-loading, server ok, no cache, with mapper fails`() { 141 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 142 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return OK from server 143 | `when`(mapper.invoke(KMockito.any())).thenThrow(RuntimeException::class.java) 144 | 145 | liveData = LiveResource.fetch(appExecutors, isStorable = false, preload = false, remoteCall = remoteCall) 146 | .setCachePolicy(cachePolicy) 147 | .mapper(mapper) 148 | .onResult { onResult } 149 | 150 | val observer: Observer> = KMockito.mock() 151 | liveData.asLiveData().observeForever(observer) 152 | 153 | drain() 154 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 155 | Mockito.verify(observer, times(1)).onChanged(KMockito.isA>()) 156 | verifyNoMoreInteractions(observer) 157 | } 158 | 159 | @Test 160 | fun `storable, no pre-loading, cache valid, server ok, with mapper ok`() { 161 | val dbData = MutableLiveData() 162 | `when`(cachePolicy.isValidCbk).thenReturn { true } // cache valid 163 | `when`(cachePolicy.loadCbk).thenReturn { dbData } 164 | 165 | liveData = LiveResource.fetch(appExecutors, isStorable = true, preload = false, remoteCall = remoteCall) 166 | .setCachePolicy(cachePolicy) 167 | .mapper(mapper) 168 | .onResult { onResult } 169 | 170 | val observer: Observer> = KMockito.mock() 171 | liveData.asLiveData().observeForever(observer) 172 | 173 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 174 | dbData.value = ServerResponse("my from db") 175 | 176 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse("my from db"))) 177 | verifyNoMoreInteractions(observer) 178 | 179 | Mockito.verify(cachePolicy, times(0)).saveCbk // save not called 180 | Mockito.verifyZeroInteractions(mapper) // No mapper calls 181 | Mockito.verifyZeroInteractions(remoteCall) // No server calls, cache is valid 182 | } 183 | 184 | @Test 185 | fun `storable, no pre-loading, cache not valid, server ok, with no mapper`() { 186 | var saved: ServerResponse? = null 187 | val dbData = MutableLiveData() 188 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 189 | `when`(cachePolicy.loadCbk).thenReturn { dbData } 190 | `when`(cachePolicy.saveCbk).thenReturn { saved = it } 191 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return ok from server 192 | 193 | liveData = LiveResource.fetch(appExecutors, isStorable = true, preload = false, remoteCall = remoteCall) 194 | .setCachePolicy(cachePolicy) 195 | .onResult { onResult } 196 | 197 | val observer: Observer> = KMockito.mock() 198 | liveData.asLiveData().observeForever(observer) 199 | 200 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 201 | dbData.value = ServerResponse("my from db") 202 | 203 | drain() 204 | 205 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse("ok from server"))) 206 | verifyNoMoreInteractions(observer) 207 | 208 | // verify save to db new value fetched from server 209 | Mockito.verify(cachePolicy, times(1)).saveCbk // save called 210 | Mockito.verifyZeroInteractions(mapper) // No mapper calls 211 | Assert.assertEquals(ServerResponse("ok from server"), saved) 212 | } 213 | 214 | 215 | @Test 216 | fun `storable, no pre-loading, cache not valid, server ok, with mapper`() { 217 | var saved: ServerResponse? = null 218 | val dbData = MutableLiveData() 219 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 220 | `when`(cachePolicy.loadCbk).thenReturn { dbData } 221 | `when`(cachePolicy.saveCbk).thenReturn { saved = it } 222 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return ok from server 223 | `when`(mapper.invoke(KMockito.any())).thenReturn(ServerResponse("after mapping")) 224 | 225 | liveData = LiveResource.fetch(appExecutors, isStorable = true, preload = false, remoteCall = remoteCall) 226 | .setCachePolicy(cachePolicy) 227 | .mapper(mapper) 228 | .onResult { onResult } 229 | 230 | val observer: Observer> = KMockito.mock() 231 | liveData.asLiveData().observeForever(observer) 232 | 233 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 234 | dbData.value = ServerResponse("my from db") 235 | 236 | drain() 237 | 238 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse("after mapping"))) 239 | verifyNoMoreInteractions(observer) 240 | 241 | // verify save to db new value fetched from server 242 | Mockito.verify(cachePolicy, times(1)).saveCbk // save called 243 | Assert.assertEquals(ServerResponse("after mapping"), saved) 244 | } 245 | 246 | 247 | @Test 248 | fun `storable, pre-loading, cache valid, server ok, with mapper`() { 249 | val dbData = MutableLiveData() 250 | `when`(cachePolicy.isValidCbk).thenReturn { true } // cache valid 251 | `when`(cachePolicy.loadCbk).thenReturn { dbData } 252 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return ok from server 253 | `when`(mapper.invoke(KMockito.any())).thenReturn(ServerResponse("after mapping")) 254 | 255 | liveData = LiveResource.fetch(appExecutors, isStorable = true, preload = true, remoteCall = remoteCall) 256 | .setCachePolicy(cachePolicy) 257 | .mapper(mapper) 258 | .onResult { onResult } 259 | 260 | val observer: Observer> = KMockito.mock() 261 | liveData.asLiveData().observeForever(observer) 262 | 263 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 264 | dbData.value = ServerResponse("my from db") 265 | 266 | drain() 267 | 268 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse("my from db"))) 269 | 270 | 271 | verifyNoMoreInteractions(observer) 272 | 273 | // verify save to db new value fetched from server 274 | Mockito.verify(cachePolicy, times(0)).saveCbk // save not called 275 | } 276 | 277 | @Test 278 | fun `storable, pre-loading, cache not valid, server ok, with mapper`() { 279 | var saved: ServerResponse? = null 280 | val dbData = MutableLiveData() 281 | `when`(cachePolicy.isValidCbk).thenReturn { false } // cache not valid 282 | `when`(cachePolicy.loadCbk).thenReturn { dbData } 283 | `when`(cachePolicy.saveCbk).thenReturn { saved = it; dbData.value = it } 284 | `when`(remoteCall.invoke()).thenReturn(serverResponseOK) // return ok from server 285 | `when`(mapper.invoke(KMockito.any())).thenReturn(ServerResponse("after mapping")) 286 | 287 | liveData = LiveResource.fetch(appExecutors, isStorable = true, preload = true, remoteCall = remoteCall) 288 | .setCachePolicy(cachePolicy) 289 | .mapper(mapper) 290 | .onResult { onResult } 291 | 292 | val observer: Observer> = KMockito.mock() 293 | liveData.asLiveData().observeForever(observer) 294 | 295 | Mockito.verify(observer).onChanged(Resource.Loading(null)) 296 | dbData.value = ServerResponse("my from db") 297 | 298 | drain() 299 | 300 | Mockito.verify(observer, times(1)).onChanged(Resource.Preload(ServerResponse("my from db"))) 301 | 302 | Mockito.verify(observer, times(1)).onChanged(Resource.Success(ServerResponse("after mapping"))) 303 | 304 | Mockito.verify(observer, times(1)).onChanged(Resource.Preload(ServerResponse("after mapping"))) 305 | 306 | verifyNoMoreInteractions(observer) 307 | 308 | // verify save to db new value fetched from server 309 | Mockito.verify(cachePolicy, times(1)).saveCbk // save called 310 | Assert.assertEquals(ServerResponse("after mapping"), saved) 311 | } 312 | 313 | } -------------------------------------------------------------------------------- /livecache/src/test/java/com/mastercard/labs/android/livecache/LiveDataResourceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 MasterCard International. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of 10 | * conditions and the following disclaimer in the documentation and/or other materials 11 | * provided with the distribution. 12 | * Neither the name of the MasterCard International Incorporated nor the names of its 13 | * contributors may be used to endorse or promote products derived from this software 14 | * without specific prior written permission. 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | package com.mastercard.labs.android.livecache 29 | 30 | import android.arch.lifecycle.LiveData 31 | import android.arch.lifecycle.MutableLiveData 32 | import android.arch.lifecycle.Observer 33 | import com.mastercard.labs.android.livecache.api.ApiResponse 34 | import com.mastercard.labs.android.livecache.api.adapter.ServerResponse 35 | import com.mastercard.labs.android.livecache.core.LiveDataResource 36 | import com.mastercard.labs.android.livecache.lifecycle.AbsentLiveData 37 | import com.mastercard.labs.android.livecache.utils.KMockito 38 | import com.mastercard.labs.android.livecache.utils.Resource 39 | import junit.framework.Assert 40 | import org.junit.Test 41 | import org.junit.runner.RunWith 42 | import org.junit.runners.JUnit4 43 | import org.mockito.Mockito.* 44 | import retrofit2.Response 45 | import java.util.concurrent.CountDownLatch 46 | 47 | 48 | /** 49 | * @author ech0s7r 50 | */ 51 | @RunWith(JUnit4::class) 52 | class LiveDataResourceTest : BaseLiveCacheTest() { 53 | 54 | data class Foo(val value: String) 55 | 56 | private fun createLiveResource(storable: Boolean, preload: Boolean, 57 | createCall: () -> Response, 58 | convertResponse: ((ApiResponse) -> T?)? = null, 59 | processResult: ((T) -> Unit)? = null, 60 | handleResponse: ((ApiResponse) -> Unit)? = null, 61 | isCacheValid: ((data: T?) -> Boolean)? = null, 62 | loadFromDb: (() -> LiveData)? = null, 63 | storeData: ((T) -> Unit)? = null): LiveDataResource { 64 | 65 | return object : LiveDataResource(appExecutors, isStorable = storable, preload = preload) { 66 | override fun handleResponse(call: ApiResponse) = handleResponse?.invoke(call) ?: Unit 67 | override fun createCall(): ApiResponse = ServerResponse(createCall.invoke()) 68 | 69 | override fun processResult(result: T) = processResult?.invoke(result) ?: Unit 70 | override fun convertResponse(call: ApiResponse): T? = convertResponse?.invoke(call) 71 | override fun isCacheValid(data: T?) = isCacheValid?.invoke(data) 72 | ?: super.isCacheValid(data) 73 | 74 | override fun loadFromDb(): LiveData = loadFromDb?.invoke() ?: super.loadFromDb() 75 | 76 | override fun storeData(data: T) { 77 | storeData?.invoke(data) 78 | } 79 | } 80 | } 81 | 82 | private val networkResult = Foo("Network") 83 | private val dbResult = Foo("Db") 84 | private val defaultNetworkResponse = Response.success(networkResult) 85 | private val dbLiveData = MutableLiveData() 86 | private var loadFromDbCalled = false 87 | private var storeCalledObject: Any? = null 88 | private var remoteCalled = false 89 | 90 | @Test 91 | fun `no storable, fetch from network ok`() { 92 | val waitForLoading = CountDownLatch(1) 93 | val liveResource = createLiveResource( 94 | storable = false, 95 | preload = false, 96 | createCall = { waitForLoading.await(); defaultNetworkResponse }, 97 | convertResponse = { networkResult }, 98 | loadFromDb = { loadFromDbCalled = true; AbsentLiveData.new() }, 99 | storeData = { storeCalledObject = it }) 100 | 101 | val observer: Observer> = KMockito.mock() 102 | liveResource.asLiveData().observeForever(observer) 103 | 104 | val inOrder = inOrder(observer) 105 | inOrder.verify(observer).onChanged(Resource.Loading(null)) 106 | waitForLoading.countDown() 107 | drain() 108 | inOrder.verify(observer).onChanged(Resource.Success(networkResult)) 109 | verifyNoMoreInteractions(observer) 110 | 111 | Assert.assertFalse(loadFromDbCalled) 112 | Assert.assertNull(storeCalledObject) 113 | } 114 | 115 | @Test 116 | fun `no storable, fetch from network fails`() { 117 | val exception = RuntimeException() 118 | val waitForLoading = CountDownLatch(1) 119 | val liveResource = createLiveResource( 120 | storable = false, 121 | preload = false, 122 | createCall = { waitForLoading.await(); throw exception }, 123 | loadFromDb = { loadFromDbCalled = true; AbsentLiveData.new() }, 124 | storeData = { storeCalledObject = it }) 125 | 126 | val observer: Observer> = KMockito.mock() 127 | liveResource.asLiveData().observeForever(observer) 128 | 129 | verify(observer).onChanged(Resource.Loading(null)) 130 | waitForLoading.countDown() 131 | drain() 132 | verify(observer).onChanged(KMockito.isA>()) 133 | verifyNoMoreInteractions(observer) 134 | 135 | Assert.assertFalse(loadFromDbCalled) 136 | Assert.assertNull(storeCalledObject) 137 | } 138 | 139 | @Test 140 | fun `no storable, fetch from network ok, but convert fail`() { 141 | val waitForLoading = CountDownLatch(1) 142 | val liveResource = createLiveResource( 143 | storable = false, 144 | preload = false, 145 | createCall = { waitForLoading.await(); defaultNetworkResponse }, 146 | loadFromDb = { loadFromDbCalled = true; AbsentLiveData.new() }, 147 | convertResponse = { null }, 148 | storeData = { storeCalledObject = it }) 149 | 150 | val observer: Observer> = KMockito.mock() 151 | liveResource.asLiveData().observeForever(observer) 152 | 153 | verify(observer).onChanged(Resource.Loading(null)) 154 | waitForLoading.countDown() 155 | drain() 156 | verify(observer, times(1)).onChanged(KMockito.isA>()) 157 | verifyNoMoreInteractions(observer) 158 | 159 | Assert.assertFalse(loadFromDbCalled) 160 | Assert.assertNull(storeCalledObject) 161 | } 162 | 163 | @Test 164 | fun `no storable, fetch from network fails and convert fail`() { 165 | val exception = RuntimeException() 166 | val waitForLoading = CountDownLatch(1) 167 | val liveResource = createLiveResource( 168 | storable = false, 169 | preload = false, 170 | createCall = { waitForLoading.await(); throw exception }, 171 | loadFromDb = { loadFromDbCalled = true; AbsentLiveData.new() }, 172 | convertResponse = { null }, 173 | storeData = { storeCalledObject = it }) 174 | 175 | val observer: Observer> = KMockito.mock() 176 | liveResource.asLiveData().observeForever(observer) 177 | 178 | verify(observer).onChanged(Resource.Loading(null)) 179 | waitForLoading.countDown() 180 | drain() 181 | verify(observer, times(1)).onChanged(KMockito.isA>()) 182 | verifyNoMoreInteractions(observer) 183 | 184 | Assert.assertFalse(loadFromDbCalled) 185 | Assert.assertNull(storeCalledObject) 186 | } 187 | 188 | @Test 189 | fun `is storable, no preload, cache is valid and read from db`() { 190 | val liveResource = createLiveResource( 191 | storable = true, 192 | preload = false, 193 | createCall = { remoteCalled = true; defaultNetworkResponse }, 194 | loadFromDb = { dbLiveData }, 195 | isCacheValid = { true }, 196 | storeData = { storeCalledObject = it }) 197 | 198 | val observer: Observer> = KMockito.mock() 199 | liveResource.asLiveData().observeForever(observer) 200 | 201 | dbLiveData.value = dbResult 202 | drain() 203 | verify(observer, times(1)).onChanged(Resource.Loading(null)) 204 | verify(observer, times(1)).onChanged(Resource.Success(dbResult)) 205 | verifyNoMoreInteractions(observer) 206 | 207 | Assert.assertFalse(remoteCalled) 208 | Assert.assertNull(storeCalledObject) 209 | } 210 | 211 | @Test 212 | fun `is storable, no preload, cache is valid but cannot read from db`() { 213 | val liveResource = createLiveResource( 214 | storable = true, 215 | preload = false, 216 | createCall = { remoteCalled = true; defaultNetworkResponse }, 217 | loadFromDb = { throw RuntimeException() }, 218 | isCacheValid = { true }, 219 | storeData = { storeCalledObject = it }) 220 | 221 | val observer: Observer> = KMockito.mock() 222 | liveResource.asLiveData().observeForever(observer) 223 | 224 | verify(observer, times(1)).onChanged(KMockito.isA>()) 225 | verifyNoMoreInteractions(observer) 226 | 227 | Assert.assertFalse(remoteCalled) 228 | Assert.assertNull(storeCalledObject) 229 | } 230 | 231 | @Test 232 | fun `is storable, with preload, cache is valid and read from db`() { 233 | val liveResource = createLiveResource( 234 | storable = true, 235 | preload = true, 236 | createCall = { remoteCalled = true; defaultNetworkResponse }, 237 | loadFromDb = { dbLiveData }, 238 | isCacheValid = { true }, 239 | storeData = { storeCalledObject = it }) 240 | 241 | val observer: Observer> = KMockito.mock() 242 | liveResource.asLiveData().observeForever(observer) 243 | 244 | verify(observer, times(1)).onChanged(Resource.Loading(null)) 245 | dbLiveData.value = dbResult 246 | drain() 247 | verify(observer, times(1)).onChanged(Resource.Success(dbResult)) 248 | verifyNoMoreInteractions(observer) 249 | 250 | Assert.assertFalse(remoteCalled) 251 | Assert.assertNull(storeCalledObject) 252 | } 253 | 254 | 255 | @Test 256 | fun `is storable, with preload, cache not valid, convert fail`() { 257 | val liveResource = createLiveResource( 258 | storable = true, 259 | preload = true, 260 | createCall = { remoteCalled = true; defaultNetworkResponse }, 261 | loadFromDb = { dbLiveData }, 262 | isCacheValid = { false }, 263 | storeData = { storeCalledObject = it }) 264 | 265 | val observer: Observer> = KMockito.mock() 266 | liveResource.asLiveData().observeForever(observer) 267 | 268 | verify(observer, times(1)).onChanged(Resource.Loading(null)) 269 | 270 | dbLiveData.value = dbResult 271 | drain() 272 | 273 | verify(observer, times(1)).onChanged(Resource.Preload(dbResult)) 274 | 275 | verifyNoMoreInteractions(observer) 276 | 277 | Assert.assertTrue(remoteCalled) 278 | Assert.assertNull(storeCalledObject) 279 | } 280 | 281 | 282 | @Test 283 | fun `is storable, with preload, cache not valid, convert ok`() { 284 | val liveResource = createLiveResource( 285 | storable = true, 286 | preload = true, 287 | createCall = { remoteCalled = true; defaultNetworkResponse }, 288 | loadFromDb = { dbLiveData }, 289 | isCacheValid = { false }, 290 | convertResponse = { networkResult }, 291 | storeData = { storeCalledObject = it }) 292 | 293 | val observer: Observer> = KMockito.mock() 294 | liveResource.asLiveData().observeForever(observer) 295 | 296 | verify(observer, times(1)).onChanged(Resource.Loading(null)) 297 | 298 | dbLiveData.value = dbResult 299 | drain() 300 | 301 | verify(observer, times(1)).onChanged(Resource.Preload(dbResult)) 302 | 303 | 304 | verify(observer, times(1)).onChanged(Resource.Success(networkResult)) 305 | verifyNoMoreInteractions(observer) 306 | 307 | Assert.assertTrue(remoteCalled) 308 | Assert.assertNotNull(storeCalledObject) 309 | Assert.assertEquals(networkResult, storeCalledObject) 310 | } 311 | 312 | @Test 313 | fun `is storable, with preload, cache valid, convert ok`() { 314 | val liveResource = createLiveResource( 315 | storable = true, 316 | preload = true, 317 | createCall = { remoteCalled = true; defaultNetworkResponse }, 318 | loadFromDb = { dbLiveData }, 319 | isCacheValid = { true }, 320 | convertResponse = { networkResult }, 321 | storeData = { storeCalledObject = it }) 322 | 323 | val observer: Observer> = KMockito.mock() 324 | liveResource.asLiveData().observeForever(observer) 325 | 326 | verify(observer, times(1)).onChanged(Resource.Loading(null)) 327 | 328 | dbLiveData.value = dbResult 329 | 330 | drain() 331 | verify(observer, times(1)).onChanged(Resource.Success(dbResult)) 332 | verifyNoMoreInteractions(observer) 333 | 334 | Assert.assertFalse(remoteCalled) 335 | Assert.assertNull(storeCalledObject) 336 | } 337 | 338 | 339 | @Test 340 | fun `is storable, with preload, cache not valid, convert ok, update db after`() { 341 | val liveResource = createLiveResource( 342 | storable = true, 343 | preload = true, 344 | createCall = { remoteCalled = true; defaultNetworkResponse }, 345 | loadFromDb = { dbLiveData }, 346 | isCacheValid = { false }, 347 | convertResponse = { networkResult }, 348 | storeData = { storeCalledObject = it; }) 349 | 350 | val observer: Observer> = KMockito.mock() 351 | liveResource.asLiveData().observeForever(observer) 352 | 353 | verify(observer, times(1)).onChanged(Resource.Loading(null)) 354 | 355 | dbLiveData.value = dbResult 356 | drain() 357 | 358 | verify(observer, times(1)).onChanged(Resource.Preload(dbResult)) 359 | 360 | verify(observer, times(1)).onChanged(Resource.Success(networkResult)) 361 | 362 | // simulate save in db after fetching from network 363 | dbLiveData.value = dbResult.copy(value = "New DB after network") 364 | 365 | drain() 366 | 367 | verify(observer, times(1)).onChanged(Resource.Preload(dbResult.copy(value = "New DB after network"))) 368 | 369 | // for some reason somebody update the db after a while... 370 | dbLiveData.value = dbResult.copy(value = "after a while...") 371 | drain() 372 | 373 | verify(observer, times(1)).onChanged(Resource.Preload(dbResult.copy(value = "after a while..."))) 374 | 375 | verifyNoMoreInteractions(observer) 376 | 377 | Assert.assertTrue(remoteCalled) 378 | Assert.assertNotNull(storeCalledObject) 379 | Assert.assertEquals(networkResult, storeCalledObject) 380 | } 381 | 382 | } --------------------------------------------------------------------------------