├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── netoloboapps │ │ └── noteapp │ │ ├── ExampleInstrumentedTest.kt │ │ ├── HiltTestRunner.kt │ │ ├── di │ │ └── TestAppModule.kt │ │ └── feature_note │ │ └── presentation │ │ ├── NotesEndToEndTest.kt │ │ └── notes │ │ └── NotesScreenTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── netoloboapps │ │ │ └── noteapp │ │ │ ├── NotesApp.kt │ │ │ ├── core │ │ │ └── util │ │ │ │ └── TestTags.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── feature_note │ │ │ ├── data │ │ │ │ ├── data_source │ │ │ │ │ ├── NoteDao.kt │ │ │ │ │ └── NoteDatabase.kt │ │ │ │ └── repository │ │ │ │ │ └── NoteRepositoryImpl.kt │ │ │ ├── domain │ │ │ │ ├── model │ │ │ │ │ └── Note.kt │ │ │ │ ├── repository │ │ │ │ │ └── NoteRepository.kt │ │ │ │ ├── use_case │ │ │ │ │ ├── AddNoteUseCase.kt │ │ │ │ │ ├── DeleteNoteUseCase.kt │ │ │ │ │ ├── GetNoteUseCase.kt │ │ │ │ │ ├── GetNotesUseCase.kt │ │ │ │ │ └── NoteUseCases.kt │ │ │ │ └── util │ │ │ │ │ ├── NoteOrder.kt │ │ │ │ │ └── OrderType.kt │ │ │ └── presentation │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── add_edit_note │ │ │ │ ├── AddEditNoteEvent.kt │ │ │ │ ├── AddEditNoteScreen.kt │ │ │ │ ├── AddEditNoteViewModel.kt │ │ │ │ ├── NoteTextFieldState.kt │ │ │ │ └── components │ │ │ │ │ └── TransparentHintTextField.kt │ │ │ │ ├── notes │ │ │ │ ├── NotesEvent.kt │ │ │ │ ├── NotesScreen.kt │ │ │ │ ├── NotesState.kt │ │ │ │ ├── NotesViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── DefaultRadioButton.kt │ │ │ │ │ ├── NoteItem.kt │ │ │ │ │ └── OrderSection.kt │ │ │ │ └── util │ │ │ │ └── Screen.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── netoloboapps │ └── noteapp │ ├── ExampleUnitTest.kt │ └── feature_note │ ├── data │ └── repository │ │ └── FakeNoteRepository.kt │ └── domain │ └── use_case │ └── GetNotesUseCaseTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Note App -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was made following the instructions of the amazing Android Developer 2 | Philipp Lackner on his YouTube channel, he has amazing videos about Android 3 | Development, so I really recommend you subscribe on his channel. This app is an 4 | example of how to use Clean Architecture, MVVM, JetPack Compose, ViewModel, Flow, 5 | Coroutines, Room, Navigation, Dependency Injection with Dagger Hilt and I updated the app to 6 | use Material Design 3. 7 | 8 | ![Screenshot_20221214-104202_Note App](https://user-images.githubusercontent.com/641469/207619695-2fda0229-b995-4e2d-b3e4-5c41e7297a24.jpeg) 9 | 10 | 11 | ![Screenshot_20221214-104252_Note App](https://user-images.githubusercontent.com/641469/207619741-8b0ec161-f6cd-4999-b691-e958b901902a.jpeg) 12 | 13 | 14 | ![Screenshot_20221214-104306_Note App](https://user-images.githubusercontent.com/641469/207619781-b1f2787a-e3e2-4b47-8d88-c0c7f00f63b0.jpeg) 15 | 16 | 17 | ![Screenshot_20221214-104456_Note App](https://user-images.githubusercontent.com/641469/207619796-93d5c723-a33c-4e66-9442-82c20bb772bc.jpeg) 18 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("kotlin-kapt") 5 | id("com.google.dagger.hilt.android") 6 | } 7 | 8 | android { 9 | namespace = "com.netoloboapps.noteapp" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | applicationId = "com.netoloboapps.noteapp" 14 | minSdk = 29 15 | targetSdk = 34 16 | versionCode = 1 17 | versionName = "1.0" 18 | 19 | testInstrumentationRunner = "com.netoloboapps.noteapp.HiltTestRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | } 24 | 25 | buildTypes { 26 | getByName("release") { 27 | isMinifyEnabled = false 28 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_17 33 | targetCompatibility = JavaVersion.VERSION_17 34 | } 35 | kotlinOptions { 36 | jvmTarget = "17" 37 | } 38 | 39 | buildFeatures { 40 | compose = true 41 | } 42 | composeOptions { 43 | kotlinCompilerExtensionVersion = "1.5.0" 44 | } 45 | packaging { 46 | resources { 47 | resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") 48 | } 49 | } 50 | hilt { 51 | enableAggregatingTask = true 52 | } 53 | } 54 | 55 | dependencies { 56 | //Navigation components 57 | implementation ("androidx.navigation:navigation-compose:2.7.1") 58 | 59 | //ViewModel-Compose 60 | implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") 61 | 62 | //Room 63 | kapt ("androidx.room:room-compiler:2.5.2") 64 | implementation ("androidx.room:room-ktx:2.5.2") 65 | 66 | //Hilt 67 | implementation ("com.google.dagger:hilt-android:2.47") 68 | kapt ("com.google.dagger:hilt-compiler:2.47") 69 | implementation ("androidx.hilt:hilt-navigation-compose:1.0.0") 70 | implementation ("com.google.dagger:dagger:2.47") 71 | kapt ("com.google.dagger:dagger-compiler:2.47") 72 | 73 | //Coroutines 74 | implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") 75 | implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") 76 | 77 | //Material icons extended 78 | implementation ("androidx.compose.material:material-icons-extended:1.5.0") 79 | 80 | implementation ("androidx.core:core-ktx:1.10.1") 81 | implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") 82 | implementation ("androidx.activity:activity-compose:1.7.2") 83 | implementation (platform("androidx.compose:compose-bom:2023.04.01")) 84 | implementation ("androidx.compose.ui:ui") 85 | implementation ("androidx.compose.ui:ui-tooling-preview") 86 | implementation ("androidx.compose.material3:material3:1.1.1") 87 | 88 | // Local unit tests 89 | testImplementation ("androidx.test:core:1.5.0") 90 | testImplementation ("junit:junit:4.13.2") 91 | testImplementation ("androidx.arch.core:core-testing:2.2.0") 92 | testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") 93 | testImplementation ("com.google.truth:truth:1.1.5") 94 | testImplementation ("com.squareup.okhttp3:mockwebserver:4.9.1") 95 | testImplementation ("io.mockk:mockk:1.10.5") 96 | debugImplementation ("androidx.compose.ui:ui-test-manifest") 97 | 98 | //Instrumentation tests 99 | androidTestImplementation ("com.google.dagger:hilt-android-testing:2.47") 100 | kaptAndroidTest ("com.google.dagger:hilt-android-compiler:2.47") 101 | androidTestImplementation ("junit:junit:4.13.2") 102 | androidTestImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") 103 | androidTestImplementation ("androidx.arch.core:core-testing:2.2.0") 104 | androidTestImplementation ("com.google.truth:truth:1.1.5") 105 | androidTestImplementation ("androidx.test.ext:junit:1.1.5") 106 | androidTestImplementation ("androidx.test:core-ktx:1.5.0") 107 | androidTestImplementation ("com.squareup.okhttp3:mockwebserver:4.9.1") 108 | androidTestImplementation ("io.mockk:mockk-android:1.10.5") 109 | androidTestImplementation ("androidx.test:runner:1.5.2") 110 | androidTestImplementation ("androidx.compose.ui:ui-test-junit4") 111 | } -------------------------------------------------------------------------------- /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.kts. 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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/netoloboapps/noteapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.netoloboapps.noteapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/netoloboapps/noteapp/HiltTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import dagger.hilt.android.testing.HiltTestApplication 7 | 8 | class HiltTestRunner : AndroidJUnitRunner() { 9 | override fun newApplication( 10 | cl: ClassLoader?, 11 | className: String?, 12 | context: Context? 13 | ): Application { 14 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/netoloboapps/noteapp/di/TestAppModule.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.di 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import com.netoloboapps.noteapp.feature_note.data.data_source.NoteDatabase 6 | import com.netoloboapps.noteapp.feature_note.data.repository.NoteRepositoryImpl 7 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 8 | import com.netoloboapps.noteapp.feature_note.domain.use_case.* 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object TestAppModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideNoteDatabase(app: Application): NoteDatabase = Room.inMemoryDatabaseBuilder( 22 | app, 23 | NoteDatabase::class.java, 24 | ).build() 25 | 26 | @Provides 27 | @Singleton 28 | fun providesNoteRepository(db: NoteDatabase): NoteRepository = NoteRepositoryImpl(db.noteDao) 29 | 30 | @Provides 31 | @Singleton 32 | fun provideNoteUseCases(repository: NoteRepository): NoteUseCases = NoteUseCases( 33 | getNotesUseCase = GetNotesUseCase(repository), 34 | deleteNoteUseCase = DeleteNoteUseCase(repository), 35 | addNoteUseCase = AddNoteUseCase(repository), 36 | getNoteUseCase = GetNoteUseCase(repository) 37 | ) 38 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/netoloboapps/noteapp/feature_note/presentation/NotesEndToEndTest.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation 2 | 3 | import androidx.activity.compose.setContent 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.assertTextContains 6 | import androidx.compose.ui.test.assertTextEquals 7 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 8 | import androidx.compose.ui.test.onAllNodesWithTag 9 | import androidx.compose.ui.test.onNodeWithContentDescription 10 | import androidx.compose.ui.test.onNodeWithTag 11 | import androidx.compose.ui.test.onNodeWithText 12 | import androidx.compose.ui.test.performClick 13 | import androidx.compose.ui.test.performTextInput 14 | import androidx.navigation.NavType 15 | import androidx.navigation.compose.NavHost 16 | import androidx.navigation.compose.composable 17 | import androidx.navigation.compose.rememberNavController 18 | import androidx.navigation.navArgument 19 | import com.netoloboapps.noteapp.core.util.TestTags.CONTENT_TEXT_FIELD 20 | import com.netoloboapps.noteapp.core.util.TestTags.NOTE_ITEM 21 | import com.netoloboapps.noteapp.core.util.TestTags.TITLE_TEXT_FIELD 22 | import com.netoloboapps.noteapp.di.AppModule 23 | import com.netoloboapps.noteapp.feature_note.presentation.add_edit_note.AddEditNoteScreen 24 | import com.netoloboapps.noteapp.feature_note.presentation.notes.NotesScreen 25 | import com.netoloboapps.noteapp.feature_note.presentation.util.Screen 26 | import com.netoloboapps.noteapp.ui.theme.NoteAppTheme 27 | import dagger.hilt.android.testing.HiltAndroidRule 28 | import dagger.hilt.android.testing.HiltAndroidTest 29 | import dagger.hilt.android.testing.UninstallModules 30 | import org.junit.Before 31 | import org.junit.Rule 32 | import org.junit.Test 33 | 34 | @HiltAndroidTest 35 | @UninstallModules(AppModule::class) 36 | class NotesEndToEndTest { 37 | @get:Rule(order = 0) 38 | val hiltRule = HiltAndroidRule(this) 39 | 40 | @get:Rule(order = 1) 41 | val composeRule = createAndroidComposeRule() 42 | 43 | @Before 44 | fun setUp() { 45 | hiltRule.inject() 46 | composeRule.activity.setContent { 47 | NoteAppTheme { 48 | val navController = rememberNavController() 49 | NavHost( 50 | navController = navController, 51 | startDestination = Screen.NotesScreen.route 52 | ) { 53 | composable(route = Screen.NotesScreen.route) { 54 | NotesScreen(navController = navController) 55 | } 56 | composable(route = Screen.AddEditNoteScreen.route + 57 | "?noteId={noteId}¬eColor={noteColor}", 58 | arguments = listOf( 59 | navArgument( 60 | name = "noteId" 61 | ) { 62 | type = NavType.IntType 63 | defaultValue = -1 64 | }, 65 | navArgument( 66 | name = "noteColor" 67 | ) { 68 | type = NavType.IntType 69 | defaultValue = -1 70 | } 71 | ) 72 | ) { 73 | val color = it.arguments?.getInt("noteColor") ?: -1 74 | AddEditNoteScreen( 75 | navController = navController, 76 | noteColor = color 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | @Test 85 | fun saveNewNote_editAfterwards() { 86 | //Click on FAB to get to add a note screen 87 | composeRule 88 | .onNodeWithContentDescription("Add note") 89 | .performClick() 90 | 91 | //Enter texts in title and content TextFields 92 | composeRule 93 | .onNodeWithTag(TITLE_TEXT_FIELD) 94 | .performTextInput("test-title") 95 | composeRule 96 | .onNodeWithTag(CONTENT_TEXT_FIELD) 97 | .performTextInput("test-content") 98 | //save the note 99 | composeRule 100 | .onNodeWithContentDescription("Save note") 101 | .performClick() 102 | 103 | //Make sure there is a note in the list with title and content 104 | composeRule 105 | .onNodeWithText("test-title") 106 | .assertIsDisplayed() 107 | //Click on the note to edit it 108 | composeRule 109 | .onNodeWithText("test-title") 110 | .performClick() 111 | 112 | //Make sure title and content TextField contains note title and content 113 | composeRule 114 | .onNodeWithTag(TITLE_TEXT_FIELD) 115 | .assertTextEquals("test-title") 116 | composeRule 117 | .onNodeWithTag(CONTENT_TEXT_FIELD) 118 | .assertTextEquals("test-content") 119 | //add the text 2 to to title TextField 120 | composeRule 121 | .onNodeWithTag(TITLE_TEXT_FIELD) 122 | .performTextInput("2") 123 | //update the note 124 | composeRule 125 | .onNodeWithContentDescription("Save note") 126 | .performClick() 127 | 128 | //Make sure the update was aplied to the list 129 | composeRule 130 | .onNodeWithText("test-title2") 131 | .assertIsDisplayed() 132 | } 133 | 134 | @Test 135 | fun saveNewNotes_orderByTitleDescending() { 136 | //Save 3 notes 137 | for (i in 0..3) { 138 | //Click on FAB to get to add a note screen 139 | composeRule 140 | .onNodeWithContentDescription("Add note") 141 | .performClick() 142 | 143 | //Enter texts in title and content TextFields 144 | composeRule 145 | .onNodeWithTag(TITLE_TEXT_FIELD) 146 | .performTextInput(i.toString()) 147 | composeRule 148 | .onNodeWithTag(CONTENT_TEXT_FIELD) 149 | .performTextInput(i.toString()) 150 | //save the note 151 | composeRule 152 | .onNodeWithContentDescription("Save note") 153 | .performClick() 154 | 155 | } 156 | 157 | //Make sure the 3 note is being diplayed on the screen 158 | composeRule 159 | .onNodeWithText("1") 160 | .assertIsDisplayed() 161 | composeRule 162 | .onNodeWithText("2") 163 | .assertIsDisplayed() 164 | composeRule 165 | .onNodeWithText("3") 166 | .assertIsDisplayed() 167 | 168 | //Click on Sort button 169 | composeRule 170 | .onNodeWithContentDescription("Sort") 171 | .performClick() 172 | //Click on title radio button 173 | composeRule 174 | .onNodeWithContentDescription("Title") 175 | .performClick() 176 | //Click on descending radio button 177 | composeRule 178 | .onNodeWithContentDescription("Descending") 179 | .performClick() 180 | 181 | //Make sure the notes are ordered in a descending order 182 | composeRule 183 | .onAllNodesWithTag(NOTE_ITEM)[0] 184 | .assertTextContains("3") 185 | composeRule 186 | .onAllNodesWithTag(NOTE_ITEM)[1] 187 | .assertTextContains("2") 188 | composeRule 189 | .onAllNodesWithTag(NOTE_ITEM)[2] 190 | .assertTextContains("1") 191 | 192 | } 193 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/netoloboapps/noteapp/feature_note/presentation/notes/NotesScreenTest.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes 2 | 3 | import androidx.activity.compose.setContent 4 | import androidx.compose.animation.ExperimentalAnimationApi 5 | import androidx.compose.ui.test.assertIsDisplayed 6 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 7 | import androidx.compose.ui.test.onNodeWithContentDescription 8 | import androidx.compose.ui.test.onNodeWithTag 9 | import androidx.compose.ui.test.performClick 10 | import androidx.navigation.compose.NavHost 11 | import androidx.navigation.compose.composable 12 | import androidx.navigation.compose.rememberNavController 13 | import com.netoloboapps.noteapp.core.util.TestTags.ORDER_SECTION 14 | import com.netoloboapps.noteapp.di.AppModule 15 | import com.netoloboapps.noteapp.feature_note.presentation.MainActivity 16 | import com.netoloboapps.noteapp.feature_note.presentation.util.Screen 17 | import com.netoloboapps.noteapp.ui.theme.NoteAppTheme 18 | import dagger.hilt.android.testing.HiltAndroidRule 19 | import dagger.hilt.android.testing.HiltAndroidTest 20 | import dagger.hilt.android.testing.UninstallModules 21 | import org.junit.Assert.* 22 | import org.junit.Before 23 | 24 | import org.junit.Rule 25 | import org.junit.Test 26 | 27 | @HiltAndroidTest 28 | @UninstallModules(AppModule::class) 29 | class NotesScreenTest { 30 | 31 | @get:Rule(order = 0) 32 | val hiltRule = HiltAndroidRule(this) 33 | 34 | @get:Rule(order = 1) 35 | val composeRule = createAndroidComposeRule() 36 | 37 | @ExperimentalAnimationApi 38 | @Before 39 | fun setUp() { 40 | hiltRule.inject() 41 | composeRule.activity.setContent { 42 | val navController = rememberNavController() 43 | NoteAppTheme { 44 | NavHost( 45 | navController = navController, 46 | startDestination = Screen.NotesScreen.route 47 | ) { 48 | composable(route = Screen.NotesScreen.route) { 49 | NotesScreen( 50 | navController = navController 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | @Test 59 | fun clickToggleOrderSection_isVisible() { 60 | composeRule.onNodeWithTag(ORDER_SECTION).assertDoesNotExist() 61 | composeRule.onNodeWithContentDescription("Sort").performClick() 62 | composeRule.onNodeWithTag(ORDER_SECTION).assertIsDisplayed() 63 | 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/NotesApp.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class NotesApp : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/core/util/TestTags.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.core.util 2 | 3 | object TestTags { 4 | const val ORDER_SECTION = "ORDER_SECTION" 5 | const val TITLE_TEXT_FIELD = "TITLE_TEXT_FIELD" 6 | const val CONTENT_TEXT_FIELD = "CONTENT_TEXT_FIELD" 7 | const val NOTE_ITEM = "NOTE_ITEM" 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.di 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import com.netoloboapps.noteapp.feature_note.data.data_source.NoteDatabase 6 | import com.netoloboapps.noteapp.feature_note.data.repository.NoteRepositoryImpl 7 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 8 | import com.netoloboapps.noteapp.feature_note.domain.use_case.* 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object AppModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideNoteDatabase(app: Application): NoteDatabase = Room.databaseBuilder( 22 | app, 23 | NoteDatabase::class.java, 24 | NoteDatabase.DATABASE_NAME 25 | ).build() 26 | 27 | @Provides 28 | @Singleton 29 | fun providesNoteRepository(db: NoteDatabase): NoteRepository = NoteRepositoryImpl(db.noteDao) 30 | 31 | @Provides 32 | @Singleton 33 | fun provideNoteUseCases(repository: NoteRepository): NoteUseCases = NoteUseCases( 34 | getNotesUseCase = GetNotesUseCase(repository), 35 | deleteNoteUseCase = DeleteNoteUseCase(repository), 36 | addNoteUseCase = AddNoteUseCase(repository), 37 | getNoteUseCase = GetNoteUseCase(repository) 38 | ) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/data/data_source/NoteDao.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.data.data_source 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | @Dao 12 | interface NoteDao { 13 | @Query("SELECT * FROM note") 14 | fun getNotes(): Flow> 15 | 16 | @Query("SELECT *FROM note WHERE id = :id") 17 | suspend fun getNoteById(id: Int): Note? 18 | 19 | @Insert(onConflict = OnConflictStrategy.REPLACE) 20 | suspend fun insertNote(note: Note) 21 | 22 | @Delete 23 | suspend fun deleteNote(note: Note) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/data/data_source/NoteDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.data.data_source 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 6 | 7 | @Database( 8 | entities = [Note::class], 9 | version = 1, 10 | exportSchema = false 11 | ) 12 | abstract class NoteDatabase : RoomDatabase() { 13 | 14 | abstract val noteDao: NoteDao 15 | 16 | companion object { 17 | const val DATABASE_NAME = "notes_db" 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/data/repository/NoteRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.data.repository 2 | 3 | import com.netoloboapps.noteapp.feature_note.data.data_source.NoteDao 4 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 5 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class NoteRepositoryImpl( 9 | private val dao: NoteDao 10 | ) : NoteRepository { 11 | override fun getNotes(): Flow> = dao.getNotes() 12 | 13 | override suspend fun getNoteById(id: Int): Note? = dao.getNoteById(id) 14 | 15 | override suspend fun inserteNote(note: Note) = dao.insertNote(note) 16 | 17 | override suspend fun deleteNote(note: Note) = dao.deleteNote(note) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/model/Note.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.netoloboapps.noteapp.ui.theme.* 6 | 7 | @Entity 8 | data class Note( 9 | val title: String, 10 | val content: String, 11 | val timestamp: Long, 12 | val color: Int, 13 | @PrimaryKey val id: Int? = null 14 | ) { 15 | companion object { 16 | val noteColors = listOf( 17 | Purple80, 18 | PurpleGrey80, 19 | Purple40, 20 | Pink80, 21 | Pink40 22 | ) 23 | } 24 | } 25 | 26 | class InvalidNoteException(message: String) : Exception(message) 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/repository/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.repository 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface NoteRepository { 7 | 8 | fun getNotes(): Flow> 9 | 10 | suspend fun getNoteById(id: Int): Note? 11 | 12 | suspend fun inserteNote(note: Note) 13 | 14 | suspend fun deleteNote(note: Note) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/use_case/AddNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.use_case 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.model.InvalidNoteException 4 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 5 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 6 | 7 | class AddNoteUseCase( 8 | private val repository: NoteRepository 9 | ) { 10 | 11 | @Throws(InvalidNoteException::class) 12 | suspend operator fun invoke(note: Note) { 13 | if (note.title.isBlank()) { 14 | throw InvalidNoteException("The title of the note can't be empty.") 15 | } 16 | if (note.content.isBlank()) { 17 | throw InvalidNoteException("The content of the note can't be empty.") 18 | } 19 | repository.inserteNote(note) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/use_case/DeleteNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.use_case 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 4 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 5 | 6 | class DeleteNoteUseCase( 7 | private val repository: NoteRepository 8 | ) { 9 | suspend operator fun invoke(note: Note) = repository.deleteNote(note) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/use_case/GetNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.use_case 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 4 | 5 | class GetNoteUseCase( 6 | private val repository: NoteRepository 7 | ) { 8 | suspend operator fun invoke(id: Int) = repository.getNoteById(id) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/use_case/GetNotesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.use_case 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 4 | import com.netoloboapps.noteapp.feature_note.domain.repository.NoteRepository 5 | import com.netoloboapps.noteapp.feature_note.domain.util.NoteOrder 6 | import com.netoloboapps.noteapp.feature_note.domain.util.OrderType 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | 10 | class GetNotesUseCase( 11 | private val repository: NoteRepository 12 | ) { 13 | operator fun invoke( 14 | noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending) 15 | ): Flow> = repository.getNotes().map { notes -> 16 | when (noteOrder.orderType) { 17 | is OrderType.Ascending -> { 18 | when (noteOrder) { 19 | is NoteOrder.Title -> notes.sortedBy { it.title.lowercase() } 20 | is NoteOrder.Date -> notes.sortedBy { it.timestamp } 21 | is NoteOrder.Color -> notes.sortedBy { it.color } 22 | } 23 | } 24 | 25 | is OrderType.Descending -> { 26 | when (noteOrder) { 27 | is NoteOrder.Title -> notes.sortedByDescending { it.title.lowercase() } 28 | is NoteOrder.Date -> notes.sortedByDescending { it.timestamp } 29 | is NoteOrder.Color -> notes.sortedByDescending { it.color } 30 | } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/use_case/NoteUseCases.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.use_case 2 | 3 | data class NoteUseCases( 4 | val getNotesUseCase: GetNotesUseCase, 5 | val deleteNoteUseCase: DeleteNoteUseCase, 6 | val addNoteUseCase: AddNoteUseCase, 7 | val getNoteUseCase: GetNoteUseCase 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/util/NoteOrder.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.util 2 | 3 | sealed class NoteOrder(val orderType: OrderType) { 4 | class Title(orderType: OrderType) : NoteOrder(orderType) 5 | class Date(orderType: OrderType) : NoteOrder(orderType) 6 | class Color(orderType: OrderType) : NoteOrder(orderType) 7 | 8 | fun copy(orderType: OrderType): NoteOrder = when (this) { 9 | is Title -> Title(orderType) 10 | is Date -> Date(orderType) 11 | is Color -> Color(orderType) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/domain/util/OrderType.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.domain.util 2 | 3 | sealed class OrderType { 4 | object Ascending : OrderType() 5 | object Descending : OrderType() 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.navigation.NavType 9 | import androidx.navigation.compose.NavHost 10 | import androidx.navigation.compose.composable 11 | import androidx.navigation.compose.rememberNavController 12 | import androidx.navigation.navArgument 13 | import com.netoloboapps.noteapp.feature_note.presentation.add_edit_note.AddEditNoteScreen 14 | import com.netoloboapps.noteapp.feature_note.presentation.notes.NotesScreen 15 | import com.netoloboapps.noteapp.feature_note.presentation.util.Screen 16 | import com.netoloboapps.noteapp.ui.theme.NoteAppTheme 17 | import dagger.hilt.android.AndroidEntryPoint 18 | 19 | @AndroidEntryPoint 20 | class MainActivity : ComponentActivity() { 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContent { 24 | NoteAppTheme { 25 | Surface( 26 | color = MaterialTheme.colorScheme.background 27 | ) { 28 | val navController = rememberNavController() 29 | NavHost( 30 | navController = navController, 31 | startDestination = Screen.NotesScreen.route 32 | ) { 33 | composable(route = Screen.NotesScreen.route) { 34 | NotesScreen(navController = navController) 35 | } 36 | composable(route = Screen.AddEditNoteScreen.route + 37 | "?noteId={noteId}¬eColor={noteColor}", 38 | arguments = listOf( 39 | navArgument( 40 | name = "noteId" 41 | ) { 42 | type = NavType.IntType 43 | defaultValue = -1 44 | }, 45 | navArgument( 46 | name = "noteColor" 47 | ) { 48 | type = NavType.IntType 49 | defaultValue = -1 50 | } 51 | ) 52 | ) { 53 | val color = it.arguments?.getInt("noteColor") ?: -1 54 | AddEditNoteScreen( 55 | navController = navController, 56 | noteColor = color 57 | ) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/add_edit_note/AddEditNoteEvent.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.add_edit_note 2 | 3 | import androidx.compose.ui.focus.FocusState 4 | 5 | sealed class AddEditNoteEvent { 6 | data class EnteredTitle(val value: String) : AddEditNoteEvent() 7 | data class ChangeTitleFocus(val focusState: FocusState) : AddEditNoteEvent() 8 | data class EnteredContent(val value: String) : AddEditNoteEvent() 9 | data class ChangeContentFocus(val focusState: FocusState) : AddEditNoteEvent() 10 | data class ChangeColor(val color: Int) : AddEditNoteEvent() 11 | object SaveNote : AddEditNoteEvent() 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/add_edit_note/AddEditNoteScreen.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.add_edit_note 2 | 3 | import androidx.compose.animation.Animatable 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.shape.CircleShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Save 12 | import androidx.compose.material3.* 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.rememberCoroutineScope 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.draw.shadow 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.toArgb 22 | import androidx.compose.ui.unit.dp 23 | import androidx.hilt.navigation.compose.hiltViewModel 24 | import androidx.navigation.NavController 25 | import com.netoloboapps.noteapp.core.util.TestTags.CONTENT_TEXT_FIELD 26 | import com.netoloboapps.noteapp.core.util.TestTags.TITLE_TEXT_FIELD 27 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 28 | import com.netoloboapps.noteapp.feature_note.presentation.add_edit_note.components.TransparentHintTextField 29 | import kotlinx.coroutines.flow.collectLatest 30 | import kotlinx.coroutines.launch 31 | 32 | @Composable 33 | fun AddEditNoteScreen( 34 | navController: NavController, 35 | noteColor: Int, 36 | viewModel: AddEditNoteViewModel = hiltViewModel() 37 | ) { 38 | val titleState = viewModel.noteTile.value 39 | val contentState = viewModel.noteContent.value 40 | val snackbarHostState = remember { SnackbarHostState() } 41 | val noteBackgroundAnimatable = remember { 42 | Animatable( 43 | Color(if (noteColor != -1) noteColor else viewModel.noteColor.value) 44 | ) 45 | } 46 | val scope = rememberCoroutineScope() 47 | 48 | LaunchedEffect(key1 = true) { 49 | viewModel.eventFlow.collectLatest { event -> 50 | when (event) { 51 | is AddEditNoteViewModel.UiEvent.ShowSnackBar -> { 52 | snackbarHostState.showSnackbar( 53 | message = event.message 54 | ) 55 | } 56 | 57 | is AddEditNoteViewModel.UiEvent.SaveNote -> { 58 | navController.navigateUp() 59 | } 60 | } 61 | } 62 | } 63 | 64 | Scaffold( 65 | floatingActionButton = { 66 | FloatingActionButton( 67 | onClick = { 68 | viewModel.onEvent(AddEditNoteEvent.SaveNote) 69 | }, 70 | contentColor = MaterialTheme.colorScheme.primary 71 | ) { 72 | Icon( 73 | imageVector = Icons.Default.Save, 74 | contentDescription = "Save note" 75 | ) 76 | } 77 | }, 78 | snackbarHost = { SnackbarHost(hostState = snackbarHostState) } 79 | ) { paddingValues -> 80 | Column( 81 | modifier = Modifier 82 | .fillMaxSize() 83 | .background(noteBackgroundAnimatable.value) 84 | .padding(paddingValues) 85 | .padding(16.dp) 86 | ) { 87 | Row( 88 | modifier = Modifier 89 | .fillMaxWidth() 90 | .padding(8.dp), 91 | horizontalArrangement = Arrangement.SpaceBetween 92 | ) { 93 | Note.noteColors.forEach { color -> 94 | val colorInt = color.toArgb() 95 | Box( 96 | modifier = Modifier 97 | .size(50.dp) 98 | .shadow(15.dp, CircleShape) 99 | .clip(CircleShape) 100 | .background(color) 101 | .border( 102 | width = 3.dp, 103 | color = if (viewModel.noteColor.value == colorInt) { 104 | Color.Black 105 | } else Color.Transparent, 106 | shape = CircleShape 107 | ) 108 | .clickable { 109 | scope.launch { 110 | noteBackgroundAnimatable.animateTo( 111 | targetValue = Color(colorInt), 112 | animationSpec = tween( 113 | durationMillis = 500 114 | ) 115 | ) 116 | } 117 | viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt)) 118 | } 119 | ) 120 | } 121 | } 122 | Spacer(modifier = Modifier.height(16.dp)) 123 | TransparentHintTextField( 124 | text = titleState.text, 125 | hint = titleState.hint, 126 | onValueChage = { 127 | viewModel.onEvent(AddEditNoteEvent.EnteredTitle(it)) 128 | }, 129 | onFocuChange = { 130 | viewModel.onEvent(AddEditNoteEvent.ChangeTitleFocus(it)) 131 | }, 132 | isHintVisible = titleState.isHintVisible, 133 | singleLine = true, 134 | textStyle = MaterialTheme.typography.headlineLarge, 135 | testTag = TITLE_TEXT_FIELD 136 | ) 137 | Spacer(modifier = Modifier.height(16.dp)) 138 | TransparentHintTextField( 139 | text = contentState.text, 140 | hint = contentState.hint, 141 | onValueChage = { 142 | viewModel.onEvent(AddEditNoteEvent.EnteredContent(it)) 143 | }, 144 | onFocuChange = { 145 | viewModel.onEvent(AddEditNoteEvent.ChangeContentFocus(it)) 146 | }, 147 | isHintVisible = contentState.isHintVisible, 148 | textStyle = MaterialTheme.typography.bodyLarge, 149 | modifier = Modifier 150 | .fillMaxHeight(), 151 | testTag = CONTENT_TEXT_FIELD 152 | ) 153 | 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/add_edit_note/AddEditNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.add_edit_note 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.ui.graphics.toArgb 6 | import androidx.lifecycle.SavedStateHandle 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.netoloboapps.noteapp.feature_note.domain.model.InvalidNoteException 10 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 11 | import com.netoloboapps.noteapp.feature_note.domain.use_case.NoteUseCases 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.flow.MutableSharedFlow 14 | import kotlinx.coroutines.flow.asSharedFlow 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class AddEditNoteViewModel @Inject constructor( 20 | private val noteUseCases: NoteUseCases, 21 | savedStateHandle: SavedStateHandle 22 | ) : ViewModel() { 23 | private val _noteTitle = mutableStateOf( 24 | NoteTextFieldState( 25 | hint = "Enter title..." 26 | ) 27 | ) 28 | val noteTile: State = _noteTitle 29 | 30 | private val _noteContent = mutableStateOf( 31 | NoteTextFieldState( 32 | hint = "Enter some content..." 33 | ) 34 | ) 35 | val noteContent: State = _noteContent 36 | 37 | private val _noteColor = mutableStateOf(Note.noteColors.random().toArgb()) 38 | val noteColor: State = _noteColor 39 | 40 | private val _eventFlow = MutableSharedFlow() 41 | val eventFlow = _eventFlow.asSharedFlow() 42 | 43 | private var currentNoteId: Int? = null 44 | 45 | init { 46 | savedStateHandle.get("noteId")?.let { noteId -> 47 | if (noteId != -1) { 48 | viewModelScope.launch { 49 | noteUseCases.getNoteUseCase(noteId)?.also { note -> 50 | currentNoteId = note.id 51 | _noteTitle.value = noteTile.value.copy( 52 | text = note.title, 53 | isHintVisible = false 54 | ) 55 | _noteContent.value = noteContent.value.copy( 56 | text = note.content, 57 | isHintVisible = false 58 | ) 59 | _noteColor.value = note.color 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | fun onEvent(event: AddEditNoteEvent) { 67 | when (event) { 68 | is AddEditNoteEvent.EnteredTitle -> { 69 | _noteTitle.value = noteTile.value.copy( 70 | text = event.value 71 | ) 72 | } 73 | 74 | is AddEditNoteEvent.ChangeTitleFocus -> { 75 | _noteTitle.value = noteTile.value.copy( 76 | isHintVisible = !event.focusState.isFocused && 77 | noteTile.value.text.isBlank() 78 | ) 79 | } 80 | 81 | is AddEditNoteEvent.EnteredContent -> { 82 | _noteContent.value = noteContent.value.copy( 83 | text = event.value 84 | ) 85 | } 86 | 87 | is AddEditNoteEvent.ChangeContentFocus -> { 88 | _noteContent.value = noteContent.value.copy( 89 | isHintVisible = !event.focusState.isFocused 90 | ) 91 | } 92 | 93 | is AddEditNoteEvent.ChangeColor -> { 94 | _noteColor.value = event.color 95 | } 96 | 97 | is AddEditNoteEvent.SaveNote -> { 98 | viewModelScope.launch { 99 | try { 100 | noteUseCases.addNoteUseCase( 101 | Note( 102 | title = noteTile.value.text, 103 | content = noteContent.value.text, 104 | timestamp = System.currentTimeMillis(), 105 | color = noteColor.value, 106 | id = currentNoteId 107 | ) 108 | ) 109 | _eventFlow.emit(UiEvent.SaveNote) 110 | } catch (e: InvalidNoteException) { 111 | _eventFlow.emit( 112 | UiEvent.ShowSnackBar( 113 | message = e.message ?: "Could save note" 114 | ) 115 | ) 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | sealed class UiEvent { 123 | data class ShowSnackBar(val message: String) : UiEvent() 124 | object SaveNote : UiEvent() 125 | } 126 | 127 | 128 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/add_edit_note/NoteTextFieldState.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.add_edit_note 2 | 3 | data class NoteTextFieldState( 4 | val text: String = "", 5 | val hint: String = "", 6 | val isHintVisible: Boolean = true 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/add_edit_note/components/TransparentHintTextField.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.add_edit_note.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.text.BasicTextField 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.focus.FocusState 10 | import androidx.compose.ui.focus.onFocusChanged 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.platform.testTag 13 | import androidx.compose.ui.text.TextStyle 14 | 15 | @Composable 16 | fun TransparentHintTextField( 17 | text: String, 18 | hint: String, 19 | modifier: Modifier = Modifier, 20 | isHintVisible: Boolean = true, 21 | onValueChage: (String) -> Unit, 22 | textStyle: TextStyle = TextStyle(), 23 | singleLine: Boolean = false, 24 | onFocuChange: (FocusState) -> Unit, 25 | testTag: String 26 | ) { 27 | Box( 28 | modifier = modifier, 29 | ) { 30 | BasicTextField( 31 | value = text, 32 | onValueChange = onValueChage, 33 | singleLine = singleLine, 34 | textStyle = textStyle, 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .onFocusChanged { onFocuChange(it) } 38 | .testTag(testTag) 39 | 40 | ) 41 | if (isHintVisible) { 42 | Text( 43 | text = hint, 44 | style = textStyle, 45 | color = Color.DarkGray 46 | ) 47 | } 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/NotesEvent.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 4 | import com.netoloboapps.noteapp.feature_note.domain.util.NoteOrder 5 | 6 | sealed class NotesEvent { 7 | data class Order(val noteOrder: NoteOrder) : NotesEvent() 8 | data class DeleteNote(val note: Note) : NotesEvent() 9 | object RestoreNote : NotesEvent() 10 | object ToggleOrderSection : NotesEvent() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/NotesScreen.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.Add 10 | import androidx.compose.material.icons.filled.Sort 11 | import androidx.compose.material3.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.testTag 18 | import androidx.compose.ui.unit.dp 19 | import androidx.hilt.navigation.compose.hiltViewModel 20 | import androidx.navigation.NavController 21 | import com.netoloboapps.noteapp.core.util.TestTags.ORDER_SECTION 22 | import com.netoloboapps.noteapp.feature_note.presentation.notes.components.NoteItem 23 | import com.netoloboapps.noteapp.feature_note.presentation.notes.components.OrderSection 24 | import com.netoloboapps.noteapp.feature_note.presentation.util.Screen 25 | import kotlinx.coroutines.launch 26 | 27 | @Composable 28 | fun NotesScreen( 29 | navController: NavController, 30 | viewModel: NotesViewModel = hiltViewModel() 31 | ) { 32 | val state = viewModel.state.value 33 | val snackbarHostState = remember { SnackbarHostState() } 34 | val scope = rememberCoroutineScope() 35 | 36 | Scaffold( 37 | floatingActionButton = { 38 | FloatingActionButton( 39 | onClick = { 40 | navController.navigate(Screen.AddEditNoteScreen.route) 41 | }, 42 | contentColor = MaterialTheme.colorScheme.primary 43 | ) { 44 | Icon(imageVector = Icons.Default.Add, contentDescription = "Add note") 45 | } 46 | }, 47 | snackbarHost = { SnackbarHost(hostState = snackbarHostState) } 48 | ) { paddingValues -> 49 | 50 | Column( 51 | modifier = Modifier 52 | .fillMaxSize() 53 | .padding(paddingValues) 54 | .padding(16.dp) 55 | 56 | ) { 57 | Row( 58 | modifier = Modifier.fillMaxWidth(), 59 | horizontalArrangement = Arrangement.SpaceBetween, 60 | verticalAlignment = Alignment.CenterVertically 61 | ) { 62 | Text( 63 | text = "Your Note", 64 | style = MaterialTheme.typography.headlineLarge 65 | ) 66 | IconButton( 67 | onClick = { 68 | viewModel.onEvent(NotesEvent.ToggleOrderSection) 69 | } 70 | ) { 71 | Icon( 72 | imageVector = Icons.Default.Sort, 73 | contentDescription = "Sort" 74 | ) 75 | } 76 | } 77 | AnimatedVisibility( 78 | visible = state.isOrderSectionVisible, 79 | enter = fadeIn() + slideInVertically(), 80 | exit = fadeOut() + slideOutVertically() 81 | ) { 82 | OrderSection( 83 | modifier = Modifier 84 | .fillMaxWidth() 85 | .padding(vertical = 16.dp) 86 | .testTag(ORDER_SECTION), 87 | noteOrder = state.noteOrder, 88 | onOrderChange = { 89 | viewModel.onEvent(NotesEvent.Order(it)) 90 | } 91 | ) 92 | } 93 | 94 | Spacer(modifier = Modifier.height(16.dp)) 95 | 96 | LazyColumn(modifier = Modifier.fillMaxSize()) { 97 | items(state.notes) { note -> 98 | NoteItem( 99 | note = note, 100 | modifier = Modifier 101 | .fillMaxWidth() 102 | .clickable { 103 | navController.navigate( 104 | Screen.AddEditNoteScreen.route + 105 | "?noteId=${note.id}¬eColor=${note.color}" 106 | ) 107 | }, 108 | onDeleteClick = { 109 | viewModel.onEvent(NotesEvent.DeleteNote(note)) 110 | scope.launch { 111 | val result = snackbarHostState.showSnackbar( 112 | message = "Note Deleted", 113 | actionLabel = "Undo", 114 | duration = SnackbarDuration.Short 115 | ) 116 | if (result == SnackbarResult.ActionPerformed) { 117 | viewModel.onEvent(NotesEvent.RestoreNote) 118 | } 119 | } 120 | } 121 | ) 122 | Spacer(modifier = Modifier.height(16.dp)) 123 | } 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/NotesState.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes 2 | 3 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 4 | import com.netoloboapps.noteapp.feature_note.domain.util.NoteOrder 5 | import com.netoloboapps.noteapp.feature_note.domain.util.OrderType 6 | 7 | data class NotesState( 8 | val notes: List = emptyList(), 9 | val noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), 10 | val isOrderSectionVisible: Boolean = false 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/NotesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 8 | import com.netoloboapps.noteapp.feature_note.domain.use_case.NoteUseCases 9 | import com.netoloboapps.noteapp.feature_note.domain.util.NoteOrder 10 | import com.netoloboapps.noteapp.feature_note.domain.util.OrderType 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.Job 13 | import kotlinx.coroutines.flow.launchIn 14 | import kotlinx.coroutines.flow.onEach 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class NotesViewModel @Inject constructor( 20 | private val noteUseCases: NoteUseCases 21 | ) : ViewModel() { 22 | 23 | private val _state = mutableStateOf(NotesState()) 24 | val state: State = _state 25 | private var recentlyDeletedNote: Note? = null 26 | private var getNotesJob: Job? = null 27 | 28 | init { 29 | getNotes(NoteOrder.Date(OrderType.Descending)) 30 | } 31 | 32 | fun onEvent(event: NotesEvent) { 33 | when (event) { 34 | is NotesEvent.Order -> { 35 | if (state.value.noteOrder::class == event.noteOrder::class && 36 | state.value.noteOrder.orderType == event.noteOrder.orderType 37 | ) { 38 | return 39 | } 40 | getNotes(event.noteOrder) 41 | } 42 | 43 | is NotesEvent.DeleteNote -> { 44 | viewModelScope.launch { 45 | noteUseCases.deleteNoteUseCase(event.note) 46 | recentlyDeletedNote = event.note 47 | } 48 | } 49 | 50 | is NotesEvent.RestoreNote -> { 51 | viewModelScope.launch { 52 | noteUseCases.addNoteUseCase(recentlyDeletedNote ?: return@launch) 53 | recentlyDeletedNote = null 54 | } 55 | } 56 | 57 | is NotesEvent.ToggleOrderSection -> { 58 | _state.value = _state.value.copy( 59 | isOrderSectionVisible = !state.value.isOrderSectionVisible 60 | ) 61 | } 62 | } 63 | } 64 | 65 | private fun getNotes(noteOrder: NoteOrder) { 66 | getNotesJob?.cancel() 67 | getNotesJob = noteUseCases.getNotesUseCase(noteOrder) 68 | .onEach { notes -> 69 | _state.value = _state.value.copy( 70 | notes = notes, 71 | noteOrder = noteOrder 72 | ) 73 | } 74 | .launchIn(viewModelScope) 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/components/DefaultRadioButton.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.RadioButton 8 | import androidx.compose.material3.RadioButtonDefaults 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.semantics.contentDescription 14 | import androidx.compose.ui.semantics.semantics 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun DefaultRadioButton( 19 | text: String, 20 | selected: Boolean, 21 | onSelect: () -> Unit, 22 | modifier: Modifier = Modifier 23 | ) { 24 | Row( 25 | modifier = modifier, 26 | verticalAlignment = Alignment.CenterVertically 27 | ) { 28 | RadioButton( 29 | selected = selected, 30 | onClick = onSelect, 31 | colors = RadioButtonDefaults.colors( 32 | selectedColor = MaterialTheme.colorScheme.primary, 33 | unselectedColor = MaterialTheme.colorScheme.onBackground 34 | ), 35 | modifier = Modifier.semantics { 36 | contentDescription = text 37 | } 38 | ) 39 | Spacer(modifier = Modifier.width(8.dp)) 40 | Text( 41 | text = text, 42 | style = MaterialTheme.typography.bodyLarge 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/components/NoteItem.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes.components 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Delete 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.geometry.CornerRadius 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.geometry.Size 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.graphics.Path 19 | import androidx.compose.ui.graphics.drawscope.clipPath 20 | import androidx.compose.ui.platform.testTag 21 | import androidx.compose.ui.text.style.TextOverflow 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | import androidx.core.graphics.ColorUtils 25 | import com.netoloboapps.noteapp.core.util.TestTags.NOTE_ITEM 26 | import com.netoloboapps.noteapp.feature_note.domain.model.Note 27 | 28 | @Composable 29 | fun NoteItem( 30 | note: Note, 31 | modifier: Modifier = Modifier, 32 | cornerRadius: Dp = 10.dp, 33 | cutCornerSize: Dp = 30.dp, 34 | onDeleteClick: () -> Unit 35 | ) { 36 | Box( 37 | modifier = modifier 38 | .testTag(NOTE_ITEM), 39 | 40 | ) { 41 | Canvas(modifier = Modifier.matchParentSize()) { 42 | val clipPath = Path().apply { 43 | lineTo(x = size.width - cutCornerSize.toPx(), y = 0f) 44 | lineTo(x = size.width, cutCornerSize.toPx()) 45 | lineTo(x = size.width, size.height) 46 | lineTo(x = 0f, size.height) 47 | close() 48 | } 49 | 50 | clipPath(clipPath) { 51 | drawRoundRect( 52 | color = Color(note.color), 53 | size = size, 54 | cornerRadius = CornerRadius(cornerRadius.toPx()) 55 | ) 56 | drawRoundRect( 57 | color = Color(ColorUtils.blendARGB(note.color, 0x000000, 0.2f)), 58 | topLeft = Offset(size.width - cutCornerSize.toPx(), -100f), 59 | size = Size(cutCornerSize.toPx() + 100f, cutCornerSize.toPx() + 100f), 60 | cornerRadius = CornerRadius(cornerRadius.toPx()) 61 | ) 62 | } 63 | } 64 | Column( 65 | modifier = Modifier 66 | .fillMaxSize() 67 | .padding(16.dp) 68 | .padding(end = 32.dp) 69 | ) { 70 | 71 | Text( 72 | text = note.title, 73 | style = MaterialTheme.typography.labelLarge, 74 | color = MaterialTheme.colorScheme.onSurface, 75 | maxLines = 1, 76 | overflow = TextOverflow.Ellipsis 77 | ) 78 | 79 | Spacer(modifier = Modifier.height(8.dp)) 80 | 81 | Text( 82 | text = note.content, 83 | style = MaterialTheme.typography.bodyLarge, 84 | color = MaterialTheme.colorScheme.onSurface, 85 | maxLines = 10, 86 | overflow = TextOverflow.Ellipsis 87 | ) 88 | } 89 | 90 | IconButton( 91 | onClick = onDeleteClick, 92 | modifier = Modifier.align(Alignment.BottomEnd) 93 | ) { 94 | Icon( 95 | imageVector = Icons.Default.Delete, 96 | contentDescription = "Delete note", 97 | tint = MaterialTheme.colorScheme.onSurface 98 | ) 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/notes/components/OrderSection.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.notes.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.unit.dp 7 | import com.netoloboapps.noteapp.feature_note.domain.util.NoteOrder 8 | import com.netoloboapps.noteapp.feature_note.domain.util.OrderType 9 | 10 | @Composable 11 | fun OrderSection( 12 | modifier: Modifier = Modifier, 13 | noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), 14 | onOrderChange: (NoteOrder) -> Unit 15 | ) { 16 | Column( 17 | modifier = modifier, 18 | ) { 19 | Row(modifier = Modifier.fillMaxWidth()) { 20 | 21 | DefaultRadioButton( 22 | text = "Title", 23 | selected = noteOrder is NoteOrder.Title, 24 | onSelect = { 25 | onOrderChange(NoteOrder.Title(noteOrder.orderType)) 26 | }) 27 | 28 | Spacer(modifier = Modifier.width(8.dp)) 29 | 30 | DefaultRadioButton( 31 | text = "Date", 32 | selected = noteOrder is NoteOrder.Date, 33 | onSelect = { 34 | onOrderChange(NoteOrder.Date(noteOrder.orderType)) 35 | }) 36 | 37 | Spacer(modifier = Modifier.width(8.dp)) 38 | 39 | DefaultRadioButton( 40 | text = "Color", 41 | selected = noteOrder is NoteOrder.Color, 42 | onSelect = { 43 | onOrderChange(NoteOrder.Color(noteOrder.orderType)) 44 | }) 45 | 46 | } 47 | Spacer(modifier = Modifier.height(8.dp)) 48 | 49 | Row(modifier = Modifier.fillMaxWidth()) { 50 | 51 | DefaultRadioButton( 52 | text = "Ascending", 53 | selected = noteOrder.orderType is OrderType.Ascending, 54 | onSelect = { 55 | onOrderChange(noteOrder.copy(OrderType.Ascending)) 56 | }) 57 | 58 | Spacer(modifier = Modifier.width(8.dp)) 59 | 60 | DefaultRadioButton( 61 | text = "Descending", 62 | selected = noteOrder.orderType is OrderType.Descending, 63 | onSelect = { 64 | onOrderChange(noteOrder.copy(OrderType.Descending)) 65 | }) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/feature_note/presentation/util/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.feature_note.presentation.util 2 | 3 | sealed class Screen(val route: String) { 4 | object NotesScreen : Screen("notes_sreen") 5 | object AddEditNoteScreen : Screen("add_edit_note_screen") 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun NoteAppTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 59 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/netoloboapps/noteapp/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.netoloboapps.noteapp.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netolobo/notes_app_clean_architecture/628e98f80e160e37f7c99a66d571d93eb21c32a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Note App 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |