├── .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 |
--------------------------------------------------------------------------------