├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wiseassblog │ │ └── jetpacknotesmvvmkotlin │ │ ├── common │ │ ├── AndroidExtensions.kt │ │ ├── BaseViewModel.kt │ │ ├── Constants.kt │ │ ├── DataExtensions.kt │ │ └── Result.kt │ │ ├── login │ │ ├── LoginActivity.kt │ │ ├── LoginEvent.kt │ │ ├── LoginView.kt │ │ ├── UserViewModel.kt │ │ └── buildlogic │ │ │ ├── LoginInjector.kt │ │ │ └── UserViewModelFactory.kt │ │ ├── model │ │ ├── FirebaseNote.kt │ │ ├── LoginResult.kt │ │ ├── Note.kt │ │ ├── NoteDao.kt │ │ ├── RoomNote.kt │ │ ├── RoomNoteDatabase.kt │ │ ├── User.kt │ │ ├── implementations │ │ │ ├── FirebaseUserRepoImpl.kt │ │ │ └── NoteRepoImpl.kt │ │ └── repository │ │ │ ├── INoteRepository.kt │ │ │ └── IUserRepository.kt │ │ └── note │ │ ├── NoteActivity.kt │ │ ├── NoteListViewModel.kt │ │ ├── NoteListViewModelFactory.kt │ │ ├── NoteViewModel.kt │ │ ├── NoteViewModelFactory.kt │ │ ├── notedetail │ │ ├── NoteDetailEvent.kt │ │ ├── NoteDetailView.kt │ │ └── buildlogic │ │ │ ├── NoteDetailInjector.kt │ │ │ └── NoteViewModelFactory.kt │ │ └── notelist │ │ ├── NoteDiffUtilCallback.kt │ │ ├── NoteListAdapter.kt │ │ ├── NoteListEvent.kt │ │ ├── NoteListView.kt │ │ └── buildlogic │ │ ├── NoteListInjector.kt │ │ └── NoteListViewModelFactory.kt │ └── res │ ├── drawable-land │ ├── space_bg_one.png │ ├── space_bg_three.png │ └── space_bg_two.png │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── antenna_loop.xml │ ├── antenna_loop_fast.xml │ ├── ic_access_time_black_24dp.xml │ ├── ic_antenna_empty.xml │ ├── ic_antenna_full.xml │ ├── ic_antenna_half.xml │ ├── ic_arrow_back_black_24dp.xml │ ├── ic_baseline_add_24px.xml │ ├── ic_baseline_event_24px.xml │ ├── ic_delete_forever_black_24dp.xml │ ├── ic_done_black_24dp.xml │ ├── ic_launcher_background.xml │ ├── ic_visibility_black_24dp.xml │ ├── ic_visibility_off_black_24dp.xml │ ├── ic_vpn_key_black_24dp.xml │ ├── im_rocket_one.xml │ ├── im_rocket_three.xml │ ├── im_rocket_two.xml │ ├── rocket_loop.xml │ ├── rocket_one.png │ ├── space_bg_one.png │ ├── space_bg_three.png │ ├── space_bg_two.png │ └── space_loop.xml │ ├── layout │ ├── activity_login.xml │ ├── activity_note.xml │ ├── fragment_login.xml │ ├── fragment_note_detail.xml │ ├── fragment_note_list.xml │ └── item_note.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── navigation │ └── nav_graph.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ ├── styles.xml │ └── view_styles.xml ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle /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: 'io.fabric' 6 | apply plugin: 'androidx.navigation.safeargs.kotlin' 7 | 8 | 9 | android { 10 | compileSdkVersion rootProject.compileSdkVersion 11 | defaultConfig { 12 | applicationId "com.wiseassblog.jetpacknotesmvvmkotlin" 13 | minSdkVersion rootProject.minSdkVersion 14 | targetSdkVersion rootProject.targetSdkVersion 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables.useSupportLibrary = true 19 | } 20 | buildTypes { 21 | debug { 22 | minifyEnabled false 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | } 31 | 32 | dependencies { 33 | 34 | implementation "androidx.appcompat:appcompat:$rootProject.supportLibraryVersion" 35 | implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion" 36 | implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerViewVersion" 37 | implementation "com.google.android.material:material:$rootProject.materialVersion" 38 | 39 | 40 | implementation "androidx.core:core-ktx:$rootProject.ktxVersion" 41 | implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleVersion" 42 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion" 43 | implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion" 44 | implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion" 45 | 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$rootProject.kotlinVersion" 47 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutinesVersion" 48 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutinesVersion" 49 | 50 | implementation "com.google.firebase:firebase-core:$rootProject.firebaseCore" 51 | implementation "com.google.firebase:firebase-auth:$rootProject.firebaseAuth" 52 | implementation "com.google.android.gms:play-services-auth:$rootProject.playServicesAuth" 53 | 54 | implementation "com.google.firebase:firebase-firestore:$rootProject.firebaseFirestore" 55 | implementation "androidx.room:room-runtime:$rootProject.room" 56 | implementation "androidx.room:room-ktx:$rootProject.room" 57 | 58 | 59 | kapt "androidx.room:room-compiler:$rootProject.room" 60 | 61 | testImplementation "org.junit.jupiter:junit-jupiter-api:$rootProject.junitVersion" 62 | testImplementation "org.junit.jupiter:junit-jupiter-engine:$rootProject.junitVersion" 63 | } 64 | 65 | apply plugin: 'com.google.gms.google-services' 66 | -------------------------------------------------------------------------------- /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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/common/AndroidExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.common 2 | 3 | import android.graphics.drawable.AnimationDrawable 4 | import android.widget.Toast 5 | import androidx.fragment.app.Fragment 6 | 7 | internal fun Fragment.makeToast(value: String) { 8 | Toast.makeText(activity, value, Toast.LENGTH_SHORT).show() 9 | } 10 | 11 | internal fun AnimationDrawable.startWithFade(){ 12 | this.setEnterFadeDuration(1000) 13 | this.setExitFadeDuration(1000) 14 | this.start() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/common/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.common 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Job 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | abstract class BaseViewModel(protected val uiContext: CoroutineContext) : ViewModel(), CoroutineScope { 11 | abstract fun handleEvent(event: T) 12 | 13 | //cancellation 14 | protected lateinit var jobTracker: Job 15 | 16 | init { 17 | jobTracker = Job() 18 | } 19 | 20 | //suggestion from Al Warren: to promote encapsulation and immutability, hide the MutableLiveData objects behind 21 | //LiveData references: 22 | protected val errorState = MutableLiveData() 23 | val error: LiveData get() = errorState 24 | 25 | protected val loadingState = MutableLiveData() 26 | val loading: LiveData get() = loadingState 27 | 28 | override val coroutineContext: CoroutineContext 29 | get() = uiContext + jobTracker 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.common 2 | 3 | internal const val LOGIN_ERROR = "Error retrieving user." 4 | internal const val LOADING = "Loading..." 5 | internal const val LOGOUT_ERROR = "Error logging out user." 6 | internal const val GET_NOTE_ERROR = "Error retrieving note." 7 | internal const val GET_NOTES_ERROR = "Error retrieving notes." 8 | internal const val SIGN_OUT = "SIGN OUT" 9 | internal const val SIGN_IN = "SIGN IN" 10 | internal const val SIGNED_IN = "Signed In" 11 | internal const val SIGNED_OUT = "Signed Out" 12 | internal const val ERROR_NETWORK_UNAVAILABLE = "Network Unavailable" 13 | internal const val ERROR_AUTH = "An Error Has Occured" 14 | internal const val RETRY = "RETRY" 15 | internal const val ANTENNA_EMPTY = "ic_antenna_empty" 16 | internal const val ANTENNA_FULL = "ic_antenna_full" 17 | internal const val ANTENNA_LOOP = "antenna_loop_fast" 18 | 19 | /** 20 | * This value is just a constant to denote our sign in request; It can be any int. 21 | * Would have been great if that was explained in the docs, I assumed at first that it had to 22 | * be a specific value. 23 | */ 24 | internal const val RC_SIGN_IN = 1337 -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/common/DataExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.common 2 | 3 | import android.text.Editable 4 | import com.google.android.gms.tasks.Task 5 | import com.google.firebase.auth.FirebaseUser 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.FirebaseNote 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.RoomNote 9 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.User 10 | import kotlin.coroutines.resume 11 | import kotlin.coroutines.resumeWithException 12 | import kotlin.coroutines.suspendCoroutine 13 | 14 | internal suspend fun awaitTaskResult(task: Task): T = suspendCoroutine { continuation -> 15 | task.addOnCompleteListener { task -> 16 | if (task.isSuccessful) { 17 | continuation.resume(task.result!!) 18 | } else { 19 | continuation.resumeWithException(task.exception!!) 20 | } 21 | } 22 | } 23 | 24 | //Wraps Firebase/GMS calls 25 | internal suspend fun awaitTaskCompletable(task: Task): Unit = suspendCoroutine { continuation -> 26 | task.addOnCompleteListener { task -> 27 | if (task.isSuccessful) { 28 | continuation.resume(Unit) 29 | } else { 30 | continuation.resumeWithException(task.exception!!) 31 | } 32 | } 33 | } 34 | 35 | internal val FirebaseUser.toUser: User 36 | get() = User( 37 | uid = this.uid, 38 | name = this.displayName ?: "" 39 | ) 40 | 41 | internal val FirebaseNote.toNote: Note 42 | get() = Note( 43 | this.creationDate ?: "", 44 | this.contents ?: "", 45 | this.upVotes ?: 0, 46 | this.imageurl ?: "", 47 | User(this.creator ?: "") 48 | ) 49 | 50 | internal val Note.toFirebaseNote: FirebaseNote 51 | get() = FirebaseNote( 52 | this.creationDate, 53 | this.contents, 54 | this.upVotes, 55 | this.imageUrl, 56 | this.safeGetUid 57 | ) 58 | 59 | internal val RoomNote.toNote: Note 60 | get() = Note( 61 | this.creationDate, 62 | this.contents, 63 | this.upVotes, 64 | this.imageUrl, 65 | User(this.creatorId) 66 | ) 67 | 68 | internal val Note.toRoomNote: RoomNote 69 | get() = RoomNote( 70 | this.creationDate, 71 | this.contents, 72 | this.upVotes, 73 | this.imageUrl, 74 | this.safeGetUid 75 | ) 76 | 77 | internal fun List.toNoteListFromRoomNote(): List = this.flatMap { 78 | listOf(it.toNote) 79 | } 80 | 81 | internal fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) 82 | 83 | internal val Note.safeGetUid: String 84 | get() = this.creator?.uid ?: "" 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/common/Result.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.common 2 | 3 | /** 4 | * Result Wrapper 5 | */ 6 | sealed class Result { 7 | 8 | data class Value(val value: V) : Result() 9 | data class Error(val error: E) : Result() 10 | 11 | companion object Factory{ 12 | inline fun build(function: () -> V): Result = 13 | try { 14 | Value(function.invoke()) 15 | } catch (e: java.lang.Exception) { 16 | Error(e) 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.login 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.google.firebase.FirebaseApp 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.R 7 | 8 | private const val LOGIN_VIEW = "LOGIN_VIEW" 9 | 10 | class LoginActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_login) 15 | FirebaseApp.initializeApp(applicationContext) 16 | 17 | val view = supportFragmentManager.findFragmentByTag(LOGIN_VIEW) as LoginView? 18 | ?: LoginView() 19 | 20 | supportFragmentManager.beginTransaction() 21 | .replace(R.id.root_activity_login, view, LOGIN_VIEW) 22 | .commitNowAllowingStateLoss() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/login/LoginEvent.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.login 2 | 3 | sealed class LoginEvent { 4 | object OnAuthButtonClick : LoginEvent() 5 | object OnStart : LoginEvent() 6 | data class OnGoogleSignInResult(val result: LoginResult) : LoginEvent() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/login/LoginView.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.login 2 | 3 | 4 | import android.content.Intent 5 | import android.graphics.drawable.AnimationDrawable 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.activity.OnBackPressedCallback 12 | import androidx.activity.addCallback 13 | import androidx.fragment.app.Fragment 14 | import androidx.lifecycle.Observer 15 | import androidx.lifecycle.ViewModelProvider 16 | import com.google.android.gms.auth.api.signin.GoogleSignIn 17 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 18 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 19 | import com.google.android.gms.common.api.ApiException 20 | import com.wiseassblog.jetpacknotesmvvmkotlin.R 21 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.ANTENNA_LOOP 22 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.RC_SIGN_IN 23 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.startWithFade 24 | import com.wiseassblog.jetpacknotesmvvmkotlin.login.buildlogic.LoginInjector 25 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.LoginResult 26 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.NoteActivity 27 | import kotlinx.android.synthetic.main.fragment_login.* 28 | 29 | //Note: if you want to support more than just English, you'll want to use Strings.xml instead of const val 30 | 31 | 32 | class LoginView : Fragment() { 33 | 34 | private lateinit var viewModel: UserViewModel 35 | 36 | override fun onCreateView( 37 | inflater: LayoutInflater, container: ViewGroup?, 38 | savedInstanceState: Bundle? 39 | ): View? { 40 | return inflater.inflate(R.layout.fragment_login, container, false) 41 | } 42 | 43 | //Create and bind to ViewModel 44 | override fun onStart() { 45 | super.onStart() 46 | viewModel = ViewModelProvider( 47 | this, 48 | LoginInjector(requireActivity().application).provideUserViewModelFactory() 49 | ).get(UserViewModel::class.java) 50 | 51 | //start background anim 52 | (root_fragment_login.background as AnimationDrawable).startWithFade() 53 | 54 | setUpClickListeners() 55 | observeViewModel() 56 | 57 | viewModel.handleEvent(LoginEvent.OnStart) 58 | } 59 | 60 | private fun setUpClickListeners() { 61 | btn_auth_attempt.setOnClickListener { viewModel.handleEvent(LoginEvent.OnAuthButtonClick) } 62 | 63 | imb_toolbar_back.setOnClickListener { startListActivity() } 64 | 65 | requireActivity().onBackPressedDispatcher.addCallback(this) { 66 | startListActivity() 67 | } 68 | 69 | 70 | } 71 | 72 | private fun observeViewModel() { 73 | viewModel.signInStatusText.observe( 74 | viewLifecycleOwner, 75 | Observer { 76 | //"it" is the alue of the MutableLiveData object, which is inferred to be a String automatically 77 | lbl_login_status_display.text = it 78 | } 79 | ) 80 | 81 | viewModel.authButtonText.observe( 82 | viewLifecycleOwner, 83 | Observer { 84 | btn_auth_attempt.text = it 85 | } 86 | ) 87 | 88 | viewModel.startAnimation.observe( 89 | viewLifecycleOwner, 90 | Observer { 91 | imv_antenna_animation.setImageResource( 92 | resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName) 93 | ) 94 | (imv_antenna_animation.drawable as AnimationDrawable).start() 95 | } 96 | ) 97 | 98 | viewModel.authAttempt.observe( 99 | viewLifecycleOwner, 100 | Observer { startSignInFlow() } 101 | ) 102 | 103 | viewModel.satelliteDrawable.observe( 104 | viewLifecycleOwner, 105 | Observer { 106 | imv_antenna_animation.setImageResource( 107 | resources.getIdentifier(it, "drawable", activity?.packageName) 108 | ) 109 | } 110 | ) 111 | } 112 | 113 | private fun startListActivity() = requireActivity().startActivity( 114 | Intent( 115 | activity, 116 | NoteActivity::class.java 117 | ) 118 | ) 119 | 120 | private fun startSignInFlow() { 121 | val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 122 | .requestIdToken(getString(R.string.default_web_client_id)) 123 | .build() 124 | 125 | val googleSignInClient = GoogleSignIn.getClient(requireActivity(), gso) 126 | 127 | val signInIntent = googleSignInClient.signInIntent 128 | startActivityForResult(signInIntent, RC_SIGN_IN) 129 | } 130 | 131 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 132 | super.onActivityResult(requestCode, resultCode, data) 133 | 134 | val task = GoogleSignIn.getSignedInAccountFromIntent(data) 135 | var userToken: String? = null 136 | 137 | try { 138 | val account: GoogleSignInAccount? = task.getResult(ApiException::class.java) 139 | 140 | if (account != null) userToken = account.idToken 141 | } catch (exception: Exception) { 142 | Log.d("LOGIN", exception.toString()) 143 | } 144 | 145 | viewModel.handleEvent( 146 | LoginEvent.OnGoogleSignInResult( 147 | LoginResult( 148 | requestCode, 149 | userToken 150 | ) 151 | ) 152 | ) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/login/UserViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.login 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.* 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.LoginResult 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.User 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.IUserRepository 8 | import kotlinx.coroutines.launch 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | /** 12 | * This approach to ViewModels reduces the complexity of the View by containing specific details about widgets and 13 | * controls present in the View. The benefit of doing so is to make the View in to a Humble Object; reducing or 14 | * eliminating the need to test the View. 15 | * 16 | * The downside of this approach, is that the ViewModel is no longer re-usable across a variety of Views. In this case, 17 | * since this ViewModel is only used by a single View, and the application architecture will not change any time soon, 18 | * losing re-usability in exchange for a simpler View is not a problem. 19 | */ 20 | class UserViewModel( 21 | val repo: IUserRepository, 22 | uiContext: CoroutineContext 23 | ) : BaseViewModel>(uiContext) { 24 | 25 | //The actual data model is kept private to avoid unwanted tampering 26 | private val userState = MutableLiveData() 27 | 28 | //Control Logic 29 | internal val authAttempt = MutableLiveData() 30 | internal val startAnimation = MutableLiveData() 31 | 32 | //UI Binding 33 | internal val signInStatusText = MutableLiveData() 34 | internal val authButtonText = MutableLiveData() 35 | internal val satelliteDrawable = MutableLiveData() 36 | 37 | private fun showErrorState() { 38 | signInStatusText.value = LOGIN_ERROR 39 | authButtonText.value = SIGN_IN 40 | satelliteDrawable.value = ANTENNA_EMPTY 41 | } 42 | 43 | private fun showLoadingState() { 44 | signInStatusText.value = LOADING 45 | satelliteDrawable.value = ANTENNA_LOOP 46 | startAnimation.value = Unit 47 | } 48 | 49 | private fun showSignedInState() { 50 | signInStatusText.value = SIGNED_IN 51 | authButtonText.value = SIGN_OUT 52 | satelliteDrawable.value = ANTENNA_FULL 53 | } 54 | 55 | private fun showSignedOutState() { 56 | signInStatusText.value = SIGNED_OUT 57 | authButtonText.value = SIGN_IN 58 | satelliteDrawable.value = ANTENNA_EMPTY 59 | } 60 | 61 | override fun handleEvent(event: LoginEvent) { 62 | //Trigger loading screen first 63 | showLoadingState() 64 | when (event) { 65 | is LoginEvent.OnStart -> getUser() 66 | is LoginEvent.OnAuthButtonClick -> onAuthButtonClick() 67 | is LoginEvent.OnGoogleSignInResult -> onSignInResult(event.result) 68 | } 69 | } 70 | 71 | private fun getUser() = launch { 72 | val result = repo.getCurrentUser() 73 | when (result) { 74 | is Result.Value -> { 75 | userState.value = result.value 76 | if (result.value == null) showSignedOutState() 77 | else showSignedInState() 78 | } 79 | is Result.Error -> showErrorState() 80 | } 81 | } 82 | 83 | /** 84 | * If user is null, tell the View to begin the authAttempt. Else, attempt to sign the user out 85 | */ 86 | private fun onAuthButtonClick() { 87 | if (userState.value == null) authAttempt.value = Unit 88 | else signOutUser() 89 | } 90 | 91 | private fun onSignInResult(result: LoginResult) = launch { 92 | if (result.requestCode == RC_SIGN_IN && result.userToken != null) { 93 | 94 | val createGoogleUserResult = repo.signInGoogleUser( 95 | result.userToken 96 | ) 97 | 98 | //Result.Value means it was successful 99 | if (createGoogleUserResult is Result.Value) getUser() 100 | else showErrorState() 101 | } else { 102 | showErrorState() 103 | } 104 | } 105 | 106 | private fun signOutUser() = launch { 107 | val result = repo.signOutCurrentUser() 108 | 109 | when (result) { 110 | is Result.Value -> { 111 | userState.value = null 112 | showSignedOutState() 113 | } 114 | is Result.Error -> showErrorState() 115 | } 116 | } 117 | 118 | 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/login/buildlogic/LoginInjector.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.login.buildlogic 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import com.google.firebase.FirebaseApp 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.implementations.FirebaseUserRepoImpl 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.IUserRepository 8 | 9 | class LoginInjector(application: Application): AndroidViewModel(application) { 10 | 11 | init { 12 | FirebaseApp.initializeApp(application) 13 | } 14 | 15 | private fun getUserRepository(): IUserRepository { 16 | return FirebaseUserRepoImpl() 17 | } 18 | 19 | fun provideUserViewModelFactory(): UserViewModelFactory = 20 | UserViewModelFactory( 21 | getUserRepository() 22 | ) 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/login/buildlogic/UserViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.login.buildlogic 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.login.UserViewModel 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.IUserRepository 7 | import kotlinx.coroutines.Dispatchers 8 | 9 | class UserViewModelFactory( 10 | private val userRepo: IUserRepository 11 | ): ViewModelProvider.NewInstanceFactory() { 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | override fun create(modelClass: Class): T { 15 | 16 | return UserViewModel(userRepo, Dispatchers.Main) as T 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/FirebaseNote.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | data class FirebaseNote( 4 | val creationDate: String? = "", 5 | val contents: String? = "", 6 | val upVotes: Int? = 0, 7 | val imageurl: String? = "", 8 | val creator: String? = "" 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/LoginResult.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | /** 4 | * Wrapper class for data recieved in LoginActivity's onActivityResult() 5 | * function 6 | */ 7 | data class LoginResult(val requestCode: Int, val userToken: String?) -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/Note.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | data class Note(val creationDate:String, 4 | val contents:String, 5 | val upVotes: Int, 6 | val imageUrl: String, 7 | val creator: User?) 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/NoteDao.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | import androidx.room.* 4 | 5 | 6 | @Dao 7 | interface NoteDao { 8 | @Query("SELECT * FROM notes") 9 | suspend fun getNotes(): List 10 | 11 | @Query("SELECT * FROM notes WHERE creation_date = :creationDate") 12 | suspend fun getNoteById(creationDate: String): RoomNote 13 | 14 | @Delete 15 | suspend fun deleteNote(note: RoomNote) 16 | 17 | @Insert(onConflict = OnConflictStrategy.REPLACE) 18 | suspend fun insertOrUpdateNote(note: RoomNote): Long 19 | } 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | //@Dao 30 | //interface NoteDao { 31 | // @Query("SELECT * FROM notes") 32 | // suspend fun getNotes(): List 33 | // 34 | // @Query("SELECT * FROM notes WHERE creation_date = :creationDate") 35 | // suspend fun getNoteById(creationDate: String): RoomNote 36 | // 37 | // @Delete 38 | // suspend fun deleteNote(note: RoomNote) 39 | // 40 | // //if update successful, will return number of rows effected, which should be 1 41 | // @Insert(onConflict = OnConflictStrategy.REPLACE) 42 | // suspend fun insertOrUpdateNote(note: RoomNote): Long 43 | //} -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/RoomNote.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | 8 | 9 | @Entity( 10 | tableName = "notes", 11 | indices = [Index("creation_date")] 12 | ) 13 | data class RoomNote( 14 | @PrimaryKey 15 | @ColumnInfo(name = "creation_date") 16 | val creationDate: String, 17 | 18 | @ColumnInfo(name = "contents") 19 | val contents: String, 20 | 21 | @ColumnInfo(name = "up_votes") 22 | val upVotes: Int, 23 | 24 | @ColumnInfo(name = "image_url") 25 | val imageUrl: String, 26 | 27 | @ColumnInfo(name = "creator_id") 28 | val creatorId: String 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/RoomNoteDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | 8 | private const val DATABASE = "notes" 9 | 10 | @Database( 11 | entities = [RoomNote::class], 12 | version = 1, 13 | exportSchema = false 14 | ) 15 | abstract class RoomNoteDatabase : RoomDatabase() { 16 | 17 | abstract fun roomNoteDao(): NoteDao 18 | 19 | //code below courtesy of https://github.com/googlesamples/android-sunflower; it is open 20 | //source just like this application. 21 | companion object { 22 | 23 | // For Singleton instantiation 24 | @Volatile 25 | private var instance: RoomNoteDatabase? = null 26 | 27 | fun getInstance(context: Context): RoomNoteDatabase { 28 | return instance ?: synchronized(this) { 29 | instance 30 | ?: buildDatabase(context).also { instance = it } 31 | } 32 | } 33 | 34 | private fun buildDatabase(context: Context): RoomNoteDatabase { 35 | return Room.databaseBuilder(context, RoomNoteDatabase::class.java, DATABASE) 36 | .build() 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model 2 | 3 | data class User(val uid: String, 4 | val name: String = "") -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/implementations/FirebaseUserRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model.implementations 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import com.google.firebase.auth.GoogleAuthProvider 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.Result 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.awaitTaskCompletable 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.User 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.IUserRepository 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.withContext 11 | 12 | class FirebaseUserRepoImpl(val auth: FirebaseAuth = FirebaseAuth.getInstance()) : IUserRepository { 13 | 14 | override suspend fun signInGoogleUser(idToken: String): 15 | Result = withContext(Dispatchers.IO) { 16 | try { 17 | val credential = GoogleAuthProvider.getCredential(idToken, null) 18 | awaitTaskCompletable(auth.signInWithCredential(credential)) 19 | 20 | Result.build { Unit } 21 | } catch (exception: Exception) { 22 | Result.build { throw exception } 23 | } 24 | 25 | } 26 | 27 | 28 | override suspend fun signOutCurrentUser(): Result { 29 | return Result.build { 30 | auth.signOut() 31 | } 32 | } 33 | 34 | override suspend fun getCurrentUser(): Result { 35 | val firebaseUser = auth.currentUser 36 | 37 | return if (firebaseUser == null) { 38 | Result.build { null } 39 | } else { 40 | Result.build { 41 | User( 42 | firebaseUser.uid, 43 | firebaseUser.displayName ?: "" 44 | ) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/implementations/NoteRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model.implementations 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import com.google.firebase.firestore.FirebaseFirestore 5 | import com.google.firebase.firestore.QuerySnapshot 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.* 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.FirebaseNote 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 9 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.NoteDao 10 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.User 11 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 12 | 13 | private const val COLLECTION_NAME = "notes" 14 | 15 | /** 16 | * If this wasn't a demo project, I would apply more abstraction to this repository (i.e. local and remote would be 17 | * separate interfaces which this class would depend on). I wanted to keep it the back end simple since this app is 18 | * a demo on MVVM, which is a front end architecture pattern. 19 | */ 20 | class NoteRepoImpl( 21 | val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance(), 22 | val remote: FirebaseFirestore = FirebaseFirestore.getInstance(), 23 | val local: NoteDao 24 | ) : INoteRepository { 25 | 26 | 27 | override suspend fun getNoteById(noteId: String): Result { 28 | val user = getActiveUser() 29 | return if (user != null) getRemoteNote(noteId, user) 30 | else getLocalNote(noteId) 31 | } 32 | 33 | override suspend fun deleteNote(note: Note): Result { 34 | val user = getActiveUser() 35 | return if (user != null) deleteRemoteNote(note.copy(creator = user)) 36 | else deleteLocalNote(note) 37 | } 38 | 39 | override suspend fun updateNote(note: Note): Result { 40 | val user = getActiveUser() 41 | return if (user != null) updateRemoteNote(note.copy(creator = user)) 42 | else updateLocalNote(note) 43 | } 44 | 45 | override suspend fun getNotes(): Result> { 46 | val user = getActiveUser() 47 | return if (user != null) getRemoteNotes(user) 48 | else getLocalNotes() 49 | } 50 | 51 | /** 52 | * if currentUser != null, return true 53 | */ 54 | private fun getActiveUser(): User? { 55 | return firebaseAuth.currentUser?.toUser 56 | } 57 | 58 | 59 | private fun resultToNoteList(result: QuerySnapshot?): Result> { 60 | val noteList = mutableListOf() 61 | 62 | result?.forEach { documentSnapshot -> 63 | noteList.add(documentSnapshot.toObject(FirebaseNote::class.java).toNote) 64 | } 65 | 66 | return Result.build { 67 | noteList 68 | } 69 | } 70 | 71 | 72 | /* Remote Datasource */ 73 | 74 | private suspend fun getRemoteNotes(user: User): Result> { 75 | return try { 76 | val task = awaitTaskResult( 77 | remote.collection(COLLECTION_NAME) 78 | .whereEqualTo("creator", user.uid) 79 | .get() 80 | ) 81 | 82 | resultToNoteList(task) 83 | } catch (exception: Exception) { 84 | Result.build { throw exception } 85 | } 86 | } 87 | 88 | private suspend fun getRemoteNote(creationDate: String, user: User): Result { 89 | return try { 90 | val task = awaitTaskResult( 91 | remote.collection(COLLECTION_NAME) 92 | .document(creationDate + user.uid) 93 | .get() 94 | ) 95 | 96 | Result.build { 97 | //Task 98 | task.toObject(FirebaseNote::class.java)?.toNote ?: throw Exception() 99 | } 100 | } catch (exception: Exception) { 101 | Result.build { throw exception } 102 | } 103 | } 104 | 105 | private suspend fun deleteRemoteNote(note: Note): Result = Result.build { 106 | awaitTaskCompletable( 107 | remote.collection(COLLECTION_NAME) 108 | .document(note.creationDate + note.creator!!.uid) 109 | .delete() 110 | ) 111 | } 112 | 113 | /** 114 | * Notes are stored with the following composite document name: 115 | * note.creationDate + note.creator.uid 116 | * The reason for this, is that if I just used the creationDate, hypothetically two users 117 | * creating a note at the same time, would have duplicate entries in the cloud database :( 118 | */ 119 | private suspend fun updateRemoteNote(note: Note): Result { 120 | return try { 121 | awaitTaskCompletable( 122 | remote.collection(COLLECTION_NAME) 123 | .document(note.creationDate + note.creator!!.uid) 124 | .set(note.toFirebaseNote) 125 | ) 126 | 127 | Result.build { Unit } 128 | 129 | } catch (exception: Exception) { 130 | Result.build { throw exception } 131 | } 132 | } 133 | 134 | /* Local Datasource */ 135 | private suspend fun getLocalNotes(): Result> = Result.build { 136 | local.getNotes().toNoteListFromRoomNote() 137 | } 138 | 139 | private suspend fun getLocalNote(id: String): Result = Result.build { 140 | local.getNoteById(id).toNote 141 | } 142 | 143 | private suspend fun deleteLocalNote(note: Note): Result = Result.build { 144 | local.deleteNote(note.toRoomNote) 145 | Unit 146 | } 147 | 148 | private suspend fun updateLocalNote(note: Note): Result = Result.build { 149 | local.insertOrUpdateNote(note.toRoomNote) 150 | Unit 151 | } 152 | 153 | 154 | 155 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/repository/INoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model.repository 2 | 3 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.Result 4 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 5 | 6 | interface INoteRepository { 7 | suspend fun getNoteById(noteId: String): Result 8 | suspend fun getNotes(): Result> 9 | suspend fun deleteNote(note: Note): Result 10 | suspend fun updateNote(note: Note): Result 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/model/repository/IUserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.model.repository 2 | 3 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.Result 4 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.User 5 | 6 | interface IUserRepository { 7 | suspend fun getCurrentUser(): Result 8 | 9 | suspend fun signOutCurrentUser(): Result 10 | 11 | suspend fun signInGoogleUser(idToken: String): Result 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/NoteActivity.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.navigation.NavController 6 | import androidx.navigation.Navigation 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.R 8 | 9 | class NoteActivity : AppCompatActivity() { 10 | 11 | private lateinit var nav: NavController 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_note) 16 | 17 | nav = Navigation.findNavController(this, R.id.fragment_nav) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/NoteListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.BaseViewModel 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.GET_NOTES_ERROR 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.Result 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 9 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 10 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist.NoteListEvent 11 | import kotlinx.coroutines.launch 12 | import kotlin.coroutines.CoroutineContext 13 | 14 | class NoteListViewModel( 15 | val noteRepo: INoteRepository, 16 | uiContext: CoroutineContext 17 | ) : BaseViewModel(uiContext) { 18 | 19 | private val noteListState = MutableLiveData>() 20 | val noteList: LiveData> get() = noteListState 21 | 22 | private val editNoteState = MutableLiveData() 23 | val editNote: LiveData get() = editNoteState 24 | 25 | 26 | override fun handleEvent(event: NoteListEvent) { 27 | when (event) { 28 | is NoteListEvent.OnStart -> getNotes() 29 | is NoteListEvent.OnNoteItemClick -> editNote(event.position) 30 | } 31 | } 32 | 33 | private fun editNote(position: Int) { 34 | editNoteState.value = noteList.value!![position].creationDate 35 | } 36 | 37 | private fun getNotes() = launch { 38 | val notesResult = noteRepo.getNotes() 39 | 40 | when (notesResult) { 41 | is Result.Value -> noteListState.value = notesResult.value 42 | is Result.Error -> errorState.value = GET_NOTES_ERROR 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/NoteListViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/NoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.BaseViewModel 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.GET_NOTE_ERROR 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.Result 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 9 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 10 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.notedetail.NoteDetailEvent 11 | import kotlinx.coroutines.launch 12 | import java.text.SimpleDateFormat 13 | import java.util.* 14 | import kotlin.coroutines.CoroutineContext 15 | 16 | class NoteViewModel( 17 | val noteRepo: INoteRepository, 18 | uiContext: CoroutineContext 19 | ) : BaseViewModel(uiContext) { 20 | 21 | private val noteState = MutableLiveData() 22 | val note: LiveData get() = noteState 23 | 24 | private val deletedState = MutableLiveData() 25 | val deleted: LiveData get() = deletedState 26 | 27 | private val updatedState = MutableLiveData() 28 | val updated: LiveData get() = updatedState 29 | 30 | override fun handleEvent(event: NoteDetailEvent) { 31 | when (event) { 32 | is NoteDetailEvent.OnStart -> getNote(event.noteId) 33 | is NoteDetailEvent.OnDeleteClick -> onDelete() 34 | is NoteDetailEvent.OnDoneClick -> updateNote(event.contents) 35 | } 36 | } 37 | 38 | private fun onDelete() = launch { 39 | val deleteResult = noteRepo.deleteNote(note.value!!) 40 | 41 | when (deleteResult) { 42 | is Result.Value -> deletedState.value = true 43 | is Result.Error -> deletedState.value = false 44 | } 45 | } 46 | 47 | 48 | private fun updateNote(contents: String) = launch { 49 | val updateResult = noteRepo.updateNote( 50 | note.value!! 51 | .copy(contents = contents) 52 | ) 53 | 54 | when (updateResult) { 55 | is Result.Value -> updatedState.value = true 56 | is Result.Error -> updatedState.value = false 57 | } 58 | } 59 | 60 | private fun getNote(noteId: String) = launch { 61 | if (noteId == "") newNote() 62 | else { 63 | val noteResult = noteRepo.getNoteById(noteId) 64 | 65 | when (noteResult) { 66 | is Result.Value -> noteState.value = noteResult.value 67 | is Result.Error -> errorState.value = GET_NOTE_ERROR 68 | } 69 | } 70 | } 71 | 72 | private fun newNote() { 73 | noteState.value = 74 | Note(getCalendarTime(), "", 0, "rocket_loop", null) 75 | } 76 | 77 | 78 | private fun getCalendarTime(): String { 79 | val cal = Calendar.getInstance(TimeZone.getDefault()) 80 | val format = SimpleDateFormat("d MMM yyyy HH:mm:ss Z") 81 | format.timeZone = cal.timeZone 82 | return format.format(cal.time) 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/NoteViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notedetail/NoteDetailEvent.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notedetail 2 | 3 | sealed class NoteDetailEvent { 4 | data class OnDoneClick(val contents: String) : NoteDetailEvent() 5 | object OnDeleteClick : NoteDetailEvent() 6 | object OnDeleteConfirmed : NoteDetailEvent() 7 | data class OnStart(val noteId: String) : NoteDetailEvent() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notedetail/NoteDetailView.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notedetail 2 | 3 | import android.graphics.drawable.AnimationDrawable 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.activity.OnBackPressedCallback 9 | import androidx.activity.addCallback 10 | import androidx.fragment.app.Fragment 11 | import androidx.lifecycle.Observer 12 | import androidx.lifecycle.ViewModelProvider 13 | import androidx.lifecycle.ViewModelProviders 14 | import androidx.navigation.fragment.findNavController 15 | import com.wiseassblog.jetpacknotesmvvmkotlin.R 16 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.makeToast 17 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.startWithFade 18 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.toEditable 19 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.NoteViewModel 20 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.notedetail.buildlogic.NoteDetailInjector 21 | import kotlinx.android.synthetic.main.fragment_note_detail.* 22 | 23 | class NoteDetailView : Fragment() { 24 | 25 | private lateinit var viewModel: NoteViewModel 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, container: ViewGroup?, 29 | savedInstanceState: Bundle? 30 | ): View? { 31 | // Inflate the layout for this fragment 32 | return inflater.inflate(R.layout.fragment_note_detail, container, false) 33 | } 34 | 35 | override fun onStart() { 36 | super.onStart() 37 | 38 | viewModel = ViewModelProvider( 39 | this, 40 | NoteDetailInjector(requireActivity().application).provideNoteViewModelFactory() 41 | ).get( 42 | NoteViewModel::class.java 43 | ) 44 | 45 | showLoadingState() 46 | 47 | imb_toolbar_done.setOnClickListener { 48 | viewModel.handleEvent( 49 | NoteDetailEvent.OnDoneClick( 50 | edt_note_detail_text.text.toString() 51 | ) 52 | ) 53 | } 54 | 55 | imb_toolbar_delete.setOnClickListener { viewModel.handleEvent(NoteDetailEvent.OnDeleteClick) } 56 | 57 | observeViewModel() 58 | 59 | (frag_note_detail.background as AnimationDrawable).startWithFade() 60 | 61 | viewModel.handleEvent( 62 | NoteDetailEvent.OnStart( 63 | //note NoteDetailViewArgs is genereted via Navigation component 64 | NoteDetailViewArgs.fromBundle(arguments!!).noteId 65 | ) 66 | ) 67 | } 68 | 69 | private fun observeViewModel() { 70 | viewModel.error.observe( 71 | viewLifecycleOwner, 72 | Observer { errorMessage -> 73 | showErrorState(errorMessage) 74 | } 75 | ) 76 | 77 | viewModel.note.observe( 78 | viewLifecycleOwner, 79 | Observer { note -> 80 | edt_note_detail_text.text = note.contents.toEditable() 81 | } 82 | ) 83 | 84 | viewModel.updated.observe( 85 | viewLifecycleOwner, 86 | Observer { 87 | findNavController().navigate(R.id.noteListView) 88 | } 89 | ) 90 | 91 | viewModel.deleted.observe( 92 | viewLifecycleOwner, 93 | Observer { 94 | findNavController().navigate(R.id.noteListView) 95 | } 96 | ) 97 | 98 | requireActivity().onBackPressedDispatcher.addCallback(this) { 99 | findNavController().navigate(R.id.noteListView) 100 | } 101 | } 102 | 103 | private fun showErrorState(errorMessage: String?) { 104 | makeToast(errorMessage!!) 105 | findNavController().navigate(R.id.noteListView) 106 | } 107 | 108 | private fun showLoadingState() { 109 | (imv_note_detail_satellite.drawable as AnimationDrawable).start() 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notedetail/buildlogic/NoteDetailInjector.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notedetail.buildlogic 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.lifecycle.AndroidViewModel 6 | import com.google.firebase.FirebaseApp 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.RoomNoteDatabase 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.implementations.NoteRepoImpl 9 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 10 | 11 | class NoteDetailInjector(application: Application): AndroidViewModel(application) { 12 | 13 | private fun getNoteRepository(): INoteRepository { 14 | 15 | FirebaseApp.initializeApp(getApplication()) 16 | return NoteRepoImpl( 17 | local = RoomNoteDatabase.getInstance(getApplication()).roomNoteDao() 18 | ) 19 | } 20 | 21 | fun provideNoteViewModelFactory(): NoteViewModelFactory = 22 | NoteViewModelFactory( 23 | getNoteRepository() 24 | ) 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notedetail/buildlogic/NoteViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notedetail.buildlogic 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.NoteViewModel 7 | import kotlinx.coroutines.Dispatchers 8 | 9 | class NoteViewModelFactory( 10 | private val noteRepo: INoteRepository 11 | ) : ViewModelProvider.NewInstanceFactory() { 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | override fun create(modelClass: Class): T { 15 | 16 | return NoteViewModel(noteRepo, Dispatchers.Main) as T 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notelist/NoteDiffUtilCallback.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist 2 | 3 | 4 | import androidx.recyclerview.widget.DiffUtil 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 6 | 7 | class NoteDiffUtilCallback : DiffUtil.ItemCallback(){ 8 | override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean { 9 | return oldItem.creationDate == newItem.creationDate 10 | } 11 | 12 | override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean { 13 | return oldItem.creationDate == newItem.creationDate 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notelist/NoteListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist 2 | 3 | 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.lifecycle.MutableLiveData 10 | import androidx.recyclerview.widget.ListAdapter 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.wiseassblog.jetpacknotesmvvmkotlin.R 13 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.Note 14 | import kotlinx.android.synthetic.main.item_note.view.* 15 | 16 | class NoteListAdapter(val event:MutableLiveData = MutableLiveData()): ListAdapter(NoteDiffUtilCallback()){ 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder { 18 | val inflater = LayoutInflater.from(parent.context) 19 | 20 | return NoteViewHolder( 21 | inflater.inflate(R.layout.item_note, parent, false) 22 | ) 23 | } 24 | 25 | override fun onBindViewHolder(holder: NoteViewHolder, position: Int) { 26 | getItem(position).let { note -> 27 | holder.content.text = note.contents 28 | holder.date.text = note.creationDate 29 | 30 | holder.itemView.setOnClickListener { 31 | event.value = NoteListEvent.OnNoteItemClick(position) 32 | } 33 | } 34 | } 35 | 36 | 37 | class NoteViewHolder(root: View): RecyclerView.ViewHolder(root){ 38 | var content: TextView = root.lbl_message 39 | var date: TextView = root.lbl_date_and_time 40 | } 41 | } 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notelist/NoteListEvent.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist 2 | 3 | sealed class NoteListEvent { 4 | data class OnNoteItemClick(val position: Int) : NoteListEvent() 5 | object OnNewNoteClick : NoteListEvent() 6 | object OnStart : NoteListEvent() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notelist/NoteListView.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist 2 | 3 | import android.graphics.drawable.AnimationDrawable 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.lifecycle.Observer 10 | import androidx.lifecycle.ViewModelProvider 11 | import androidx.lifecycle.ViewModelProviders 12 | import androidx.navigation.fragment.findNavController 13 | import com.wiseassblog.jetpacknotesmvvmkotlin.R 14 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.makeToast 15 | import com.wiseassblog.jetpacknotesmvvmkotlin.common.startWithFade 16 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.NoteListViewModel 17 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist.buildlogic.NoteListInjector 18 | import kotlinx.android.synthetic.main.fragment_note_list.* 19 | 20 | class NoteListView : Fragment() { 21 | 22 | private lateinit var viewModel: NoteListViewModel 23 | private lateinit var adapter: NoteListAdapter 24 | 25 | override fun onCreateView( 26 | inflater: LayoutInflater, container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View? { 29 | // Inflate the layout for this fragment 30 | return inflater.inflate(R.layout.fragment_note_list, container, false) 31 | } 32 | 33 | override fun onDestroyView() { 34 | super.onDestroyView() 35 | //THIS IS IMPORTANT!!! 36 | rec_list_fragment.adapter = null 37 | } 38 | 39 | override fun onStart() { 40 | super.onStart() 41 | viewModel = ViewModelProvider( 42 | this, 43 | NoteListInjector(requireActivity().application).provideNoteListViewModelFactory() 44 | ).get( 45 | NoteListViewModel::class.java 46 | ) 47 | 48 | (imv_space_background.drawable as AnimationDrawable).startWithFade() 49 | 50 | showLoadingState() 51 | setUpAdapter() 52 | observeViewModel() 53 | 54 | fab_create_new_item.setOnClickListener { 55 | val direction = NoteListViewDirections.actionNoteListViewToNoteDetailView("") 56 | findNavController().navigate(direction) 57 | } 58 | 59 | imv_toolbar_auth.setOnClickListener { 60 | findNavController().navigate(R.id.loginView) 61 | } 62 | 63 | viewModel.handleEvent( 64 | NoteListEvent.OnStart 65 | ) 66 | } 67 | 68 | private fun setUpAdapter() { 69 | adapter = NoteListAdapter() 70 | adapter.event.observe( 71 | viewLifecycleOwner, 72 | Observer { 73 | viewModel.handleEvent(it) 74 | } 75 | ) 76 | 77 | rec_list_fragment.adapter = adapter 78 | } 79 | 80 | private fun observeViewModel() { 81 | viewModel.error.observe( 82 | viewLifecycleOwner, 83 | Observer { errorMessage -> 84 | showErrorState(errorMessage) 85 | } 86 | ) 87 | 88 | viewModel.noteList.observe( 89 | viewLifecycleOwner, 90 | Observer { noteList -> 91 | adapter.submitList(noteList) 92 | 93 | if (noteList.isNotEmpty()) { 94 | (imv_satellite_animation.drawable as AnimationDrawable).stop() 95 | imv_satellite_animation.visibility = View.INVISIBLE 96 | rec_list_fragment.visibility = View.VISIBLE 97 | } 98 | } 99 | ) 100 | 101 | viewModel.editNote.observe( 102 | viewLifecycleOwner, 103 | Observer { noteId -> 104 | startNoteDetailWithArgs(noteId) 105 | } 106 | ) 107 | } 108 | 109 | private fun startNoteDetailWithArgs(noteId: String) = findNavController().navigate( 110 | NoteListViewDirections.actionNoteListViewToNoteDetailView(noteId) 111 | ) 112 | 113 | 114 | private fun showErrorState(errorMessage: String?) = makeToast(errorMessage!!) 115 | 116 | 117 | private fun showLoadingState() = (imv_satellite_animation.drawable as AnimationDrawable).start() 118 | 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notelist/buildlogic/NoteListInjector.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist.buildlogic 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import com.google.firebase.FirebaseApp 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.RoomNoteDatabase 7 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.implementations.NoteRepoImpl 8 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 9 | 10 | class NoteListInjector(application:Application): AndroidViewModel(application) { 11 | private fun getNoteRepository(): INoteRepository { 12 | FirebaseApp.initializeApp(getApplication()) 13 | return NoteRepoImpl( 14 | local = RoomNoteDatabase.getInstance(getApplication()).roomNoteDao() 15 | ) 16 | } 17 | 18 | fun provideNoteListViewModelFactory(): NoteListViewModelFactory = 19 | NoteListViewModelFactory( 20 | getNoteRepository() 21 | ) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wiseassblog/jetpacknotesmvvmkotlin/note/notelist/buildlogic/NoteListViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.wiseassblog.jetpacknotesmvvmkotlin.note.notelist.buildlogic 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.wiseassblog.jetpacknotesmvvmkotlin.model.repository.INoteRepository 6 | import com.wiseassblog.jetpacknotesmvvmkotlin.note.NoteListViewModel 7 | import kotlinx.coroutines.Dispatchers 8 | 9 | class NoteListViewModelFactory( 10 | private val noteRepo: INoteRepository 11 | ) : ViewModelProvider.NewInstanceFactory() { 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | override fun create(modelClass: Class): T { 15 | 16 | return NoteListViewModel(noteRepo, Dispatchers.Main) as T 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-land/space_bg_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable-land/space_bg_one.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-land/space_bg_three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable-land/space_bg_three.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-land/space_bg_two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable-land/space_bg_two.png -------------------------------------------------------------------------------- /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/antenna_loop.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/antenna_loop_fast.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_access_time_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_antenna_empty.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_antenna_full.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_antenna_half.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_add_24px.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_event_24px.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_forever_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/drawable/ic_visibility_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visibility_off_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_vpn_key_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/im_rocket_one.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/im_rocket_three.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/im_rocket_two.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rocket_loop.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rocket_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable/rocket_one.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/space_bg_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable/space_bg_one.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/space_bg_three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable/space_bg_three.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/space_bg_two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BracketCove/JetpackNotesMvvmKotlin/511cdfa91ac82dbf1b3479feb90e1035e88c173c/app/src/main/res/drawable/space_bg_two.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/space_loop.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_note.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 19 | 28 | 36 | 45 |