├── .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 [](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 |
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 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/unitTests/kotlin/io/github/plastix/kotlinboilerplate/data/network/NetworkInteractorTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.plastix.kotlinboilerplate.data.network
2 |
3 | import android.net.ConnectivityManager
4 | import android.net.NetworkInfo
5 | import io.reactivex.Completable
6 | import org.junit.Assert
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.mockito.Mock
10 | import org.mockito.Mockito
11 | import org.mockito.MockitoAnnotations
12 |
13 | class NetworkInteractorTest {
14 |
15 | lateinit var networkInteractor: NetworkInteractor
16 |
17 | @Mock
18 | lateinit var connectivityManager: ConnectivityManager
19 |
20 | @Mock
21 | lateinit var networkInfo: NetworkInfo
22 |
23 | @Before
24 | fun setUp() {
25 | MockitoAnnotations.initMocks(this)
26 | networkInteractor = NetworkInteractorImpl(connectivityManager)
27 | Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo)
28 | }
29 |
30 | @Test
31 | fun hasNetworkConnection_shouldReturnFalseWhenNoNetwork() {
32 | Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn(null)
33 |
34 | Assert.assertFalse(networkInteractor.hasNetworkConnection())
35 | }
36 |
37 | @Test
38 | fun `hasNetworkConnection_shouldReturnFalseWhenNotConnected`() {
39 | Mockito.`when`(networkInfo.isConnectedOrConnecting).thenReturn(false)
40 |
41 | Assert.assertFalse(networkInteractor.hasNetworkConnection())
42 | }
43 |
44 | @Test
45 | fun hasNetworkConnection_shouldReturnTrueWhenConnected() {
46 | Mockito.`when`(networkInfo.isConnectedOrConnecting).thenReturn(true)
47 |
48 | Assert.assertTrue(networkInteractor.hasNetworkConnection())
49 | }
50 |
51 | @Test
52 | fun hasNetworkConnectionCompletable_shouldCompleteWhenConnected() {
53 | Mockito.`when`(networkInfo.isConnectedOrConnecting).thenReturn(true)
54 |
55 | Assert.assertEquals(networkInteractor.hasNetworkConnectionCompletable(), Completable.complete())
56 | }
57 |
58 | @Test
59 | fun hasNetworkConnectionCompletable_shouldErrorWhenNotConnected() {
60 | Mockito.`when`(networkInfo.isConnectedOrConnecting).thenReturn(false)
61 |
62 | Assert.assertTrue(networkInteractor.hasNetworkConnectionCompletable().blockingGet() is NetworkInteractor.NetworkUnavailableException)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/unitTests/kotlin/io/github/plastix/kotlinboilerplate/ui/base/RxViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.plastix.kotlinboilerplate.ui.base
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 | import io.reactivex.disposables.Disposables
5 | import io.reactivex.observers.TestObserver
6 | import org.junit.Assert
7 | import org.junit.Before
8 | import org.junit.Test
9 |
10 | class RxViewModelTest {
11 |
12 | lateinit var sub: TestObserver
13 | lateinit var viewModel: ViewModelSubclass
14 |
15 | @Before
16 | fun setup() {
17 | sub = TestObserver.create()
18 | viewModel = ViewModelSubclass()
19 | }
20 |
21 | @Test
22 | fun noViewAttachedByDefault() {
23 | viewModel.getViewState().subscribe(sub)
24 |
25 | sub.assertNoErrors()
26 | sub.assertNotComplete()
27 | sub.assertValue(false)
28 | }
29 |
30 | @Test
31 | fun bindViewUpdatesViewState() {
32 | viewModel.getViewState().subscribe(sub)
33 |
34 | viewModel.bind()
35 |
36 | sub.assertNoErrors()
37 | sub.assertNotComplete()
38 | sub.assertValues(false, true)
39 | }
40 |
41 | @Test
42 | fun unbindViewUpdatesViewState() {
43 | viewModel.getViewState().subscribe(sub)
44 |
45 | viewModel.unbind()
46 |
47 | sub.assertNoErrors()
48 | sub.assertNotComplete()
49 | sub.assertValues(false, false)
50 | }
51 |
52 | @Test
53 | fun addSubscriptionUpdatesCompositeSubscription() {
54 | Assert.assertTrue(viewModel.getSubcriptions().size() == 0)
55 | viewModel.addDisposable(Disposables.empty())
56 | Assert.assertTrue(viewModel.getSubcriptions().size() == 1)
57 | }
58 |
59 | @Test
60 | fun onDestroyClearsSubscriptionsAndUpdatesView() {
61 | viewModel.getViewState().subscribe(sub)
62 |
63 | viewModel.addDisposable(Disposables.empty())
64 |
65 | Assert.assertTrue(viewModel.getSubcriptions().size() == 1)
66 |
67 | viewModel.onDestroy()
68 |
69 | sub.assertNoErrors()
70 | sub.assertValues(false)
71 | sub.assertComplete()
72 |
73 | Assert.assertTrue(viewModel.getSubcriptions().size() == 0)
74 |
75 | }
76 |
77 | class ViewModelSubclass : RxViewModel() {
78 |
79 | fun getSubcriptions(): CompositeDisposable = disposables
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/unitTests/kotlin/io/github/plastix/kotlinboilerplate/ui/detail/DetailViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.plastix.kotlinboilerplate.ui.detail
2 |
3 | import io.github.plastix.kotlinboilerplate.data.remote.model.Owner
4 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo
5 | import org.junit.Assert
6 | import org.junit.Before
7 | import org.junit.Test
8 |
9 | class DetailViewModelTest {
10 |
11 | lateinit var owner: Owner
12 | lateinit var repo: Repo
13 | lateinit var viewModel: DetailViewModel
14 |
15 | @Before
16 | fun setUp() {
17 | owner = Owner("Author",
18 | "someURL")
19 |
20 | repo = Repo("Name",
21 | "Author/Name",
22 | owner,
23 | "Some random repo",
24 | 50,
25 | 100)
26 |
27 | viewModel = DetailViewModel(repo)
28 | viewModel.bind()
29 | }
30 |
31 | @Test
32 | fun getName_returnsCorrectName() {
33 | Assert.assertEquals(viewModel.getName(), repo.fullName)
34 | }
35 |
36 | @Test
37 | fun getDescription_returnsCorrectDescription() {
38 | Assert.assertEquals(viewModel.getDescription(), repo.description)
39 | }
40 |
41 | @Test
42 | fun getStars_returnsCorrectStarCount() {
43 | Assert.assertEquals(viewModel.getStars(), repo.stars.toString())
44 | }
45 |
46 | @Test
47 | fun getForks_returnsCorrectForkCount() {
48 | Assert.assertEquals(viewModel.getForks(), repo.forks.toString())
49 | }
50 |
51 | @Test
52 | fun getAvatarURL_returnsCorrectString() {
53 | Assert.assertEquals(viewModel.getAvatarURL(), repo.owner.avatarUrl)
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/unitTests/kotlin/io/github/plastix/kotlinboilerplate/ui/list/ListViewModelTest.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.GithubApiService
5 | import io.github.plastix.kotlinboilerplate.data.remote.model.Owner
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.rxschedulerrule.RxSchedulerRule
9 | import io.reactivex.Completable
10 | import io.reactivex.Single
11 | import org.junit.Before
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.mockito.Mock
15 | import org.mockito.Mockito
16 | import org.mockito.MockitoAnnotations
17 |
18 | class ListViewModelTest {
19 |
20 | @get:Rule @Suppress("unused")
21 | val schedulerRule = RxSchedulerRule()
22 |
23 | @Mock
24 | lateinit var apiService: GithubApiService
25 |
26 | @Mock
27 | lateinit var networkInteractor: NetworkInteractor
28 |
29 | lateinit var viewModel: ListViewModel
30 |
31 | @Before
32 | fun setUp() {
33 | MockitoAnnotations.initMocks(this)
34 |
35 | viewModel = ListViewModel(apiService, networkInteractor)
36 | viewModel.bind()
37 | }
38 |
39 | @Test
40 | fun getRepos_shouldReturnEmpty() {
41 | viewModel.getRepos().test().assertValue(emptyList())
42 | }
43 |
44 | @Test
45 | fun loadingState_shouldBeFalse() {
46 | viewModel.loadingState().test().assertValue(false)
47 | }
48 |
49 | @Test
50 | fun fetchErrors_shouldReturnNothing() {
51 | viewModel.fetchErrors().test().assertNoErrors()
52 | }
53 |
54 | @Test
55 | fun networkErrors_shouldReturnNothing() {
56 | viewModel.networkErrors().test().assertNoErrors()
57 | }
58 |
59 | @Test
60 | fun getKotlinRepos_shouldUpdateViewWithApiData() {
61 | Mockito.`when`(networkInteractor.hasNetworkConnectionCompletable())
62 | .thenReturn(Completable.complete())
63 |
64 | val repos: List = listOf(mockRepo(), mockRepo())
65 | val response: SearchResponse = SearchResponse(0, repos)
66 | Mockito.`when`(apiService.repoSearch(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()))
67 | .thenReturn(Single.just(response))
68 |
69 | viewModel.fetchRepos()
70 |
71 | viewModel.getRepos().test().assertValue(repos)
72 | viewModel.loadingState().test().assertValue(false)
73 | }
74 |
75 | @Test
76 | fun getKotlinRepos_shouldErrorWithNetworkMessage() {
77 | val error = NetworkInteractor.NetworkUnavailableException()
78 | Mockito.`when`(networkInteractor.hasNetworkConnectionCompletable())
79 | .thenReturn(
80 | Completable.error(error)
81 | )
82 |
83 | val response: SearchResponse = SearchResponse(0, listOf(mockRepo()))
84 | Mockito.`when`(apiService.repoSearch(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()))
85 | .thenReturn(Single.just(response))
86 | val networkObserver = viewModel.networkErrors().test()
87 |
88 | viewModel.fetchRepos()
89 |
90 | viewModel.loadingState().test().assertValue(false)
91 | networkObserver.assertValue(error)
92 | }
93 |
94 | @Test
95 | fun getKotlinRepos_shouldErrorWithFetchMessage() {
96 | Mockito.`when`(networkInteractor.hasNetworkConnectionCompletable())
97 | .thenReturn(Completable.complete())
98 |
99 | val error = Throwable("Error")
100 | Mockito.`when`(apiService.repoSearch(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()))
101 | .thenReturn(Single.error(error))
102 | val fetchObserver = viewModel.fetchErrors().test()
103 |
104 | viewModel.fetchRepos()
105 |
106 | viewModel.loadingState().test().assertValue(false)
107 | fetchObserver.assertValue(error)
108 | }
109 |
110 | fun mockRepo() = Repo("", "", Owner("", ""), "", 0, 0)
111 |
112 | }
--------------------------------------------------------------------------------
/app/src/unitTests/kotlin/io/github/plastix/kotlinboilerplate/ui/list/RepoViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.plastix.kotlinboilerplate.ui.list
2 |
3 | import io.github.plastix.kotlinboilerplate.data.remote.model.Owner
4 | import io.github.plastix.kotlinboilerplate.data.remote.model.Repo
5 | import org.junit.Assert
6 | import org.junit.Before
7 | import org.junit.Test
8 |
9 | class RepoViewModelTest {
10 |
11 |
12 | lateinit var owner: Owner
13 | lateinit var repo: Repo
14 | lateinit var viewModel: RepoViewModel
15 |
16 | @Before
17 | fun setUp() {
18 | owner = Owner("Author",
19 | "someURL")
20 |
21 | repo = Repo("Name",
22 | "Author/Name",
23 | owner,
24 | "Some random repo",
25 | 50,
26 | 100)
27 |
28 | viewModel = RepoViewModel(repo)
29 | viewModel.bind()
30 | }
31 |
32 | @Test
33 | fun getName_returnsCorrectName() {
34 | Assert.assertEquals(viewModel.getName(), repo.fullName)
35 | }
36 |
37 | @Test
38 | fun getDescription_returnsCorrectDescription() {
39 | Assert.assertEquals(viewModel.getDescription(), repo.description)
40 | }
41 |
42 | @Test
43 | fun clicks_returnsNoClicks() {
44 | viewModel.clicks().test().assertNoValues()
45 | }
46 |
47 | @Test
48 | fun clicks_clicksOnce() {
49 | val observer = viewModel.clicks().test()
50 |
51 | viewModel.onClick()
52 | observer.assertValueCount(1)
53 | }
54 | }
--------------------------------------------------------------------------------
/art/screenshots/detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/art/screenshots/detail.png
--------------------------------------------------------------------------------
/art/screenshots/detail_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/art/screenshots/detail_night.png
--------------------------------------------------------------------------------
/art/screenshots/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/art/screenshots/list.png
--------------------------------------------------------------------------------
/art/screenshots/list_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/art/screenshots/list_night.png
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.1.3-2'
5 | ext.android_plugin_version = "2.3.3"
6 |
7 | repositories {
8 | jcenter()
9 | }
10 |
11 | dependencies {
12 |
13 | classpath "com.android.tools.build:gradle:$android_plugin_version"
14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
15 |
16 | // NOTE: Do not place your application dependencies here; they belong
17 | // in the individual module build.gradle files
18 | }
19 | }
20 |
21 | allprojects {
22 | repositories {
23 | jcenter()
24 | }
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
31 | task wrapper(type: Wrapper) {
32 | gradleVersion = "4.0"
33 | }
34 |
35 | // Disable incremental builds on the build server
36 | project.ext.preDexLibs = !project.hasProperty('disablePreDex')
37 |
38 | subprojects {
39 | project.plugins.whenPluginAdded { plugin ->
40 | if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) {
41 | project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
42 | } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
43 | project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/ci.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -xe
3 |
4 | # You can run it from any directory.
5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
6 |
7 | # This will:
8 | # 1. Clean the project
9 | # 2. Run Android Lint
10 | # 3. Run tests under JVM
11 |
12 | # We run each step separately to consume less memory (free CI sometimes fails with OOM)
13 |
14 | GRADLE=""$DIR"/gradlew -PdisablePreDex --no-daemon"
15 |
16 | # 1
17 | eval "$GRADLE clean"
18 |
19 | # 2
20 | eval "$GRADLE lintDebug"
21 | eval "$GRADLE lintRelease"
22 |
23 | # 3
24 | eval "$GRADLE testDebugUnitTest"
25 | eval "$GRADLE testReleaseUnitTest"
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | java:
3 | version: oraclejdk8
4 | environment:
5 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx3500m -XX:+HeapDumpOnOutOfMemoryError"'
6 |
7 | dependencies:
8 | pre:
9 | - echo y | android update sdk --no-ui --all --filter "platform-tools,tools,android-25,extra-android-support,extra-android-m2repository"
10 | # Build tools should be installed after "tools", uh.
11 | - echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2"
12 |
13 | test:
14 | override:
15 | - sh ci.sh
16 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Plastix/Kotlin-Android-Boilerplate/443762ca4f331cd2d37a042298e57416d3c63ab1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jul 17 20:28:16 PDT 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save ( ) {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/signing.properties.sample:
--------------------------------------------------------------------------------
1 | STORE_FILE=/path/to/your.keystore
2 | STORE_PASSWORD=yourkeystorepass
3 | KEY_ALIAS=projectkeyalias
4 | KEY_PASSWORD=keyaliaspassword
--------------------------------------------------------------------------------