├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── kotlin │ │ └── com │ │ └── globant │ │ └── myapplication │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── globant │ │ │ ├── SampleApplication.kt │ │ │ ├── activities │ │ │ └── MainActivity.kt │ │ │ ├── di │ │ │ └── KoinModules.kt │ │ │ ├── utils │ │ │ ├── Constants.kt │ │ │ └── Data.kt │ │ │ └── viewmodels │ │ │ ├── CharacterViewModel.kt │ │ │ └── base │ │ │ └── BaseViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── 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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── globant │ └── myapplication │ ├── CharacterViewModelTest.kt │ ├── ExampleUnitTest.kt │ └── util │ └── LiveDataTestExtensions.kt ├── build.gradle ├── data ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── kotlin │ │ └── com │ │ └── globant │ │ └── data │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── globant │ │ │ └── data │ │ │ ├── MarvelRequestGenerator.kt │ │ │ ├── Utils.kt │ │ │ ├── database │ │ │ ├── CharacterDatabase.kt │ │ │ ├── entity │ │ │ │ └── MarvelCharacterRealm.kt │ │ │ └── response │ │ │ │ └── DataBaseResponse.kt │ │ │ ├── mapper │ │ │ ├── BaseMapperRepository.kt │ │ │ ├── CharacterMapperLocal.kt │ │ │ └── CharacterMapperService.kt │ │ │ ├── repositories │ │ │ └── MarvelCharacterRepositoryImpl.kt │ │ │ └── service │ │ │ ├── CharacterService.kt │ │ │ ├── api │ │ │ └── MarvelApi.kt │ │ │ └── response │ │ │ ├── CharacterResponse.kt │ │ │ └── MarvelBaseResponse.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── globant │ └── data │ └── ExampleUnitTest.java ├── di ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── globant │ │ └── di │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── globant │ │ │ └── KoinModules.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── globant │ └── di │ └── ExampleUnitTest.java ├── domain ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── globant │ └── domain │ ├── entities │ └── MarvelCharacter.kt │ ├── repositories │ └── MarvelCharacterRepository.kt │ ├── usecases │ └── GetCharacterByIdUseCase.kt │ └── utils │ └── Result.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | /captures 2 | .externalNativeBuild 3 | 4 | gradle.properties 5 | 6 | # Built application files 7 | *.apk 8 | *.ap_ 9 | 10 | # Files for the Dalvik VM 11 | *.dex 12 | 13 | # Java class files 14 | *.class 15 | 16 | # Make files 17 | *.mk 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | out/ 23 | 24 | # Gradle files 25 | .gradle/ 26 | gradlew.bat 27 | gradlew 28 | build/ 29 | gradle.properties 30 | 31 | # Local configuration file (sdk path, etc) 32 | local.properties 33 | 34 | # Proguard folder generated by Eclipse 35 | proguard/ 36 | 37 | # Log Files 38 | *.log 39 | 40 | # Android Studio Navigation editor temp files 41 | .navigation/ 42 | 43 | # Android Studio captures folder 44 | captures/ 45 | 46 | # Android Studio & IntelliJ 47 | *.iws 48 | .idea/ 49 | .idea/tasks.xml 50 | .idea/workspace.xml 51 | .idea/libraries 52 | *.iml 53 | 54 | # Keystore files 55 | *.jks 56 | 57 | # OS 58 | .DS_Store 59 | .navigation/ 60 | 61 | ### Android Patch ### 62 | gen-external-apklibs 63 | 64 | # Thumbnails 65 | ._* 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin + MVVM + Clean Architecture + Coroutines + Koin 2 | 3 | This project was made with the objective of creating a base structure for new apps, using tools and components supported by Google and by most of the Android development community. 4 | 5 | 6 | ## Clean Architecture 7 | 8 | Clean architecture promotes separation of concerns, making the code loosely coupled. This results in a more testable and flexible code. This approach divides the project in 3 modules: presentation, data and domain. 9 | 10 | * __Presentation__: Layer with the Android Framework, the MVVM pattern and the DI module. Depends on domain to access the use cases and on di, to inject dependencies. 11 | * __Domain__: Layer with the business logic. Contains the use cases, in charge of calling the correct repository or data member. 12 | * __Data__: Layer with the responsibility of selecting the proper data source for the domain layer. It contains the implementations of the repositories declared in the domain layer. It may, for example, check if the data in a database is up to date, and retrieve it from a service if it’s not. 13 | 14 | As there isn’t a single way to implement Clean Architecture, this could affront changes in the future. 15 | 16 | ### Domain 17 | 18 | Business logic can be defined as the core operations done by the application. The domain tries to encapsulate this business logic, to make it agnostic of its context. The components of the domain are: 19 | 20 | * __Entities__: Simple classes that represent the objects in which the business is based. 21 | * __Repositories__: Interfaces used by the use cases. Implemented in the data layer. 22 | * __Use cases__: Also called interactors. They enclose a single action, like getting data from a database or posting to a service. They use the repositories to resolve the action they are supposed to do. They usually override the operator “invoke”, so they can be called as a function. 23 | 24 | ### Data 25 | 26 | The data layer is the implementation of all the repositories declared by the domain layer. This acts as a support of the business layer, from where it obtains the data needed to be shown in the UI. 27 | 28 | Data is also an Android module so, besides databases and network requests, it can provide locations, bluetooth access, gyroscope data, among other information respective to the device. This could be separated in another module to provide independency from the framework. 29 | 30 | It’s the repository job to know what should be the source of the data. The repository should decide whether the data in the local database is good enough or if it should pull it from a service. The repository shouldn’t be tied to an implementation of database/services. It should have references to interfaces that access the actual framework. A boolean may be passed as a parameter to the repository to force an update from a specific source. 31 | 32 | ### Presentation 33 | 34 | Presentation layer contains every component involved in showing information to the user. The main part of this layer are the Views and ViewModels that will be explained in the next section. In general, the presentation layer is the one using all the Use Cases/Interactors that we created in the domain layer. 35 | 36 | Views in this layer are the fragments and activities designed to show information to the user. In MVVM, these views are separated from the logic, which is encapsulated in the ViewModel. 37 | 38 | ## MVVM 39 | 40 | The Model View ViewModel pattern helps with the separation of concerns, dividing the user interface with the logic behind. The decision to use this pattern is mainly based on the support Google has been giving to it. Not only they have created a ViewModel class to use as a parent to the viewmodels, there is also a huge use of the pattern in official Android presentations and samples. Moreover, MVVM is vastly used in today’s Android development, and combines very well with Android Architecture Components like LiveData and DataBindings. 41 | 42 | ### Model 43 | 44 | As we are implementing MVVM alongside with Clean Architecture, we decided not to have a model class per se. The ViewModel interacts directly with the domain, utilizing the use cases. 45 | 46 | ### View Model 47 | 48 | The orchestrator of the relationship between the data and the user interface of the application. The ViewModel has the logic to convert what the use cases provide into information that the view can understand and present. Furthermore, it has the logic to react to the user’s input, and call the pertinent use cases. 49 | 50 | The most useful part of the Android’s ViewModel class is its lifecycle consciousness. It only communicates to the View with LiveData components, so it’s totally agnostic of contexts and activities: it can keep the information alive even against configuration changes like screen rotations or calls to background. 51 | 52 | ### View 53 | 54 | The view in our implementation of MVVM is actually a Fragment or an Activity. The views enclose everything needed to handle the user interface. They observe the ViewModel, using LiveData components, and react to its changes as they need to. 55 | 56 | ### LiveData Architecture Component 57 | 58 | The view uses LiveData to observe changes in the ViewModel. This has several advantages: 59 | 60 | * The UI matches the data state, and this keeps data up to date. 61 | * Not having to worry about stopped activities and memory leaks. Live data objects are subscript to a lifecycle and automatically stop observing when that lifecycle is ended. 62 | * Handles configuration changes properly. 63 | * The same data could be shared between activities. 64 | 65 | ## Dependency Injection with Koin 66 | 67 | Dependency injection is closely related to two SOLID concepts: dependency inversion, which states that high level modules should not depend on low level modules, both should depend on abstractions; and single responsibility principle, which states that every class or module is responsible for just a single piece of functionality. 68 | DI supports these goals by decoupling the creation and the usage of an object. It allows you to replace dependencies without changing the class that uses them and also reduces the risk of modifying a class because one of its dependencies changed. 69 | This sample app uses Koin as the dependency injection library. 70 | 71 | Koin is one of the most popular dependency injection frameworks for Android. It’s written purely in Kotlin, it’s lightweight and it’s easier to learn than its competition. It works in three simple steps: 72 | 73 | 1. __Declare a module__: Defines those entities which will be injected at some point in the app. 74 | 75 | ``` 76 | val applicationModule = module { 77 | single { AppRepository } 78 | } 79 | ``` 80 | 81 | 2. __Start Koin__: A single line, startKoin(this, listOf(applicationModule)), allows you to launch the DI process and indicate which modules will be available when needed, in this case, only applicationModule. 82 | 83 | ``` 84 | class BaseApplication : Application() { 85 | override fun onCreate() { 86 | super.onCreate() 87 | startKoin(this, listOf(applicationModule)) 88 | } 89 | } 90 | ``` 91 | 92 | 3. __Perform an injection__: 93 | In consonance with Kotlin features, Koin allows to perform lazy injections in a very convenient way. 94 | 95 | ``` 96 | class FeatureActivity : AppCompatActivity() { 97 | private val appRepository: AppRepository by inject() 98 | ... 99 | } 100 | ``` 101 | 102 | ## Coroutines 103 | 104 | Coroutines are a new way of managing background threads that can simplify code by reducing the need for callbacks. They convert async callbacks for long-running tasks, such as database or network access, into sequential code. 105 | We use coroutines to do tasks in a background thread. This goes very well with the idea of use cases, single actions that the ViewModel calls depending of its needs. The guideline should be that every task executed by a use case should be done in a background thread, so, in the main thread, we could show a loading screen or any alternative, and the UI doesn’t get blocked. 106 | 107 | __Job__: a job is a cancellable task with a life-cycle that culminates in its completion. By default, a failure of any of the job’s children leads to an immediate failure of its parent and cancellation of the rest of its children. This behavior can be customized using SupervisorJob. 108 | 109 | __Dispatchers__: 110 | * Dispatchers.Default – is used by all standard builders by default. It uses a common pool of shared background threads. This is an appropriate choice for compute-intensive coroutines that consume CPU resources. 111 | * Dispatchers.IO – uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive blocking operations (like file I/O and blocking socket I/O). 112 | 113 | ## Other concepts 114 | 115 | ### AndroidX 116 | 117 | AndroidX is a redesigned library, replacing the support library, to make package names more clear. Each androidx package has it own version, detached from the Android API version, so extension libraries can be developed independently. 118 | 119 | Androidx also improves understanding of what is added to the app: 120 | 121 | * android.* -> bundled in the platform. 122 | * androidx.* -> extension library. 123 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'realm-android' 6 | 7 | buildscript { 8 | repositories { 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath "io.realm:realm-gradle-plugin:5.11.0" 13 | } 14 | } 15 | 16 | realm { 17 | syncEnabled = true; 18 | } 19 | android { 20 | compileSdkVersion 28 21 | defaultConfig { 22 | applicationId "com.globant.myapplication" 23 | minSdkVersion 17 24 | targetSdkVersion 28 25 | versionCode 1 26 | versionName "1.0" 27 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation project(path: ':domain') 40 | implementation project(path: ':di') 41 | 42 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 43 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' 44 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' 45 | implementation 'androidx.appcompat:appcompat:1.0.2' 46 | implementation 'androidx.core:core-ktx:1.0.2' 47 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 48 | implementation "org.koin:koin-androidx-viewmodel:2.0.0-GA" 49 | 50 | testImplementation 'junit:junit:4.12' 51 | androidTestImplementation 'androidx.test:runner:1.1.1' 52 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 53 | 54 | testImplementation "com.google.truth:truth:0.42" 55 | testImplementation 'org.mockito:mockito-core:2.27.0' 56 | testImplementation 'org.koin:koin-test:2.0.0-GA' 57 | 58 | def lifecycle_version = '2.0.0-beta01' 59 | testImplementation "androidx.arch.core:core-testing:$lifecycle_version" 60 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' 61 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' 62 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1' 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/globant/myapplication/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.globant.myapplication 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.globant.myapplication", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/globant/SampleApplication.kt: -------------------------------------------------------------------------------- 1 | package com.globant 2 | 3 | import android.app.Application 4 | import com.globant.di.useCasesModule 5 | import com.globant.di.viewModelsModule 6 | import io.realm.Realm 7 | import org.koin.core.context.startKoin 8 | 9 | class SampleApplication: Application() { 10 | override fun onCreate() { 11 | super.onCreate() 12 | Realm.init(this) 13 | 14 | startKoin { 15 | modules(listOf(repositoriesModule, viewModelsModule, useCasesModule)) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/globant/activities/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.globant.activities 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.Toast 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.globant.domain.entities.MarvelCharacter 8 | import com.globant.utils.Data 9 | import com.globant.utils.Status 10 | import com.globant.viewmodels.CharacterViewModel 11 | import com.globant.myapplication.R 12 | import com.globant.utils.MINUS_ONE 13 | import kotlinx.android.synthetic.main.activity_main.* 14 | import org.koin.androidx.viewmodel.ext.android.viewModel 15 | 16 | class MainActivity : AppCompatActivity() { 17 | 18 | private val viewModel by viewModel() 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | 24 | viewModel.mainState.observe(::getLifecycle, ::updateUI) 25 | 26 | buttonSearchRemote.setOnClickListener { onSearchRemoteClicked() } 27 | buttonSearchLocal.setOnClickListener { onSearchLocalClicked() } 28 | } 29 | 30 | private fun updateUI(characterData: Data) { 31 | when (characterData.responseType) { 32 | Status.ERROR -> { 33 | hideProgress() 34 | characterData.error?.message?.let { showMessage(it) } 35 | characterData.data?.let { setCharacter(it) } 36 | } 37 | Status.LOADING -> { 38 | showProgress() 39 | } 40 | Status.SUCCESSFUL -> { 41 | hideProgress() 42 | characterData.data?.let { setCharacter(it) } 43 | } 44 | } 45 | } 46 | 47 | private fun showProgress() { 48 | progress.visibility = View.VISIBLE 49 | textViewDetails.visibility = View.GONE 50 | } 51 | 52 | private fun hideProgress() { 53 | progress.visibility = View.GONE 54 | textViewDetails.visibility = View.VISIBLE 55 | } 56 | 57 | private fun setCharacter(character: MarvelCharacter) { 58 | textViewDetails.text = character.description 59 | } 60 | 61 | private fun showMessage(message: String) { 62 | Toast.makeText(this, message, Toast.LENGTH_LONG).show() 63 | } 64 | 65 | private fun onSearchRemoteClicked() { 66 | val id = if (characterID.text.toString().isNotEmpty()) { 67 | characterID.text.toString().toInt() 68 | } else { 69 | MINUS_ONE 70 | } 71 | viewModel.onSearchRemoteClicked(id) 72 | } 73 | private fun onSearchLocalClicked() { 74 | val id = if (characterID.text.toString().isNotEmpty()) { 75 | characterID.text.toString().toInt() 76 | } else { 77 | MINUS_ONE 78 | } 79 | viewModel.onSearchLocalClicked(id) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/globant/di/KoinModules.kt: -------------------------------------------------------------------------------- 1 | package com.globant.di 2 | 3 | import com.globant.domain.usecases.GetCharacterByIdUseCase 4 | import com.globant.viewmodels.CharacterViewModel 5 | import org.koin.androidx.viewmodel.dsl.viewModel 6 | import org.koin.dsl.module 7 | 8 | val viewModelsModule = module { 9 | viewModel { CharacterViewModel(get()) } 10 | } 11 | 12 | val useCasesModule = module { 13 | single { GetCharacterByIdUseCase() } 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/globant/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.globant.utils 2 | 3 | const val MINUS_ONE = -1 -------------------------------------------------------------------------------- /app/src/main/java/com/globant/utils/Data.kt: -------------------------------------------------------------------------------- 1 | package com.globant.utils 2 | 3 | /** 4 | * A generic wrapper class around data request 5 | */ 6 | data class Data(var responseType: Status, var data: RequestData? = null, var error: Exception? = null) 7 | 8 | enum class Status { SUCCESSFUL, ERROR, LOADING } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/globant/viewmodels/CharacterViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.globant.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.globant.domain.entities.MarvelCharacter 6 | import com.globant.domain.usecases.GetCharacterByIdUseCase 7 | import com.globant.domain.utils.Result 8 | import com.globant.utils.Data 9 | import com.globant.utils.Status 10 | import com.globant.viewmodels.base.BaseViewModel 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | 15 | class CharacterViewModel(val getCharacterById: GetCharacterByIdUseCase) : BaseViewModel() { 16 | 17 | private var mutableMainState: MutableLiveData> = MutableLiveData() 18 | val mainState: LiveData> 19 | get() { 20 | return mutableMainState 21 | } 22 | 23 | fun onSearchRemoteClicked(id: Int) = launch { 24 | mutableMainState.value = Data(responseType = Status.LOADING) 25 | when (val result = withContext(Dispatchers.IO) { getCharacterById(id, true) }) { 26 | is Result.Failure -> { 27 | mutableMainState.value = Data(responseType = Status.ERROR, error = result.exception) 28 | } 29 | is Result.Success -> { 30 | mutableMainState.value = Data(responseType = Status.SUCCESSFUL, data = result.data) 31 | } 32 | } 33 | } 34 | 35 | fun onSearchLocalClicked(id: Int) = launch { 36 | mutableMainState.value = Data(responseType = Status.LOADING) 37 | when (val result = withContext(Dispatchers.IO) { getCharacterById(id, false) }) { 38 | is Result.Failure -> { 39 | mutableMainState.value = Data(responseType = Status.ERROR, error = result.exception) 40 | } 41 | is Result.Success -> { 42 | mutableMainState.value = Data(responseType = Status.SUCCESSFUL, data = result.data) 43 | } 44 | } 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/globant/viewmodels/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.globant.viewmodels.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.cancel 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | open class BaseViewModel : ViewModel(), CoroutineScope { 11 | override val coroutineContext: CoroutineContext 12 | get() = Dispatchers.Main + SupervisorJob() 13 | 14 | override fun onCleared() { 15 | super.onCleared() 16 | coroutineContext.cancel() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | 25 | 26 | 36 | 37 | 40 | 41 |