├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── appfactorylogo.webp │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ └── colors.xml │ │ │ ├── drawable-v24 │ │ │ │ ├── splash_drawable.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── list_item_employee.xml │ │ │ │ ├── start_fragment.xml │ │ │ │ ├── search_fragment.xml │ │ │ │ ├── list_fragment.xml │ │ │ │ └── detail_fragment.xml │ │ │ └── navigation │ │ │ │ └── nav_graph.xml │ │ ├── java │ │ │ └── ladd │ │ │ │ └── marshall │ │ │ │ └── androidmvvmexample │ │ │ │ ├── utils │ │ │ │ ├── Constants.kt │ │ │ │ └── Extensions.kt │ │ │ │ ├── viewModel │ │ │ │ ├── viewModels │ │ │ │ │ ├── SearchViewModel.kt │ │ │ │ │ ├── ListViewModel.kt │ │ │ │ │ └── DetailViewModel.kt │ │ │ │ └── repositories │ │ │ │ │ └── EmployeeRepository.kt │ │ │ │ ├── api │ │ │ │ ├── RetroFitInstance.kt │ │ │ │ └── EmployeeEndpoints.kt │ │ │ │ ├── BaseApplication.kt │ │ │ │ ├── model │ │ │ │ ├── database │ │ │ │ │ ├── daos │ │ │ │ │ │ └── EmployeeDAO.kt │ │ │ │ │ └── ExampleRoomDB.kt │ │ │ │ └── models │ │ │ │ │ └── Employee.kt │ │ │ │ └── view │ │ │ │ ├── fragments │ │ │ │ ├── StartFragment.kt │ │ │ │ ├── DetailFragment.kt │ │ │ │ ├── SearchFragment.kt │ │ │ │ └── ListFragment.kt │ │ │ │ ├── activities │ │ │ │ ├── MainActivity.kt │ │ │ │ └── SplashActivity.kt │ │ │ │ └── adapters │ │ │ │ └── EmployeeRecyclerViewAdapter.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── ladd │ │ │ └── marshall │ │ │ └── androidmvvmexample │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── ladd │ │ └── marshall │ │ └── androidmvvmexample │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml └── runConfigurations.xml ├── gradle.properties ├── gradlew.bat ├── gradlew ├── .gitignore └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name='Android MVVM Example' 3 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/appfactorylogo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/drawable/appfactorylogo.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sortagreg/Android-MVVM-Example/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 09 19:38:15 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.utils 2 | 3 | const val EMPLOYEE_ID = "EMPLOYEE_ID" 4 | const val EMPLOYEE_NAME = "EMPLOYEE_NAME" 5 | 6 | const val BASE_URL = "http://dummy.restapiexample.com/" 7 | 8 | const val ROOM_DB_NAME = "ROOM_DB_NAME" 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Android MVVM Example 3 | Search for Employee 4 | View All Employees 5 | Employee ID 6 | Search 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/splash_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/viewModel/viewModels/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.viewModel.viewModels 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class SearchViewModel : ViewModel() { 6 | /** 7 | * TODO: Implement the ViewModel for a more advanced search functionality using LiveData and 8 | * switchMap transformations 9 | */ 10 | } 11 | -------------------------------------------------------------------------------- /app/src/test/java/ladd/marshall/androidmvvmexample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.utils 2 | 3 | import com.google.android.material.textfield.TextInputEditText 4 | 5 | /** 6 | * Extension functions allow us to add functions to Classes that we otherwise would not have access 7 | * to. More information can be found in Kotlin's official documentation. 8 | */ 9 | 10 | fun TextInputEditText.toInt(): Int { 11 | return when { 12 | this.text.toString().isBlank() -> 0 13 | else -> this.text.toString().toInt() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/api/RetroFitInstance.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.api 2 | 3 | import ladd.marshall.androidmvvmexample.utils.BASE_URL 4 | import retrofit2.Retrofit 5 | import retrofit2.converter.gson.GsonConverterFactory 6 | 7 | object RetroFitInstance { 8 | 9 | private var INSTANCE: Retrofit? = null 10 | 11 | fun getInstance(): Retrofit = INSTANCE ?: kotlin.run { 12 | Retrofit.Builder() 13 | .baseUrl(BASE_URL) 14 | .addConverterFactory(GsonConverterFactory.create()) 15 | .build() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample 2 | 3 | import android.app.Application 4 | import timber.log.Timber 5 | 6 | /** 7 | * Overridden Application class to initialize certain libraries at a higher level than just a 8 | * Fragment or Activity. 9 | * 10 | * In the Manifest, this class will be listed under the Application tag in the name field. 11 | */ 12 | class BaseApplication : Application() { 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | 17 | // Initialize Timber. More information can be found in the README. 18 | if (BuildConfig.DEBUG) { 19 | Timber.plant(Timber.DebugTree()) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /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/java/ladd/marshall/androidmvvmexample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("ladd.marshall.androidmvvmexample", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/model/database/daos/EmployeeDAO.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.model.database.daos 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.Dao 5 | import androidx.room.Delete 6 | import androidx.room.Insert 7 | import androidx.room.OnConflictStrategy 8 | import androidx.room.Query 9 | import ladd.marshall.androidmvvmexample.model.models.Employee 10 | 11 | @Dao 12 | interface EmployeeDAO { 13 | 14 | @Insert(onConflict = OnConflictStrategy.REPLACE) 15 | fun insertEmployee(employee: Employee) 16 | 17 | @Delete 18 | fun deleteEmployee(employee: Employee) 19 | 20 | @Query("SELECT * FROM employee_table") 21 | fun getAllEmployeesLiveData(): LiveData> 22 | 23 | @Query("SELECT * FROM employee_table WHERE id = :employeeId") 24 | fun getEmployeeByIdLiveData(employeeId: Int): LiveData 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/viewModel/viewModels/ListViewModel.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.viewModel.viewModels 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.liveData 7 | import kotlinx.coroutines.Dispatchers 8 | import ladd.marshall.androidmvvmexample.model.models.Employee 9 | import ladd.marshall.androidmvvmexample.viewModel.repositories.EmployeeRepository 10 | 11 | class ListViewModel(application: Application) : AndroidViewModel(application) { 12 | 13 | // coordinates between the local and remote databases 14 | private val employeeRepository = EmployeeRepository.getInstance(application) 15 | 16 | // This LiveData is created using a ktx library shortcut 17 | val employeeListLiveData: LiveData> = liveData(Dispatchers.IO) { 18 | emitSource(employeeRepository.getAllEmployeesLiveData()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_employee.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/viewModel/viewModels/DetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.viewModel.viewModels 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.liveData 7 | import kotlinx.coroutines.Dispatchers 8 | import ladd.marshall.androidmvvmexample.model.models.Employee 9 | import ladd.marshall.androidmvvmexample.viewModel.repositories.EmployeeRepository 10 | 11 | class DetailViewModel(application: Application) : AndroidViewModel(application) { 12 | 13 | // coordinates between the local and remote databases 14 | private val employeeRepository = EmployeeRepository.getInstance(application) 15 | 16 | // This LiveData is created using a ktx library shortcut 17 | fun employeeLiveData(id: Int): LiveData = liveData(Dispatchers.IO) { 18 | emitSource(employeeRepository.getEmployeeByIdLiveData(id)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/fragments/StartFragment.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.navigation.fragment.findNavController 7 | import kotlinx.android.synthetic.main.start_fragment.* 8 | import ladd.marshall.androidmvvmexample.R 9 | 10 | /** 11 | * This is the first Fragment that is loaded by the NavController. It implements a simple 12 | * navigation using the Navigation component. 13 | */ 14 | class StartFragment : Fragment(R.layout.start_fragment) { 15 | 16 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 17 | super.onViewCreated(view, savedInstanceState) 18 | buttonAllEmployees.setOnClickListener { findNavController().navigate(R.id.action_startFragment_to_listFragment) } 19 | buttonEmployeeSearch.setOnClickListener { findNavController().navigate(R.id.action_startFragment_to_searchFragment) } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/api/EmployeeEndpoints.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.api 2 | 3 | import ladd.marshall.androidmvvmexample.model.models.Employee 4 | import retrofit2.http.GET 5 | import retrofit2.http.Header 6 | import retrofit2.http.Path 7 | 8 | interface EmployeeEndpoints { 9 | 10 | // Simple get request to get a list of all employees 11 | @GET("api/v1/employees") 12 | suspend fun getEmployeeList(): List 13 | 14 | // A request to get an employee based on their employee id #. 15 | // In the URL, we wrap what could change in {} and give it a name. 16 | // Then, use @Path(variableName) in the arguments to the function. 17 | // The value to be placed in the URL can then be passed in 18 | // dynamically at runtime. 19 | @GET("api/v1/employee/{employeeId}") 20 | suspend fun getEmployeeById(@Path("employeeId") employeeId: Int): Employee 21 | 22 | // If your endpoint requires an API token, here is how the first call would look. 23 | // There are other, better ways to do this, such as by using an OkHttpClient. 24 | @GET("api/v1/employees") 25 | suspend fun getEmployeeListRequireToken(@Header("x-api-key") key: String): List 26 | } 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/activities/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.activities 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.navigation.NavController 6 | import androidx.navigation.findNavController 7 | import androidx.navigation.ui.NavigationUI 8 | import ladd.marshall.androidmvvmexample.R 9 | 10 | /** 11 | * The Activity that is loaded immediately after the splash screen. 12 | * 13 | * It is responsible for coordinating the Navigation Component. A link is provided in the README 14 | * to explain how this component works and is built. Please refer to that for an explanation of 15 | * how to implement this. 16 | */ 17 | class MainActivity : AppCompatActivity() { 18 | 19 | private lateinit var navController: NavController 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContentView(R.layout.activity_main) 24 | navController = findNavController(R.id.navHostFragment) 25 | NavigationUI.setupActionBarWithNavController(this, navController) 26 | } 27 | 28 | override fun onSupportNavigateUp(): Boolean { 29 | return navController.navigateUp() || super.onSupportNavigateUp() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #29B6F6 4 | #0288D1 5 | #1DE9B6 6 | #00BFA5 7 | 8 | 9 | 10 | 11 | 12 | 13 | #B3E5FC 14 | #ffffff 15 | #b00020 16 | 17 | 18 | 19 | #ffffff 20 | #ffffff 21 | #000000 22 | #000000 23 | #ffffff 24 | 25 | 26 | #ADDBDBDB 27 | #ffffff 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/model/database/ExampleRoomDB.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.model.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import ladd.marshall.androidmvvmexample.model.database.daos.EmployeeDAO 8 | import ladd.marshall.androidmvvmexample.model.models.Employee 9 | import ladd.marshall.androidmvvmexample.utils.ROOM_DB_NAME 10 | 11 | /** 12 | * Room Database class. Refer to README for more information 13 | */ 14 | @Database(entities = [Employee::class], version = 1, exportSchema = false) 15 | abstract class ExampleRoomDB : RoomDatabase() { 16 | 17 | abstract fun getEmployeeDao(): EmployeeDAO 18 | 19 | /** 20 | * Creates a way to ensure that the database accessed in different locations is the same 21 | * instance. Also known as a Singleton pattern. Further explained in the Employee Repository. 22 | */ 23 | companion object { 24 | private var INSTANCE: ExampleRoomDB? = null 25 | 26 | fun getDatabase(context: Context) = INSTANCE ?: kotlin.run { 27 | Room.databaseBuilder( 28 | context.applicationContext, 29 | ExampleRoomDB::class.java, 30 | ROOM_DB_NAME 31 | ) 32 | .fallbackToDestructiveMigration() 33 | .build() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/start_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/activities/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | /** 8 | * This is the starting point of the app. Looking at the manifest.xml, you should see that this 9 | * Activity has the Launcher and Main Intent flags associated with it. This is how you tell the 10 | * OS which Class to open first. 11 | * 12 | * Follow the tutorial listed in the README on how to implement this. 13 | */ 14 | class SplashActivity : AppCompatActivity() { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | val intent = Intent(this, MainActivity::class.java) 20 | startActivity(intent) 21 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) 22 | finish() 23 | 24 | // An Example of how a theoretical Firebase User check would be implemented. 25 | // // Get Firebase Intance 26 | // val auth = FirebaseAuth.getInstance() 27 | // 28 | // val intent = auth.currentUser?.let { 29 | // // if currentUser is NOT null, go to MainActivity 30 | // Intent(this, MainActivity::class.java) 31 | // // else, when currentUser IS null, go to LoginActivity (This is not implemented in this app) 32 | // } ?: Intent(this, LoginActivity::class.java) 33 | // // launch whichever Intent was created 34 | // startActivity(intent) 35 | // overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) 36 | // finish() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/model/models/Employee.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.model.models 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.google.gson.annotations.Expose 6 | import com.google.gson.annotations.SerializedName 7 | 8 | /** 9 | * 06 Create your data class. 10 | * 11 | * Ensure your data class matches the data(JSON, XML, HTML) 12 | * you receive, if you are connecting to an endpoint. 13 | * 14 | * This particular project does use an endpoint for it's data. 15 | * Samples of the JSON are in the readme package, in the 16 | * SampleJson.json file. 17 | * 18 | * @author Marshall Ladd 19 | */ 20 | // 06 This creates our class, and defines our constructor, all in one line 21 | @Entity(tableName = "employee_table") 22 | data class Employee( 23 | @Expose 24 | @PrimaryKey 25 | val id: Int, 26 | /** 27 | * 06 @SerializedName is used to tell the GSON library that 28 | * the variable name from the JSON, does not match what you have 29 | * named Kotlin variable. In this case, the JSON field 30 | * employee_name is mapped to the Kotlin variable employeeName. 31 | * 32 | * Notice we did not need this line for the id variable. This is 33 | * because the variable 'id' in the Kotlin matches the JSON name exactly. 34 | */ 35 | @SerializedName("employee_name") 36 | @Expose 37 | val employeeName: String, 38 | @SerializedName("employee_salary") 39 | @Expose 40 | val employeeSalary: Int, 41 | @SerializedName("employee_age") 42 | @Expose 43 | val employeeAge: Int, 44 | @SerializedName("profile_image") 45 | @Expose 46 | val profileImage: String 47 | ) 48 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/fragments/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.viewModels 7 | import androidx.lifecycle.observe 8 | import kotlinx.android.synthetic.main.detail_fragment.* 9 | import ladd.marshall.androidmvvmexample.R 10 | import ladd.marshall.androidmvvmexample.utils.EMPLOYEE_ID 11 | import ladd.marshall.androidmvvmexample.viewModel.viewModels.DetailViewModel 12 | 13 | class DetailFragment : Fragment(R.layout.detail_fragment) { 14 | 15 | private val viewModel by viewModels() 16 | 17 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 18 | super.onViewCreated(view, savedInstanceState) 19 | // Retrieves the bundle passed to it from the previous Fragment and checks if it is null 20 | arguments?.let { bundle -> 21 | // If not null, get the data passed to it, using the constant, and observe the LiveData 22 | viewModel.employeeLiveData(bundle.getInt(EMPLOYEE_ID)).observe(this) { employeeOrNull -> 23 | // Check if the result of the search is null or not 24 | employeeOrNull?.let { employee -> 25 | // if not null, update the UI with the update data 26 | textViewName.text = employee.employeeName 27 | textViewId.text = employee.id.toString() 28 | textViewAge.text = employee.employeeAge.toString() 29 | textViewImageLink.text = employee.profileImage 30 | textViewSalary.text = employee.employeeSalary.toString() 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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/layout/search_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 28 | 29 | 30 | 31 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/fragments/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.os.bundleOf 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.navigation.fragment.findNavController 9 | import kotlinx.android.synthetic.main.search_fragment.* 10 | import ladd.marshall.androidmvvmexample.R 11 | import ladd.marshall.androidmvvmexample.utils.EMPLOYEE_ID 12 | import ladd.marshall.androidmvvmexample.utils.EMPLOYEE_NAME 13 | import ladd.marshall.androidmvvmexample.utils.toInt 14 | import ladd.marshall.androidmvvmexample.viewModel.viewModels.SearchViewModel 15 | 16 | class SearchFragment : Fragment(R.layout.search_fragment) { 17 | 18 | // Currently not used 19 | private val viewModel by viewModels() 20 | 21 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 22 | super.onViewCreated(view, savedInstanceState) 23 | buttonSearch.setOnClickListener { 24 | /** 25 | * A Bundle is how you can share data between Fragments in the nav graph. Bundles use 26 | * Pair in a Map, where the first part is the Key, and the second is the data. 27 | * 28 | * In Kotlin, the 'to' keyword creates Pair Objects quickly. 29 | * 30 | * The Key values for the pairs come from the utils/Constants.kt file. Using a 31 | * constants file prevents mistakes when retrieving the data later, and makes for 32 | * faster refactoring, if needed. 33 | * 34 | * The first pair uses a Kotlin Extension function, which can be found in 35 | * utils/Extensions.kt file, to get the text value from the input field. 36 | */ 37 | val bundle = bundleOf(EMPLOYEE_ID to textInputEmployeeId.toInt(), EMPLOYEE_NAME to "Searching...") 38 | findNavController().navigate(R.id.action_searchFragment_to_detailFragment, bundle) } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 20 | 21 | 31 | 32 | 36 | 37 | 44 | 45 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/adapters/EmployeeRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.ListAdapter 9 | import androidx.recyclerview.widget.RecyclerView 10 | import ladd.marshall.androidmvvmexample.R 11 | import ladd.marshall.androidmvvmexample.model.models.Employee 12 | 13 | /** 14 | * Uses a ListAdapter instead of a standard RecyclerViewAdapter. This class 15 | * offers built in animations when updating a data set, like in a LiveData Observer. 16 | * 17 | * More info here: 18 | * https://medium.com/@trionkidnapper/recyclerview-more-animations-with-less-code-using-support-library-listadapter-62e65126acdb 19 | */ 20 | class EmployeeRecyclerViewAdapter(private val onClickListener: (Employee) -> Unit) : 21 | ListAdapter(EmployeeDiffCallback()) { 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder { 24 | val view = LayoutInflater.from(parent.context) 25 | .inflate(R.layout.list_item_employee, parent, false) 26 | return EmployeeViewHolder(view, onClickListener) 27 | } 28 | 29 | override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) { 30 | holder.item = getItem(position) 31 | } 32 | } 33 | 34 | class EmployeeViewHolder(private val view: View, private val onClickListener: (Employee) -> Unit) : 35 | RecyclerView.ViewHolder(view) { 36 | 37 | var item: Employee? = null 38 | set(value) { 39 | value?.let { newValue -> 40 | field = newValue 41 | view.setOnClickListener { onClickListener(newValue) } 42 | view.findViewById(R.id.textViewName).text = newValue.employeeName 43 | view.findViewById(R.id.textViewId).text = "${newValue.id}" 44 | } 45 | } 46 | } 47 | 48 | class EmployeeDiffCallback : DiffUtil.ItemCallback() { 49 | 50 | override fun areItemsTheSame(oldItem: Employee, newItem: Employee): Boolean { 51 | return oldItem.id == newItem.id 52 | } 53 | 54 | override fun areContentsTheSame(oldItem: Employee, newItem: Employee): Boolean { 55 | return oldItem == newItem 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/view/fragments/ListFragment.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.view.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.os.bundleOf 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.observe 9 | import androidx.navigation.fragment.findNavController 10 | import androidx.recyclerview.widget.DividerItemDecoration 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import kotlinx.android.synthetic.main.list_fragment.* 13 | import ladd.marshall.androidmvvmexample.R 14 | import ladd.marshall.androidmvvmexample.utils.EMPLOYEE_ID 15 | import ladd.marshall.androidmvvmexample.utils.EMPLOYEE_NAME 16 | import ladd.marshall.androidmvvmexample.view.adapters.EmployeeRecyclerViewAdapter 17 | import ladd.marshall.androidmvvmexample.viewModel.viewModels.ListViewModel 18 | 19 | class ListFragment : Fragment(R.layout.list_fragment) { 20 | 21 | private val viewModel by viewModels() 22 | 23 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 24 | super.onViewCreated(view, savedInstanceState) 25 | // Sets up the RecyclerView using Kotlin's functional programming capabilities. 26 | recyclerView.apply { 27 | this.layoutManager = LinearLayoutManager(this.context) 28 | this.adapter = EmployeeRecyclerViewAdapter { employee -> 29 | val bundle = bundleOf(EMPLOYEE_ID to employee.id, EMPLOYEE_NAME to employee.employeeName) 30 | findNavController().navigate(R.id.action_listFragment_to_detailFragment, bundle) 31 | } 32 | // This a simple divider between each list item in the RecyclerView 33 | this.addItemDecoration( 34 | DividerItemDecoration( 35 | this.context, 36 | (this.layoutManager as LinearLayoutManager).orientation 37 | ) 38 | ) 39 | } 40 | /** 41 | * Observes the LiveData> from the ViewModel. Any time this object updates, 42 | * the code within the lambda will be executed with the most current data. 43 | */ 44 | viewModel.employeeListLiveData.observe(this) { employeeList -> 45 | 46 | cardViewListLoading.visibility = when { 47 | employeeList.isEmpty() -> View.VISIBLE 48 | else -> View.GONE 49 | } 50 | /** 51 | * Get the adapter from recyclerView and cast it to the correct class and then pass it 52 | * the update List. 53 | */ 54 | (recyclerView.adapter as EmployeeRecyclerViewAdapter).submitList(employeeList) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 20 | 27 | 28 | 33 | 40 | 41 | 46 | 53 | 54 | 59 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | apply plugin: 'kotlin-kapt' 8 | 9 | android { 10 | compileSdkVersion 29 11 | buildToolsVersion "29.0.2" 12 | defaultConfig { 13 | applicationId "ladd.marshall.androidmvvmexample" 14 | minSdkVersion 23 15 | targetSdkVersion 29 16 | versionCode 1 17 | versionName "1.0" 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | // The following two blocks are needed for some of the newer Kotlin features to work in Android. 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(dir: 'libs', include: ['*.jar']) 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 47 | implementation 'androidx.appcompat:appcompat:1.1.0' 48 | implementation 'androidx.core:core-ktx:1.1.0' 49 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 50 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 51 | testImplementation 'junit:junit:4.12' 52 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 54 | 55 | // 01 - Add dependencies 56 | 57 | // kotlin ktx 58 | implementation 'androidx.core:core-ktx:1.1.0' 59 | // LiveData ktx 60 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc02' 61 | // Lifecycle ktx 62 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc02' 63 | 64 | // coroutines 65 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2' 66 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2' 67 | 68 | // Navigation component 69 | implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0' 70 | implementation 'android.arch.navigation:navigation-ui-ktx:1.0.0' 71 | 72 | // Room 73 | implementation 'androidx.room:room-runtime:2.2.1' 74 | kapt 'androidx.room:room-compiler:2.2.1' 75 | 76 | // Lifecycle Extensions and ViewModel 77 | implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' 78 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0' 79 | implementation 'androidx.activity:activity:1.0.0' 80 | implementation 'androidx.activity:activity-ktx:1.0.0' 81 | implementation 'androidx.fragment:fragment-ktx:1.1.0' 82 | 83 | // Material Design 84 | implementation 'com.google.android.material:material:1.2.0-alpha01' 85 | 86 | // Better log messages 87 | implementation 'com.jakewharton.timber:timber:4.7.1' 88 | 89 | // RetroFit 90 | implementation 'com.squareup.retrofit2:retrofit:2.6.2' 91 | implementation 'com.squareup.retrofit2:converter-gson:2.6.2' 92 | 93 | // GSON 94 | implementation 'com.google.code.gson:gson:2.8.6' 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/ladd/marshall/androidmvvmexample/viewModel/repositories/EmployeeRepository.kt: -------------------------------------------------------------------------------- 1 | package ladd.marshall.androidmvvmexample.viewModel.repositories 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.LiveData 5 | import ladd.marshall.androidmvvmexample.api.EmployeeEndpoints 6 | import ladd.marshall.androidmvvmexample.api.RetroFitInstance 7 | import ladd.marshall.androidmvvmexample.model.database.ExampleRoomDB 8 | import ladd.marshall.androidmvvmexample.model.database.daos.EmployeeDAO 9 | import ladd.marshall.androidmvvmexample.model.models.Employee 10 | import timber.log.Timber 11 | 12 | /** 13 | * Repository classes coordinate between local and remote databases. This provides what is known as 14 | * a 'single source of truth'. 15 | */ 16 | 17 | /** 18 | * 'private constructor' prevents this class from being instatiated directly. A builder function 19 | * is provided in a companion object instead. This is essentially a Singleton pattern from java. 20 | */ 21 | class EmployeeRepository private constructor(application: Application) { 22 | 23 | private val employeeDAO: EmployeeDAO = ExampleRoomDB.getDatabase(application).getEmployeeDao() 24 | private val employeeCalls = RetroFitInstance.getInstance().create(EmployeeEndpoints::class.java) 25 | 26 | /** 27 | * An 'init' block in Kotlin is called immediately after an Object is created, and never again. 28 | * This block is used only for demoing the Singleton pattern. EmployeeRepository is accessed 29 | * from many classes at different times. If you watch the Logcot messages while switching 30 | * between screens, and filter by this class name, you'll notice that this message only appears 31 | * once, implying that its constructor is only being called once. 32 | */ 33 | init { 34 | Timber.d("${this.javaClass.name} init for the first time.") 35 | } 36 | 37 | private suspend fun insertEmployee(employee: Employee) { 38 | employeeDAO.insertEmployee(employee) 39 | } 40 | 41 | private suspend fun deleteEmployee(employee: Employee) { 42 | employeeDAO.deleteEmployee(employee) 43 | } 44 | 45 | suspend fun getAllEmployeesLiveData(): LiveData> { 46 | return employeeDAO.getAllEmployeesLiveData().also { 47 | getAllEmployeesFromRemote() 48 | } 49 | } 50 | 51 | private suspend fun getAllEmployeesFromRemote() { 52 | try { 53 | val employeeList = employeeCalls.getEmployeeList() 54 | employeeList.forEach { 55 | insertEmployee(it) 56 | } 57 | } catch (exception: Throwable) { 58 | Timber.e(exception) 59 | } 60 | } 61 | 62 | suspend fun getEmployeeByIdLiveData(employeeId: Int): LiveData { 63 | return employeeDAO.getEmployeeByIdLiveData(employeeId).also { 64 | try { 65 | val employee = employeeCalls.getEmployeeById(employeeId) 66 | insertEmployee(employee) 67 | } catch (exception: Throwable) { 68 | Timber.e(exception) 69 | } 70 | } 71 | } 72 | 73 | // Singleton Pattern for Repository. 74 | companion object { 75 | /** 76 | * This is where the EmployeeRepository all callers will receive. Set it to null at first 77 | * and make it private so it can't be directly accessed. 78 | */ 79 | private var INSTANCE: EmployeeRepository? = null 80 | 81 | /** 82 | * This method checks whether or not INSTANCE is null. If it's not null, it returns the 83 | * Singleton INSTANCE. If it is null, it creates a new Object, sets INSTANCE equal to that, 84 | * and returns INSTANCE. From here on out, this method will now return the same INSTANCE, 85 | * every time. 86 | */ 87 | fun getInstance(application: Application): EmployeeRepository = INSTANCE ?: kotlin.run { 88 | INSTANCE = EmployeeRepository(application = application) 89 | INSTANCE!! 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 13 | 14 | 15 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | xmlns:android 24 | 25 | ^$ 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 | xmlns:.* 35 | 36 | ^$ 37 | 38 | 39 | BY_NAME 40 | 41 |
42 |
43 | 44 | 45 | 46 | .*:id 47 | 48 | http://schemas.android.com/apk/res/android 49 | 50 | 51 | 52 |
53 |
54 | 55 | 56 | 57 | .*:name 58 | 59 | http://schemas.android.com/apk/res/android 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | name 69 | 70 | ^$ 71 | 72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | style 80 | 81 | ^$ 82 | 83 | 84 | 85 |
86 |
87 | 88 | 89 | 90 | .* 91 | 92 | ^$ 93 | 94 | 95 | BY_NAME 96 | 97 |
98 |
99 | 100 | 101 | 102 | .* 103 | 104 | http://schemas.android.com/apk/res/android 105 | 106 | 107 | ANDROID_ATTRIBUTE_ORDER 108 | 109 |
110 |
111 | 112 | 113 | 114 | .* 115 | 116 | .* 117 | 118 | 119 | BY_NAME 120 | 121 |
122 |
123 |
124 |
125 | 126 | 128 |
129 |
-------------------------------------------------------------------------------- /app/src/main/res/layout/detail_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 23 | 24 | 33 | 34 | 47 | 48 | 61 | 62 | 75 | 76 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | 16 | # Created by https://www.gitignore.io/api/kotlin,android,intellij,androidstudio 17 | # Edit at https://www.gitignore.io/?templates=kotlin,android,intellij,androidstudio 18 | 19 | ### Android ### 20 | # Built application files 21 | *.apk 22 | *.ap_ 23 | *.aab 24 | 25 | # Files for the ART/Dalvik VM 26 | *.dex 27 | 28 | # Java class files 29 | *.class 30 | 31 | # Generated files 32 | bin/ 33 | gen/ 34 | out/ 35 | release/ 36 | 37 | # Gradle files 38 | .gradle/ 39 | build/ 40 | 41 | # Local configuration file (sdk path, etc) 42 | local.properties 43 | 44 | # Proguard folder generated by Eclipse 45 | proguard/ 46 | 47 | # Log Files 48 | *.log 49 | 50 | # Android Studio Navigation editor temp files 51 | .navigation/ 52 | 53 | # Android Studio captures folder 54 | captures/ 55 | 56 | # IntelliJ 57 | *.iml 58 | .idea/workspace.xml 59 | .idea/tasks.xml 60 | .idea/gradle.xml 61 | .idea/assetWizardSettings.xml 62 | .idea/dictionaries 63 | .idea/libraries 64 | # Android Studio 3 in .gitignore file. 65 | .idea/caches 66 | .idea/modules.xml 67 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 68 | .idea/navEditor.xml 69 | 70 | # Keystore files 71 | # Uncomment the following lines if you do not want to check your keystore files in. 72 | #*.jks 73 | #*.keystore 74 | 75 | # External native build folder generated in Android Studio 2.2 and later 76 | .externalNativeBuild 77 | 78 | # Google Services (e.g. APIs or Firebase) 79 | # google-services.json 80 | 81 | # Freeline 82 | freeline.py 83 | freeline/ 84 | freeline_project_description.json 85 | 86 | # fastlane 87 | fastlane/report.xml 88 | fastlane/Preview.html 89 | fastlane/screenshots 90 | fastlane/test_output 91 | fastlane/readme.md 92 | 93 | # Version control 94 | vcs.xml 95 | 96 | # lint 97 | lint/intermediates/ 98 | lint/generated/ 99 | lint/outputs/ 100 | lint/tmp/ 101 | # lint/reports/ 102 | 103 | ### Android Patch ### 104 | gen-external-apklibs 105 | output.json 106 | 107 | # Replacement of .externalNativeBuild directories introduced 108 | # with Android Studio 3.5. 109 | .cxx/ 110 | 111 | ### Intellij ### 112 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 113 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 114 | 115 | # User-specific stuff 116 | .idea/**/workspace.xml 117 | .idea/**/tasks.xml 118 | .idea/**/usage.statistics.xml 119 | .idea/**/dictionaries 120 | .idea/**/shelf 121 | 122 | # Generated files 123 | .idea/**/contentModel.xml 124 | 125 | # Sensitive or high-churn files 126 | .idea/**/dataSources/ 127 | .idea/**/dataSources.ids 128 | .idea/**/dataSources.local.xml 129 | .idea/**/sqlDataSources.xml 130 | .idea/**/dynamic.xml 131 | .idea/**/uiDesigner.xml 132 | .idea/**/dbnavigator.xml 133 | 134 | # Gradle 135 | .idea/**/gradle.xml 136 | .idea/**/libraries 137 | 138 | # Gradle and Maven with auto-import 139 | # When using Gradle or Maven with auto-import, you should exclude module files, 140 | # since they will be recreated, and may cause churn. Uncomment if using 141 | # auto-import. 142 | # .idea/modules.xml 143 | # .idea/*.iml 144 | # .idea/modules 145 | # *.iml 146 | # *.ipr 147 | 148 | # CMake 149 | cmake-build-*/ 150 | 151 | # Mongo Explorer plugin 152 | .idea/**/mongoSettings.xml 153 | 154 | # File-based project format 155 | *.iws 156 | 157 | # IntelliJ 158 | 159 | # mpeltonen/sbt-idea plugin 160 | .idea_modules/ 161 | 162 | # JIRA plugin 163 | atlassian-ide-plugin.xml 164 | 165 | # Cursive Clojure plugin 166 | .idea/replstate.xml 167 | 168 | # Crashlytics plugin (for Android Studio and IntelliJ) 169 | com_crashlytics_export_strings.xml 170 | crashlytics.properties 171 | crashlytics-build.properties 172 | fabric.properties 173 | 174 | # Editor-based Rest Client 175 | .idea/httpRequests 176 | 177 | # Android studio 3.1+ serialized cache file 178 | .idea/caches/build_file_checksums.ser 179 | 180 | ### Intellij Patch ### 181 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 182 | 183 | # *.iml 184 | # modules.xml 185 | # .idea/misc.xml 186 | # *.ipr 187 | 188 | # Sonarlint plugin 189 | .idea/**/sonarlint/ 190 | 191 | # SonarQube Plugin 192 | .idea/**/sonarIssues.xml 193 | 194 | # Markdown Navigator plugin 195 | .idea/**/markdown-navigator.xml 196 | .idea/**/markdown-navigator/ 197 | 198 | ### Kotlin ### 199 | # Compiled class file 200 | 201 | # Log file 202 | 203 | # BlueJ files 204 | *.ctxt 205 | 206 | # Mobile Tools for Java (J2ME) 207 | .mtj.tmp/ 208 | 209 | # Package Files # 210 | *.jar 211 | *.war 212 | *.nar 213 | *.ear 214 | *.zip 215 | *.tar.gz 216 | *.rar 217 | 218 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 219 | hs_err_pid* 220 | 221 | ### AndroidStudio ### 222 | # Covers files to be ignored for android development using Android Studio. 223 | 224 | # Built application files 225 | 226 | # Files for the ART/Dalvik VM 227 | 228 | # Java class files 229 | 230 | # Generated files 231 | 232 | # Gradle files 233 | .gradle 234 | 235 | # Signing files 236 | .signing/ 237 | 238 | # Local configuration file (sdk path, etc) 239 | 240 | # Proguard folder generated by Eclipse 241 | 242 | # Log Files 243 | 244 | # Android Studio 245 | /*/build/ 246 | /*/local.properties 247 | /*/out 248 | /*/*/build 249 | /*/*/production 250 | *.ipr 251 | *~ 252 | *.swp 253 | 254 | # Android Patch 255 | 256 | # External native build folder generated in Android Studio 2.2 and later 257 | 258 | # NDK 259 | obj/ 260 | 261 | # IntelliJ IDEA 262 | /out/ 263 | 264 | # User-specific configurations 265 | .idea/caches/ 266 | .idea/libraries/ 267 | .idea/shelf/ 268 | .idea/.name 269 | .idea/compiler.xml 270 | .idea/copyright/profiles_settings.xml 271 | .idea/encodings.xml 272 | .idea/misc.xml 273 | .idea/scopes/scope_settings.xml 274 | .idea/vcs.xml 275 | .idea/jsLibraryMappings.xml 276 | .idea/datasources.xml 277 | .idea/dataSources.ids 278 | .idea/sqlDataSources.xml 279 | .idea/dynamic.xml 280 | .idea/uiDesigner.xml 281 | 282 | # OS-specific files 283 | .DS_Store 284 | .DS_Store? 285 | ._* 286 | .Spotlight-V100 287 | .Trashes 288 | ehthumbs.db 289 | Thumbs.db 290 | 291 | # Legacy Eclipse project files 292 | .classpath 293 | .project 294 | .cproject 295 | .settings/ 296 | 297 | # Mobile Tools for Java (J2ME) 298 | 299 | # Package Files # 300 | 301 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 302 | 303 | ## Plugin-specific files: 304 | 305 | # mpeltonen/sbt-idea plugin 306 | 307 | # JIRA plugin 308 | 309 | # Mongo Explorer plugin 310 | .idea/mongoSettings.xml 311 | 312 | # Crashlytics plugin (for Android Studio and IntelliJ) 313 | 314 | ### AndroidStudio Patch ### 315 | 316 | !/gradle/wrapper/gradle-wrapper.jar 317 | 318 | # End of https://www.gitignore.io/api/kotlin,android,intellij,androidstudio 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android App Architecture in The App Factory 2 | #### Authors 3 | - Marshall Ladd 4 | - 5 | ### The MVVM Design Pattern 6 | 7 | #### Contents 8 | - [Introduction](#introduction) 9 | - [How to Use This Guide](#how-to-use-this-guide) 10 | - [MVVM](#mvvm) 11 | - [What is MVVM?](#what-is-mvvm) 12 | - [How it Works](#how-it-works) 13 | - [Room Database](#room-database) 14 | - [Repositories](#repositories) 15 | - [Code Style Guide](#code-style-guide) 16 | - [Other Tools](#other-tools) 17 | - [Navigation](#navigation) 18 | - [Splash Screens](#splash-screens) 19 | - [Retrofit2](#retrofit2) 20 | - [Timber](#timber) 21 | - [Material Components](#material-components) 22 | - [Instructions](#instructions) 23 | - [Step By Step](#step-by-step) 24 | - [How to Learn an App](#how-to-learn-an-app) 25 | - [Other](#other) 26 | - [Disclaimer](#disclaimer) 27 | 28 | # Introduction 29 | This project is to provide a simple example app for Android Developers 30 | at The App Factory to reference when starting a new app, or adding new 31 | features to an existing app. 32 | 33 | The architecture pattern shown here and to be used in apps, whenever 34 | possible, is Model-View-ViewModel, or MVVM. This is the pattern 35 | recommended and used by Google. 36 | 37 | ### App Features 38 | - Connects to a remote database - 39 | [documentation](http://dummy.restapiexample.com/) 40 | - Downloads JSON(Employee) data from remote database 41 | - Stores data in Room Database in app 42 | - Displays list of Employees from database 43 | - Allows searching of database 44 | - Select a single Employee and show details of 45 | 46 | ### Language and IDE 47 | This project was written using Kotlin 1.3.60 in Android Studio 3.5.2. 48 | All Android apps in the App Factory will be written in Kotlin. 49 | 50 | ### Prerequisites 51 | This project assumes a base knowledge of Kotlin and Android, such as 52 | Activities, Fragments, RecyclerViews, and the Manifest. 53 | 54 | ## How to Use This Guide 55 | This README has a general summary of the design principles used in this 56 | app, using code snippets to show simplified examples to accompany the 57 | explanations. You should reference the code in the app for the complete 58 | implementation of classes. 59 | 60 | Further down the guide, there is a [Step By Step](#step-by-step) summary 61 | of how I built this app. I will briefly explain what I implemented, in 62 | the order I implemented it. 63 | 64 | The Master branch will always have the latest full, approved, running, 65 | and commented code. 66 | 67 | After that, there is a section for developers, new and experienced, who 68 | are asked to jump on a project that's already going. This section will 69 | walk you through my steps on how I approach someone else's code. The 70 | steps I will go over are applicable to any and all Android apps, not 71 | just one's that use MVVM. I will be using this app as an example, and 72 | pretending we had no comments while I do so. 73 | 74 | # MVVM 75 | 76 | ## What is MVVM? 77 | MVVM is a flexible guide and set of libraries used to standardize an 78 | app's architecture. What this means is that your code is split into 79 | distinct components that hold specific parts of code in specific areas. 80 | These parts then interact with each other in a set order. These parts 81 | are: 82 | 83 | #### Model 84 | The Model consists of three parts 85 | 86 | - Database Class 87 | - DAO Interfaces 88 | - Data Model Classes 89 | 90 | In this app, our local database is Room. In other apps, Firebase may be 91 | the database. The flexibility of MVVM allows for these differences, and 92 | does not specify **what** to use, just **where** to put the code. 93 | 94 | #### View 95 | The View is the UI presentation logic. It consists of 96 | 97 | - Activities 98 | - Fragments 99 | - Adapters 100 | - XML 101 | 102 | The View should only be responsible for displaying values and state. 103 | Another way to say that is you should not make database requests and/or 104 | network requests in the View. These will be done elsewhere. This means 105 | the Views don't care where the data comes from or how it gets there, it 106 | just shows whatever data there is. This way, if a change needs to be 107 | made to the database, the View should not need to be changed at all. 108 | 109 | #### View Model 110 | The View Model is responsible for interacting with the Database and 111 | coordinating between any remote sources. It is then responsible for 112 | representing the state of the data to the View. This coordination 113 | creates what is known as a **single source of truth** and exposes it to 114 | the Views. This way, you can be as sure as you can be, that what is 115 | being shown to the user is accurate, and where it came from. 116 | 117 | ViewModels provided by the Jetpack components are also Lifecycle aware, 118 | and can survive configuration changes. This helps: 119 | - Prevent memory leaks 120 | - Prevent errant network calls 121 | - Prevent null pointer exceptions due to UI changes 122 | - Solves the issue of what happens when you rotate an app 123 | 124 | ## How it works 125 | As mentioned before MVVM breaks the app into components and they 126 | interact in a certain way. This interaction follows the pattern: 127 | 1. The View subscribes to a LiveData from a ViewModel 128 | 2. The ViewModel connects to a repository 129 | 3. The Repository connects to databases, both remote and local, if there 130 | are both 131 | 4. The Repository returns the requested data to the the ViewModel 132 | 5. The ViewModel formats the data and exposes it through a LiveData 133 | 6. The View's subscriptions are notified of any changes through the 134 | LIveData, and updates the UI to match 135 | 136 | #### LiveData 137 | LiveData was mentioned a few times, so what is it? LiveData is a wrapper 138 | class for data objects. It is observable from a View. Being observable 139 | is a way to automatically call update UI logic, anytime the data is 140 | changed. There's more to it than all that, but those are the very 141 | basics. If they don't make sense now, they will after you use them. 142 | 143 | ### Basic MVVM Example 144 | Inside of a Fragment such as ListFragment, get an instance of the 145 | ViewModel needed. Here we are using a shortcut method, thanks to a KTX 146 | library. 147 | 148 | ```kotlin 149 | private val listViewModel by viewModels() 150 | ``` 151 | 152 | The ViewModel will have a Function or Object that triggers background 153 | database and network requests, and immediately returns a LiveData Object 154 | to be observed. 155 | 156 | ```kotlin 157 | fun getAllEmployees(): LiveData> { 158 | // Database and or network logic happens 159 | // return LiveData> 160 | } 161 | ``` 162 | 163 | Then, back in the Fragment, you can call to this method, and observe the 164 | returned LiveData and update your UI with the contained data. When the 165 | background database or network requests finish, they post their updates 166 | to this Object, triggering a UI update. 167 | 168 | ```kotlin 169 | listViewModel.getAllEmployees().observe(this) { employeeList -> 170 | recyclerViewAdapter.submitList(employeeList) 171 | // other operations related to getting new data 172 | } 173 | ``` 174 | 175 | ## Room Database 176 | This app uses the Room Database library, provided by Google as part of 177 | Android Jetpack. Room is a wrapper for Android's SQLite DB. Essentially, 178 | it operates like Retrofit, but for database calls. It relies heavily on 179 | annotations to generate code for you. Room also natively supports 180 | LiveData. 181 | 182 | Documentation can be found [here](https://developer.android.com/training/data-storage/room). 183 | 184 | A tutorial on Room, provided by Google, can be found 185 | [here](https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0). 186 | 187 | ### Basic Room Example 188 | Room consist of three main parts 189 | - The Database Class 190 | - Data Access Object Interfaces 191 | - Annotated Data Classes 192 | 193 | First, create a Data class to store in Room and add the **@Entity** and 194 | **@PrimaryKey** annotations where needed. 195 | 196 | ```kotlin 197 | @Entity(tableName = "employee_table") 198 | data class Employee(@PrimaryKey val id: Int, val name: String) 199 | ``` 200 | 201 | Next, create a Data Access Object Interface or DAO. This is where you 202 | will define all of your database CRUD actions. Note all the annotations 203 | and that this is an interface, and you do not write any function bodies. 204 | Room will write these for you. 205 | 206 | ```kotlin 207 | @Dao 208 | interface EmployeeDAO { 209 | 210 | @Insert(onConflict = OnConflictStrategy.REPLACE) 211 | fun insertEmployee(employee: Employee) 212 | 213 | @Delete 214 | fun deleteEmployee(employee: Employee) 215 | 216 | @Query("SELECT * FROM employee_table") 217 | fun getAllEmployeesLiveData(): LiveData> 218 | 219 | @Query("SELECT * FROM employee_table WHERE id = :employeeId") 220 | fun getEmployeeByIdLiveData(employeeId: Int): LiveData 221 | } 222 | ``` 223 | 224 | Finally, we create our database class by extending RoomDatabase and 225 | marking it with some more annotations. 226 | 227 | ```kotlin 228 | @Database(entities = [Employee::class], version = 1, exportSchema = false) 229 | abstract class ExampleRoomDB : RoomDatabase() { 230 | 231 | abstract fun getEmployeeDao(): EmployeeDAO 232 | 233 | companion object { 234 | private var INSTANCE: ExampleRoomDB? = null 235 | 236 | fun getDatabase(context: Context) = INSTANCE ?: kotlin.run { 237 | Room.databaseBuilder( 238 | context.applicationContext, 239 | ExampleRoomDB::class.java, 240 | ROOM_DB_NAME 241 | ) 242 | .fallbackToDestructiveMigration() 243 | .build() 244 | } 245 | } 246 | } 247 | ``` 248 | 249 | In the above code, you tell Room which classes are going to be stored in 250 | the DB, what version the DB is on, and another flag that isn't important 251 | now. Keep it false. We also define an abstract function that return the 252 | DAO we created earlier. Do this for all DAOs in your app. 253 | 254 | When using Room, we also need to make a slight change to our ViewModels. 255 | If the ViewModel needs to access the Room Database, either directly or 256 | through a repository, it will need to extend **AndroidViewModel** 257 | instead of the normal **ViewModel**. 258 | 259 | ##### ViewModel Used WITHOUT Room 260 | ```kotlin 261 | class DetailViewModel : ViewModel() { 262 | // ViewModel methods here 263 | } 264 | ``` 265 | ##### ViewModel Used WITH Room 266 | ```kotlin 267 | class DetailViewModel(application: Application) : AndroidViewModel(application) { 268 | // ViewModel methods here 269 | } 270 | ``` 271 | 272 | At this point, you can get an instance of your DB and get an instance of 273 | the DAO, and start making calls to and storing data in your DB. These 274 | calls could be made directly in the Fragment, but that would be mixing 275 | components in MVVM(calling DB logic from the View). 276 | 277 | We could call the DAO methods from the ViewModel and expose the results 278 | to the UI through that. This is an acceptable approach, and a relatively 279 | good one. However, there is a better way. 280 | 281 | ## Repositories 282 | Repositories, while not required and aren't an official component of 283 | MVVM, they are considered a good practice. Especially when you have data 284 | being stored locally and data available on a remote API. The repository 285 | provides what is known as a **single source of truth**. More simply put, 286 | when data is coming possibly coming from multiple locations, the 287 | repository decides what is valid, what to show, where to call, and when 288 | to make those calls. 289 | 290 | ### Basic Repository Example 291 | Let's assume that our app has a Room Database completely set up, 292 | Retrofit for API calls, and an AndroidViewModel as described in previous 293 | sections. Create a new repository class. In this class, get an instance 294 | of the DAO and RetroFit interfaces. 295 | 296 | ```kotlin 297 | class EmployeeRepository private constructor(application: Application) { 298 | private val employeeDAO: EmployeeDAO = ExampleRoomDB.getDatabase(application).getEmployeeDao() 299 | private val employeeCalls = RetroFitInstance.getInstance().create(EmployeeEndpoints::class.java) 300 | } 301 | ``` 302 | 303 | Suppose we want to show the entire list of Employees. We need to check 304 | the API for current data, store this data, and show it to the user. But 305 | what if there is an update to the data, or what if the network is down. 306 | This is where the repository comes into play. 307 | 308 | You will create a method that calls to the API **AND** to the local 309 | database. 310 | 311 | ```kotlin 312 | suspend fun getAllEmployeesLiveData(): LiveData> { 313 | return employeeDAO.getAllEmployeesLiveData().also { 314 | try { 315 | val employeeList = employeeCalls.getEmployeeList() 316 | employeeList.forEach { 317 | insertEmployee(it) 318 | } 319 | } catch (exception: Throwable) { 320 | Timber.e(exception) 321 | } 322 | } 323 | } 324 | ``` 325 | 326 | The data in the database will be shown to the user immediately, through 327 | LiveData, while, in the background, a request to the API for new and 328 | current data has been made. When that request comes back, each result is 329 | inserted into the database. Since we returned a LiveData, any updates to 330 | the Database will notify the LiveData, which will update itself with the 331 | current information. 332 | 333 | Finally, update our ViewModel to use our repository, instead of calling 334 | to the API or to the database directly. 335 | 336 | ```kotlin 337 | class ListViewModel(application: Application) : AndroidViewModel(application) { 338 | 339 | private val employeeRepository = EmployeeRepository.getInstance(application) 340 | 341 | val employeeListLiveData: LiveData> = liveData(Dispatchers.IO) { 342 | emitSource(employeeRepository.getAllEmployeesLiveData()) 343 | } 344 | } 345 | ``` 346 | 347 | Now your UI should update with the most current data, and also be able 348 | to show data that was stored if there is no network. 349 | 350 | If you follow the steps in this guide, you'll notice that once we set up 351 | our Fragment, and subscribed to a LiveData object from a ViewModel, we 352 | never had to change anything in our Fragment. It just kept on working. 353 | This is what MVVM does for you. When you need to change a component, it 354 | doesn't affect other components. 355 | 356 | For example, it's a week before this app is deploying and we decided to 357 | change our Database from a REST API, like this app does now, to 358 | Firebase's Firestore. What would need to be changed? Firebase provides 359 | it's own local backup, so we won't need Room anymore. Firebase also has 360 | it's own calls to the API, so we can't use Retrofit calls either. 361 | Thankfully, all of that is in one place, the repository. We just change 362 | the functions in the repository to use Firebase, remove the unneeded 363 | code, and everything else remains the same. As long as we keep returning 364 | LiveData, the UI won't care where it came from, just as that it gets 365 | there. 366 | 367 | ## Final Thoughts on MVVM 368 | That's it. This app is ready to be released or expanded on. You could 369 | add more endpoints, add new database operations, add new screens, add 370 | new data classes, add whole new features, whatever. Just follow the 371 | patterns seen here when doing so as best you can. 372 | 373 | This guide is meant to be a general summary of the MVVM concept and give 374 | you an idea of how to implement it in your app. It is not a perfect 375 | example. For instance, MVVM recommends the use of DataBinding or 376 | ViewBinding and I did not use any of it in this app. This app is meant 377 | to be a starting point. Any improvements and further refinement to the 378 | design should be encouraged, however, deviations from the main design 379 | pattern should be kept to a minimum whenever possible. 380 | 381 | # Code Style Guide 382 | The following naming and style conventions should be used across all 383 | projects to aid in readability of code from one project to another. 384 | 385 | ##### Variables 386 | - Standard variables shall be named using camelCaseNotation. 387 | - Hungarian notation(mVarName, sVarname, etc.) is **NOT** to be used. 388 | 389 | ```kotlin 390 | // Do this 391 | val thisIsAVariable = "Some String value" 392 | // Don't do this 393 | val mThisIsAMemberVariable = 42 394 | ``` 395 | 396 | ##### Constants 397 | - Constants shall be declared in all caps. 398 | 399 | ```kotlin 400 | const val THIS_IS_A_URL_CONSTANT = "https:\\www.google.com" 401 | ``` 402 | 403 | ##### Functions 404 | - Functions and their parameters will be named using camelCaseNotation. 405 | - If there is no return value, omit return value in signature. 406 | 407 | ```kotlin 408 | // A function with no parameters or return value 409 | fun aBoringFunction() { 410 | // Does stuff 411 | } 412 | // A function with parameters and a return type 413 | fun doSomeMath(inputA: Int, inputB: Int): Int { 414 | return inputA + inputB 415 | } 416 | // This is wrong. 417 | fun NotInCamelCase(): Unit { 418 | println("I return nothing and should not have Unit in my signature.") 419 | } 420 | ``` 421 | 422 | ##### Classes 423 | - Classes shall be named in FirstLetterCapsCamelCase. 424 | - When possible, data classes should be used over standard classes. 425 | 426 | ```kotlin 427 | data class ExampleDataClass( 428 | val id: Int, 429 | val exampleField: String 430 | ) 431 | ``` 432 | 433 | ##### ktlint 434 | ktlint is a linter and formatter for Kotlin code. What that means is 435 | that it will go through your code and look for formatting errors and fix 436 | them for you. Things like adding to many indents or spaces before or 437 | after lines, removing unused imports, and ensuring the guidelines above 438 | are being followed are taken care of for you by running a simple script. 439 | 440 | Installation and usage instructions can be found here: 441 | - [ktlint](https://ktlint.github.io/) 442 | 443 | ##### View id's 444 | - View id's shall be named using camelCaseNotation 445 | - id's shall be formatted viewTypeThenFunction 446 | 447 | ```xml 448 | 450 |