├── .github └── workflows │ └── detekt-analysis.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── sonarIssues.xml └── vcs.xml ├── CONTRIBUTING.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── android │ │ └── dayplanner │ │ └── app │ │ ├── BaseUIClass.kt │ │ ├── TaskPlannerTests.kt │ │ └── screens │ │ ├── HomeScreen.kt │ │ └── NewTaskScreen.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── android │ │ │ └── dayplanner │ │ │ └── app │ │ │ ├── Extensions.kt │ │ │ ├── data │ │ │ ├── Task.kt │ │ │ ├── TaskDao.kt │ │ │ ├── TaskDatabase.kt │ │ │ └── TasksRepository.kt │ │ │ └── ui │ │ │ ├── MainActivity.kt │ │ │ ├── detail │ │ │ ├── TaskDetailsFragment.kt │ │ │ ├── TaskDetailsViewModel.kt │ │ │ └── TaskViewModelFactory.kt │ │ │ ├── home │ │ │ ├── HomeFragment.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── HomeViewModelFactory.kt │ │ │ └── TasksRenderer.kt │ │ │ ├── resourceidling │ │ │ └── SimpleIdlingResource.kt │ │ │ └── tasks │ │ │ ├── TasksFragment.kt │ │ │ ├── TasksViewModel.kt │ │ │ └── TasksViewModelFactory.kt │ └── res │ │ ├── drawable-hdpi │ │ └── splash_logo.png │ │ ├── drawable-ldpi │ │ └── splash_logo.png │ │ ├── drawable-mdpi │ │ └── splash_logo.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── splash_logo.png │ │ ├── drawable-xxhdpi │ │ └── splash_logo.png │ │ ├── drawable-xxxhdpi │ │ └── splash_logo.png │ │ ├── drawable │ │ ├── ic_delete.xml │ │ ├── ic_launcher_background.xml │ │ ├── logo.xml │ │ └── splash_logo_background_circle.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── home_fragment.xml │ │ ├── item_task.xml │ │ ├── task_details_fragment.xml │ │ ├── tasks_fragment.xml │ │ └── tasks_list_placeholder.xml │ │ ├── menu │ │ └── home_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── android │ └── dayplanner │ └── app │ └── data │ └── TasksRepositoryTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Project Header.png └── android12_splash_screen.png └── settings.gradle.kts /.github/workflows/detekt-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow performs a static analysis of your Kotlin source code using 2 | # Detekt. 3 | # 4 | # Scans are triggered: 5 | # 1. On every push to default and protected branches 6 | # 2. On every Pull Request targeting the default branch 7 | # 3. On a weekly schedule 8 | # 4. Manually, on demand, via the "workflow_dispatch" event 9 | # 10 | # The workflow should work with no modifications, but you might like to use a 11 | # later version of the Detekt CLI by modifing the $DETEKT_RELEASE_TAG 12 | # environment variable. 13 | name: Scan with Detekt 14 | 15 | on: 16 | # Triggers the workflow on push or pull request events but only for default and protected branches 17 | push: 18 | branches: [ develop ] 19 | pull_request: 20 | branches: [ develop ] 21 | schedule: 22 | - cron: '30 4 * * 1' 23 | 24 | # Allows you to run this workflow manually from the Actions tab 25 | workflow_dispatch: 26 | 27 | env: 28 | # Release tag associated with version of Detekt to be installed 29 | # SARIF support (required for this workflow) was introduced in Detekt v1.15.0 30 | DETEKT_RELEASE_TAG: v1.15.0 31 | 32 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 33 | jobs: 34 | # This workflow contains a single job called "scan" 35 | scan: 36 | name: Scan 37 | # The type of runner that the job will run on 38 | runs-on: ubuntu-latest 39 | 40 | # Steps represent a sequence of tasks that will be executed as part of the job 41 | steps: 42 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 43 | - uses: actions/checkout@v2 44 | 45 | # Gets the download URL associated with the $DETEKT_RELEASE_TAG 46 | - name: Get Detekt download URL 47 | id: detekt_info 48 | env: 49 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | run: | 51 | DETEKT_DOWNLOAD_URL=$( gh api graphql --field tagName=$DETEKT_RELEASE_TAG --raw-field query=' 52 | query getReleaseAssetDownloadUrl($tagName: String!) { 53 | repository(name: "detekt", owner: "detekt") { 54 | release(tagName: $tagName) { 55 | releaseAssets(name: "detekt", first: 1) { 56 | nodes { 57 | downloadUrl 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ' | \ 64 | jq --raw-output '.data.repository.release.releaseAssets.nodes[0].downloadUrl' ) 65 | echo "::set-output name=download_url::$DETEKT_DOWNLOAD_URL" 66 | 67 | # Sets up the detekt cli 68 | - name: Setup Detekt 69 | run: | 70 | dest=$( mktemp -d ) 71 | curl --request GET \ 72 | --url ${{ steps.detekt_info.outputs.download_url }} \ 73 | --silent \ 74 | --location \ 75 | --output $dest/detekt 76 | chmod a+x $dest/detekt 77 | echo $dest >> $GITHUB_PATH 78 | 79 | # Performs static analysis using Detekt 80 | - name: Run Detekt 81 | continue-on-error: true 82 | run: | 83 | detekt --input ${{ github.workspace }} --report sarif:${{ github.workspace }}/detekt.sarif.json 84 | 85 | # Modifies the SARIF output produced by Detekt so that absolute URIs are relative 86 | # This is so we can easily map results onto their source files 87 | # This can be removed once relative URI support lands in Detekt: https://git.io/JLBbA 88 | - name: Make artifact location URIs relative 89 | continue-on-error: true 90 | run: | 91 | echo "$( 92 | jq \ 93 | --arg github_workspace ${{ github.workspace }} \ 94 | '. | ( .runs[].results[].locations[].physicalLocation.artifactLocation.uri |= if test($github_workspace) then .[($github_workspace | length | . + 1):] else . end )' \ 95 | ${{ github.workspace }}/detekt.sarif.json 96 | )" > ${{ github.workspace }}/detekt.sarif.json 97 | 98 | # Uploads results to GitHub repository using the upload-sarif action 99 | - uses: github/codeql-action/upload-sarif@v1 100 | with: 101 | # Path to SARIF file relative to the root of the repository 102 | sarif_file: ${{ github.workspace }}/detekt.sarif.json 103 | checkout_path: ${{ github.workspace }} 104 | -------------------------------------------------------------------------------- /.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 | android-dayplanner-app -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /.idea/sonarIssues.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 438 | 439 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 1- Pull `develop` branch 2 | 3 | 2- Create new feature branch from `develop` 4 | - Always keep the readable name for the feature. 5 | - Please note that feature branch shouldn't have too many changes. Keep it as much small as possible. 6 | 7 | 3- Push you feature branch on Github 8 | 9 | 4- Create pull request 10 | 11 | 5- Merge it after getting enough approvals 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Day Planner 2 | Day Planner helps user to create tasks for daily routine work. It will be showing all the pending tasks on home screen. I have written this app using in Kotlin. 3 | I use this app as a playground for all the new stuff I learn and implement. 4 | 5 | ![alt text](https://github.com/JunydDEV/android-dayplanner-app/blob/develop/images/Project%20Header.png) 6 | 7 | 8 | # Android Stack 9 | I have used MVVM architecture in this project along with following Android Jetpack Components. 10 |

✅ Navigation Components

11 |

✅ Room Data Persistence

12 |

✅ LiveData and ViewModel

13 |

✅ Views Binding

14 | 15 | # Android 12 16 | This app contains following Android 12 improvemetns and changes. 17 |

✅ Splash API [Migration guide]

18 | 19 | 20 | # Testing Frameworks 21 | I have used following Android UI and Unit testing frameworks in this project. 22 |

✅ JUnit 4

23 |

✅ Mockito

24 |

✅ Espresso

25 |

✅ Kakao

26 | 27 | # Kotlin Features 28 | I have used following features of Kotlin in this project. 29 |

✅ High Order functions

30 |

✅ Coroutines

31 |

✅ Scope Functions

32 |

✅ Delegates

33 |

✅ Gradle Kotlin DSL

34 | 35 | 36 | # Need you to contribute 37 | I need you to contribute to this project. Simply create PR for any improvements or fixes and we will merge it after the review process. 38 | 39 | # ToDo 40 | - Implement DataStore for sharedpreferences 41 | - Replace KAPT with KSP 42 | - Implement two-way databinding 43 | - Implement dark theme 44 | 45 | # Pre Requisites 46 | - Android Studio version 4.2 or above (I use Bumble Bee preview version) 47 | - compileSdkVersion, targetSdkVersion version 31 48 | - buildToolsVersion 30.0.3 49 | - Java version 11 50 | - Android Gradle version 7.0.2 51 | - Kotlin version 1.5.30 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | id("androidx.navigation.safeargs.kotlin") 5 | id("kotlin-parcelize") 6 | id("kotlin-kapt") 7 | } 8 | 9 | android { 10 | compileSdk = 31 11 | buildToolsVersion = "30.0.3" 12 | 13 | defaultConfig { 14 | applicationId = "com.android.dayplanner.app" 15 | minSdk = 21 16 | targetSdk = 31 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | getByName("release") { 25 | isMinifyEnabled = false 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 28 | ) 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_11 33 | targetCompatibility = JavaVersion.VERSION_11 34 | } 35 | tasks.withType { 36 | kotlinOptions { 37 | jvmTarget = "1.8" 38 | } 39 | } 40 | 41 | buildFeatures { 42 | dataBinding = true 43 | } 44 | } 45 | 46 | dependencies { 47 | val roomVersion = "2.3.0" 48 | val kotlinVersion = "1.5.30" 49 | 50 | implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") 51 | implementation("androidx.core:core-ktx:1.6.0") 52 | implementation("androidx.appcompat:appcompat:1.3.1") 53 | implementation("com.google.android.material:material:1.4.0") 54 | implementation("androidx.constraintlayout:constraintlayout:2.1.0") 55 | implementation("androidx.navigation:navigation-fragment-ktx:2.3.5") 56 | implementation("androidx.navigation:navigation-ui-ktx:2.3.5") 57 | implementation("androidx.legacy:legacy-support-v4:1.0.0") 58 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1") 59 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") 60 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0") 61 | implementation("com.github.pedrovgs:renderers:4.1.0") 62 | 63 | implementation("androidx.room:room-ktx:$roomVersion") 64 | kapt("androidx.room:room-compiler:$roomVersion") 65 | 66 | testImplementation("junit:junit:4.13.2") 67 | testImplementation("org.mockito:mockito-core:3.11.2") 68 | testImplementation("org.mockito.kotlin:mockito-kotlin:3.2.0") 69 | testImplementation("androidx.arch.core:core-testing:2.1.0") 70 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0") 71 | 72 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 73 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 74 | androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0") 75 | implementation("androidx.test.espresso:espresso-idling-resource:3.4.0") 76 | implementation("androidx.core:core-splashscreen:1.0.0-alpha01") 77 | 78 | androidTestImplementation("io.github.kakaocup:kakao:3.0.4") 79 | 80 | } -------------------------------------------------------------------------------- /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/android/dayplanner/app/BaseUIClass.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import androidx.test.espresso.IdlingRegistry 5 | import com.android.dayplanner.app.ui.MainActivity 6 | import com.android.dayplanner.app.ui.resourceidling.SimpleIdlingResource 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.After 10 | import org.junit.Before 11 | 12 | 13 | open class BaseUIClass { 14 | 15 | private var resourceIdling: SimpleIdlingResource? = null 16 | 17 | @Before 18 | open fun setup() { 19 | val activityScenario: ActivityScenario = 20 | ActivityScenario.launch(MainActivity::class.java) 21 | 22 | resourceIdling = SimpleIdlingResource 23 | 24 | activityScenario.onActivity { 25 | IdlingRegistry.getInstance().register(resourceIdling) 26 | } 27 | } 28 | 29 | @After 30 | open fun tearDown() { 31 | resourceIdling?.let { 32 | IdlingRegistry.getInstance().unregister(SimpleIdlingResource) 33 | } 34 | 35 | runBlocking { 36 | delay(3000) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/dayplanner/app/TaskPlannerTests.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app 2 | 3 | import androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu 4 | import com.android.dayplanner.app.screens.HomeScreen 5 | import com.android.dayplanner.app.screens.NewTaskScreen 6 | import io.github.kakaocup.kakao.screen.Screen.Companion.onScreen 7 | import org.junit.Test 8 | 9 | class TaskPlannerTests : BaseUIClass() { 10 | 11 | @Test 12 | fun insertNewTaskHappyPath() { 13 | onScreen { 14 | performClickOnFAButton() 15 | } 16 | onScreen { 17 | saveHappyTask() 18 | } 19 | onScreen { 20 | assertRecyclerView(newTaskTitle) 21 | } 22 | } 23 | 24 | @Test 25 | fun insertNewTaskUnHappyPath() { 26 | onScreen { 27 | performClickOnFAButton() 28 | } 29 | onScreen { 30 | saveUnhappyTask() 31 | } 32 | } 33 | 34 | @Test 35 | fun deleteTask() { 36 | onScreen { 37 | deleteTaskFromList() 38 | } 39 | } 40 | 41 | @Test 42 | fun updateTaskHappyPath() { 43 | onScreen { 44 | if (tasksListIsEmpty()) { 45 | performClickOnFAButton() 46 | createNewTask() 47 | } 48 | clickOnTask() 49 | onScreen { 50 | updateTaskWithValidDetails() 51 | } 52 | onScreen { 53 | assertRecyclerView(updateTaskTitle) 54 | } 55 | } 56 | } 57 | 58 | @Test 59 | fun updateTaskUnHappyPath() { 60 | onScreen { 61 | if (tasksListIsEmpty()) { 62 | performClickOnFAButton() 63 | onScreen { 64 | saveHappyTask() 65 | } 66 | onScreen { 67 | assertRecyclerView(newTaskTitle) 68 | } 69 | } 70 | clickOnTask() 71 | onScreen { 72 | updateTaskWithInvalidDetails() 73 | } 74 | } 75 | } 76 | 77 | @Test 78 | fun testDeleteAllTasks(){ 79 | onScreen { 80 | assertToolbar() 81 | if(tasksListIsEmpty()) { 82 | performClickOnFAButton() 83 | createNewTask() 84 | } 85 | } 86 | 87 | openContextualActionModeOverflowMenu() 88 | 89 | onScreen { 90 | showConfirmationDialog() 91 | confirmDeleteAllTask(HomeScreen.ConfirmOption.YES) 92 | assertTasksListEmptiness() 93 | } 94 | } 95 | 96 | private fun createNewTask() { 97 | onScreen { 98 | saveHappyTask() 99 | } 100 | onScreen { 101 | assertRecyclerView(newTaskTitle) 102 | } 103 | } 104 | 105 | companion object { 106 | const val newTaskTitle = "New Task Title" 107 | const val updateTaskTitle = "Update Task Title" 108 | } 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/dayplanner/app/screens/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.screens 2 | 3 | import android.view.View 4 | import com.android.dayplanner.app.R 5 | import io.github.kakaocup.kakao.check.KCheckBox 6 | import io.github.kakaocup.kakao.common.views.KView 7 | import io.github.kakaocup.kakao.image.KImageView 8 | import io.github.kakaocup.kakao.recycler.KRecyclerItem 9 | import io.github.kakaocup.kakao.recycler.KRecyclerView 10 | import io.github.kakaocup.kakao.screen.Screen 11 | import io.github.kakaocup.kakao.text.KButton 12 | import io.github.kakaocup.kakao.text.KTextView 13 | import io.github.kakaocup.kakao.toolbar.KToolbar 14 | import org.hamcrest.Matcher 15 | 16 | 17 | class HomeScreen : Screen() { 18 | 19 | private val floatingActionButton = KButton { withId(R.id.floating_action_button) } 20 | private val toolbar = KToolbar { withId(R.id.toolbar) } 21 | private val deleteAllView = KView { withText(R.string.label_delete_all) } 22 | 23 | private val yesView = KView { withText(R.string.yes) } 24 | private val noView = KView { withText(R.string.no) } 25 | private val deleteAllTitle = KView { withText(R.string.delete_all_title) } 26 | private val deleteAllDescription = KView { withText(R.string.delete_all_description) } 27 | 28 | private val recyclerView = KRecyclerView({ 29 | withId(R.id.recyclerView) 30 | }, itemTypeBuilder = { 31 | itemType(::TaskItem) 32 | }) 33 | 34 | class TaskItem(parent: Matcher) : KRecyclerItem(parent) { 35 | val title = KTextView(parent) { withId(R.id.textView_title) } 36 | val description = KTextView(parent) { withId(R.id.textView_description) } 37 | val date = KTextView(parent) { withId(R.id.textView_date) } 38 | val deleteTaskButton = KImageView(parent) { withId(R.id.imageView_delete) } 39 | val completeTaskButton = KCheckBox(parent) { withId(R.id.checkBox) } 40 | } 41 | 42 | fun assertRecyclerView(text: String) { 43 | recyclerView { 44 | firstChild { 45 | title.hasText(text) 46 | } 47 | } 48 | } 49 | 50 | fun performClickOnFAButton() { 51 | recyclerView.isVisible() 52 | floatingActionButton.isVisible() 53 | 54 | floatingActionButton.click() 55 | } 56 | 57 | fun deleteTaskFromList() { 58 | recyclerView { 59 | firstChild { 60 | title.isVisible() 61 | description.isVisible() 62 | date.isVisible() 63 | completeTaskButton.isVisible() 64 | 65 | deleteTaskButton.click() 66 | } 67 | } 68 | } 69 | 70 | fun clickOnTask() { 71 | recyclerView { 72 | firstChild { 73 | title.isVisible() 74 | description.isVisible() 75 | date.isVisible() 76 | completeTaskButton.isVisible() 77 | deleteTaskButton.isVisible() 78 | 79 | click() 80 | } 81 | } 82 | } 83 | 84 | fun tasksListIsEmpty(): Boolean { 85 | return recyclerView.getSize() == 0 86 | } 87 | 88 | fun assertToolbar() { 89 | toolbar.isDisplayed() 90 | } 91 | 92 | fun showConfirmationDialog() { 93 | deleteAllView.click() 94 | assertConfirmationDialog() 95 | } 96 | 97 | fun confirmDeleteAllTask(confirmOption: ConfirmOption){ 98 | if (confirmOption == ConfirmOption.YES) { 99 | yesView.perform { 100 | click() 101 | } 102 | } else { 103 | noView.perform { 104 | click() 105 | } 106 | } 107 | } 108 | 109 | fun assertTasksListEmptiness(){ 110 | recyclerView { 111 | hasSize(0) 112 | } 113 | } 114 | 115 | private fun assertConfirmationDialog() { 116 | deleteAllTitle.isDisplayed() 117 | deleteAllDescription.isDisplayed() 118 | yesView.isDisplayed() 119 | noView.isDisplayed() 120 | } 121 | 122 | enum class ConfirmOption { 123 | YES, 124 | NO 125 | } 126 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/dayplanner/app/screens/NewTaskScreen.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.screens 2 | 3 | import androidx.test.espresso.Espresso 4 | import com.android.dayplanner.app.R 5 | import com.android.dayplanner.app.TaskPlannerTests 6 | import io.github.kakaocup.kakao.edit.KEditText 7 | import io.github.kakaocup.kakao.screen.Screen 8 | import io.github.kakaocup.kakao.text.KButton 9 | 10 | class NewTaskScreen: Screen() { 11 | 12 | private val buttonCreateTask = KButton{ withId(R.id.button_createLongTempTask) } 13 | private val buttonSaveTask = KButton{ withId(R.id.button_saveTask) } 14 | private val editTextTitle = KEditText{ withId(R.id.editText_title) } 15 | private val editTextDescription = KEditText{ withId(R.id.editText_description) } 16 | private val editTextDate = KEditText{ withId(R.id.editText_taskDate) } 17 | 18 | fun saveHappyTask(){ 19 | buttonCreateTask.click() 20 | 21 | editTextTitle.replaceText(TaskPlannerTests.newTaskTitle) 22 | editTextTitle.hasAnyText() 23 | editTextDescription.hasAnyText() 24 | editTextDate.hasAnyText() 25 | 26 | buttonSaveTask.click() 27 | } 28 | 29 | fun saveUnhappyTask(){ 30 | buttonCreateTask.click() 31 | 32 | editTextTitle.replaceText("") 33 | editTextDescription.hasAnyText() 34 | editTextDate.hasAnyText() 35 | 36 | Espresso.closeSoftKeyboard() 37 | 38 | buttonSaveTask.click() 39 | } 40 | 41 | fun updateTaskWithValidDetails() { 42 | editTextTitle.replaceText(TaskPlannerTests.updateTaskTitle) 43 | 44 | editTextTitle.hasAnyText() 45 | editTextDescription.hasAnyText() 46 | editTextDate.hasAnyText() 47 | 48 | Espresso.closeSoftKeyboard() 49 | 50 | buttonSaveTask.click() 51 | } 52 | 53 | 54 | fun updateTaskWithInvalidDetails() { 55 | editTextTitle.replaceText("") 56 | 57 | editTextDescription.hasAnyText() 58 | editTextDate.hasAnyText() 59 | 60 | Espresso.closeSoftKeyboard() 61 | 62 | buttonSaveTask.click() 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app 2 | 3 | import com.android.dayplanner.app.data.Task 4 | 5 | fun MutableList.addAll(trashExistingItems: Boolean, items: List) { 6 | clear() 7 | addAll(items) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/data/Task.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.data 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import kotlinx.parcelize.Parcelize 8 | import java.util.* 9 | 10 | @Entity 11 | @Parcelize 12 | data class Task( 13 | @PrimaryKey val id: String = UUID.randomUUID().toString(), 14 | @ColumnInfo(name = "title") var title: String, 15 | @ColumnInfo(name = "description") var description: String, 16 | @ColumnInfo(name = "createDate") var createDate: String = Calendar.getInstance().time.toString(), 17 | @ColumnInfo(name = "status")var status: Status = Status.PENDING 18 | ) : Parcelable { 19 | fun performValidation(block: (Boolean, String) -> Unit) { 20 | when { 21 | title.isBlank() -> return block.invoke(false, "Title is empty") 22 | description.isBlank() -> return block.invoke(false, "Description is empty") 23 | else -> block.invoke(true, "Task is valid") 24 | } 25 | } 26 | } 27 | 28 | enum class Status { 29 | COMPLETED, 30 | PENDING, 31 | CANCELLED 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/data/TaskDao.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.data 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.* 5 | 6 | @Dao 7 | interface TaskDao { 8 | @Insert 9 | fun insertTask(task: Task) 10 | 11 | @Delete 12 | fun deleteTask(task: Task) 13 | 14 | @Query("SELECT * FROM Task") 15 | fun getTasks(): List 16 | 17 | @Update 18 | fun updateTask(task: Task) 19 | 20 | @Query("DELETE FROM Task WHERE status = :status") 21 | fun deleteTaskByStatus(status: Status): Int 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/data/TaskDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.data 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = [Task::class], version = 1) 7 | abstract class TaskDatabase: RoomDatabase(){ 8 | abstract fun taskDao(): TaskDao 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/data/TasksRepository.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.data 2 | 3 | class TasksRepository(var db: TaskDatabase) { 4 | 5 | fun addTask(task: Task, onComplete: (String) -> Unit) { 6 | task.performValidation { isValid, message -> 7 | if (isValid) { 8 | db.taskDao().insertTask(task) 9 | onComplete.invoke("Task inserted successfully") 10 | } else { 11 | onComplete.invoke(message) 12 | } 13 | } 14 | } 15 | 16 | fun getAllTasks(): List { 17 | return db.taskDao().getTasks().reversed() 18 | } 19 | 20 | fun getPendingTasks(): List { 21 | return db.taskDao().getTasks().filter { it.status == Status.PENDING }.reversed() 22 | } 23 | 24 | fun removeTask(task: Task, onComplete: (String) -> Unit) { 25 | db.taskDao().deleteTask(task) 26 | onComplete.invoke("Task removed") 27 | } 28 | 29 | fun updateTask(task: Task, onComplete: (String) -> Unit) { 30 | task.performValidation { isValid, message -> 31 | if (isValid) { 32 | db.taskDao().updateTask(task) 33 | onComplete.invoke("Task updated successfully") 34 | } else { 35 | onComplete.invoke(message) 36 | } 37 | } 38 | } 39 | 40 | fun changeTaskStatus(task: Task, onComplete: (String) -> Unit) { 41 | db.taskDao().updateTask(task) 42 | val message = if (task.status == Status.PENDING) { 43 | "Task is pending" 44 | } else { 45 | "Task completed" 46 | } 47 | onComplete.invoke(message) 48 | } 49 | 50 | fun deletePendingTasks(onDelete: (String) -> Unit) { 51 | val deletedTasksCount = db.taskDao().deleteTaskByStatus(Status.PENDING) 52 | if ( deletedTasksCount > 0){ 53 | onDelete.invoke("$deletedTasksCount task(s) deleted.") 54 | } else { 55 | onDelete.invoke("there are no tasks to be deleted.") 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui 2 | 3 | import android.animation.ObjectAnimator 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.view.animation.AnticipateInterpolator 7 | import androidx.annotation.VisibleForTesting 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.animation.doOnEnd 10 | import androidx.core.splashscreen.SplashScreen 11 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 12 | import androidx.core.splashscreen.SplashScreenViewProvider 13 | import androidx.room.Room 14 | import androidx.test.espresso.IdlingResource 15 | import com.android.dayplanner.app.R 16 | import com.android.dayplanner.app.data.TaskDatabase 17 | import com.android.dayplanner.app.ui.resourceidling.SimpleIdlingResource 18 | 19 | 20 | class MainActivity : AppCompatActivity() { 21 | 22 | private lateinit var db: TaskDatabase 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | val splashScreen = installSplashScreen() 27 | setContentView(R.layout.activity_main) 28 | 29 | setSplashExitAnimation(splashScreen) 30 | } 31 | 32 | private fun setSplashExitAnimation(splashScreen: SplashScreen) { 33 | splashScreen.setOnExitAnimationListener { splashScreenView -> 34 | configureObjectAnimator(splashScreenView) { slideUpAnimation -> 35 | with(slideUpAnimation) { 36 | interpolator = AnticipateInterpolator() 37 | duration = 500L 38 | doOnEnd { 39 | splashScreenView.remove() 40 | } 41 | start() 42 | } 43 | } 44 | } 45 | } 46 | 47 | private fun configureObjectAnimator( 48 | splashScreenView: SplashScreenViewProvider, 49 | onComplete: (ObjectAnimator) -> Unit 50 | ) { 51 | val objectAnimator = ObjectAnimator.ofFloat( 52 | splashScreenView.view, 53 | View.TRANSLATION_Y, 54 | 0f, 55 | -splashScreenView.view.height.toFloat() 56 | ) 57 | onComplete.invoke(objectAnimator) 58 | } 59 | 60 | fun getDatabaseInstance(): TaskDatabase { 61 | if(!this::db.isInitialized) { 62 | db = Room.databaseBuilder( 63 | applicationContext, 64 | TaskDatabase::class.java, "tasks-database" 65 | ).allowMainThreadQueries().build() 66 | } 67 | return db 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/detail/TaskDetailsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.detail 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.Button 9 | import android.widget.EditText 10 | import android.widget.TextView 11 | import android.widget.Toast 12 | import androidx.appcompat.widget.Toolbar 13 | import androidx.databinding.DataBindingUtil 14 | import androidx.fragment.app.Fragment 15 | import androidx.fragment.app.viewModels 16 | import androidx.navigation.findNavController 17 | import com.android.dayplanner.app.R 18 | import com.android.dayplanner.app.data.Task 19 | import com.android.dayplanner.app.databinding.TaskDetailsFragmentBinding 20 | import com.android.dayplanner.app.ui.MainActivity 21 | import java.util.* 22 | 23 | class TaskDetailsFragment : Fragment() { 24 | 25 | private lateinit var mainActivity: MainActivity 26 | private lateinit var binding: TaskDetailsFragmentBinding 27 | 28 | private val viewModel by viewModels { 29 | TaskViewModelFactory(mainActivity.getDatabaseInstance()) 30 | } 31 | 32 | override fun onAttach(context: Context) { 33 | super.onAttach(context) 34 | mainActivity = context as MainActivity 35 | } 36 | 37 | override fun onCreateView( 38 | inflater: LayoutInflater, 39 | container: ViewGroup?, 40 | savedInstanceState: Bundle? 41 | ): View { 42 | binding = 43 | DataBindingUtil.inflate(inflater, R.layout.task_details_fragment, container, false) 44 | 45 | arguments?.getParcelable("keyTask").apply { 46 | this?.let { 47 | binding.editTextTitle.apply { 48 | setText(title) 49 | } 50 | binding.editTextDescription.apply { 51 | setText(description) 52 | } 53 | binding.editTextTaskDate.apply { 54 | setText(createDate) 55 | } 56 | binding.buttonSaveTask.apply { 57 | text = context.getString(R.string.edit) 58 | } 59 | 60 | binding.toolbar.apply { 61 | title = context.getString(R.string.button_edit_task) 62 | } 63 | } 64 | } 65 | 66 | //binding invoking 67 | binding.buttonSaveTask.apply { 68 | setOnClickListener { 69 | if (arguments != null) { 70 | viewModel.updateTask(getUpdateTask()!!) { 71 | Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() 72 | findNavController().popBackStack() 73 | } 74 | } else { 75 | viewModel.saveTask(createTask()) { 76 | Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() 77 | findNavController().popBackStack() 78 | } 79 | } 80 | } 81 | } 82 | binding.buttonCreateLongTempTask.apply { 83 | setOnClickListener { 84 | binding.editTextTitle.apply { 85 | setText(context.getString(R.string.dummy_brief_task_title)) 86 | } 87 | binding.editTextDescription.apply { 88 | setText(context.getString(R.string.dummy_brief_task_description)) 89 | } 90 | binding.editTextTaskDate.apply { 91 | setText(Calendar.getInstance().time.toString()) 92 | } 93 | } 94 | } 95 | binding.buttonCreateShortTempTask.apply { 96 | setOnClickListener { 97 | binding.editTextTitle.apply { 98 | setText(context.getString(R.string.dummy_short_task_title)) 99 | } 100 | binding.editTextDescription.apply { 101 | setText(context.getString(R.string.dummy_short_task_description)) 102 | } 103 | binding.editTextTaskDate.apply { 104 | setText(Calendar.getInstance().time.toString()) 105 | } 106 | } 107 | } 108 | 109 | return binding.root 110 | } 111 | 112 | private fun createTask(): Task { 113 | return Task( 114 | title = getValueFromField(binding.editTextTitle), 115 | description = getValueFromField(binding.editTextDescription), 116 | createDate = getValueFromField(binding.editTextTaskDate) 117 | ) 118 | } 119 | 120 | private fun getUpdateTask(): Task? { 121 | arguments?.getParcelable("keyTask")?.run { 122 | title = getValueFromField(binding.editTextTitle) 123 | description = getValueFromField(binding.editTextDescription) 124 | createDate = getValueFromField(binding.editTextTaskDate) 125 | return this 126 | } 127 | return null 128 | } 129 | 130 | private fun getValueFromField(view: TextView): String { 131 | view.apply { 132 | return text.toString() 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/detail/TaskDetailsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.detail 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.android.dayplanner.app.data.Task 6 | import com.android.dayplanner.app.data.TaskDatabase 7 | import com.android.dayplanner.app.data.TasksRepository 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.GlobalScope 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | 13 | class TaskDetailsViewModel(private val taskDatabase: TaskDatabase) : ViewModel() { 14 | 15 | private val tasksRepository by lazy { TasksRepository(taskDatabase) } 16 | 17 | fun saveTask(task: Task, onComplete: (String) -> Unit) { 18 | viewModelScope.launch { 19 | tasksRepository.addTask(task){ 20 | onComplete.invoke(it) 21 | } 22 | } 23 | } 24 | 25 | fun updateTask(task: Task, onComplete: (String) -> Unit) { 26 | viewModelScope.launch { 27 | tasksRepository.updateTask(task) { 28 | onComplete.invoke(it) 29 | } 30 | } 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/detail/TaskViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.detail 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.android.dayplanner.app.data.TaskDatabase 6 | 7 | class TaskViewModelFactory(private val taskDatabase: TaskDatabase) : ViewModelProvider.Factory { 8 | override fun create(modelClass: Class): T { 9 | return TaskDetailsViewModel(taskDatabase) as T 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.home 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.AlertDialog 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.view.* 8 | import android.widget.Toast 9 | import androidx.databinding.DataBindingUtil 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.viewModels 12 | import androidx.navigation.fragment.findNavController 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import com.android.dayplanner.app.R 15 | import com.android.dayplanner.app.addAll 16 | import com.android.dayplanner.app.data.Task 17 | import com.android.dayplanner.app.databinding.HomeFragmentBinding 18 | import com.android.dayplanner.app.ui.MainActivity 19 | import com.pedrogomez.renderers.RVRendererAdapter 20 | import com.pedrogomez.renderers.RendererBuilder 21 | 22 | 23 | class HomeFragment : Fragment() { 24 | 25 | private lateinit var binding: HomeFragmentBinding 26 | private lateinit var mainActivity: MainActivity 27 | 28 | private val viewModel by viewModels { 29 | HomeViewModelFactory(mainActivity.getDatabaseInstance()) 30 | } 31 | 32 | private val tasksList by lazy { mutableListOf() } 33 | 34 | private val rendererBuilder by lazy { 35 | RendererBuilder(TasksRenderer { view, task -> 36 | registerTaskViewsClickListeners(view, task) 37 | }) 38 | } 39 | 40 | private val tasksAdapter by lazy { RVRendererAdapter(rendererBuilder, tasksList) } 41 | 42 | override fun onAttach(context: Context) { 43 | super.onAttach(context) 44 | mainActivity = context as MainActivity 45 | } 46 | 47 | override fun onCreate(savedInstanceState: Bundle?) { 48 | super.onCreate(savedInstanceState) 49 | setHasOptionsMenu(true) 50 | } 51 | 52 | @SuppressLint("NotifyDataSetChanged") 53 | override fun onCreateView( 54 | inflater: LayoutInflater, 55 | container: ViewGroup?, 56 | savedInstanceState: Bundle? 57 | ): View { 58 | 59 | binding = DataBindingUtil.inflate(inflater, R.layout.home_fragment, container, false) 60 | 61 | //viewModel invoking 62 | viewModel.loadPendingTasks() 63 | viewModel.tasksListLiveData.observe(viewLifecycleOwner) { list -> 64 | tasksList.addAll(trashExistingItems = true, list) 65 | tasksAdapter.notifyDataSetChanged() 66 | binding.placeholder.visibility = if (list.isNotEmpty()) { 67 | View.GONE 68 | } else { 69 | View.VISIBLE 70 | } 71 | } 72 | 73 | //binding invoking 74 | binding.recyclerView.apply { 75 | layoutManager = LinearLayoutManager(requireContext()) 76 | adapter = tasksAdapter 77 | } 78 | binding.toolbar.setOnMenuItemClickListener { 79 | onOptionsItemSelected(it) 80 | } 81 | binding.floatingActionButton.setOnClickListener { 82 | findNavController().navigate(R.id.action_homeFragment_to_taskDetailsFragment) 83 | } 84 | 85 | return binding.root 86 | } 87 | 88 | 89 | private fun gotoTaskDetails(task: Task) { 90 | findNavController().navigate( 91 | HomeFragmentDirections.actionHomeFragmentToTaskDetailsFragment( 92 | task 93 | ) 94 | ) 95 | } 96 | 97 | private fun registerTaskViewsClickListeners(view: View, task: Task) { 98 | when (view.id) { 99 | R.id.imageView_delete -> { 100 | viewModel.deleteTask(task) { 101 | Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() 102 | } 103 | } 104 | R.id.checkBox -> { 105 | viewModel.completeTask(task) { 106 | Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() 107 | } 108 | } 109 | else -> { 110 | gotoTaskDetails(task) 111 | } 112 | } 113 | } 114 | 115 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 116 | inflater.inflate(R.menu.home_menu, menu) 117 | super.onCreateOptionsMenu(menu, inflater) 118 | } 119 | 120 | 121 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 122 | when (item.itemId) { 123 | R.id.showHistory -> { 124 | findNavController().navigate(R.id.action_homeFragment_to_tasksFragment) 125 | } 126 | else -> { 127 | if (tasksList.isNotEmpty()) { 128 | askForConfirmation { 129 | viewModel.deleteAllTasks { message -> 130 | Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() 131 | } 132 | } 133 | } else { 134 | Toast.makeText(requireContext(), "No tasks available", Toast.LENGTH_SHORT) 135 | .show() 136 | } 137 | } 138 | } 139 | return true 140 | } 141 | 142 | private fun askForConfirmation(onConfirm: () -> Unit) { 143 | AlertDialog.Builder(requireActivity()) 144 | .setTitle(getString(R.string.delete_all_title)) 145 | .setMessage(getString(R.string.delete_all_description)) 146 | .setPositiveButton(getString(R.string.yes)) { _, _ -> onConfirm.invoke() } 147 | .setNegativeButton(getString(R.string.no)) { dialog, _ -> dialog.dismiss() } 148 | .create() 149 | .show() 150 | } 151 | 152 | } 153 | 154 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.home 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.android.dayplanner.app.addAll 7 | import com.android.dayplanner.app.data.Task 8 | import com.android.dayplanner.app.data.TaskDatabase 9 | import com.android.dayplanner.app.data.TasksRepository 10 | import kotlinx.coroutines.launch 11 | 12 | class HomeViewModel(private val taskDatabase: TaskDatabase) : ViewModel() { 13 | 14 | private val tasksList = mutableListOf() 15 | val tasksListLiveData = MutableLiveData>().apply { 16 | value = tasksList 17 | } 18 | 19 | private val tasksRepository by lazy { TasksRepository(taskDatabase) } 20 | 21 | fun loadPendingTasks() { 22 | viewModelScope.launch { 23 | tasksList.addAll(true, tasksRepository.getPendingTasks()).also { 24 | tasksListLiveData.value = tasksList 25 | } 26 | } 27 | } 28 | 29 | fun deleteTask(task: Task, onComplete: (String) -> Unit) { 30 | viewModelScope.launch { 31 | tasksRepository.removeTask(task){ 32 | tasksList.remove(task).also { 33 | tasksListLiveData.value = tasksList 34 | } 35 | onComplete.invoke(it) 36 | } 37 | } 38 | } 39 | 40 | fun completeTask(task: Task, onComplete: (String) -> Unit) { 41 | viewModelScope.launch { 42 | tasksRepository.changeTaskStatus(task) { message -> 43 | tasksList.removeAll { it.id == task.id }.also { 44 | tasksListLiveData.value =tasksList 45 | } 46 | onComplete.invoke(message) 47 | } 48 | } 49 | } 50 | 51 | fun deleteAllTasks(onDelete: (String) -> Unit) { 52 | viewModelScope.launch { 53 | tasksRepository.deletePendingTasks{ message -> 54 | tasksList.clear() 55 | tasksListLiveData.value = tasksList 56 | onDelete.invoke(message) 57 | } 58 | } 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/home/HomeViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.home 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.android.dayplanner.app.data.TaskDatabase 7 | 8 | class HomeViewModelFactory(private val taskDatabase: TaskDatabase) : ViewModelProvider.Factory { 9 | override fun create(modelClass: Class): T { 10 | return HomeViewModel(taskDatabase) as T 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/home/TasksRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.home 2 | 3 | import android.graphics.Paint 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.CheckBox 8 | import android.widget.ImageView 9 | import android.widget.TextView 10 | import com.android.dayplanner.app.R 11 | import com.android.dayplanner.app.data.Status 12 | import com.android.dayplanner.app.data.Task 13 | import com.pedrogomez.renderers.Renderer 14 | import kotlinx.coroutines.DelicateCoroutinesApi 15 | import kotlinx.coroutines.GlobalScope 16 | import kotlinx.coroutines.launch 17 | 18 | class TasksRenderer(private val onTaskClick: (View, Task) -> Unit) : Renderer() { 19 | 20 | lateinit var textViewTitle: TextView 21 | lateinit var textViewDescription: TextView 22 | lateinit var textViewDate: TextView 23 | lateinit var statusView: View 24 | lateinit var imageViewDelete: ImageView 25 | lateinit var checkBox: CheckBox 26 | 27 | override fun inflate(inflater: LayoutInflater?, parent: ViewGroup?): View { 28 | val view = inflater?.inflate(R.layout.item_task, parent, false) 29 | 30 | view?.let { 31 | textViewTitle = view.findViewById(R.id.textView_title) 32 | textViewDescription = view.findViewById(R.id.textView_description) 33 | textViewDate = view.findViewById(R.id.textView_date) 34 | statusView = view.findViewById(R.id.view_status) 35 | imageViewDelete = view.findViewById(R.id.imageView_delete) 36 | checkBox = view.findViewById(R.id.checkBox) 37 | } 38 | 39 | return view!! 40 | } 41 | 42 | @DelicateCoroutinesApi 43 | override fun render() { 44 | textViewTitle.text = content.title 45 | textViewDescription.text = content.description 46 | textViewDate.text = content.createDate 47 | checkBox.isChecked = content.status == Status.COMPLETED 48 | } 49 | 50 | override fun hookListeners(rootView: View?) { 51 | super.hookListeners(rootView) 52 | 53 | rootView?.setOnClickListener { 54 | onTaskClick.invoke(rootView, content) 55 | } 56 | 57 | imageViewDelete.setOnClickListener { 58 | onTaskClick.invoke(it, content) 59 | } 60 | 61 | checkBox.setOnCheckedChangeListener { button, flag -> 62 | if(button.isPressed) { 63 | content.status = if (flag) Status.COMPLETED else Status.PENDING 64 | onTaskClick.invoke(checkBox, content) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/resourceidling/SimpleIdlingResource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.dayplanner.app.ui.resourceidling 17 | 18 | import androidx.test.espresso.IdlingResource 19 | import androidx.test.espresso.IdlingResource.ResourceCallback 20 | import java.util.concurrent.atomic.AtomicBoolean 21 | 22 | /** 23 | * A very simple implementation of [IdlingResource]. 24 | * 25 | * 26 | * Consider using CountingIdlingResource from espresso-contrib package if you use this class from 27 | * multiple threads or need to keep a count of pending operations. 28 | */ 29 | object SimpleIdlingResource : IdlingResource { 30 | @Volatile 31 | private var mCallback: ResourceCallback? = null 32 | 33 | // Idleness is controlled with this boolean. 34 | private val mIsIdleNow = AtomicBoolean(true) 35 | 36 | override fun getName(): String { 37 | return this.javaClass.name 38 | } 39 | 40 | override fun isIdleNow(): Boolean { 41 | return mIsIdleNow.get() 42 | } 43 | 44 | override fun registerIdleTransitionCallback(callback: ResourceCallback) { 45 | mCallback = callback 46 | } 47 | 48 | /** 49 | * Sets the new idle state, if isIdleNow is true, it pings the [ResourceCallback]. 50 | * @param isIdleNow false if there are pending operations, true if idle. 51 | */ 52 | fun setIdleState(isIdleNow: Boolean) { 53 | mIsIdleNow.set(isIdleNow) 54 | if (isIdleNow && mCallback != null) { 55 | mCallback!!.onTransitionToIdle() 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/tasks/TasksFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.tasks 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModelProvider 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Button 11 | import android.widget.EditText 12 | import android.widget.Toast 13 | import androidx.core.widget.doAfterTextChanged 14 | import androidx.databinding.DataBindingUtil 15 | import androidx.fragment.app.viewModels 16 | import androidx.navigation.fragment.findNavController 17 | import androidx.recyclerview.widget.LinearLayoutManager 18 | import androidx.recyclerview.widget.RecyclerView 19 | import com.android.dayplanner.app.R 20 | import com.android.dayplanner.app.addAll 21 | import com.android.dayplanner.app.data.Task 22 | import com.android.dayplanner.app.databinding.TasksFragmentBinding 23 | import com.android.dayplanner.app.ui.MainActivity 24 | import com.android.dayplanner.app.ui.detail.TaskDetailsViewModel 25 | import com.android.dayplanner.app.ui.detail.TaskViewModelFactory 26 | import com.android.dayplanner.app.ui.home.HomeFragmentDirections 27 | import com.android.dayplanner.app.ui.home.TasksRenderer 28 | import com.pedrogomez.renderers.RVRendererAdapter 29 | import com.pedrogomez.renderers.RendererBuilder 30 | 31 | class TasksFragment : Fragment() { 32 | 33 | private lateinit var mainActivity: MainActivity 34 | private lateinit var binding: TasksFragmentBinding 35 | 36 | private val viewModel by viewModels { 37 | TasksViewModelFactory(mainActivity.getDatabaseInstance()) 38 | } 39 | 40 | private val tasksList by lazy { mutableListOf() } 41 | private val rendererBuilder by lazy { 42 | RendererBuilder(TasksRenderer { view, task -> 43 | registerTaskViewsClickListeners(view, task) 44 | }) 45 | } 46 | private val tasksAdapter by lazy { RVRendererAdapter(rendererBuilder, tasksList) } 47 | 48 | override fun onAttach(context: Context) { 49 | super.onAttach(context) 50 | mainActivity = context as MainActivity 51 | } 52 | 53 | override fun onCreateView( 54 | inflater: LayoutInflater, 55 | container: ViewGroup?, 56 | savedInstanceState: Bundle? 57 | ): View? { 58 | binding = DataBindingUtil.inflate(inflater, R.layout.tasks_fragment,container,false) 59 | 60 | //binding invoking 61 | binding.recyclerView.apply { 62 | layoutManager = LinearLayoutManager(requireContext()) 63 | adapter = tasksAdapter 64 | } 65 | 66 | //viewmodel invoking 67 | viewModel.loadTasks() 68 | viewModel.tasksListLiveData.observe(viewLifecycleOwner) { list -> 69 | tasksList.addAll(trashExistingItems = true, list) 70 | tasksAdapter.notifyDataSetChanged() 71 | } 72 | 73 | return binding.root 74 | } 75 | 76 | private fun registerTaskViewsClickListeners(view: View, task: Task) { 77 | when (view.id) { 78 | R.id.imageView_delete -> { 79 | viewModel.deleteTask(task) { 80 | Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() 81 | } 82 | } 83 | R.id.checkBox -> { 84 | viewModel.completeTask(task) { 85 | Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() 86 | } 87 | } 88 | else -> { 89 | gotoTaskDetails(task) 90 | } 91 | } 92 | } 93 | 94 | private fun gotoTaskDetails(task: Task) { 95 | findNavController().navigate(TasksFragmentDirections.actionTasksFragmentToTaskDetailsFragment(task)) 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/tasks/TasksViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.tasks 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.android.dayplanner.app.addAll 7 | import com.android.dayplanner.app.data.Task 8 | import com.android.dayplanner.app.data.TaskDatabase 9 | import com.android.dayplanner.app.data.TasksRepository 10 | import kotlinx.coroutines.launch 11 | 12 | class TasksViewModel(private var taskDatabase: TaskDatabase) : ViewModel() { 13 | 14 | private val tasksList = mutableListOf() 15 | val tasksListLiveData = MutableLiveData>().apply { 16 | value = tasksList 17 | } 18 | private val tasksRepository by lazy { TasksRepository(taskDatabase) } 19 | 20 | fun loadTasks(){ 21 | viewModelScope.launch { 22 | tasksList.addAll(true, tasksRepository.getAllTasks()) 23 | tasksListLiveData.value = tasksList 24 | } 25 | } 26 | 27 | fun deleteTask(task: Task, onComplete: (String) -> Unit) { 28 | viewModelScope.launch { 29 | tasksRepository.removeTask(task){ 30 | tasksList.remove(task).also { 31 | tasksListLiveData.value = tasksList 32 | } 33 | onComplete.invoke(it) 34 | } 35 | } 36 | } 37 | 38 | fun completeTask(task: Task, onComplete: (String) -> Unit) { 39 | viewModelScope.launch { 40 | tasksRepository.changeTaskStatus(task) { message -> 41 | tasksList.removeAll { it.id == task.id }.also { 42 | tasksListLiveData.value =tasksList 43 | } 44 | onComplete.invoke(message) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/dayplanner/app/ui/tasks/TasksViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.android.dayplanner.app.ui.tasks 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.android.dayplanner.app.data.TaskDatabase 6 | 7 | class TasksViewModelFactory(private val taskDatabase: TaskDatabase) : ViewModelProvider.Factory { 8 | override fun create(modelClass: Class): T { 9 | return TasksViewModel(taskDatabase) as T 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunydDEV/android-dayplanner-app/388a71c4983206af8d54a140f1d136c7089a4ecc/app/src/main/res/drawable-hdpi/splash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunydDEV/android-dayplanner-app/388a71c4983206af8d54a140f1d136c7089a4ecc/app/src/main/res/drawable-ldpi/splash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunydDEV/android-dayplanner-app/388a71c4983206af8d54a140f1d136c7089a4ecc/app/src/main/res/drawable-mdpi/splash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunydDEV/android-dayplanner-app/388a71c4983206af8d54a140f1d136c7089a4ecc/app/src/main/res/drawable-xhdpi/splash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunydDEV/android-dayplanner-app/388a71c4983206af8d54a140f1d136c7089a4ecc/app/src/main/res/drawable-xxhdpi/splash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunydDEV/android-dayplanner-app/388a71c4983206af8d54a140f1d136c7089a4ecc/app/src/main/res/drawable-xxxhdpi/splash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_logo_background_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 42 | 43 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_task.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 25 | 26 | 34 | 35 | 42 | 43 | 50 | 51 | 61 | 62 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/res/layout/task_details_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 41 | 42 | 48 | 49 | 50 | 51 | 61 | 62 | 67 | 68 | 69 | 70 | 80 | 81 | 87 | 88 | 89 | 90 |