├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── dn │ │ └── marjan │ │ └── githubapp │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── dn │ │ │ └── marjan │ │ │ └── githubapp │ │ │ ├── Application.kt │ │ │ ├── MainActivity.kt │ │ │ ├── base │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseViewModel.kt │ │ │ ├── CoroutineDispatcherProvider.kt │ │ │ └── ViewModelFactory.kt │ │ │ ├── data │ │ │ ├── Error.kt │ │ │ ├── Resource.kt │ │ │ ├── Status.kt │ │ │ ├── local │ │ │ │ ├── LocalDataService.kt │ │ │ │ └── LocalDataServiceImp.kt │ │ │ └── remote │ │ │ │ ├── RemoteDataService.kt │ │ │ │ └── RemoteDataServiceImp.kt │ │ │ ├── di │ │ │ ├── AppComponent.kt │ │ │ ├── keys │ │ │ │ └── ViewModelKey.kt │ │ │ └── modules │ │ │ │ ├── ActivityModule.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── DataModule.kt │ │ │ │ ├── NetworkModule.kt │ │ │ │ ├── RepositoryModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── entity │ │ │ ├── ReceivedEvents.kt │ │ │ ├── Repository.kt │ │ │ └── UserInfo.kt │ │ │ ├── server │ │ │ └── TaskService.kt │ │ │ ├── ui │ │ │ ├── dashboard │ │ │ │ ├── BottomBarItems.kt │ │ │ │ ├── DashboardActivity.kt │ │ │ │ └── DashboardViewModel.kt │ │ │ ├── home │ │ │ │ ├── repo │ │ │ │ │ ├── HomeRepository.kt │ │ │ │ │ └── HomeRepositoryImp.kt │ │ │ │ └── ui │ │ │ │ │ ├── HomePage.kt │ │ │ │ │ └── HomeViewModel.kt │ │ │ ├── login │ │ │ │ ├── repo │ │ │ │ │ ├── LoginRepository.kt │ │ │ │ │ └── LoginRepositoryImp.kt │ │ │ │ └── ui │ │ │ │ │ ├── LoginActivity.kt │ │ │ │ │ └── LoginViewModel.kt │ │ │ ├── profile │ │ │ │ ├── repo │ │ │ │ │ ├── ProfileRepository.kt │ │ │ │ │ └── ProfileRepositoryImp.kt │ │ │ │ └── ui │ │ │ │ │ ├── ProfilePage.kt │ │ │ │ │ └── ProfileViewModel.kt │ │ │ ├── repository │ │ │ │ ├── repo │ │ │ │ │ ├── RepoRepository.kt │ │ │ │ │ └── RepoRepositoryImp.kt │ │ │ │ └── ui │ │ │ │ │ ├── RepositoryPage.kt │ │ │ │ │ └── RepositoryViewModel.kt │ │ │ ├── splash │ │ │ │ ├── repo │ │ │ │ │ ├── SplashRepository.kt │ │ │ │ │ └── SplashRepositoryImp.kt │ │ │ │ └── ui │ │ │ │ │ ├── SplashActivity.kt │ │ │ │ │ └── SplashViewModel.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── utils │ │ │ ├── Config.kt │ │ │ └── Utility.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── circle_shape.xml │ │ ├── fork.png │ │ ├── ic_account.xml │ │ ├── ic_eye.xml │ │ ├── ic_folder.xml │ │ ├── ic_github.png │ │ ├── ic_home.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_link.xml │ │ ├── ic_location.xml │ │ ├── ic_people.xml │ │ ├── ic_star.xml │ │ └── ic_time.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-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── sharedTest │ └── java │ │ └── dn.marjan.githubapp │ │ └── ApplicationTest.kt │ └── test │ ├── java │ └── dn │ │ └── marjan │ │ └── githubapp │ │ ├── ExampleUnitTest.kt │ │ ├── di │ │ ├── component │ │ │ └── TestAppComponent.kt │ │ ├── dispatchers │ │ │ └── TestCoroutineDispatcherProvider.kt │ │ └── modules │ │ │ ├── AppModuleTest.kt │ │ │ ├── DataModuleTest.kt │ │ │ └── RepositoryModuleTest.kt │ │ └── ui │ │ ├── home │ │ ├── HomeRepositoryTest.kt │ │ └── HomeViewModelTest.kt │ │ ├── login │ │ ├── LoginRepositoryTest.kt │ │ └── LoginViewModelTest.kt │ │ ├── repository │ │ └── RepositoryViewModelTest.kt │ │ └── splash │ │ ├── SplashActivityTest.kt │ │ ├── SplashRepositoryTest.kt │ │ └── SplashViewModelTest.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── 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/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 116 | 117 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitHub application using 2 | - [GitHub REST API](https://docs.github.com/en/rest) 3 | - [Dagger](https://github.com/google/dagger) 4 | - MVVM architecture 5 | - [Mockk](https://mockk.io) 6 | - [Jetpack Compose](developer.android.com/jetpack/compose) 7 | - [Kotlin Coroutines](developer.android.com/kotlin/coroutines) 8 | 9 | ## Application pages 10 |
11 | 12 | 13 | 14 |
15 | 16 | ## Attention 17 | If you want to use it, put your [GitHub Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) in Config.kt file. 18 | 19 | ## License 20 | 21 | Copyright 2022 marjandn 22 | 23 | Licensed under the Apache License, Version 2.0 (the "License"); 24 | you may not use this file except in compliance with the License. 25 | You may obtain a copy of the License at 26 | 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | Unless required by applicable law or agreed to in writing, software 30 | distributed under the License is distributed on an "AS IS" BASIS, 31 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | See the License for the specific language governing permissions and 33 | limitations under the License. 34 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | android { 8 | compileSdk 31 9 | 10 | defaultConfig { 11 | applicationId "dn.marjan.githubapp" 12 | minSdk 21 13 | targetSdk 31 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_11 30 | targetCompatibility JavaVersion.VERSION_11 31 | } 32 | kotlinOptions { 33 | jvmTarget = '11' 34 | useIR = true 35 | freeCompilerArgs += [ 36 | "-Xjvm-default=all", 37 | ] 38 | } 39 | buildFeatures { 40 | compose true 41 | } 42 | composeOptions { 43 | kotlinCompilerExtensionVersion composeVersion 44 | // kotlinCompilerVersion kotlinVersion 45 | } 46 | packagingOptions { 47 | resources { 48 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 49 | } 50 | } 51 | sourceSets { 52 | String sharedTestDir = 'src/sharedTest/java' 53 | test { 54 | java.srcDir sharedTestDir 55 | } 56 | androidTest { 57 | java.srcDir sharedTestDir 58 | } 59 | } 60 | testOptions{ 61 | unitTests{ 62 | includeAndroidResources true 63 | } 64 | } 65 | } 66 | 67 | 68 | dependencies { 69 | implementation 'androidx.core:core-ktx:1.7.0' 70 | implementation 'androidx.appcompat:appcompat:1.4.1' 71 | implementation 'com.google.android.material:material:1.6.0' 72 | implementation "androidx.compose.ui:ui:$composeVersion" 73 | implementation "androidx.compose.material:material:$composeVersion" 74 | implementation "androidx.compose.ui:ui-tooling-preview:$composeVersion" 75 | implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" 76 | 77 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" 78 | implementation 'androidx.activity:activity-compose:1.4.0' 79 | implementation "androidx.navigation:navigation-compose:2.5.0-rc01" 80 | 81 | // Unit Test 82 | // Required -- JUnit 4 framework 83 | testImplementation "junit:junit:$jUnitVersion" 84 | // Robolectric environment 85 | testImplementation "androidx.test:core:$androidXTestVersion" 86 | testImplementation 'org.robolectric:robolectric:4.8' 87 | // Mockk framework 88 | testImplementation "io.mockk:mockk:$mockkVersion" 89 | // For runBlockingTest, CoroutineDispatcher etc. 90 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1" 91 | testImplementation 'org.json:json:20211205' 92 | testImplementation 'androidx.arch.core:core-testing:2.1.0' 93 | 94 | implementation "androidx.preference:preference-ktx:1.2.0" 95 | 96 | // ViewModel 97 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" 98 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" 99 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" 100 | implementation "android.arch.lifecycle:extensions:1.1.1" 101 | 102 | // Retrofit 103 | implementation 'com.google.code.gson:gson:2.8.9' 104 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 105 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 106 | 107 | // Dagger 108 | implementation "com.google.dagger:dagger:$daggerVersion" 109 | implementation "com.google.dagger:dagger-android:$daggerVersion" 110 | implementation "com.google.dagger:dagger-android-support:$daggerVersion" 111 | kapt "com.google.dagger:dagger-compiler:$daggerVersion" 112 | kapt "com.google.dagger:dagger-android-processor:$daggerVersion" 113 | 114 | // Using Dagger in androidTest and Robolectric too 115 | kaptTest "com.google.dagger:dagger-compiler:$daggerVersion" 116 | kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion" 117 | 118 | 119 | implementation "io.coil-kt:coil-compose:1.4.0" 120 | 121 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "dn.marjan.githubapp", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 1, 15 | "versionName": "1.0", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/dn/marjan/githubapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp 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 | /* 17 | @RunWith(AndroidJUnit4::class) 18 | class ExampleInstrumentedTest { 19 | @Test 20 | fun useAppContext() { 21 | // Context of the app under test. 22 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 23 | assertEquals("dn.marjan.githubapp", appContext.packageName) 24 | } 25 | }*/ 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/Application.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp 2 | 3 | import dagger.android.AndroidInjector 4 | import dagger.android.DaggerApplication 5 | import dn.marjan.githubapp.di.DaggerAppComponent 6 | 7 | 8 | open class Application : DaggerApplication() { 9 | 10 | open val applicationInjector= DaggerAppComponent.builder().application(this).build() 11 | 12 | override fun applicationInjector(): AndroidInjector = applicationInjector 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Surface 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import dn.marjan.githubapp.ui.theme.GithubAppTheme 12 | 13 | class MainActivity : ComponentActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContent { 17 | GithubAppTheme { 18 | // A surface container using the 'background' color from the theme 19 | Surface(color = MaterialTheme.colors.background) { 20 | Greeting("Android") 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | @Composable 28 | fun Greeting(name: String) { 29 | Text(text = "Hello $name!") 30 | } 31 | 32 | @Preview(showBackground = true) 33 | @Composable 34 | fun DefaultPreview() { 35 | GithubAppTheme { 36 | Greeting("Android") 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.base 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.CircularProgressIndicator 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.livedata.observeAsState 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.ui.window.Dialog 17 | import androidx.compose.ui.window.DialogProperties 18 | import androidx.lifecycle.ViewModelProvider 19 | import androidx.lifecycle.ViewModelProviders 20 | import dagger.android.support.DaggerAppCompatActivity 21 | import dn.marjan.githubapp.ui.theme.GithubAppTheme 22 | import javax.inject.Inject 23 | 24 | abstract class BaseActivity : DaggerAppCompatActivity() { 25 | 26 | @Inject 27 | lateinit var viewModelFactory: ViewModelProvider.Factory 28 | 29 | 30 | 31 | protected lateinit var viewModel: V 32 | 33 | protected abstract fun getViewModel(): Class 34 | 35 | @Composable 36 | protected abstract fun ProvideCompose() 37 | 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | viewModel = ViewModelProviders.of(this, viewModelFactory)[getViewModel()] 42 | 43 | setContent { 44 | GithubAppTheme { 45 | ProvideCompose() 46 | ProvideLoadingView() 47 | } 48 | } 49 | } 50 | 51 | 52 | @Composable 53 | fun ProvideLoadingView() { 54 | val loading = viewModel.showLoadingView.observeAsState() 55 | 56 | loading.value?.let { 57 | if (it) 58 | ShowLoading() 59 | } 60 | } 61 | 62 | @Composable 63 | protected open fun ShowLoading() { 64 | 65 | Dialog( 66 | onDismissRequest = { viewModel.hideLoading() }, 67 | DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) 68 | ) { 69 | Box( 70 | contentAlignment = Alignment.Center, 71 | modifier = Modifier 72 | .size(100.dp) 73 | .background(Color.White, shape = RoundedCornerShape(8.dp)) 74 | ) { 75 | CircularProgressIndicator() 76 | } 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.base 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import dn.marjan.githubapp.data.Resource 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | 11 | open class BaseViewModel: ViewModel() { 12 | 13 | private val _showLoadingView = MutableLiveData() 14 | val showLoadingView:LiveData get() = _showLoadingView 15 | 16 | 17 | fun showLoading(){ 18 | _showLoadingView.postValue(true) 19 | } 20 | fun hideLoading(){ 21 | _showLoadingView.postValue(false) 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/base/CoroutineDispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.base 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import javax.inject.Inject 6 | 7 | 8 | class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider 9 | 10 | interface CoroutineDispatcherProvider { 11 | 12 | fun IO(): CoroutineDispatcher = Dispatchers.IO 13 | 14 | fun Main(): CoroutineDispatcher = Dispatchers.Main 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/base/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | import javax.inject.Singleton 8 | 9 | /* 10 | We can make use of that Map by creating a ViewModelFactory by extending ViewModelProvider.Factory and passing the map into it 11 | for more information about "How to inject ViewModels" read this article: 12 | https://www.techyourchance.com/dependency-injection-viewmodel-with-dagger-2 13 | */ 14 | 15 | 16 | @Singleton 17 | class ViewModelFactory @Inject constructor( 18 | private val creators: Map, @JvmSuppressWildcards Provider> 19 | ) : ViewModelProvider.Factory { 20 | 21 | override fun create(modelClass: Class): T { 22 | val creator = creators[modelClass] ?: creators.entries.firstOrNull { 23 | modelClass.isAssignableFrom(it.key) 24 | }?.value ?: throw IllegalArgumentException("Hey! unknown model class $modelClass") 25 | @Suppress("UNCHECKED_CAST") 26 | return creator.get() as T 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/Error.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data 2 | 3 | 4 | sealed class Error: Throwable() { 5 | object NetworkErrors: Error() 6 | object EmptyInputError: Error() 7 | object EmptyResultError: Error() 8 | object SingleError: Error() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/Resource.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data 2 | 3 | sealed class Resource( 4 | val status: Status = Status.UNKNOWN, 5 | val data: T? = null, 6 | val message: String? = null, 7 | val error: Error? = null 8 | ) { 9 | 10 | } 11 | 12 | 13 | class SuccessResource(data: T) : Resource(status = Status.SUCCESS, data = data) 14 | 15 | class ErrorResource(error: Error, message: String = "") : 16 | Resource(status = Status.ERROR, error = error, message = message) 17 | 18 | /*fun loading(): Resource = 19 | Resource(status = Status.LOADING)*/ 20 | 21 | 22 | /*data class Resource( 23 | val status: Status = Status.UNKNOWN, 24 | val data: T? = null, 25 | val message: String? = null, 26 | val error: Error? = null 27 | ) { 28 | 29 | companion object { 30 | 31 | fun success(data: Any): Resource = 32 | Resource(status = Status.SUCCESS, data = data) 33 | 34 | fun error(error: Error , message: String=""): Resource = 35 | Resource(status = Status.ERROR , error= error, message = message ) 36 | 37 | fun loading(): Resource = 38 | Resource(status = Status.LOADING) 39 | } 40 | }*/ 41 | -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/Status.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data 2 | 3 | 4 | enum class Status { 5 | UNKNOWN, 6 | LOADING, 7 | SUCCESS, 8 | ERROR, 9 | INFO, 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/local/LocalDataService.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data.local 2 | 3 | import dn.marjan.githubapp.entity.UserInfo 4 | 5 | interface LocalDataService { 6 | 7 | fun saveUserData(user: UserInfo) 8 | 9 | fun getFullName():String 10 | fun getUsername():String 11 | fun getFollowers():String 12 | fun getFollowing():String 13 | fun getBlog():String 14 | fun getLocation():String 15 | fun getReposCount():String 16 | fun getUserImage():String 17 | 18 | fun isUserLogin(): Boolean 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/local/LocalDataServiceImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data.local 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.preference.PreferenceManager 6 | import dn.marjan.githubapp.data.local.LocalDataServiceImp.PreferenceHelper.get 7 | import dn.marjan.githubapp.data.local.LocalDataServiceImp.PreferenceHelper.set 8 | import dn.marjan.githubapp.entity.UserInfo 9 | import javax.inject.Inject 10 | 11 | class LocalDataServiceImp @Inject constructor(val context: Context) : LocalDataService { 12 | 13 | private var session: SharedPreferences 14 | 15 | init { 16 | session = PreferenceHelper.defaultPrefs(context) 17 | } 18 | 19 | 20 | object PreferenceHelper { 21 | 22 | fun defaultPrefs(context: Context): SharedPreferences = 23 | PreferenceManager.getDefaultSharedPreferences(context) 24 | 25 | fun customPrefs(context: Context, name: String): SharedPreferences = 26 | context.getSharedPreferences(name, Context.MODE_PRIVATE) 27 | 28 | inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) { 29 | val editor = this.edit() 30 | operation(editor) 31 | editor.apply() 32 | } 33 | 34 | /** 35 | * puts a key value pair in shared prefs if doesn't exists, otherwise updates value on given [key] 36 | * 37 | * .AccountSession.PreferenceHelper.set 38 | * and 39 | * .AccountSession.PreferenceHelper.get 40 | * in which activity you want to use it 41 | */ 42 | operator fun SharedPreferences.set(key: String, value: Any) { 43 | when (value) { 44 | is String -> edit { it.putString(key, value) } 45 | is Int -> edit { it.putInt(key, value) } 46 | is Boolean -> edit { it.putBoolean(key, value) } 47 | is Float -> edit { it.putFloat(key, value) } 48 | is Long -> edit { it.putLong(key, value) } 49 | else -> throw UnsupportedOperationException("Not yet implemented") 50 | } 51 | } 52 | 53 | /** 54 | * finds value on given key. 55 | * [T] is the type of value 56 | * @param defaultValue optional default value - will take null for strings, false for bool and -1 for numeric values if [defaultValue] is not specified 57 | */ 58 | inline operator fun SharedPreferences.get( 59 | key: String, 60 | defaultValue: T 61 | ): T { 62 | return when (T::class) { 63 | String::class -> getString(key, defaultValue as String) as T 64 | Int::class -> getInt(key, defaultValue as Int) as T 65 | Boolean::class -> getBoolean(key, defaultValue as Boolean) as T 66 | Float::class -> getFloat(key, defaultValue as Float) as T 67 | Long::class -> getLong(key, defaultValue as Long) as T 68 | else -> throw UnsupportedOperationException("Not yet implemented") 69 | } 70 | } 71 | } 72 | 73 | private val prefUsername = "pref_username" 74 | private val prefUserImage = "pref_user_image" 75 | private val prefUserGitHubUrl = "pref_user_github_url" 76 | private val prefUserFullName = "pref_user_fullname" 77 | private val prefUserBlog = "pref_user_blog" 78 | private val prefUserLocation = "pref_user_location" 79 | private val prefUserEmail = "pref_user_email" 80 | private val prefUserFollowers = "pref_user_followers" 81 | private val prefUserFollowing = "pref_user_following" 82 | private val prefUserReposCount = "pref_user_repos_count" 83 | 84 | override fun saveUserData(user: UserInfo) { 85 | session[prefUsername] = user.login.toString() 86 | session[prefUserImage] = user.avatarUrl.toString() 87 | session[prefUserGitHubUrl] = user.url.toString() 88 | session[prefUserFullName] = user.name.toString() 89 | session[prefUserBlog] = user.blog.toString() 90 | session[prefUserLocation] = user.location.toString() 91 | session[prefUserEmail] = user.email.toString() 92 | session[prefUserFollowers] = user.followers.toString() 93 | session[prefUserFollowing] = user.following.toString() 94 | session[prefUserReposCount] = user.publicRepos.toString() 95 | } 96 | 97 | override fun getFullName(): String = session[prefUserFullName, ""] 98 | 99 | override fun getUsername(): String = session[prefUsername, ""] 100 | 101 | override fun getFollowers(): String = session[prefUserFollowers, ""] 102 | 103 | override fun getFollowing(): String = session[prefUserFollowing, ""] 104 | 105 | override fun getBlog(): String = session[prefUserBlog, ""] 106 | 107 | override fun getLocation(): String = session[prefUserLocation, ""] 108 | 109 | override fun getReposCount(): String = session[prefUserReposCount, ""] 110 | 111 | override fun getUserImage(): String = session[prefUserImage, ""] 112 | 113 | override fun isUserLogin(): Boolean = session.contains(prefUsername) 114 | 115 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/remote/RemoteDataService.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data.remote 2 | 3 | import dn.marjan.githubapp.entity.ReceivedEvents 4 | import dn.marjan.githubapp.entity.Repository 5 | import dn.marjan.githubapp.entity.UserInfo 6 | 7 | interface RemoteDataService { 8 | suspend fun doLogin(): UserInfo 9 | 10 | suspend fun getReceivedEvents(username: String): List 11 | 12 | suspend fun getRepositories(username: String): List 13 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/data/remote/RemoteDataServiceImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.data.remote 2 | 3 | import dn.marjan.githubapp.entity.ReceivedEvents 4 | import dn.marjan.githubapp.entity.Repository 5 | import dn.marjan.githubapp.entity.UserInfo 6 | import dn.marjan.githubapp.server.TaskService 7 | import dn.marjan.githubapp.utils.Config 8 | import javax.inject.Inject 9 | 10 | class RemoteDataServiceImp @Inject constructor(val taskService: TaskService): RemoteDataService { 11 | 12 | override suspend fun doLogin(): UserInfo { 13 | return taskService.login("Bearer ${Config.GITHUB_ACCESS_TOKEN}") 14 | } 15 | 16 | override suspend fun getReceivedEvents(username: String): List { 17 | return taskService.getReceivedEvents(username = username , page = "1") // TODO: dynamic page value with list Lazy Load 18 | } 19 | 20 | override suspend fun getRepositories(username: String): List { 21 | return taskService.getRepositories(username = username , page = "1") 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di 2 | 3 | import dagger.BindsInstance 4 | import dagger.Component 5 | import dagger.android.AndroidInjectionModule 6 | import dagger.android.AndroidInjector 7 | import dn.marjan.githubapp.Application 8 | import dn.marjan.githubapp.di.modules.AppModule 9 | import dn.marjan.githubapp.di.modules.NetworkModule 10 | import javax.inject.Singleton 11 | 12 | 13 | @Singleton 14 | @Component(modules = [ 15 | AndroidInjectionModule::class, 16 | AppModule::class, 17 | ]) 18 | interface AppComponent : AndroidInjector { 19 | 20 | @Component.Builder 21 | interface Builder { 22 | @BindsInstance 23 | fun application(application: android.app.Application): Builder 24 | 25 | fun build(): AppComponent 26 | } 27 | 28 | override fun inject(app: Application) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/keys/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.keys 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | 8 | /* 9 | ViewModelKey annotation, when used on methods annotated with @Provides (“provider methods”), 10 | basically says that the services returned by these methods should be inserted into Map. 11 | The keys in this Map will be of type Class and the values will be of type (subclass of ViewModel). 12 | As a result, Dagger will create an implicit Map filled with Provider objects and put it onto the objects graph 13 | */ 14 | 15 | @MustBeDocumented 16 | @Target( 17 | AnnotationTarget.FUNCTION, 18 | AnnotationTarget.PROPERTY_GETTER, 19 | AnnotationTarget.PROPERTY_SETTER 20 | ) 21 | @Retention(AnnotationRetention.RUNTIME) 22 | @MapKey 23 | annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/modules/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.modules 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | import dn.marjan.githubapp.ui.dashboard.DashboardActivity 6 | import dn.marjan.githubapp.ui.login.ui.LoginActivity 7 | import dn.marjan.githubapp.ui.splash.ui.SplashActivity 8 | 9 | @Module 10 | abstract class ActivityModule { 11 | 12 | @ContributesAndroidInjector 13 | abstract fun bindLoginActivity(): LoginActivity 14 | 15 | @ContributesAndroidInjector 16 | abstract fun bindDashboardActivity(): DashboardActivity 17 | 18 | @ContributesAndroidInjector 19 | abstract fun bindSplashAcitvity(): SplashActivity 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/modules/AppModule.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.modules 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dn.marjan.githubapp.base.AppCoroutineDispatcherProvider 7 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 8 | import kotlinx.coroutines.CoroutineDispatcher 9 | import kotlinx.coroutines.Dispatchers 10 | import javax.inject.Singleton 11 | 12 | @Module( 13 | includes = [ 14 | DataModule::class, 15 | RepositoryModule::class, 16 | NetworkModule::class, 17 | ViewModelModule::class, 18 | ActivityModule::class 19 | ] 20 | ) 21 | object AppModule { 22 | 23 | @Singleton 24 | @Provides 25 | fun provideContext(application: android.app.Application):Context = application.applicationContext 26 | 27 | @Singleton 28 | @Provides 29 | fun provideDispatchers(dispatcher: AppCoroutineDispatcherProvider): CoroutineDispatcherProvider = dispatcher 30 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/modules/DataModule.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.modules 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dn.marjan.githubapp.data.local.LocalDataService 7 | import dn.marjan.githubapp.data.local.LocalDataServiceImp 8 | import dn.marjan.githubapp.data.remote.RemoteDataService 9 | import dn.marjan.githubapp.data.remote.RemoteDataServiceImp 10 | import dn.marjan.githubapp.server.TaskService 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | object DataModule { 15 | 16 | @Singleton 17 | @Provides 18 | fun provideRemoteDataService(taskService: TaskService): RemoteDataService= RemoteDataServiceImp(taskService) 19 | 20 | @Singleton 21 | @Provides 22 | fun provideLocalDataService(context: Context): LocalDataService = LocalDataServiceImp(context) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/modules/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.modules 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import dagger.Module 6 | import dagger.Provides 7 | import dn.marjan.githubapp.server.TaskService 8 | import dn.marjan.githubapp.utils.Config 9 | import okhttp3.OkHttpClient 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.gson.GsonConverterFactory 12 | import java.util.concurrent.TimeUnit 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | object NetworkModule { 17 | 18 | @Singleton 19 | @Provides 20 | fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder() 21 | .connectTimeout(1, TimeUnit.MINUTES) 22 | .readTimeout(1, TimeUnit.MINUTES) 23 | .build() 24 | 25 | 26 | @Singleton 27 | @Provides 28 | fun provideGson(): Gson = GsonBuilder() 29 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") 30 | .setLenient() 31 | .create() 32 | 33 | @Singleton 34 | @Provides 35 | fun provideRetrofit(gson: Gson, okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() 36 | .baseUrl(Config.BASE_URL) 37 | .addConverterFactory(GsonConverterFactory.create(gson)) 38 | .client(okHttpClient) 39 | .build() 40 | 41 | @Singleton 42 | @Provides 43 | fun provideTaskService(retrofit: Retrofit): TaskService = retrofit.create(TaskService::class.java) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/modules/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.modules 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dn.marjan.githubapp.data.local.LocalDataService 6 | import dn.marjan.githubapp.data.remote.RemoteDataService 7 | import dn.marjan.githubapp.ui.home.repo.HomeRepository 8 | import dn.marjan.githubapp.ui.home.repo.HomeRepositoryImp 9 | import dn.marjan.githubapp.ui.login.repo.LoginRepository 10 | import dn.marjan.githubapp.ui.login.repo.LoginRepositoryImp 11 | import dn.marjan.githubapp.ui.profile.repo.ProfileRepository 12 | import dn.marjan.githubapp.ui.profile.repo.ProfileRepositoryImp 13 | import dn.marjan.githubapp.ui.repository.repo.RepoRepository 14 | import dn.marjan.githubapp.ui.repository.repo.RepoRepositoryImp 15 | import dn.marjan.githubapp.ui.splash.repo.SplashRepository 16 | import dn.marjan.githubapp.ui.splash.repo.SplashRepositoryImp 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | class RepositoryModule { 21 | 22 | @Singleton 23 | @Provides 24 | fun provideProfileRepository(localDataService: LocalDataService): ProfileRepository = 25 | ProfileRepositoryImp(localDataService = localDataService) 26 | 27 | @Singleton 28 | @Provides 29 | fun provideLoginRepository( 30 | localDataService: LocalDataService, 31 | remoteDataService: RemoteDataService 32 | ): LoginRepository = 33 | LoginRepositoryImp(remoteDataService, localDataService) 34 | 35 | @Singleton 36 | @Provides 37 | fun provideHomeRepository( 38 | localDataService: LocalDataService, 39 | remoteDataService: RemoteDataService 40 | ): HomeRepository = 41 | HomeRepositoryImp(remoteDataService, localDataService) 42 | 43 | @Singleton 44 | @Provides 45 | fun provideRepoRepository( 46 | localDataService: LocalDataService, 47 | remoteDataService: RemoteDataService 48 | ): RepoRepository = 49 | RepoRepositoryImp(remoteDataService, localDataService) 50 | 51 | @Singleton 52 | @Provides 53 | fun provideSplashRepository(localDataService: LocalDataService): SplashRepository = 54 | SplashRepositoryImp(localDataService = localDataService) 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/di/modules/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.di.modules 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.multibindings.IntoMap 9 | import dn.marjan.githubapp.base.ViewModelFactory 10 | import dn.marjan.githubapp.di.keys.ViewModelKey 11 | import dn.marjan.githubapp.ui.dashboard.DashboardViewModel 12 | import dn.marjan.githubapp.ui.home.ui.HomeViewModel 13 | import dn.marjan.githubapp.ui.login.ui.LoginViewModel 14 | import dn.marjan.githubapp.ui.profile.ui.ProfileViewModel 15 | import dn.marjan.githubapp.ui.repository.ui.RepositoryViewModel 16 | import dn.marjan.githubapp.ui.splash.ui.SplashViewModel 17 | import javax.inject.Provider 18 | 19 | 20 | /** 21 | Note that the return type of the provider method is ViewModel, not ViewModel1. It’s intentional. 22 | @IntoMap annotation says that Provider object for this service will be inserted into Map, and @ViewModelKey annotation specifies under which 23 | key it will reside. 24 | The net result of the above code will be that Dagger will create Map data structure 25 | filled with Provider objects and then provide it implicitly to other services. 26 | **/ 27 | 28 | 29 | @Module 30 | abstract class ViewModelModule { 31 | 32 | 33 | @Binds 34 | @IntoMap 35 | @ViewModelKey(DashboardViewModel::class) 36 | abstract fun bindDashboardActivityVM(dashboardViewModel: DashboardViewModel): ViewModel 37 | 38 | @Binds 39 | @IntoMap 40 | @ViewModelKey(HomeViewModel::class) 41 | abstract fun bindHomeVM(homeViewModel: HomeViewModel): ViewModel 42 | 43 | @Binds 44 | @IntoMap 45 | @ViewModelKey(LoginViewModel::class) 46 | abstract fun bindLoginActivityVM(loginViewModel: LoginViewModel): ViewModel 47 | 48 | @Binds 49 | @IntoMap 50 | @ViewModelKey(SplashViewModel::class) 51 | abstract fun bindSplashActivityVM(splashViewModel: SplashViewModel): ViewModel 52 | 53 | @Binds 54 | @IntoMap 55 | @ViewModelKey(RepositoryViewModel::class) 56 | abstract fun bindRepositoryVM(repositoryViewModel: RepositoryViewModel): ViewModel 57 | 58 | @Binds 59 | @IntoMap 60 | @ViewModelKey(ProfileViewModel::class) 61 | abstract fun bindProfileVM(profileViewModel: ProfileViewModel): ViewModel 62 | 63 | @Binds 64 | internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/entity/ReceivedEvents.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.entity 2 | 3 | class ReceivedEvents { 4 | var id = "" 5 | var type = "" 6 | var actor: Actor? = null 7 | var repo: Repository? = null 8 | var payload: Payload? = null 9 | 10 | 11 | class Payload { 12 | var action = "" 13 | } 14 | 15 | class Repository { 16 | var name = "" 17 | var url = "" 18 | } 19 | 20 | class Actor { 21 | var login = "" 22 | var avatar_url = "" 23 | var url = "" 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/entity/Repository.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class Repository { 6 | var name: String? = null 7 | var description: String? = null 8 | var language: String? = null 9 | 10 | @SerializedName("updated_at") 11 | var lastUpdate: String? = null 12 | 13 | @SerializedName("stargazers_count") 14 | var starsCount: String? = null 15 | 16 | @SerializedName("fork") 17 | var isFork = false 18 | 19 | @SerializedName("forks_count") 20 | var forkCounts: String? = null 21 | 22 | @SerializedName("watchers_count") 23 | var watchersCount: String? = null 24 | 25 | var topics: List? = null 26 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/entity/UserInfo.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.entity 2 | 3 | import com.google.gson.annotations.Expose 4 | 5 | import com.google.gson.annotations.SerializedName 6 | 7 | 8 | class UserInfo { 9 | @SerializedName("login") 10 | @Expose 11 | val login: String? = null 12 | 13 | @SerializedName("id") 14 | @Expose 15 | val id: Int? = null 16 | 17 | @SerializedName("node_id") 18 | @Expose 19 | val nodeId: String? = null 20 | 21 | @SerializedName("avatar_url") 22 | @Expose 23 | val avatarUrl: String? = null 24 | 25 | @SerializedName("gravatar_id") 26 | @Expose 27 | val gravatarId: String? = null 28 | 29 | @SerializedName("url") 30 | @Expose 31 | val url: String? = null 32 | 33 | @SerializedName("html_url") 34 | @Expose 35 | val htmlUrl: String? = null 36 | 37 | @SerializedName("followers_url") 38 | @Expose 39 | val followersUrl: String? = null 40 | 41 | @SerializedName("following_url") 42 | @Expose 43 | val followingUrl: String? = null 44 | 45 | @SerializedName("gists_url") 46 | @Expose 47 | val gistsUrl: String? = null 48 | 49 | @SerializedName("starred_url") 50 | @Expose 51 | val starredUrl: String? = null 52 | 53 | @SerializedName("subscriptions_url") 54 | @Expose 55 | val subscriptionsUrl: String? = null 56 | 57 | @SerializedName("organizations_url") 58 | @Expose 59 | val organizationsUrl: String? = null 60 | 61 | @SerializedName("repos_url") 62 | @Expose 63 | val reposUrl: String? = null 64 | 65 | @SerializedName("events_url") 66 | @Expose 67 | val eventsUrl: String? = null 68 | 69 | @SerializedName("received_events_url") 70 | @Expose 71 | val receivedEventsUrl: String? = null 72 | 73 | @SerializedName("type") 74 | @Expose 75 | val type: String? = null 76 | 77 | @SerializedName("site_admin") 78 | @Expose 79 | val siteAdmin: Boolean? = null 80 | 81 | @SerializedName("name") 82 | @Expose 83 | val name: String? = null 84 | 85 | @SerializedName("company") 86 | @Expose 87 | val company: Any? = null 88 | 89 | @SerializedName("blog") 90 | @Expose 91 | val blog: String? = null 92 | 93 | @SerializedName("location") 94 | @Expose 95 | val location: String? = null 96 | 97 | @SerializedName("email") 98 | @Expose 99 | val email: String? = null 100 | 101 | @SerializedName("hireable") 102 | @Expose 103 | val hireable: Any? = null 104 | 105 | @SerializedName("bio") 106 | @Expose 107 | val bio: String? = null 108 | 109 | @SerializedName("twitter_username") 110 | @Expose 111 | val twitterUsername: Any? = null 112 | 113 | @SerializedName("public_repos") 114 | @Expose 115 | val publicRepos: Int? = null 116 | 117 | @SerializedName("public_gists") 118 | @Expose 119 | val publicGists: Int? = null 120 | 121 | @SerializedName("followers") 122 | @Expose 123 | val followers: Int? = null 124 | 125 | @SerializedName("following") 126 | @Expose 127 | val following: Int? = null 128 | 129 | @SerializedName("created_at") 130 | @Expose 131 | val createdAt: String? = null 132 | 133 | @SerializedName("updated_at") 134 | @Expose 135 | val updatedAt: String? = null 136 | 137 | @SerializedName("_gists") 138 | @Expose 139 | val Gists: Int? = null 140 | 141 | @SerializedName("total__repos") 142 | @Expose 143 | val totalRepos: Int? = null 144 | 145 | @SerializedName("owned__repos") 146 | @Expose 147 | val ownedRepos: Int? = null 148 | 149 | @SerializedName("disk_usage") 150 | @Expose 151 | val diskUsage: Int? = null 152 | 153 | @SerializedName("collaborators") 154 | @Expose 155 | val collaborators: Int? = null 156 | 157 | @SerializedName("two_factor_authentication") 158 | @Expose 159 | val twoFactorAuthentication: Boolean? = null 160 | 161 | @SerializedName("plan") 162 | @Expose 163 | val plan: Plan? = null 164 | 165 | class Plan{ 166 | 167 | @SerializedName("name") 168 | @Expose 169 | val name: String? = null 170 | 171 | @SerializedName("space") 172 | @Expose 173 | val space: Int? = null 174 | 175 | @SerializedName("collaborators") 176 | @Expose 177 | val collaborators: Int? = null 178 | 179 | @SerializedName("_repos") 180 | @Expose 181 | val Repos: Int? = null 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/server/TaskService.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.server 2 | 3 | import dn.marjan.githubapp.entity.ReceivedEvents 4 | import dn.marjan.githubapp.entity.Repository 5 | import dn.marjan.githubapp.entity.UserInfo 6 | import okhttp3.ResponseBody 7 | import retrofit2.Call 8 | import retrofit2.http.* 9 | 10 | 11 | interface TaskService { 12 | 13 | @GET("user") 14 | suspend fun login( 15 | @Header("Authorization") auth: String 16 | ) : UserInfo 17 | 18 | @GET("users/{username}/received_events?") 19 | suspend fun getReceivedEvents( 20 | @Path("username") username: String, 21 | @Query("page") page: String 22 | ) : List 23 | 24 | @GET("users/{username}/repos?") 25 | suspend fun getRepositories( 26 | @Path("username") username: String, 27 | @Query("page") page: String 28 | ) : List 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/dashboard/BottomBarItems.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.dashboard 2 | 3 | import android.graphics.drawable.Drawable 4 | import dn.marjan.githubapp.R 5 | 6 | sealed class BottomBarItems(val name: String , val icon: Int, val route: String ) { 7 | object Home: BottomBarItems("Home" , R.drawable.ic_home , "home") 8 | object Repository: BottomBarItems("Repositories" , R.drawable.ic_folder , "repo") 9 | object Profile: BottomBarItems("Profile" , R.drawable.ic_account , "profile") 10 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/dashboard/DashboardActivity.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.dashboard 2 | 3 | import androidx.compose.material.* 4 | import androidx.compose.runtime.* 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.res.colorResource 7 | import androidx.compose.ui.res.painterResource 8 | import androidx.core.content.ContextCompat 9 | import androidx.navigation.NavController 10 | import androidx.navigation.NavHostController 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | import androidx.navigation.compose.currentBackStackEntryAsState 14 | import androidx.navigation.compose.rememberNavController 15 | import dn.marjan.githubapp.R 16 | import dn.marjan.githubapp.base.BaseActivity 17 | import dn.marjan.githubapp.ui.home.ui.HomePage 18 | import dn.marjan.githubapp.ui.home.ui.HomeViewModel 19 | import dn.marjan.githubapp.ui.profile.ui.ProfilePage 20 | import dn.marjan.githubapp.ui.profile.ui.ProfileViewModel 21 | import dn.marjan.githubapp.ui.repository.ui.RepositoryPage 22 | import dn.marjan.githubapp.ui.repository.ui.RepositoryViewModel 23 | import javax.inject.Inject 24 | 25 | class DashboardActivity : BaseActivity() { 26 | 27 | @Inject 28 | lateinit var homeViewModel: HomeViewModel 29 | @Inject 30 | lateinit var repositoryViewModel: RepositoryViewModel 31 | @Inject 32 | lateinit var profiViewModel: ProfileViewModel 33 | 34 | private lateinit var pageTitle: MutableState 35 | 36 | 37 | override fun getViewModel(): Class = DashboardViewModel::class.java 38 | 39 | private val items = listOf( 40 | BottomBarItems.Home, 41 | BottomBarItems.Repository, 42 | BottomBarItems.Profile 43 | ) 44 | 45 | 46 | @Composable 47 | override fun ProvideCompose() { 48 | val navController = rememberNavController() 49 | pageTitle = remember { mutableStateOf("") } 50 | 51 | Scaffold( 52 | topBar = { 53 | TopAppBar( 54 | title = { Text(text = pageTitle.value, color = Color.White) }, 55 | backgroundColor = colorResource(R.color.black), 56 | ) 57 | }, 58 | bottomBar = { BottomBar(navController = navController) }, 59 | backgroundColor =colorResource(R.color.black) 60 | ) { 61 | CurrentPage(navController) 62 | } 63 | } 64 | 65 | 66 | @Composable 67 | fun BottomBar(navController: NavController) { 68 | BottomNavigation( 69 | backgroundColor = colorResource(R.color.black), 70 | contentColor = Color.White 71 | ) { 72 | val navBackStackEntry by navController.currentBackStackEntryAsState() 73 | val currentRoute = navBackStackEntry?.destination?.route 74 | 75 | items.forEach { item -> 76 | BottomNavigationItem( 77 | selected = currentRoute == item.route, 78 | icon = { 79 | Icon( 80 | painter = painterResource(id = item.icon), 81 | contentDescription = item.name 82 | ) 83 | }, 84 | label = { Text(text = item.name) }, 85 | selectedContentColor = Color.White, 86 | unselectedContentColor = Color.Gray, 87 | alwaysShowLabel = true, 88 | onClick = { 89 | navController.navigate(item.route) { 90 | 91 | navController.graph.startDestinationRoute?.let { 92 | popUpTo(it) { 93 | saveState = true 94 | } 95 | } 96 | 97 | launchSingleTop = true 98 | restoreState = true 99 | } 100 | } 101 | ) 102 | } 103 | } 104 | } 105 | 106 | @Composable 107 | fun CurrentPage(navController: NavHostController) { 108 | NavHost( 109 | navController = navController, 110 | startDestination = BottomBarItems.Home.route, 111 | builder = { 112 | 113 | composable(BottomBarItems.Home.route) { 114 | pageTitle.value = BottomBarItems.Home.name 115 | HomePage(this@DashboardActivity, homeViewModel) 116 | } 117 | composable(BottomBarItems.Repository.route) { 118 | pageTitle.value = BottomBarItems.Repository.name 119 | RepositoryPage(this@DashboardActivity, repositoryViewModel) 120 | } 121 | composable(BottomBarItems.Profile.route) { 122 | pageTitle.value = BottomBarItems.Profile.name 123 | ProfilePage( profiViewModel) 124 | } 125 | }) 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/dashboard/DashboardViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.dashboard 2 | 3 | import dn.marjan.githubapp.base.BaseViewModel 4 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 5 | import dn.marjan.githubapp.ui.login.repo.LoginRepository 6 | import javax.inject.Inject 7 | 8 | class DashboardViewModel @Inject constructor( 9 | val repository: LoginRepository, 10 | val dispatcher: CoroutineDispatcherProvider 11 | ) : BaseViewModel() { 12 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/home/repo/HomeRepository.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.home.repo 2 | 3 | import dn.marjan.githubapp.entity.ReceivedEvents 4 | 5 | interface HomeRepository { 6 | suspend fun getReceivedEvents(username:String) : List 7 | 8 | fun getUsername():String 9 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/home/repo/HomeRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.home.repo 2 | 3 | import dn.marjan.githubapp.data.local.LocalDataService 4 | import dn.marjan.githubapp.data.remote.RemoteDataService 5 | import dn.marjan.githubapp.entity.ReceivedEvents 6 | import javax.inject.Inject 7 | 8 | class HomeRepositoryImp @Inject constructor( 9 | val remoteDataService: RemoteDataService, 10 | val localDataService: LocalDataService 11 | ) : HomeRepository { 12 | 13 | override suspend fun getReceivedEvents(username: String): List { 14 | return remoteDataService.getReceivedEvents(username) 15 | } 16 | 17 | override fun getUsername(): String { 18 | return localDataService.getUsername() 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/home/ui/HomePage.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.home.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.clip 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.text.SpanStyle 13 | import androidx.compose.ui.text.buildAnnotatedString 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.text.withStyle 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import coil.compose.rememberImagePainter 20 | import dn.marjan.githubapp.data.Status 21 | import dn.marjan.githubapp.entity.ReceivedEvents 22 | import dn.marjan.githubapp.ui.dashboard.DashboardActivity 23 | import dn.marjan.githubapp.utils.log 24 | import androidx.compose.foundation.lazy.items 25 | import dn.marjan.githubapp.utils.getAction 26 | 27 | val receivedEvents: MutableState> = mutableStateOf(ArrayList()) 28 | 29 | @Composable 30 | fun HomePage(dashboard: DashboardActivity, viewModel: HomeViewModel) { 31 | viewModel. getReceivedEvents() 32 | 33 | PageContent() 34 | 35 | 36 | viewModel.homeResponse.observe(dashboard) { 37 | when (it.status) { 38 | Status.SUCCESS -> { 39 | it.data?.let { list -> 40 | val items: List = (list) 41 | receivedEvents.value = items 42 | } 43 | } 44 | else -> { 45 | log("some error") 46 | } 47 | } 48 | } 49 | } 50 | 51 | 52 | @Preview 53 | @Composable 54 | fun PageContent() { 55 | LazyColumn( 56 | contentPadding = PaddingValues( 57 | top = 16.dp, 58 | bottom = 40.dp, 59 | start = 16.dp, 60 | end = 16.dp 61 | ) 62 | ) { 63 | items(receivedEvents.value){item -> 64 | EventItem(item) 65 | } 66 | } 67 | } 68 | 69 | // TODO: change UI of Event items 70 | @Composable 71 | fun EventItem(item: ReceivedEvents) { 72 | return Row( 73 | modifier = Modifier 74 | .fillMaxSize() 75 | .padding(bottom = 16.dp) 76 | ) { 77 | Image( 78 | painter = rememberImagePainter( 79 | data = item.actor?.avatar_url, 80 | builder = { 81 | crossfade(true) 82 | }, 83 | ), contentDescription = "User avatar image", 84 | modifier = Modifier 85 | .size(45.dp) 86 | .clip(CircleShape) 87 | ) 88 | Text( 89 | buildAnnotatedString { 90 | withStyle( 91 | style = SpanStyle( 92 | color = Color.White, 93 | fontSize = 16.sp, 94 | fontWeight = FontWeight.Bold 95 | ) 96 | ) { 97 | append(item.actor?.login.toString()) 98 | } 99 | withStyle(style = SpanStyle(color = Color.White, fontSize = 15.sp)) { 100 | append(" ${getAction(item)} ") 101 | } 102 | withStyle( 103 | style = SpanStyle( 104 | color = Color.White, 105 | fontSize = 16.sp, 106 | fontWeight = FontWeight.Bold 107 | ) 108 | ) { 109 | append(item.repo?.name.toString()) 110 | } 111 | }, 112 | modifier = Modifier 113 | .padding(top = 10.dp, bottom = 8.dp, start = 16.dp, end = 16.dp) 114 | ) 115 | } 116 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/home/ui/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.home.ui 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.viewModelScope 8 | import dn.marjan.githubapp.base.BaseViewModel 9 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 10 | import dn.marjan.githubapp.data.* 11 | import dn.marjan.githubapp.entity.ReceivedEvents 12 | import dn.marjan.githubapp.ui.home.repo.HomeRepository 13 | import dn.marjan.githubapp.ui.login.repo.LoginRepository 14 | import dn.marjan.githubapp.utils.log 15 | import kotlinx.coroutines.launch 16 | import retrofit2.HttpException 17 | import javax.inject.Inject 18 | 19 | class HomeViewModel @Inject constructor( 20 | val repository: HomeRepository, 21 | val dispatcher: CoroutineDispatcherProvider 22 | ) : BaseViewModel() { 23 | 24 | 25 | private val _homeResponse = MutableLiveData>>() 26 | 27 | 28 | val homeResponse: LiveData>> 29 | get() = _homeResponse 30 | 31 | fun getReceivedEvents(){ 32 | viewModelScope.launch(dispatcher.IO()){ 33 | showLoading() 34 | try{ 35 | _homeResponse.postValue(SuccessResource(data = repository.getReceivedEvents(username = repository.getUsername()))) 36 | hideLoading() 37 | 38 | }catch (ex: HttpException){ 39 | _homeResponse.postValue(ErrorResource(error = Error.NetworkErrors, message = ex.message().toString())) 40 | } 41 | } 42 | } 43 | 44 | fun justLog() { 45 | log("WE RE RENDER HOME PAGE MARJAN :)") 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/login/repo/LoginRepository.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.login.repo 2 | 3 | import dn.marjan.githubapp.entity.UserInfo 4 | 5 | 6 | interface LoginRepository { 7 | 8 | suspend fun loginReq(username: String , password: String): UserInfo 9 | 10 | fun saveUserData(user: UserInfo) 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/login/repo/LoginRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.login.repo 2 | 3 | import dn.marjan.githubapp.data.remote.RemoteDataService 4 | import dn.marjan.githubapp.data.local.LocalDataService 5 | import dn.marjan.githubapp.entity.UserInfo 6 | import javax.inject.Inject 7 | 8 | class LoginRepositoryImp @Inject constructor( 9 | val remoteDataService: RemoteDataService, 10 | val localDataService: LocalDataService 11 | ) : LoginRepository { 12 | 13 | override suspend fun loginReq(username: String, password: String): UserInfo { 14 | return remoteDataService.doLogin() 15 | } 16 | 17 | override fun saveUserData(user: UserInfo) { 18 | localDataService.saveUserData(user) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/login/ui/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.login.ui 2 | 3 | import android.content.Intent 4 | import android.widget.ScrollView 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.gestures.Orientation 7 | import androidx.compose.foundation.gestures.scrollable 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.foundation.verticalScroll 13 | import androidx.compose.material.* 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.res.colorResource 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.text.input.PasswordVisualTransformation 22 | import androidx.compose.ui.text.input.VisualTransformation 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import dn.marjan.githubapp.R 26 | import dn.marjan.githubapp.base.BaseActivity 27 | import dn.marjan.githubapp.data.Status 28 | import dn.marjan.githubapp.ui.dashboard.DashboardActivity 29 | import kotlinx.coroutines.launch 30 | 31 | /* 32 | Google: If you use another observable type such as LiveData in Compose, you should convert it to State before reading it in a composable 33 | using a composable extension function like LiveData.observeAsState(). 34 | */ 35 | 36 | class LoginActivity : BaseActivity() { 37 | 38 | 39 | override fun getViewModel(): Class = LoginViewModel::class.java 40 | 41 | @Composable 42 | override fun ProvideCompose() { 43 | val scaffoldState = rememberScaffoldState() 44 | val coroutineScope = rememberCoroutineScope() 45 | 46 | 47 | var username by remember { mutableStateOf("") } 48 | var password by remember { mutableStateOf("") } 49 | 50 | 51 | 52 | viewModel.loginResponse.observe(this) { 53 | when (it.status) { 54 | Status.SUCCESS -> { 55 | startActivity( 56 | Intent( 57 | this, 58 | DashboardActivity::class.java 59 | ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 60 | ) 61 | } 62 | else -> { 63 | coroutineScope.launch { 64 | scaffoldState.snackbarHostState.showSnackbar( 65 | message = it.message.toString(), 66 | actionLabel = "OK" 67 | ) 68 | } 69 | } 70 | } 71 | } 72 | 73 | 74 | 75 | 76 | Scaffold( 77 | scaffoldState = scaffoldState, 78 | ) { 79 | MainView( 80 | username = username, 81 | onUsernameChange = { username = it }, 82 | password = password, 83 | onPasswordChange = { password = it }, 84 | onLoginButtonClick = { viewModel.validateLoginReq(username, password) }) 85 | } 86 | 87 | } 88 | 89 | 90 | @Composable 91 | fun MainView( 92 | username: String, 93 | onUsernameChange: (String) -> Unit, 94 | password: String, 95 | onPasswordChange: (String) -> Unit, 96 | onLoginButtonClick: () -> Unit 97 | ) { 98 | LazyColumn(content = { 99 | item { 100 | Column( 101 | modifier = Modifier 102 | .fillMaxSize() , 103 | horizontalAlignment = Alignment.CenterHorizontally, 104 | ) { 105 | Image( 106 | painter = painterResource(id = R.drawable.ic_github), 107 | contentDescription = "Github", 108 | Modifier 109 | .padding(end = 32.dp, start = 32.dp, top = 50.dp, bottom = 40.dp) 110 | ) 111 | OutlinedTextField( 112 | value = username, 113 | onValueChange = onUsernameChange, 114 | label = { Text(text = "Username / email") }, 115 | modifier = Modifier 116 | .padding(end = 32.dp, start = 32.dp, bottom = 12.dp) 117 | .fillMaxWidth() 118 | ) 119 | OutlinedTextField( 120 | value = password, 121 | onValueChange = onPasswordChange, 122 | visualTransformation = PasswordVisualTransformation(), 123 | label = { Text(text = "Password") }, 124 | modifier = Modifier 125 | .padding(end = 32.dp, start = 32.dp, bottom = 32.dp) 126 | .fillMaxWidth() 127 | ) 128 | Button( 129 | onClick = onLoginButtonClick, 130 | modifier = Modifier 131 | .padding(end = 32.dp, start = 32.dp, bottom = 12.dp) 132 | .fillMaxWidth() 133 | .clip(RoundedCornerShape(9.dp)), 134 | colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.black)) 135 | ) { 136 | Text( 137 | text = "Sign in", color = Color.White, fontSize = 18.sp, modifier = Modifier 138 | .padding(4.dp) 139 | ) 140 | } 141 | } 142 | } 143 | }) 144 | 145 | } 146 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/login/ui/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.login.ui 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import androidx.lifecycle.* 5 | import dn.marjan.githubapp.base.BaseViewModel 6 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 7 | import dn.marjan.githubapp.data.Resource 8 | import dn.marjan.githubapp.data.Error 9 | import dn.marjan.githubapp.data.ErrorResource 10 | import dn.marjan.githubapp.data.SuccessResource 11 | import dn.marjan.githubapp.entity.UserInfo 12 | import dn.marjan.githubapp.ui.login.repo.LoginRepository 13 | import kotlinx.coroutines.* 14 | import org.json.JSONObject 15 | import retrofit2.HttpException 16 | import javax.inject.Inject 17 | 18 | /** 19 | MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern 20 | and we should encapsulate access to MutableLiveData. 21 | **/ 22 | 23 | class LoginViewModel @Inject constructor( 24 | val repository: LoginRepository, 25 | val dispatcher: CoroutineDispatcherProvider 26 | ) : BaseViewModel() { 27 | 28 | private val _loginResponse = MutableLiveData>() 29 | 30 | val loginResponse: LiveData> 31 | get() = _loginResponse 32 | 33 | 34 | fun validateLoginReq(username: String, password: String) { 35 | if (username.isEmpty() || password.isEmpty()) { 36 | _loginResponse.postValue(ErrorResource(error = Error.EmptyInputError)) 37 | } else { 38 | doLogin(username = username, password = password) 39 | } 40 | } 41 | 42 | 43 | fun doLogin(username: String, password: String) { 44 | viewModelScope.launch(dispatcher.IO()) { 45 | showLoading() 46 | try { 47 | val res = SuccessResource(data = repository.loginReq(username, password)) 48 | repository.saveUserData(res.data as UserInfo) 49 | _loginResponse.postValue(res) 50 | 51 | hideLoading() 52 | } catch (ex: HttpException) { 53 | 54 | ex.response()?.let { response -> 55 | response.errorBody()?.let { responseBody -> 56 | val msg = JSONObject(responseBody.string()).get("message") 57 | _loginResponse.postValue( 58 | (ErrorResource( 59 | error = Error.NetworkErrors, 60 | message = msg.toString() 61 | )) 62 | ) 63 | } 64 | } 65 | hideLoading() 66 | } 67 | } 68 | } 69 | 70 | 71 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/profile/repo/ProfileRepository.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.profile.repo 2 | 3 | 4 | interface ProfileRepository { 5 | 6 | fun getFullName():String 7 | fun getUsername():String 8 | fun getFollowers():String 9 | fun getFollowing():String 10 | fun getBlog():String 11 | fun getLocation():String 12 | fun getReposCount():String 13 | fun getUserImage():String 14 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/profile/repo/ProfileRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.profile.repo 2 | 3 | import dn.marjan.githubapp.data.local.LocalDataService 4 | import dn.marjan.githubapp.entity.UserInfo 5 | import javax.inject.Inject 6 | 7 | class ProfileRepositoryImp @Inject constructor( 8 | var localDataService: LocalDataService 9 | ) : ProfileRepository{ 10 | override fun getFullName(): String = localDataService.getFullName() 11 | 12 | override fun getUsername(): String = localDataService.getUsername() 13 | 14 | override fun getFollowers(): String = localDataService.getFollowers() 15 | 16 | override fun getFollowing(): String = localDataService.getFollowing() 17 | 18 | override fun getBlog(): String = localDataService.getBlog() 19 | 20 | override fun getLocation(): String = localDataService.getLocation() 21 | 22 | override fun getReposCount(): String = localDataService.getReposCount() 23 | 24 | override fun getUserImage(): String = localDataService.getUserImage() 25 | 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/profile/ui/ProfilePage.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.profile.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.CircleShape 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.clip 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.ColorFilter 13 | import androidx.compose.ui.res.colorResource 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.text.style.TextAlign 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import coil.compose.rememberImagePainter 21 | import dn.marjan.githubapp.R 22 | 23 | 24 | @Composable 25 | fun ProfilePage(profileViewModel: ProfileViewModel) { 26 | 27 | Column( 28 | horizontalAlignment = Alignment.CenterHorizontally, 29 | modifier = Modifier 30 | .fillMaxSize() 31 | .padding(bottom = 16.dp, top = 32.dp) 32 | ) { 33 | Image( 34 | painter = rememberImagePainter( 35 | data = profileViewModel.getUserImage(), 36 | builder = { crossfade(true) }), contentDescription = "Profile Image", 37 | modifier = Modifier 38 | .size(200.dp) 39 | .fillMaxWidth() 40 | .clip(CircleShape) 41 | ) 42 | Text( 43 | text = profileViewModel.getFullName(), 44 | color = Color.White, 45 | fontSize = 16.sp, 46 | fontWeight = FontWeight.Bold, 47 | textAlign = TextAlign.Center, 48 | modifier = Modifier 49 | .padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 32.dp) 50 | .fillMaxWidth() 51 | 52 | ) 53 | Text( 54 | text = profileViewModel.getUsername(), 55 | color = Color.White, 56 | fontSize = 14.sp, 57 | textAlign = TextAlign.Center, 58 | modifier = Modifier 59 | .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) 60 | .fillMaxWidth() 61 | ) 62 | Row( 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .padding(start = 32.dp, top = 12.dp) 66 | ) { 67 | Image( 68 | painter = painterResource(id = R.drawable.ic_people), 69 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 70 | contentDescription = "Followers and Following", 71 | modifier = Modifier 72 | .size(20.dp) 73 | ) 74 | Text( 75 | text = "${profileViewModel.getFollowers()} followers . ${profileViewModel.getFollowing()} following", 76 | fontSize = 15.sp, 77 | color = Color.White, 78 | modifier = Modifier 79 | .padding(start = 10.dp) 80 | ) 81 | } 82 | Row( 83 | modifier = Modifier 84 | .fillMaxWidth() 85 | .padding(start = 32.dp, top = 12.dp) 86 | ) { 87 | Image( 88 | painter = painterResource(id = R.drawable.ic_link), 89 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 90 | contentDescription = "Blog", 91 | modifier = Modifier 92 | .size(20.dp) 93 | ) 94 | Text( 95 | text = profileViewModel.getBlog(), 96 | fontSize = 15.sp, 97 | color = Color.White, 98 | modifier = Modifier 99 | .padding(start = 10.dp) 100 | ) 101 | } 102 | Row( 103 | modifier = Modifier 104 | .fillMaxWidth() 105 | .padding(start = 32.dp, top = 12.dp) 106 | ) { 107 | Image( 108 | painter = painterResource(id = R.drawable.ic_location), 109 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 110 | contentDescription = "Location", 111 | modifier = Modifier 112 | .size(20.dp) 113 | ) 114 | Text( 115 | text = profileViewModel.getLocation(), 116 | fontSize = 15.sp, 117 | color = Color.White, 118 | modifier = Modifier 119 | .padding(start = 10.dp) 120 | ) 121 | } 122 | Row( 123 | modifier = Modifier 124 | .fillMaxWidth() 125 | .padding(start = 32.dp, top = 12.dp) 126 | ) { 127 | Image( 128 | painter = painterResource(id = R.drawable.ic_folder), 129 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 130 | contentDescription = "Repos count", 131 | modifier = Modifier 132 | .size(20.dp) 133 | ) 134 | Text( 135 | text = profileViewModel.getReposCount(), 136 | fontSize = 15.sp, 137 | color = Color.White, 138 | modifier = Modifier 139 | .padding(start = 10.dp) 140 | ) 141 | } 142 | 143 | } 144 | 145 | } 146 | 147 | @Preview 148 | @Composable 149 | fun PageContent() { 150 | 151 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/profile/ui/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.profile.ui 2 | 3 | import dn.marjan.githubapp.base.BaseViewModel 4 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 5 | import dn.marjan.githubapp.ui.profile.repo.ProfileRepository 6 | import javax.inject.Inject 7 | 8 | class ProfileViewModel @Inject constructor( 9 | val repository: ProfileRepository, 10 | val dispatcherProvider: CoroutineDispatcherProvider 11 | ) : BaseViewModel(){ 12 | 13 | fun getFullName(): String = repository.getFullName() 14 | 15 | fun getUsername(): String = repository.getUsername() 16 | 17 | fun getFollowers(): String = repository.getFollowers() 18 | 19 | fun getFollowing(): String = repository.getFollowing() 20 | 21 | fun getBlog(): String = repository.getBlog() 22 | 23 | fun getLocation(): String = repository.getLocation() 24 | 25 | fun getReposCount(): String = repository.getReposCount() 26 | 27 | fun getUserImage(): String = repository.getUserImage() 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/repository/repo/RepoRepository.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.repository.repo 2 | 3 | import dn.marjan.githubapp.entity.Repository 4 | 5 | interface RepoRepository { 6 | 7 | fun getUsername():String 8 | 9 | suspend fun getRepositories(username: String): List 10 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/repository/repo/RepoRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.repository.repo 2 | 3 | import dn.marjan.githubapp.data.local.LocalDataService 4 | import dn.marjan.githubapp.data.remote.RemoteDataService 5 | import dn.marjan.githubapp.entity.Repository 6 | import javax.inject.Inject 7 | 8 | class RepoRepositoryImp @Inject constructor( 9 | val remoteDataService: RemoteDataService, 10 | val localDataService: LocalDataService 11 | ) : RepoRepository{ 12 | 13 | override fun getUsername(): String = localDataService.getUsername() 14 | 15 | override suspend fun getRepositories(username: String): List { 16 | return remoteDataService.getRepositories(username) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/repository/ui/RepositoryPage.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.repository.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.Divider 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.graphics.ColorFilter 19 | import androidx.compose.ui.res.colorResource 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import dn.marjan.githubapp.R 26 | import dn.marjan.githubapp.data.Status 27 | import dn.marjan.githubapp.entity.Repository 28 | import dn.marjan.githubapp.ui.dashboard.DashboardActivity 29 | import dn.marjan.githubapp.utils.getLanguageColor 30 | import dn.marjan.githubapp.utils.log 31 | 32 | val repositories: MutableState> = mutableStateOf(ArrayList()) 33 | 34 | 35 | @Composable 36 | fun RepositoryPage(dashboard: DashboardActivity, viewModel: RepositoryViewModel) { 37 | viewModel.getRepositories() 38 | 39 | PageContent() 40 | 41 | viewModel.repoResponse.observe(dashboard) { 42 | when (it.status) { 43 | Status.SUCCESS -> { 44 | it.data?.let { list -> 45 | repositories.value = list 46 | } 47 | } 48 | else -> { 49 | log("sth has error") 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Preview 56 | @Composable 57 | fun PageContent() { 58 | 59 | LazyColumn( 60 | contentPadding = PaddingValues( 61 | top = 16.dp, 62 | bottom = 40.dp, 63 | start = 16.dp, 64 | end = 16.dp 65 | ) 66 | ) { 67 | items(repositories.value) { item -> 68 | RepositoryItem(item) 69 | } 70 | 71 | } 72 | 73 | } 74 | 75 | // TODO: reDesign repositories item base of github 76 | @Composable 77 | fun RepositoryItem(item: Repository) { 78 | Column( 79 | modifier = Modifier 80 | .fillMaxSize() 81 | .padding(bottom = 16.dp, end = 16.dp, start = 16.dp) 82 | ) { 83 | Text( 84 | text = item.name ?: "", 85 | color = colorResource(id = R.color.purple_200), 86 | fontSize = 20.sp, 87 | fontWeight = FontWeight.Bold, 88 | modifier = Modifier 89 | .fillMaxWidth() 90 | .padding(bottom = 16.dp), 91 | 92 | ) 93 | item.description?.let { 94 | Text( 95 | text = it, 96 | color = Color.White, 97 | fontSize = 14.sp, 98 | modifier = Modifier 99 | .fillMaxWidth() 100 | .padding(bottom = 16.dp), 101 | ) 102 | } 103 | Row(modifier = Modifier.padding(bottom = 16.dp) , verticalAlignment = Alignment.CenterVertically) { 104 | item.language?.let { 105 | Box( 106 | modifier = Modifier 107 | .size(17.dp) 108 | .clip(CircleShape) 109 | .background(colorResource(id = getLanguageColor(it))) 110 | ) 111 | Text( 112 | text = it, 113 | color = Color.White, 114 | fontSize = 14.sp, 115 | modifier = Modifier 116 | .padding(start = 5.dp, end = 16.dp), 117 | 118 | ) 119 | } 120 | Image( 121 | painter = painterResource(id = R.drawable.ic_star), 122 | contentDescription = "star", 123 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 124 | modifier = Modifier 125 | .width(20.dp) 126 | .height(20.dp) 127 | ) 128 | Text( 129 | text = item.starsCount ?: "", 130 | color = Color.White, 131 | fontSize = 14.sp, 132 | modifier = Modifier 133 | .padding(start = 5.dp, end = 16.dp), 134 | 135 | ) 136 | 137 | Image( 138 | painter = painterResource(id = R.drawable.fork), 139 | contentDescription = "fork", 140 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 141 | modifier = Modifier 142 | .width(20.dp) 143 | .height(20.dp) 144 | ) 145 | Text( 146 | text = item.forkCounts ?: "", 147 | color = Color.White, 148 | fontSize = 14.sp, 149 | modifier = Modifier 150 | .padding(start = 5.dp, end = 16.dp), 151 | ) 152 | Image( 153 | painter = painterResource(id = R.drawable.ic_eye), 154 | contentDescription = "watch", 155 | colorFilter = ColorFilter.tint(colorResource(id = R.color.gray)), 156 | modifier = Modifier 157 | .width(20.dp) 158 | .height(20.dp) 159 | ) 160 | Text( 161 | text = item.watchersCount ?: "", 162 | color = Color.White, 163 | fontSize = 14.sp, 164 | modifier = Modifier 165 | .padding(start = 5.dp, end = 16.dp), 166 | 167 | ) 168 | } 169 | Divider(color = colorResource(id = R.color.gray), thickness = 0.5.dp) 170 | 171 | } 172 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/repository/ui/RepositoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.repository.ui 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import dn.marjan.githubapp.base.BaseViewModel 7 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 8 | import dn.marjan.githubapp.data.Error 9 | import dn.marjan.githubapp.data.ErrorResource 10 | import dn.marjan.githubapp.data.Resource 11 | import dn.marjan.githubapp.data.SuccessResource 12 | import dn.marjan.githubapp.entity.Repository 13 | import dn.marjan.githubapp.ui.repository.repo.RepoRepository 14 | import kotlinx.coroutines.launch 15 | import retrofit2.HttpException 16 | import javax.inject.Inject 17 | 18 | class RepositoryViewModel @Inject constructor( 19 | val repoRepository: RepoRepository, 20 | val dispatcherProvider: CoroutineDispatcherProvider 21 | ) : BaseViewModel() { 22 | 23 | private val _repoResponse = MutableLiveData>>() 24 | 25 | val repoResponse: LiveData>> 26 | get() = _repoResponse 27 | 28 | fun getRepositories(){ 29 | viewModelScope.launch(dispatcherProvider.IO()) { 30 | try{ 31 | val username = repoRepository.getUsername() 32 | val dd = repoRepository.getRepositories(username = username) 33 | _repoResponse.postValue(SuccessResource(data =dd )) 34 | }catch (e: HttpException){ 35 | _repoResponse.postValue(ErrorResource(error = Error.NetworkErrors , message = e.message())) 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/splash/repo/SplashRepository.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.splash.repo 2 | 3 | interface SplashRepository { 4 | fun checkUserLogin():Boolean 5 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/splash/repo/SplashRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.splash.repo 2 | 3 | import dn.marjan.githubapp.data.local.LocalDataService 4 | import javax.inject.Inject 5 | 6 | class SplashRepositoryImp @Inject constructor( 7 | val localDataService: LocalDataService 8 | ) : SplashRepository{ 9 | 10 | override fun checkUserLogin(): Boolean { 11 | return localDataService.isUserLogin() 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/splash/ui/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.splash.ui 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.Image 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.Surface 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import dn.marjan.githubapp.R 21 | import dn.marjan.githubapp.base.BaseActivity 22 | import dn.marjan.githubapp.data.Resource 23 | import dn.marjan.githubapp.data.Status 24 | import dn.marjan.githubapp.ui.dashboard.DashboardActivity 25 | import dn.marjan.githubapp.ui.login.ui.LoginActivity 26 | 27 | class SplashActivity : BaseActivity() { 28 | 29 | 30 | override fun getViewModel(): Class = SplashViewModel::class.java 31 | 32 | @Composable 33 | override fun ProvideCompose() { 34 | viewModel.checkUserLogin() 35 | ContentUI() 36 | 37 | viewModel.splashResponse.observe(this) { 38 | when (it.status) { 39 | Status.SUCCESS -> startActivity( 40 | Intent( 41 | this, 42 | DashboardActivity::class.java 43 | ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 44 | ) 45 | else -> startActivity( 46 | Intent( 47 | this, 48 | LoginActivity::class.java 49 | ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 50 | ) 51 | } 52 | 53 | } 54 | } 55 | 56 | @Preview 57 | @Composable 58 | fun ContentUI() { 59 | Column( 60 | verticalArrangement = Arrangement.Center, 61 | horizontalAlignment = Alignment.CenterHorizontally, 62 | modifier = Modifier.fillMaxSize() 63 | ) { 64 | Image( 65 | painter = painterResource(id = R.drawable.ic_github), 66 | contentDescription = "Github", 67 | modifier = Modifier.padding(all = 32.dp) 68 | ) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/splash/ui/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.splash.ui 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import dn.marjan.githubapp.base.BaseViewModel 7 | import dn.marjan.githubapp.base.CoroutineDispatcherProvider 8 | import dn.marjan.githubapp.data.Error 9 | import dn.marjan.githubapp.data.ErrorResource 10 | import dn.marjan.githubapp.data.Resource 11 | import dn.marjan.githubapp.data.SuccessResource 12 | import dn.marjan.githubapp.ui.splash.repo.SplashRepository 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | class SplashViewModel @Inject constructor( 17 | val repository: SplashRepository, 18 | val dispatcherProvider: CoroutineDispatcherProvider 19 | ) : BaseViewModel() { 20 | 21 | private var _splashResponse = MutableLiveData>() 22 | 23 | val splashResponse: LiveData> 24 | get() = _splashResponse 25 | 26 | fun checkUserLogin(){ 27 | 28 | viewModelScope.launch(dispatcherProvider.Main()) { 29 | val isUserLogin = repository.checkUserLogin() 30 | 31 | if(isUserLogin) _splashResponse.postValue(SuccessResource(data = true)) 32 | else _splashResponse.postValue(ErrorResource(error = Error.EmptyResultError)) 33 | } 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun GithubAppTheme(darkTheme: Boolean = isSystemInDarkTheme() , content: @Composable() () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = Typography, 41 | shapes = Shapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.ui.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/utils/Config.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.utils 2 | 3 | object Config { 4 | const val GITHUB_ACCESS_TOKEN ="XXXXXXXX" 5 | const val BASE_URL = "https://api.github.com/" 6 | } -------------------------------------------------------------------------------- /app/src/main/java/dn/marjan/githubapp/utils/Utility.kt: -------------------------------------------------------------------------------- 1 | package dn.marjan.githubapp.utils 2 | 3 | import android.util.Log 4 | import androidx.compose.material.Snackbar 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.res.colorResource 7 | import dn.marjan.githubapp.R 8 | import dn.marjan.githubapp.entity.ReceivedEvents 9 | 10 | fun log(msg: String){ 11 | Log.d("MARJANTAG", msg); 12 | } 13 | 14 | 15 | fun getAction(item: ReceivedEvents): String { 16 | if (item.payload!!.action.isNotEmpty()) { 17 | return item.payload!!.action 18 | } else { 19 | when (item.type) { 20 | "ForkEvent" -> return "forked" 21 | "CreateEvent" -> return "created a repository" 22 | "PublicEvent" -> return "made" 23 | } 24 | } 25 | return "" 26 | } 27 | 28 | fun getLanguageColor(lang: String): Int { 29 | return when(lang){ 30 | "Java" -> R.color.JavaColor 31 | "Kotlin" -> R.color.KotlinColor 32 | "Dart" -> R.color.DartColor 33 | else -> R.color.HtmlColor 34 | } 35 | } -------------------------------------------------------------------------------- /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/circle_shape.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/drawable/fork.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_account.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_eye.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/drawable/ic_github.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /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/drawable/ic_link.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_people.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_time.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /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/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marjandn/GitHub/4ea5a59c4d5e38ad7b2c0c1dd0ba4e19693b8186/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #101010 9 | #FFFFFFFF 10 | #8B949E 11 | #F44336 12 | #58A6FF 13 | 14 | #00B4AB 15 | #B07219 16 | #E34C26 17 | #AA7DFF 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GithubApp 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |