├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── plastix │ │ │ └── kotlinboilerplate │ │ │ ├── ApplicationComponent.kt │ │ │ ├── ApplicationModule.kt │ │ │ ├── ApplicationQualifier.kt │ │ │ ├── KotlinBoilerplateApp.kt │ │ │ ├── data │ │ │ ├── network │ │ │ │ ├── NetworkInteractor.kt │ │ │ │ ├── NetworkInteractorImpl.kt │ │ │ │ └── NetworkModule.kt │ │ │ └── remote │ │ │ │ ├── ApiConstants.kt │ │ │ │ ├── ApiModule.kt │ │ │ │ ├── GithubApiService.kt │ │ │ │ └── model │ │ │ │ ├── GithubApiModels.kt │ │ │ │ └── Parcels.kt │ │ │ ├── extensions │ │ │ ├── ActivityExtensions.kt │ │ │ └── ViewExtensions.kt │ │ │ └── ui │ │ │ ├── ActivityScope.kt │ │ │ ├── base │ │ │ ├── AbstractViewModel.kt │ │ │ ├── ActivityModule.kt │ │ │ ├── BaseActivity.kt │ │ │ ├── RxViewModel.kt │ │ │ ├── ViewModel.kt │ │ │ ├── ViewModelActivity.kt │ │ │ └── ViewModelLoader.kt │ │ │ ├── detail │ │ │ ├── DetailActivity.kt │ │ │ ├── DetailBindingAdapter.kt │ │ │ ├── DetailComponent.kt │ │ │ ├── DetailModule.kt │ │ │ └── DetailViewModel.kt │ │ │ ├── list │ │ │ ├── ListActivity.kt │ │ │ ├── ListComponent.kt │ │ │ ├── ListModule.kt │ │ │ ├── ListViewModel.kt │ │ │ ├── RepoAdapter.kt │ │ │ ├── RepoDiffCallback.kt │ │ │ └── RepoViewModel.kt │ │ │ └── misc │ │ │ └── SimpleDividerItemDecoration.kt │ └── res │ │ ├── drawable │ │ ├── ic_github_24dp_black.xml │ │ ├── ic_repo_12dp_black.xml │ │ ├── ic_repo_fork_10dp_black.xml │ │ ├── ic_star_24dp_black.xml │ │ └── line_divider.xml │ │ ├── layout │ │ ├── activity_detail.xml │ │ ├── activity_list.xml │ │ ├── empty_view.xml │ │ └── item_repo.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_web.png │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── unitTests │ └── kotlin │ └── io │ └── github │ └── plastix │ └── kotlinboilerplate │ ├── data │ └── network │ │ └── NetworkInteractorTest.kt │ └── ui │ ├── base │ └── RxViewModelTest.kt │ ├── detail │ └── DetailViewModelTest.kt │ └── list │ ├── ListViewModelTest.kt │ └── RepoViewModelTest.kt ├── art └── screenshots │ ├── detail.png │ ├── detail_night.png │ ├── list.png │ └── list_night.png ├── build.gradle ├── ci.sh ├── circle.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── signing.properties.sample /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android,java,macos 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/ 42 | .idea/workspace.xml 43 | .idea/libraries 44 | 45 | # Keystore files 46 | *.jks 47 | 48 | ### Android Patch ### 49 | gen-external-apklibs 50 | 51 | 52 | ### macOS ### 53 | *.DS_Store 54 | .AppleDouble 55 | .LSOverride 56 | 57 | # Icon must end with two \r 58 | Icon 59 | 60 | 61 | # Thumbnails 62 | ._* 63 | 64 | # Files that might appear in the root of a volume 65 | .DocumentRevisions-V100 66 | .fseventsd 67 | .Spotlight-V100 68 | .TemporaryItems 69 | .Trashes 70 | .VolumeIcon.icns 71 | .com.apple.timemachine.donotpresent 72 | 73 | # Directories potentially created on remote AFP share 74 | .AppleDB 75 | .AppleDesktop 76 | Network Trash Folder 77 | Temporary Items 78 | .apdisk 79 | 80 | 81 | ### Java ### 82 | *.class 83 | 84 | # Mobile Tools for Java (J2ME) 85 | .mtj.tmp/ 86 | 87 | # Package Files # 88 | *.jar 89 | *.war 90 | *.ear 91 | 92 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 93 | hs_err_pid* 94 | 95 | # Allow anything in the gradle wrapper folder 96 | !gradle/wrapper/* 97 | !gradle/wrapper/gradle-wrapper.jar 98 | !gradle/wrapper/gradle-wrapper.properties 99 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2016` `Plastix` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin-Android-Boilerplate [![CircleCI](https://img.shields.io/circleci/project/Plastix/Kotlin-Android-Boilerplate/master.svg)](https://circleci.com/gh/Plastix/Kotlin-Android-Boilerplate) 2 | 3 |

4 | 5 |

6 | 7 | An MVVM Boilerplate Android project written in [Kotlin](https://kotlinlang.org/). This sample 8 | application fetches the top starred Kotlin repositories from Github and displays them. Inspired by 9 | @hitherejoe's [Android-Boilerplate](https://github.com/hitherejoe/Android-Boilerplate) project. 10 | 11 | >**Note**: 12 | >This project was developed before Google's introduction of Android Architecture Components at I/O 2017. I strongly encourage that you read their [application architecture guide](https://developer.android.com/topic/libraries/architecture/guide.html). 13 | 14 | ## Screenshots 15 | 16 | 17 | 18 | 19 | 20 | 21 | ## Libraries 22 | * [Dagger 2](http://google.github.io/dagger/) 23 | * [RxJava 2](https://github.com/ReactiveX/RxJava) and [RxAndroid](https://github.com/ReactiveX/RxAndroid) 24 | * [Retrofit 2](http://square.github.io/retrofit/) 25 | * [Picasso](http://square.github.io/picasso/) 26 | * [Google Support Libraries](http://developer.android.com/tools/support-library/index.html) 27 | 28 | ## Testing Libraries 29 | * [JUnit](http://junit.org/junit4/) 30 | * [Mockito](http://mockito.org/) 31 | 32 | ## Requirements 33 | To compile and run the project you'll need: 34 | 35 | - [Android SDK](http://developer.android.com/sdk/index.html) 36 | - [Android N (API 25)](http://developer.android.com/tools/revisions/platforms.html) 37 | - Android SDK Tools 38 | - Android SDK Build Tools `24.0.3` 39 | - Android Support Repository 40 | - [Kotlin](https://kotlinlang.org/) `1.0.6` 41 | - Kotlin plugin for Android Studio 42 | 43 | Building 44 | -------- 45 | 46 | To build, install and run a debug version, run this from the root of the project: 47 | 48 | ``` 49 | ./gradlew assembleDebug 50 | ``` 51 | 52 | Testing 53 | ------- 54 | 55 | To run **unit** tests on your machine: 56 | 57 | ``` 58 | ./gradlew test 59 | ``` 60 | 61 | To run **instrumentation** tests on connected devices: 62 | 63 | ``` 64 | ./gradlew connectedAndroidTest 65 | ``` 66 | 67 | 68 | ## Release Builds 69 | A release build needs to be signed with an Android Keystore. The easiest way to generate a keystore is to open 70 | Android Studio and go to `Build -> Generate Signed Apk -> Create New...` After that you need to create a 71 | `signing.properties` file in the root directory and add the following info to it: 72 | ```INI 73 | STORE_FILE=/path/to/your.keystore 74 | STORE_PASSWORD=yourkeystorepass 75 | KEY_ALIAS=projectkeyalias 76 | KEY_PASSWORD=keyaliaspassword 77 | ``` 78 | Running `./gradlew assembleRelease` will then build and sign a release version of the app. 79 | 80 | ## FAQ 81 | #### Why Kotlin? 82 | In a nutshell, Kotlin throws all the bad parts of Java out the window and brings lots of great features from 83 | Java 8 and functional programming (Yet still compiling to Java 6 bytecode). Kotlin brings much needed language 84 | features to Android which is stuck on Java 6. 85 | 86 | #### What is with all the interfaces? 87 | 88 | By default Kotlin classes are closed (`final`). This makes them hard to mock unless you use a tool like 89 | [Powermock](https://github.com/jayway/powermock). I'd rather just mock interfaces with [Mockito](http://mockito.org/) 90 | than go through the hassle of using Powermock. 91 | 92 | **Update**: [Mockito 2.0](https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2) Supports mocking 93 | static and final out of the box. Thus, it removes the need for all of these interfaces. 94 | 95 | #### How do I use this project? 96 | This is a boilerplate project aimed to help bootstrap new Kotlin applications. Feel free to fork this application 97 | or use this project [generator](https://github.com/ravidsrk/generator-kotlin-android-boilerplate). Don't 98 | forget to change the following things for your application: 99 | 100 | * Application ID (Gradle) 101 | * Application Name (String resource) 102 | * Package names 103 | 104 | ## Attributions 105 | - [Kotlin Logo](http://instantlogosearch.com/kotlin) 106 | - [Github Icons](https://octicons.github.com/) 107 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' // Use experimental kapt implementation 5 | 6 | android { 7 | compileSdkVersion 25 8 | buildToolsVersion '25.0.2' 9 | 10 | defaultConfig { 11 | applicationId "io.github.plastix.kotlinboilerplate" 12 | 13 | minSdkVersion 16 14 | targetSdkVersion 25 15 | versionCode 1 16 | versionName "0.5" 17 | 18 | // Enable VectorDrawableCompat for API < 21 19 | vectorDrawables.useSupportLibrary = true 20 | } 21 | 22 | signingConfigs { 23 | release 24 | } 25 | 26 | buildTypes { 27 | debug { 28 | applicationIdSuffix '.debug' 29 | } 30 | release { 31 | applicationIdSuffix '.release' 32 | signingConfig signingConfigs.release 33 | 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | } 37 | } 38 | 39 | dataBinding { 40 | enabled = true 41 | } 42 | 43 | sourceSets { 44 | // Main source set is Kotlin! 45 | main.java.srcDirs += 'src/main/kotlin' 46 | 47 | // Unit tests live in src/unitTest. 48 | test.java.srcDir 'src/unitTests/kotlin' 49 | 50 | // Integration tests live in src/integrationTest. 51 | test.java.srcDir 'src/integrationTests/kotlin' 52 | 53 | // Unit tests for debug build type specific code live in src/debugUnitTest. 54 | testDebug.java.srcDir 'src/debugUnitTests/kotlin' 55 | 56 | // Unit tests for release build type specific code live in src/releaseUnitTest. 57 | testRelease.java.srcDir 'src/releaseUnitTests/kotlin' 58 | 59 | // Functional tests live in src/functionalTests. 60 | androidTest.java.srcDir 'src/functionalTests/kotlin' 61 | } 62 | 63 | lintOptions { 64 | warningsAsErrors false 65 | abortOnError true // Fail early. 66 | 67 | lintConfig file("lint.xml") 68 | } 69 | 70 | // Show all test output in the command line! 71 | testOptions.unitTests.all { 72 | testLogging { 73 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 74 | exceptionFormat 'full' 75 | } 76 | } 77 | } 78 | 79 | 80 | // Use for legacy kapt implementation 81 | // Required for annotation processing plugins like Dagger 82 | // kapt { 83 | // generateStubs = true 84 | //} 85 | 86 | dependencies { 87 | compile fileTree(dir: 'libs', include: ['*.jar']) 88 | 89 | // Kotlin 90 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 91 | 92 | // Google Support Libraries 93 | compile 'com.android.support:support-v4:25.3.1' 94 | compile 'com.android.support:support-fragment:25.3.1' 95 | compile 'com.android.support:appcompat-v7:25.3.1' 96 | compile 'com.android.support:design:25.3.1' 97 | compile 'com.android.support:recyclerview-v7:25.3.1' 98 | 99 | // RxJava 100 | compile 'io.reactivex.rxjava2:rxjava:2.0.0' 101 | compile 'io.reactivex.rxjava2:rxandroid:2.0.0' 102 | compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' 103 | compile 'com.github.Plastix.RxSchedulerRule:rx2:1.0.2' 104 | compile 'com.github.Plastix.RxDelay:rx2:0.5.0' 105 | 106 | // Dagger 2 107 | kapt 'com.google.dagger:dagger-compiler:2.8' 108 | compile 'com.google.dagger:dagger:2.8' 109 | 110 | // Retrofit 111 | compile 'com.squareup.retrofit2:retrofit:2.0.0' 112 | compile 'com.squareup.retrofit2:converter-gson:2.0.0' 113 | 114 | // Misc 115 | compile 'com.squareup.picasso:picasso:2.5.2' 116 | compile 'com.jakewharton.timber:timber:4.1.2' 117 | 118 | // Databinding 119 | kapt "com.android.databinding:compiler:$android_plugin_version" 120 | 121 | // Unit Testing 122 | testCompile 'junit:junit:4.12' 123 | testCompile "org.mockito:mockito-core:1.10.19" 124 | } 125 | 126 | repositories { 127 | mavenCentral() 128 | maven { url 'https://jitpack.io' } 129 | } 130 | 131 | // Signing Config code 132 | // From https://gist.github.com/gabrielemariotti/6856974 133 | def Properties props = new Properties() 134 | def propFile = new File('signing.properties') 135 | if (propFile.canRead()) { 136 | props.load(new FileInputStream(propFile)) 137 | 138 | if (props != null && props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') && 139 | props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) { 140 | android.signingConfigs.release.storeFile = file(props['STORE_FILE']) 141 | android.signingConfigs.release.storePassword = props['STORE_PASSWORD'] 142 | android.signingConfigs.release.keyAlias = props['KEY_ALIAS'] 143 | android.signingConfigs.release.keyPassword = props['KEY_PASSWORD'] 144 | } else { 145 | println 'signing.properties found but some entries are missing' 146 | android.buildTypes.release.signingConfig = null 147 | } 148 | } else { 149 | println 'signing.properties not found' 150 | android.buildTypes.release.signingConfig = null 151 | } 152 | 153 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/android-sdk-mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate 2 | 3 | import dagger.Component 4 | import io.github.plastix.kotlinboilerplate.data.network.NetworkModule 5 | import io.github.plastix.kotlinboilerplate.data.remote.ApiModule 6 | import io.github.plastix.kotlinboilerplate.ui.detail.DetailComponent 7 | import io.github.plastix.kotlinboilerplate.ui.detail.DetailModule 8 | import io.github.plastix.kotlinboilerplate.ui.list.ListComponent 9 | import io.github.plastix.kotlinboilerplate.ui.list.ListModule 10 | import javax.inject.Singleton 11 | 12 | @Singleton 13 | @Component(modules = arrayOf( 14 | ApplicationModule::class, 15 | NetworkModule::class, 16 | ApiModule::class 17 | )) 18 | interface ApplicationComponent { 19 | 20 | // Injectors 21 | fun injectTo(app: KotlinBoilerplateApp) 22 | 23 | // Submodule methods 24 | // Every screen is its own submodule of the graph and must be added here. 25 | fun plus(module: ListModule): ListComponent 26 | fun plus(module: DetailModule): DetailComponent 27 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import android.view.LayoutInflater 7 | import dagger.Module 8 | import dagger.Provides 9 | import timber.log.Timber 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | class ApplicationModule(private val app: KotlinBoilerplateApp) { 14 | 15 | @Provides @Singleton 16 | fun provideApplication(): Application = app 17 | 18 | @Provides @Singleton @ApplicationQualifier 19 | fun provideContext(): Context = app.baseContext 20 | 21 | @Provides @Singleton 22 | fun provideResources(): Resources = app.resources 23 | 24 | @Provides @Singleton 25 | fun provideLayoutInflater(@ApplicationQualifier context: Context): LayoutInflater { 26 | return LayoutInflater.from(context) 27 | } 28 | 29 | @Provides 30 | fun provideDebugTree(): Timber.DebugTree = Timber.DebugTree() 31 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ApplicationQualifier.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | annotation class ApplicationQualifier 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/KotlinBoilerplateApp.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate 2 | 3 | import android.app.Application 4 | import dagger.Lazy 5 | import timber.log.Timber 6 | import javax.inject.Inject 7 | 8 | class KotlinBoilerplateApp : Application() { 9 | 10 | @Inject 11 | lateinit var debugTree: Lazy 12 | 13 | companion object { 14 | lateinit var graph: ApplicationComponent 15 | } 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | 20 | initDependencyGraph() 21 | 22 | if (BuildConfig.DEBUG) { 23 | Timber.plant(debugTree.get()) 24 | } 25 | } 26 | 27 | private fun initDependencyGraph() { 28 | graph = DaggerApplicationComponent.builder() 29 | .applicationModule(ApplicationModule(this)) 30 | .build() 31 | graph.injectTo(this) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/network/NetworkInteractor.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.network 2 | 3 | import io.reactivex.Completable 4 | 5 | 6 | interface NetworkInteractor { 7 | 8 | fun hasNetworkConnection(): Boolean 9 | 10 | fun hasNetworkConnectionCompletable(): Completable 11 | 12 | class NetworkUnavailableException : Throwable("No network available!") 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/network/NetworkInteractorImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.network 2 | 3 | import android.net.ConnectivityManager 4 | import io.reactivex.Completable 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | class NetworkInteractorImpl @Inject constructor( 10 | private val connectivityManager: ConnectivityManager 11 | ) : NetworkInteractor { 12 | 13 | override fun hasNetworkConnection(): Boolean = 14 | connectivityManager.activeNetworkInfo?.isConnectedOrConnecting ?: false 15 | 16 | override fun hasNetworkConnectionCompletable(): Completable = 17 | if (hasNetworkConnection()) { 18 | Completable.complete() 19 | } else { 20 | Completable.error { NetworkInteractor.NetworkUnavailableException() } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/network/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.network 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import dagger.Module 6 | import dagger.Provides 7 | import io.github.plastix.kotlinboilerplate.ApplicationQualifier 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | class NetworkModule { 12 | 13 | @Provides @Singleton 14 | fun provideConnectivityManager(@ApplicationQualifier context: Context): ConnectivityManager = 15 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 16 | 17 | @Provides @Singleton 18 | fun provideNetworkInteractor(networkInteractorImpl: NetworkInteractorImpl): NetworkInteractor = networkInteractorImpl 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/remote/ApiConstants.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.remote 2 | 3 | object ApiConstants { 4 | const val GITHUB_API_BASE_ENDPOINT = "https://api.github.com" 5 | 6 | const val SEARCH_QUERY_KOTLIN = "language:kotlin" 7 | const val SEARCH_SORT_STARS = "stars" 8 | const val SEARCH_ORDER_DESCENDING = "desc" 9 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/remote/ApiModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.remote 2 | 3 | import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 4 | import dagger.Module 5 | import dagger.Provides 6 | import retrofit2.Retrofit 7 | import retrofit2.converter.gson.GsonConverterFactory 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | class ApiModule { 12 | 13 | @Provides @Singleton 14 | fun provideApiService(retrofit: Retrofit): GithubApiService { 15 | return retrofit.create(GithubApiService::class.java) 16 | } 17 | 18 | @Provides @Singleton 19 | fun provideRetrofit( 20 | rxJavaCallAdapterFactory: RxJava2CallAdapterFactory, 21 | gsonConverterFactory: GsonConverterFactory 22 | ): Retrofit { 23 | return Retrofit.Builder() 24 | .baseUrl(ApiConstants.GITHUB_API_BASE_ENDPOINT) 25 | .addCallAdapterFactory(rxJavaCallAdapterFactory) 26 | .addConverterFactory(gsonConverterFactory) 27 | .build() 28 | } 29 | 30 | @Provides @Singleton 31 | fun provideGsonConverterFactory(): GsonConverterFactory { 32 | return GsonConverterFactory.create() 33 | } 34 | 35 | @Provides @Singleton 36 | fun provideRxJavaCallAdapter(): RxJava2CallAdapterFactory { 37 | return RxJava2CallAdapterFactory.create() 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/remote/GithubApiService.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.remote 2 | 3 | import io.github.plastix.kotlinboilerplate.data.remote.model.SearchResponse 4 | import io.reactivex.Single 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface GithubApiService { 9 | 10 | @GET("/search/repositories") 11 | fun repoSearch( 12 | @Query("q") query: String, 13 | @Query("sort") sort: String, 14 | @Query("order") order: String 15 | ): Single 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/remote/model/GithubApiModels.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.remote.model 2 | 3 | import android.os.Parcel 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class SearchResponse( 7 | @SerializedName("total_count") 8 | val count: Int, 9 | @SerializedName("items") 10 | val repos: List 11 | ) 12 | 13 | data class Repo( 14 | val name: String, 15 | @SerializedName("full_name") 16 | val fullName: String, 17 | val owner: Owner, 18 | val description: String, 19 | @SerializedName("stargazers_count") 20 | val stars: Int, 21 | val forks: Int 22 | ) : DefaultParcelable { 23 | 24 | override fun writeToParcel(dest: Parcel, flags: Int) { 25 | dest.write(name, fullName, owner, description, stars, forks) 26 | } 27 | 28 | companion object { 29 | @JvmField val CREATOR = DefaultParcelable.generateCreator { 30 | Repo(it.read(), it.read(), it.read(), it.read(), it.read(), it.read()) 31 | } 32 | } 33 | } 34 | 35 | data class Owner( 36 | @SerializedName("login") 37 | val name: String, 38 | @SerializedName("avatar_url") 39 | val avatarUrl: String 40 | ) : DefaultParcelable { 41 | 42 | override fun writeToParcel(dest: Parcel, flags: Int) { 43 | dest.write(name, avatarUrl) 44 | } 45 | 46 | companion object { 47 | @JvmField val CREATOR = DefaultParcelable.generateCreator { 48 | Owner(it.read(), it.read()) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/data/remote/model/Parcels.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.data.remote.model 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | // Reduce boilerplate in creating parcelables 7 | // From http://stackoverflow.com/questions/33551972/is-there-a-convenient-way-to-create-parcelable-data-classes-in-android-with-kotl/35700144#35700144 8 | // Opting for this since annotation processing libraries (like PaperParcel) can have issues on Kotlin 9 | // when using Dagger 2 10 | // See https://github.com/gen0083/KotlinDaggerDataBinding 11 | interface DefaultParcelable : Parcelable { 12 | override fun describeContents(): Int = 0 13 | 14 | companion object { 15 | fun generateCreator(create: (source: Parcel) -> T): Parcelable.Creator = object: Parcelable.Creator { 16 | override fun createFromParcel(source: Parcel): T = create(source) 17 | 18 | override fun newArray(size: Int): Array? = newArray(size) 19 | } 20 | 21 | } 22 | } 23 | inline fun Parcel.read(): T = readValue(T::class.javaClass.classLoader) as T 24 | fun Parcel.write(vararg values: Any?) = values.forEach { writeValue(it) } 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/extensions/ActivityExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.extensions 2 | 3 | import android.support.v7.app.AppCompatActivity 4 | 5 | /** 6 | * Remember to set the android:parentActivityName attribute on the activity you are calling this 7 | * from! 8 | */ 9 | fun AppCompatActivity.enableToolbarBackButton() { 10 | delegate.supportActionBar?.setDisplayHomeAsUpEnabled(true) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/extensions/ViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.extensions 2 | 3 | import android.content.Context 4 | import android.support.annotation.ColorInt 5 | import android.support.annotation.StringRes 6 | import android.support.design.widget.Snackbar 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.ImageView 11 | import com.squareup.picasso.Picasso 12 | 13 | var View.isVisible: Boolean 14 | get() = visibility == View.VISIBLE 15 | set(value) { 16 | visibility = if(value) View.VISIBLE else View.GONE 17 | } 18 | 19 | fun Context.inflateLayout(layoutResId: Int): View { 20 | return inflateView(this, layoutResId, null, false) 21 | } 22 | 23 | fun Context.inflateLayout(layoutResId: Int, parent: ViewGroup): View { 24 | return inflateLayout(layoutResId, parent, true) 25 | } 26 | 27 | fun Context.inflateLayout(layoutResId: Int, parent: ViewGroup, attachToRoot: Boolean): View { 28 | return inflateView(this, layoutResId, parent, attachToRoot) 29 | } 30 | 31 | private fun inflateView(context: Context, layoutResId: Int, parent: ViewGroup?, attachToRoot: Boolean): View { 32 | return LayoutInflater.from(context).inflate(layoutResId, parent, attachToRoot) 33 | } 34 | 35 | fun ImageView.loadImage(url: String) { 36 | Picasso.with(context).load(url).into(this) 37 | } 38 | 39 | fun View.showSnackbar(message: String, length: Int = Snackbar.LENGTH_LONG, f: (Snackbar.() -> Unit) = {}) { 40 | val snack = Snackbar.make(this, message, length) 41 | snack.f() 42 | snack.show() 43 | } 44 | 45 | fun View.showSnackbar(@StringRes message: Int, length: Int = Snackbar.LENGTH_LONG, f: (Snackbar.() -> Unit) = {}) { 46 | showSnackbar(resources.getString(message), length, f) 47 | } 48 | 49 | fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) { 50 | setAction(action, listener) 51 | color?.let { setActionTextColor(color) } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | annotation class ActivityScope -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/AbstractViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | import android.databinding.BaseObservable 4 | 5 | abstract class AbstractViewModel : BaseObservable(), ViewModel { 6 | 7 | override fun bind() { 8 | } 9 | 10 | override fun unbind() { 11 | } 12 | 13 | override fun onDestroy() { 14 | // Hook for subclasses to clean up used resources 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | import android.content.Context 4 | import android.support.v7.app.AppCompatActivity 5 | import dagger.Module 6 | import dagger.Provides 7 | 8 | @Module 9 | abstract class ActivityModule(private val activity: AppCompatActivity) { 10 | 11 | @Provides 12 | fun provideActivity(): AppCompatActivity = activity 13 | 14 | @Provides 15 | fun provideActivityContext(): Context = activity.baseContext 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | import android.os.Bundle 4 | import android.support.annotation.CallSuper 5 | import android.support.v7.app.AppCompatActivity 6 | import io.github.plastix.kotlinboilerplate.ApplicationComponent 7 | import io.github.plastix.kotlinboilerplate.KotlinBoilerplateApp 8 | 9 | abstract class BaseActivity: AppCompatActivity() { 10 | 11 | @CallSuper 12 | override fun onCreate(savedInstanceState: Bundle?){ 13 | super.onCreate(savedInstanceState) 14 | injectDependencies(KotlinBoilerplateApp.graph) 15 | } 16 | 17 | abstract fun injectDependencies(graph: ApplicationComponent) 18 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/RxViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | import android.support.annotation.CallSuper 4 | import io.reactivex.Observable 5 | import io.reactivex.disposables.CompositeDisposable 6 | import io.reactivex.disposables.Disposable 7 | import io.reactivex.subjects.BehaviorSubject 8 | 9 | abstract class RxViewModel : AbstractViewModel() { 10 | 11 | protected val disposables: CompositeDisposable = CompositeDisposable() 12 | private val viewState: BehaviorSubject = BehaviorSubject.createDefault(false) 13 | 14 | @CallSuper 15 | override fun bind() { 16 | super.bind() 17 | viewState.onNext(true) 18 | } 19 | 20 | @CallSuper 21 | override fun unbind() { 22 | super.unbind() 23 | viewState.onNext(false) 24 | } 25 | 26 | @CallSuper 27 | override fun onDestroy() { 28 | super.onDestroy() 29 | viewState.onComplete() 30 | clearSubscriptions() 31 | } 32 | 33 | private fun clearSubscriptions() { 34 | disposables.clear() 35 | } 36 | 37 | fun addDisposable(disposable: Disposable) { 38 | disposables.add(disposable) 39 | } 40 | 41 | /** 42 | * Returns an Observable which omits the current state of the view. This observable emits 43 | * true when the view is attached and false when it is detached. 44 | */ 45 | fun getViewState(): Observable = viewState.hide() 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | interface ViewModel { 4 | 5 | fun bind() 6 | 7 | fun unbind() 8 | 9 | fun onDestroy() 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/ViewModelActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | import android.databinding.ViewDataBinding 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.support.annotation.CallSuper 7 | import android.support.v4.app.LoaderManager 8 | import android.support.v4.content.Loader 9 | import javax.inject.Inject 10 | import javax.inject.Provider 11 | 12 | abstract class ViewModelActivity 13 | : BaseActivity(), 14 | LoaderManager.LoaderCallbacks { 15 | 16 | private val LOADER_ID = 1 17 | 18 | protected lateinit var viewModel: T 19 | protected lateinit var binding: B 20 | 21 | @Inject 22 | protected lateinit var viewModelLoaderProvider: Provider> 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | binding = getViewBinding() 27 | 28 | initLoader() 29 | } 30 | 31 | abstract fun getViewBinding(): B 32 | 33 | @CallSuper 34 | open protected fun onViewModelProvided(viewModel: T) { 35 | this.viewModel = viewModel 36 | } 37 | 38 | @CallSuper 39 | open protected fun onViewModelReset() { 40 | // Hook for subclasses 41 | } 42 | 43 | @CallSuper 44 | override fun onStart() { 45 | super.onStart() 46 | onBind() 47 | } 48 | 49 | @CallSuper 50 | open protected fun onBind() { 51 | viewModel.bind() 52 | } 53 | 54 | // On Nougat and above onStop is no longer lazy!! 55 | // This makes sure to unbind our viewModel properly 56 | // See https://www.bignerdranch.com/blog/android-activity-lifecycle-onStop/ 57 | @CallSuper 58 | override fun onStop() { 59 | super.onStop() 60 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { 61 | onUnbind() 62 | } 63 | } 64 | 65 | @CallSuper 66 | open protected fun onUnbind() { 67 | viewModel.unbind() 68 | } 69 | 70 | override fun onPause() { 71 | super.onPause() 72 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { 73 | onUnbind() 74 | } 75 | } 76 | 77 | private fun initLoader() { 78 | supportLoaderManager.initLoader(LOADER_ID, null, this) 79 | } 80 | 81 | override fun onLoadFinished(loader: Loader?, viewModel: T) { 82 | onViewModelProvided(viewModel) 83 | } 84 | 85 | override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { 86 | return viewModelLoaderProvider.get() 87 | } 88 | 89 | override fun onLoaderReset(loader: Loader?) { 90 | onViewModelReset() 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/base/ViewModelLoader.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.base 2 | 3 | import android.content.Context 4 | import android.support.v4.content.Loader 5 | import io.github.plastix.kotlinboilerplate.ui.ActivityScope 6 | import javax.inject.Inject 7 | import javax.inject.Provider 8 | 9 | class ViewModelLoader @Inject constructor( 10 | @ActivityScope context: Context, 11 | private val viewModelFactory: Provider 12 | ) : Loader(context) { 13 | 14 | private var viewModel: T? = null 15 | 16 | override fun onStartLoading() { 17 | super.onStartLoading() 18 | 19 | if (viewModel == null) 20 | forceLoad() 21 | else 22 | deliverResult(viewModel) 23 | } 24 | 25 | override fun onForceLoad() { 26 | super.onForceLoad() 27 | viewModel = viewModelFactory.get() 28 | 29 | deliverResult(viewModel) 30 | } 31 | 32 | override fun onReset() { 33 | super.onReset() 34 | 35 | viewModel?.onDestroy() 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/detail/DetailActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.detail 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.databinding.DataBindingUtil 6 | import android.os.Bundle 7 | import io.github.plastix.kotlinboilerplate.ApplicationComponent 8 | import io.github.plastix.kotlinboilerplate.R 9 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 10 | import io.github.plastix.kotlinboilerplate.databinding.ActivityDetailBinding 11 | import io.github.plastix.kotlinboilerplate.extensions.enableToolbarBackButton 12 | import io.github.plastix.kotlinboilerplate.ui.base.ViewModelActivity 13 | 14 | open class DetailActivity : ViewModelActivity() { 15 | 16 | companion object { 17 | val EXTRA_REPO_OBJECT = "REPO_ITEM" 18 | 19 | fun newIntent(context: Context, repo: Repo): Intent { 20 | val intent = Intent(context, DetailActivity::class.java) 21 | intent.putExtra(EXTRA_REPO_OBJECT, repo) 22 | return intent 23 | } 24 | } 25 | 26 | private val repo by lazy { intent.getParcelableExtra(EXTRA_REPO_OBJECT) } 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setSupportActionBar(binding.detailToolbar) 31 | enableToolbarBackButton() 32 | } 33 | 34 | override fun onBind() { 35 | super.onBind() 36 | binding.viewModel = viewModel 37 | } 38 | 39 | override fun getViewBinding(): ActivityDetailBinding { 40 | return DataBindingUtil.setContentView(this, R.layout.activity_detail) 41 | } 42 | 43 | override fun injectDependencies(graph: ApplicationComponent) { 44 | graph.plus(DetailModule(this, repo)) 45 | .injectTo(this) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/detail/DetailBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.detail 2 | 3 | import android.databinding.BindingAdapter 4 | import android.widget.ImageView 5 | import io.github.plastix.kotlinboilerplate.extensions.loadImage 6 | 7 | @BindingAdapter("android:src") 8 | fun setImageBinding(view: ImageView, url: String){ 9 | view.loadImage(url) 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/detail/DetailComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.detail 2 | 3 | import dagger.Subcomponent 4 | import io.github.plastix.kotlinboilerplate.ui.ActivityScope 5 | 6 | @ActivityScope 7 | @Subcomponent(modules = arrayOf( 8 | DetailModule::class 9 | )) 10 | interface DetailComponent { 11 | fun injectTo(activity: DetailActivity) 12 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/detail/DetailModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.detail 2 | 3 | import android.support.v7.app.AppCompatActivity 4 | import dagger.Module 5 | import dagger.Provides 6 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 7 | import io.github.plastix.kotlinboilerplate.ui.base.ActivityModule 8 | 9 | @Module 10 | class DetailModule(activity: AppCompatActivity, val repo: Repo) : ActivityModule(activity) { 11 | 12 | @Provides 13 | fun provideRepo(): Repo = repo 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/detail/DetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.detail 2 | 3 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 4 | import io.github.plastix.kotlinboilerplate.ui.base.AbstractViewModel 5 | import javax.inject.Inject 6 | 7 | class DetailViewModel @Inject constructor(val repo: Repo) : AbstractViewModel() { 8 | 9 | fun getName() = repo.fullName 10 | 11 | fun getDescription() = repo.description 12 | 13 | fun getStars() = repo.stars.toString() 14 | 15 | fun getForks() = repo.forks.toString() 16 | 17 | fun getAvatarURL() = repo.owner.avatarUrl 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/ListActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatDelegate 6 | import android.support.v7.widget.LinearLayoutManager 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import io.github.plastix.kotlinboilerplate.ApplicationComponent 10 | import io.github.plastix.kotlinboilerplate.R 11 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 12 | import io.github.plastix.kotlinboilerplate.databinding.ActivityListBinding 13 | import io.github.plastix.kotlinboilerplate.extensions.isVisible 14 | import io.github.plastix.kotlinboilerplate.extensions.showSnackbar 15 | import io.github.plastix.kotlinboilerplate.ui.base.ViewModelActivity 16 | import io.github.plastix.kotlinboilerplate.ui.detail.DetailActivity 17 | import io.github.plastix.kotlinboilerplate.ui.misc.SimpleDividerItemDecoration 18 | import io.reactivex.disposables.CompositeDisposable 19 | import timber.log.Timber 20 | import javax.inject.Inject 21 | 22 | class ListActivity : ViewModelActivity() { 23 | 24 | @Inject 25 | lateinit var adapter: RepoAdapter 26 | 27 | @Inject 28 | lateinit var layoutManager: LinearLayoutManager 29 | 30 | @Inject 31 | lateinit var dividerDecorator: SimpleDividerItemDecoration 32 | 33 | val disposables: CompositeDisposable = CompositeDisposable() 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setSupportActionBar(binding.listToolbar) 38 | } 39 | 40 | override fun onBind() { 41 | super.onBind() 42 | binding.viewModel = viewModel 43 | setupRecyclerView() 44 | setupSwipeRefresh() 45 | updateEmptyView() 46 | 47 | disposables.add(viewModel.getRepos().subscribe { 48 | updateList(it) 49 | }) 50 | 51 | disposables.add(viewModel.loadingState().subscribe { 52 | binding.listSwipeRefresh.isRefreshing = it 53 | }) 54 | 55 | disposables.add(viewModel.fetchErrors().subscribe { 56 | errorFetchRepos() 57 | }) 58 | 59 | disposables.add(viewModel.networkErrors().subscribe { 60 | errorNoNetwork() 61 | }) 62 | } 63 | 64 | override fun getViewBinding(): ActivityListBinding { 65 | return DataBindingUtil.setContentView(this, R.layout.activity_list) 66 | } 67 | 68 | private fun setupSwipeRefresh() { 69 | binding.listSwipeRefresh.setOnRefreshListener { 70 | viewModel.fetchRepos() 71 | } 72 | } 73 | 74 | private fun setupRecyclerView() { 75 | binding.listRecyclerView.adapter = adapter 76 | binding.listRecyclerView.layoutManager = layoutManager 77 | binding.listRecyclerView.addItemDecoration(dividerDecorator) 78 | 79 | adapter.setClickListener { 80 | onItemClick(it) 81 | } 82 | } 83 | 84 | override fun injectDependencies(graph: ApplicationComponent) { 85 | graph.plus(ListModule(this)) 86 | .injectTo(this) 87 | } 88 | 89 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 90 | // Inflate the menu; this adds items to the action bar if it is present. 91 | menuInflater.inflate(R.menu.menu_main, menu) 92 | return true 93 | } 94 | 95 | private fun onItemClick(repo: Repo) { 96 | startActivity(DetailActivity.newIntent(this, repo)) 97 | } 98 | 99 | private fun updateList(repos: List) { 100 | adapter.updateRepos(repos) 101 | updateEmptyView() 102 | } 103 | 104 | private fun updateEmptyView() { 105 | val thereIsNoItems = adapter.itemCount == 0 106 | binding.emptyView.root.isVisible = thereIsNoItems 107 | } 108 | 109 | private fun errorNoNetwork() { 110 | binding.listCoordinatorLayout.showSnackbar(R.string.list_error_no_network) 111 | } 112 | 113 | private fun errorFetchRepos() { 114 | binding.listCoordinatorLayout.showSnackbar(R.string.list_error_failed_fetch) 115 | } 116 | 117 | override fun onPause() { 118 | super.onPause() 119 | disposables.clear() 120 | } 121 | 122 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 123 | // Handle action bar item clicks here. The action bar will 124 | // automatically handle clicks on the Home/Up button, so long 125 | // as you specify a parent activity in AndroidManifest.xml. 126 | return when (item.itemId) { 127 | R.id.action_settings -> { 128 | Timber.d("Settings menu clicked!") 129 | true 130 | } 131 | 132 | R.id.action_night -> { 133 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 134 | recreate() 135 | true 136 | } 137 | 138 | R.id.action_day -> { 139 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 140 | recreate() 141 | true 142 | } 143 | 144 | else -> super.onOptionsItemSelected(item) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/ListComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import dagger.Subcomponent 4 | import io.github.plastix.kotlinboilerplate.ui.ActivityScope 5 | 6 | @ActivityScope 7 | @Subcomponent(modules = arrayOf( 8 | ListModule::class 9 | )) 10 | interface ListComponent { 11 | 12 | fun injectTo(activity: ListActivity) 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/ListModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import android.content.Context 4 | import android.support.v7.app.AppCompatActivity 5 | import android.support.v7.widget.LinearLayoutManager 6 | import dagger.Module 7 | import dagger.Provides 8 | import io.github.plastix.kotlinboilerplate.ui.base.ActivityModule 9 | 10 | @Module 11 | class ListModule(activity: AppCompatActivity) : ActivityModule(activity) { 12 | 13 | @Provides 14 | fun provideLinearLayoutManager(context: Context): LinearLayoutManager = LinearLayoutManager(context) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/ListViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import io.github.plastix.kotlinboilerplate.data.network.NetworkInteractor 4 | import io.github.plastix.kotlinboilerplate.data.remote.ApiConstants 5 | import io.github.plastix.kotlinboilerplate.data.remote.GithubApiService 6 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 7 | import io.github.plastix.kotlinboilerplate.data.remote.model.SearchResponse 8 | import io.github.plastix.kotlinboilerplate.ui.base.RxViewModel 9 | import io.github.plastix.rxdelay.RxDelay 10 | import io.reactivex.Observable 11 | import io.reactivex.android.schedulers.AndroidSchedulers 12 | import io.reactivex.disposables.Disposable 13 | import io.reactivex.disposables.Disposables 14 | import io.reactivex.observers.DisposableSingleObserver 15 | import io.reactivex.schedulers.Schedulers 16 | import io.reactivex.subjects.BehaviorSubject 17 | import io.reactivex.subjects.PublishSubject 18 | import javax.inject.Inject 19 | 20 | class ListViewModel @Inject constructor( 21 | private val apiService: GithubApiService, 22 | private val networkInteractor: NetworkInteractor 23 | ) : RxViewModel() { 24 | 25 | private var networkRequest: Disposable = Disposables.disposed() 26 | 27 | private var repos: BehaviorSubject> = BehaviorSubject.createDefault(emptyList()) 28 | private var loadingState: BehaviorSubject = BehaviorSubject.createDefault(false) 29 | private val fetchErrors: PublishSubject = PublishSubject.create() 30 | private val networkErrors: PublishSubject = PublishSubject.create() 31 | 32 | fun fetchRepos() { 33 | networkRequest = networkInteractor.hasNetworkConnectionCompletable() 34 | .andThen(apiService.repoSearch(ApiConstants.SEARCH_QUERY_KOTLIN, 35 | ApiConstants.SEARCH_SORT_STARS, 36 | ApiConstants.SEARCH_ORDER_DESCENDING)) 37 | .subscribeOn(Schedulers.io()) 38 | .compose(RxDelay.delaySingle(getViewState())) 39 | .observeOn(AndroidSchedulers.mainThread()) 40 | .doOnSubscribe { 41 | networkRequest.dispose() // Cancel any current running request 42 | loadingState.onNext(true) 43 | } 44 | .doOnEvent { searchResponse, throwable -> 45 | loadingState.onNext(false) 46 | } 47 | .subscribeWith(object : DisposableSingleObserver() { 48 | override fun onError(e: Throwable) { 49 | System.out.println(e.toString()) 50 | when (e) { 51 | is NetworkInteractor.NetworkUnavailableException -> networkErrors.onNext(e) 52 | else -> fetchErrors.onNext(e) 53 | } 54 | } 55 | 56 | override fun onSuccess(value: SearchResponse) { 57 | repos.onNext(value.repos) 58 | } 59 | }) 60 | 61 | addDisposable(networkRequest) 62 | } 63 | 64 | fun getRepos(): Observable> = repos.hide() 65 | 66 | fun fetchErrors(): Observable = fetchErrors.hide() 67 | 68 | fun networkErrors(): Observable = networkErrors.hide() 69 | 70 | fun loadingState(): Observable = loadingState.hide() 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/RepoAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.support.v7.util.DiffUtil 5 | import android.support.v7.widget.RecyclerView 6 | import android.view.LayoutInflater 7 | import android.view.ViewGroup 8 | import io.github.plastix.kotlinboilerplate.R 9 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 10 | import io.github.plastix.kotlinboilerplate.databinding.ItemRepoBinding 11 | import io.github.plastix.kotlinboilerplate.ui.ActivityScope 12 | import javax.inject.Inject 13 | 14 | @ActivityScope 15 | class RepoAdapter @Inject constructor() : RecyclerView.Adapter() { 16 | 17 | private var repos: List = emptyList() 18 | private var itemClick: ((Repo) -> Unit)? = null 19 | 20 | override fun onBindViewHolder(holder: RepoViewHolder, position: Int) { 21 | val binding = holder.binding 22 | val repo = repos[position] 23 | var viewModel = binding.viewModel 24 | 25 | // Unbind old iewModel if we have one 26 | viewModel?.unbind() 27 | 28 | // Create new ViewModel, set it, and bind it 29 | viewModel = RepoViewModel(repo) 30 | binding.viewModel = viewModel 31 | viewModel.bind() 32 | 33 | holder.setClickListener(itemClick) 34 | } 35 | 36 | override fun getItemCount(): Int = repos.size 37 | 38 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder { 39 | val binding = DataBindingUtil.inflate( 40 | LayoutInflater.from(parent.context), 41 | R.layout.item_repo, 42 | parent, 43 | false 44 | ) 45 | 46 | return RepoViewHolder(binding) 47 | } 48 | 49 | fun updateRepos(repos: List) { 50 | val diff = RepoDiffCallback(this.repos, repos) 51 | val result = DiffUtil.calculateDiff(diff) 52 | 53 | this.repos = repos 54 | result.dispatchUpdatesTo(this) 55 | } 56 | 57 | fun setClickListener(itemClick: ((Repo) -> Unit)?) { 58 | this.itemClick = itemClick 59 | } 60 | 61 | class RepoViewHolder(val binding: ItemRepoBinding) : RecyclerView.ViewHolder(binding.root) { 62 | 63 | fun setClickListener(callback: ((Repo) -> Unit)?){ 64 | binding.viewModel.clicks().subscribe { 65 | callback?.invoke(binding.viewModel.repo) 66 | } 67 | } 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/RepoDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import android.support.v7.util.DiffUtil 4 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 5 | 6 | class RepoDiffCallback(private val old: List, private val new: List) : DiffUtil.Callback() { 7 | 8 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 9 | return old[oldItemPosition].fullName == new[newItemPosition].fullName 10 | } 11 | 12 | override fun getOldListSize(): Int { 13 | return old.size 14 | } 15 | 16 | override fun getNewListSize(): Int { 17 | return new.size 18 | } 19 | 20 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 21 | return old[oldItemPosition] == new[newItemPosition] 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/list/RepoViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.list 2 | 3 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo 4 | import io.github.plastix.kotlinboilerplate.ui.base.AbstractViewModel 5 | import io.reactivex.Observable 6 | import io.reactivex.subjects.PublishSubject 7 | 8 | class RepoViewModel(val repo: Repo) : AbstractViewModel() { 9 | 10 | private val clicks = PublishSubject.create() 11 | 12 | fun getName() = repo.fullName 13 | 14 | fun getDescription() = repo.description 15 | 16 | fun onClick() { 17 | clicks.onNext(Unit) 18 | } 19 | 20 | fun clicks(): Observable = clicks.hide() 21 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/plastix/kotlinboilerplate/ui/misc/SimpleDividerItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package io.github.plastix.kotlinboilerplate.ui.misc 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.drawable.Drawable 6 | import android.support.v4.content.ContextCompat 7 | import android.support.v7.widget.RecyclerView 8 | import io.github.plastix.kotlinboilerplate.ApplicationQualifier 9 | import io.github.plastix.kotlinboilerplate.R 10 | import javax.inject.Inject 11 | 12 | /** 13 | * Simple divider decorator for a RecyclerView. 14 | * 15 | * Adapted from https://gist.github.com/polbins/e37206fbc444207c0e92 16 | */ 17 | class SimpleDividerItemDecoration @Inject constructor( 18 | @ApplicationQualifier context: Context 19 | ) : RecyclerView.ItemDecoration() { 20 | 21 | private val divider: Drawable = ContextCompat.getDrawable(context, R.drawable.line_divider) 22 | 23 | override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 24 | val left = parent.paddingLeft 25 | val right = parent.width - parent.paddingRight 26 | 27 | val childCount = parent.childCount 28 | for (i in 0..childCount - 1) { 29 | val child = parent.getChildAt(i) 30 | 31 | val params = child.layoutParams as RecyclerView.LayoutParams 32 | 33 | val top = child.bottom + params.bottomMargin 34 | val bottom = top + divider.intrinsicHeight 35 | 36 | divider.setBounds(left, top, right, bottom) 37 | divider.draw(c) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github_24dp_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repo_12dp_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repo_fork_10dp_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_24dp_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/line_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 18 | 19 | 23 | 24 | 30 | 31 | 32 | 33 | 42 | 43 | 51 | 52 | 61 | 62 | 70 | 71 | 77 | 78 | 84 | 85 | 96 | 97 | 105 | 106 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 19 | 20 | 24 | 25 | 31 | 32 | 33 | 34 | 39 | 40 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/empty_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 23 | 24 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_repo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 20 | 21 | 29 | 30 | 45 | 46 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | 14 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/app/src/main/res/mipmap-xxxhdpi/ic_web.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #607D8B 3 | #455A64 4 | #F57F17 5 | 6 | #757575 7 | #424242 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #03A9F4 4 | #0288D1 5 | #FF9800 6 | 7 | #9E9E9E 8 | #e0e0e0 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | KotlinBoilerplate 3 | Settings 4 | Enable Day Mode 5 | Enable Night Mode 6 | 7 | No downloaded repos! 8 | Github Logo 9 | No internet connection! 10 | Failed to fetch Github repos! 11 | 12 | Github Owner Avatar Icon 13 | Repo Star Icon 14 | Repo Fork Icon 15 | Repo Icon 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |