├── .circleci └── config.yml ├── .github └── workflows │ └── pr.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── final ├── .gitignore ├── README.md ├── app │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── rocketreserver │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── graphql │ │ │ ├── BookTrip.graphql │ │ │ ├── CancelTrip.graphql │ │ │ ├── LaunchDetails.graphql │ │ │ ├── LaunchList.graphql │ │ │ ├── Login.graphql │ │ │ ├── TripsBooked.graphql │ │ │ └── schema.graphqls │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── rocketreserver │ │ │ │ ├── Apollo.kt │ │ │ │ ├── LaunchDetails.kt │ │ │ │ ├── LaunchList.kt │ │ │ │ ├── Login.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── Navigation.kt │ │ │ │ ├── TokenRepository.kt │ │ │ │ └── ui │ │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_placeholder.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── backup_rules.xml │ │ │ ├── data_extraction_rules.xml │ │ │ └── network_security_config.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── rocketreserver │ │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle │ ├── libs.versions.toml │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts └── start ├── .gitignore ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── rocketreserver │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── example │ │ │ └── rocketreserver │ │ │ ├── LaunchDetails.kt │ │ │ ├── LaunchList.kt │ │ │ ├── Login.kt │ │ │ ├── MainActivity.kt │ │ │ ├── Navigation.kt │ │ │ ├── TokenRepository.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── ic_placeholder.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── example │ └── rocketreserver │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | secops: apollo/circleci-secops-orb@2.0.6 5 | 6 | workflows: 7 | security-scans: 8 | jobs: 9 | - secops/gitleaks: 10 | context: 11 | - platform-docker-ro 12 | - github-orb 13 | - secops-oidc 14 | git-base-revision: <<#pipeline.git.base_revision>><><> 15 | git-revision: << pipeline.git.revision >> 16 | - secops/semgrep: 17 | context: 18 | - secops-oidc 19 | - github-orb 20 | git-base-revision: <<#pipeline.git.base_revision>><><> 21 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request 5 | 6 | # Cancel any current or previous job from the same PR 7 | concurrency: 8 | group: ${{ github.head_ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build-app: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'temurin' 20 | java-version: 17 21 | - name: Build tutorial 22 | working-directory: ./final 23 | run: | 24 | ./gradlew assembleDebug 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Idea 2 | **/.idea/* 3 | !**/.idea/codeStyles 4 | !**/.idea/icon.png 5 | !**/.idea/runConfigurations 6 | !**/.idea/scopes 7 | *.iml 8 | 9 | .gradle 10 | /local.properties 11 | /.idea/caches 12 | /.idea/libraries 13 | /.idea/modules.xml 14 | /.idea/workspace.xml 15 | /.idea/navEditor.xml 16 | /.idea/assetWizardSettings.xml 17 | .DS_Store 18 | /build 19 | /captures 20 | .externalNativeBuild 21 | .cxx 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @martinbonnin @BoD 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Meteor Development Group, 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 | # The tutorial is now part of the Odyssey learning platform and moved to https://github.com/apollographql/apollo-kotlin-odyssey 2 | 3 | # Apollo Kotlin Tutorial 4 | 5 | Repository for the [Apollo Kotlin](https://github.com/apollographql/apollo-kotlin) tutorial. 6 | 7 | The tutorial is available through our [documentation site](https://www.apollographql.com/docs/kotlin/tutorial/00-introduction/). This repository contains the corresponding code. 8 | 9 | - [start](./start): the starter project with the boilerplate and UI code already written but no Apollo Kotlin code. 10 | - [final](./final): the final state of the application with all functionality 11 | 12 | For copy errors in the tutorial, please file bugs against the main [`apollo-kotlin` repo](https://github.com/apollographql/apollo-kotlin). For broken code, please file issues on this repo. 13 | -------------------------------------------------------------------------------- /final/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | .DS_Store 4 | build 5 | captures 6 | .externalNativeBuild 7 | .cxx 8 | local.properties 9 | -------------------------------------------------------------------------------- /final/README.md: -------------------------------------------------------------------------------- 1 | # Apollo Kotlin Tutorial 2 | 3 | Repository for the [Apollo Kotlin](https://github.com/apollographql/apollo-kotlin) tutorial. 4 | 5 | This is the final state of the application with all functionality. 6 | -------------------------------------------------------------------------------- /final/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.compose.compiler) 5 | alias(libs.plugins.apollo) 6 | } 7 | 8 | android { 9 | namespace = "com.example.rocketreserver" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | applicationId = "com.example.rocketreserver" 14 | minSdk = 24 15 | targetSdk = 34 16 | versionCode = 1 17 | versionName = "1.0" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = false 28 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = "1.8" 37 | } 38 | buildFeatures { 39 | compose = true 40 | } 41 | packaging { 42 | resources { 43 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation(libs.androidx.core.ktx) 50 | implementation(libs.androidx.lifecycle.runtime.ktx) 51 | implementation(libs.androidx.activity.compose) 52 | implementation(platform(libs.androidx.compose.bom)) 53 | implementation(libs.androidx.ui) 54 | implementation(libs.androidx.ui.graphics) 55 | implementation(libs.androidx.ui.tooling.preview) 56 | implementation(libs.androidx.material3) 57 | implementation(libs.androidx.navigation.compose) 58 | implementation(libs.androidx.security.crypto) 59 | implementation(libs.coil.compose) 60 | 61 | implementation(libs.apollo.runtime) 62 | 63 | testImplementation(libs.junit) 64 | androidTestImplementation(libs.androidx.junit) 65 | androidTestImplementation(libs.androidx.espresso.core) 66 | androidTestImplementation(platform(libs.androidx.compose.bom)) 67 | androidTestImplementation(libs.androidx.ui.test.junit4) 68 | debugImplementation(libs.androidx.ui.tooling) 69 | debugImplementation(libs.androidx.ui.test.manifest) 70 | } 71 | 72 | apollo { 73 | service("service") { 74 | packageName.set("com.example.rocketreserver") 75 | introspection { 76 | endpointUrl.set("https://apollo-fullstack-tutorial.herokuapp.com/graphql") 77 | schemaFile.set(file("src/main/graphql/schema.graphqls")) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /final/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 -------------------------------------------------------------------------------- /final/app/src/androidTest/java/com/example/rocketreserver/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.rocketreserver", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /final/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/BookTrip.graphql: -------------------------------------------------------------------------------- 1 | mutation BookTrip($id: ID!) { 2 | bookTrips(launchIds: [$id]) { 3 | success 4 | message 5 | launches { 6 | id 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/CancelTrip.graphql: -------------------------------------------------------------------------------- 1 | mutation CancelTrip($id: ID!) { 2 | cancelTrip(launchId: $id) { 3 | success 4 | message 5 | launches { 6 | id 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/LaunchDetails.graphql: -------------------------------------------------------------------------------- 1 | query LaunchDetails($id: ID!) { 2 | launch(id: $id) { 3 | id 4 | site 5 | mission { 6 | name 7 | missionPatch(size: LARGE) 8 | } 9 | rocket { 10 | name 11 | type 12 | } 13 | isBooked 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/LaunchList.graphql: -------------------------------------------------------------------------------- 1 | query LaunchList($cursor: String) { 2 | launches(after: $cursor) { 3 | cursor 4 | launches { 5 | id 6 | site 7 | mission { 8 | name 9 | missionPatch(size: SMALL) 10 | } 11 | } 12 | hasMore 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/Login.graphql: -------------------------------------------------------------------------------- 1 | mutation Login($email: String!) { 2 | login(email: $email) { 3 | token 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/TripsBooked.graphql: -------------------------------------------------------------------------------- 1 | subscription TripsBooked { 2 | tripsBooked 3 | } 4 | -------------------------------------------------------------------------------- /final/app/src/main/graphql/schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | launches("The number of results to show. Must be >= 1. Default = 20" pageSize: Int, "If you add a cursor here, it will only return results _after_ this cursor" after: String): LaunchConnection! 3 | 4 | launch(id: ID!): Launch 5 | 6 | me: User 7 | 8 | totalTripsBooked: Int 9 | } 10 | 11 | """ 12 | Simple wrapper around our list of launches that contains a cursor to the 13 | last item in the list. Pass this cursor to the launches query to fetch results 14 | after these. 15 | """ 16 | type LaunchConnection { 17 | cursor: String! 18 | 19 | hasMore: Boolean! 20 | 21 | launches: [Launch]! 22 | } 23 | 24 | type Launch { 25 | id: ID! 26 | 27 | site: String 28 | 29 | mission: Mission 30 | 31 | rocket: Rocket 32 | 33 | isBooked: Boolean! 34 | } 35 | 36 | type Mission { 37 | name: String 38 | 39 | missionPatch(size: PatchSize): String 40 | } 41 | 42 | enum PatchSize { 43 | SMALL 44 | 45 | LARGE 46 | } 47 | 48 | type Rocket { 49 | id: ID! 50 | 51 | name: String 52 | 53 | type: String 54 | } 55 | 56 | type User { 57 | id: ID! 58 | 59 | email: String! 60 | 61 | profileImage: String 62 | 63 | trips: [Launch]! 64 | 65 | token: String 66 | } 67 | 68 | type Mutation { 69 | bookTrips(launchIds: [ID]!): TripUpdateResponse! 70 | 71 | cancelTrip(launchId: ID!): TripUpdateResponse! 72 | 73 | login(email: String): User 74 | } 75 | 76 | type TripUpdateResponse { 77 | success: Boolean! 78 | 79 | message: String 80 | 81 | launches: [Launch] 82 | } 83 | 84 | type Subscription { 85 | tripsBooked: Int 86 | } 87 | 88 | """ 89 | The `Upload` scalar type represents a file upload. 90 | """ 91 | scalar Upload 92 | 93 | schema { 94 | query: Query 95 | mutation: Mutation 96 | subscription: Subscription 97 | } 98 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/Apollo.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver 2 | 3 | import android.util.Log 4 | import com.apollographql.apollo.ApolloClient 5 | import com.apollographql.apollo.network.okHttpClient 6 | import kotlinx.coroutines.delay 7 | import okhttp3.Interceptor 8 | import okhttp3.OkHttpClient 9 | import okhttp3.Response 10 | 11 | private class AuthorizationInterceptor() : Interceptor { 12 | override fun intercept(chain: Interceptor.Chain): Response { 13 | val request = chain.request().newBuilder() 14 | .apply { 15 | TokenRepository.getToken()?.let { token -> 16 | addHeader("Authorization", token) 17 | } 18 | } 19 | .build() 20 | return chain.proceed(request) 21 | } 22 | } 23 | 24 | val apolloClient = ApolloClient.Builder() 25 | .serverUrl("https://apollo-fullstack-tutorial.herokuapp.com/graphql") 26 | .webSocketServerUrl("wss://apollo-fullstack-tutorial.herokuapp.com/graphql") 27 | .okHttpClient( 28 | OkHttpClient.Builder() 29 | .addInterceptor(AuthorizationInterceptor()) 30 | .build() 31 | ) 32 | .webSocketReopenWhen { throwable, attempt -> 33 | Log.d("Apollo", "WebSocket got disconnected, reopening after a delay", throwable) 34 | delay(attempt * 1000) 35 | true 36 | } 37 | 38 | .build() 39 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/LaunchDetails.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.CircularProgressIndicator 14 | import androidx.compose.material3.LocalContentColor 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.res.painterResource 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import androidx.compose.ui.unit.dp 29 | import coil.compose.AsyncImage 30 | import com.apollographql.apollo.exception.ApolloNetworkException 31 | import com.example.rocketreserver.LaunchDetailsState.Loading 32 | import com.example.rocketreserver.LaunchDetailsState.Success 33 | import com.example.rocketreserver.LaunchDetailsState.Error 34 | import kotlinx.coroutines.launch 35 | 36 | private sealed interface LaunchDetailsState { 37 | object Loading : LaunchDetailsState 38 | data class Error(val message: String) : LaunchDetailsState 39 | data class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState 40 | } 41 | 42 | @Composable 43 | fun LaunchDetails(launchId: String, navigateToLogin: () -> Unit) { 44 | var state by remember { mutableStateOf(Loading) } 45 | LaunchedEffect(Unit) { 46 | val response = apolloClient.query(LaunchDetailsQuery(launchId)).execute() 47 | state = when { 48 | response.errors.orEmpty().isNotEmpty() -> { 49 | // GraphQL error 50 | Error(response.errors!!.first().message) 51 | } 52 | response.exception is ApolloNetworkException -> { 53 | // Network error 54 | Error("Please check your network connectivity.") 55 | } 56 | response.data != null -> { 57 | // data (never partial) 58 | Success(response.data!!) 59 | } 60 | else -> { 61 | // Another fetch error, maybe a cache miss? 62 | // Or potentially a non-compliant server returning data: null without an error 63 | Error("Oh no... An error happened.") 64 | } 65 | } 66 | } 67 | when (val s = state) { 68 | Loading -> Loading() 69 | is Error -> ErrorMessage(s.message) 70 | is Success -> LaunchDetails(s.data, navigateToLogin) 71 | } 72 | } 73 | 74 | @Composable 75 | private fun LaunchDetails( 76 | data: LaunchDetailsQuery.Data, 77 | navigateToLogin: () -> Unit, 78 | ) { 79 | Column( 80 | modifier = Modifier 81 | .fillMaxSize() 82 | .padding(16.dp) 83 | ) { 84 | Row(verticalAlignment = Alignment.CenterVertically) { 85 | // Mission patch 86 | AsyncImage( 87 | modifier = Modifier.size(160.dp, 160.dp), 88 | model = data.launch?.mission?.missionPatch, 89 | placeholder = painterResource(R.drawable.ic_placeholder), 90 | error = painterResource(R.drawable.ic_placeholder), 91 | contentDescription = "Mission patch" 92 | ) 93 | 94 | Spacer(modifier = Modifier.size(16.dp)) 95 | 96 | Column { 97 | // Mission name 98 | Text( 99 | style = MaterialTheme.typography.headlineMedium, 100 | text = data.launch?.mission?.name ?: "" 101 | ) 102 | 103 | // Rocket name 104 | Text( 105 | modifier = Modifier.padding(top = 8.dp), 106 | style = MaterialTheme.typography.headlineSmall, 107 | text = data.launch?.rocket?.name?.let { "🚀 $it" } ?: "", 108 | ) 109 | 110 | // Site 111 | Text( 112 | modifier = Modifier.padding(top = 8.dp), 113 | style = MaterialTheme.typography.titleMedium, 114 | text = data.launch?.site ?: "", 115 | ) 116 | } 117 | } 118 | // Book button 119 | var loading by remember { mutableStateOf(false) } 120 | val scope = rememberCoroutineScope() 121 | var isBooked by remember { mutableStateOf(data.launch?.isBooked == true) } 122 | Button( 123 | modifier = Modifier 124 | .padding(top = 32.dp) 125 | .fillMaxWidth(), 126 | enabled = !loading, 127 | onClick = { 128 | loading = true 129 | scope.launch { 130 | val ok = onBookButtonClick( 131 | launchId = data.launch?.id ?: "", 132 | isBooked = isBooked, 133 | navigateToLogin = navigateToLogin 134 | ) 135 | if (ok) { 136 | isBooked = !isBooked 137 | } 138 | loading = false 139 | } 140 | } 141 | ) { 142 | if (loading) { 143 | SmallLoading() 144 | } else { 145 | Text(text = if (!isBooked) "Book now" else "Cancel booking") 146 | } 147 | } 148 | } 149 | } 150 | 151 | private suspend fun onBookButtonClick( 152 | launchId: String, 153 | isBooked: Boolean, 154 | navigateToLogin: () -> Unit 155 | ): Boolean { 156 | if (TokenRepository.getToken() == null) { 157 | navigateToLogin() 158 | return false 159 | } 160 | val mutation = if (isBooked) { 161 | CancelTripMutation(id = launchId) 162 | } else { 163 | BookTripMutation(id = launchId) 164 | } 165 | val response = apolloClient.mutation(mutation).execute() 166 | return if (response.data != null) { 167 | true 168 | } else { 169 | if (response.exception != null) { 170 | Log.w("LaunchDetails", "Failed to book/cancel trip", response.exception) 171 | false 172 | } else { 173 | Log.w("LaunchDetails", "Failed to book/cancel trip: ${response.errors!![0].message}") 174 | false 175 | } 176 | } 177 | } 178 | 179 | @Composable 180 | private fun ErrorMessage(text: String) { 181 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 182 | Text(text = text) 183 | } 184 | } 185 | 186 | @Composable 187 | private fun Loading() { 188 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 189 | CircularProgressIndicator() 190 | } 191 | } 192 | 193 | @Composable 194 | private fun SmallLoading() { 195 | CircularProgressIndicator( 196 | modifier = Modifier.size(24.dp), 197 | color = LocalContentColor.current, 198 | strokeWidth = 2.dp, 199 | ) 200 | } 201 | 202 | @Preview(showBackground = true) 203 | @Composable 204 | private fun LaunchDetailsPreview() { 205 | LaunchDetails(launchId = "42", navigateToLogin = {}) 206 | } 207 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/LaunchList.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.example.rocketreserver 4 | 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.material3.CircularProgressIndicator 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.ListItem 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.res.painterResource 26 | import androidx.compose.ui.unit.dp 27 | import coil.compose.AsyncImage 28 | import com.apollographql.apollo.api.ApolloResponse 29 | import com.apollographql.apollo.api.Optional 30 | 31 | @Composable 32 | fun LaunchList(onLaunchClick: (launchId: String) -> Unit) { 33 | var cursor: String? by remember { mutableStateOf(null) } 34 | var response: ApolloResponse? by remember { mutableStateOf(null) } 35 | var launchList by remember { mutableStateOf(emptyList()) } 36 | LaunchedEffect(cursor) { 37 | response = apolloClient.query(LaunchListQuery(Optional.present(cursor))).execute() 38 | launchList = launchList + response?.data?.launches?.launches?.filterNotNull().orEmpty() 39 | } 40 | 41 | LazyColumn(modifier = Modifier.fillMaxSize()) { 42 | items(launchList) { launch -> 43 | LaunchItem(launch = launch, onClick = onLaunchClick) 44 | } 45 | 46 | item { 47 | if (response?.data?.launches?.hasMore == true) { 48 | LoadingItem() 49 | cursor = response?.data?.launches?.cursor 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | private fun LaunchItem(launch: LaunchListQuery.Launch, onClick: (launchId: String) -> Unit) { 57 | ListItem( 58 | modifier = Modifier.clickable { onClick(launch.id) }, 59 | headlineContent = { 60 | // Mission name 61 | Text(text = launch.mission?.name ?: "") 62 | }, 63 | supportingContent = { 64 | // Site 65 | Text(text = launch.site ?: "") 66 | }, 67 | leadingContent = { 68 | // Mission patch 69 | AsyncImage( 70 | modifier = Modifier.size(68.dp, 68.dp), 71 | model = launch.mission?.missionPatch, 72 | placeholder = painterResource(R.drawable.ic_placeholder), 73 | error = painterResource(R.drawable.ic_placeholder), 74 | contentDescription = "Mission patch" 75 | ) 76 | } 77 | ) 78 | } 79 | 80 | @Composable 81 | private fun LoadingItem() { 82 | Box( 83 | contentAlignment = Alignment.Center, 84 | modifier = Modifier 85 | .fillMaxWidth() 86 | .padding(16.dp) 87 | ) { 88 | CircularProgressIndicator() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/Login.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.example.rocketreserver 4 | 5 | import android.util.Log 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.text.KeyboardOptions 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.CircularProgressIndicator 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.LocalContentColor 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.OutlinedTextField 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.text.input.KeyboardType 27 | import androidx.compose.ui.text.style.TextAlign 28 | import androidx.compose.ui.tooling.preview.Preview 29 | import androidx.compose.ui.unit.dp 30 | import kotlinx.coroutines.launch 31 | 32 | @Composable 33 | fun Login(navigateBack: () -> Unit) { 34 | Column( 35 | modifier = Modifier 36 | .fillMaxSize() 37 | .padding(16.dp) 38 | ) { 39 | // Title 40 | Text( 41 | modifier = Modifier.fillMaxWidth(), 42 | textAlign = TextAlign.Center, 43 | style = MaterialTheme.typography.headlineMedium, 44 | text = "Login" 45 | ) 46 | 47 | // Email 48 | var email by remember { mutableStateOf("") } 49 | OutlinedTextField( 50 | modifier = Modifier 51 | .padding(top = 16.dp) 52 | .fillMaxWidth(), 53 | label = { Text("Email") }, 54 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), 55 | value = email, 56 | onValueChange = { email = it } 57 | ) 58 | 59 | // Submit button 60 | var loading by remember { mutableStateOf(false) } 61 | val scope = rememberCoroutineScope() 62 | Button( 63 | modifier = Modifier 64 | .padding(top = 32.dp) 65 | .fillMaxWidth(), 66 | enabled = !loading, 67 | onClick = { 68 | loading = true 69 | scope.launch { 70 | val ok = login(email) 71 | loading = false 72 | if (ok) navigateBack() 73 | } 74 | } 75 | ) { 76 | if (loading) { 77 | Loading() 78 | } else { 79 | Text(text = "Submit") 80 | } 81 | } 82 | } 83 | } 84 | 85 | private suspend fun login(email: String): Boolean { 86 | val response = apolloClient.mutation(LoginMutation(email = email)).execute() 87 | val data = response.data 88 | return if (data != null) { 89 | if (data.login?.token != null) { 90 | TokenRepository.setToken(data.login.token) 91 | true 92 | } else { 93 | Log.w("Login", "Failed to login: no token returned by the backend") 94 | false 95 | } 96 | } else { 97 | if (response.exception != null) { 98 | Log.w("Login", "Failed to login", response.exception) 99 | false 100 | } else { 101 | Log.w("Login", "Failed to login: ${response.errors!![0].message}") 102 | false 103 | } 104 | } 105 | } 106 | 107 | @Composable 108 | private fun Loading() { 109 | CircularProgressIndicator( 110 | modifier = Modifier.size(24.dp), 111 | color = LocalContentColor.current, 112 | strokeWidth = 2.dp, 113 | ) 114 | } 115 | 116 | @Preview(showBackground = true) 117 | @Composable 118 | private fun LoginPreview() { 119 | Login(navigateBack = { }) 120 | } 121 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/MainActivity.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.example.rocketreserver 4 | 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.material3.SnackbarDuration 13 | import androidx.compose.material3.SnackbarHost 14 | import androidx.compose.material3.SnackbarHostState 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TopAppBar 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.collectAsState 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.navigation.compose.NavHost 25 | import androidx.navigation.compose.composable 26 | import androidx.navigation.compose.rememberNavController 27 | import com.apollographql.apollo.api.ApolloResponse 28 | import com.example.rocketreserver.ui.theme.RocketReserverTheme 29 | 30 | class MainActivity : ComponentActivity() { 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | TokenRepository.init(this) 34 | setContent { 35 | RocketReserverTheme { 36 | val snackbarHostState = remember { SnackbarHostState() } 37 | val tripBookedFlow = remember { apolloClient.subscription(TripsBookedSubscription()).toFlow() } 38 | val tripBookedResponse: ApolloResponse? by tripBookedFlow.collectAsState(initial = null) 39 | LaunchedEffect(tripBookedResponse) { 40 | if (tripBookedResponse == null) return@LaunchedEffect 41 | val message = when (tripBookedResponse!!.data?.tripsBooked) { 42 | null -> "Subscription error" 43 | -1 -> "Trip cancelled" 44 | else -> "Trip booked! 🚀" 45 | } 46 | snackbarHostState.showSnackbar( 47 | message = message, 48 | duration = SnackbarDuration.Short 49 | ) 50 | } 51 | 52 | Scaffold( 53 | topBar = { TopAppBar({ Text(stringResource(R.string.app_name)) }) }, 54 | snackbarHost = { SnackbarHost(snackbarHostState) }, 55 | ) { paddingValues -> 56 | Box(Modifier.padding(paddingValues)) { 57 | MainNavHost() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | @Composable 66 | private fun MainNavHost() { 67 | val navController = rememberNavController() 68 | NavHost(navController, startDestination = NavigationDestinations.LAUNCH_LIST) { 69 | composable(route = NavigationDestinations.LAUNCH_LIST) { 70 | LaunchList( 71 | onLaunchClick = { launchId -> 72 | navController.navigate("${NavigationDestinations.LAUNCH_DETAILS}/$launchId") 73 | } 74 | ) 75 | } 76 | 77 | composable(route = "${NavigationDestinations.LAUNCH_DETAILS}/{${NavigationArguments.LAUNCH_ID}}") { navBackStackEntry -> 78 | LaunchDetails( 79 | launchId = navBackStackEntry.arguments!!.getString(NavigationArguments.LAUNCH_ID)!!, 80 | navigateToLogin = { 81 | navController.navigate(NavigationDestinations.LOGIN) 82 | } 83 | ) 84 | } 85 | 86 | composable(route = NavigationDestinations.LOGIN) { 87 | Login( 88 | navigateBack = { 89 | navController.popBackStack() 90 | } 91 | ) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/Navigation.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver 2 | 3 | object NavigationDestinations { 4 | const val LAUNCH_LIST = "launchList" 5 | const val LAUNCH_DETAILS = "launchDetails" 6 | const val LOGIN = "login" 7 | } 8 | 9 | object NavigationArguments { 10 | const val LAUNCH_ID = "launchId" 11 | } 12 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/TokenRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.security.crypto.EncryptedSharedPreferences 6 | import androidx.security.crypto.MasterKey 7 | 8 | object TokenRepository { 9 | private const val KEY_TOKEN = "TOKEN" 10 | 11 | private lateinit var preferences: SharedPreferences 12 | 13 | fun init(context: Context) { 14 | val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) 15 | .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) 16 | .build() 17 | 18 | preferences = EncryptedSharedPreferences.create( 19 | context, 20 | "secret_shared_prefs", 21 | masterKey, 22 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, 23 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, 24 | ) 25 | } 26 | 27 | fun getToken(): String? { 28 | return preferences.getString(KEY_TOKEN, null) 29 | } 30 | 31 | fun setToken(token: String) { 32 | preferences.edit().apply { 33 | putString(KEY_TOKEN, token) 34 | apply() 35 | } 36 | } 37 | 38 | fun removeToken() { 39 | preferences.edit().apply { 40 | remove(KEY_TOKEN) 41 | apply() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun RocketReserverTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /final/app/src/main/kotlin/com/example/rocketreserver/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.rocketreserver.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /final/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /final/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /final/app/src/main/res/drawable/ic_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /final/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-kotlin-tutorial/3fa8d8f47e2e12406ce9c3019fd0f768258f8e2c/final/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /final/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /final/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Rocket Reserver 3 | -------------------------------------------------------------------------------- /final/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |