Repository: BracketCove/SpaceNotes Branch: master Commit: d799560fbb94 Files: 115 Total size: 230.6 KB Directory structure: gitextract_qf805k6a/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── wiseassblog/ │ │ │ └── spacenotes/ │ │ │ ├── SpaceNotes.kt │ │ │ ├── common/ │ │ │ │ ├── AndroidExt.kt │ │ │ │ ├── BaseLogic.kt │ │ │ │ ├── Constants.kt │ │ │ │ └── Navigation.kt │ │ │ ├── login/ │ │ │ │ ├── ILoginContract.kt │ │ │ │ ├── LoginActivity.kt │ │ │ │ ├── LoginLogic.kt │ │ │ │ ├── LoginResult.kt │ │ │ │ └── buildlogic/ │ │ │ │ └── LoginInjector.kt │ │ │ ├── notedetail/ │ │ │ │ ├── INoteDetailContract.kt │ │ │ │ ├── NoteDetailActivity.kt │ │ │ │ ├── NoteDetailLogic.kt │ │ │ │ ├── NoteDetailView.kt │ │ │ │ ├── NoteDetailViewModel.kt │ │ │ │ └── buildlogic/ │ │ │ │ └── NoteDetailInjector.kt │ │ │ └── notelist/ │ │ │ ├── INoteListContract.kt │ │ │ ├── NoteDiffUtilCallback.kt │ │ │ ├── NoteListActivity.kt │ │ │ ├── NoteListAdapter.kt │ │ │ ├── NoteListLogic.kt │ │ │ ├── NoteListView.kt │ │ │ ├── NoteListViewModel.kt │ │ │ └── buildlogic/ │ │ │ └── NoteListInjector.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── antenna_loop.xml │ │ │ ├── antenna_loop_fast.xml │ │ │ ├── ic_access_time_black_24dp.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_off_black_24dp.xml │ │ │ ├── ic_vpn_key_black_24dp.xml │ │ │ ├── satellite_beam.xml │ │ │ └── space_loop.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_login.xml │ │ │ ├── activity_note_detail.xml │ │ │ ├── activity_note_list.xml │ │ │ ├── activity_user_auth.xml │ │ │ ├── fragment_note_detail.xml │ │ │ ├── fragment_note_list.xml │ │ │ └── item_note.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── view_styles.xml │ └── test/ │ └── java/ │ └── com/ │ └── wiseassblog/ │ └── spacenotes/ │ ├── LoginLogicTest.kt │ ├── NoteDetailLogicTest.kt │ └── NoteListLogicTest.kt ├── build.gradle ├── data/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── wiseassblog/ │ │ │ └── data/ │ │ │ ├── DataExt.kt │ │ │ ├── auth/ │ │ │ │ └── FirebaseAuthRepositoryImpl.kt │ │ │ ├── datamodels/ │ │ │ │ ├── AnonymousRoomNote.kt │ │ │ │ ├── FirebaseNote.kt │ │ │ │ ├── RegisteredRoomNote.kt │ │ │ │ └── RegisteredRoomTransaction.kt │ │ │ ├── note/ │ │ │ │ ├── anonymous/ │ │ │ │ │ ├── AnonymousNoteDao.kt │ │ │ │ │ ├── RoomAnonymousNoteDatabase.kt │ │ │ │ │ └── RoomLocalAnonymousRepositoryImpl.kt │ │ │ │ ├── public/ │ │ │ │ │ └── FirestorePublicNoteRepositoryImpl.kt │ │ │ │ └── registered/ │ │ │ │ ├── FirestorePrivateRemoteNoteImpl.kt │ │ │ │ ├── RegisteredNoteDao.kt │ │ │ │ ├── RegisteredNoteRepositoryImpl.kt │ │ │ │ ├── RegisteredTransactionDao.kt │ │ │ │ ├── RoomLocalCacheImpl.kt │ │ │ │ └── RoomRegisteredNoteDatabase.kt │ │ │ └── transaction/ │ │ │ ├── RoomRegisteredTransactionDatabase.kt │ │ │ └── RoomTransactionRepositoryImpl.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── wiseassblog/ │ └── data/ │ ├── ExtTest.kt │ └── RegisteredNoteRepositoryTest.kt ├── domain/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── wiseassblog/ │ │ └── domain/ │ │ ├── DispatcherProvider.kt │ │ ├── domainmodel/ │ │ │ ├── Note.kt │ │ │ ├── NoteTransaction.kt │ │ │ ├── Result.kt │ │ │ └── User.kt │ │ ├── error/ │ │ │ └── SpaceNotesError.kt │ │ ├── interactor/ │ │ │ ├── AnonymousNoteSource.kt │ │ │ ├── AuthSource.kt │ │ │ ├── PublicNoteSource.kt │ │ │ └── RegisteredNoteSource.kt │ │ ├── repository/ │ │ │ ├── IAuthRepository.kt │ │ │ ├── ILocalNoteRepository.kt │ │ │ ├── IPublicNoteRepository.kt │ │ │ ├── IRemoteNoteRepository.kt │ │ │ └── ITransactionRepository.kt │ │ └── servicelocator/ │ │ ├── NoteServiceLocator.kt │ │ └── UserServiceLocator.kt │ └── test/ │ └── java/ │ └── com/ │ └── wiseassblog/ │ └── domain/ │ ├── AnonymousNoteSourceTest.kt │ ├── PublicNoteSourceTest.kt │ └── RegisteredNoteSourceTest.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── versions.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/libraries /.idea/modules.xml /.idea/workspace.xml .DS_Store /build /captures .externalNativeBuild ================================================ FILE: README.md ================================================ ### Please Note: - Since I see people are still looking at this repo, I want to be clear that I no longer recommend multi-module (i.e. multiple gradle subprojects) architecture unless you actually have a good reason for it. In this case, it really just adds extra complexity for no real benefit. However, multi-platform projects would be a great example of a situation where you'd want to use multi-module. # SpaceNotes New to Kotlin? Whether you are a seasoned Java veteran, or you're just starting out with Kotlin for Android, consider checking out [Application Programming Fundamentals w/ Kotlin](https://www.udemy.com/application-programming-fundamentals-with-kotlin/learn/v4/overview). If you like my videos and repositories, I think you'll really enjoy a more polished course experience from wiseAss! ## What is SpaceNotes? SpaceNotes is a Kotlin based Android Application, which was built with best practices an innovation in mind. The app uses Coroutines for concurrency and cross-module/boundary communication, a Clean Domain Layer to allow the application to work properly across multiple platforms, and a few of my favourite APIs from Android Architecture Component and Firebase. ## Software Architecture ### Feature Specific (Front End Android): #### NoteList Feature Note List This feature displays whatever Notes are currently available based on the user's status, such as: Anonymous, Registered Private, Registered Public * INoteListContract specifies the different interactions between classes and the events which may occur in this particular feature * NoteListActivity is a feature level “container”, within which these different things are deployed in to (it is also the entry point of the feature) * NoteListLogic is the “decision maker” of the feature, which handles the events and interactions specified in the contract (this kind of class is the most important to test) * NoteListView contains logic and bindings to the user interface * NoteListAdapter contains a decoupled RecyclerView.ListAdapter w/ DiffUtil * NoteListViewModel contains the most recent data which has been returned from the “backend” of the application (or data which is passed into the feature via navigation), and persists this data so that the logic class or view does not need to (if they did, it would break the separation of concerns) * NoteListInjector: Build logic (Dpependency Injection Implementation) for this feature. #### NoteDetail Feature Note Detail This feature allows the User to view, update, create, and delete a Note. Data is stored in various local/remote datasources based on whether the user is or isn't logged in, and if they are in public or private mode. * INoteDetailContract specifies the different interactions between classes and the events which may occur in this particular feature * NoteDetailActivity is a feature level “container”, within which these different things are deployed in to (it is also the entry point of the feature) * NoteDetailLogic is the “decision maker” of the feature, which handles the events and interactions specified in the contract (this kind of class is the most important to test) * NoteDetailView contains logic and bindings to the user interface * NoteDetailViewModel contains the most recent data which has been returned from the “backend” of the application (or data which is passed into the feature via navigation), and persists this data so that the logic class or view does not need to (if they did, it would break the separation of concerns) * NoteDetailInjector: Build logic (Dpependency Injection Implementation) for this feature. #### Login Feature Login This feature allows the User to authenticate with GoogleSignIn; which is currently the only supported sign in function. No passwords or in-app Sign up is required. **Note:** I normally advocate against using Activities as Views, but I ran in to a tight-coupling problem with GoogleSignIn API (which requires you to override Activity.onActivityResult(...). Given this tight coupling, and the simplicity of this feature (it only has two buttons including the toolbar), I decided to just use the Activity as a pragmatic decision. * ILoginContract specifies the different interactions between classes and the events which may occur in this particular feature * LoginActivity acts as the View and Container in this feature (for reasons mentioned above) * LoginLogic is the “decision maker” of the feature, which handles the events and interactions specified in the contract (this kind of class is the most important to test) * LoginResult Wrapper for when GoogleSignInProviders does it's thing (logging a User In) * LoginInjector: Build logic (Dpependency Injection Implementation) for this feature. #### Common: * Navigation.kt: Contains Top-level functions for starting each feature with the appropriate arguments. * Constants.kt: Contains messages and keys for front end Android * BaseLogic.kt: Abstract class for Logic classes. Could be optimized, currently just contains a DispatcherProvider (for Coroutines) as a property, and a Job object for keeping track and disposing in-flight coroutines. * AndroidExt.kt: Some handy Extensions functions for front end Android ### Domain: The Domain Layer of this application has three primary purposes: * Abstraction of the Data Layer of the application * Providing a common, 3rd party library free set of Models (such as Note.kt) for different platforms of the App * Providing a High-Level description of the applications primary functions based on problem domain analysis (such as User Stories) #### Packages: * domainmodel: POKOs (Plain Old Kotlin Objects) to be shared as a common model between different modules * error: Sealed Class which contains application specific errors * interactor: Interactors exist to coordinate the back end data sources. This is generally only necessary when there is more than one back data source, otherwise it ends up being another unnecessary layer of abstraction over the repository. * repository: Repository Interfaces which dictate the contractual obligations of each part of the back end. This allows the domain layer to coordinate the different back end components without needing to know their real implementations/libraries/dependencies. * servicelocator: Service Locator promotes Functional Purity of the Interactor's functions, and acts as a method of Dependency Injection (by providing the dependencies as arguments to functions) * DispatcherProvider: Used for Coroutines Implementation ### Data (Android Back End): The Data Layer of this application contains implementations of the data sources which are described in the repository package of the domain layer. #### Auth: Currently implemented with FirebaseAuth; manages user authentication. #### Data Models: API specific data models, which are mapped from/to domain models. Each data model is created for a particular API, such as Firestore and Room. #### Note: Implementations for the anonymous, registered private, and registered public data sources which persist Note objects. #### Transaction: Transaction is only used for registered private users. It's purpose is to store offline transactions that the user makes to their registered repository, and attempts to push those transactions when the user reconnects to the remote firestore database. #### DataExt.kt: Contains all of the obnoxious but necessary Data Model Mapping functions, and some Coroutine wrappers over Firebase/GMS Tasks API ## Can I use code from this Repo? Absolutely, pursuant to the project's [LICENSE](LICENSE.md). That being said, the logo and name are my intellectual creations, so don't use them unless you are linking/reffering to this Repo. Follow the rules in the license, and you're good. ## Architecture Style: This project uses Model-View-Whatever. It is the software architecture that has no particular style, yet accommodates all situations. In a less Zen way of speaking, I don't follow MVP, MVC, or MVVM strictly. I use parts of all styles of architectures based on whatever feature I'm creating, and that is what dictates the ultimate architecture of a given feature. If you want me to explain in slightly more familiar terms, I basically apply MVP + VM as a front-end session datastore (such as arguments passed in from Activity Intents, current user states, current note to be displayed). I don't use ViewModels as Decision Maker Classes, hence the Logic class. ## Contact/Support me: Follow the wiseAss Community: https://www.instagram.com/wiseassbrand/ https://www.facebook.com/wiseassblog/ https://twitter.com/wiseass301 http://wiseassblog.com/ https://www.linkedin.com/in/ryan-kay-808388114 Support wiseAss here: https://www.paypal.me/ryanmkay ## License * Copyright 2016, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion build_versions.target_sdk defaultConfig { applicationId "com.wiseassblog.spacenotes" minSdkVersion build_versions.min_sdk targetSdkVersion build_versions.target_sdk versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } buildTypes { debug { testCoverageEnabled = true minifyEnabled false } release { testCoverageEnabled = false minifyEnabled true } } testOptions.unitTests.all { useJUnitPlatform() testLogging { events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' } } } dependencies { implementation project(":domain") implementation project(":data") //Dependencies implementation deps.android.constraint_layout implementation deps.android.lifecycle_extensions implementation deps.android.ktx_fragment implementation deps.android.fragment implementation deps.android.appcompat implementation deps.android.recyclerview implementation deps.android.design implementation deps.room.runtime implementation deps.firebase.auth implementation deps.firebase.firestore implementation deps.play_services.auth implementation deps.kotlin.kotlin_jre implementation deps.kotlin.coroutines_core implementation deps.kotlin.coroutines_android kapt deps.room.compiler testImplementation deps.test.junit testRuntimeOnly deps.test.jupiter_engine testRuntimeOnly deps.test.vintage_engine testImplementation deps.test.mockk testImplementation deps.test.kotlin_junit } apply plugin: 'com.google.gms.google-services' ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/SpaceNotes.kt ================================================ package com.wiseassblog.spacenotes import android.app.Application class SpaceNotes: Application() { override fun onCreate() { super.onCreate() } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/common/AndroidExt.kt ================================================ package com.wiseassblog.spacenotes.common import android.app.Activity import android.content.Intent import android.text.Editable import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.wiseassblog.spacenotes.notedetail.NoteDetailActivity import com.wiseassblog.spacenotes.notedetail.NoteDetailView import com.wiseassblog.spacenotes.notelist.NoteListActivity import java.text.SimpleDateFormat import java.util.* internal fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) internal fun Activity.attachFragment(manager: FragmentManager, containerId: Int, view: Fragment, tag: String) { manager.beginTransaction() .replace(containerId, view, tag) .commitNowAllowingStateLoss() } internal fun Fragment.getCalendarTime(): String { val cal = Calendar.getInstance(TimeZone.getDefault()) val format = SimpleDateFormat("d MMM yyyy HH:mm:ss Z") format.timeZone = cal.timeZone return format.format(cal.time) } internal fun Fragment.makeToast(value: String) { Toast.makeText(activity, value, Toast.LENGTH_SHORT).show() } internal fun Fragment.restartCurrentFeature() { val i: Intent when (this) { is NoteDetailView -> { i = Intent(this.activity, NoteDetailActivity::class.java) } //To Be Added else -> { i = Intent(this.activity, NoteListActivity::class.java) } } this.activity?.finish() startActivity(i) } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/common/BaseLogic.kt ================================================ package com.wiseassblog.spacenotes.common import com.wiseassblog.domain.DispatcherProvider import kotlinx.coroutines.Job /** * Why use a base class? To both share implementation (properties and functions), and enforce a contract (interface) for all listener classes */ abstract class BaseLogic(val dispatcher: DispatcherProvider) { protected lateinit var jobTracker: Job } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/common/Constants.kt ================================================ package com.wiseassblog.spacenotes.common internal const val MESSAGE_DELETE_SUCCESSFUL = "Note successfully deleted." internal const val MESSAGE_DELETE = "DELETE" internal const val MESSAGE_DELETE_CONFIRMATION = "Delete note permanently?" internal const val MESSAGE_GENERIC_ERROR = "An error has occured." internal const val MESSAGE_LOGIN = "Log in to use public mode." internal const val BOOLEAN_EXTRA_IS_PRIVATE = "BOOLEAN_EXTRA_IS_PRIVATE" internal const val STRING_EXTRA_NOTE_ID = "STRING_EXTRA_NOTE_ID" internal const val MODE_PRIVATE = "Private Notes" internal const val MODE_PUBLIC = "Public Notes" ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/common/Navigation.kt ================================================ package com.wiseassblog.spacenotes.common import android.app.Activity import android.content.Intent import com.wiseassblog.spacenotes.login.LoginActivity import com.wiseassblog.spacenotes.notedetail.NoteDetailActivity import com.wiseassblog.spacenotes.notelist.NoteListActivity internal fun startListFeature(activity: Activity?) { activity?.startActivity( Intent( activity, NoteListActivity::class.java ) ).also { activity?.finish() } } internal fun startNoteDetailFeatureWithExtras(activity: Activity?, noteId: String, isPrivate: Boolean) { val i = Intent(activity, NoteDetailActivity::class.java) i.putExtra(STRING_EXTRA_NOTE_ID, noteId) i.putExtra(BOOLEAN_EXTRA_IS_PRIVATE, isPrivate) activity?.startActivity(i) } internal fun startLoginFeature(activity: Activity?) { val i = Intent(activity, LoginActivity::class.java) activity?.startActivity(i) .also { activity?.finish() } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/login/ILoginContract.kt ================================================ package com.wiseassblog.spacenotes.login import androidx.lifecycle.Observer interface ILoginContract { interface View { fun setLoginStatus(text: String) fun setAuthButton(text: String) fun showLoopAnimation() fun setStatusDrawable(imageURL: String) fun startSignInFlow() fun setObserver(observer: Observer>) fun startListFeature() } interface Logic { fun event(event: LoginEvent) } } internal const val SIGN_OUT = "SIGN OUT" internal const val SIGN_IN = "SIGN IN" internal const val SIGNED_IN = "Signed In" internal const val SIGNED_OUT = "Signed Out" internal const val ERROR_NETWORK_UNAVAILABLE = "Network Unavailable" internal const val ERROR_AUTH = "An Error Has Occured" internal const val RETRY = "RETRY" internal const val ANTENNA_EMPTY = "antenna_empty" internal const val ANTENNA_FULL = "antenna_full" /** * This value is just a constant to denote our sign in request; It can be any int. * Would have been great if that was explained in the docs, I assumed at first that it had to * be a specific value. */ internal const val RC_SIGN_IN = 1337 sealed class LoginEvent { object OnAuthButtonClick : LoginEvent() object OnBackClick : LoginEvent() object OnStart : LoginEvent() data class OnGoogleSignInResult(val result: LoginResult) : LoginEvent() object OnDestroy : LoginEvent() } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/login/LoginActivity.kt ================================================ package com.wiseassblog.spacenotes.login import android.content.Intent import android.graphics.drawable.AnimationDrawable import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.api.ApiException import com.wiseassblog.spacenotes.R import com.wiseassblog.spacenotes.login.buildlogic.LoginInjector import kotlinx.android.synthetic.main.activity_login.* /** * Q: Why did I decide to use an Activity as the View in this feature? * A: Since I want to be able to use the GoogleSignIn API, there is necessary tight coupling * with Activity in this feature. Further, since this feature is quite simple to begin with, * I didn't mind breaking SoC a little bit in exchange for the GoogleSignIn functionality. * */ class LoginActivity : AppCompatActivity(), ILoginContract.View { override fun setObserver(observer: Observer>) = event.observeForever(observer) override fun startListFeature() = com.wiseassblog.spacenotes.common.startListFeature(this) val event = MutableLiveData>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) //Note: I cal setObserver within the LoginInjector function ViewModelProviders.of(this) .get(LoginInjector::class.java) .buildLoginLogic(this) btn_auth_attempt.setOnClickListener { event.value = LoginEvent.OnAuthButtonClick } imb_toolbar_back.setOnClickListener { event.value = LoginEvent.OnBackClick } } override fun onResume() { super.onResume() event.value = LoginEvent.OnStart } override fun setLoginStatus(text: String) { lbl_login_status_display.text = text } override fun setAuthButton(text: String) { btn_auth_attempt.text = text } override fun showLoopAnimation() { imv_antenna_animation.setImageResource( resources.getIdentifier("antenna_loop_fast", "drawable", this.packageName) ) val satelliteLoop = imv_antenna_animation.drawable as AnimationDrawable satelliteLoop.start() } override fun setStatusDrawable(imageURL: String) { imv_antenna_animation.setImageResource( resources.getIdentifier(imageURL, "drawable", this.packageName) ) } override fun startSignInFlow() { val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .build() val googleSignInClient = GoogleSignIn.getClient(this, gso) val signInIntent = googleSignInClient.signInIntent startActivityForResult(signInIntent, RC_SIGN_IN) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == RC_SIGN_IN) { val task = GoogleSignIn.getSignedInAccountFromIntent(data) try { val account: GoogleSignInAccount? = task.getResult(ApiException::class.java) event.value = LoginEvent.OnGoogleSignInResult( LoginResult( requestCode, account ) ) } catch (exception: Exception) { event.value = LoginEvent.OnGoogleSignInResult( LoginResult( 0, null ) ) } } } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/login/LoginLogic.kt ================================================ package com.wiseassblog.spacenotes.login import androidx.lifecycle.Observer import com.wiseassblog.domain.DispatcherProvider import com.wiseassblog.domain.servicelocator.UserServiceLocator import com.wiseassblog.domain.domainmodel.Result import com.wiseassblog.domain.error.SpaceNotesError import com.wiseassblog.domain.interactor.AuthSource import com.wiseassblog.spacenotes.common.BaseLogic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext class LoginLogic(dispatcher: DispatcherProvider, val userLocator: UserServiceLocator, val view: ILoginContract.View, val authSource: AuthSource) : BaseLogic(dispatcher), CoroutineScope, Observer> { init { jobTracker = Job() } override val coroutineContext: CoroutineContext get() = dispatcher.provideUIContext() + jobTracker override fun onChanged(event: LoginEvent) { when (event) { is LoginEvent.OnStart -> onStart() is LoginEvent.OnDestroy -> jobTracker.cancel() is LoginEvent.OnBackClick -> onBackClick() is LoginEvent.OnAuthButtonClick -> onAuthButtonClick() is LoginEvent.OnGoogleSignInResult -> onSignInResult(event.result) } } private fun onSignInResult(result: LoginResult) = launch { if (result.requestCode == RC_SIGN_IN && result.account != null) { view.showLoopAnimation() val createGoogleUserResult = authSource.createGoogleUser( result.account.idToken!!, userLocator ) when (createGoogleUserResult) { is Result.Value -> onStart() is Result.Error -> handleError(createGoogleUserResult.error) } } else { renderErrorState(ERROR_AUTH) } } private fun onAuthButtonClick() = launch { view.showLoopAnimation() val authResult = authSource.getCurrentUser(userLocator) when (authResult) { is Result.Value -> { if (authResult.value == null) view.startSignInFlow() else signUserOut() } is Result.Error -> handleError(authResult.error) } } private fun handleError(error: Exception) { when (error) { is SpaceNotesError.NetworkUnavailableException -> renderErrorState( ERROR_NETWORK_UNAVAILABLE ) else -> renderErrorState(ERROR_AUTH) } } private suspend fun signUserOut() { val signOutResult = authSource.signOutCurrentUser(userLocator) when (signOutResult) { is Result.Value -> renderNullUser() is Result.Error -> renderErrorState(ERROR_AUTH) } } private fun onBackClick() { view.startListFeature() } private fun onStart() = launch { view.showLoopAnimation() val authResult = authSource.getCurrentUser(userLocator) when (authResult) { is Result.Value -> { if (authResult.value == null) renderNullUser() else renderActiveUser() } is Result.Error -> handleError(authResult.error) } } private fun renderActiveUser() { view.setStatusDrawable(ANTENNA_FULL) view.setAuthButton(SIGN_OUT) view.setLoginStatus(SIGNED_IN) } private fun renderNullUser() { view.setStatusDrawable(ANTENNA_EMPTY) view.setAuthButton(SIGN_IN) view.setLoginStatus(SIGNED_OUT) } private fun renderErrorState(message: String) { view.setStatusDrawable(ANTENNA_EMPTY) view.setAuthButton(RETRY) view.setLoginStatus(message) } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/login/LoginResult.kt ================================================ package com.wiseassblog.spacenotes.login import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.tasks.Task /** * Wrapper class for data recieved in LoginActivity's onActivityResult() * function */ data class LoginResult(val requestCode: Int, val account: GoogleSignInAccount?) ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/login/buildlogic/LoginInjector.kt ================================================ package com.wiseassblog.spacenotes.login.buildlogic import android.app.Application import androidx.lifecycle.AndroidViewModel import com.google.firebase.FirebaseApp import com.wiseassblog.data.auth.FirebaseAuthRepositoryImpl import com.wiseassblog.domain.DispatcherProvider import com.wiseassblog.domain.servicelocator.UserServiceLocator import com.wiseassblog.domain.interactor.AuthSource import com.wiseassblog.domain.repository.IAuthRepository import com.wiseassblog.spacenotes.login.LoginActivity import com.wiseassblog.spacenotes.login.LoginLogic class LoginInjector(application: Application) : AndroidViewModel(application) { init { FirebaseApp.initializeApp(application) } //For user management private val auth: IAuthRepository by lazy { //by using lazy, I don't load this resource until I need it FirebaseAuthRepositoryImpl() } fun buildLoginLogic(view: LoginActivity): LoginLogic = LoginLogic( DispatcherProvider, UserServiceLocator(auth), view, AuthSource() ).also { view.setObserver(it) } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/INoteDetailContract.kt ================================================ package com.wiseassblog.spacenotes.notedetail import androidx.lifecycle.Observer import com.wiseassblog.domain.domainmodel.Note /** * Created by R_KAY on 10/8/2017. */ interface INoteDetailContract { interface View { fun setBackgroundImage(imageUrl: String) fun setDateLabel(date: String) fun setNoteBody(content: String) fun setObserver(observer: Observer) fun hideBackButton() fun getNoteBody(): String fun getTime(): String fun restartFeature() fun showMessage(message: String) fun showConfirmDeleteSnackbar() fun startListFeature() } interface ViewModel { fun setIsPrivateMode(isPrivateMode: Boolean) fun getIsPrivateMode(): Boolean fun setNoteState(note: Note) fun getNoteState(): Note? fun setId(id: String) fun getId(): String? } } sealed class NoteDetailEvent { object OnDoneClick : NoteDetailEvent() object OnDeleteClick : NoteDetailEvent() object OnDeleteConfirmed : NoteDetailEvent() object OnBackClick : NoteDetailEvent() object OnStart : NoteDetailEvent() object OnBind : NoteDetailEvent() object OnDestroy : NoteDetailEvent() } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailActivity.kt ================================================ package com.wiseassblog.spacenotes.notedetail import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.wiseassblog.spacenotes.R import com.wiseassblog.spacenotes.common.BOOLEAN_EXTRA_IS_PRIVATE import com.wiseassblog.spacenotes.common.STRING_EXTRA_NOTE_ID import com.wiseassblog.spacenotes.common.attachFragment import com.wiseassblog.spacenotes.notedetail.buildlogic.NoteDetailInjector import com.wiseassblog.spacenotes.notelist.NoteListActivity private const val VIEW = "NOTE_DETAIL" class NoteDetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_note_detail) //Elvis Operator val i:Intent = if intent is null, // assign i to Intent(this, NoteListActivity::class.java) val i: Intent = intent ?: Intent(this, NoteListActivity::class.java) //if intent is null, then it's time to gtfo if (intent == null) { Toast.makeText(this, "Application Restarted.", Toast.LENGTH_SHORT).show() startActivity(i) } val noteId = i.getStringExtra(STRING_EXTRA_NOTE_ID) val isPrivate = i.getBooleanExtra(BOOLEAN_EXTRA_IS_PRIVATE, true) val view = this.supportFragmentManager.findFragmentByTag(VIEW) as NoteDetailView? ?: NoteDetailView.newInstance() attachFragment(supportFragmentManager, R.id.root_activity_detail, view, VIEW) NoteDetailInjector(application) .buildNoteDetailLogic(view as NoteDetailView, noteId, isPrivate) } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailLogic.kt ================================================ package com.wiseassblog.spacenotes.notedetail import androidx.lifecycle.Observer import com.wiseassblog.domain.DispatcherProvider import com.wiseassblog.domain.servicelocator.NoteServiceLocator import com.wiseassblog.domain.servicelocator.UserServiceLocator import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.domain.domainmodel.Result import com.wiseassblog.domain.domainmodel.User import com.wiseassblog.domain.interactor.AnonymousNoteSource import com.wiseassblog.domain.interactor.AuthSource import com.wiseassblog.domain.interactor.PublicNoteSource import com.wiseassblog.domain.interactor.RegisteredNoteSource import com.wiseassblog.spacenotes.common.BaseLogic import com.wiseassblog.spacenotes.common.MESSAGE_DELETE_SUCCESSFUL import com.wiseassblog.spacenotes.common.MESSAGE_GENERIC_ERROR import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext class NoteDetailLogic(dispatcher: DispatcherProvider, val noteLocator: NoteServiceLocator, val userLocator: UserServiceLocator, val vModel: INoteDetailContract.ViewModel, val view: INoteDetailContract.View, val anonymousNoteSource: AnonymousNoteSource, val registeredNoteSource: RegisteredNoteSource, val publicNoteSource: PublicNoteSource, val authSource: AuthSource, id: String, isPrivate: Boolean) : BaseLogic(dispatcher), CoroutineScope, Observer { init { vModel.setId(id) vModel.setIsPrivateMode(isPrivate) jobTracker = Job() } fun clear() { jobTracker.cancel() } override val coroutineContext: CoroutineContext get() = dispatcher.provideUIContext() + jobTracker override fun onChanged(event: NoteDetailEvent) { when (event) { is NoteDetailEvent.OnDoneClick -> onDoneClick() is NoteDetailEvent.OnDeleteClick -> onDeleteClick() is NoteDetailEvent.OnBackClick -> onBackClick() is NoteDetailEvent.OnDeleteConfirmed -> onDeleteConfirmed() is NoteDetailEvent.OnStart -> onStart() is NoteDetailEvent.OnBind -> bind() is NoteDetailEvent.OnDestroy -> clear() } } fun onDoneClick() = launch { val userResult = authSource.getCurrentUser(userLocator) when (userResult) { is Result.Value -> { //if null, user is anonymous if (userResult.value == null) prepareAnonymousRepoUpdate() else if (vModel.getIsPrivateMode()) prepareRegisteredRepoUpdate() else preparePublicRepoUpdate() } } } private suspend fun prepareAnonymousRepoUpdate() { val updatedNote = vModel.getNoteState()!!.copy(contents = view.getNoteBody()) val result = anonymousNoteSource.updateNote(updatedNote, noteLocator) when (result) { is Result.Value -> view.startListFeature() is Result.Error -> view.showMessage(result.error.toString()) } } suspend fun prepareRegisteredRepoUpdate() { val updatedNote = vModel.getNoteState()!!.copy(contents = view.getNoteBody()) val result = registeredNoteSource.updateNote(updatedNote, noteLocator) when (result) { is Result.Value -> view.startListFeature() is Result.Error -> view.showMessage(result.error.toString()) } } suspend fun preparePublicRepoUpdate() { val updatedNote = vModel.getNoteState()!! .copy(contents = view.getNoteBody()) val result = publicNoteSource.updateNote(updatedNote, noteLocator) when (result) { is Result.Value -> view.startListFeature() is Result.Error -> view.showMessage(result.error.toString()) } } fun bind() = launch { val userResult = authSource.getCurrentUser(userLocator) when (userResult) { is Result.Value -> { val id = vModel.getId() if (id == "" || id == null) createNewNote(userResult.value) else getNoteFromSource(id, userResult.value) } is Result.Error -> view.showMessage(userResult.error.toString()) } } fun createNewNote(user: User?) { vModel.setNoteState( Note( view.getTime(), "", 0, "satellite_beam", user ) ) //only save or delete with new note view.hideBackButton() onStart() } fun getNoteFromSource(id: String, user: User?) = launch { val noteResult: Result //private anonymous if (user == null) noteResult = anonymousNoteSource.getNoteById(id, noteLocator) //private registered else if (vModel.getIsPrivateMode()) noteResult = registeredNoteSource.getNoteById(id, noteLocator) //public registered else noteResult = publicNoteSource.getNoteById(id, noteLocator) when (noteResult) { is Result.Value -> { vModel.setNoteState(noteResult.value!!) onStart() } is Result.Error -> { val message = noteResult.error.message ?: "An error has occured." view.showMessage(message) } } } fun onStart() { val state = vModel.getNoteState() //LiveData requires null checks due to nullable return types if (state != null) { renderView(state) } else { view.showMessage(MESSAGE_GENERIC_ERROR) view.startListFeature() } } private fun renderView(state: Note) { view.setBackgroundImage(state.imageUrl) view.setDateLabel(state.creationDate) view.setNoteBody(state.contents) } fun onBackClick() { view.startListFeature() } fun onDeleteClick() { view.showConfirmDeleteSnackbar() } fun onDeleteConfirmed() = launch { val currentNote = vModel.getNoteState() //if VM data is null, we're in a bad spot if (currentNote == null) { view.showMessage(MESSAGE_GENERIC_ERROR) view.restartFeature() } else { val userResult = authSource.getCurrentUser(userLocator) when (userResult) { is Result.Value -> { if (userResult.value == null) prepareAnonymousRepoDelete(currentNote) else if (vModel.getIsPrivateMode()) prepareRegisteredRepoDelete(currentNote) else preparePublicRepoDelete(currentNote) } is Result.Error -> view.showMessage(userResult.error.toString()) } } } private fun preparePublicRepoDelete(note: Note) = launch { val result = publicNoteSource.deleteNote(note, noteLocator) when (result) { is Result.Value -> { view.showMessage(MESSAGE_DELETE_SUCCESSFUL) view.startListFeature() } is Result.Error -> view.showMessage(result.error.toString()) } } private fun prepareRegisteredRepoDelete(note: Note) = launch { val result = registeredNoteSource.deleteNote(note, noteLocator) when (result) { is Result.Value -> { view.showMessage(MESSAGE_DELETE_SUCCESSFUL) view.startListFeature() } is Result.Error -> view.showMessage(result.error.toString()) } } private fun prepareAnonymousRepoDelete(note: Note) = launch { val result = anonymousNoteSource.deleteNote(note, noteLocator) when (result) { is Result.Value -> { view.showMessage(MESSAGE_DELETE_SUCCESSFUL) view.startListFeature() } is Result.Error -> view.showMessage(result.error.toString()) } } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailView.kt ================================================ package com.wiseassblog.spacenotes.notedetail import android.graphics.drawable.AnimationDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.google.android.material.snackbar.Snackbar import com.wiseassblog.spacenotes.R import com.wiseassblog.spacenotes.R.id.* import com.wiseassblog.spacenotes.common.* import com.wiseassblog.spacenotes.notedetail.buildlogic.NoteDetailInjector import com.wiseassblog.spacenotes.notelist.NoteListEvent import kotlinx.android.synthetic.main.fragment_note_detail.* class NoteDetailView : Fragment(), INoteDetailContract.View { val event = MutableLiveData() override fun setObserver(observer: Observer) = event.observeForever(observer) override fun startListFeature() = com.wiseassblog.spacenotes.common.startListFeature(this.activity) override fun hideBackButton() { imb_toolbar_back.visibility = View.INVISIBLE imb_toolbar_back.isEnabled = false } override fun getTime(): String = getCalendarTime() override fun showConfirmDeleteSnackbar() { if (activity != null) { Snackbar.make(frag_note_detail, MESSAGE_DELETE_CONFIRMATION, Snackbar.LENGTH_LONG) .setAction(MESSAGE_DELETE) { event.value = NoteDetailEvent.OnDeleteConfirmed } .show() } } override fun showMessage(message: String) = makeToast(message) override fun restartFeature() = restartCurrentFeature() override fun getNoteBody(): String { return edt_note_detail_text.text.toString() } override fun setBackgroundImage(imageUrl: String) { imv_note_detail_satellite.setImageResource( resources.getIdentifier(imageUrl, "drawable", context?.packageName) ) val satelliteLoop = imv_note_detail_satellite.drawable as AnimationDrawable satelliteLoop.start() } override fun setDateLabel(date: String) { lbl_note_detail_date.text = date } override fun setNoteBody(content: String) { edt_note_detail_text.text = content.toEditable() } override fun onStart() { super.onStart() event.value = NoteDetailEvent.OnBind } override fun onDestroy() { event.value = NoteDetailEvent.OnDestroy super.onDestroy() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_note_detail, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { imb_toolbar_done.setOnClickListener { event.value = NoteDetailEvent.OnDoneClick } imb_toolbar_back.setOnClickListener { event.value = NoteDetailEvent.OnBackClick } imb_toolbar_delete.setOnClickListener { event.value = NoteDetailEvent.OnDeleteClick } val spaceLoop = frag_note_detail.background as AnimationDrawable spaceLoop.setEnterFadeDuration(1000) spaceLoop.setExitFadeDuration(1000) spaceLoop.start() super.onViewCreated(view, savedInstanceState) } companion object { @JvmStatic fun newInstance() = NoteDetailView() } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailViewModel.kt ================================================ package com.wiseassblog.spacenotes.notedetail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.spacenotes.notedetail.INoteDetailContract class NoteDetailViewModel(private var displayState: MutableLiveData = MutableLiveData(), private var id: MutableLiveData = MutableLiveData(), private var isPrivateMode: MutableLiveData = MutableLiveData()) : ViewModel(), INoteDetailContract.ViewModel { override fun getIsPrivateMode(): Boolean { return isPrivateMode.value!! } override fun setIsPrivateMode(isPrivateMode: Boolean) { this.isPrivateMode.value = isPrivateMode } override fun setId(id: String) { this.id.value = id } override fun getId(): String? { return this.id.value } override fun getNoteState(): Note? { return displayState.value } override fun setNoteState(note: Note) { displayState.value = note } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/buildlogic/NoteDetailInjector.kt ================================================ package com.wiseassblog.spacenotes.notedetail.buildlogic import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModelProviders import com.google.firebase.FirebaseApp import com.wiseassblog.data.auth.FirebaseAuthRepositoryImpl import com.wiseassblog.data.note.anonymous.AnonymousNoteDao import com.wiseassblog.data.note.anonymous.AnonymousNoteDatabase import com.wiseassblog.data.note.anonymous.RoomLocalAnonymousRepositoryImpl import com.wiseassblog.data.note.public.FirestoreRemoteNoteImpl import com.wiseassblog.data.note.registered.* import com.wiseassblog.data.transaction.RoomRegisteredTransactionDatabase import com.wiseassblog.data.transaction.RoomTransactionRepositoryImpl import com.wiseassblog.domain.DispatcherProvider import com.wiseassblog.domain.servicelocator.NoteServiceLocator import com.wiseassblog.domain.servicelocator.UserServiceLocator import com.wiseassblog.domain.interactor.AnonymousNoteSource import com.wiseassblog.domain.interactor.AuthSource import com.wiseassblog.domain.interactor.PublicNoteSource import com.wiseassblog.domain.interactor.RegisteredNoteSource import com.wiseassblog.domain.repository.* import com.wiseassblog.spacenotes.notedetail.* /** * */ class NoteDetailInjector(application: Application) : AndroidViewModel(application) { init { FirebaseApp.initializeApp(application) } private val anonNoteDao: AnonymousNoteDao by lazy { AnonymousNoteDatabase.getInstance(getApplication()).roomNoteDao() } private val regNoteDao: RegisteredNoteDao by lazy { RegisteredNoteDatabase.getInstance(getApplication()).roomNoteDao() } private val transactionDao: RegisteredTransactionDao by lazy { RoomRegisteredTransactionDatabase.getInstance(getApplication()).roomTransactionDao() } //For non-registered user persistence private val localAnon: ILocalNoteRepository by lazy { RoomLocalAnonymousRepositoryImpl(anonNoteDao) } //For registered user remote persistence (Source of Truth) private val remotePrivate: IRemoteNoteRepository by lazy { FirestorePrivateRemoteNoteImpl() } //For registered user remote persistence (Source of Truth) private val remotePublic: IPublicNoteRepository by lazy { FirestoreRemoteNoteImpl } //For registered user local persistience (cache) private val cacheReg: ILocalNoteRepository by lazy { RoomLocalCacheImpl(regNoteDao) } //For registered user remote persistence (Source of Truth) private val remoteRepo: IRemoteNoteRepository by lazy { RegisteredNoteRepositoryImpl(remotePrivate, cacheReg) } //For registered user local persistience (cache) private val transactionReg: ITransactionRepository by lazy { RoomTransactionRepositoryImpl(transactionDao) } //For user management private val auth: IAuthRepository by lazy { FirebaseAuthRepositoryImpl() } private lateinit var logic: NoteDetailLogic fun buildNoteDetailLogic(view: NoteDetailView, id: String, isPrivate: Boolean): NoteDetailLogic { logic = NoteDetailLogic( DispatcherProvider, NoteServiceLocator(localAnon, remoteRepo, transactionReg, remotePublic), UserServiceLocator(auth), ViewModelProviders.of(view) .get(NoteDetailViewModel::class.java), view, AnonymousNoteSource(), RegisteredNoteSource(), PublicNoteSource(), AuthSource(), id, isPrivate ) view.setObserver(logic) return logic } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/INoteListContract.kt ================================================ package com.wiseassblog.spacenotes.notelist import androidx.lifecycle.Observer import androidx.recyclerview.widget.ListAdapter import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.domain.domainmodel.User /** * Created by R_KAY on 10/8/2017. */ interface INoteListContract { interface View { fun setAdapter(adapter: ListAdapter) fun setPrivateIcon(isPrivate: Boolean) fun showList() fun showEmptyState() fun showErrorState(message:String) fun showLoadingView() fun setToolbarTitle(title: String) fun startLoginFeature() fun setObserver(observer: Observer>) fun startNoteDetailFeatureWithExtras(noteId: String, isPrivate: Boolean) } interface ViewModel { fun setAdapterState(result: List) fun getAdapterState(): List fun getUserState(): User? fun setUserState(userResult: User?) fun getIsPrivateMode(): Boolean fun setIsPrivateMode(isPrivateMode: Boolean) } } sealed class NoteListEvent { data class OnNoteItemClick(val position: Int) : NoteListEvent() object OnNewNoteClick : NoteListEvent() object OnLoginClick : NoteListEvent() object OnTogglePublicMode : NoteListEvent() object OnStart : NoteListEvent() object OnBind : NoteListEvent() object OnDestroy : NoteListEvent() } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteDiffUtilCallback.kt ================================================ package com.wiseassblog.spacenotes.notelist import androidx.recyclerview.widget.DiffUtil import com.wiseassblog.domain.domainmodel.Note class NoteDiffUtilCallback : DiffUtil.ItemCallback(){ override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean { return oldItem.creationDate == newItem.creationDate } override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean { return oldItem.creationDate == newItem.creationDate } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListActivity.kt ================================================ package com.wiseassblog.spacenotes.notelist import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders import com.wiseassblog.spacenotes.R import com.wiseassblog.spacenotes.common.attachFragment import com.wiseassblog.spacenotes.notelist.buildlogic.NoteListInjector private const val VIEW = "NOTE_LIST" class NoteListActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_note_list) //A container basically just builds things and sets the feature in motion val view = this.supportFragmentManager.findFragmentByTag(VIEW) ?: NoteListView.newInstance() attachFragment(supportFragmentManager, R.id.root_activity_list, view, VIEW) NoteListInjector(application) .buildNoteListLogic(view as NoteListView) } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListAdapter.kt ================================================ package com.wiseassblog.spacenotes.notelist import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.spacenotes.R import kotlinx.android.synthetic.main.item_note.view.* class NoteListAdapter(var event: MutableLiveData> = MutableLiveData() ) : ListAdapter(NoteDiffUtilCallback()) { internal fun setObserver(observer: Observer>) = event.observeForever(observer) override fun onBindViewHolder(holder: NoteListAdapter.NoteViewHolder, position: Int) { getItem(position).let { note -> with(holder) { holder.content.text = note.contents holder.date.text = note.creationDate holder.square.setImageResource(R.drawable.gps_icon) holder.content.text = note.contents holder.itemView.setOnClickListener { event.value = NoteListEvent.OnNoteItemClick(position) } } } holder.apply { } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder { val inflater = LayoutInflater.from(parent.context) return NoteViewHolder( inflater.inflate(R.layout.item_note, parent, false) ) } class NoteViewHolder(private val root: View) : RecyclerView.ViewHolder(root) { var square: ImageView = root.imv_list_item_icon var dateIcon: ImageView = root.imv_date_and_time var content: TextView = root.lbl_message var date: TextView = root.lbl_date_and_time var loading: ProgressBar = root.pro_item_data } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListLogic.kt ================================================ package com.wiseassblog.spacenotes.notelist import androidx.lifecycle.Observer import com.wiseassblog.domain.DispatcherProvider import com.wiseassblog.domain.servicelocator.NoteServiceLocator import com.wiseassblog.domain.servicelocator.UserServiceLocator import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.domain.domainmodel.Result import com.wiseassblog.domain.error.SpaceNotesError import com.wiseassblog.domain.interactor.AnonymousNoteSource import com.wiseassblog.domain.interactor.AuthSource import com.wiseassblog.domain.interactor.PublicNoteSource import com.wiseassblog.domain.interactor.RegisteredNoteSource import com.wiseassblog.spacenotes.common.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext class NoteListLogic(dispatcher: DispatcherProvider, val noteLocator: NoteServiceLocator, val userLocator: UserServiceLocator, val vModel: INoteListContract.ViewModel, var adapter: NoteListAdapter, val view: INoteListContract.View, val anonymousNoteSource: AnonymousNoteSource, val registeredNoteSource: RegisteredNoteSource, val publicNoteSource: PublicNoteSource, val authSource: AuthSource) : BaseLogic(dispatcher), CoroutineScope, Observer> { override fun onChanged(event: NoteListEvent?) { when (event) { is NoteListEvent.OnNoteItemClick -> onNoteItemClick(event.position) is NoteListEvent.OnNewNoteClick -> onNewNoteClick() is NoteListEvent.OnLoginClick -> onLoginClick() is NoteListEvent.OnTogglePublicMode -> onTogglePublicMode() is NoteListEvent.OnStart -> onStart() is NoteListEvent.OnBind -> bind() is NoteListEvent.OnDestroy -> clear() } } init { //This is directly analogous to CompositeDisposable jobTracker = Job() } //dispatcher.provideUIContext is very analogous to observeOn(Dispatchers.UI) override val coroutineContext: CoroutineContext get() = dispatcher.provideUIContext() + jobTracker private fun onNewNoteClick() = view.startNoteDetailFeatureWithExtras( "", vModel.getIsPrivateMode() ) private fun onStart() { getListData(vModel.getIsPrivateMode()) } fun getListData(isPrivateMode: Boolean) = launch { val dataResult: Result> when (isPrivateMode) { true -> dataResult = getPrivateListData() false -> dataResult = getPublicListData() } when (dataResult) { is Result.Value -> { vModel.setAdapterState(dataResult.value) renderView(dataResult.value) } is Result.Error -> { view.showEmptyState() view.showErrorState(MESSAGE_GENERIC_ERROR) } } } suspend fun getPublicListData(): Result> { return if (vModel.getUserState() != null) publicNoteSource.getNotes(noteLocator) else Result.build { throw SpaceNotesError.LocalIOException } } suspend fun getPrivateListData(): Result> { return if (vModel.getUserState() == null) anonymousNoteSource.getNotes(noteLocator) else registeredNoteSource.getNotes(noteLocator) } fun renderView(list: List) { view.setPrivateIcon(vModel.getIsPrivateMode()) if (vModel.getIsPrivateMode()) view.setToolbarTitle(MODE_PRIVATE) else view.setToolbarTitle(MODE_PUBLIC) if (list.isEmpty()) view.showEmptyState() else view.showList() adapter.submitList(list) } private fun onTogglePublicMode() { if (vModel.getUserState() != null) { if (vModel.getIsPrivateMode()) { vModel.setIsPrivateMode(false) getListData(false) } else { vModel.setIsPrivateMode(true) getListData(true) } } else { view.showErrorState(MESSAGE_LOGIN) } } private fun onLoginClick() { view.startLoginFeature() } private fun onNoteItemClick(position: Int) { val listData = vModel.getAdapterState() view.startNoteDetailFeatureWithExtras( listData[position].creationDate, vModel.getIsPrivateMode()) } fun bind() { view.setToolbarTitle(MODE_PRIVATE) view.showLoadingView() adapter.setObserver(this) view.setAdapter(adapter) view.setObserver(this) launch { val result = authSource.getCurrentUser(userLocator) if (result is Result.Value) vModel.setUserState(result.value) //otherwise defaults to null } } //Single Expression Syntax fun clear() = jobTracker.cancel() } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListView.kt ================================================ package com.wiseassblog.spacenotes.notelist import android.graphics.drawable.AnimationDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.ListAdapter import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.spacenotes.R import com.wiseassblog.spacenotes.R.id.* import com.wiseassblog.spacenotes.common.makeToast import com.wiseassblog.spacenotes.notelist.buildlogic.NoteListInjector import kotlinx.android.synthetic.main.fragment_note_list.* class NoteListView : Fragment(), INoteListContract.View { override fun setPrivateIcon(isPrivate: Boolean) { //private mode if (isPrivate) imv_toolbar_private_toggle.setImageResource(R.drawable.design_ic_visibility_off) //public mode else imv_toolbar_private_toggle.setImageResource(R.drawable.design_ic_visibility) } val event = MutableLiveData>() //Event listener override fun setObserver(observer: Observer>) = event.observeForever(observer) override fun showErrorState(message: String) = this.makeToast(message) override fun startLoginFeature() = com.wiseassblog.spacenotes.common.startLoginFeature(this.activity) override fun startNoteDetailFeatureWithExtras(noteId: String, isPrivate: Boolean) = com.wiseassblog.spacenotes.common.startNoteDetailFeatureWithExtras(this.activity, noteId, isPrivate) override fun setToolbarTitle(title: String) { lbl_toolbar_title.text = title } override fun setAdapter(adapter: ListAdapter) { rec_list_activity.adapter = adapter } override fun showLoadingView() { rec_list_activity.visibility = View.INVISIBLE fab_create_new_item.hide() imv_satellite_animation.visibility = View.VISIBLE //set loading animation val satelliteLoop = imv_satellite_animation.drawable as AnimationDrawable satelliteLoop.start() } override fun showEmptyState() { rec_list_activity.visibility = View.INVISIBLE fab_create_new_item.show() imv_satellite_animation.visibility = View.VISIBLE val satelliteLoop = imv_satellite_animation.drawable as AnimationDrawable satelliteLoop.start() } override fun showList() { rec_list_activity.visibility = View.VISIBLE fab_create_new_item.show() imv_satellite_animation.visibility = View.INVISIBLE val satelliteLoop = imv_satellite_animation.drawable as AnimationDrawable satelliteLoop.stop() } override fun onStart() { super.onStart() event.value = NoteListEvent.OnBind } override fun onResume() { super.onResume() event.value = NoteListEvent.OnStart } override fun onDestroy() { event.value = NoteListEvent.OnDestroy super.onDestroy() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_note_list, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { event.value = NoteListEvent.OnBind imv_toolbar_auth.setOnClickListener { event.value = NoteListEvent.OnLoginClick } imv_toolbar_private_toggle.setOnClickListener { event.value = NoteListEvent.OnTogglePublicMode } fab_create_new_item.setOnClickListener { event.value = NoteListEvent.OnNewNoteClick } val spaceLoop = imv_space_background.drawable as AnimationDrawable spaceLoop.setEnterFadeDuration(1000) spaceLoop.setExitFadeDuration(1000) spaceLoop.start() super.onViewCreated(view, savedInstanceState) } companion object { @JvmStatic fun newInstance(): Fragment = NoteListView() } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListViewModel.kt ================================================ package com.wiseassblog.spacenotes.notelist import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.wiseassblog.domain.domainmodel.Note import com.wiseassblog.domain.domainmodel.User /** * isPrivateMode refers to whether the User wants to post to and read from a shared repo, or they * would like to store their note in private storage. */ class NoteListViewModel(private var adapterData: MutableLiveData> = MutableLiveData(), private var user: MutableLiveData = MutableLiveData(), private var isPrivateMode: MutableLiveData = MutableLiveData()) : ViewModel(), INoteListContract.ViewModel { init { isPrivateMode.value = true } override fun getIsPrivateMode(): Boolean { return isPrivateMode.value!! } override fun setIsPrivateMode(isPrivateMode: Boolean) { this.isPrivateMode.value = isPrivateMode } override fun setAdapterState(result: List) { adapterData.value = result } override fun setUserState(userResult: User?) { user.value = userResult } override fun getUserState(): User? { return user.value } override fun getAdapterState(): List { //return current display state or empty string if value is null //see "Elvis Operator" return adapterData.value ?: emptyList() } } ================================================ FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/buildlogic/NoteListInjector.kt ================================================ package com.wiseassblog.spacenotes.notelist.buildlogic import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModelProviders import com.google.firebase.FirebaseApp import com.wiseassblog.data.auth.FirebaseAuthRepositoryImpl import com.wiseassblog.data.note.anonymous.AnonymousNoteDao import com.wiseassblog.data.note.anonymous.AnonymousNoteDatabase import com.wiseassblog.data.note.anonymous.RoomLocalAnonymousRepositoryImpl import com.wiseassblog.data.note.public.FirestoreRemoteNoteImpl import com.wiseassblog.data.note.registered.* import com.wiseassblog.data.transaction.RoomRegisteredTransactionDatabase import com.wiseassblog.data.transaction.RoomTransactionRepositoryImpl import com.wiseassblog.domain.DispatcherProvider import com.wiseassblog.domain.servicelocator.NoteServiceLocator import com.wiseassblog.domain.servicelocator.UserServiceLocator import com.wiseassblog.domain.interactor.AnonymousNoteSource import com.wiseassblog.domain.interactor.AuthSource import com.wiseassblog.domain.interactor.PublicNoteSource import com.wiseassblog.domain.interactor.RegisteredNoteSource import com.wiseassblog.domain.repository.* import com.wiseassblog.spacenotes.notelist.NoteListAdapter import com.wiseassblog.spacenotes.notelist.NoteListLogic import com.wiseassblog.spacenotes.notelist.NoteListView import com.wiseassblog.spacenotes.notelist.NoteListViewModel class NoteListInjector(application: Application) : AndroidViewModel(application) { init { FirebaseApp.initializeApp(application) } private val anonNoteDao: AnonymousNoteDao by lazy { AnonymousNoteDatabase.getInstance(application).roomNoteDao() } private val regNoteDao: RegisteredNoteDao by lazy { RegisteredNoteDatabase.getInstance(application).roomNoteDao() } private val transactionDao: RegisteredTransactionDao by lazy { RoomRegisteredTransactionDatabase.getInstance(application).roomTransactionDao() } //For non-registered user persistence private val localAnon: ILocalNoteRepository by lazy { RoomLocalAnonymousRepositoryImpl(anonNoteDao) } //For registered user remote persistence (Source of Truth) private val remotePrivate: IRemoteNoteRepository by lazy { FirestorePrivateRemoteNoteImpl() } //For registered user remote persistence (Source of Truth) private val remotePublicRepo: IPublicNoteRepository by lazy { FirestoreRemoteNoteImpl } //For registered user local persistience (cache) private val cacheReg: ILocalNoteRepository by lazy { RoomLocalCacheImpl(regNoteDao) } //For registered user remote persistence (Source of Truth) private val remotePrivateRepo: IRemoteNoteRepository by lazy { RegisteredNoteRepositoryImpl(remotePrivate, cacheReg) } //For registered user local persistience (cache) private val transactionReg: ITransactionRepository by lazy { RoomTransactionRepositoryImpl(transactionDao) } //For user management private val auth: IAuthRepository by lazy { FirebaseAuthRepositoryImpl() } private lateinit var logic: NoteListLogic fun buildNoteListLogic(view: NoteListView): NoteListLogic { logic = NoteListLogic( DispatcherProvider, NoteServiceLocator(localAnon, remotePrivateRepo, transactionReg, remotePublicRepo), UserServiceLocator(auth), ViewModelProviders.of(view) .get(NoteListViewModel::class.java), NoteListAdapter(), view, AnonymousNoteSource(), RegisteredNoteSource(), PublicNoteSource(), AuthSource() ) view.setObserver(logic) return logic } } ================================================ FILE: app/src/main/res/drawable/antenna_loop.xml ================================================ ================================================ FILE: app/src/main/res/drawable/antenna_loop_fast.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_access_time_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_add_24px.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_event_24px.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_delete_forever_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_done_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_visibility_off_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_vpn_key_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/satellite_beam.xml ================================================ ================================================ FILE: app/src/main/res/drawable/space_loop.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_login.xml ================================================