├── .github └── FUNDING.yml ├── library ├── core │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── mayconcardoso │ │ │ └── mvvm │ │ │ └── core │ │ │ ├── ViewCommand.kt │ │ │ ├── UserInteraction.kt │ │ │ ├── ComponentState.kt │ │ │ ├── SingleLiveEvent.kt │ │ │ └── BaseViewModel.kt │ ├── build.gradle │ └── README.md ├── networking │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── mayconcardoso │ │ │ │ └── networking │ │ │ │ ├── RetrofitBuilder.kt │ │ │ │ ├── SecureRequestHandler.kt │ │ │ │ ├── NetworkError.kt │ │ │ │ └── NetworkErrorTransformer.kt │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── mayconcardoso │ │ │ └── networking │ │ │ ├── SecureRequestHandlerTest.kt │ │ │ └── NetworkErrorTransformerTest.kt │ ├── build.gradle │ └── README.md ├── core-extentions │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── mayconcardoso │ │ │ └── mvvm │ │ │ └── core │ │ │ └── ktx │ │ │ ├── ContextExtentions.kt │ │ │ ├── CommandFunctions.kt │ │ │ ├── ActivityExtention.kt │ │ │ └── FragmentExtention.kt │ ├── build.gradle │ └── README.md ├── core-testing │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── mayconcardoso │ │ │ │ └── mvvm │ │ │ │ └── core │ │ │ │ └── testing │ │ │ │ ├── BaseViewModelTest.kt │ │ │ │ ├── TestRunner.kt │ │ │ │ ├── extentions │ │ │ │ ├── ListAssertionExtention.kt │ │ │ │ ├── ComponentStateFlowValidation.kt │ │ │ │ └── TestObserverScenario.kt │ │ │ │ ├── agent │ │ │ │ ├── LiveDataObserverAgent.kt │ │ │ │ └── FlowObserverAgent.kt │ │ │ │ └── rules │ │ │ │ └── CoroutinesMainTestRule.kt │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── mayconcardoso │ │ │ └── mvvm │ │ │ └── core │ │ │ └── testing │ │ │ └── extentions │ │ │ └── ComponentStateFlowValidationKtTest.kt │ ├── build.gradle │ └── README.md └── simple-recyclerview │ ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── io │ │ └── github │ │ └── mayconcardoso │ │ └── simple │ │ └── recyclerview │ │ ├── base │ │ ├── SimpleBindingHolder.kt │ │ └── SimpleBindingAdapter.kt │ │ ├── utils │ │ ├── SimpleItemDiffCallback.kt │ │ └── LoadNextPageScrollMonitor.kt │ │ └── RecyclerViewBinding.kt │ ├── build.gradle │ └── README.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── sample ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── item_image.xml │ │ │ │ ├── fragment_list_of_images.xml │ │ │ │ └── fragment_details_of_image.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── mctech │ │ │ │ └── architecture │ │ │ │ └── mvvm │ │ │ │ ├── App.kt │ │ │ │ ├── data │ │ │ │ ├── ImageDataSource.kt │ │ │ │ ├── ImageRepository.kt │ │ │ │ └── ImageMockedDataSource.kt │ │ │ │ ├── domain │ │ │ │ ├── entities │ │ │ │ │ ├── Image.kt │ │ │ │ │ └── ImageDetails.kt │ │ │ │ ├── error │ │ │ │ │ └── ImageException.kt │ │ │ │ ├── InteractionResult.kt │ │ │ │ ├── service │ │ │ │ │ └── ImageService.kt │ │ │ │ └── interactions │ │ │ │ │ ├── LoadImageListCase.kt │ │ │ │ │ └── LoadImageDetailsCase.kt │ │ │ │ ├── presentation │ │ │ │ ├── ImageCommands.kt │ │ │ │ ├── ImageInteraction.kt │ │ │ │ ├── view │ │ │ │ │ ├── ImageDetailsFragment.kt │ │ │ │ │ └── ImageListFragment.kt │ │ │ │ └── ImageViewModel.kt │ │ │ │ ├── di │ │ │ │ └── DataDiModule.kt │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── com │ │ └── mctech │ │ └── architecture │ │ └── mvvm │ │ ├── domain │ │ └── interactions │ │ │ ├── ResultExtention.kt │ │ │ ├── LoadImageListCaseTest.kt │ │ │ └── LoadImageDetailsCaseTest.kt │ │ └── presentation │ │ └── ImageViewModelTest.kt └── build.gradle ├── settings.gradle ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: mayconcardoso 2 | -------------------------------------------------------------------------------- /library/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/networking/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/core-extentions/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/core-testing/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/simple-recyclerview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Mvvm-Architecture-Toolkit/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/App.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/data/ImageDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.data 2 | 3 | import com.mctech.architecture.mvvm.domain.service.ImageService 4 | interface ImageDataSource : ImageService -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Mvvm-Toolkit-Architecture 3 | Maycon Cardoso 4 | 23/04/2020 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/entities/Image.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.entities 2 | 3 | data class Image( 4 | val id: Long, 5 | val title: String, 6 | val date: String, 7 | val thumbnailUrlSource: String 8 | ) -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='Mvvm-Toolkit-Architecture' 2 | include ':sample' 3 | include ':library:core' 4 | include ':library:core-testing' 5 | include ':library:core-extentions' 6 | include ':library:networking' 7 | include ':library:simple-recyclerview' 8 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/data/ImageRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.data 2 | 3 | import com.mctech.architecture.mvvm.domain.service.ImageService 4 | 5 | class ImageRepository(dataSource : ImageDataSource) : ImageService by dataSource -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 27 10:33:51 BST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/presentation/ImageCommands.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.presentation 2 | 3 | import io.github.mayconcardoso.mvvm.core.ViewCommand 4 | 5 | sealed class ImageCommands : ViewCommand { 6 | object OpenImageDetails : ImageCommands() 7 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/error/ImageException.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.error 2 | 3 | sealed class ImageException : RuntimeException() { 4 | object CannotFetchImages : ImageException() 5 | object UnknownImageException : ImageException() 6 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.useAndroidX=true 3 | android.nonTransitiveRClass=true 4 | org.gradle.jvmargs=-Xmx6g -XX:MaxPermSize=6g -XX:+UseParallelGC 5 | org.gradle.parallel=true 6 | org.gradle.vfs.watch=true 7 | org.gradle.caching=false 8 | org.gradle.unsafe.configuration-cache=false -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/entities/ImageDetails.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.entities 2 | 3 | data class ImageDetails( 4 | val image: Image, 5 | val description: String, 6 | val bigImageUrlSource: String, 7 | val heightSize: Int, 8 | val widthSize: Int 9 | ) -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/InteractionResult.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain 2 | 3 | sealed class InteractionResult { 4 | data class Success(val result: T) : InteractionResult() 5 | data class Error(val error: Throwable) : InteractionResult() 6 | } -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/presentation/ImageInteraction.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.presentation 2 | 3 | import com.mctech.architecture.mvvm.domain.entities.Image 4 | import io.github.mayconcardoso.mvvm.core.UserInteraction 5 | 6 | sealed class ImageInteraction : UserInteraction { 7 | data class OpenDetails(val image: Image) : ImageInteraction() 8 | 9 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/service/ImageService.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.service 2 | 3 | import com.mctech.architecture.mvvm.domain.entities.Image 4 | import com.mctech.architecture.mvvm.domain.entities.ImageDetails 5 | 6 | interface ImageService { 7 | suspend fun getAllImages(): List 8 | suspend fun getImageDetails(image: Image): ImageDetails 9 | } -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/BaseViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing 2 | 3 | import io.github.mayconcardoso.mvvm.core.testing.rules.CoroutinesMainTestRule 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import org.junit.Rule 6 | 7 | abstract class BaseViewModelTest { 8 | @get:Rule 9 | val coroutinesTestRule = CoroutinesMainTestRule() 10 | } 11 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /library/simple-recyclerview/src/main/java/io/github/mayconcardoso/simple/recyclerview/base/SimpleBindingHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.simple.recyclerview.base 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import androidx.viewbinding.ViewBinding 5 | 6 | /** 7 | * Default view holder to create lists without boilerplate 8 | */ 9 | class SimpleBindingHolder( 10 | val binding: VDB, 11 | ) : RecyclerView.ViewHolder(binding.root) -------------------------------------------------------------------------------- /library/core/src/main/java/io/github/mayconcardoso/mvvm/core/ViewCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core 2 | 3 | /** 4 | * A command is something that happen just once like for example: 5 | * - The ViewModel send 'a command' to the view to make it navigate to another screen. 6 | * 7 | * It's like the 'State' of the view, but used to send, again, 'a command' to the screen. 8 | * It's everything the doesn't change the 'visual state' of the view. 9 | */ 10 | interface ViewCommand -------------------------------------------------------------------------------- /library/networking/src/main/java/io/github/mayconcardoso/networking/RetrofitBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.networking 2 | 3 | import okhttp3.OkHttpClient 4 | import retrofit2.Retrofit 5 | import retrofit2.converter.gson.GsonConverterFactory 6 | 7 | object RetrofitBuilder { 8 | operator fun invoke(apiURL: String, httpClient: OkHttpClient): Retrofit = Retrofit.Builder() 9 | .baseUrl(apiURL) 10 | .client(httpClient) 11 | .addConverterFactory(GsonConverterFactory.create()) 12 | .build() 13 | } -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/TestRunner.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.test.runTest 5 | 6 | /** 7 | * Used only to make your test more readable. 8 | */ 9 | @ExperimentalCoroutinesApi 10 | fun testScenario( 11 | scenario: suspend () -> Unit = {}, 12 | action: suspend () -> T, 13 | assertions: suspend (result: T) -> Unit 14 | ) = runTest { 15 | scenario.invoke() 16 | assertions.invoke(action.invoke()) 17 | } -------------------------------------------------------------------------------- /library/simple-recyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | ext { 6 | PUBLISH_ARTIFACT_ID = 'simple-recyclerview' 7 | } 8 | 9 | apply from: "$rootDir/buildSrc/publish-module.gradle" 10 | 11 | dependencies { 12 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" 13 | implementation 'androidx.appcompat:appcompat:1.6.1' 14 | implementation 'androidx.recyclerview:recyclerview:1.3.0' 15 | implementation 'androidx.databinding:databinding-runtime:7.4.2' 16 | } -------------------------------------------------------------------------------- /library/networking/src/test/java/io/github/mayconcardoso/networking/SecureRequestHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.networking 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.Test 6 | 7 | class SecureRequestHandlerTest { 8 | 9 | @Test 10 | fun `should return a known network error`(): Unit = runBlocking { 11 | val result = runCatching { 12 | secureRequest(suspend { throw Throwable() }) 13 | }.exceptionOrNull() 14 | 15 | assertThat(result).isExactlyInstanceOf(NetworkError.UnknownNetworkingError::class.java) 16 | } 17 | } -------------------------------------------------------------------------------- /library/core/src/main/java/io/github/mayconcardoso/mvvm/core/UserInteraction.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * An interaction is some user's 'intention' to do something. For example: 7 | * - Load this list for me 8 | * - Try sign in with these credentials 9 | * - Etc. 10 | */ 11 | interface UserInteraction 12 | 13 | /** 14 | * You can annotate your function with this policy to make it be called at runtime. 15 | */ 16 | @Target(AnnotationTarget.FUNCTION) 17 | @Retention(AnnotationRetention.RUNTIME) 18 | annotation class OnInteraction(val target : KClass) -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/extentions/ListAssertionExtention.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.extentions 2 | 3 | import org.assertj.core.api.Assertions 4 | 5 | fun List.assertEmpty() = assertCount(0) 6 | fun List.assertFirst() = assertAtPosition(0) 7 | fun List.assertSecond() = assertAtPosition(1) 8 | fun List.assertLast() = assertAtPosition(size - 1) 9 | 10 | fun List.assertCount(count: Int) = Assertions.assertThat(size).isEqualTo(count) 11 | fun List.assertAtPosition(position: Int) = Assertions.assertThat(get(position)) 12 | 13 | fun Any.assertThat() = Assertions.assertThat(this) -------------------------------------------------------------------------------- /library/networking/src/main/java/io/github/mayconcardoso/networking/SecureRequestHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.networking 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | 6 | /** 7 | * This method helps the application to avoid unexpected crashes during some network request. 8 | * Basically, if there is any issue on the request, we can transform the error into 9 | * another one that the app know about. 10 | */ 11 | suspend fun secureRequest(target: suspend () -> T): T = withContext(Dispatchers.IO) { 12 | try { 13 | target.invoke() 14 | } catch (incoming: Throwable) { 15 | throw NetworkErrorTransformer.transform(incoming) 16 | } 17 | } -------------------------------------------------------------------------------- /library/networking/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | ext { 5 | PUBLISH_ARTIFACT_ID = 'networking' 6 | } 7 | 8 | apply from: "$rootDir/buildSrc/publish-module.gradle" 9 | 10 | dependencies { 11 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" 12 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" 13 | 14 | implementation "com.squareup.retrofit2:retrofit:2.9.0" 15 | implementation "com.squareup.retrofit2:converter-gson:2.9.0" 16 | implementation "com.squareup.okhttp3:okhttp:4.10.0" 17 | testImplementation 'junit:junit:4.13.2' 18 | testImplementation 'org.assertj:assertj-core:3.24.2' 19 | } -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/agent/LiveDataObserverAgent.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.agent 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.Observer 5 | 6 | internal class LiveDataObserverAgent( 7 | private val liveData: LiveData, 8 | private val assert: suspend (List) -> Unit, 9 | ) { 10 | private val emittedValues = mutableListOf() 11 | private val observer = Observer { 12 | emittedValues.add(it) 13 | } 14 | 15 | fun observe() { 16 | liveData.observeForever(observer) 17 | } 18 | 19 | suspend fun resume() { 20 | assert.invoke(emittedValues) 21 | } 22 | 23 | fun release() { 24 | liveData.removeObserver(observer) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/core-testing/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | ext { 5 | PUBLISH_ARTIFACT_ID = 'mvvm-core-testing' 6 | } 7 | 8 | apply from: "$rootDir/buildSrc/publish-module.gradle" 9 | 10 | dependencies { 11 | implementation project(path: ':library:core') 12 | 13 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' 14 | implementation 'junit:junit:4.13.2' 15 | implementation 'androidx.test:runner:1.5.2' 16 | implementation 'org.assertj:assertj-core:3.24.2' 17 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' 18 | implementation 'androidx.arch.core:core-testing:2.2.0' 19 | testImplementation 'junit:junit:4.13.2' 20 | testImplementation 'org.assertj:assertj-core:3.24.2' 21 | } -------------------------------------------------------------------------------- /library/core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | ext { 6 | PUBLISH_ARTIFACT_ID = 'mvvm-core' 7 | } 8 | 9 | apply from: "$rootDir/buildSrc/publish-module.gradle" 10 | 11 | dependencies { 12 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" 13 | implementation 'androidx.appcompat:appcompat:1.6.1' 14 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 15 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 16 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' 17 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' 18 | implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /library/simple-recyclerview/src/main/java/io/github/mayconcardoso/simple/recyclerview/utils/SimpleItemDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.simple.recyclerview.utils 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import java.util.Objects 5 | 6 | class SimpleItemDiffCallback( 7 | private val itemsTheSame: (oldItem: T, newItem: T) -> Boolean = { oldItem, newItem -> oldItem == newItem }, 8 | private val contentsTheSame: (oldItem: T, newItem: T) -> Boolean = { oldItem, newItem -> Objects.equals(oldItem, newItem) }, 9 | ) : DiffUtil.ItemCallback() { 10 | 11 | override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemsTheSame(oldItem, newItem) 12 | 13 | override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentsTheSame(oldItem, newItem) 14 | } 15 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/di/DataDiModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.di 2 | 3 | import com.mctech.architecture.mvvm.data.ImageDataSource 4 | import com.mctech.architecture.mvvm.data.ImageMockedDataSource 5 | import com.mctech.architecture.mvvm.data.ImageRepository 6 | import com.mctech.architecture.mvvm.domain.service.ImageService 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DataDiModule { 15 | 16 | @Provides 17 | fun providesImageDataSource(): ImageDataSource = ImageMockedDataSource() 18 | 19 | @Provides 20 | fun providesImageService(dataSource: ImageDataSource): ImageService = ImageRepository( 21 | dataSource 22 | ) 23 | 24 | } -------------------------------------------------------------------------------- /library/core-extentions/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | ext { 6 | PUBLISH_ARTIFACT_ID = 'mvvm-core-ktx' 7 | } 8 | 9 | apply from: "$rootDir/buildSrc/publish-module.gradle" 10 | 11 | dependencies { 12 | implementation project(path: ':library:core') 13 | 14 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" 15 | implementation 'androidx.appcompat:appcompat:1.6.1' 16 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 17 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 18 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' 19 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' 20 | implementation 'androidx.databinding:databinding-runtime:7.4.2' 21 | } -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/agent/FlowObserverAgent.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.agent 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.launchIn 7 | import kotlinx.coroutines.flow.onEach 8 | 9 | class FlowObserverAgent( 10 | private val flow: Flow, 11 | private val assert: suspend (List) -> Unit, 12 | ) { 13 | 14 | private val results = mutableListOf() 15 | private var job: Job? = null 16 | 17 | fun collect(scope: CoroutineScope) { 18 | job = flow 19 | .onEach(results::add) 20 | .launchIn(scope) 21 | } 22 | 23 | suspend fun resume() { 24 | assert(results) 25 | } 26 | 27 | fun release() { 28 | job?.cancel() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/rules/CoroutinesMainTestRule.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.rules 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.test.* 7 | import org.junit.runner.Description 8 | 9 | @OptIn(ExperimentalCoroutinesApi::class) 10 | class CoroutinesMainTestRule constructor( 11 | private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() 12 | ) : InstantTaskExecutorRule() { 13 | 14 | override fun starting(description: Description) { 15 | super.starting(description) 16 | Dispatchers.setMain(testDispatcher) 17 | } 18 | 19 | override fun finished(description: Description) { 20 | super.finished(description) 21 | Dispatchers.resetMain() 22 | } 23 | } -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/interactions/LoadImageListCase.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.interactions 2 | 3 | import com.mctech.architecture.mvvm.domain.InteractionResult 4 | import com.mctech.architecture.mvvm.domain.entities.Image 5 | import com.mctech.architecture.mvvm.domain.error.ImageException 6 | import com.mctech.architecture.mvvm.domain.service.ImageService 7 | import javax.inject.Inject 8 | 9 | class LoadImageListCase @Inject constructor( 10 | private val service: ImageService 11 | ) { 12 | suspend fun execute(): InteractionResult> { 13 | try { 14 | return InteractionResult.Success(service.getAllImages()) 15 | } catch (exception: Throwable) { 16 | if (exception is ImageException) { 17 | return InteractionResult.Error(exception) 18 | } 19 | 20 | return InteractionResult.Error(ImageException.UnknownImageException) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /library/networking/README.md: -------------------------------------------------------------------------------- 1 | # Core Networking Library 2 | 3 | This is a networking module to create APIs easily without boilerplate. 4 | It also maps all network issues and translates it to another exception to make it possible to handle network error. 5 | 6 | All you have to do is surround your network call with the function ```secureRequest``` and then you avoid crashes related to network. 7 | With that you also get a predefined and high level mapped exception where you can decided what to do based on your retry police, etc. 8 | 9 | ```kotlin 10 | class YourRemoteDataSource( 11 | private val yourRetrofitOrAnyOtherApi: YourApi, 12 | ) { 13 | 14 | suspend fun fetchYourData(): List = secureRequest { 15 | yourRetrofitOrAnyOtherApi.fetchYourData() 16 | } 17 | 18 | } 19 | ``` 20 | 21 | If anything unexpected happened, one of those [pre mapped exceptions](src/main/java/io/github/mayconcardoso/networking/NetworkErrorTransformer.kt) will be thrown and you can decide what to do. 22 | 23 | -------------------------------------------------------------------------------- /sample/src/test/java/com/mctech/architecture/mvvm/domain/interactions/ResultExtention.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.interactions 2 | 3 | import com.mctech.architecture.mvvm.domain.InteractionResult 4 | import org.assertj.core.api.Assertions 5 | 6 | fun InteractionResult<*>.assertResultFailure(expectedException: Exception){ 7 | val resultException = (this as InteractionResult.Error).error 8 | Assertions.assertThat(this).isInstanceOf(InteractionResult.Error::class.java) 9 | Assertions.assertThat(resultException).isEqualTo(expectedException) 10 | } 11 | 12 | fun InteractionResult.assertResultSuccess(expectedValue : T){ 13 | val expectedResult = InteractionResult.Success(expectedValue) 14 | val entity = (this as InteractionResult.Success).result 15 | 16 | Assertions.assertThat(this) 17 | .isExactlyInstanceOf(InteractionResult.Success::class.java) 18 | .isEqualTo(expectedResult) 19 | 20 | Assertions.assertThat(entity).isEqualTo(expectedValue) 21 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/domain/interactions/LoadImageDetailsCase.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.interactions 2 | 3 | import com.mctech.architecture.mvvm.domain.InteractionResult 4 | import com.mctech.architecture.mvvm.domain.entities.Image 5 | import com.mctech.architecture.mvvm.domain.entities.ImageDetails 6 | import com.mctech.architecture.mvvm.domain.error.ImageException 7 | import com.mctech.architecture.mvvm.domain.service.ImageService 8 | import javax.inject.Inject 9 | 10 | class LoadImageDetailsCase @Inject constructor( 11 | private val service: ImageService 12 | ) { 13 | suspend fun execute(image: Image): InteractionResult { 14 | try { 15 | return InteractionResult.Success(service.getImageDetails(image)) 16 | } catch (exception: Throwable) { 17 | if (exception is ImageException) { 18 | return InteractionResult.Error(exception) 19 | } 20 | 21 | return InteractionResult.Error(ImageException.UnknownImageException) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /library/networking/src/main/java/io/github/mayconcardoso/networking/NetworkError.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.networking 2 | 3 | /** 4 | * @author MAYCON CARDOSO 5 | */ 6 | sealed class NetworkError(originalError: String? = null) : Exception(originalError) { 7 | data class ClientException(val code: Int, private val error: String?) : NetworkError(error) 8 | object RemoteException : NetworkError() 9 | 10 | object HostUnreachable : NetworkError() 11 | object OperationTimeout : NetworkError() 12 | object ConnectionSpike : NetworkError() 13 | object UnknownNetworkingError : NetworkError() 14 | 15 | override fun toString() = 16 | when (this) { 17 | is ClientException -> "Issue originated from client" 18 | RemoteException -> "Issue incoming from server" 19 | HostUnreachable -> "Cannot reach remote host" 20 | OperationTimeout -> "Networking operation timed out" 21 | ConnectionSpike -> "In-flight networking operation interrupted" 22 | UnknownNetworkingError -> "Fatal networking exception" 23 | } 24 | } -------------------------------------------------------------------------------- /library/core-extentions/src/main/java/io/github/mayconcardoso/mvvm/core/ktx/ContextExtentions.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.ktx 2 | 3 | import android.content.Context 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.lifecycle.LifecycleOwner 6 | 7 | fun Context.getLifeCycleOwner(): LifecycleOwner { 8 | if (this is LifecycleOwner) { 9 | return this 10 | } 11 | 12 | if (this is android.view.ContextThemeWrapper) { 13 | return this.baseContext.getLifeCycleOwner() 14 | } 15 | 16 | if (this is androidx.appcompat.view.ContextThemeWrapper) { 17 | return this.baseContext.getLifeCycleOwner() 18 | } 19 | 20 | throw IllegalArgumentException("The provided context is not a LifecycleOwner") 21 | } 22 | 23 | fun Context.getActivity(): AppCompatActivity { 24 | if (this is AppCompatActivity) { 25 | return this 26 | } 27 | 28 | if (this is android.view.ContextThemeWrapper) { 29 | return this.baseContext.getActivity() 30 | } 31 | 32 | if (this is androidx.appcompat.view.ContextThemeWrapper) { 33 | return this.baseContext.getActivity() 34 | } 35 | 36 | throw IllegalArgumentException("The provided context is not a Activity") 37 | } -------------------------------------------------------------------------------- /library/core-extentions/src/main/java/io/github/mayconcardoso/mvvm/core/ktx/CommandFunctions.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.ktx 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import io.github.mayconcardoso.mvvm.core.BaseViewModel 5 | import io.github.mayconcardoso.mvvm.core.SingleLiveEvent 6 | import io.github.mayconcardoso.mvvm.core.ViewCommand 7 | 8 | /** 9 | * It is called when you wanna observe a single event and then stop to observing it. 10 | */ 11 | internal fun autoDisposeCommandObserver( 12 | lifecycle: LifecycleOwner, 13 | viewModel: BaseViewModel, 14 | block: (result: ViewCommand) -> Unit, 15 | ) { 16 | val key = lifecycle.toString() 17 | val commandObservable = ((viewModel.commandObservable) as SingleLiveEvent) 18 | commandObservable.observe(key, lifecycle) { 19 | block(it) 20 | commandObservable.removeObserver(key) 21 | } 22 | } 23 | 24 | /** 25 | * It is called when you wanna observe all commands while the lifecycle owner is activated. 26 | */ 27 | internal fun commandObserver( 28 | lifecycle: LifecycleOwner, 29 | viewModel: BaseViewModel, 30 | block: (result: ViewCommand) -> Unit, 31 | ) { 32 | ((viewModel.commandObservable) as SingleLiveEvent).observe( 33 | lifecycle.toString(), 34 | lifecycle, 35 | ) { 36 | block(it) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /library/simple-recyclerview/src/main/java/io/github/mayconcardoso/simple/recyclerview/base/SimpleBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.simple.recyclerview.base 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.viewbinding.ViewBinding 8 | import io.github.mayconcardoso.simple.recyclerview.utils.SimpleItemDiffCallback 9 | 10 | /** 11 | * Default adapter to create lists without boilerplate. 12 | */ 13 | class SimpleBindingAdapter( 14 | // Diff algorithm 15 | diffCallback: DiffUtil.ItemCallback = SimpleItemDiffCallback(), 16 | 17 | // Prepare view binding. 18 | private val viewHolderFactory: (parent: ViewGroup, inflater: LayoutInflater) -> VDB, 19 | 20 | // Delegate to bind the item on the view holder. 21 | private val bindView: (item: T, viewBinding: VDB) -> Unit, 22 | 23 | ) : ListAdapter>(diffCallback) { 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = SimpleBindingHolder( 25 | viewHolderFactory(parent, LayoutInflater.from(parent.context)) 26 | ) 27 | 28 | override fun onBindViewHolder(holder: SimpleBindingHolder, position: Int) { 29 | bindView(getItem(position), holder.binding) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /library/core-extentions/README.md: -------------------------------------------------------------------------------- 1 | # Core Extension Library 2 | 3 | This is a simple Extension library to help you 'Observe' your ComponentState on your Activities and Fragments without that boilerplate. 4 | 5 | ## Binding View 6 | Just introduces an easier way to bind your view with ViewBinding for Fragments and Activities. 7 | 8 | ### Fragment 9 | ```kotlin 10 | class ImageListFragment : Fragment(R.layout.fragment_list_of_images) { 11 | private val binding by viewBinding(FragmentListOfImagesBinding::bind) 12 | } 13 | ``` 14 | 15 | ### Activity 16 | ```kotlin 17 | class MainActivity : AppCompatActivity() { 18 | private val binding by viewBinding(ActivityMainBinding::inflate) 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(binding.root) 23 | } 24 | } 25 | ``` 26 | 27 | ## Binding View Model States with your Fragment or Activity 28 | 29 | ### Without this library 30 | ```kotlin 31 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 32 | lifecycleScope.launch { 33 | viewModel.imageDetailsComponent.observe(this, Observer { 34 | // Your code 35 | }) 36 | } 37 | } 38 | ``` 39 | 40 | ### Using this library 41 | ```kotlin 42 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 43 | // Observes image component state. 44 | bindState(viewModel.state, ::consumeComponentState) 45 | 46 | // Observe commands 47 | bindCommand(viewModel, ::consumeCommand) 48 | } 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_image.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/extentions/ComponentStateFlowValidation.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.extentions 2 | 3 | import io.github.mayconcardoso.mvvm.core.ComponentState 4 | import org.assertj.core.api.Assertions.assertThat 5 | 6 | 7 | fun List>.assertFlow(vararg expectedState: ComponentState<*>) { 8 | // The count must be the same. 9 | assertThat(size).isEqualTo(expectedState.size) 10 | 11 | // Check all items. 12 | for ((index, value) in expectedState.withIndex()) { 13 | when (value) { 14 | is ComponentState.Success -> { 15 | // The item at the same position must be a Success instance. 16 | assertThat(this[index]).isExactlyInstanceOf(ComponentState.Success::class.java) 17 | 18 | // Get item 19 | val checkingValue = this[index] as ComponentState.Success<*> 20 | 21 | // It is the same object 22 | assertThat(value.result).isEqualTo(checkingValue.result) 23 | } 24 | is ComponentState.Error -> { 25 | // The item at the same position must be a Success instance. 26 | assertThat(this[index]).isExactlyInstanceOf(ComponentState.Error::class.java) 27 | 28 | // Get item 29 | val checkingValue = this[index] as ComponentState.Error 30 | 31 | // It is the same object 32 | assertThat(value.reason).isEqualTo(checkingValue.reason) 33 | } 34 | else -> { 35 | assertThat(value).isEqualTo(this[index]) 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /library/simple-recyclerview/src/main/java/io/github/mayconcardoso/simple/recyclerview/utils/LoadNextPageScrollMonitor.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.simple.recyclerview.utils 2 | 3 | import androidx.recyclerview.widget.LinearLayoutManager 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | /** 7 | * Handle the pagination when the list is almost in the and. 8 | */ 9 | class LoadNextPageScrollMonitor( 10 | private val loadNextPageHandler: () -> Unit, 11 | ) : RecyclerView.OnScrollListener() { 12 | 13 | private var lastItemVisiblePositionOnList = 0 14 | 15 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 16 | val layoutManager = recyclerView.layoutManager as LinearLayoutManager 17 | val lastItemVisiblePosition = layoutManager.findLastVisibleItemPosition() 18 | 19 | // It is at last but one. 20 | if (isScrollingDown(lastItemVisiblePosition) && recyclerView.shouldLoadMoreItems()) { 21 | loadNextPageHandler.invoke() 22 | } 23 | 24 | lastItemVisiblePositionOnList = lastItemVisiblePosition 25 | } 26 | 27 | private fun isScrollingDown(lastItemVisiblePosition: Int): Boolean { 28 | return lastItemVisiblePosition > lastItemVisiblePositionOnList 29 | } 30 | 31 | private fun RecyclerView.shouldLoadMoreItems(): Boolean { 32 | val layoutManager = layoutManager as LinearLayoutManager 33 | 34 | val totalItemCount = layoutManager.itemCount 35 | val lastCompletelyVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition() 36 | 37 | return lastCompletelyVisibleItemPosition > totalItemCount - 3 38 | } 39 | } -------------------------------------------------------------------------------- /library/core/src/main/java/io/github/mayconcardoso/mvvm/core/ComponentState.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core 2 | 3 | /** 4 | * Used to manage separated component lifecycle. For example, let's say we have 3 different components on your screen. 5 | * Each component should load some individual data and show on the screen. 6 | * 7 | * So basically, you can start three Coroutines loading data flow and update each Livedata with this component. 8 | * And according the requests return some data or error you just update this component that your screen component will be notified with the changes and so on. 9 | * 10 | */ 11 | sealed class ComponentState { 12 | /** 13 | * This is the loading state of your component. 14 | */ 15 | sealed class Loading : ComponentState() { 16 | /** 17 | * Let's say you have a list component. 18 | * When you are loading from empty you wanna show a 'Screen loading progress' 19 | */ 20 | object FromEmpty : Loading() 21 | 22 | /** 23 | * Let's say you have a list component. 24 | * When you are loading from a not empty state you wanna show only a small loading progress at the bottom of your screen. 25 | */ 26 | data class FromState(val previousState: ComponentState) : Loading() 27 | } 28 | 29 | /** 30 | * When same error happen on your component. 31 | */ 32 | data class Error(val reason: Throwable, val lastData: T? = null) : ComponentState() 33 | 34 | /** 35 | * When everything is ok and loaded on your component. 36 | */ 37 | data class Success(val result: T) : ComponentState() 38 | } 39 | -------------------------------------------------------------------------------- /library/core-testing/src/test/java/io/github/mayconcardoso/mvvm/core/testing/extentions/ComponentStateFlowValidationKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.extentions 2 | 3 | import io.github.mayconcardoso.mvvm.core.ComponentState 4 | import io.github.mayconcardoso.mvvm.core.testing.extentions.assertFlow 5 | import io.github.mayconcardoso.mvvm.core.testing.testScenario 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import org.junit.Test 8 | 9 | @ExperimentalCoroutinesApi 10 | class ComponentStateFlowValidationKtTest { 11 | private val expectedValue = "Hello world" 12 | private val expectedError = RuntimeException() 13 | 14 | private val expectedSuccessFLow = mutableListOf>().apply { 15 | add(ComponentState.Loading.FromEmpty) 16 | add(ComponentState.Success(expectedValue)) 17 | }.toList() 18 | 19 | private val expectedErrorFLow = mutableListOf>().apply { 20 | add(ComponentState.Loading.FromEmpty) 21 | add(ComponentState.Error(expectedError)) 22 | }.toList() 23 | 24 | @Test 25 | fun `should assert success flow`() = testScenario( 26 | action = { 27 | expectedSuccessFLow 28 | }, 29 | assertions = { stateFlow -> 30 | stateFlow.assertFlow( 31 | ComponentState.Loading.FromEmpty, 32 | ComponentState.Success(expectedValue) 33 | ) 34 | } 35 | ) 36 | 37 | @Test 38 | fun `should assert error flow`() = testScenario( 39 | action = { 40 | expectedErrorFLow 41 | }, 42 | assertions = { stateFlow -> 43 | stateFlow.assertFlow( 44 | ComponentState.Loading.FromEmpty, 45 | ComponentState.Error(expectedError) 46 | ) 47 | } 48 | ) 49 | } -------------------------------------------------------------------------------- /library/networking/src/main/java/io/github/mayconcardoso/networking/NetworkErrorTransformer.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.networking 2 | 3 | import retrofit2.HttpException 4 | import java.io.IOException 5 | import java.net.ConnectException 6 | import java.net.NoRouteToHostException 7 | import java.net.SocketTimeoutException 8 | import java.net.UnknownHostException 9 | 10 | /** 11 | * @author MAYCON CARDOSO 12 | * 13 | * Transform any exception into a NetworkError in order to avoid any crash on the app. 14 | */ 15 | object NetworkErrorTransformer { 16 | fun transform(incoming: Throwable) = when (incoming) { 17 | is HttpException -> { 18 | translateHttpExceptionUsingStatusCode( 19 | incoming.code(), 20 | incoming.response()?.errorBody()?.string() 21 | ) 22 | } 23 | is SocketTimeoutException -> { 24 | NetworkError.OperationTimeout 25 | } 26 | is UnknownHostException, 27 | is ConnectException, 28 | is NoRouteToHostException -> { 29 | NetworkError.HostUnreachable 30 | } 31 | else -> { 32 | resolveOtherException(incoming) 33 | } 34 | } 35 | 36 | private fun resolveOtherException(incoming: Throwable) = if (isRequestCanceled(incoming)) { 37 | NetworkError.ConnectionSpike 38 | } else { 39 | NetworkError.UnknownNetworkingError 40 | } 41 | 42 | private fun isRequestCanceled(throwable: Throwable) = 43 | throwable is IOException && 44 | throwable.message?.contentEquals("Canceled") ?: false 45 | 46 | private fun translateHttpExceptionUsingStatusCode(code: Int, error: String?) = 47 | when (code) { 48 | in 400..499 -> NetworkError.ClientException(code, error) 49 | else -> NetworkError.RemoteException 50 | } 51 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | *./build/ 24 | 25 | # Local configuration file (sdk path, etc) 26 | local.properties 27 | 28 | # Proguard folder generated by Eclipse 29 | proguard/ 30 | 31 | # Log Files 32 | *.log 33 | 34 | # Android Studio Navigation editor temp files 35 | .navigation/ 36 | 37 | # Android Studio captures folder 38 | captures/ 39 | 40 | # IntelliJ 41 | *.iml 42 | .idea/.* 43 | .idea/* 44 | .idea/workspace.xml 45 | .idea/tasks.xml 46 | .idea/gradle.xml 47 | .idea/assetWizardSettings.xml 48 | .idea/dictionaries 49 | .idea/libraries 50 | # Android Studio 3 in .gitignore file. 51 | .idea/caches 52 | .idea/modules.xml 53 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 54 | .idea/navEditor.xml 55 | 56 | # Keystore files 57 | # Uncomment the following lines if you do not want to check your keystore files in. 58 | #*.jks 59 | #*.keystore 60 | 61 | # External native build folder generated in Android Studio 2.2 and later 62 | .externalNativeBuild 63 | .cxx/ 64 | 65 | # Google Services (e.g. APIs or Firebase) 66 | # google-services.json 67 | 68 | # Freeline 69 | freeline.py 70 | freeline/ 71 | freeline_project_description.json 72 | 73 | # fastlane 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots 77 | fastlane/test_output 78 | fastlane/readme.md 79 | 80 | # Version control 81 | vcs.xml 82 | 83 | # lint 84 | lint/intermediates/ 85 | lint/generated/ 86 | lint/outputs/ 87 | lint/tmp/ 88 | lint/reports/ 89 | 90 | # DB Store 91 | *./.DS_Store 92 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm 2 | 3 | import android.os.Bundle 4 | import androidx.activity.viewModels 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.mctech.architecture.mvvm.presentation.ImageCommands 7 | import com.mctech.architecture.mvvm.presentation.ImageViewModel 8 | import com.mctech.architecture.mvvm.presentation.view.ImageDetailsFragment 9 | import com.mctech.architecture.mvvm.presentation.view.ImageListFragment 10 | import io.github.mayconcardoso.mvvm.core.ViewCommand 11 | import io.github.mayconcardoso.mvvm.core.ktx.bindCommand 12 | import dagger.hilt.android.AndroidEntryPoint 13 | 14 | @AndroidEntryPoint 15 | class MainActivity : AppCompatActivity() { 16 | 17 | /** 18 | * Holds the feature view model 19 | */ 20 | private val viewModel by viewModels() 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.activity_main) 25 | 26 | // Load List Fragment - Just for sample purpose. 27 | // On a real project it could be replaced by a navigation logic. 28 | showDetailFragment() 29 | 30 | // Observe commands 31 | bindCommand(viewModel, ::consumeCommand) 32 | } 33 | 34 | private fun consumeCommand(command: ViewCommand) { 35 | when (command) { 36 | is ImageCommands.OpenImageDetails -> { 37 | openDetailScreen() 38 | } 39 | } 40 | } 41 | 42 | private fun showDetailFragment() { 43 | supportFragmentManager 44 | .beginTransaction() 45 | .replace(R.id.containerFragment, ImageListFragment()) 46 | .commit() 47 | } 48 | 49 | private fun openDetailScreen() { 50 | supportFragmentManager 51 | .beginTransaction() 52 | .replace(R.id.containerFragment, ImageDetailsFragment()) 53 | .commit() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_list_of_images.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 33 | 34 | 44 | -------------------------------------------------------------------------------- /library/core/src/main/java/io/github/mayconcardoso/mvvm/core/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.DefaultLifecycleObserver 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.MediatorLiveData 7 | import androidx.lifecycle.Observer 8 | import java.util.concurrent.atomic.AtomicBoolean 9 | 10 | /** 11 | * The original google code with some changes to make it easier to be used. 12 | */ 13 | class SingleLiveEvent : MediatorLiveData() { 14 | private val mPending = AtomicBoolean(false) 15 | private val mObservers = mutableMapOf>() 16 | 17 | fun observe(key: String, owner: LifecycleOwner, observer: Observer) { 18 | mObservers[key] = observer 19 | 20 | super.observe(owner) { t -> 21 | synchronized(mObservers) { 22 | if (mPending.compareAndSet(true, false)) { 23 | mObservers.forEach { 24 | it.value.onChanged(t) 25 | } 26 | } 27 | } 28 | } 29 | 30 | val lifecycleObserver = object : DefaultLifecycleObserver { 31 | override fun onDestroy(owner: LifecycleOwner) { 32 | owner.lifecycle.removeObserver(this) 33 | mObservers.remove(key) 34 | } 35 | } 36 | 37 | owner.lifecycle.addObserver(lifecycleObserver) 38 | } 39 | 40 | fun removeObserver(key: String) { 41 | synchronized(mObservers) { 42 | mObservers.filter { 43 | it.key == key 44 | }.forEach { 45 | super.removeObserver(it.value) 46 | } 47 | mObservers.remove(key) 48 | } 49 | } 50 | 51 | @MainThread 52 | override fun setValue(t: T?) { 53 | mPending.set(true) 54 | super.setValue(t) 55 | } 56 | 57 | /** 58 | * Used for cases where T is Void, to make calls cleaner. 59 | */ 60 | @MainThread 61 | fun call() { 62 | value = null 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_details_of_image.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 25 | 26 | 35 | 36 | 46 | 47 | -------------------------------------------------------------------------------- /library/core-testing/README.md: -------------------------------------------------------------------------------- 1 | # Core Testing Library 2 | 3 | It makes your ComponentState, live data and StateFlow testing extremely easier. There are a lot of [extensions](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/core-testing/src/main/java/com/mctech/architecture/mvvm/core/testing/extentions) to help you. 4 | 5 | 6 | ```kotlin 7 | internal class ImageViewModelTest : BaseViewModelTest() { 8 | private val expectedList = mutableListOf() 9 | private val loadImageDetailsCase = mock() 10 | private val loadImageListCase = mock() 11 | private val viewModel = ImageViewModel( 12 | loadImageListCase, 13 | loadImageDetailsCase 14 | ) 15 | 16 | @Test 17 | fun `should initialize components`() = observerScenario { 18 | thenAssertFlow(viewModel.state) { 19 | it.assertFlow(ComponentState.Loading.FromEmpty) 20 | } 21 | 22 | thenAssertFlow(viewModel.detailState) { 23 | it.assertFlow(ComponentState.Loading.FromEmpty) 24 | } 25 | } 26 | 27 | @Test 28 | fun `should show data on list component`() = observerScenario { 29 | givenScenario { 30 | whenever(loadImageListCase.execute()).thenReturn( 31 | InteractionResult.Success(expectedList) 32 | ) 33 | } 34 | 35 | whenAction { 36 | viewModel.initialize() 37 | } 38 | 39 | thenAssertFlow(viewModel.state) { 40 | it.assertFlow( 41 | ComponentState.Loading.FromEmpty, 42 | ComponentState.Success(expectedList) 43 | ) 44 | verify(loadImageListCase, times(1)).execute() 45 | } 46 | } 47 | 48 | @Test 49 | fun `should show error on list component`() = observerScenario { 50 | givenScenario { 51 | whenever(loadImageListCase.execute()).thenReturn( 52 | InteractionResult.Error(ImageException.CannotFetchImages) 53 | ) 54 | } 55 | 56 | whenAction { 57 | viewModel.initialize() 58 | } 59 | 60 | thenAssertFlow(viewModel.state) { 61 | it.assertFlow( 62 | ComponentState.Loading.FromEmpty, 63 | ComponentState.Error>(ImageException.CannotFetchImages) 64 | ) 65 | verify(loadImageListCase, times(1)).execute() 66 | } 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /library/simple-recyclerview/src/main/java/io/github/mayconcardoso/simple/recyclerview/RecyclerViewBinding.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.simple.recyclerview 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.* 6 | import androidx.viewbinding.ViewBinding 7 | import io.github.mayconcardoso.simple.recyclerview.base.SimpleBindingAdapter 8 | import io.github.mayconcardoso.simple.recyclerview.base.SimpleBindingHolder 9 | import io.github.mayconcardoso.simple.recyclerview.utils.SimpleItemDiffCallback 10 | 11 | /** 12 | * Encapsulate the creation of recycler views in order to reduce all the necessary boilerplate. 13 | */ 14 | fun RecyclerView.prepareRecyclerView( 15 | // Data that will be initially rendered. 16 | items: List = listOf(), 17 | 18 | // Diff algorithm 19 | diffCallbackFactory: () -> DiffUtil.ItemCallback = { 20 | SimpleItemDiffCallback() 21 | }, 22 | 23 | // Setup recycler view callback. 24 | setupRecyclerView: (RecyclerView) -> Unit = { recyclerView -> 25 | recyclerView.setHasFixedSize(true) 26 | recyclerView.itemAnimator = DefaultItemAnimator() 27 | recyclerView.layoutManager = LinearLayoutManager(context).apply { 28 | orientation = RecyclerView.VERTICAL 29 | } 30 | }, 31 | 32 | // Prepare view binding 33 | viewHolderFactory: (parent: ViewGroup, inflater: LayoutInflater) -> VDB, 34 | 35 | // Delegate to bind the item on the view holder. 36 | bindView: (item: T, viewBinding: VDB) -> Unit, 37 | 38 | // Called when list is done 39 | onAdapterAttached: (adapter: SimpleBindingAdapter) -> Unit = {} 40 | ) { 41 | // If adapter has already been attached. 42 | // We just update the content. 43 | if (adapter != null) { 44 | (adapter as ListAdapter>).submitList(items) 45 | return 46 | } 47 | 48 | // Setup recycler 49 | setupRecyclerView(this) 50 | 51 | // Create a new adapter. 52 | val adapter = SimpleBindingAdapter( 53 | bindView = bindView, 54 | diffCallback = diffCallbackFactory(), 55 | viewHolderFactory = viewHolderFactory, 56 | ) 57 | 58 | // Attach it on the recycler view. 59 | this.adapter = adapter.apply { 60 | submitList(items) 61 | } 62 | 63 | // Inform the client that everything is set. 64 | onAdapterAttached.invoke(adapter) 65 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/presentation/view/ImageDetailsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.presentation.view 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.view.isVisible 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import com.mctech.architecture.mvvm.R 9 | import com.mctech.architecture.mvvm.databinding.FragmentDetailsOfImageBinding 10 | import com.mctech.architecture.mvvm.domain.entities.ImageDetails 11 | import com.mctech.architecture.mvvm.presentation.ImageViewModel 12 | import io.github.mayconcardoso.mvvm.core.ComponentState 13 | import io.github.mayconcardoso.mvvm.core.ktx.bindState 14 | import io.github.mayconcardoso.mvvm.core.ktx.viewBinding 15 | import dagger.hilt.android.AndroidEntryPoint 16 | 17 | @AndroidEntryPoint 18 | class ImageDetailsFragment : Fragment(R.layout.fragment_details_of_image) { 19 | 20 | // region Variables 21 | 22 | /** 23 | * Holds the feature view model 24 | */ 25 | private val viewModel by viewModels( 26 | ownerProducer = { requireActivity() } 27 | ) 28 | 29 | /** 30 | * Holds the feature view binding 31 | */ 32 | private val binding by viewBinding(FragmentDetailsOfImageBinding::bind) 33 | 34 | // endregion 35 | 36 | // region Lifecycle 37 | 38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 39 | // Start view model flow 40 | viewModel.initialize() 41 | 42 | // Observes image component state. 43 | bindState(viewModel.detailState, ::consumeComponentState) 44 | } 45 | 46 | // endregion 47 | 48 | // region State Manipulation 49 | 50 | private fun consumeComponentState(state: ComponentState) = when (state) { 51 | is ComponentState.Error -> renderErrorState() 52 | is ComponentState.Loading -> renderLoadingState() 53 | is ComponentState.Success -> renderSuccessState(state.result) 54 | } 55 | 56 | private fun renderLoadingState() { 57 | binding.progressState.isVisible = true 58 | binding.successContainer.isVisible = false 59 | } 60 | 61 | private fun renderErrorState() { 62 | binding.progressState.isVisible = false 63 | binding.successContainer.isVisible = false 64 | } 65 | 66 | private fun renderSuccessState(details: ImageDetails) { 67 | binding.progressState.isVisible = false 68 | binding.successContainer.isVisible = true 69 | binding.tvTitle.text = details.description 70 | } 71 | 72 | // endregion 73 | 74 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-kapt' 3 | apply plugin: 'kotlin-android' 4 | apply plugin: "dagger.hilt.android.plugin" 5 | 6 | android { 7 | compileSdkVersion targets.compileSdk 8 | defaultConfig { 9 | minSdkVersion targets.minSdk 10 | targetSdkVersion targets.targetSdk 11 | versionCode 1 12 | versionName "1.0.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | debug { 18 | } 19 | release { 20 | } 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_11 25 | targetCompatibility JavaVersion.VERSION_11 26 | } 27 | 28 | buildFeatures { 29 | viewBinding true 30 | } 31 | } 32 | 33 | dependencies { 34 | // [REQUIRED] Core library. With the architecture components. 35 | implementation project(path: ':library:core') 36 | implementation project(path: ':library:core-extentions') 37 | implementation project(path: ':library:simple-recyclerview') 38 | 39 | // Dagger 40 | implementation "com.google.dagger:dagger:2.45" 41 | implementation "com.google.dagger:hilt-android:2.45" 42 | kapt "com.google.dagger:hilt-compiler:2.45" 43 | 44 | // Plataform 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" 46 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" 47 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" 48 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 49 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 50 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' 51 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' 52 | implementation "androidx.fragment:fragment-ktx:1.5.6" 53 | 54 | implementation 'androidx.appcompat:appcompat:1.6.1' 55 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 56 | implementation 'androidx.recyclerview:recyclerview:1.3.0' 57 | 58 | testImplementation project(path: ':library:core-testing') 59 | testImplementation 'junit:junit:4.13.2' 60 | testImplementation 'androidx.test:runner:1.5.2' 61 | testImplementation 'org.assertj:assertj-core:3.24.2' 62 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' 63 | testImplementation 'androidx.arch.core:core-testing:2.2.0' 64 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" 65 | testImplementation "org.mockito:mockito-inline:2.21.0" 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MVVM Architecture Toolkit 2 | This is only a personal implementation of MVVM architecture that makes your life easier by helping you to keep your screen components independently. It also has a concept of "interaction" defining exactly what the user can do on your screen turning the testing process extremely easier, once now you are able to test the "state" of your app. 3 | 4 | It is extremely simple to use it and to test it. But again, it is only a personal implementation. However, if this library help you anyway, please give me a star :) 5 | 6 | ## Download 7 | ```groovy 8 | ext { 9 | MVVM_ARC_VERSION = '2.1.0' 10 | } 11 | 12 | // [REQUIRED] Core library. With the architecture components. 13 | implementation "io.github.mayconcardoso:mvvm-core:${MVVM_ARC_VERSION}" 14 | 15 | // [OPTIONAL] Core Extension library. With the architecture components extensions to bind states and so on. 16 | implementation "io.github.mayconcardoso:mvvm-core-ktx:${MVVM_ARC_VERSION}" 17 | 18 | // [OPTIONAL] Testing library. To test your architecture easily with contextual functions to make your tests cleaner. 19 | testImplementation "io.github.mayconcardoso:mvvm-core-testing:${MVVM_ARC_VERSION}" 20 | 21 | // [OPTIONAL] Networking library. To help you create your APIs easily with mapped errors to better handle business logic and avoid crashes. 22 | implementation "io.github.mayconcardoso:networking:${MVVM_ARC_VERSION}" 23 | 24 | // [OPTIONAL] Simpler recyclerview library. To make it simpler and reduce the boilerplate needed to render a list of items. 25 | implementation "io.github.mayconcardoso:simple-recyclerview:${MVVM_ARC_VERSION}" 26 | 27 | ``` 28 | 29 | ## Related Library 30 | 31 | [Architecture Boilerplate Generator](https://github.com/MayconCardoso/ArchitectureBoilerplateGenerator) - It is a personal code generator to create new features and to avoid writing a lot of boilerplate. 32 | 33 | ## Documentation 34 | * [Core Library](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/core) 35 | * [Core Extensions Library](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/core-extentions) 36 | * [Core Testing Library](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/core-testing) 37 | * [Networking Library](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/networking) 38 | * [Simple Recycler View Library](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/simple-recyclerview) 39 | 40 | ## Sample 41 | 42 | Here are a couple of real android Apps implementing this library to define their architecture. 43 | * [Poker Grinder](https://github.com/MayconCardoso/poker-grinder) 44 | * [StockTradeTracking](https://github.com/MayconCardoso/StockTradeTracking) 45 | -------------------------------------------------------------------------------- /library/core-extentions/src/main/java/io/github/mayconcardoso/mvvm/core/ktx/ActivityExtention.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.ktx 2 | 3 | import android.view.LayoutInflater 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.lifecycleScope 7 | import androidx.viewbinding.ViewBinding 8 | import io.github.mayconcardoso.mvvm.core.BaseViewModel 9 | import io.github.mayconcardoso.mvvm.core.ViewCommand 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.flow.collectLatest 12 | import kotlinx.coroutines.flow.stateIn 13 | import kotlinx.coroutines.launch 14 | 15 | /** 16 | * Called to observe any state flow from your view model. 17 | */ 18 | inline fun AppCompatActivity.bindState( 19 | observable: StateFlow, 20 | crossinline block: (result: T) -> Unit, 21 | ) = lifecycleScope.launch { 22 | observable 23 | .stateIn(lifecycleScope) 24 | .collectLatest { 25 | block(it) 26 | } 27 | } 28 | 29 | /** 30 | * Called to observe any live data from your view model. 31 | */ 32 | inline fun AppCompatActivity.bindState( 33 | observable: LiveData, 34 | crossinline block: (result: T) -> Unit, 35 | ) = lifecycleScope.launch { 36 | observable 37 | .observe(this@bindState) { 38 | block(it) 39 | } 40 | } 41 | 42 | /** 43 | * Called to observe all commands sent from your view mode. 44 | */ 45 | fun AppCompatActivity.bindCommand( 46 | viewModel: BaseViewModel, 47 | block: (result: ViewCommand) -> Unit, 48 | ) { 49 | commandObserver( 50 | lifecycle = this@bindCommand, 51 | viewModel = viewModel, 52 | block = block, 53 | ) 54 | } 55 | 56 | /** 57 | * Called to observe only the first command sent from your view mode. 58 | */ 59 | fun AppCompatActivity.bindAutoDisposableCommand( 60 | viewModel: BaseViewModel, 61 | block: (result: ViewCommand) -> Unit, 62 | ) { 63 | autoDisposeCommandObserver( 64 | lifecycle = this@bindAutoDisposableCommand, 65 | viewModel = viewModel, 66 | block = block, 67 | ) 68 | } 69 | 70 | /** 71 | * Delegate to bind the view of an activity. It can be used from [AppCompatActivity.onCreate] to 72 | * [AppCompatActivity.onDestroy] (inclusive). 73 | * 74 | * Sample usage: 75 | * ``` 76 | * class SampleActivity : AppCompatActivity() { 77 | * private val binding by viewBinding(ActivitySampleBinding::inflate) 78 | * 79 | * override fun onCreate(savedInstanceState: Bundle?) { 80 | * super.onCreate(savedInstanceState) 81 | * setContentView(binding.root) 82 | * // binding is ready to use 83 | * } 84 | * } 85 | * ``` 86 | */ 87 | inline fun AppCompatActivity.viewBinding( 88 | crossinline factory: (LayoutInflater) -> T, 89 | ): Lazy = lazy(LazyThreadSafetyMode.NONE) { 90 | factory(layoutInflater) 91 | } -------------------------------------------------------------------------------- /sample/src/test/java/com/mctech/architecture/mvvm/domain/interactions/LoadImageListCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.interactions 2 | 3 | import com.mctech.architecture.mvvm.domain.entities.Image 4 | import com.mctech.architecture.mvvm.domain.error.ImageException 5 | import com.mctech.architecture.mvvm.domain.service.ImageService 6 | import io.github.mayconcardoso.mvvm.core.testing.testScenario 7 | import com.nhaarman.mockitokotlin2.mock 8 | import com.nhaarman.mockitokotlin2.verify 9 | import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions 10 | import com.nhaarman.mockitokotlin2.whenever 11 | import kotlinx.coroutines.ExperimentalCoroutinesApi 12 | import org.junit.Before 13 | import org.junit.Test 14 | 15 | @ExperimentalCoroutinesApi 16 | class LoadImageListCaseTest{ 17 | 18 | private val service = mock() 19 | private val expectedException = mock() 20 | private val imageException = ImageException.CannotFetchImages 21 | private val expectedValue = listOf() 22 | 23 | private lateinit var useCase: LoadImageListCase 24 | 25 | @Before 26 | fun `before each test`() { 27 | useCase = LoadImageListCase(service) 28 | } 29 | 30 | @Test 31 | fun `should delegate call`() = testScenario( 32 | scenario = { 33 | whenever(service.getAllImages()).thenReturn(expectedValue) 34 | }, 35 | action = { 36 | useCase.execute() 37 | }, 38 | assertions = { 39 | verify(service).getAllImages() 40 | verifyNoMoreInteractions(service) 41 | } 42 | ) 43 | 44 | @Test 45 | fun `should return success`() = testScenario( 46 | scenario = { 47 | whenever(service.getAllImages()).thenReturn(expectedValue) 48 | }, 49 | action = { 50 | useCase.execute() 51 | }, 52 | assertions = { result -> 53 | result.assertResultSuccess(expectedValue) 54 | } 55 | ) 56 | 57 | 58 | @Test 59 | fun `should return unknown exception`() = testScenario( 60 | scenario = { 61 | whenever(service.getAllImages()).thenThrow(expectedException) 62 | }, 63 | action = { 64 | useCase.execute() 65 | }, 66 | assertions = { result -> 67 | result.assertResultFailure(ImageException.UnknownImageException) 68 | } 69 | ) 70 | 71 | @Test 72 | fun `should return known exception`() = testScenario( 73 | scenario = { 74 | whenever(service.getAllImages()).thenThrow(imageException) 75 | }, 76 | action = { 77 | useCase.execute() 78 | }, 79 | assertions = { result -> 80 | result.assertResultFailure(imageException) 81 | } 82 | ) 83 | } -------------------------------------------------------------------------------- /sample/src/test/java/com/mctech/architecture/mvvm/domain/interactions/LoadImageDetailsCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.domain.interactions 2 | 3 | import com.mctech.architecture.mvvm.domain.entities.Image 4 | import com.mctech.architecture.mvvm.domain.entities.ImageDetails 5 | import com.mctech.architecture.mvvm.domain.error.ImageException 6 | import com.mctech.architecture.mvvm.domain.service.ImageService 7 | import io.github.mayconcardoso.mvvm.core.testing.testScenario 8 | import com.nhaarman.mockitokotlin2.* 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import org.junit.Before 11 | import org.junit.Test 12 | 13 | @ExperimentalCoroutinesApi 14 | class LoadImageDetailsCaseTest{ 15 | 16 | private val service = mock() 17 | private val expectedException = mock() 18 | private val imageException = ImageException.CannotFetchImages 19 | private val expectedValue = mock() 20 | private val expectedRequest = mock() 21 | 22 | private lateinit var useCase: LoadImageDetailsCase 23 | 24 | @Before 25 | fun `before each test`() { 26 | useCase = LoadImageDetailsCase(service) 27 | } 28 | 29 | @Test 30 | fun `should delegate call`() = testScenario( 31 | scenario = { 32 | whenever(service.getImageDetails(any())).thenReturn(expectedValue) 33 | }, 34 | action = { 35 | useCase.execute(expectedRequest) 36 | }, 37 | assertions = { 38 | verify(service).getImageDetails(expectedRequest) 39 | verifyNoMoreInteractions(service) 40 | } 41 | ) 42 | 43 | @Test 44 | fun `should return success`() = testScenario( 45 | scenario = { 46 | whenever(service.getImageDetails(any())).thenReturn(expectedValue) 47 | }, 48 | action = { 49 | useCase.execute(expectedRequest) 50 | }, 51 | assertions = { result -> 52 | result.assertResultSuccess(expectedValue) 53 | } 54 | ) 55 | 56 | 57 | @Test 58 | fun `should return unknown exception`() = testScenario( 59 | scenario = { 60 | whenever(service.getImageDetails(any())).thenThrow(expectedException) 61 | }, 62 | action = { 63 | useCase.execute(expectedRequest) 64 | }, 65 | assertions = { result -> 66 | result.assertResultFailure(ImageException.UnknownImageException) 67 | } 68 | ) 69 | 70 | @Test 71 | fun `should return known exception`() = testScenario( 72 | scenario = { 73 | whenever(service.getImageDetails(any())).thenThrow(imageException) 74 | }, 75 | action = { 76 | useCase.execute(expectedRequest) 77 | }, 78 | assertions = { result -> 79 | result.assertResultFailure(imageException) 80 | } 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/presentation/ImageViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.presentation 2 | 3 | import com.mctech.architecture.mvvm.domain.InteractionResult 4 | import com.mctech.architecture.mvvm.domain.entities.Image 5 | import com.mctech.architecture.mvvm.domain.entities.ImageDetails 6 | import com.mctech.architecture.mvvm.domain.interactions.LoadImageDetailsCase 7 | import com.mctech.architecture.mvvm.domain.interactions.LoadImageListCase 8 | import io.github.mayconcardoso.mvvm.core.BaseViewModel 9 | import io.github.mayconcardoso.mvvm.core.ComponentState 10 | import io.github.mayconcardoso.mvvm.core.OnInteraction 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class ImageViewModel @Inject constructor( 18 | private val loadImageListCase: LoadImageListCase, 19 | private val loadImageDetailsCase: LoadImageDetailsCase 20 | ) : BaseViewModel() { 21 | 22 | private val _state by lazy { 23 | MutableStateFlow>>(ComponentState.Loading.FromEmpty) 24 | } 25 | val state: StateFlow>> by lazy { _state } 26 | 27 | private val _detailState by lazy { 28 | MutableStateFlow>(ComponentState.Loading.FromEmpty) 29 | } 30 | val detailState: StateFlow> by lazy { _detailState } 31 | 32 | override suspend fun initializeComponents() { 33 | loadImagesInteraction() 34 | } 35 | 36 | private suspend fun loadImagesInteraction() { 37 | when (val listResult = loadImageListCase.execute()) { 38 | // Set the list component with 'Success' state. 39 | is InteractionResult.Success -> { 40 | _state.value = ComponentState.Success(listResult.result) 41 | } 42 | 43 | // Set the list component with 'Error' state. 44 | is InteractionResult.Error -> { 45 | _state.value = ComponentState.Error(listResult.error) 46 | } 47 | } 48 | } 49 | 50 | @OnInteraction(ImageInteraction.OpenDetails::class) 51 | private suspend fun openImageDetailsInteraction(interaction: ImageInteraction.OpenDetails) { 52 | // Set the details component with 'loading' state. 53 | _detailState.value = ComponentState.Loading.FromEmpty 54 | 55 | // Open the details screen. 56 | sendCommand(ImageCommands.OpenImageDetails) 57 | 58 | // Load image's details. 59 | when (val detailsResult = loadImageDetailsCase.execute(interaction.image)) { 60 | // Set the details component with 'Success' state. 61 | is InteractionResult.Success -> { 62 | _detailState.value = ComponentState.Success(detailsResult.result) 63 | } 64 | 65 | // Set the details component with 'Error' state. 66 | is InteractionResult.Error -> { 67 | _detailState.value = ComponentState.Error(detailsResult.error) 68 | } 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /library/networking/src/test/java/io/github/mayconcardoso/networking/NetworkErrorTransformerTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.networking 2 | 3 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 4 | import okhttp3.ResponseBody.Companion.toResponseBody 5 | import org.assertj.core.api.Assertions 6 | import org.junit.Test 7 | import retrofit2.HttpException 8 | import retrofit2.Response 9 | import java.io.IOException 10 | import java.net.ConnectException 11 | import java.net.NoRouteToHostException 12 | import java.net.SocketTimeoutException 13 | import java.net.UnknownHostException 14 | 15 | 16 | class NetworkErrorTransformerTest { 17 | 18 | @Test 19 | fun `should return a client exception`() { 20 | internalAssertion( 21 | exception = createHttpException(400, "Bad format request"), 22 | expectedValue = NetworkError.ClientException::class.java 23 | ) 24 | 25 | internalAssertion( 26 | exception = createHttpException(404, "Not found"), 27 | expectedValue = NetworkError.ClientException::class.java 28 | ) 29 | } 30 | 31 | @Test 32 | fun `should return a server exception`() { 33 | internalAssertion( 34 | exception = createHttpException(500, "Internal server error"), 35 | expectedValue = NetworkError.RemoteException::class.java 36 | ) 37 | } 38 | 39 | @Test 40 | fun `should return a operation timeout`() { 41 | internalAssertion( 42 | exception = SocketTimeoutException(), 43 | expectedValue = NetworkError.OperationTimeout::class.java 44 | ) 45 | } 46 | 47 | @Test 48 | fun `should return a host unreachable`() { 49 | internalAssertion( 50 | exception = UnknownHostException(), 51 | expectedValue = NetworkError.HostUnreachable::class.java 52 | ) 53 | internalAssertion( 54 | exception = ConnectException(), 55 | expectedValue = NetworkError.HostUnreachable::class.java 56 | ) 57 | internalAssertion( 58 | exception = NoRouteToHostException(), 59 | expectedValue = NetworkError.HostUnreachable::class.java 60 | ) 61 | } 62 | 63 | @Test 64 | fun `should return a connection spike`() { 65 | internalAssertion( 66 | exception = IOException("Canceled"), 67 | expectedValue = NetworkError.ConnectionSpike::class.java 68 | ) 69 | } 70 | 71 | @Test 72 | fun `should return a default exception`() { 73 | internalAssertion( 74 | exception = Throwable(), 75 | expectedValue = NetworkError.UnknownNetworkingError::class.java 76 | ) 77 | } 78 | 79 | private fun internalAssertion(exception: Throwable, expectedValue: Class<*>) { 80 | val result = NetworkErrorTransformer.transform(exception) 81 | Assertions.assertThat(result) 82 | .isExactlyInstanceOf( 83 | expectedValue 84 | ) 85 | } 86 | 87 | private fun createHttpException(code: Int, error: String): HttpException { 88 | val format = "application/json".toMediaTypeOrNull() 89 | val responseBody = error.toResponseBody(format) 90 | return HttpException(Response.error(code, responseBody)) 91 | } 92 | } -------------------------------------------------------------------------------- /sample/src/test/java/com/mctech/architecture/mvvm/presentation/ImageViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.presentation 2 | 3 | 4 | import androidx.lifecycle.viewModelScope 5 | import io.github.mayconcardoso.mvvm.core.ComponentState 6 | import io.github.mayconcardoso.mvvm.core.testing.BaseViewModelTest 7 | import io.github.mayconcardoso.mvvm.core.testing.extentions.TestObserverScenario.Companion.observerScenario 8 | import io.github.mayconcardoso.mvvm.core.testing.extentions.assertFlow 9 | import com.mctech.architecture.mvvm.domain.InteractionResult 10 | import com.mctech.architecture.mvvm.domain.entities.Image 11 | import com.mctech.architecture.mvvm.domain.error.ImageException 12 | import com.mctech.architecture.mvvm.domain.interactions.LoadImageDetailsCase 13 | import com.mctech.architecture.mvvm.domain.interactions.LoadImageListCase 14 | import com.nhaarman.mockitokotlin2.mock 15 | import com.nhaarman.mockitokotlin2.times 16 | import com.nhaarman.mockitokotlin2.verify 17 | import com.nhaarman.mockitokotlin2.whenever 18 | import kotlinx.coroutines.ExperimentalCoroutinesApi 19 | import kotlinx.coroutines.cancel 20 | import org.junit.After 21 | import org.junit.Before 22 | import org.junit.Test 23 | 24 | @ExperimentalCoroutinesApi 25 | internal class ImageViewModelTest : BaseViewModelTest() { 26 | private val expectedList = mutableListOf() 27 | private val loadImageDetailsCase = mock() 28 | private val loadImageListCase = mock() 29 | private val viewModel = ImageViewModel( 30 | loadImageListCase, 31 | loadImageDetailsCase 32 | ) 33 | 34 | @Test 35 | fun `should initialize components`() = observerScenario { 36 | thenAssertFlow(viewModel.state) { 37 | it.assertFlow(ComponentState.Loading.FromEmpty) 38 | } 39 | 40 | thenAssertFlow(viewModel.detailState) { 41 | it.assertFlow(ComponentState.Loading.FromEmpty) 42 | } 43 | } 44 | 45 | @Test 46 | fun `should show data on list component`() = observerScenario { 47 | givenScenario { 48 | whenever(loadImageListCase.execute()).thenReturn( 49 | InteractionResult.Success(expectedList) 50 | ) 51 | } 52 | 53 | whenAction { 54 | viewModel.initialize() 55 | } 56 | 57 | thenAssertFlow(viewModel.state) { 58 | it.assertFlow( 59 | ComponentState.Loading.FromEmpty, 60 | ComponentState.Success(expectedList) 61 | ) 62 | verify(loadImageListCase, times(1)).execute() 63 | } 64 | } 65 | 66 | @Test 67 | fun `should show error on list component`() = observerScenario { 68 | givenScenario { 69 | whenever(loadImageListCase.execute()).thenReturn( 70 | InteractionResult.Error(ImageException.CannotFetchImages) 71 | ) 72 | } 73 | 74 | whenAction { 75 | viewModel.initialize() 76 | } 77 | 78 | thenAssertFlow(viewModel.state) { 79 | it.assertFlow( 80 | ComponentState.Loading.FromEmpty, 81 | ComponentState.Error>(ImageException.CannotFetchImages) 82 | ) 83 | verify(loadImageListCase, times(1)).execute() 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/presentation/view/ImageListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.presentation.view 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.core.view.isVisible 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.recyclerview.widget.DiffUtil 11 | import com.mctech.architecture.mvvm.R 12 | import com.mctech.architecture.mvvm.databinding.FragmentListOfImagesBinding 13 | import com.mctech.architecture.mvvm.databinding.ItemImageBinding 14 | import com.mctech.architecture.mvvm.domain.entities.Image 15 | import com.mctech.architecture.mvvm.presentation.ImageInteraction 16 | import com.mctech.architecture.mvvm.presentation.ImageViewModel 17 | import io.github.mayconcardoso.mvvm.core.ComponentState 18 | import io.github.mayconcardoso.mvvm.core.ktx.bindState 19 | import io.github.mayconcardoso.mvvm.core.ktx.viewBinding 20 | import io.github.mayconcardoso.simple.recyclerview.prepareRecyclerView 21 | import dagger.hilt.android.AndroidEntryPoint 22 | 23 | @AndroidEntryPoint 24 | class ImageListFragment : Fragment(R.layout.fragment_list_of_images) { 25 | 26 | // region Variables 27 | 28 | /** 29 | * Holds the feature view model 30 | */ 31 | private val viewModel by viewModels( 32 | ownerProducer = { requireActivity() } 33 | ) 34 | 35 | /** 36 | * Holds the feature view binding 37 | */ 38 | private val binding by viewBinding(FragmentListOfImagesBinding::bind) 39 | 40 | // endregion 41 | 42 | // region Lifecycle 43 | 44 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 45 | // Start view model flow 46 | viewModel.initialize() 47 | 48 | // Observes image component state. 49 | bindState(viewModel.state, ::consumeComponentState) 50 | } 51 | 52 | // endregion 53 | 54 | // region State Manipulation 55 | 56 | private fun consumeComponentState(state: ComponentState>) = when (state) { 57 | is ComponentState.Error -> renderErrorState() 58 | is ComponentState.Loading -> renderLoadingState() 59 | is ComponentState.Success -> renderSuccessState(state.result) 60 | } 61 | 62 | private fun renderLoadingState() { 63 | binding.recyclerList.isVisible = false 64 | binding.progressState.isVisible = true 65 | binding.errorComponent.isVisible = false 66 | } 67 | 68 | private fun renderErrorState() { 69 | binding.recyclerList.isVisible = false 70 | binding.progressState.isVisible = false 71 | binding.errorComponent.isVisible = true 72 | } 73 | 74 | private fun renderSuccessState(images: List) { 75 | binding.recyclerList.isVisible = true 76 | binding.progressState.isVisible = false 77 | binding.errorComponent.isVisible = false 78 | binding.recyclerList.prepareRecyclerView( 79 | items = images, 80 | bindView = this::renderImageItem, 81 | viewHolderFactory = this::createViewHolder, 82 | diffCallbackFactory = this::createImageDiffAlgorithm, 83 | ) 84 | } 85 | 86 | private fun createViewHolder(parent: ViewGroup, inflater: LayoutInflater): ItemImageBinding { 87 | return ItemImageBinding.inflate(inflater, parent, false) 88 | } 89 | 90 | private fun createImageDiffAlgorithm(): DiffUtil.ItemCallback { 91 | return object : DiffUtil.ItemCallback() { 92 | override fun areItemsTheSame(left: Image, right: Image) = left.id == right.id 93 | override fun areContentsTheSame(left: Image, right: Image): Boolean { 94 | return left.title == right.title && left.date == right.date 95 | } 96 | } 97 | } 98 | 99 | private fun renderImageItem(item: Image, binding: ItemImageBinding) { 100 | binding.tvTitle.text = item.title 101 | binding.tvDate.text = item.date 102 | 103 | binding.root.setOnClickListener { 104 | viewModel.interact(ImageInteraction.OpenDetails(item)) 105 | } 106 | } 107 | 108 | // endregion 109 | 110 | } -------------------------------------------------------------------------------- /library/core-testing/src/main/java/io/github/mayconcardoso/mvvm/core/testing/extentions/TestObserverScenario.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.testing.extentions 2 | 3 | import androidx.lifecycle.LiveData 4 | import io.github.mayconcardoso.mvvm.core.testing.agent.FlowObserverAgent 5 | import io.github.mayconcardoso.mvvm.core.testing.agent.LiveDataObserverAgent 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 10 | import kotlinx.coroutines.test.runTest 11 | import org.assertj.core.api.Assertions.assertThat 12 | import kotlin.coroutines.CoroutineContext 13 | import kotlin.coroutines.EmptyCoroutineContext 14 | 15 | @OptIn(ExperimentalCoroutinesApi::class) 16 | class TestObserverScenario { 17 | private val flowAgents = mutableListOf>() 18 | private val liveDataAgents = mutableListOf>() 19 | private var action: (suspend () -> Unit)? = null 20 | private var scenario: (suspend () -> Unit)? = null 21 | private var assertion: (suspend () -> Unit)? = null 22 | 23 | fun givenScenario(scenario: suspend () -> Unit) { 24 | if (this.scenario != null) { 25 | throw IllegalArgumentException("You must have only one scenario by test.") 26 | } 27 | 28 | this.scenario = scenario 29 | } 30 | 31 | fun whenAction(action: suspend () -> Unit) { 32 | if (this.action != null) { 33 | throw IllegalArgumentException("You must have only one action by test.") 34 | } 35 | 36 | this.action = action 37 | } 38 | 39 | fun thenAssert(assert: suspend () -> Unit) { 40 | if (this.assertion != null) { 41 | throw IllegalArgumentException("You must have only one assertion block by test.") 42 | } 43 | 44 | assertion = assert 45 | } 46 | 47 | fun thenAssertFlow(flow: Flow, assert: suspend (List) -> Unit) { 48 | flowAgents.add( 49 | FlowObserverAgent(flow, assert), 50 | ) 51 | } 52 | 53 | fun thenAssertFlowContainsExactly(flow: Flow, vararg values: T) { 54 | thenAssertFlow(flow) { data -> 55 | assertThat(data).containsExactly(*values) 56 | } 57 | } 58 | 59 | fun thenAssertFlowIsEmpty(flow: Flow) { 60 | thenAssertFlow(flow) { data -> 61 | assertThat(data).isEmpty() 62 | } 63 | } 64 | 65 | fun thenAssertLiveData(liveData: LiveData, assert: suspend (List) -> Unit) { 66 | liveDataAgents.add( 67 | LiveDataObserverAgent(liveData, assert), 68 | ) 69 | } 70 | 71 | fun thenAssertLiveDataContainsExactly(liveData: LiveData, vararg values: T) { 72 | thenAssertLiveData(liveData) { data -> 73 | assertThat(data).containsExactly(*values) 74 | } 75 | } 76 | 77 | fun thenAssertLiveDataFlowIsEmpty(liveData: LiveData) { 78 | thenAssertLiveData(liveData) { data -> 79 | assertThat(data).isEmpty() 80 | } 81 | } 82 | 83 | private fun execute(context: CoroutineContext) { 84 | try { 85 | runTest(context) { 86 | // Prepare test 87 | scenario?.invoke() 88 | 89 | // Start commands collection 90 | liveDataAgents.forEach { 91 | it.observe() 92 | } 93 | 94 | // Start states collection 95 | val scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)) 96 | flowAgents.forEach { 97 | it.collect(scope) 98 | } 99 | 100 | // Action trigger 101 | action?.invoke() 102 | 103 | // Notify 104 | liveDataAgents.forEach { 105 | it.resume() 106 | } 107 | 108 | testScheduler.advanceUntilIdle() 109 | flowAgents.forEach { 110 | it.resume() 111 | } 112 | 113 | assertion?.invoke() 114 | } 115 | } finally { 116 | liveDataAgents.forEach { 117 | it.release() 118 | } 119 | flowAgents.forEach { 120 | it.release() 121 | } 122 | } 123 | } 124 | 125 | companion object { 126 | fun observerScenario( 127 | context: CoroutineContext = EmptyCoroutineContext, 128 | block: TestObserverScenario.() -> Unit, 129 | ) { 130 | TestObserverScenario() 131 | .apply(block) 132 | .execute(context) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /library/simple-recyclerview/README.md: -------------------------------------------------------------------------------- 1 | # Simple RecyclerView Library 2 | 3 | We all know how boring it is to create a simple list of items on the screen. We usually need to setup a lot of boilerplate code to: 4 | * Create the Adapter 5 | * Create the Adapter View Holder 6 | * Bind the View Holder with our view 7 | * Etc. 8 | 9 | Those few steps usually requires us to create a few classes with many lines of code. 10 | 11 | That's where the Simple RecyclerView Library comes for. 12 | It tries to remove all of that required boilerplate and allows you to focus on the only important piece, which is Bind your Data and your View. 13 | 14 | Below here you can see a simple example of its usage and then a more complex example where you need to handle animations or other customizations. 15 | The usage is self explanatory, but feel free to check all available functions. 16 | 17 | ### Files created to make it work 18 | 19 | Activity/Fragment XML with your recycler view. (simple_fragment.xml) 20 | ```xml 21 | 25 | 26 | 32 | 33 | 34 | ``` 35 | 36 | Item XML defining the list item layout. (item_list_row.xml) 37 | ```xml 38 | 43 | 44 | 56 | 57 | 58 | ``` 59 | 60 | Then the code to create your recycler view and render your data on it. 61 | ```kotlin 62 | 63 | /** 64 | * The default implementation is always a vertical list. 65 | */ 66 | private fun setupSimpleRecyclerView(items: List) { 67 | binding.countryList.prepareRecyclerView( 68 | items = items, 69 | bindView = { country, binding -> 70 | binding.country.text = country 71 | }, 72 | viewHolderFactory = { parent, inflater -> 73 | ItemListRowBinding.inflate(inflater, parent, false) 74 | }, 75 | ) 76 | } 77 | 78 | /** 79 | * Customizing recycler view, just add the function setupRecyclerView and make any change you need 80 | * You can change the LinearLayoutManager to make it horizontal or have a grid instead. 81 | * 82 | * Basically setupRecyclerView is used to setup any customization you need on your list. 83 | */ 84 | private fun setupComplexRecyclerView(items: List) { 85 | binding.countryList.prepareRecyclerView( 86 | items = items, 87 | setupRecyclerView = { recyclerView -> 88 | recyclerView.setHasFixedSize(true) 89 | recyclerView.itemAnimator = DefaultItemAnimator() 90 | recyclerView.layoutManager = LinearLayoutManager(context).apply { 91 | orientation = RecyclerView.HORIZONTAL 92 | } 93 | }, 94 | bindView = { country, binding -> 95 | binding.country.text = country 96 | }, 97 | viewHolderFactory = { parent, inflater -> 98 | ItemListRowBinding.inflate(inflater, parent, false) 99 | }, 100 | diffCallbackFactory = { 101 | object : DiffUtil.ItemCallback() { 102 | override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { 103 | return oldItem == newItem 104 | } 105 | 106 | override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { 107 | return oldItem == newItem 108 | } 109 | 110 | } 111 | } 112 | ) 113 | } 114 | ``` -------------------------------------------------------------------------------- /library/core-extentions/src/main/java/io/github/mayconcardoso/mvvm/core/ktx/FragmentExtention.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core.ktx 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.lifecycle.DefaultLifecycleObserver 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleOwner 9 | import androidx.lifecycle.LiveData 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.viewbinding.ViewBinding 12 | import io.github.mayconcardoso.mvvm.core.BaseViewModel 13 | import io.github.mayconcardoso.mvvm.core.ViewCommand 14 | import kotlinx.coroutines.flow.StateFlow 15 | import kotlinx.coroutines.flow.collectLatest 16 | import kotlinx.coroutines.flow.stateIn 17 | import kotlinx.coroutines.launch 18 | import kotlin.properties.ReadOnlyProperty 19 | import kotlin.reflect.KProperty 20 | 21 | 22 | /** 23 | * Called to observe any state flow from your view model. 24 | */ 25 | inline fun Fragment.bindState( 26 | observable: StateFlow, 27 | crossinline block: (result: T) -> Unit, 28 | ) = viewLifecycleOwner.lifecycleScope.launch { 29 | observable 30 | .stateIn(viewLifecycleOwner.lifecycleScope) 31 | .collectLatest { 32 | block(it) 33 | } 34 | } 35 | 36 | /** 37 | * Called to observe any live data from your view model. 38 | */ 39 | inline fun Fragment.bindState( 40 | observable: LiveData, 41 | crossinline block: (result: T) -> Unit, 42 | ) = viewLifecycleOwner.lifecycleScope.launch { 43 | observable.observe(this@bindState) { 44 | block(it) 45 | } 46 | } 47 | 48 | /** 49 | * Called to observe all commands sent from your view model. 50 | */ 51 | fun Fragment.bindCommand( 52 | viewModel: BaseViewModel, 53 | block: (result: ViewCommand) -> Unit, 54 | ) { 55 | commandObserver( 56 | lifecycle = viewLifecycleOwner, 57 | viewModel = viewModel, 58 | block = block, 59 | ) 60 | } 61 | 62 | /** 63 | * Called to observe only the first command sent from your view mode. 64 | */ 65 | fun Fragment.bindAutoDisposableCommand( 66 | viewModel: BaseViewModel, 67 | block: (result: ViewCommand) -> Unit, 68 | ) { 69 | autoDisposeCommandObserver( 70 | lifecycle = viewLifecycleOwner, 71 | viewModel = viewModel, 72 | block = block, 73 | ) 74 | } 75 | 76 | /** 77 | * Delegate to bind the view of a fragment. It can be used from [Fragment.onViewCreated] to 78 | * [Fragment.onDestroyView] (inclusive). 79 | * 80 | * Using this delegate **requires** using the ContentView constructor of [Fragment] passing the contentLayoutId, 81 | * or calling [Fragment.onCreateView] to inflate the layout. 82 | * 83 | * Sample usage: 84 | * ``` 85 | * class SampleFragment : Fragment(R.layout.fragment_sample) { 86 | * private val binding by viewBinding(FragmentSampleBinding::bind) 87 | 88 | * override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 89 | * super.onViewCreated(view, savedInstanceState) 90 | * // binding is ready to use 91 | * } 92 | * } 93 | * ``` 94 | */ 95 | fun Fragment.viewBinding( 96 | factory: (View) -> T, 97 | ): ReadOnlyProperty = object : ReadOnlyProperty { 98 | 99 | private var binding: T? = null 100 | private val lifecycleObserver = object : DefaultLifecycleObserver { 101 | 102 | override fun onDestroy(owner: LifecycleOwner) { 103 | owner.lifecycle.removeObserver(this) 104 | binding = null 105 | } 106 | } 107 | 108 | override fun getValue( 109 | thisRef: Fragment, 110 | property: KProperty<*>, 111 | ): T = binding ?: factory(requireView()).also { 112 | // Only schedule clearing and keep a reference if not called from Fragment.onDestroyView() 113 | if (viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { 114 | viewLifecycleOwner.lifecycle.addObserver(lifecycleObserver) 115 | binding = it 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Handy function to perform setup operations during a lifecycle method of the [LifecycleOwner] that won't 122 | * prevent that lifecycle method from finishing as the given [body] is launched as a coroutine in the [lifecycleScope]. 123 | */ 124 | inline fun LifecycleOwner.avoidFrozenFrames(crossinline body: suspend () -> Unit) { 125 | lifecycleScope.launch { body() } 126 | } 127 | 128 | inline fun Fragment.attachToParentOrContextOptional(context: Context): T? { 129 | return when { 130 | parentFragment is T -> parentFragment as T 131 | context is T -> context 132 | else -> null 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mctech/architecture/mvvm/data/ImageMockedDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.architecture.mvvm.data 2 | 3 | import com.mctech.architecture.mvvm.domain.entities.Image 4 | import com.mctech.architecture.mvvm.domain.entities.ImageDetails 5 | import kotlinx.coroutines.delay 6 | import java.text.SimpleDateFormat 7 | import java.util.* 8 | 9 | class ImageMockedDataSource : ImageDataSource { 10 | 11 | override suspend fun getAllImages(): List { 12 | delay(3000) 13 | 14 | val mockedImages = mutableListOf() 15 | for (id in 1..100) { 16 | mockedImages.add( 17 | Image( 18 | id = id.toLong(), 19 | title = "Lorem ipsum: $id", 20 | date = SimpleDateFormat( 21 | "dd/MM/yyyy", 22 | Locale.getDefault() 23 | ).format(Calendar.getInstance().time), 24 | thumbnailUrlSource = "https://i.gzn.jp/img/2019/08/23/android-10/00.png" 25 | ) 26 | ) 27 | } 28 | return mockedImages 29 | } 30 | 31 | override suspend fun getImageDetails(image: Image): ImageDetails { 32 | delay(3000) 33 | 34 | return ImageDetails( 35 | image = image, 36 | description = mockedDescription, 37 | heightSize = 1024, 38 | widthSize = 520, 39 | bigImageUrlSource = "https://i.gzn.jp/img/2019/08/23/android-10/00.png" 40 | ) 41 | } 42 | 43 | private val mockedDescription = 44 | "Lorem ipsum dolor sit amet consectetur adipiscing elit habitasse conubia, auctor quis quisque fusce enim montes aptent gravida, faucibus porta augue himenaeos cursus elementum eget suspendisse. Ad montes eleifend magnis nullam eget iaculis pharetra porttitor sit lectus primis pretium urna adipiscing, etiam tincidunt aptent ipsum luctus vestibulum elit nam ante arcu penatibus vulputate. Ligula litora neque orci tincidunt metus ad habitasse quis suspendisse ullamcorper malesuada facilisis ultrices, mattis amet habitant venenatis purus porttitor sollicitudin dictum ante scelerisque netus efficitur. Sodales iaculis at feugiat erat vitae facilisi nibh mi, nam orci lorem pharetra nisi a conubia. Nostra ex ullamcorper praesent rhoncus feugiat sodales suscipit lacinia, laoreet porttitor gravida venenatis lacus dignissim volutpat litora taciti, arcu viverra maecenas mattis ante parturient faucibus. Nisl semper egestas faucibus facilisi neque suscipit, felis convallis purus himenaeos venenatis, dui porttitor quam amet ipsum. Aptent taciti morbi phasellus volutpat ultrices convallis cubilia, mi tortor sodales risus iaculis porta aenean, ridiculus dictumst commodo ante et pharetra. Adipiscing dictum massa quam nullam elementum integer ad phasellus curae vehicula molestie volutpat nibh commodo, potenti eu inceptos feugiat himenaeos torquent fames condimentum justo dolor dictumst pharetra primis. Venenatis interdum finibus nulla vulputate arcu accumsan viverra class auctor placerat est, ante adipiscing felis tempus eget ipsum molestie ultricies cubilia elit pellentesque consequat, aliquet erat potenti massa phasellus mollis neque fringilla velit sodales.\n" + 45 | "\n" + 46 | "Etiam cursus litora vivamus diam pharetra pretium aptent accumsan, metus id faucibus ornare eleifend lacus potenti quisque, pellentesque vehicula class facilisis ultricies viverra proin. Curae mi volutpat sapien odio lectus, sodales eget himenaeos iaculis parturient hendrerit, cursus lacus cras sollicitudin. Torquent elit sollicitudin ipsum vivamus quis aptent egestas blandit, augue pellentesque sodales eu placerat phasellus dignissim, eleifend vehicula ligula mauris habitasse tempus varius. Nunc mollis integer sapien congue class quam augue laoreet dictumst vel, nascetur lacus lorem non ut phasellus commodo praesent felis, nam accumsan ornare convallis mauris neque ligula blandit suspendisse. Eu vel donec rutrum accumsan ullamcorper duis semper nec curae, orci lacus quam class senectus litora egestas phasellus, praesent pretium per dapibus diam erat ipsum eros. Commodo blandit hendrerit velit phasellus inceptos potenti cubilia, ipsum tellus elementum in purus maecenas, a iaculis integer nec erat habitasse. Pulvinar lobortis enim lacus ornare vivamus tortor quisque conubia faucibus justo, cursus ultricies id nisl phasellus ut mattis sodales tristique praesent pellentesque, urna mauris dolor ante placerat sapien suspendisse pretium iaculis. Fermentum per neque amet dictumst venenatis, magnis felis dis malesuada morbi leo, efficitur arcu ligula diam. Dictum justo lobortis ipsum dis blandit porttitor gravida phasellus, ridiculus semper in maecenas egestas parturient montes lectus, mauris penatibus integer class nam congue nascetur. Efficitur sapien platea eu ornare cras ultricies, rutrum nam vitae ligula conubia dignissim tortor, urna dui donec iaculis lobortis." 47 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /library/core/src/main/java/io/github/mayconcardoso/mvvm/core/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.mayconcardoso.mvvm.core 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.withContext 11 | import java.util.* 12 | import kotlin.reflect.KClass 13 | import kotlin.reflect.full.callSuspend 14 | import kotlin.reflect.jvm.isAccessible 15 | 16 | /** 17 | * I do not like 'Base' classes. But in this case, it helps a lot. 18 | * This class is basically a simple ViewModel, but it also keep the whole user flow interactions with your screen. 19 | */ 20 | abstract class BaseViewModel : ViewModel() { 21 | 22 | // region Variables 23 | 24 | /** 25 | * Control the initialization flow avoiding recreate the same flow twice. 26 | */ 27 | private var isInitialized = false 28 | 29 | /** 30 | * It is gonna keep the whole user flow on your view. 31 | * So let's say some error happen, you could print the whole stack trace and send it to your backend error log. 32 | * It will help you to trace your issues on your code and make the testing process easier. 33 | * Records the last 30 [UserInteraction], this value may change as we get more insight about it. 34 | */ 35 | @VisibleForTesting 36 | val userFlowInteraction = Stack() 37 | 38 | /** 39 | * It keeps the consumers for each user interaction on the flow. 40 | */ 41 | private var mappedUserInteractions = 42 | hashMapOf, suspend (UserInteraction) -> Unit>() 43 | 44 | /** 45 | * This is a simple observable that your view will be observing to handle commands. 46 | * A command is an action that ViewModel will send only once for the attached view, suck as: 47 | * - Open this dialog. 48 | * - Navigate to this screen. 49 | * - Navigate back from this screen. 50 | */ 51 | private val _commandObservable = SingleLiveEvent() 52 | val commandObservable: LiveData get() = _commandObservable 53 | 54 | // endregion 55 | 56 | // region Initialization 57 | 58 | /** 59 | * Initialize mapped interactions 60 | */ 61 | init { 62 | mapInteractionObservers() 63 | } 64 | 65 | /** 66 | * Call this function on `onCreate` of your Activity or Fragment. 67 | */ 68 | fun initialize() { 69 | if (isInitialized) { 70 | return 71 | } 72 | 73 | viewModelScope.launch { 74 | initializeComponents() 75 | } 76 | 77 | isInitialized = true 78 | } 79 | 80 | /** 81 | * Override this function to initialize your component flow. 82 | * It will be called only once when the view model is created. 83 | */ 84 | protected open suspend fun initializeComponents() = Unit 85 | 86 | /** 87 | * This is experimental. You should not use this code on production code. 88 | */ 89 | private fun mapInteractionObservers() { 90 | this::class.members.forEach { member -> 91 | member.annotations.forEach { annotation -> 92 | if (annotation is OnInteraction) { 93 | 94 | member.isAccessible = true 95 | 96 | mappedUserInteractions[annotation.target] = { 97 | if (member.parameters.size <= 1) { 98 | member.callSuspend(this) 99 | } else { 100 | member.callSuspend(this, it) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | // endregion 109 | 110 | // region Lifecycle 111 | 112 | /** 113 | * Used to clean all cache when view model is destroyed. 114 | */ 115 | override fun onCleared() { 116 | userFlowInteraction.clear() 117 | super.onCleared() 118 | } 119 | 120 | // endregion 121 | 122 | // region Interactions 123 | 124 | /** 125 | * Called by view to send 'an interaction' to the view model by using the view model scope. 126 | */ 127 | fun interact(userInteraction: UserInteraction, delay: Long = 0L) { 128 | viewModelScope.launch { 129 | delay(delay) 130 | userFlowInteraction.add(userInteraction) 131 | internalInteractionHandler(userInteraction) 132 | } 133 | } 134 | 135 | /** 136 | * It is the function that is called every single interaction the screen send. 137 | * So you can basically override it and handle the specific interaction by using a 'when' flow for example. 138 | */ 139 | private suspend fun internalInteractionHandler(interaction: UserInteraction) { 140 | // It is a mapped function 141 | if (mappedUserInteractions.containsKey(interaction::class)) { 142 | mappedUserInteractions[interaction::class]?.invoke(interaction) 143 | return 144 | } 145 | 146 | // Handle by using legacy way 147 | handleUserInteraction(interaction) 148 | } 149 | 150 | /** 151 | * It is the function that is called every single interaction the screen send. 152 | * So you can basically override it and handle the specific interaction by using a 'when' flow for example. 153 | */ 154 | protected open suspend fun handleUserInteraction(interaction: UserInteraction) = Unit 155 | 156 | /** 157 | * Let's say the user has filled in your login form. When the 'Sign in' button is pressed 158 | * your view will send a 'TryLoginInteraction(user, password)' to your view model handle it. 159 | * 160 | * But some error happen and your view will receive a error state. But you wanna try to sign in again. 161 | * So, instead of create another interaction and send to the view model. You could just call this method 162 | * and it will make sure to call the last interaction you've tried to send. 163 | */ 164 | fun reprocessLastInteraction() { 165 | viewModelScope.launch { 166 | handleUserInteraction(userFlowInteraction.last()) 167 | } 168 | } 169 | 170 | /** 171 | * If you need to check if there is a specific interaction on your flow. 172 | */ 173 | fun hasInteractionOnFlow(item: T) = hasMoreInteractionOnFlowThen(item, 0) 174 | 175 | /** 176 | * If you need to check if there is a specific interaction on your flow. 177 | */ 178 | fun hasMoreInteractionOnFlowThen(item: T, count: Int): Boolean { 179 | return userFlowInteraction.count { 180 | it.javaClass == item.javaClass 181 | } > count 182 | } 183 | 184 | // endregion 185 | 186 | /** 187 | * Used to send a command to your view. 188 | */ 189 | protected open suspend fun sendCommand(viewCommand: ViewCommand) = withContext(Dispatchers.Main) { 190 | _commandObservable.value = viewCommand 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /library/core/README.md: -------------------------------------------------------------------------------- 1 | # Core Library 2 | 3 | In this module we are going to see the core MVVM architecture. There are only [a few classes](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/tree/master/library/core/src/main/java/com/mctech/architecture/mvvm/core) here, but they are very important 4 | 5 | ### [ComponentState](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/blob/master/library/core/src/main/java/com/mctech/architecture/mvvm/x/core/ComponentState.kt) 6 | 7 | This is the coolest one to me. We know how difficult is to handle many different components on our screen, don't we? Thinking about that, I have been using this simple sealed class that defines all states my component can be. 8 | So now, I do not need to care about the lifecycle of my component ever since I use this class attached to your view lifecycle. 9 | 10 | ### [UserInteraction](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/blob/master/library/core/src/main/java/com/mctech/architecture/mvvm/x/core/UserInteraction.kt) 11 | 12 | This represents every single event the user performs on your screen/component. Those events will be stored in a Stack on your BaseViewModel to help you track the user flow and make it possible to test your code. 13 | 14 | ### [ViewCommand](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/blob/master/library/core/src/main/java/com/mctech/architecture/mvvm/x/core/ViewCommand.kt) 15 | 16 | It is basically the same as UserInteraction but it is called inside your ViewModel to send same event that will be consumed only once by your view. 17 | 18 | ### [BaseViewModel](https://github.com/MayconCardoso/Mvvm-Architecture-Toolkit/blob/master/library/core/src/main/java/com/mctech/architecture/mvvm/x/core/BaseViewModel.kt) 19 | 20 | Last but not least important, the BaseViewModel. I do not like base classes overall, to be honest. But I use this BaseViewModel class a lot. Because it has only a few methods that help us to follow the architecture patters. Please take a look at this class to understand it better. 21 | 22 | ## Simple example: 23 | 24 | On my ViewModel class, I have a LiveData of my generic ComponentState. 25 | ```kotlin 26 | class ImageViewModel() : BaseViewModel(){ 27 | 28 | private val _state by lazy { 29 | MutableStateFlow>>(ComponentState.Loading.FromEmpty) 30 | } 31 | val state: StateFlow>> by lazy { _state } 32 | 33 | private val _detailState by lazy { 34 | MutableStateFlow>(ComponentState.Loading.FromEmpty) 35 | } 36 | val detailState: StateFlow> by lazy { _detailState } 37 | 38 | } 39 | ``` 40 | 41 | I can change the state of my component any time inside a coroutine flow on my ViewModel, for example. 42 | ```kotlin 43 | class ImageViewModel() : BaseViewModel(){ 44 | 45 | // ... rest of your code 46 | 47 | private suspend fun loadImagesInteraction() { 48 | when (val listResult = loadImageListCase.execute()) { 49 | // Set the list component with 'Success' state. 50 | is InteractionResult.Success -> { 51 | _state.value = ComponentState.Success(listResult.result) 52 | } 53 | 54 | // Set the list component with 'Error' state. 55 | is InteractionResult.Error -> { 56 | _state.value = ComponentState.Error(listResult.error) 57 | } 58 | } 59 | } 60 | 61 | // ... rest of your code 62 | 63 | } 64 | ``` 65 | 66 | I can also send commands to my view and make it navigate to another screen, for example 67 | ```kotlin 68 | class ImageViewModel() : BaseViewModel(){ 69 | 70 | // ... rest of your code 71 | 72 | @OnInteraction(ImageInteraction.OpenDetails::class) 73 | private suspend fun openImageDetailsInteraction(interaction: ImageInteraction.OpenDetails) { 74 | // Set the details component with 'loading' state. 75 | _detailState.value = ComponentState.Loading.FromEmpty 76 | 77 | // Open the details screen. 78 | sendCommand(ImageCommands.OpenImageDetails) 79 | 80 | // Load image's details. 81 | when (val detailsResult = loadImageDetailsCase.execute(interaction.image)) { 82 | // Set the details component with 'Success' state. 83 | is InteractionResult.Success -> { 84 | _detailState.value = ComponentState.Success(detailsResult.result) 85 | } 86 | 87 | // Set the details component with 'Error' state. 88 | is InteractionResult.Error -> { 89 | _detailState.value = ComponentState.Error(detailsResult.error) 90 | } 91 | } 92 | } 93 | 94 | // ... rest of your code 95 | 96 | } 97 | ``` 98 | 99 | On my screen, I just need to observe this state to handle my component state 100 | 101 | ```kotlin 102 | @AndroidEntryPoint 103 | class ImageListFragment : Fragment(R.layout.fragment_list_of_images) { 104 | 105 | private val viewModel by viewModels() 106 | private val binding by viewBinding(FragmentListOfImagesBinding::bind) 107 | 108 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 109 | // Start view model flow 110 | viewModel.initialize() 111 | 112 | // Observes image component state. 113 | bindState(viewModel.state, ::consumeComponentState) 114 | } 115 | 116 | private fun consumeComponentState(state: ComponentState>) = when (state) { 117 | is ComponentState.Error -> renderErrorState() 118 | is ComponentState.Loading -> renderLoadingState() 119 | is ComponentState.Success -> renderSuccessState(state.result) 120 | } 121 | 122 | private fun renderLoadingState() { 123 | binding.recyclerList.isVisible = false 124 | binding.progressState.isVisible = true 125 | binding.errorComponent.isVisible = false 126 | } 127 | 128 | private fun renderErrorState() { 129 | binding.recyclerList.isVisible = false 130 | binding.progressState.isVisible = false 131 | binding.errorComponent.isVisible = true 132 | } 133 | 134 | private fun renderSuccessState(images: List) { 135 | binding.recyclerList.isVisible = true 136 | binding.progressState.isVisible = false 137 | binding.errorComponent.isVisible = false 138 | binding.recyclerList.prepareRecyclerView( 139 | items = images, 140 | bindView = this::renderImageItem, 141 | viewHolderFactory = this::createViewHolder, 142 | diffCallbackFactory = this::createImageDiffAlgorithm, 143 | ) 144 | } 145 | 146 | private fun renderImageItem(item: Image, binding: ItemImageBinding) { 147 | binding.tvTitle.text = item.title 148 | binding.tvDate.text = item.date 149 | 150 | binding.root.setOnClickListener { 151 | viewModel.interact(ImageInteraction.OpenDetails(item)) 152 | } 153 | } 154 | 155 | // ... rest of your code 156 | } 157 | ``` 158 | 159 | Every interaction that the user is sending to the ViewModel is handled here 160 | ```kotlin 161 | class ImageViewModel() : BaseViewModel(){ 162 | 163 | // ... rest of your code 164 | 165 | override suspend fun handleUserInteraction(interaction: UserInteraction) { 166 | when(interaction){ 167 | is ImageInteraction.OpenDetails -> openImageDetailsInteraction(interaction.image) 168 | } 169 | } 170 | 171 | // ... rest of your code 172 | 173 | } 174 | ``` 175 | 176 | Here are my 'ViewCommand' and 'UserInteraction' classes used on this example: 177 | 178 | ```kotlin 179 | sealed class ImageCommands : ViewCommand { 180 | object OpenImageDetails : ImageCommands() 181 | } 182 | 183 | sealed class ImageInteraction : UserInteraction { 184 | data class OpenDetails(val image: Image) : ImageInteraction() 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------