├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── objectbox-models │ ├── default.json │ └── default.json.bak ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── freeportmetrics │ │ └── kotlin_internal_project │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── freeportmetrics │ │ │ └── kotlin_internal_project │ │ │ ├── database │ │ │ └── ObjectBox.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── helper │ │ │ ├── ConvertHelper.kt │ │ │ ├── DarkModeHelper.kt │ │ │ └── DialogHelper.kt │ │ │ ├── model │ │ │ ├── Event.kt │ │ │ ├── Flight.kt │ │ │ └── Rocket.kt │ │ │ ├── repository │ │ │ ├── BaseRepository.kt │ │ │ ├── EventRepository.kt │ │ │ ├── FlightRepository.kt │ │ │ └── RocketRepository.kt │ │ │ ├── retrofit │ │ │ └── SpaceXService.kt │ │ │ ├── view │ │ │ ├── EventViewHolder.kt │ │ │ ├── FlightViewHolder.kt │ │ │ ├── GenericAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ └── RocketViewHolder.kt │ │ │ └── viewmodel │ │ │ ├── BaseViewModel.kt │ │ │ ├── EventViewModel.kt │ │ │ ├── FlightViewModel.kt │ │ │ └── RocketViewModel.kt │ └── res │ │ ├── drawable-night │ │ └── ic_btn.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_btn.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dialog_message.xml │ │ ├── item_event.xml │ │ ├── item_flight.xml │ │ └── item_rocket.xml │ │ ├── menu │ │ └── switch_modes_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── freeportmetrics │ └── kotlin_internal_project │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Freeport Metrics Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Android Kotlin starter project** 2 | 3 | ### **Purpose** 4 | To show possibilities and good practices using Kotlin language. 5 | 6 | 7 | ### **Description** 8 | Application connects to SpaceX API to download 3 types of data: 9 | 10 | * SpaceX rocket fleet 11 | * Upcoming flights 12 | * Past events 13 | 14 | Data is presented with generic adapter approach and is saved in the database. 15 | 16 | Clicking on each item navigates user to a browser to read more information on the Web. 17 | 18 | Use swipe-down gesture to refresh downloaded data. 19 | 20 | Click on top-right button to change between Light Mode and Dark Mode. 21 | 22 | 23 | ### **Libraries/concepts used** 24 | 25 | * MVVM architecture 26 | * Retrofit - for networking 27 | * Gson converter - for JSON parsing 28 | * ObjectBox - for NoSQL database 29 | * Koin - for service locator pattern implementation 30 | * Glide - for image loading 31 | * Timber - for logging 32 | * Android Architecture Components (LiveData, ViewModel classes) - for observer pattern and MVVM implementation 33 | * Kotlin coroutines - to manage threads gracefully 34 | * Kotlin View Binding - to ease connection with XML code 35 | * Generic adapter - to use single adapter for multiple object types 36 | -------------------------------------------------------------------------------- /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' 5 | apply plugin: 'io.objectbox' 6 | 7 | android { 8 | flavorDimensions "default" 9 | compileSdkVersion 28 10 | defaultConfig { 11 | applicationId "com.freeportmetrics.kotlin_internal_project" 12 | minSdkVersion 23 13 | targetSdkVersion 28 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | productFlavors { 25 | dev { 26 | buildConfigField "String", "SPACEX_API_URL", "\"https://api.spacexdata.com/v3/\"" 27 | } 28 | } 29 | } 30 | 31 | ext { 32 | coroutinesVersion = '1.0.1' 33 | ktxVersion = '1.0.1' 34 | appCompatVersion = '1.0.2' 35 | recyclerViewVersion = '1.0.0' 36 | lifecycleExtensionsVersion = '2.0.0' 37 | 38 | retrofitVersion = '2.5.0' 39 | retrofitCoroutinesAdapterVersion = '0.9.2' 40 | timberVersion = '4.7.1' 41 | koinVersion = '2.0.0-rc-2' 42 | glideVersion = '4.9.0' 43 | 44 | testJunitVersion = '4.12' 45 | testRunnerVersion = '1.1.1' 46 | testEspressoVersion = '3.1.1' 47 | } 48 | 49 | dependencies { 50 | implementation fileTree(dir: 'libs', include: ['*.jar']) 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 52 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" 53 | implementation "androidx.core:core-ktx:$ktxVersion" 54 | implementation "androidx.appcompat:appcompat:$appCompatVersion" 55 | implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion" 56 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleExtensionsVersion" 57 | 58 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 59 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 60 | implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$retrofitCoroutinesAdapterVersion" 61 | implementation "com.jakewharton.timber:timber:$timberVersion" 62 | implementation "org.koin:koin-android:$koinVersion" 63 | implementation "org.koin:koin-androidx-viewmodel:$koinVersion" 64 | implementation "io.objectbox:objectbox-kotlin:$objectboxVersion" 65 | implementation "com.github.bumptech.glide:glide:$glideVersion" 66 | kapt "com.github.bumptech.glide:compiler:$glideVersion" 67 | 68 | testImplementation "junit:junit:$testJunitVersion" 69 | androidTestImplementation "androidx.test:runner:$testRunnerVersion" 70 | androidTestImplementation "androidx.test.espresso:espresso-core:$testEspressoVersion" 71 | } 72 | -------------------------------------------------------------------------------- /app/objectbox-models/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", 3 | "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", 4 | "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", 5 | "entities": [ 6 | { 7 | "id": "4:8997267092427629326", 8 | "lastPropertyId": "11:8196038746891972390", 9 | "name": "Rocket", 10 | "properties": [ 11 | { 12 | "id": "1:7139511088173987756", 13 | "name": "image" 14 | }, 15 | { 16 | "id": "4:4956696553688012856", 17 | "name": "id" 18 | }, 19 | { 20 | "id": "5:3006712570559858515", 21 | "name": "name" 22 | }, 23 | { 24 | "id": "6:4694656277290009100", 25 | "name": "cost" 26 | }, 27 | { 28 | "id": "7:2998340680876056352", 29 | "name": "firstFlight" 30 | }, 31 | { 32 | "id": "8:153683174949848154", 33 | "name": "url" 34 | }, 35 | { 36 | "id": "10:6002916619708317047", 37 | "name": "height" 38 | }, 39 | { 40 | "id": "11:8196038746891972390", 41 | "name": "weight" 42 | } 43 | ], 44 | "relations": [] 45 | }, 46 | { 47 | "id": "5:3774110694116411874", 48 | "lastPropertyId": "6:3676800138233286703", 49 | "name": "Flight", 50 | "properties": [ 51 | { 52 | "id": "1:626332835206343025", 53 | "name": "id" 54 | }, 55 | { 56 | "id": "2:7993802601039111357", 57 | "name": "name" 58 | }, 59 | { 60 | "id": "3:8192067070488677027", 61 | "name": "launchDate" 62 | }, 63 | { 64 | "id": "6:3676800138233286703", 65 | "name": "urls" 66 | } 67 | ], 68 | "relations": [] 69 | }, 70 | { 71 | "id": "6:3199441985123381604", 72 | "lastPropertyId": "5:8609104638879090383", 73 | "name": "Event", 74 | "properties": [ 75 | { 76 | "id": "1:137238333079015904", 77 | "name": "id" 78 | }, 79 | { 80 | "id": "2:9205777784403291666", 81 | "name": "name" 82 | }, 83 | { 84 | "id": "3:2799334129419917946", 85 | "name": "date" 86 | }, 87 | { 88 | "id": "4:4898210385303123729", 89 | "name": "info" 90 | }, 91 | { 92 | "id": "5:8609104638879090383", 93 | "name": "urls" 94 | } 95 | ], 96 | "relations": [] 97 | } 98 | ], 99 | "lastEntityId": "6:3199441985123381604", 100 | "lastIndexId": "0:0", 101 | "lastRelationId": "0:0", 102 | "lastSequenceId": "0:0", 103 | "modelVersion": 4, 104 | "modelVersionParserMinimum": 4, 105 | "retiredEntityUids": [ 106 | 9059557678869301136, 107 | 2595229960809927845, 108 | 797828973248672978 109 | ], 110 | "retiredIndexUids": [], 111 | "retiredPropertyUids": [ 112 | 4051168560384758984, 113 | 6984389046907358508, 114 | 7623821168106054916, 115 | 1305947313796483568, 116 | 6589822814077259068, 117 | 8039739031557401805, 118 | 7976486627180801383, 119 | 433828257879240310, 120 | 9110279351248890984, 121 | 3879260289940935174, 122 | 7618930284939146735, 123 | 7867750146084933515, 124 | 8878250518912646344, 125 | 4289558388605460653, 126 | 528841095966552631, 127 | 8499293206396390180, 128 | 8805483336228136972, 129 | 6813566292694380342, 130 | 5196026490206898654, 131 | 1029942598495812163, 132 | 8153309559121762549, 133 | 6971054748935808656 134 | ], 135 | "retiredRelationUids": [], 136 | "version": 1 137 | } -------------------------------------------------------------------------------- /app/objectbox-models/default.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", 3 | "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", 4 | "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", 5 | "entities": [ 6 | { 7 | "id": "4:8997267092427629326", 8 | "lastPropertyId": "11:8196038746891972390", 9 | "name": "Rocket", 10 | "properties": [ 11 | { 12 | "id": "1:7139511088173987756", 13 | "name": "image" 14 | }, 15 | { 16 | "id": "4:4956696553688012856", 17 | "name": "id" 18 | }, 19 | { 20 | "id": "5:3006712570559858515", 21 | "name": "name" 22 | }, 23 | { 24 | "id": "6:4694656277290009100", 25 | "name": "cost" 26 | }, 27 | { 28 | "id": "7:2998340680876056352", 29 | "name": "firstFlight" 30 | }, 31 | { 32 | "id": "8:153683174949848154", 33 | "name": "url" 34 | }, 35 | { 36 | "id": "10:6002916619708317047", 37 | "name": "height" 38 | }, 39 | { 40 | "id": "11:8196038746891972390", 41 | "name": "weight" 42 | } 43 | ], 44 | "relations": [] 45 | }, 46 | { 47 | "id": "5:3774110694116411874", 48 | "lastPropertyId": "5:6971054748935808656", 49 | "name": "Flight", 50 | "properties": [ 51 | { 52 | "id": "1:626332835206343025", 53 | "name": "id" 54 | }, 55 | { 56 | "id": "2:7993802601039111357", 57 | "name": "name" 58 | }, 59 | { 60 | "id": "3:8192067070488677027", 61 | "name": "launchDate" 62 | } 63 | ], 64 | "relations": [] 65 | }, 66 | { 67 | "id": "6:3199441985123381604", 68 | "lastPropertyId": "5:8609104638879090383", 69 | "name": "Event", 70 | "properties": [ 71 | { 72 | "id": "1:137238333079015904", 73 | "name": "id" 74 | }, 75 | { 76 | "id": "2:9205777784403291666", 77 | "name": "name" 78 | }, 79 | { 80 | "id": "3:2799334129419917946", 81 | "name": "date" 82 | }, 83 | { 84 | "id": "4:4898210385303123729", 85 | "name": "info" 86 | }, 87 | { 88 | "id": "5:8609104638879090383", 89 | "name": "urls" 90 | } 91 | ], 92 | "relations": [] 93 | } 94 | ], 95 | "lastEntityId": "6:3199441985123381604", 96 | "lastIndexId": "0:0", 97 | "lastRelationId": "0:0", 98 | "lastSequenceId": "0:0", 99 | "modelVersion": 4, 100 | "modelVersionParserMinimum": 4, 101 | "retiredEntityUids": [ 102 | 9059557678869301136, 103 | 2595229960809927845, 104 | 797828973248672978 105 | ], 106 | "retiredIndexUids": [], 107 | "retiredPropertyUids": [ 108 | 4051168560384758984, 109 | 6984389046907358508, 110 | 7623821168106054916, 111 | 1305947313796483568, 112 | 6589822814077259068, 113 | 8039739031557401805, 114 | 7976486627180801383, 115 | 433828257879240310, 116 | 9110279351248890984, 117 | 3879260289940935174, 118 | 7618930284939146735, 119 | 7867750146084933515, 120 | 8878250518912646344, 121 | 4289558388605460653, 122 | 528841095966552631, 123 | 8499293206396390180, 124 | 8805483336228136972, 125 | 6813566292694380342, 126 | 5196026490206898654, 127 | 1029942598495812163, 128 | 8153309559121762549, 129 | 6971054748935808656 130 | ], 131 | "retiredRelationUids": [], 132 | "version": 1 133 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/freeportmetrics/kotlin_internal_project/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.freeportmetrics.kotlin_internal_project", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/database/ObjectBox.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.database 2 | 3 | import android.content.Context 4 | import com.freeportmetrics.kotlin_internal_project.model.MyObjectBox 5 | import io.objectbox.BoxStore 6 | 7 | object ObjectBox { 8 | 9 | lateinit var boxStore: BoxStore 10 | private set 11 | 12 | fun init(context: Context): BoxStore { 13 | if (::boxStore.isInitialized && !boxStore.isClosed) { 14 | return boxStore 15 | } 16 | 17 | boxStore = MyObjectBox.builder().androidContext(context.applicationContext).build() 18 | return boxStore 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.di 2 | 3 | import com.freeportmetrics.kotlin_internal_project.database.ObjectBox 4 | import com.freeportmetrics.kotlin_internal_project.helper.DarkModeHelper 5 | import com.freeportmetrics.kotlin_internal_project.helper.DialogHelper 6 | import com.freeportmetrics.kotlin_internal_project.repository.EventRepository 7 | import com.freeportmetrics.kotlin_internal_project.repository.FlightRepository 8 | import com.freeportmetrics.kotlin_internal_project.repository.RocketRepository 9 | import com.freeportmetrics.kotlin_internal_project.retrofit.SpaceXService 10 | import com.freeportmetrics.kotlin_internal_project.viewmodel.EventViewModel 11 | import com.freeportmetrics.kotlin_internal_project.viewmodel.FlightViewModel 12 | import com.freeportmetrics.kotlin_internal_project.viewmodel.RocketViewModel 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.androidx.viewmodel.dsl.viewModel 15 | import org.koin.dsl.module 16 | 17 | val helperModule = module { 18 | single { DarkModeHelper(androidContext()) } 19 | single { DialogHelper(androidContext()) } 20 | } 21 | 22 | val networkModule = module { 23 | single { SpaceXService.create() } 24 | } 25 | 26 | val boxModule = module { 27 | single { ObjectBox.init(androidContext()) } 28 | } 29 | 30 | val repositoryModule = module { 31 | single { RocketRepository(get(), get()) } 32 | single { FlightRepository(get(), get()) } 33 | single { EventRepository(get(), get()) } 34 | } 35 | 36 | val viewModelModule = module { 37 | viewModel { RocketViewModel(get()) } 38 | viewModel { FlightViewModel(get()) } 39 | viewModel { EventViewModel(get()) } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/helper/ConvertHelper.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.helper 2 | 3 | import com.freeportmetrics.kotlin_internal_project.model.Flight.Link 4 | import io.objectbox.converter.PropertyConverter 5 | import java.text.SimpleDateFormat 6 | import java.util.* 7 | 8 | fun epochToDate(epoch: Long?): String { 9 | if (epoch != null) { 10 | val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) 11 | return dateFormat.format(java.util.Date(epoch * 1000)) 12 | } 13 | return "" 14 | } 15 | 16 | abstract class BaseMapConverter(private val property: String) : PropertyConverter, V?> { 17 | override fun convertToDatabaseValue(entityProperty: Map): V? { 18 | return entityProperty[property] 19 | } 20 | 21 | override fun convertToEntityProperty(databaseValue: V?): Map { 22 | return if (databaseValue != null) { 23 | mapOf(Pair(property, databaseValue)) 24 | } else { 25 | mapOf(Pair(property, null)) 26 | } 27 | } 28 | } 29 | 30 | class MapToDoubleConverter : BaseMapConverter("meters") 31 | 32 | class MapToIntConverter : BaseMapConverter("kg") 33 | 34 | class MapToStringConverter : BaseMapConverter("article") 35 | 36 | class LinksConverter : PropertyConverter { 37 | override fun convertToDatabaseValue(entityProperty: Link?): String? { 38 | return entityProperty?.reddit_campaign 39 | } 40 | 41 | override fun convertToEntityProperty(databaseValue: String?): Link? { 42 | return if (databaseValue != null) { 43 | Link(databaseValue) 44 | } else { 45 | Link(null) 46 | } 47 | } 48 | } 49 | 50 | class ListStringConverter : PropertyConverter, String> { 51 | override fun convertToDatabaseValue(entityProperty: List?): String? { 52 | return entityProperty?.get(0) 53 | } 54 | 55 | override fun convertToEntityProperty(databaseValue: String?): List? { 56 | return if (databaseValue != null) { 57 | listOf(databaseValue) 58 | } else { 59 | listOf() 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/helper/DarkModeHelper.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.helper 2 | 3 | import android.content.Context 4 | import android.view.Menu 5 | import android.widget.Button 6 | import androidx.appcompat.app.AppCompatDelegate 7 | import androidx.appcompat.content.res.AppCompatResources.getDrawable 8 | import com.freeportmetrics.kotlin_internal_project.R 9 | 10 | internal class DarkModeHelper(private val context: Context) { 11 | internal fun onModeChanged(newMode: Boolean, delegate: AppCompatDelegate) { 12 | if (newMode) { 13 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 14 | } else { 15 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 16 | } 17 | delegate.applyDayNight() 18 | } 19 | 20 | internal fun setButtonColors(darkMode: Boolean, active: Button, vararg notActiveOnes: Button) { 21 | active.backgroundTintList = context.getColorStateList(R.color.colorAccent) 22 | for (notActive in notActiveOnes) { 23 | if (darkMode) { 24 | notActive.backgroundTintList = context.getColorStateList(R.color.button_material_dark) 25 | } else { 26 | notActive.backgroundTintList = context.getColorStateList(R.color.button_material_light) 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/helper/DialogHelper.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.helper 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import androidx.appcompat.app.AlertDialog 8 | import androidx.core.content.edit 9 | import com.freeportmetrics.kotlin_internal_project.R 10 | import kotlinx.android.synthetic.main.dialog_message.view.* 11 | 12 | private const val BUTTON_DEFAULT_ID = 0 13 | private const val BUTTON_POSITIVE_ID = -1 14 | private const val PREF_DIALOG = "PREF_DIALOG" 15 | private const val PREF_KEY_DIALOG_BUTTON = "PREF_KEY_DIALOG_BUTTON" 16 | private const val PREF_KEY_DIALOG_DONT_ASK_AGAIN = "PREF_KEY_DIALOG_DONT_ASK_AGAIN" 17 | 18 | class DialogHelper(private val context: Context) { 19 | private val sharedPrefs = context.getSharedPreferences(PREF_DIALOG, Context.MODE_PRIVATE) 20 | 21 | fun handleCheckBox(intent: Intent) { 22 | if (sharedPrefs.getBoolean(PREF_KEY_DIALOG_DONT_ASK_AGAIN, false) && 23 | sharedPrefs.getInt(PREF_KEY_DIALOG_BUTTON, BUTTON_DEFAULT_ID) == BUTTON_POSITIVE_ID) { 24 | context.startActivity(intent) 25 | } else { 26 | showDialog { context.startActivity(intent) } 27 | } 28 | } 29 | 30 | private fun showDialog(completion: () -> Unit) { 31 | val builder = 32 | AlertDialog.Builder(context) // possible change to MaterialAlertDialogBuilder when it comes out of alpha 33 | val customDialog = (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate( 34 | R.layout.dialog_message, 35 | null 36 | ) 37 | 38 | with(builder) { 39 | setTitle(context.resources.getString(R.string.dialog_title)) 40 | setMessage(context.resources.getString(R.string.dialog_message)) 41 | setView(customDialog) 42 | setPositiveButton(context.resources.getString(R.string.dialog_btn_positive)) { _, which -> 43 | context.getSharedPreferences(PREF_DIALOG, Context.MODE_PRIVATE).edit { 44 | putInt(PREF_KEY_DIALOG_BUTTON, which) 45 | } 46 | completion() 47 | } 48 | setNegativeButton(context.resources.getString(R.string.dialog_btn_negative)) { dialog, which -> 49 | context.getSharedPreferences(PREF_DIALOG, Context.MODE_PRIVATE).edit { 50 | putInt(PREF_KEY_DIALOG_BUTTON, which) 51 | } 52 | dialog.dismiss() 53 | } 54 | customDialog.checkBox.setOnClickListener { 55 | saveCurrentCheckBoxState(customDialog) 56 | } 57 | show() 58 | } 59 | 60 | saveCurrentCheckBoxState(customDialog) 61 | } 62 | 63 | private fun saveCurrentCheckBoxState(customDialog: View) { 64 | context.getSharedPreferences(PREF_DIALOG, Context.MODE_PRIVATE).edit { 65 | putBoolean(PREF_KEY_DIALOG_DONT_ASK_AGAIN, customDialog.checkBox.isChecked) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/model/Event.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.model 2 | 3 | import com.freeportmetrics.kotlin_internal_project.helper.MapToStringConverter 4 | import com.google.gson.annotations.SerializedName 5 | import io.objectbox.annotation.Convert 6 | import io.objectbox.annotation.Entity 7 | import io.objectbox.annotation.Id 8 | 9 | @Entity 10 | data class Event( 11 | @Id(assignable = true) var id: Long = 0, 12 | @SerializedName("title") val name: String, 13 | @SerializedName("event_date_unix") val date: Long, 14 | @SerializedName("details") val info: String, 15 | @SerializedName("links") @Convert(converter = MapToStringConverter::class, dbType = String::class) val urls: Map? 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/model/Flight.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.model 2 | 3 | import com.freeportmetrics.kotlin_internal_project.helper.LinksConverter 4 | import com.google.gson.annotations.SerializedName 5 | import io.objectbox.annotation.Convert 6 | import io.objectbox.annotation.Entity 7 | import io.objectbox.annotation.Id 8 | 9 | @Entity 10 | data class Flight( 11 | @Id(assignable = true) var id: Long = 0, 12 | @SerializedName("mission_name") val name: String, 13 | @SerializedName("launch_date_unix") val launchDate: Long, 14 | @SerializedName("links") @Convert(converter = LinksConverter::class, dbType = String::class) val urls: Link? 15 | ) { 16 | data class Link( // another class needed due to more complicated 'links' JSON 17 | @SerializedName("reddit_campaign") val reddit_campaign: String? 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/model/Rocket.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.model 2 | 3 | import com.freeportmetrics.kotlin_internal_project.helper.ListStringConverter 4 | import com.freeportmetrics.kotlin_internal_project.helper.MapToDoubleConverter 5 | import com.freeportmetrics.kotlin_internal_project.helper.MapToIntConverter 6 | import com.google.gson.annotations.SerializedName 7 | import io.objectbox.annotation.Convert 8 | import io.objectbox.annotation.Entity 9 | import io.objectbox.annotation.Id 10 | 11 | @Entity 12 | data class Rocket( 13 | @Id(assignable = true) var id: Long = 0, 14 | @SerializedName("rocket_name") val name: String, 15 | @SerializedName("cost_per_launch") val cost: Int, 16 | @SerializedName("first_flight") val firstFlight: String, 17 | @SerializedName("wikipedia") val url: String?, 18 | @SerializedName("flickr_images") @Convert(converter = ListStringConverter::class, dbType = String::class) val image: List, 19 | @SerializedName("height") @Convert(converter = MapToDoubleConverter::class, dbType = Double::class) val height: Map, 20 | @SerializedName("mass") @Convert(converter = MapToIntConverter::class, dbType = Int::class) val weight: Map 21 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/repository/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.freeportmetrics.kotlin_internal_project.retrofit.SpaceXService 6 | import io.objectbox.BoxStore 7 | import io.objectbox.kotlin.boxFor 8 | import kotlinx.coroutines.* 9 | import retrofit2.HttpException 10 | import retrofit2.Response 11 | import timber.log.Timber 12 | 13 | abstract class BaseRepository(@PublishedApi internal val service: SpaceXService, @PublishedApi internal val boxStore: BoxStore) { 14 | 15 | abstract fun loadData() : LiveData> 16 | 17 | inline fun fetchData(crossinline call: (SpaceXService) -> Deferred>>): LiveData> { 18 | val result = MutableLiveData>() 19 | 20 | CoroutineScope(Dispatchers.IO).launch { 21 | val request = call(service) 22 | withContext(Dispatchers.Main) { 23 | try { 24 | val response = request.await() 25 | if (response.isSuccessful) { 26 | result.value = response.body() 27 | } else { 28 | Timber.d("Error occurred with code ${response.code()}") 29 | } 30 | } catch (e: HttpException) { 31 | Timber.d("Error: ${e.message()}") 32 | } catch (e: Throwable) { 33 | Timber.d("Error: ${e.message}") 34 | } 35 | } 36 | } 37 | 38 | return result 39 | } 40 | 41 | inline fun saveToDatabase(data: List) { 42 | CoroutineScope(Dispatchers.IO).launch { 43 | boxStore.boxFor().removeAll() // deleting and inserting data to avoid sync issues 44 | boxStore.boxFor().put(data) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/repository/EventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.freeportmetrics.kotlin_internal_project.model.Event 5 | import com.freeportmetrics.kotlin_internal_project.retrofit.SpaceXService 6 | import io.objectbox.BoxStore 7 | 8 | class EventRepository(service: SpaceXService, store: BoxStore) : BaseRepository(service, store) { 9 | 10 | override fun loadData(): LiveData> { 11 | return fetchData { service.getPastEventsAsync() } 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/repository/FlightRepository.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.freeportmetrics.kotlin_internal_project.model.Flight 5 | import com.freeportmetrics.kotlin_internal_project.retrofit.SpaceXService 6 | import io.objectbox.BoxStore 7 | 8 | class FlightRepository(service: SpaceXService, store: BoxStore) : BaseRepository(service, store) { 9 | 10 | override fun loadData(): LiveData> { 11 | return fetchData { service.getNextFlightsAsync() } 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/repository/RocketRepository.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.freeportmetrics.kotlin_internal_project.model.Rocket 5 | import com.freeportmetrics.kotlin_internal_project.retrofit.SpaceXService 6 | import io.objectbox.BoxStore 7 | 8 | class RocketRepository(service: SpaceXService, store: BoxStore) : BaseRepository(service, store) { 9 | 10 | override fun loadData(): LiveData> { 11 | return fetchData { service.getRocketsAsync() } 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/retrofit/SpaceXService.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.retrofit 2 | 3 | import com.freeportmetrics.kotlin_internal_project.BuildConfig 4 | import com.freeportmetrics.kotlin_internal_project.model.Flight 5 | import com.freeportmetrics.kotlin_internal_project.model.Rocket 6 | import com.freeportmetrics.kotlin_internal_project.model.Event 7 | import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory 8 | import kotlinx.coroutines.Deferred 9 | import retrofit2.Response 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.gson.GsonConverterFactory 12 | import retrofit2.http.GET 13 | 14 | interface SpaceXService { 15 | 16 | @GET("rockets") 17 | fun getRocketsAsync(): Deferred>> 18 | 19 | @GET("launches/upcoming") 20 | fun getNextFlightsAsync(): Deferred>> 21 | 22 | @GET("history") 23 | fun getPastEventsAsync(): Deferred>> 24 | 25 | companion object { 26 | fun create(): SpaceXService { 27 | return Retrofit.Builder() 28 | .baseUrl(BuildConfig.SPACEX_API_URL) 29 | .addConverterFactory(GsonConverterFactory.create()) 30 | .addCallAdapterFactory(CoroutineCallAdapterFactory()) 31 | .build() 32 | .create(SpaceXService::class.java) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/view/EventViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.view 2 | 3 | import android.content.Intent 4 | import android.view.View 5 | import android.widget.Toast 6 | import androidx.core.net.toUri 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.freeportmetrics.kotlin_internal_project.R 9 | import com.freeportmetrics.kotlin_internal_project.helper.DialogHelper 10 | import com.freeportmetrics.kotlin_internal_project.helper.epochToDate 11 | import com.freeportmetrics.kotlin_internal_project.model.Event 12 | import kotlinx.android.synthetic.main.item_event.view.* 13 | 14 | class EventViewHolder(private val view: View, private val dialogHelper: DialogHelper) : RecyclerView.ViewHolder(view), GenericAdapter.Binder { 15 | override fun bind(data: Event) { 16 | view.tv_event_name.text = data.name 17 | view.tv_event_date.text = view.context.getString(R.string.flight_event_date, epochToDate(data.date)) 18 | view.tv_event_info.text = data.info 19 | 20 | view.setOnClickListener { 21 | if (data.urls?.get("article") != null) { 22 | val intent = Intent(Intent.ACTION_VIEW) 23 | intent.data = data.urls["article"].toString().toUri() 24 | dialogHelper.handleCheckBox(intent) 25 | } else { 26 | Toast.makeText(view.context, view.context.getString(R.string.toast_no_article), Toast.LENGTH_SHORT).show() 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/view/FlightViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.view 2 | 3 | import android.content.Intent 4 | import android.view.View 5 | import android.widget.Toast 6 | import androidx.core.net.toUri 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.freeportmetrics.kotlin_internal_project.R 9 | import com.freeportmetrics.kotlin_internal_project.helper.DialogHelper 10 | import com.freeportmetrics.kotlin_internal_project.helper.epochToDate 11 | import com.freeportmetrics.kotlin_internal_project.model.Flight 12 | import kotlinx.android.synthetic.main.item_flight.view.* 13 | 14 | class FlightViewHolder(private val view: View, private val dialogHelper: DialogHelper) : RecyclerView.ViewHolder(view), GenericAdapter.Binder { 15 | override fun bind(data: Flight) { 16 | view.tv_flight_name.text = view.context.getString(R.string.flight_name, data.name) 17 | view.tv_flight_date.text = view.context.getString(R.string.flight_event_date, epochToDate(data.launchDate)) 18 | 19 | view.setOnClickListener { 20 | if (data.urls?.reddit_campaign != null) { 21 | val intent = Intent(Intent.ACTION_VIEW) 22 | intent.data = data.urls.reddit_campaign.toUri() 23 | dialogHelper.handleCheckBox(intent) 24 | } else { 25 | Toast.makeText(view.context, view.context.getString(R.string.toast_no_campaign), Toast.LENGTH_SHORT).show() 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/view/GenericAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.view 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.freeportmetrics.kotlin_internal_project.R 8 | import com.freeportmetrics.kotlin_internal_project.helper.DialogHelper 9 | import com.freeportmetrics.kotlin_internal_project.model.Flight 10 | import com.freeportmetrics.kotlin_internal_project.model.Rocket 11 | 12 | class GenericAdapter(private val context: Context, private val items: List, private val dialogHelper: DialogHelper) 13 | : RecyclerView.Adapter() { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 16 | return when { 17 | items.all { it is Rocket } -> RocketViewHolder(LayoutInflater.from(context).inflate(R.layout.item_rocket, parent, false), dialogHelper) 18 | items.all { it is Flight } -> FlightViewHolder(LayoutInflater.from(context).inflate(R.layout.item_flight, parent, false), dialogHelper) 19 | else -> EventViewHolder(LayoutInflater.from(context).inflate(R.layout.item_event, parent, false), dialogHelper) 20 | } 21 | } 22 | 23 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 24 | (holder as Binder).bind(items[position]) 25 | } 26 | 27 | override fun getItemCount(): Int = items.size 28 | 29 | internal interface Binder { 30 | fun bind(data: T) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/view/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.view 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.content.res.Configuration 6 | import android.os.Build 7 | import androidx.appcompat.app.AppCompatActivity 8 | import android.os.Bundle 9 | import android.view.Menu 10 | import android.view.MenuItem 11 | import androidx.core.content.edit 12 | import androidx.lifecycle.Observer 13 | import androidx.recyclerview.widget.DividerItemDecoration 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import com.freeportmetrics.kotlin_internal_project.R 16 | import com.freeportmetrics.kotlin_internal_project.di.* 17 | import com.freeportmetrics.kotlin_internal_project.helper.* 18 | import com.freeportmetrics.kotlin_internal_project.model.Event 19 | import com.freeportmetrics.kotlin_internal_project.model.Flight 20 | import com.freeportmetrics.kotlin_internal_project.model.Rocket 21 | import com.freeportmetrics.kotlin_internal_project.viewmodel.BaseViewModel 22 | import com.freeportmetrics.kotlin_internal_project.viewmodel.EventViewModel 23 | import com.freeportmetrics.kotlin_internal_project.viewmodel.FlightViewModel 24 | import com.freeportmetrics.kotlin_internal_project.viewmodel.RocketViewModel 25 | import io.objectbox.BoxStore 26 | import io.objectbox.kotlin.boxFor 27 | import kotlinx.android.synthetic.main.activity_main.* 28 | import org.koin.android.ext.android.inject 29 | import org.koin.android.ext.koin.androidContext 30 | import org.koin.android.ext.koin.androidLogger 31 | import org.koin.androidx.viewmodel.ext.android.viewModel 32 | import org.koin.core.context.startKoin 33 | import org.koin.core.context.stopKoin 34 | import timber.log.Timber 35 | 36 | class MainActivity : AppCompatActivity() { 37 | private enum class DataType(val id: Int) { 38 | NO_TYPE(-1), 39 | ROCKETS(0), 40 | NEXT_FLIGHTS(1), 41 | PAST_EVENTS(2), 42 | } 43 | 44 | private val boxStore: BoxStore by inject() 45 | private val darkModeHelper: DarkModeHelper by inject() 46 | private val dialogHelper: DialogHelper by inject() 47 | private val rocketVm: RocketViewModel by viewModel() 48 | private val flightVm: FlightViewModel by viewModel() 49 | private val eventVm: EventViewModel by viewModel() 50 | 51 | private lateinit var sharedPref: SharedPreferences 52 | private var darkMode = false 53 | private var currentDataType = DataType.NO_TYPE.id 54 | 55 | //region Override 56 | override fun onCreate(savedInstanceState: Bundle?) { 57 | super.onCreate(savedInstanceState) 58 | setContentView(R.layout.activity_main) 59 | Timber.plant(Timber.DebugTree()) 60 | startKoin { 61 | androidLogger() 62 | androidContext(this@MainActivity) 63 | modules(listOf(repositoryModule, networkModule, boxModule, viewModelModule, helperModule)) 64 | } 65 | 66 | sharedPref = getPreferences(Context.MODE_PRIVATE) 67 | darkMode = sharedPref.getBoolean("darkMode", false) 68 | currentDataType = sharedPref.getInt("currentDataType", DataType.NO_TYPE.id) 69 | 70 | rv_generic?.layoutManager = LinearLayoutManager(this) 71 | rv_generic.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) 72 | 73 | setListeners() 74 | } 75 | 76 | override fun onResume() { 77 | super.onResume() 78 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 79 | configureAutoDarkMode() 80 | } else { 81 | darkModeHelper.onModeChanged(darkMode, delegate) 82 | } 83 | handleDataReloading() 84 | } 85 | 86 | override fun onDestroy() { 87 | super.onDestroy() 88 | stopKoin() 89 | } 90 | //endregion 91 | 92 | //region Dark Mode 93 | private fun configureAutoDarkMode() { 94 | val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 95 | when (currentNightMode) { 96 | Configuration.UI_MODE_NIGHT_YES -> darkMode = true 97 | Configuration.UI_MODE_NIGHT_NO -> darkMode = false 98 | } 99 | } 100 | 101 | private fun handleDataReloading() { 102 | when (currentDataType) { 103 | DataType.ROCKETS.id -> { 104 | darkModeHelper.setButtonColors(darkMode, btn_rockets, btn_next_flights, btn_events) 105 | populateAdapter(boxStore.boxFor().all) 106 | } 107 | DataType.NEXT_FLIGHTS.id -> { 108 | darkModeHelper.setButtonColors(darkMode, btn_next_flights, btn_rockets, btn_events) 109 | populateAdapter(boxStore.boxFor().all) 110 | } 111 | DataType.PAST_EVENTS.id -> { 112 | darkModeHelper.setButtonColors(darkMode, btn_events, btn_rockets, btn_next_flights) 113 | populateAdapter(boxStore.boxFor().all) 114 | } 115 | } 116 | } 117 | //endregion 118 | 119 | //region Data loading 120 | private fun setListeners() { 121 | val rocketBox = boxStore.boxFor() 122 | val flightBox = boxStore.boxFor() 123 | val eventBox = boxStore.boxFor() 124 | 125 | btn_rockets.setOnClickListener { 126 | currentDataType = DataType.ROCKETS.id 127 | darkModeHelper.setButtonColors(darkMode, btn_rockets, btn_next_flights, btn_events) 128 | if (rocketBox.all == null || rocketBox.all.isEmpty()) { 129 | downloadAndSaveData(rocketVm) 130 | } else { 131 | populateAdapter(rocketBox.all) 132 | } 133 | } 134 | 135 | btn_next_flights.setOnClickListener { 136 | currentDataType = DataType.NEXT_FLIGHTS.id 137 | darkModeHelper.setButtonColors(darkMode, btn_next_flights, btn_rockets, btn_events) 138 | if (flightBox.all == null || flightBox.all.isEmpty()) { 139 | downloadAndSaveData(flightVm) 140 | } else { 141 | populateAdapter(flightBox.all) 142 | } 143 | } 144 | 145 | btn_events.setOnClickListener { 146 | currentDataType = DataType.PAST_EVENTS.id 147 | darkModeHelper.setButtonColors(darkMode, btn_events, btn_rockets, btn_next_flights) 148 | if (eventBox.all == null || eventBox.all.isEmpty()) { 149 | downloadAndSaveData(eventVm) 150 | } else { 151 | populateAdapter(eventBox.all) 152 | } 153 | } 154 | 155 | swipe_refresh.setOnRefreshListener { 156 | when (currentDataType) { 157 | DataType.ROCKETS.id -> downloadAndSaveData(rocketVm) 158 | DataType.NEXT_FLIGHTS.id -> downloadAndSaveData(flightVm) 159 | DataType.PAST_EVENTS.id -> downloadAndSaveData(eventVm) 160 | } 161 | } 162 | } 163 | 164 | private fun downloadAndSaveData(viewModel: BaseViewModel) { 165 | val liveData = viewModel.getDataFromRetrofit() 166 | liveData.observe(this, Observer { data -> 167 | if (data != null) { 168 | populateAdapter(data) 169 | viewModel.saveToDatabase(data) 170 | swipe_refresh.isRefreshing = false 171 | } 172 | }) 173 | } 174 | 175 | private fun populateAdapter(data: List) { 176 | rv_generic.adapter = GenericAdapter(this@MainActivity, data, dialogHelper) 177 | } 178 | //endregion 179 | 180 | //region Menu 181 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 182 | menuInflater.inflate(R.menu.switch_modes_menu, menu) 183 | return true 184 | } 185 | 186 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 187 | return when (item?.itemId) { 188 | R.id.mnu_set_theme -> { 189 | darkMode = !darkMode 190 | sharedPref.edit { 191 | putBoolean("darkMode", darkMode) 192 | putInt("currentDataType", currentDataType) 193 | } 194 | darkModeHelper.onModeChanged(darkMode, delegate) 195 | true 196 | } 197 | else -> super.onOptionsItemSelected(item) 198 | } 199 | } 200 | //endregion 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/view/RocketViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.view 2 | 3 | import android.content.Intent 4 | import android.view.View 5 | import android.widget.Toast 6 | import androidx.core.net.toUri 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.bumptech.glide.Glide 9 | import com.freeportmetrics.kotlin_internal_project.R 10 | import com.freeportmetrics.kotlin_internal_project.helper.* 11 | import com.freeportmetrics.kotlin_internal_project.model.Rocket 12 | import kotlinx.android.synthetic.main.item_rocket.view.* 13 | 14 | class RocketViewHolder(private val view: View, private val dialogHelper: DialogHelper) : RecyclerView.ViewHolder(view), GenericAdapter.Binder { 15 | 16 | override fun bind(data: Rocket) { 17 | view.tv_rocket_name.text = data.name 18 | view.tv_rocket_cost_per_launch.text = view.context.getString(R.string.rocket_name, data.cost / 1_000_000) 19 | view.tv_rocket_first_flight.text = view.context.getString(R.string.rocket_first_flight, data.firstFlight) 20 | view.tv_rocket_height.text = view.context.getString(R.string.rocket_height, data.height["meters"]) 21 | view.tv_rocket_weight.text = view.context.getString(R.string.rocket_weight, data.weight["kg"]?.div(1_000)) 22 | Glide.with(view.context).load(data.image[0]).into(view.iv_rocket) 23 | 24 | view.setOnClickListener { 25 | if (data.url != null) { 26 | val intent = Intent(Intent.ACTION_VIEW) 27 | intent.data = data.url.toUri() 28 | dialogHelper.handleCheckBox(intent) 29 | } else { 30 | Toast.makeText(view.context, view.context.getString(R.string.toast_no_link), Toast.LENGTH_SHORT).show() 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/viewmodel/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | 6 | abstract class BaseViewModel : ViewModel() { 7 | 8 | abstract fun getDataFromRetrofit(): LiveData> 9 | abstract fun saveToDatabase(data: List) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/viewmodel/EventViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.freeportmetrics.kotlin_internal_project.model.Event 5 | import com.freeportmetrics.kotlin_internal_project.repository.EventRepository 6 | 7 | class EventViewModel(private val repository: EventRepository) : BaseViewModel() { 8 | 9 | private lateinit var eventsList: LiveData> 10 | 11 | override fun getDataFromRetrofit(): LiveData> { 12 | eventsList = repository.loadData() 13 | return eventsList 14 | } 15 | 16 | override fun saveToDatabase(data: List) { 17 | repository.saveToDatabase(data) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/viewmodel/FlightViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.freeportmetrics.kotlin_internal_project.model.Flight 5 | import com.freeportmetrics.kotlin_internal_project.repository.FlightRepository 6 | 7 | class FlightViewModel(private val repository: FlightRepository) : BaseViewModel() { 8 | 9 | private lateinit var flightList: LiveData> 10 | 11 | override fun getDataFromRetrofit(): LiveData> { 12 | flightList = repository.loadData() 13 | return flightList 14 | } 15 | 16 | override fun saveToDatabase(data: List) { 17 | repository.saveToDatabase(data) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/freeportmetrics/kotlin_internal_project/viewmodel/RocketViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.freeportmetrics.kotlin_internal_project.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.freeportmetrics.kotlin_internal_project.model.Rocket 5 | import com.freeportmetrics.kotlin_internal_project.repository.RocketRepository 6 | 7 | class RocketViewModel(private val repository: RocketRepository) : BaseViewModel() { 8 | 9 | private lateinit var rocketList: LiveData> 10 | 11 | override fun getDataFromRetrofit(): LiveData> { 12 | rocketList = repository.loadData() 13 | return rocketList 14 | } 15 | 16 | override fun saveToDatabase(data: List) { 17 | repository.saveToDatabase(data) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/ic_btn.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_btn.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 |