diff --git a/.fleet/receipt.json b/.fleet/receipt.json new file mode 100644 index 0000000..2393e03 --- /dev/null +++ b/.fleet/receipt.json @@ -0,0 +1,24 @@ +// Project generated by Kotlin Multiplatform Wizard +{ + "spec": { + "template_id": "kmt", + "targets": { + "android": { + "ui": [ + "compose" + ] + }, + "ios": { + "ui": [ + "compose" + ] + }, + "desktop": { + "ui": [ + "compose" + ] + } + } + }, + "timestamp": "2023-12-03T19:28:10.118100282Z" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c91a391..014127c 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,11 @@ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ + +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name index b8117e7..d69616e 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -Rick and Morty \ No newline at end of file +RickandMorty \ No newline at end of file diff --git a/.idea/artifacts/composeApp_desktop.xml b/.idea/artifacts/composeApp_desktop.xml new file mode 100644 index 0000000..2858e5a --- /dev/null +++ b/.idea/artifacts/composeApp_desktop.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/composeApp/build/libs + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 7829f3c..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
- - -
-
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 6e6eec1..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/detekt.xml b/.idea/detekt.xml index d7ca9af..bfab4a3 100644 --- a/.idea/detekt.xml +++ b/.idea/detekt.xml @@ -1,5 +1,9 @@ + + true true diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 9029a53..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml deleted file mode 100644 index 7d04a74..0000000 --- a/.idea/inspectionProfiles/ktlint.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 64580d1..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index e34606c..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 75ad4d2..0fe79d2 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,102 @@ -# ![launcher icon](logo.png) Rick and Morty [![Actions Status](https://github.com/mohsenoid/Rick-and-Morty/workflows/Android%20CI/badge.svg)](https://github.com/mohsenoid/Rick-and-Morty/actions) [![codecov](https://codecov.io/gh/mohsenoid/Rick-and-Morty/branch/master/graph/badge.svg)](https://codecov.io/gh/mohsenoid/Rick-and-Morty) [![CodeFactor](https://www.codefactor.io/repository/github/mohsenoid/rick-and-morty/badge)](https://www.codefactor.io/repository/github/mohsenoid/rick-and-morty) - -This repository contains Rick and Morty Android application which I am using as training material. - -![Screenshot](SCREENSHOT1.png) ![Screenshot](SCREENSHOT2.png) - -## User Stories - -1. The first thing a user should see is a list of episodes. -2. If the user taps into an episode the app has to display a list of characters with a clear distinction between dead and alive characters -3. If the user taps into a character the app has to display that character's picture and information. -4. The user should have the ability to kill a character and if a character gets killed the character lists should update accordingly - -## Functionality - -The app is composed of 3 main screens: - -### Episodes List - -It allows you to list episodes in pages. Network results are kept in the database in the `episodes` table. Each time a new page is fetched, the same `episode` record in the database is updated with the new data. - -### Episode's Characters List - -Shows you the list of episode characters. Network results are also kept in the database in the `characters table`. Each time a new character is fetched, the same `character` record in the database is updated with the new data and we make sure we don't override *is killed by user* data. - -### Character Details - -This screen displays the details of a character and if *is killed by the user*. - -## Building - -You can clone and open the project in Android studio and press run! - -## Testing - -The project uses local unit tests that run on your computer. To run those tests and generate a coverage report, you can run: - - ./gradlew jacocoReport - -***NOTE:*** You can find the tests report in `app/build/reports/jacoco/jacocoReport/html/index.html` - -## Technical details - -The Application implemented and structured based on **Clean Architecture** and **SOLID** principles best practices and the presentation layer is implemented based on the **MVP** pattern. - -The **Data** layer contains **Network Client** implemented by *Retrofit* library to get access to remote data on [Rick And Morty API](https://rickandmortyapi.com/) and **DB** implemented by *Room* library to cache and persist those data locally in case of offline usage. - -The **Domain** layer consists of a **Repository** which allows access to the Data layer. It also uses *Kotlin Coroutines* **IO** and **Main** *dispatchers* to execute long-running tasks in the background and reflect the result on UI. There is also a **Test** *dispatcher* that executes tasks immediately on the same unit test thread. - -![Repository Pattern](REPOSITORY_PATTERN.png) - -The **View** layer is done with the [Android Navigation Component](https://developer.android.com/guide/navigation) including one MainActivity which holds the navigation host fragment and 3 different Fragments which uses their contract interfaces to implement the *view* and *presenter* for responding to user interactions. - -![Navigation Graph](NAV_GRAPH.png) - -The **Koin** library does the *dependency injections* in the whole app. It also uses **Base** objects to define scopes and inject dependencies into **Activities** and **Fragments**. - -[**GitHub Actions CI service**](https://github.com/mohsenoid/Rick-and-Morty/actions) is running the repo tests and build Gradle tasks and **jacoco** plugin generates and submits the code coverage reports to [**codecov.io**](https://codecov.io/gh/mohsenoid/Rick-and-Morty). - -Code is covered by unit tests implemented using **Mockito** and **Kluent**. Also, some Android tests using **Robolectric**. - -## Libraries - -- **Timber** logger library made by [Jake Wharton](https://github.com/JakeWharton/timber) -- **Picasso** image downloading and caching library made by [square](https://github.com/square/picasso) -- **Retrofit** and **OkHttp** API libraries made by [square](https://github.com/square/retrofit) -- **Kotlin Serialization** plugin made by [jetbrains](https://github.com/Kotlin/kotlinx.serialization) -- **Koin** dependency injector library made by [InsertKoinIO](https://github.com/InsertKoinIO/koin) -- **Kluent** assertions library made by [MarkusAmshove](https://github.com/MarkusAmshove/Kluent) - -### References - -- App **Launcher Icon** made by [freepngimg.com](http://freepngimg.com) - -- **Dead/Alive Icons** made by [Freepik](https://flaticon.com/authors/freepik) from [flaticon.com](https://flaticon.com) - -## License - -Copyright 2020 Mohsen Mirhoseini Argi - -Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +# ![launcher icon](logo.png) Rick and Morty [![Actions Status](https://github.com/mohsenoid/Rick-and-Morty/workflows/Android%20CI/badge.svg)](https://github.com/mohsenoid/Rick-and-Morty/actions) [![codecov](https://codecov.io/gh/mohsenoid/Rick-and-Morty/branch/master/graph/badge.svg)](https://codecov.io/gh/mohsenoid/Rick-and-Morty) [![CodeFactor](https://www.codefactor.io/repository/github/mohsenoid/rick-and-morty/badge)](https://www.codefactor.io/repository/github/mohsenoid/rick-and-morty) + +This repository contains Rick and Morty Android application which I am using as training material. + +![Screenshot](SCREENSHOT1.png) ![Screenshot](SCREENSHOT2.png) + +## User Stories + +1. The first thing a user should see is a list of episodes. +2. If the user taps into an episode the app has to display a list of characters with a clear distinction between dead and alive characters +3. If the user taps into a character the app has to display that character's picture and information. +4. The user should have the ability to kill a character and if a character gets killed the character lists should update accordingly + +## Functionality + +The app is composed of 3 main screens: + +### Episodes List + +It allows you to list episodes in pages. Network results are kept in the database in the `episodes` table. Each time a new page is fetched, the same `episode` record in the database is updated with the new data. + +### Episode's Characters List + +Shows you the list of episode characters. Network results are also kept in the database in the `characters table`. Each time a new character is fetched, the same `character` record in the database is updated with the new data and we make sure we don't override *is killed by user* data. + +### Character Details + +This screen displays the details of a character and if *is killed by the user*. + +## Building + +You can clone and open the project in Android studio and press run! + +## Testing + +The project uses local unit tests that run on your computer. To run those tests and generate a coverage report, you can run: + + ./gradlew jacocoReport + +***NOTE:*** You can find the tests report in `app/build/reports/jacoco/jacocoReport/html/index.html` + +## Technical details + +The Application implemented and structured based on **Clean Architecture** and **SOLID** principles best practices and the presentation layer is implemented based on the **MVP** pattern. + +The **Data** layer contains **Network Client** implemented by *Retrofit* library to get access to remote data on [Rick And Morty API](https://rickandmortyapi.com/) and **DB** implemented by *Room* library to cache and persist those data locally in case of offline usage. + +The **Domain** layer consists of a **Repository** which allows access to the Data layer. It also uses *Kotlin Coroutines* **IO** and **Main** *dispatchers* to execute long-running tasks in the background and reflect the result on UI. There is also a **Test** *dispatcher* that executes tasks immediately on the same unit test thread. + +![Repository Pattern](REPOSITORY_PATTERN.png) + +The **View** layer is done with the [Android Navigation Component](https://developer.android.com/guide/navigation) including one MainActivity which holds the navigation host fragment and 3 different Fragments which uses their contract interfaces to implement the *view* and *presenter* for responding to user interactions. + +![Navigation Graph](NAV_GRAPH.png) + +The **Koin** library does the *dependency injections* in the whole app. It also uses **Base** objects to define scopes and inject dependencies into **Activities** and **Fragments**. + +[**GitHub Actions CI service**](https://github.com/mohsenoid/Rick-and-Morty/actions) is running the repo tests and build Gradle tasks and **jacoco** plugin generates and submits the code coverage reports to [**codecov.io**](https://codecov.io/gh/mohsenoid/Rick-and-Morty). + +Code is covered by unit tests implemented using **Mockito** and **Kluent**. Also, some Android tests using **Robolectric**. + +## Libraries + +- **Timber** logger library made by [Jake Wharton](https://github.com/JakeWharton/timber) +- **Picasso** image downloading and caching library made by [square](https://github.com/square/picasso) +- **Retrofit** and **OkHttp** API libraries made by [square](https://github.com/square/retrofit) +- **Kotlin Serialization** plugin made by [jetbrains](https://github.com/Kotlin/kotlinx.serialization) +- **Koin** dependency injector library made by [InsertKoinIO](https://github.com/InsertKoinIO/koin) +- **Kluent** assertions library made by [MarkusAmshove](https://github.com/MarkusAmshove/Kluent) + +### References + +- App **Launcher Icon** made by [freepngimg.com](http://freepngimg.com) + +- **Dead/Alive Icons** made by [Freepik](https://flaticon.com/authors/freepik) from [flaticon.com](https://flaticon.com) + +## License + +Copyright 2020 Mohsen Mirhoseini Argi + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +----- + +This is a Kotlin Multiplatform project targeting Android, iOS, Desktop. + +* `/composeApp` is for code that will be shared across your Compose Multiplatform applications. + It contains several subfolders: + - `commonMain` is for code that’s common for all targets. + - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. + For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, + `iosMain` would be the right folder for such calls. + +* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, + you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. + + +Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/app/build.gradle.kts b/app/build.gradle.kts deleted file mode 100644 index e868a0b..0000000 --- a/app/build.gradle.kts +++ /dev/null @@ -1,198 +0,0 @@ -import org.jlleitschuh.gradle.ktlint.reporter.ReporterType - -plugins { - id("com.android.application") - kotlin("android") - kotlin("kapt") - kotlin("plugin.serialization") version "1.5.31" - id("org.jetbrains.dokka") version "1.5.30" - id("androidx.navigation.safeargs.kotlin") - - id("io.gitlab.arturbosch.detekt") version "1.18.1" - id("org.jlleitschuh.gradle.ktlint") version "10.2.0" - - jacoco -} - -android { - compileSdk = 31 - - defaultConfig { - applicationId = "com.mohsenoid.rickandmorty" - - minSdk = 24 - targetSdk = 31 - multiDexEnabled = true - - versionCode = 10 - versionName = "2.7.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - defaultConfig { - buildConfigField("String", "BASE_URL", "\"https://rickandmortyapi.com/api/\"") - } - - debug { - isMinifyEnabled = false - isTestCoverageEnabled = true - } - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - sourceSets { - getByName("main") { - java { - srcDirs("src/main/kotlin") - srcDirs("${buildDir.absolutePath}/generated/source/kaptKotlin/") - } - } - getByName("test") { - java { - srcDirs("src/test/kotlin") - } - } - getByName("androidTest") { - java { - srcDirs("src/androidTest/kotlin") - } - } - } - - testOptions { - execution = "ANDROIDX_TEST_ORCHESTRATOR" - animationsDisabled = true - - unitTests { - isIncludeAndroidResources = true - } - } - - buildFeatures { - dataBinding = true - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - allWarningsAsErrors = true - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") - } - - lint { - isIgnoreWarnings = false - isWarningsAsErrors = true - } -} - -// https://arturbosch.github.io/detekt/#quick-start-with-gradle -detekt { - allRules = true - source = files("src/main/kotlin/") - baseline = file("detekt-baseline.xml") - buildUponDefaultConfig = true - reports { - html { enabled = true } - xml { enabled = true } - txt { enabled = false } - } -} - -// https://github.com/JLLeitschuh/ktlint-gradle#configuration -ktlint { - reporters { - reporter(ReporterType.HTML) - reporter(ReporterType.CHECKSTYLE) - } -} - -// https://docs.gradle.org/current/userguide/jacoco_plugin.html -jacoco { - toolVersion = "0.8.7" -} - -dependencies { - // Android Jetpack - implementation("androidx.appcompat:appcompat:1.4.0") - implementation("androidx.core:core-ktx:1.7.0") - implementation("androidx.recyclerview:recyclerview:1.2.1") - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") - implementation("androidx.cardview:cardview:1.0.0") - implementation("androidx.multidex:multidex:2.0.1") - testImplementation("androidx.test:core:1.4.0") - testImplementation("androidx.arch.core:core-testing:2.1.0") - androidTestImplementation("androidx.test.ext:junit:1.1.3") - androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") - - // Kotlin - implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.5.31")) - implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") - - implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") - - dokkaHtmlPlugin("org.jetbrains.dokka:kotlin-as-java-plugin:1.5.30") - - // Test - testImplementation("junit:junit:4.13.2") - testImplementation("org.robolectric:robolectric:4.7.2") - testImplementation("io.mockk:mockk:1.12.1") - - // Koin - val koinVersion = "3.1.4" - implementation("io.insert-koin:koin-android:$koinVersion") - implementation("io.insert-koin:koin-android-compat:$koinVersion") - testImplementation("io.insert-koin:koin-test:$koinVersion") - - // Retrofit - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0") - val okhttpVersion = "4.9.3" - implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") - implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") - - // Room - val roomVersion = "2.3.0" - implementation("androidx.room:room-runtime:$roomVersion") - kapt("androidx.room:room-compiler:$roomVersion") - implementation("androidx.room:room-ktx:$roomVersion") - - // Navigation component - val navigationComponentVersion = "2.3.5" - implementation("androidx.navigation:navigation-fragment-ktx:$navigationComponentVersion") - implementation("androidx.navigation:navigation-ui-ktx:$navigationComponentVersion") - implementation("androidx.navigation:navigation-dynamic-features-fragment:$navigationComponentVersion") - androidTestImplementation("androidx.navigation:navigation-testing:$navigationComponentVersion") - - // timber - implementation("com.jakewharton.timber:timber:5.0.1") - - // picasso - implementation("com.squareup.picasso:picasso:2.71828") - - // chucker - val chuckerVersion = "3.5.2" - debugImplementation("com.github.chuckerteam.chucker:library:$chuckerVersion") - releaseImplementation("com.github.chuckerteam.chucker:library-no-op:$chuckerVersion") - - // leakcanary - debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7") - - // Startup - implementation("androidx.startup:startup-runtime:1.1.0") -} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/app/src/debug/res/xml/network_security_config.xml b/app/src/debug/res/xml/network_security_config.xml deleted file mode 100644 index f17a19f..0000000 --- a/app/src/debug/res/xml/network_security_config.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index 576c956..0000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png deleted file mode 100644 index 46f3584..0000000 Binary files a/app/src/main/ic_launcher-web.png and /dev/null differ diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/RickAndMortyApplication.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/RickAndMortyApplication.kt deleted file mode 100644 index 682f6a1..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/RickAndMortyApplication.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mohsenoid.rickandmorty - -import androidx.multidex.MultiDexApplication -import com.mohsenoid.rickandmorty.data.dataModule -import com.mohsenoid.rickandmorty.util.KoinQualifiersNames -import org.koin.android.ext.koin.androidContext -import org.koin.android.ext.koin.androidLogger -import org.koin.core.component.KoinComponent -import org.koin.core.context.startKoin - -class RickAndMortyApplication : MultiDexApplication(), KoinComponent { - - private val isDebug: Boolean = BuildConfig.DEBUG - - override fun onCreate() { - super.onCreate() - - startKoin { - val appProperties: Map = mapOf( - KoinQualifiersNames.BASE_URL to BuildConfig.BASE_URL, - ) - properties(appProperties) - - if (isDebug) androidLogger() - - androidContext(this@RickAndMortyApplication) - modules(appModule + dataModule) - } - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/RepositoryImpl.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/RepositoryImpl.kt deleted file mode 100644 index 0adec23..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/RepositoryImpl.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.mohsenoid.rickandmorty.data - -import com.mohsenoid.rickandmorty.data.api.ApiRickAndMorty -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisode -import com.mohsenoid.rickandmorty.data.api.model.ApiResult -import com.mohsenoid.rickandmorty.data.db.DbRickAndMorty -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.data.mapper.toDbCharacter -import com.mohsenoid.rickandmorty.data.mapper.toDbEpisode -import com.mohsenoid.rickandmorty.data.mapper.toModelCharacter -import com.mohsenoid.rickandmorty.data.mapper.toModelEpisode -import com.mohsenoid.rickandmorty.domain.Repository -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode -import com.mohsenoid.rickandmorty.domain.model.PageQueryResult -import com.mohsenoid.rickandmorty.domain.model.QueryResult -import com.mohsenoid.rickandmorty.util.StatusProvider - -@Suppress("TooManyFunctions") -class RepositoryImpl internal constructor( - private val db: DbRickAndMorty, - private val api: ApiRickAndMorty, - private val statusProvider: StatusProvider, -) : Repository { - - override suspend fun getEpisodes(page: Int): PageQueryResult> { - if (statusProvider.isOnline()) { - val result = fetchNetworkEpisodes(page) - if (result is PageQueryResult.Successful) cacheNetworkEpisodes(result.data) - } - - return queryDbEpisodes(page, PAGE_SIZE) - } - - private suspend fun fetchNetworkEpisodes(page: Int): PageQueryResult> = - when (val result = api.fetchEpisodes(page)) { - is ApiResult.Success -> PageQueryResult.Successful(result.data.results) - is ApiResult.Error.ServerError, - is ApiResult.Error.UnknownError, - -> PageQueryResult.Error - } - - private suspend fun cacheNetworkEpisodes(episodes: List) { - episodes.map(ApiEpisode::toDbEpisode) - .forEach { db.insertEpisode(it) } - } - - private suspend fun queryDbEpisodes( - page: Int, - pageSize: Int, - ): PageQueryResult> { - val dbEntityEpisodes: List = db.queryAllEpisodesByPage(page, pageSize) - val episodes: List = dbEntityEpisodes.map(DbEpisode::toModelEpisode) - - if (episodes.isEmpty()) return PageQueryResult.EndOfList - - return PageQueryResult.Successful(episodes) - } - - override suspend fun getCharactersByIds( - characterIds: List, - ): QueryResult> { - if (statusProvider.isOnline()) { - val result = fetchNetworkCharactersByIds(characterIds) - if (result is QueryResult.Successful) cacheNetworkCharacters(result.data) - } - - return queryDbCharactersByIds(characterIds) - } - - private suspend fun fetchNetworkCharactersByIds( - characterIds: List, - ): QueryResult> = - when (val result = api.fetchCharactersByIds(characterIds.joinToString(","))) { - is ApiResult.Success -> QueryResult.Successful(result.data) - is ApiResult.Error.ServerError, - is ApiResult.Error.UnknownError, - -> QueryResult.Error - } - - private suspend fun cacheNetworkCharacters(characters: List) { - characters.map(ApiCharacter::toDbCharacter) - .forEach { db.insertOrUpdateCharacter(it) } - } - - private suspend fun queryDbCharactersByIds( - characterIds: List, - ): QueryResult> { - val dbCharacters: List = db.queryCharactersByIds(characterIds) - - val characters: List = dbCharacters.map(DbCharacter::toModelCharacter) - - if (characters.isEmpty()) QueryResult.NoCache - - return QueryResult.Successful(characters) - } - - override suspend fun getCharacterDetails(characterId: Int): QueryResult { - if (statusProvider.isOnline()) { - val result = fetchNetworkCharacterDetails(characterId) - if (result is QueryResult.Successful) cacheNetworkCharacter(result.data) - } - - return queryDbCharacterDetails(characterId) - } - - private suspend fun fetchNetworkCharacterDetails(characterId: Int): QueryResult = - when (val result = api.fetchCharacterDetails(characterId)) { - is ApiResult.Success -> QueryResult.Successful(result.data) - is ApiResult.Error.ServerError, - is ApiResult.Error.UnknownError, - -> QueryResult.Error - } - - private suspend fun cacheNetworkCharacter(character: ApiCharacter) { - db.insertOrUpdateCharacter(character.toDbCharacter()) - } - - private suspend fun queryDbCharacterDetails(characterId: Int): QueryResult { - val dbCharacter: DbCharacter = - db.queryCharacter(characterId) ?: return QueryResult.NoCache - - return QueryResult.Successful(dbCharacter.toModelCharacter()) - } - - override suspend fun killCharacter(characterId: Int): QueryResult { - db.killCharacter(characterId) - return getCharacterDetails(characterId) - } - - companion object { - const val PAGE_SIZE: Int = 20 - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/ApiRickAndMorty.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/ApiRickAndMorty.kt deleted file mode 100644 index 3f9abcc..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/ApiRickAndMorty.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api - -import android.content.Context -import com.chuckerteam.chucker.api.ChuckerCollector -import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import com.mohsenoid.rickandmorty.BuildConfig -import com.mohsenoid.rickandmorty.data.api.mapper.ApiResultMapper.toApiResult -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisodes -import com.mohsenoid.rickandmorty.data.api.model.ApiResult -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query -import java.net.UnknownHostException - -@OptIn(ExperimentalSerializationApi::class) -internal class ApiRickAndMorty(applicationContext: Context, baseUrl: String) { - - private val api = ApiClient.create(applicationContext, baseUrl) - - suspend fun fetchEpisodes(page: Int): ApiResult = - toApiResult { - api.fetchEpisodes(page) - } - - suspend fun fetchCharactersByIds(characterIds: String): ApiResult> = - toApiResult { - api.fetchCharactersByIds(characterIds) - } - - suspend fun fetchCharacterDetails(characterId: Int): ApiResult = - toApiResult { - api.fetchCharacterDetails(characterId) - } - - internal interface ApiClient { - - @GET(value = EPISODE_ENDPOINT) - suspend fun fetchEpisodes( - @Query(value = PARAM_KEY_PAGE) - page: Int, - ): Response - - @GET(value = "$CHARACTER_ENDPOINT{$PATH_KEY_CHARACTER_IDS}") - suspend fun fetchCharactersByIds( - @Path(value = PATH_KEY_CHARACTER_IDS) - characterIds: String, - ): Response> - - @GET(value = "$CHARACTER_ENDPOINT{$PATH_KEY_CHARACTER_ID}") - suspend fun fetchCharacterDetails( - @Path(value = PATH_KEY_CHARACTER_ID) - characterId: Int, - ): Response - - companion object { - private const val EPISODE_ENDPOINT: String = "episode/" - private const val CHARACTER_ENDPOINT: String = "character/" - - private const val PARAM_KEY_PAGE: String = "page" - - private const val PATH_KEY_CHARACTER_IDS: String = "characterIds" - private const val PATH_KEY_CHARACTER_ID: String = "characterId" - - internal fun create(applicationContext: Context, baseUrl: String): ApiClient { - val httpUrl = baseUrl.toHttpUrlOrNull() - ?: throw UnknownHostException("Invalid host: $baseUrl") - - val contentType = "application/json".toMediaType() - val converterFactory = Json.asConverterFactory(contentType) - - val loggerInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - - val chuckerInterceptor = ChuckerInterceptor.Builder(applicationContext) - .collector(ChuckerCollector(applicationContext)) - .build() - - val okHttpClient = OkHttpClient.Builder().apply { - val isDebug: Boolean = BuildConfig.DEBUG - if (isDebug) { - addInterceptor(loggerInterceptor) - } - addInterceptor(chuckerInterceptor) - }.build() - - val retrofit = Retrofit.Builder() - .baseUrl(httpUrl) - .addConverterFactory(converterFactory) - .client(okHttpClient) - .build() - - return retrofit.create(ApiClient::class.java) - } - } - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/mapper/ApiResultMapper.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/mapper/ApiResultMapper.kt deleted file mode 100644 index b9f8867..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/mapper/ApiResultMapper.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.mapper - -import com.mohsenoid.rickandmorty.data.api.model.ApiResult -import retrofit2.Response -import timber.log.Timber -import java.net.UnknownHostException - -object ApiResultMapper { - - private const val HTTP_CODE_400_BAD_REQUEST = 400 - private const val HTTP_CODE_401_UNAUTHORIZED = 401 - private const val HTTP_CODE_403_FORBIDDEN = 403 - private const val HTTP_CODE_503_SERVICE_UNAVAILABLE = 503 - - suspend inline fun toApiResult( - crossinline apiCall: suspend () -> Response, - ): ApiResult = kotlin.runCatching { - val response = apiCall() - if (response.isSuccessful) { - handleSuccessfulApiResponse(response) - } else { - handleErrorApiResponse(response) - } - }.getOrElse { throwable -> - Timber.e(throwable) - - when (throwable) { - is UnknownHostException -> ApiResult.Error.ServerError( - ApiResult.Error.ServerError.Reason.SERVER_UNREACHABLE, - ) - else -> ApiResult.Error.UnknownError(throwable) - } - } - - inline fun handleSuccessfulApiResponse(response: Response): ApiResult { - val body = response.body() - return when { - body != null -> ApiResult.Success(body) - else -> ApiResult.Error.UnknownError( - exception = IllegalStateException("response body is null"), - ) - } - } - - fun handleErrorApiResponse(response: Response<*>): ApiResult.Error.ServerError { - val reason = when (response.code()) { - HTTP_CODE_400_BAD_REQUEST -> ApiResult.Error.ServerError.Reason.BAD_REQUEST - HTTP_CODE_401_UNAUTHORIZED -> ApiResult.Error.ServerError.Reason.UNAUTHORIZED - HTTP_CODE_403_FORBIDDEN -> ApiResult.Error.ServerError.Reason.FORBIDDEN - HTTP_CODE_503_SERVICE_UNAVAILABLE -> ApiResult.Error.ServerError.Reason.SERVICE_UNAVAILABLE - else -> ApiResult.Error.ServerError.Reason.UNKNOWN - } - return ApiResult.Error.ServerError(reason = reason) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiCharacter.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiCharacter.kt deleted file mode 100644 index d2a89c1..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiCharacter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiCharacter( - - @SerialName(value = "id") - val id: Int, - - @SerialName(value = "name") - val name: String, - - @SerialName(value = "status") - val status: String, - - @SerialName(value = "species") - val species: String, - - @SerialName(value = "type") - val type: String, - - @SerialName(value = "gender") - val gender: String, - - @SerialName(value = "origin") - val origin: ApiOrigin, - - @SerialName(value = "location") - val location: ApiLocation, - - @SerialName(value = "image") - val image: String, - - @SerialName(value = "episode") - val episodes: List, - - @SerialName(value = "url") - val url: String, - - @SerialName(value = "created") - val created: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiEpisode.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiEpisode.kt deleted file mode 100644 index b84f052..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiEpisode.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiEpisode( - - @SerialName(value = "id") - val id: Int, - - @SerialName(value = "name") - val name: String, - - @SerialName(value = "air_date") - val airDate: String, - - @SerialName(value = "episode") - val episode: String, - - @SerialName(value = "characters") - val characters: List, - - @SerialName(value = "url") - val url: String, - - @SerialName(value = "created") - val created: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiEpisodes.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiEpisodes.kt deleted file mode 100644 index fef9b1e..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiEpisodes.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiEpisodes( - - @SerialName(value = "info") - val info: ApiInfo, - - @SerialName(value = "results") - val results: List, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiErrorResponse.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiErrorResponse.kt deleted file mode 100644 index ad9ce61..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiErrorResponse.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName - -data class ApiErrorResponse( - - @SerialName(value = "error") - val error: String? = null, - - @SerialName(value = "message") - val message: String? = null, - - @SerialName(value = "orgMessage") - val orgMessage: String? = null, - - @SerialName(value = "httpStatus") - val httpStatus: Int? = null, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiInfo.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiInfo.kt deleted file mode 100644 index b2e099a..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiInfo.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiInfo( - - @SerialName(value = "count") - val count: Int, - - @SerialName(value = "pages") - val pages: Int, - - @SerialName(value = "next") - val next: String?, - - @SerialName(value = "prev") - val prev: String?, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiLocation.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiLocation.kt deleted file mode 100644 index c2e369a..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiLocation.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiLocation( - - @SerialName(value = "name") - val name: String, - - @SerialName(value = "url") - val url: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiOrigin.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiOrigin.kt deleted file mode 100644 index 7602f13..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiOrigin.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiOrigin( - - @SerialName(value = "name") - val name: String, - - @SerialName(value = "url") - val url: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiResult.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiResult.kt deleted file mode 100644 index 2a2c3d7..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/model/ApiResult.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api.model - -sealed class ApiResult { - - data class Success(val data: R) : ApiResult() - - sealed class Error : ApiResult() { - - data class ServerError(val reason: Reason) : Error() { - - enum class Reason { - UNAUTHORIZED, - BAD_REQUEST, - FORBIDDEN, - SERVICE_UNAVAILABLE, - SERVER_UNREACHABLE, - UNKNOWN - } - } - - data class UnknownError(val exception: Throwable) : Error() - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/module.kt deleted file mode 100644 index 8705d1f..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/api/module.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mohsenoid.rickandmorty.data.api - -import com.mohsenoid.rickandmorty.util.KoinQualifiersNames -import kotlinx.serialization.ExperimentalSerializationApi -import org.koin.android.ext.koin.androidApplication -import org.koin.dsl.module - -@OptIn(ExperimentalSerializationApi::class) -val apiModule = module { - - single { - ApiRickAndMorty( - applicationContext = androidApplication(), - baseUrl = getProperty(KoinQualifiersNames.BASE_URL), - ) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/DbRickAndMorty.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/DbRickAndMorty.kt deleted file mode 100644 index 1b1c66d..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/DbRickAndMorty.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.mohsenoid.rickandmorty.data.db.converters.DbTypeConverters -import com.mohsenoid.rickandmorty.data.db.dao.DbCharacterDaoAbs -import com.mohsenoid.rickandmorty.data.db.dao.DbEpisodeDao -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.data.db.model.DbLocation -import com.mohsenoid.rickandmorty.data.db.model.DbOrigin - -internal class DbRickAndMorty(context: Context) { - - private val db = Db.create(context) - - suspend fun insertEpisode(entityEpisode: DbEpisode) = - db.episodeDao.insertEpisode(entityEpisode) - - suspend fun queryAllEpisodes(): List = - db.episodeDao.queryAllEpisodes() - - suspend fun queryAllEpisodesByPage(page: Int, pageSize: Int): List = - db.episodeDao.queryAllEpisodesByPage(page, pageSize) - - suspend fun insertCharacter(character: DbCharacter) = - db.characterDao.insertCharacter(character) - - suspend fun queryCharactersByIds(characterIds: List): List = - db.characterDao.queryCharactersByIds(characterIds) - - suspend fun queryCharacter(characterId: Int): DbCharacter? = - db.characterDao.queryCharacter(characterId) - - suspend fun killCharacter(characterId: Int) = - db.characterDao.killCharacter(characterId) - - suspend fun insertOrUpdateCharacter(character: DbCharacter) = - db.characterDao.insertOrUpdateCharacter(character) - - @Database( - entities = [ - DbCharacter::class, - DbEpisode::class, - DbLocation::class, - DbOrigin::class, - ], - version = DATABASE_VERSION, - ) - @TypeConverters(DbTypeConverters::class) - internal abstract class Db : RoomDatabase() { - - abstract val episodeDao: DbEpisodeDao - - abstract val characterDao: DbCharacterDaoAbs - - companion object { - fun create(context: Context): Db { - return Room.databaseBuilder( - context, - Db::class.java, - DATABASE_NAME, - ) - .fallbackToDestructiveMigration() - .build() - } - } - } - - companion object { - private const val DATABASE_NAME: String = "rickandmorty.db" - private const val DATABASE_VERSION: Int = 5 - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/converters/DbTypeConverters.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/converters/DbTypeConverters.kt deleted file mode 100644 index 0a6011f..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/converters/DbTypeConverters.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.converters - -import androidx.room.TypeConverter -import com.mohsenoid.rickandmorty.data.db.model.DbLocation -import com.mohsenoid.rickandmorty.data.db.model.DbOrigin -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.json.Json - -internal class DbTypeConverters { - - @TypeConverter - fun locationToJson(location: DbLocation?): String? { - return location?.let { Json.encodeToString(DbLocation.serializer(), it) } - } - - @TypeConverter - fun jsonToLocation(locationJson: String?): DbLocation? { - return locationJson?.let { Json.decodeFromString(DbLocation.serializer(), it) } - } - - @TypeConverter - fun originToJson(origin: DbOrigin?): String? { - return origin?.let { Json.encodeToString(DbOrigin.serializer(), it) } - } - - @TypeConverter - fun jsonToOrigin(originJson: String?): DbOrigin? { - return originJson?.let { Json.decodeFromString(DbOrigin.serializer(), it) } - } - - @TypeConverter - fun listOfIntToString(list: List?): String? { - return list?.let { Json.encodeToString(ListSerializer(Int.serializer()), it) } - } - - @TypeConverter - fun stringToListOfInt(string: String?): List? { - return string?.let { Json.decodeFromString(ListSerializer(Int.serializer()), it) } - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbCharacterDao.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbCharacterDao.kt deleted file mode 100644 index 0903eb5..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbCharacterDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.dao - -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter - -internal interface DbCharacterDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCharacter(character: DbCharacter) - - @Query(value = "SELECT * FROM characters WHERE _id IN (:characterIds)") - suspend fun queryCharactersByIds(characterIds: List): List - - @Query(value = "SELECT * FROM characters WHERE _id = :characterId LIMIT 1") - suspend fun queryCharacter(characterId: Int): DbCharacter? - - @Query(value = "UPDATE characters SET is_killed_by_user = 1 WHERE _id = :characterId") - suspend fun killCharacter(characterId: Int) - - suspend fun insertOrUpdateCharacter(character: DbCharacter) -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbCharacterDaoAbs.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbCharacterDaoAbs.kt deleted file mode 100644 index 20fdfcc..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbCharacterDaoAbs.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.dao - -import androidx.room.Dao -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter - -@Dao -internal abstract class DbCharacterDaoAbs : DbCharacterDao { - - override suspend fun insertOrUpdateCharacter(character: DbCharacter) { - val oldCharacter: DbCharacter? = queryCharacter(characterId = character.id) - - insertCharacter( - character = if (oldCharacter != null) { - character.copy(isKilledByUser = oldCharacter.isKilledByUser) - } else { - character - }, - ) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbEpisodeDao.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbEpisodeDao.kt deleted file mode 100644 index 2c6034a..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/dao/DbEpisodeDao.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode - -@Dao -internal interface DbEpisodeDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertEpisode(entityEpisode: DbEpisode) - - @Query(value = "SELECT * FROM episodes") - suspend fun queryAllEpisodes(): List - - @Query(value = "SELECT * FROM episodes LIMIT :pageSize OFFSET (:page - 1) * :pageSize") - suspend fun queryAllEpisodesByPage(page: Int, pageSize: Int): List -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbCharacter.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbCharacter.kt deleted file mode 100644 index bb2bb75..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbCharacter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.serialization.Serializable - -@Serializable -@Entity(tableName = "characters") -internal data class DbCharacter( - - @PrimaryKey - @ColumnInfo(name = "_id") - val id: Int, - - @ColumnInfo(name = "name") - val name: String, - - @ColumnInfo(name = "status") - val status: String, - - @ColumnInfo(name = "is_alive") - val isAlive: Boolean, - - @ColumnInfo(name = "species") - val species: String, - - @ColumnInfo(name = "type") - val type: String, - - @ColumnInfo(name = "gender") - val gender: String, - - @ColumnInfo(name = "origin") - val origin: DbOrigin, - - @ColumnInfo(name = "location") - val location: DbLocation, - - @ColumnInfo(name = "image") - val image: String, - - @ColumnInfo(name = "episode_ids") - val episodeIds: List, - - @ColumnInfo(name = "url") - val url: String, - - @ColumnInfo(name = "created") - val created: String, - - @ColumnInfo(name = "is_killed_by_user", defaultValue = "0") - val isKilledByUser: Boolean, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbEpisode.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbEpisode.kt deleted file mode 100644 index b0c18ab..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbEpisode.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.serialization.Serializable - -@Serializable -@Entity(tableName = "episodes") -internal data class DbEpisode( - - @PrimaryKey - @ColumnInfo(name = "_id") - val id: Int, - - @ColumnInfo(name = "name") - val name: String, - - @ColumnInfo(name = "air_date") - val airDate: String, - - @ColumnInfo(name = "episode") - val episode: String, - - @ColumnInfo(name = "character_ids") - val characterIds: List, - - @ColumnInfo(name = "url") - val url: String, - - @ColumnInfo(name = "created") - val created: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbLocation.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbLocation.kt deleted file mode 100644 index b020611..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbLocation.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.serialization.Serializable - -@Serializable -@Entity(tableName = "locations") -internal data class DbLocation( - - @PrimaryKey - @ColumnInfo(name = "name") - val name: String, - - @ColumnInfo(name = "url") - val url: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbOrigin.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbOrigin.kt deleted file mode 100644 index 7ea1d67..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/model/DbOrigin.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.serialization.Serializable - -@Serializable -@Entity(tableName = "origins") -internal data class DbOrigin( - - @PrimaryKey - @ColumnInfo(name = "name") - val name: String, - - @ColumnInfo(name = "url") - val url: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/module.kt deleted file mode 100644 index 87e4c0a..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/db/module.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db - -import org.koin.core.annotation.KoinReflectAPI -import org.koin.dsl.module -import org.koin.dsl.single - -@OptIn(KoinReflectAPI::class) -val dbModule = module { - single() -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/mapper/CharacterMapper.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/mapper/CharacterMapper.kt deleted file mode 100644 index 52a5a43..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/mapper/CharacterMapper.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.mohsenoid.rickandmorty.data.mapper - -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiLocation -import com.mohsenoid.rickandmorty.data.api.model.ApiOrigin -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.data.db.model.DbLocation -import com.mohsenoid.rickandmorty.data.db.model.DbOrigin -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter -import com.mohsenoid.rickandmorty.domain.model.ModelLocation -import com.mohsenoid.rickandmorty.domain.model.ModelOrigin - -private const val DEAD: String = "dead" - -private const val SEPARATOR: Char = '/' - -internal fun extractEpisodeIds(episodes: List): List { - return episodes.map { it.split(SEPARATOR).last().toInt() } -} - -internal fun ApiCharacter.toDbCharacter() = - DbCharacter( - id = id, - name = name, - status = status, - isAlive = !status.equals(DEAD, ignoreCase = true), - species = species, - type = type, - gender = gender, - origin = origin.toDbOrigin(), - location = location.toDbLocation(), - image = image, - episodeIds = extractEpisodeIds(episodes), - url = url, - created = created, - isKilledByUser = false, - ) - -private fun ApiOrigin.toDbOrigin() = - DbOrigin( - name = name, - url = url, - ) - -private fun ApiLocation.toDbLocation() = - DbLocation( - name = name, - url = url, - ) - -internal fun DbCharacter.toModelCharacter() = - ModelCharacter( - id = id, - name = name, - status = status, - isAlive = isAlive, - species = species, - type = type, - gender = gender, - origin = origin.toModelOrigin(), - location = location.toModelLocation(), - imageUrl = image, - episodeIds = episodeIds, - url = url, - created = created, - isKilledByUser = isKilledByUser, - ) - -private fun DbOrigin.toModelOrigin() = - ModelOrigin( - name = name, - url = url, - ) - -private fun DbLocation.toModelLocation() = - ModelLocation( - name = name, - url = url, - ) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/mapper/EpisodeMapper.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/mapper/EpisodeMapper.kt deleted file mode 100644 index 1c3fe3b..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/mapper/EpisodeMapper.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.mohsenoid.rickandmorty.data.mapper - -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisode -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode - -private const val SEPARATOR: Char = '/' - -internal fun extractCharacterIds(characters: List): List { - return characters.map { it.split(SEPARATOR).last().toInt() } -} - -internal fun ApiEpisode.toDbEpisode() = - DbEpisode( - id = id, - name = name, - airDate = airDate, - episode = episode, - characterIds = extractCharacterIds(characters), - url = url, - created = created, - ) - -internal fun DbEpisode.toModelEpisode() = - ModelEpisode( - id = id, - name = name, - airDate = airDate, - episode = episode, - characterIds = characterIds, - url = url, - created = created, - ) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/module.kt deleted file mode 100644 index a07587e..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/data/module.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mohsenoid.rickandmorty.data - -import com.mohsenoid.rickandmorty.data.api.apiModule -import com.mohsenoid.rickandmorty.data.db.dbModule -import com.mohsenoid.rickandmorty.domain.Repository -import org.koin.dsl.module - -val dataModule = dbModule + apiModule + module { - - single { - RepositoryImpl( - db = get(), - api = get(), - statusProvider = get(), - ) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/Repository.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/Repository.kt deleted file mode 100644 index e92aaaa..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/Repository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mohsenoid.rickandmorty.domain - -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode -import com.mohsenoid.rickandmorty.domain.model.PageQueryResult -import com.mohsenoid.rickandmorty.domain.model.QueryResult - -interface Repository { - - suspend fun getEpisodes(page: Int): PageQueryResult> - - suspend fun getCharactersByIds(characterIds: List): QueryResult> - - suspend fun getCharacterDetails(characterId: Int): QueryResult - - suspend fun killCharacter(characterId: Int): QueryResult -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelCharacter.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelCharacter.kt deleted file mode 100644 index 06d7ed2..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelCharacter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.mohsenoid.rickandmorty.domain.model - -data class ModelCharacter( - val id: Int, - val name: String, - val status: String, - val isAlive: Boolean, - val species: String, - val type: String, - val gender: String, - val origin: ModelOrigin, - val location: ModelLocation, - val imageUrl: String, - val episodeIds: List, - val url: String, - val created: String, - val isKilledByUser: Boolean, -) { - - val isAliveAndNotKilledByUser: Boolean - get() = isAlive && !isKilledByUser -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelEpisode.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelEpisode.kt deleted file mode 100644 index 70a3140..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelEpisode.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.mohsenoid.rickandmorty.domain.model - -data class ModelEpisode( - val id: Int, - val name: String, - val airDate: String, - val episode: String, - val characterIds: List, - val url: String, - val created: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelLocation.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelLocation.kt deleted file mode 100644 index f01deab..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelLocation.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mohsenoid.rickandmorty.domain.model - -data class ModelLocation( - val name: String, - val url: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelOrigin.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelOrigin.kt deleted file mode 100644 index ed5fb7d..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/ModelOrigin.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mohsenoid.rickandmorty.domain.model - -data class ModelOrigin( - val name: String, - val url: String, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/PageQueryResult.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/PageQueryResult.kt deleted file mode 100644 index 315d28a..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/PageQueryResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mohsenoid.rickandmorty.domain.model - -sealed interface PageQueryResult { - data class Successful(val data: T) : PageQueryResult - object EndOfList : PageQueryResult - object Error : PageQueryResult -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/QueryResult.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/QueryResult.kt deleted file mode 100644 index 3afbc60..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/domain/model/QueryResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mohsenoid.rickandmorty.domain.model - -sealed interface QueryResult { - data class Successful(val data: T) : QueryResult - object NoCache : QueryResult - object Error : QueryResult -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/module.kt deleted file mode 100644 index 94d307f..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/module.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mohsenoid.rickandmorty - -import com.mohsenoid.rickandmorty.util.StatusProvider -import com.mohsenoid.rickandmorty.util.StatusProviderImpl -import org.koin.android.ext.koin.androidContext -import org.koin.dsl.module - -val appModule = module { - single { StatusProviderImpl(context = androidContext()) } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/startup/TimberInitializer.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/startup/TimberInitializer.kt deleted file mode 100644 index 1927ffe..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/startup/TimberInitializer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.mohsenoid.rickandmorty.startup - -import android.content.Context -import androidx.startup.Initializer -import com.mohsenoid.rickandmorty.BuildConfig -import timber.log.Timber - -class TimberInitializer : Initializer { - - private val isDebug: Boolean = BuildConfig.DEBUG - - override fun create(context: Context) { - if (isDebug) setupTimber() - } - - private fun setupTimber() { - Timber.plant( - object : Timber.DebugTree() { - override fun createStackElementTag(element: StackTraceElement): String { - // adding file name and line number link to logs - return "${super.createStackElementTag(element)}(${element.fileName}:${element.lineNumber})" - } - }, - ) - } - - override fun dependencies(): MutableList>> = mutableListOf() -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/KoinQualifiersNames.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/KoinQualifiersNames.kt deleted file mode 100644 index 4c447e2..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/KoinQualifiersNames.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.mohsenoid.rickandmorty.util - -object KoinQualifiersNames { - const val BASE_URL = "baseUrl" -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/StatusProvider.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/StatusProvider.kt deleted file mode 100644 index 4ed9e8f..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/StatusProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.mohsenoid.rickandmorty.util - -/** - * Application Status Provider. - */ -interface StatusProvider { - - /** - * Checks network connectivity status. - */ - fun isOnline(): Boolean -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/StatusProviderImpl.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/StatusProviderImpl.kt deleted file mode 100644 index 13d33cb..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/util/StatusProviderImpl.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.mohsenoid.rickandmorty.util - -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities - -/** - * - * This class implements [StatusProvider] and provides configurations required on runtime. - * - * @constructor Creates a ConfigProvider using the Android Application context. - * @param context the Android Application context. - */ -class StatusProviderImpl(private val context: Context) : StatusProvider { - - /** - * This function uses [ConnectivityManager] to check active Network Capabilities to know - * the Android phone network connectivity status. - * - * @return is the phone connected to a network through cellular or WiFi. - */ - @Suppress("ReturnCount") - override fun isOnline(): Boolean { - val connectivityManager: ConnectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager - ?: return false - - val activeNetwork: Network = connectivityManager.activeNetwork ?: return false - - val networkCapabilities: NetworkCapabilities = - connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false - - return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/MainActivity.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/MainActivity.kt deleted file mode 100644 index fa454c7..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/MainActivity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mohsenoid.rickandmorty.view - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import com.mohsenoid.rickandmorty.R - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/CharacterDetailsFragment.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/CharacterDetailsFragment.kt deleted file mode 100644 index 66b474c..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/CharacterDetailsFragment.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.details - -import android.app.Activity -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.navArgs -import com.mohsenoid.rickandmorty.R -import com.mohsenoid.rickandmorty.databinding.FragmentCharacterDetailsBinding -import com.mohsenoid.rickandmorty.view.util.launchWhileResumed -import kotlinx.coroutines.flow.collectLatest -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.context.loadKoinModules -import org.koin.core.context.unloadKoinModules -import org.koin.core.parameter.parametersOf - -@Suppress("TooManyFunctions") -class CharacterDetailsFragment : Fragment() { - - private var _binding: FragmentCharacterDetailsBinding? = null - private val binding get() = _binding!! - - private val args: CharacterDetailsFragmentArgs by navArgs() - - private val viewModel: CharacterDetailsViewModel by viewModel { - parametersOf(args.characterId) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - loadKoinModules(characterDetailsFragmentModule) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentCharacterDetailsBinding.inflate(inflater, container, false) - binding.lifecycleOwner = viewLifecycleOwner - binding.viewModel = viewModel - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - lifecycle.launchWhileResumed { - viewModel.onError.collectLatest { - if (it) showErrorMessage() - } - } - - lifecycle.launchWhileResumed { - viewModel.isOffline.collectLatest { - if (it) showOfflineMessage() - } - } - - lifecycle.launchWhileResumed { - viewModel.isNoCache.collectLatest { - if (it) onNoOfflineData() - } - } - } - - private fun showErrorMessage() { - Toast.makeText(context, R.string.error_message, Toast.LENGTH_LONG).show() - } - - private fun showOfflineMessage() { - Toast.makeText(context, R.string.offline_app, Toast.LENGTH_LONG).show() - } - - private fun onNoOfflineData() { - Toast.makeText(context, R.string.no_offline_data, Toast.LENGTH_LONG).show() - parentActivityOnBackPressed() - } - - private fun parentActivityOnBackPressed() { - val parentActivity: Activity? = activity - parentActivity?.onBackPressed() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onDestroy() { - unloadKoinModules(characterDetailsFragmentModule) - super.onDestroy() - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/CharacterDetailsViewModel.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/CharacterDetailsViewModel.kt deleted file mode 100644 index b748581..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/CharacterDetailsViewModel.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.details - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mohsenoid.rickandmorty.domain.Repository -import com.mohsenoid.rickandmorty.domain.model.QueryResult -import com.mohsenoid.rickandmorty.util.StatusProvider -import com.mohsenoid.rickandmorty.view.mapper.toViewCharacterDetails -import com.mohsenoid.rickandmorty.view.model.LoadingState -import com.mohsenoid.rickandmorty.view.model.ViewCharacterDetails -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch - -class CharacterDetailsViewModel( - private val characterId: Int, - private val repository: Repository, - private val statusProvider: StatusProvider, -) : ViewModel() { - - private val _loadingState: MutableStateFlow = MutableStateFlow(LoadingState.None) - val loadingState: StateFlow = _loadingState - - private val _isOffline: MutableStateFlow = MutableStateFlow(false) - val isOffline: StateFlow = _isOffline - - private val _isNoCache: MutableStateFlow = MutableStateFlow(false) - val isNoCache: StateFlow = _isNoCache - - private val _onError: MutableStateFlow = MutableStateFlow(false) - val onError: StateFlow = _onError - - private val _character: MutableStateFlow = MutableStateFlow(null) - val character: StateFlow = _character - - init { - loadCharacter() - } - - private fun loadCharacter() { - _loadingState.value = LoadingState.Loading - _onError.value = false - _isNoCache.value = false - _isOffline.value = false - queryCharacter() - } - - private fun queryCharacter() { - if (!statusProvider.isOnline()) { - _isOffline.value = true - } - - viewModelScope.launch { - when (val result = repository.getCharacterDetails(characterId)) { - is QueryResult.Successful -> _character.value = result.data.toViewCharacterDetails() - QueryResult.NoCache -> _isNoCache.value = true - QueryResult.Error -> _onError.value = true - } - - _loadingState.value = LoadingState.None - } - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/module.kt deleted file mode 100644 index fb983ec..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/details/module.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.details - -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val characterDetailsFragmentModule = module { - - viewModel { (characterId: Int) -> - CharacterDetailsViewModel( - characterId = characterId, - repository = get(), - statusProvider = get(), - ) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/CharacterListFragment.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/CharacterListFragment.kt deleted file mode 100644 index 83a8c61..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/CharacterListFragment.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.list - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.navigation.findNavController -import androidx.navigation.fragment.navArgs -import com.mohsenoid.rickandmorty.R -import com.mohsenoid.rickandmorty.databinding.FragmentCharacterListBinding -import com.mohsenoid.rickandmorty.view.util.launchWhileResumed -import kotlinx.coroutines.flow.collectLatest -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.context.loadKoinModules -import org.koin.core.context.unloadKoinModules -import org.koin.core.parameter.parametersOf - -@Suppress("TooManyFunctions") -class CharacterListFragment : Fragment() { - - private var _binding: FragmentCharacterListBinding? = null - private val binding get() = _binding!! - - private val args: CharacterListFragmentArgs by navArgs() - - private val viewModel: CharacterListViewModel by viewModel { - parametersOf(args.characterIds.toList()) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - loadKoinModules(characterListFragmentModule) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentCharacterListBinding.inflate(inflater, container, false) - binding.lifecycleOwner = viewLifecycleOwner - binding.viewModel = viewModel - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - lifecycle.launchWhileResumed { - viewModel.onError.collectLatest { - if (it) showErrorMessage() - } - } - - lifecycle.launchWhileResumed { - viewModel.isOffline.collectLatest { - if (it) showOfflineMessage() - } - } - - lifecycle.launchWhileResumed { - viewModel.isNoCache.collectLatest { - if (it) onNoOfflineData() - } - } - - lifecycle.launchWhileResumed { - viewModel.selectedCharacterId.collectLatest { characterId -> - onCharacterRowClick(characterId) - } - } - } - - private fun showErrorMessage() { - Toast.makeText(context, R.string.error_message, Toast.LENGTH_LONG).show() - } - - private fun showOfflineMessage() { - Toast.makeText(context, R.string.offline_app, Toast.LENGTH_LONG).show() - } - - private fun onNoOfflineData() { - Toast.makeText(context, R.string.no_offline_data, Toast.LENGTH_LONG).show() - activity?.onBackPressed() - } - - fun onCharacterRowClick(characterId: Int) { - val action = CharacterListFragmentDirections - .actionCharacterListFragmentToCharacterDetailsFragment(characterId) - view?.findNavController()?.navigate(action) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onDestroy() { - unloadKoinModules(characterListFragmentModule) - super.onDestroy() - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/CharacterListViewModel.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/CharacterListViewModel.kt deleted file mode 100644 index 72ec3b8..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/CharacterListViewModel.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.list - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mohsenoid.rickandmorty.domain.Repository -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter -import com.mohsenoid.rickandmorty.domain.model.QueryResult -import com.mohsenoid.rickandmorty.util.StatusProvider -import com.mohsenoid.rickandmorty.view.mapper.toViewCharacterItem -import com.mohsenoid.rickandmorty.view.model.LoadingState -import com.mohsenoid.rickandmorty.view.model.ViewCharacterItem -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -class CharacterListViewModel( - private val characterIds: List, - private val repository: Repository, - private val statusProvider: StatusProvider, -) : ViewModel() { - - private val _loadingState: MutableStateFlow = MutableStateFlow(LoadingState.None) - val loadingState: StateFlow = _loadingState - - private val _isOffline: MutableStateFlow = MutableStateFlow(false) - val isOffline: StateFlow = _isOffline - - private val _isNoCache: MutableStateFlow = MutableStateFlow(false) - val isNoCache: StateFlow = _isNoCache - - private val _onError: MutableStateFlow = MutableStateFlow(false) - val onError: StateFlow = _onError - - private val _characters: MutableStateFlow> = - MutableStateFlow(mutableListOf()) - val characters: StateFlow> = _characters - - private val _selectedCharacterId: Channel = Channel() - val selectedCharacterId: Flow = _selectedCharacterId.receiveAsFlow() - - init { - loadCharacters() - } - - private fun loadCharacters() { - _loadingState.value = LoadingState.Loading - _onError.value = false - _isNoCache.value = false - _isOffline.value = false - queryCharacters() - } - - private fun queryCharacters() { - if (!statusProvider.isOnline()) { - _isOffline.value = true - } - - viewModelScope.launch { - when (val result = repository.getCharactersByIds(characterIds)) { - is QueryResult.Successful -> _characters.value = result.data.toViewCharacterItems() - QueryResult.NoCache -> _isNoCache.value = true - QueryResult.Error -> _onError.value = true - } - - _loadingState.value = LoadingState.None - } - } - - private fun killCharacter(character: ModelCharacter) { - if (!character.isAliveAndNotKilledByUser) return - - if (!statusProvider.isOnline()) { - _isOffline.value = true - } - - viewModelScope.launch { - when (repository.killCharacter(character.id)) { - is QueryResult.Successful -> { - queryCharacters() - } - QueryResult.NoCache -> _isNoCache.value = true - QueryResult.Error -> _onError.value = true - } - } - } - - private fun List.toViewCharacterItems(): MutableList = - map { character -> - character.toViewCharacterItem( - onKill = { - killCharacter(character) - }, - onClick = { - _selectedCharacterId.trySend(character.id) - }, - ) - }.toMutableList() -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/adapter/CharacterListAdapter.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/adapter/CharacterListAdapter.kt deleted file mode 100644 index 4dc014d..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/adapter/CharacterListAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.list.adapter - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.mohsenoid.rickandmorty.databinding.ItemCharacterBinding -import com.mohsenoid.rickandmorty.view.model.ViewCharacterItem -import java.util.ArrayList - -class CharacterListAdapter : RecyclerView.Adapter() { - - private var characters: MutableList = ArrayList() - - @SuppressLint("NotifyDataSetChanged") - fun setCharacters(characters: List) { - this.characters = characters.toMutableList() - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { - val binding = - ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return CharacterViewHolder(binding) - } - - override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { - val character: ViewCharacterItem = characters[position] - holder.setCharacter(character) - } - - override fun getItemCount(): Int { - return characters.size - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/adapter/CharacterViewHolder.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/adapter/CharacterViewHolder.kt deleted file mode 100644 index 33556c1..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/adapter/CharacterViewHolder.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.list.adapter - -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.mohsenoid.rickandmorty.databinding.ItemCharacterBinding -import com.mohsenoid.rickandmorty.view.model.ViewCharacterItem - -class CharacterViewHolder internal constructor( - internal val binding: ItemCharacterBinding, -) : ViewHolder(binding.root) { - - fun setCharacter(character: ViewCharacterItem) { - binding.character = character - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/module.kt deleted file mode 100644 index e6a621c..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/character/list/module.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.mohsenoid.rickandmorty.view.character.list - -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val characterListFragmentModule = module { - - viewModel { (characterIds: List) -> - CharacterListViewModel( - characterIds = characterIds, - repository = get(), - statusProvider = get(), - ) - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/EpisodeListFragment.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/EpisodeListFragment.kt deleted file mode 100644 index 474a1e7..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/EpisodeListFragment.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.mohsenoid.rickandmorty.view.episode.list - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.navigation.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.mohsenoid.rickandmorty.R -import com.mohsenoid.rickandmorty.databinding.FragmentEpisodeListBinding -import com.mohsenoid.rickandmorty.view.util.EndlessRecyclerViewScrollListener -import com.mohsenoid.rickandmorty.view.util.launchWhileResumed -import kotlinx.coroutines.flow.collectLatest -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.context.loadKoinModules -import org.koin.core.context.unloadKoinModules - -@Suppress("TooManyFunctions") -class EpisodeListFragment : Fragment() { - - private var _binding: FragmentEpisodeListBinding? = null - private val binding get() = _binding!! - - private val viewModel: EpisodeListViewModel by viewModel() - - private var endlessScrollListener: EndlessRecyclerViewScrollListener? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - loadKoinModules(episodeListFragmentModule) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentEpisodeListBinding.inflate(inflater, container, false) - binding.lifecycleOwner = viewLifecycleOwner - binding.viewModel = viewModel - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val linearLayoutManager = LinearLayoutManager(context) - binding.episodeList.layoutManager = linearLayoutManager - endlessScrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) { - override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView) { - viewModel.loadMoreEpisodes(page = page + 1) - } - }.also { - binding.episodeList.addOnScrollListener(it) - } - - lifecycle.launchWhileResumed { - viewModel.onError.collectLatest { - if (it) showErrorMessage() - } - } - - lifecycle.launchWhileResumed { - viewModel.isOffline.collectLatest { - if (it) showOfflineMessage() - } - } - - lifecycle.launchWhileResumed { - viewModel.isEndOfList.collectLatest { - if (it) reachedEndOfList() - } - } - - lifecycle.launchWhileResumed { - viewModel.selectedEpisodeCharacterIds.collectLatest { characterIds -> - onEpisodeRowClick(characterIds) - } - } - } - - private fun showErrorMessage() { - Toast.makeText(context, R.string.error_message, Toast.LENGTH_LONG).show() - } - - private fun showOfflineMessage() { - Toast.makeText(context, R.string.offline_app, Toast.LENGTH_SHORT).show() - } - - private fun reachedEndOfList() { - binding.episodeListProgress.visibility = View.GONE - } - - private fun onEpisodeRowClick(characterIds: IntArray) { - val action = EpisodeListFragmentDirections - .actionEpisodeListFragmentToCharacterListFragment(characterIds) - view?.findNavController()?.navigate(action) - } - - override fun onDestroyView() { - super.onDestroyView() - endlessScrollListener = null - _binding = null - } - - override fun onDestroy() { - unloadKoinModules(episodeListFragmentModule) - super.onDestroy() - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/EpisodeListViewModel.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/EpisodeListViewModel.kt deleted file mode 100644 index f343f46..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/EpisodeListViewModel.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.mohsenoid.rickandmorty.view.episode.list - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mohsenoid.rickandmorty.domain.Repository -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode -import com.mohsenoid.rickandmorty.domain.model.PageQueryResult -import com.mohsenoid.rickandmorty.util.StatusProvider -import com.mohsenoid.rickandmorty.view.mapper.toViewEpisodeItem -import com.mohsenoid.rickandmorty.view.model.LoadingState -import com.mohsenoid.rickandmorty.view.model.ViewEpisodeItem -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -class EpisodeListViewModel( - private val repository: Repository, - private val statusProvider: StatusProvider, -) : ViewModel() { - - private val _loadingState: MutableStateFlow = MutableStateFlow(LoadingState.None) - val loadingState: StateFlow = _loadingState - - private val _isOffline: MutableStateFlow = MutableStateFlow(false) - val isOffline: StateFlow = _isOffline - - private val _isEndOfList: MutableStateFlow = MutableStateFlow(false) - val isEndOfList: StateFlow = _isEndOfList - - private val _onError: MutableStateFlow = MutableStateFlow(false) - val onError: StateFlow = _onError - - private val _episodes: MutableStateFlow> = MutableStateFlow(emptyList()) - val episodes: StateFlow> = _episodes - - private val _selectedEpisodeCharacterIds: Channel = Channel() - val selectedEpisodeCharacterIds: Flow = _selectedEpisodeCharacterIds.receiveAsFlow() - - private var page = 1 - - init { - loadEpisodes() - } - - fun loadEpisodes() { - _loadingState.value = LoadingState.Loading - _isEndOfList.value = false - _onError.value = false - _isOffline.value = false - page = 1 - getEpisodes() - } - - fun loadMoreEpisodes(page: Int) { - _loadingState.value = LoadingState.LoadingMore - this.page = page - getEpisodes() - } - - private fun getEpisodes() { - if (!statusProvider.isOnline()) { - _isOffline.value = true - } - - viewModelScope.launch { - when (val result = repository.getEpisodes(page)) { - is PageQueryResult.Successful -> { - if (page == 1) { - _episodes.value = - result.data.toViewEpisodeItems() - } else { - _episodes.value = _episodes.value + result.data.toViewEpisodeItems() - } - } - PageQueryResult.EndOfList -> _isEndOfList.value = true - PageQueryResult.Error -> _onError.value = true - } - - _loadingState.value = LoadingState.None - } - } - - private fun List.toViewEpisodeItems(): List = - map { episode -> - episode.toViewEpisodeItem { - _selectedEpisodeCharacterIds.trySend(episode.characterIds.toIntArray()) - } - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/adapter/EpisodeListAdapter.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/adapter/EpisodeListAdapter.kt deleted file mode 100644 index 94b7859..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/adapter/EpisodeListAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mohsenoid.rickandmorty.view.episode.list.adapter - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.mohsenoid.rickandmorty.databinding.ItemEpisodeBinding -import com.mohsenoid.rickandmorty.view.model.ViewEpisodeItem -import java.util.ArrayList - -class EpisodeListAdapter : - RecyclerView.Adapter() { - - private var episodes: MutableList = ArrayList() - - @SuppressLint("NotifyDataSetChanged") - fun setEpisodes(episodes: List) { - this.episodes = episodes.toMutableList() - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder { - val binding = ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return EpisodeViewHolder(binding) - } - - override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) { - val episode: ViewEpisodeItem = episodes[position] - holder.setEpisode(episode) - } - - override fun getItemCount(): Int { - return episodes.size - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/adapter/EpisodeViewHolder.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/adapter/EpisodeViewHolder.kt deleted file mode 100644 index d4e7981..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/adapter/EpisodeViewHolder.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mohsenoid.rickandmorty.view.episode.list.adapter - -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.mohsenoid.rickandmorty.databinding.ItemEpisodeBinding -import com.mohsenoid.rickandmorty.view.model.ViewEpisodeItem - -class EpisodeViewHolder internal constructor(internal val binding: ItemEpisodeBinding) : - ViewHolder(binding.root) { - - fun setEpisode(episode: ViewEpisodeItem) { - binding.episode = episode - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/module.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/module.kt deleted file mode 100644 index 415d7d4..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/episode/list/module.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.mohsenoid.rickandmorty.view.episode.list - -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.annotation.KoinReflectAPI -import org.koin.dsl.module - -@OptIn(KoinReflectAPI::class) -val episodeListFragmentModule = module { - - viewModel() -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/mapper/CharacterMapper.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/mapper/CharacterMapper.kt deleted file mode 100644 index 6d52806..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/mapper/CharacterMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mohsenoid.rickandmorty.view.mapper - -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter -import com.mohsenoid.rickandmorty.view.model.ViewCharacterDetails -import com.mohsenoid.rickandmorty.view.model.ViewCharacterItem - -fun ModelCharacter.toViewCharacterItem(onKill: () -> Unit, onClick: () -> Unit) = ViewCharacterItem( - name = name, - imageUrl = imageUrl, - isAliveAndNotKilledByUser = isAliveAndNotKilledByUser, - onKill = onKill, - onClick = onClick, -) - -fun ModelCharacter.toViewCharacterDetails() = ViewCharacterDetails( - id = id, - name = name, - status = status, - species = species, - gender = gender, - origin = origin.name, - location = location.name, - imageUrl = imageUrl, - created = created, - isKilledByUser = isKilledByUser, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/mapper/EpisodeMapper.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/mapper/EpisodeMapper.kt deleted file mode 100644 index f199d72..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/mapper/EpisodeMapper.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.mohsenoid.rickandmorty.view.mapper - -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode -import com.mohsenoid.rickandmorty.view.model.ViewEpisodeItem - -fun ModelEpisode.toViewEpisodeItem(onClick: () -> Unit) = ViewEpisodeItem( - name = name, - airDate = airDate, - episode = episode, - onClick = onClick, -) diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/LoadingState.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/LoadingState.kt deleted file mode 100644 index d96f823..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/LoadingState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mohsenoid.rickandmorty.view.model - -sealed interface LoadingState { - object None : LoadingState - object Loading : LoadingState - object LoadingMore : LoadingState -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewCharacterItem.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewCharacterItem.kt deleted file mode 100644 index eb3e00a..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewCharacterItem.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.mohsenoid.rickandmorty.view.model - -data class ViewCharacterItem( - val name: String, - val imageUrl: String, - val isAliveAndNotKilledByUser: Boolean, - val onKill: () -> Unit, - val onClick: () -> Unit, -) { - fun onKill() = onKill.invoke() - fun onClick() = onClick.invoke() -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewEpisodeItem.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewEpisodeItem.kt deleted file mode 100644 index 41b39ab..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewEpisodeItem.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mohsenoid.rickandmorty.view.model - -data class ViewEpisodeItem( - val name: String, - val airDate: String, - val episode: String, - val onClick: () -> Unit, -) { - fun onClick() = onClick.invoke() -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/BindingAdapters.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/BindingAdapters.kt deleted file mode 100644 index 6effd20..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/BindingAdapters.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.mohsenoid.rickandmorty.view.util - -import android.view.View -import android.widget.ImageView -import android.widget.ProgressBar -import androidx.databinding.BindingAdapter -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.mohsenoid.rickandmorty.R -import com.mohsenoid.rickandmorty.view.character.list.adapter.CharacterListAdapter -import com.mohsenoid.rickandmorty.view.episode.list.adapter.EpisodeListAdapter -import com.mohsenoid.rickandmorty.view.model.LoadingState -import com.mohsenoid.rickandmorty.view.model.ViewCharacterItem -import com.mohsenoid.rickandmorty.view.model.ViewEpisodeItem -import com.squareup.picasso.Picasso - -@BindingAdapter("visibility") -fun View.setVisibility(isVisible: Boolean) { - visibility = if (isVisible) View.VISIBLE else View.GONE -} - -@BindingAdapter("onRefreshListener") -fun SwipeRefreshLayout.onRefreshListener(listener: SwipeRefreshLayout.OnRefreshListener?) { - setOnRefreshListener(listener) -} - -@BindingAdapter("isRefreshing") -fun SwipeRefreshLayout.setIsRefreshing(loadingState: LoadingState?) { - isRefreshing = loadingState == LoadingState.Loading -} - -@BindingAdapter("isLoading") -fun ProgressBar.setIsLoading(loadingState: LoadingState?) { - visibility = if (loadingState == LoadingState.Loading) View.VISIBLE else View.GONE -} - -@BindingAdapter("isLoadingMore") -fun ProgressBar.setIsLoadingMore(loadingState: LoadingState?) { - visibility = if (loadingState == LoadingState.LoadingMore) View.VISIBLE else View.GONE -} - -@BindingAdapter("episodes") -fun RecyclerView.setEpisodes(itemViewModels: List?) { - val adapter = getOrCreateEpisodeListAdapter() - adapter.setEpisodes(itemViewModels ?: emptyList()) -} - -private fun RecyclerView.getOrCreateEpisodeListAdapter(): EpisodeListAdapter { - return if (adapter != null && adapter is EpisodeListAdapter) { - adapter as EpisodeListAdapter - } else { - val newAdapter = EpisodeListAdapter() - adapter = newAdapter - newAdapter - } -} - -@BindingAdapter("characters") -fun RecyclerView.setCharacters(itemViewModels: List?) { - val adapter = getOrCreateCharacterListAdapter() - adapter.setCharacters(itemViewModels ?: emptyList()) -} - -private fun RecyclerView.getOrCreateCharacterListAdapter(): CharacterListAdapter { - return if (adapter != null && adapter is CharacterListAdapter) { - adapter as CharacterListAdapter - } else { - val newAdapter = CharacterListAdapter() - adapter = newAdapter - newAdapter - } -} - -@BindingAdapter("imageUrl") -fun ImageView.setImageUrl(imageUrl: String?) { - Picasso.get() - .load(imageUrl) - .placeholder(R.drawable.ic_placeholder) - .into(this) -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/EndlessRecyclerViewScrollListener.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/EndlessRecyclerViewScrollListener.kt deleted file mode 100644 index a1ff170..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/EndlessRecyclerViewScrollListener.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.mohsenoid.rickandmorty.view.util - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView - -abstract class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager) : - RecyclerView.OnScrollListener() { - - private var visibleThreshold = DEFAULT_VISIBLE_THRESHOLD - private var currentPage = 0 - private var previousTotalItemCount = 0 - private var loading = true - private val startingPageIndex = 0 - - override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { - val totalItemCount: Int = layoutManager.itemCount - val lastVisibleItemPosition: Int = layoutManager.findLastVisibleItemPosition() - - if (totalItemCount < previousTotalItemCount) { - currentPage = startingPageIndex - previousTotalItemCount = totalItemCount - if (totalItemCount == 0) { - loading = true - } - } - if (loading && totalItemCount > previousTotalItemCount) { - loading = false - previousTotalItemCount = totalItemCount - } - if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) { - currentPage++ - onLoadMore(currentPage, totalItemCount, view) - loading = true - } - } - - fun resetState() { - currentPage = startingPageIndex - previousTotalItemCount = 0 - loading = true - } - - abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView) - - companion object { - const val DEFAULT_VISIBLE_THRESHOLD = 5 - } -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/LifecycleExtensions.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/LifecycleExtensions.kt deleted file mode 100644 index 2028bb8..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/LifecycleExtensions.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.mohsenoid.rickandmorty.view.util - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.coroutineScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job - -fun Lifecycle.launchWhileResumed(block: suspend CoroutineScope.() -> Unit): Job { - val job = coroutineScope.launchWhenResumed(block) - addObserver( - object : DefaultLifecycleObserver { - override fun onPause(owner: LifecycleOwner) { - job.cancel() - removeObserver(this) - } - }, - ) - return job -} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/SquareImageView.kt b/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/SquareImageView.kt deleted file mode 100644 index 95e71cd..0000000 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/util/SquareImageView.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.mohsenoid.rickandmorty.view.util - -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatImageView - -class SquareImageView : AppCompatImageView { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr, - ) - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val width: Int = measuredWidth - setMeasuredDimension(width, width) - } -} diff --git a/app/src/main/res/drawable/ic_alive.xml b/app/src/main/res/drawable/ic_alive.xml deleted file mode 100644 index eac7637..0000000 --- a/app/src/main/res/drawable/ic_alive.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_dead.xml b/app/src/main/res/drawable/ic_dead.xml deleted file mode 100644 index a59b991..0000000 --- a/app/src/main/res/drawable/ic_dead.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_placeholder.xml b/app/src/main/res/drawable/ic_placeholder.xml deleted file mode 100644 index 11e94c0..0000000 --- a/app/src/main/res/drawable/ic_placeholder.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index ed1c066..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/app/src/main/res/layout/fragment_character_details.xml b/app/src/main/res/layout/fragment_character_details.xml deleted file mode 100644 index 1bd6360..0000000 --- a/app/src/main/res/layout/fragment_character_details.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_character_list.xml b/app/src/main/res/layout/fragment_character_list.xml deleted file mode 100644 index d6c4cfa..0000000 --- a/app/src/main/res/layout/fragment_character_list.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_episode_list.xml b/app/src/main/res/layout/fragment_episode_list.xml deleted file mode 100644 index 445490b..0000000 --- a/app/src/main/res/layout/fragment_episode_list.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_character.xml b/app/src/main/res/layout/item_character.xml deleted file mode 100644 index a38d81d..0000000 --- a/app/src/main/res/layout/item_character.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_episode.xml b/app/src/main/res/layout/item_episode.xml deleted file mode 100644 index 30c7493..0000000 --- a/app/src/main/res/layout/item_episode.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index c9ad5f9..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index c9ad5f9..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 2c416f7..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index ec199a5..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 819854b..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 23b2d43..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index f18a35c..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 88d6634..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 82f0040..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 91cfb44..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index a3c8b23..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 297179b..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 3a73d92..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 08d0930..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index cb62cb8..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index f72aa91..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 0d31c90..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index 85e1e7c..0000000 --- a/app/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml deleted file mode 100644 index 10e5b96..0000000 --- a/app/src/main/res/values-w600dp/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 24dp - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index 9f42545..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - #008577 - #00574B - #D81B60 - - #E6555555 - - @color/transparentGray - @color/transparentGray - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index ac7f5df..0000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 16dp - diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index f42ada6..0000000 --- a/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index 5832c69..0000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,18 +0,0 @@ - - Rick and Morty - - No network connections - Something went wrong! - - STATUS - SPECIES - GENDER - ORIGIN - LAST LOCATION - - id: %1$d - created: %2$s - - No offline data!\nConnect network connection and try again… - - (Killed) - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index f4b555d..0000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/release/res/xml/network_security_config.xml b/app/src/release/res/xml/network_security_config.xml deleted file mode 100644 index 82cc113..0000000 --- a/app/src/release/res/xml/network_security_config.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetCharacterDetailsTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetCharacterDetailsTest.kt deleted file mode 100644 index 78fe2ea..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetCharacterDetailsTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.mohsenoid.rickandmorty.data - -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiResult -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.test.ApiFactory -import com.mohsenoid.rickandmorty.test.CharacterDataFactory -import com.mohsenoid.rickandmorty.test.DataFactory -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.runs -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class RepositoryGetCharacterDetailsTest : RepositoryTest() { - - @Test - fun `test if getCharacterDetails calls networkClient when isOnline`() = runBlockingTest { - // GIVEN - val characterId: Int = DataFactory.randomInt() - stubStatusProviderIsOnline(isOnline = true) - stubApiFetchCharacterDetails(ApiFactory.CharacterDetails.makeCharacter()) - stubSbQueryCharacter( - character = CharacterDataFactory.makeDbCharacter(characterId = characterId), - ) - stubDbInsertOrUpdateCharacter() - - // WHEN - repository.getCharacterDetails(characterId = characterId) - - // THEN - coVerify(exactly = 1) { api.fetchCharacterDetails(characterId = any()) } - - coVerify(exactly = 1) { db.insertOrUpdateCharacter(character = any()) } - coVerify(exactly = 1) { db.queryCharacter(characterId = any()) } - } - - @Test - fun `test if getCharacterDetails calls db only when isOffline`() = runBlockingTest { - // GIVEN - val characterId: Int = DataFactory.randomInt() - stubStatusProviderIsOnline(isOnline = false) - stubSbQueryCharacter(CharacterDataFactory.makeDbCharacter(characterId = characterId)) - - // WHEN - repository.getCharacterDetails(characterId = characterId) - - // THEN - coVerify(exactly = 0) { api.fetchCharacterDetails(characterId = any()) } - coVerify(exactly = 1) { db.queryCharacter(characterId = any()) } - } - - private suspend fun stubApiFetchCharacterDetails(character: ApiCharacter) { - coEvery { api.fetchCharacterDetails(characterId = any()) } returns - ApiResult.Success(character) - } - - private fun stubDbInsertOrUpdateCharacter() { - coEvery { db.insertOrUpdateCharacter(any()) } just runs - } - - private fun stubSbQueryCharacter(character: DbCharacter?) { - coEvery { db.queryCharacter(characterId = any()) } returns character - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetCharactersTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetCharactersTest.kt deleted file mode 100644 index 3dfce72..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetCharactersTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.mohsenoid.rickandmorty.data - -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiResult -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.test.ApiFactory -import com.mohsenoid.rickandmorty.test.CharacterDataFactory -import com.mohsenoid.rickandmorty.test.DataFactory -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.runs -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class RepositoryGetCharactersTest : RepositoryTest() { - - @Test - fun `test if getCharactersByIds calls networkClient when isOnline`() = runBlockingTest { - // GIVEN - val characterIds: List = DataFactory.randomIntList(count = 5) - stubStatusProviderIsOnline(isOnline = true) - stubApiFetchCharactersByIds(ApiFactory.Characters.makeCharacters()) - stubDbQueryCharactersByIds(CharacterDataFactory.makeDbCharacters(count = 5)) - stubDbInsertOrUpdateCharacter() - - // WHEN - repository.getCharactersByIds(characterIds = characterIds) - - // THEN - coVerify(exactly = 1) { api.fetchCharactersByIds(characterIds = any()) } - - coVerify(exactly = 1) { db.insertOrUpdateCharacter(character = any()) } - coVerify(exactly = 1) { db.queryCharactersByIds(characterIds = any()) } - } - - @Test - fun `test if getCharactersByIds calls db only when isOffline`() = runBlockingTest { - // GIVEN - val characterIds: List = DataFactory.randomIntList(count = 5) - stubStatusProviderIsOnline(isOnline = false) - stubDbQueryCharactersByIds(CharacterDataFactory.makeDbCharacters(count = 5)) - - // WHEN - repository.getCharactersByIds(characterIds = characterIds) - - // THEN - coVerify(exactly = 0) { api.fetchCharactersByIds(characterIds = any()) } - coVerify(exactly = 1) { db.queryCharactersByIds(characterIds = any()) } - } - - private fun stubApiFetchCharactersByIds(characters: List) { - coEvery { api.fetchCharactersByIds(characterIds = any()) } returns - ApiResult.Success(characters) - } - - private fun stubDbQueryCharactersByIds(characters: List) { - coEvery { db.queryCharactersByIds(characterIds = any()) } returns characters - } - - private fun stubDbInsertOrUpdateCharacter() { - coEvery { db.insertOrUpdateCharacter(any()) } just runs - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetEpisodesTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetEpisodesTest.kt deleted file mode 100644 index 971ccde..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryGetEpisodesTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.mohsenoid.rickandmorty.data - -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisodes -import com.mohsenoid.rickandmorty.data.api.model.ApiResult -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.test.ApiFactory -import com.mohsenoid.rickandmorty.test.EpisodeDataFactory -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.runs -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class RepositoryGetEpisodesTest : RepositoryTest() { - - @Test - fun `getEpisodes calls networkClient and db when isOnline`() = runBlockingTest { - // GIVEN - stubStatusProviderIsOnline(isOnline = true) - stubApiFetchEpisodes(ApiFactory.Episode.makeApiEpisodes()) - val entityEpisodes = EpisodeDataFactory.makeDbEpisodes(count = 5) - stubEpisodeDaoQueryAllEpisodesByPage(entityEpisodes) - stubDbInsertEpisode() - - // WHEN - repository.getEpisodes(page = 1) - - // THEN - coVerify(exactly = 1) { api.fetchEpisodes(page = 1) } - coVerify(exactly = 1) { db.insertEpisode(entityEpisode = any()) } - coVerify(exactly = 1) { db.queryAllEpisodesByPage(page = any(), pageSize = any()) } - } - - @Test - fun `if getEpisodes calls db only when isOffline`() = runBlockingTest { - // GIVEN - stubStatusProviderIsOnline(isOnline = false) - stubEpisodeDaoQueryAllEpisodesByPage(EpisodeDataFactory.makeDbEpisodes(count = 5)) - - // WHEN - repository.getEpisodes(page = 1) - - // THEN - coVerify(exactly = 0) { api.fetchEpisodes(page = 1) } - coVerify(exactly = 1) { db.queryAllEpisodesByPage(page = any(), pageSize = any()) } - } - - private fun stubApiFetchEpisodes(episodes: ApiEpisodes) { - coEvery { api.fetchEpisodes(page = any()) } returns ApiResult.Success(episodes) - } - - private fun stubDbInsertEpisode() { - coEvery { db.insertEpisode(any()) } just runs - } - - private fun stubEpisodeDaoQueryAllEpisodesByPage(entityEpisodes: List) { - coEvery { db.queryAllEpisodesByPage(page = any(), pageSize = any()) } returns entityEpisodes - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryTest.kt deleted file mode 100644 index 90960a7..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/RepositoryTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.mohsenoid.rickandmorty.data - -import com.mohsenoid.rickandmorty.data.api.ApiRickAndMorty -import com.mohsenoid.rickandmorty.data.db.DbRickAndMorty -import com.mohsenoid.rickandmorty.domain.Repository -import com.mohsenoid.rickandmorty.util.StatusProvider -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import org.junit.Before - -open class RepositoryTest { - - @MockK - internal lateinit var db: DbRickAndMorty - - @MockK - internal lateinit var api: ApiRickAndMorty - - @MockK - internal lateinit var statusProvider: StatusProvider - - lateinit var repository: Repository - - @Before - fun setUp() { - MockKAnnotations.init(this) - - repository = RepositoryImpl( - db = db, - api = api, - statusProvider = statusProvider, - ) - } - - fun stubStatusProviderIsOnline(isOnline: Boolean) { - every { statusProvider.isOnline() } returns isOnline - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/db/DbRickAndMortyTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/db/DbRickAndMortyTest.kt deleted file mode 100644 index 2afbe60..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/db/DbRickAndMortyTest.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.mohsenoid.rickandmorty.data.db - -import android.app.Application -import android.os.Build -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.test.CharacterDataFactory -import com.mohsenoid.rickandmorty.test.DataFactory -import com.mohsenoid.rickandmorty.test.EpisodeDataFactory -import io.mockk.every -import io.mockk.mockkObject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.koin.core.context.stopKoin -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.io.IOException -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -@OptIn(ExperimentalCoroutinesApi::class) -@Config(sdk = [Build.VERSION_CODES.P]) -@RunWith(RobolectricTestRunner::class) -class DbRickAndMortyTest { - - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - private lateinit var roomDb: DbRickAndMorty.Db - - private lateinit var db: DbRickAndMorty - - @Before - fun setUp() { - val application: Application = ApplicationProvider.getApplicationContext() - mockkObject(DbRickAndMorty.Db) { - roomDb = Room.inMemoryDatabaseBuilder(application, DbRickAndMorty.Db::class.java) - .allowMainThreadQueries() - .build() - - every { DbRickAndMorty.Db.create(application) } returns roomDb - - db = DbRickAndMorty(application) - } - } - - @After - @Throws(IOException::class) - fun tearDown() { - roomDb.close() - stopKoin() - } - - @Test - fun `insertEpisode can inserts a new Episode`() = runBlockingTest { - // GIVEN - val expectedEntityEpisode: DbEpisode = EpisodeDataFactory.makeDbEpisode() - - // WHEN - db.insertEpisode(expectedEntityEpisode) - - // THEN - val actualEntityEpisodes: List = db.queryAllEpisodes() - - assertFalse(actualEntityEpisodes.isEmpty()) - assertContains(actualEntityEpisodes, expectedEntityEpisode) - } - - @Test - fun `insertEpisode with same ID updates the old one`() = runBlockingTest { - // GIVEN - val episodeId: Int = DataFactory.randomInt() - val oldEntityEpisode: DbEpisode = - EpisodeDataFactory.makeDbEpisode(episodeId = episodeId) - val updatedEntityEpisode: DbEpisode = - EpisodeDataFactory.makeDbEpisode(episodeId = episodeId) - - // WHEN - db.insertEpisode(oldEntityEpisode) - db.insertEpisode(updatedEntityEpisode) - - // THEN - val actualEntityEpisodes: List = db.queryAllEpisodes() - - assertFalse(actualEntityEpisodes.isEmpty()) - assertContains(actualEntityEpisodes, updatedEntityEpisode) - // assertNotContains(actualEntityEpisodes, oldEntityEpisode) - } - - @Test - fun `insertCharacter can inserts a new Character`() = runBlockingTest { - // GIVEN - val expectedCharacter: DbCharacter = CharacterDataFactory.makeDbCharacter() - - // WHEN - db.insertOrUpdateCharacter(expectedCharacter) - - // THEN - val actualCharacter: DbCharacter? = - db.queryCharacter(expectedCharacter.id) - - assertNotNull(actualCharacter) - assertEquals(actualCharacter, expectedCharacter) - } - - @Test - fun `insertCharacter with same ID updates the old one`() = runBlockingTest { - // GIVEN - val characterId: Int = DataFactory.randomInt() - val oldCharacter: DbCharacter = - CharacterDataFactory.makeDbCharacter(characterId = characterId) - val updatedCharacter: DbCharacter = - CharacterDataFactory.makeDbCharacter(characterId = characterId) - - // WHEN - db.insertOrUpdateCharacter(oldCharacter) - db.insertOrUpdateCharacter(updatedCharacter) - - // THEN - val actualCharacter: DbCharacter? = db.queryCharacter(characterId) - - assertNotNull(actualCharacter) - assertEquals(actualCharacter, updatedCharacter) - } - - @Test - fun `queryCharacter returns the same Character details inserted`() = runBlockingTest { - // GIVEN - val characterId: Int = DataFactory.randomInt() - val expectedCharacter: DbCharacter = - CharacterDataFactory.makeDbCharacter(characterId = characterId) - - // WHEN - db.insertOrUpdateCharacter(expectedCharacter) - - // THEN - val actualCharacter: DbCharacter? = db.queryCharacter(characterId) - - assertNotNull(actualCharacter) - assertEquals(actualCharacter, expectedCharacter) - } - - @Test - fun `queryCharactersByIds returns the Characters asked for`() = runBlockingTest { - // GIVEN - val expected: DbCharacter = CharacterDataFactory.makeDbCharacter() - val notExpected: DbCharacter = CharacterDataFactory.makeDbCharacter() - val expectedCharacterIds: List = listOf(expected.id) - - // WHEN - db.insertOrUpdateCharacter(expected) - db.insertOrUpdateCharacter(notExpected) - - // THEN - val characters: List = - db.queryCharactersByIds(expectedCharacterIds) - - assertFalse(characters.isEmpty()) - assertContains(characters, expected) - // assertNotContains(characters, expected) - } - - @Test - fun `Character which status is Alive and is not KilledByUser isAlive`() = runBlockingTest { - // GIVEN - val expectedCharacterId: Int = DataFactory.randomInt() - val expectedCharacter: DbCharacter = CharacterDataFactory.makeDbCharacter( - characterId = expectedCharacterId, - status = ALIVE, - isAlive = true, - isKilledByUser = false, - ) - - // WHEN - db.insertOrUpdateCharacter(expectedCharacter) - - // THEN - val actualCharacter: DbCharacter? = - db.queryCharacter(expectedCharacterId) - - assertNotNull(actualCharacter) - assertEquals(actualCharacter.status, ALIVE) - assertTrue(actualCharacter.isAlive) - assertFalse(actualCharacter.isKilledByUser) - } - - @Test - fun `test user can kill a Character`() = runBlockingTest { - // GIVEN - val expectedCharacterId: Int = DataFactory.randomInt() - val expectedCharacter: DbCharacter = CharacterDataFactory.makeDbCharacter( - characterId = expectedCharacterId, - status = ALIVE, - isAlive = true, - isKilledByUser = false, - ) - - // WHEN - db.insertOrUpdateCharacter(expectedCharacter) - db.killCharacter(expectedCharacterId) - - // THEN - val actualCharacter: DbCharacter? = - db.queryCharacter(expectedCharacterId) - - assertNotNull(actualCharacter) - assertEquals(actualCharacter.status, ALIVE) - assertTrue(actualCharacter.isAlive) - assertTrue(actualCharacter.isKilledByUser) - } - - @Test - fun `test a Character which isKilledByUser keeps dead after update by insert`() = - runBlockingTest { - // GIVEN - val expectedCharacterId: Int = DataFactory.randomInt() - val expectedCharacter: DbCharacter = CharacterDataFactory.makeDbCharacter( - characterId = expectedCharacterId, - status = ALIVE, - isAlive = true, - isKilledByUser = false, - ) - - // WHEN - db.insertOrUpdateCharacter(expectedCharacter) - db.killCharacter(expectedCharacterId) - db.insertOrUpdateCharacter(expectedCharacter) - - // THEN - val actualCharacter: DbCharacter? = - db.queryCharacter(expectedCharacterId) - - assertNotNull(actualCharacter) - assertEquals(actualCharacter.status, ALIVE) - assertTrue(actualCharacter.isAlive) - assertTrue(actualCharacter.isKilledByUser) - } - - companion object { - private const val ALIVE = "alive" - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/mapper/CharacterMapperTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/mapper/CharacterMapperTest.kt deleted file mode 100644 index e63df3d..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/mapper/CharacterMapperTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.mohsenoid.rickandmorty.data.mapper - -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiLocation -import com.mohsenoid.rickandmorty.data.api.model.ApiOrigin -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.data.db.model.DbLocation -import com.mohsenoid.rickandmorty.data.db.model.DbOrigin -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter -import com.mohsenoid.rickandmorty.domain.model.ModelLocation -import com.mohsenoid.rickandmorty.domain.model.ModelOrigin -import org.junit.Test -import kotlin.test.assertEquals - -class CharacterMapperTest { - - @Test - fun modelToDbCharacter() { - // GIVEN - val apiCharacter = ApiCharacter( - id = CHARACTER_ID, - name = CHARACTER_NAME, - status = CHARACTER_STATUS, - species = CHARACTER_SPECIES, - type = CHARACTER_TYPE, - gender = CHARACTER_GENDER, - origin = ApiOrigin(CHARACTER_ORIGIN_NAME, CHARACTER_ORIGIN_URL), - location = ApiLocation(CHARACTER_LOCATION_NAME, CHARACTER_LOCATION_URL), - image = CHARACTER_IMAGE, - episodes = CHARACTER_EPISODES, - url = CHARACTER_URL, - created = CHARACTER_CREATED, - ) - - val expectedDbCharacter = DbCharacter( - id = CHARACTER_ID, - name = CHARACTER_NAME, - status = CHARACTER_STATUS, - isAlive = CHARACTER_STATUS_ALIVE, - species = CHARACTER_SPECIES, - type = CHARACTER_TYPE, - gender = CHARACTER_GENDER, - origin = DbOrigin(CHARACTER_ORIGIN_NAME, CHARACTER_ORIGIN_URL), - location = DbLocation(CHARACTER_LOCATION_NAME, CHARACTER_LOCATION_URL), - image = CHARACTER_IMAGE, - episodeIds = CHARACTER_EPISODE_IDS, - url = CHARACTER_URL, - created = CHARACTER_CREATED, - isKilledByUser = false, - ) - - // WHEN - val actualDbCharacter: DbCharacter = apiCharacter.toDbCharacter() - - // THEN - assertEquals(expectedDbCharacter, actualDbCharacter) - } - - @Test - fun dbToModelCharacter() { - // GIVEN - val dbCharacter = DbCharacter( - id = CHARACTER_ID, - name = CHARACTER_NAME, - status = CHARACTER_STATUS, - isAlive = CHARACTER_STATUS_ALIVE, - species = CHARACTER_SPECIES, - type = CHARACTER_TYPE, - gender = CHARACTER_GENDER, - origin = DbOrigin(CHARACTER_ORIGIN_NAME, CHARACTER_ORIGIN_URL), - location = DbLocation(CHARACTER_LOCATION_NAME, CHARACTER_LOCATION_URL), - image = CHARACTER_IMAGE, - episodeIds = CHARACTER_EPISODE_IDS, - url = CHARACTER_URL, - created = CHARACTER_CREATED, - isKilledByUser = CHARACTER_IS_KILLED_BY_USER, - ) - - val expectedModelCharacter = ModelCharacter( - id = CHARACTER_ID, - name = CHARACTER_NAME, - status = CHARACTER_STATUS, - isAlive = CHARACTER_STATUS_ALIVE, - species = CHARACTER_SPECIES, - type = CHARACTER_TYPE, - gender = CHARACTER_GENDER, - origin = ModelOrigin(CHARACTER_ORIGIN_NAME, CHARACTER_ORIGIN_URL), - location = ModelLocation(CHARACTER_LOCATION_NAME, CHARACTER_LOCATION_URL), - imageUrl = CHARACTER_IMAGE, - episodeIds = CHARACTER_EPISODE_IDS, - url = CHARACTER_URL, - created = CHARACTER_CREATED, - isKilledByUser = CHARACTER_IS_KILLED_BY_USER, - ) - - // WHEN - val actualModelCharacter: ModelCharacter = dbCharacter.toModelCharacter() - - // THEN - assertEquals(expectedModelCharacter, actualModelCharacter) - } - - companion object { - private const val CHARACTER_ID = 123 - private const val CHARACTER_NAME = "some name" - private const val CHARACTER_STATUS = "dead" - private const val CHARACTER_STATUS_ALIVE = false - private const val CHARACTER_SPECIES = "some species" - private const val CHARACTER_TYPE = "some type" - private const val CHARACTER_GENDER = "some gender" - private const val CHARACTER_ORIGIN_NAME = "some origin name" - private const val CHARACTER_ORIGIN_URL = "some origin url" - private const val CHARACTER_LOCATION_NAME = "some location name" - private const val CHARACTER_LOCATION_URL = "some location url" - private const val CHARACTER_IMAGE = "some image" - private val CHARACTER_EPISODES = listOf( - "https://test.com/api/episode/1", - "https://test.com/api/episode/2", - "https://test.com/api/episode/3", - ) - private val CHARACTER_EPISODE_IDS = listOf(1, 2, 3) - private const val CHARACTER_URL = "some url" - private const val CHARACTER_CREATED = "some created" - private const val CHARACTER_IS_KILLED_BY_USER = true - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/mapper/EpisodeMapperTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/mapper/EpisodeMapperTest.kt deleted file mode 100644 index 6581fb1..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/data/mapper/EpisodeMapperTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.mohsenoid.rickandmorty.data.mapper - -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisode -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode -import org.junit.Test -import kotlin.test.assertEquals - -class EpisodeMapperTest { - - @Test - fun apiToDbEpisode() { - // GIVEN - val apiEpisode = ApiEpisode( - id = EPISODE_ID, - name = EPISODE_NAME, - airDate = EPISODE_AIR_DATE, - episode = EPISODE_EPISODE, - characters = CHARACTER_CHARACTERS, - url = EPISODE_URL, - created = EPISODE_CREATED, - ) - - val expectedDbEpisode = DbEpisode( - id = EPISODE_ID, - name = EPISODE_NAME, - airDate = EPISODE_AIR_DATE, - episode = EPISODE_EPISODE, - characterIds = CHARACTER_CHARACTER_IDS, - url = EPISODE_URL, - created = EPISODE_CREATED, - ) - - // WHEN - val actualDbEpisode: DbEpisode = apiEpisode.toDbEpisode() - - // THEN - assertEquals(expectedDbEpisode, actualDbEpisode) - } - - @Test - fun dbToModelEpisode() { - // GIVEN - val dbEpisode = DbEpisode( - id = EPISODE_ID, - name = EPISODE_NAME, - airDate = EPISODE_AIR_DATE, - episode = EPISODE_EPISODE, - characterIds = CHARACTER_CHARACTER_IDS, - url = EPISODE_URL, - created = EPISODE_CREATED, - ) - - val expectedModelEpisode = ModelEpisode( - id = EPISODE_ID, - name = EPISODE_NAME, - airDate = EPISODE_AIR_DATE, - episode = EPISODE_EPISODE, - characterIds = CHARACTER_CHARACTER_IDS, - url = EPISODE_URL, - created = EPISODE_CREATED, - ) - - // WHEN - val actualModelEpisode: ModelEpisode = dbEpisode.toModelEpisode() - - // THEN - assertEquals(expectedModelEpisode, actualModelEpisode) - } - - companion object { - private const val EPISODE_ID = 123 - private const val EPISODE_NAME = "some name" - private const val EPISODE_AIR_DATE = "some air date" - private const val EPISODE_EPISODE = "some episode" - private val CHARACTER_CHARACTERS = listOf( - "https://test.com/api/character/1", - "https://test.com/api/character/2", - "https://test.com/api/character/3", - ) - private val CHARACTER_CHARACTER_IDS = listOf(1, 2, 3) - private const val EPISODE_URL = "some url" - private const val EPISODE_CREATED = "some created" - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/AppModuleTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/AppModuleTest.kt deleted file mode 100644 index 09837d9..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/AppModuleTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.mohsenoid.rickandmorty.injection - -import android.os.Build -import com.mohsenoid.rickandmorty.appModule -import org.junit.Test -import org.junit.runner.RunWith -import org.koin.android.ext.koin.androidContext -import org.koin.core.context.startKoin -import org.koin.test.check.checkModules -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@Config(sdk = [Build.VERSION_CODES.P]) -@RunWith(RobolectricTestRunner::class) -class AppModuleTest : ModuleTest() { - - @Test - fun `check all definitions from appModule`() { - startKoin { - androidContext(application) - modules(appModule) - }.checkModules() - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/DataModuleTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/DataModuleTest.kt deleted file mode 100644 index 20151be..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/DataModuleTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mohsenoid.rickandmorty.injection - -import android.os.Build -import com.mohsenoid.rickandmorty.appModule -import com.mohsenoid.rickandmorty.data.dataModule -import com.mohsenoid.rickandmorty.util.KoinQualifiersNames -import org.junit.Test -import org.junit.runner.RunWith -import org.koin.android.ext.koin.androidContext -import org.koin.core.context.startKoin -import org.koin.test.check.checkModules -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@Config(sdk = [Build.VERSION_CODES.P]) -@RunWith(RobolectricTestRunner::class) -class DataModuleTest : ModuleTest() { - - @Test - fun `check all definitions from dataModule`() { - startKoin { - val appProperties: Map = mapOf( - KoinQualifiersNames.BASE_URL to BASE_URL, - ) - properties(appProperties) - - androidContext(application) - modules(appModule + dataModule) - }.checkModules() - } - - companion object { - private const val BASE_URL = "https://test.com/api/" - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/ModuleTest.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/ModuleTest.kt deleted file mode 100644 index e97468b..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/injection/ModuleTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mohsenoid.rickandmorty.injection - -import android.app.Application -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import org.junit.After -import org.junit.Before -import org.junit.runner.RunWith -import org.koin.core.component.KoinComponent -import org.koin.core.context.stopKoin -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@Config(sdk = [Build.VERSION_CODES.P]) -@RunWith(RobolectricTestRunner::class) -abstract class ModuleTest : KoinComponent { - - internal lateinit var application: Application - - @Before - fun setUp() { - application = ApplicationProvider.getApplicationContext() - stopKoin() - } - - @After - fun tearDown() { - stopKoin() - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/ApiFactory.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/ApiFactory.kt deleted file mode 100644 index ff94490..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/ApiFactory.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.mohsenoid.rickandmorty.test - -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisode -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisodes -import com.mohsenoid.rickandmorty.data.api.model.ApiInfo -import com.mohsenoid.rickandmorty.data.api.model.ApiLocation -import com.mohsenoid.rickandmorty.data.api.model.ApiOrigin - -object ApiFactory { - - object Episode { - private const val VALUE_COUNT: Int = 1 - private const val VALUE_PAGE: Int = 1 - private const val VALUE_NEXT: String = "" - private const val VALUE_PREV: String = "" - - private const val VALUE_ID: Int = 1 - private const val VALUE_NAME: String = "Pilot" - private const val VALUE_AIR_DATE: String = "December 2, 2013" - private const val VALUE_EPISODE: String = "S01E01" - private const val VALUE_CHARACTER: String = "https://rickandmortyapi.com/api/character/1" - private const val VALUE_URL: String = "https://rickandmortyapi.com/api/episode/1" - private const val VALUE_CREATED: String = "2017-11-10T12:56:33.798Z" - - const val EPISODES_JSON: String = """{ - "info": { - "count": $VALUE_COUNT, - "pages": $VALUE_PAGE, - "next": $VALUE_NEXT, - "prev": $VALUE_PREV - }, - "results": [ - { - "id": $VALUE_ID, - "name": $VALUE_NAME, - "air_date": $VALUE_AIR_DATE, - "episode": $VALUE_EPISODE, - "characters": [ - $VALUE_CHARACTER - ], - "url": $VALUE_URL, - "created": $VALUE_CREATED - } - ] -}""" - - internal fun makeApiEpisodes(): ApiEpisodes { - val info = ApiInfo( - count = VALUE_COUNT, - pages = VALUE_PAGE, - next = VALUE_NEXT, - prev = VALUE_PREV, - ) - - val episode = ApiEpisode( - id = VALUE_ID, - name = VALUE_NAME, - airDate = VALUE_AIR_DATE, - episode = VALUE_EPISODE, - characters = arrayListOf(VALUE_CHARACTER), - url = VALUE_URL, - created = VALUE_CREATED, - ) - - return ApiEpisodes( - info = info, - results = arrayListOf(episode), - ) - } - } - - object Characters { - const val CHARACTERS_JSON: String = "[\n ${CharacterDetails.CHARACTER_DETAILS_JSON}\n]" - - internal fun makeCharacters(): List { - val character: ApiCharacter = CharacterDetails.makeCharacter() - return arrayListOf(character) - } - } - - object CharacterDetails { - private const val VALUE_ID: Int = 1 - private const val VALUE_NAME: String = "Rick Sanchez" - private const val VALUE_STATUS: String = "Alive" - private const val VALUE_SPECIES: String = "Human" - private const val VALUE_TYPE: String = "" - private const val VALUE_GENDER: String = "Male" - private const val VALUE_ORIGIN_NAME: String = "Earth (C-137)" - private const val VALUE_ORIGIN_URL: String = "https://rickandmortyapi.com/api/location/1" - private const val VALUE_LOCATION_NAME: String = "Earth (Replacement Dimension)" - private const val VALUE_LOCATION_URL: String = "https://rickandmortyapi.com/api/location/20" - private const val VALUE_IMAGE: String = - "https://rickandmortyapi.com/api/character/avatar/1.jpeg" - private const val VALUE_EPISODE: String = "https://rickandmortyapi.com/api/episode/1" - private const val VALUE_URL: String = "https://rickandmortyapi.com/api/character/1" - private const val VALUE_CREATED: String = "2017-11-04T18:48:46.250Z" - - const val CHARACTER_DETAILS_JSON: String = """{ - "id": $VALUE_ID, - "name": "$VALUE_NAME", - "status": "$VALUE_STATUS", - "species": "$VALUE_SPECIES", - "type": "$VALUE_TYPE", - "gender": "$VALUE_GENDER", - "origin": { - "name": "$VALUE_ORIGIN_NAME", - "url": "$VALUE_ORIGIN_URL" - }, - "location": { - "name": "$VALUE_LOCATION_NAME", - "url": "$VALUE_LOCATION_URL" - }, - "image": "$VALUE_IMAGE", - "episode": [ - "$VALUE_EPISODE" - ], - "url": "$VALUE_URL", - "created": "$VALUE_CREATED" -}""" - - internal fun makeCharacter(): ApiCharacter { - val origin = ApiOrigin( - name = VALUE_ORIGIN_NAME, - url = VALUE_ORIGIN_URL, - ) - - val location = ApiLocation( - name = VALUE_LOCATION_NAME, - url = VALUE_LOCATION_URL, - ) - - return ApiCharacter( - id = VALUE_ID, - name = VALUE_NAME, - status = VALUE_STATUS, - species = VALUE_SPECIES, - type = VALUE_TYPE, - gender = VALUE_GENDER, - origin = origin, - location = location, - image = VALUE_IMAGE, - episodes = arrayListOf(VALUE_EPISODE), - url = VALUE_URL, - created = VALUE_CREATED, - ) - } - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/CharacterDataFactory.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/CharacterDataFactory.kt deleted file mode 100644 index 22bdd5d..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/CharacterDataFactory.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.mohsenoid.rickandmorty.test - -import com.mohsenoid.rickandmorty.data.api.model.ApiCharacter -import com.mohsenoid.rickandmorty.data.db.model.DbCharacter -import com.mohsenoid.rickandmorty.domain.model.ModelCharacter - -object CharacterDataFactory { - - internal fun makeDbCharacter( - characterId: Int = DataFactory.randomInt(), - status: String = DataFactory.randomString(), - isAlive: Boolean = DataFactory.randomBoolean(), - isKilledByUser: Boolean = false, - ): DbCharacter = - DbCharacter( - id = characterId, - name = DataFactory.randomString(), - status = status, - isAlive = isAlive, - species = DataFactory.randomString(), - type = DataFactory.randomString(), - gender = DataFactory.randomString(), - origin = OriginDataFactory.makeDbOrigin(), - location = LocationDataFactory.makeDbLocation(), - image = DataFactory.randomString(), - episodeIds = DataFactory.randomIntList(count = 5), - url = DataFactory.randomString(), - created = DataFactory.randomString(), - isKilledByUser = isKilledByUser, - ) - - internal fun makeDbCharacters(count: Int): List { - val characters: MutableList = ArrayList() - repeat(count) { - val character: DbCharacter = makeDbCharacter() - characters.add(character) - } - return characters - } - - internal fun makeApiCharacter(characterId: Int = DataFactory.randomInt()): ApiCharacter = - ApiCharacter( - id = characterId, - name = DataFactory.randomString(), - status = DataFactory.randomString(), - species = DataFactory.randomString(), - type = DataFactory.randomString(), - gender = DataFactory.randomString(), - origin = OriginDataFactory.makeApiOrigin(), - location = LocationDataFactory.makeApiLocation(), - image = DataFactory.randomString(), - episodes = DataFactory.randomIntList(count = 5) - .map { "${DataFactory.randomString()}/$it" }, - url = DataFactory.randomString(), - created = DataFactory.randomString(), - ) - - internal fun makeApiCharacters(count: Int): List { - val characters: MutableList = ArrayList() - repeat(count) { - val character: ApiCharacter = makeApiCharacter() - characters.add(character) - } - return characters - } - - internal fun makeCharacter( - id: Int = DataFactory.randomInt(), - status: String = DataFactory.randomString(), - isAlive: Boolean = DataFactory.randomBoolean(), - isKilledByUser: Boolean = false, - ): ModelCharacter = ModelCharacter( - id = id, - name = DataFactory.randomString(), - status = status, - isAlive = isAlive, - species = DataFactory.randomString(), - type = DataFactory.randomString(), - gender = DataFactory.randomString(), - origin = OriginDataFactory.makeOrigin(), - location = LocationDataFactory.makeLocation(), - imageUrl = DataFactory.randomString(), - episodeIds = DataFactory.randomIntList(count = 5), - url = DataFactory.randomString(), - created = DataFactory.randomString(), - isKilledByUser = isKilledByUser, - ) - - internal fun makeCharacters(count: Int): List { - val characters: MutableList = ArrayList() - repeat(count) { - val character: ModelCharacter = makeCharacter() - characters.add(character) - } - return characters - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/DataFactory.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/DataFactory.kt deleted file mode 100644 index 308e096..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/DataFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mohsenoid.rickandmorty.test - -import java.util.ArrayList -import java.util.UUID -import java.util.concurrent.ThreadLocalRandom - -object DataFactory { - - fun randomString(): String = UUID.randomUUID().toString() - - fun randomStringList(count: Int): List { - val list: MutableList = ArrayList() - - repeat(count) { - list.add(randomString()) - } - - return list - } - - fun randomInt(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE): Int = - ThreadLocalRandom.current().nextInt(min, max) - - fun randomIntList(count: Int, min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE): List { - val list: MutableList = ArrayList() - - repeat(count) { - list.add(randomInt(min, max)) - } - - return list - } - - fun randomBoolean(): Boolean = Math.random() < 0.5 -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/EpisodeDataFactory.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/EpisodeDataFactory.kt deleted file mode 100644 index ecb6be9..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/EpisodeDataFactory.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.mohsenoid.rickandmorty.test - -import com.mohsenoid.rickandmorty.data.api.model.ApiEpisode -import com.mohsenoid.rickandmorty.data.db.model.DbEpisode -import com.mohsenoid.rickandmorty.domain.model.ModelEpisode - -object EpisodeDataFactory { - - internal fun makeDbEpisode(episodeId: Int = DataFactory.randomInt()): DbEpisode = - DbEpisode( - id = episodeId, - name = DataFactory.randomString(), - airDate = DataFactory.randomString(), - episode = DataFactory.randomString(), - characterIds = DataFactory.randomIntList(count = 5), - url = DataFactory.randomString(), - created = DataFactory.randomString(), - ) - - internal fun makeDbEpisodes(count: Int): List { - val entityEpisodes: MutableList = ArrayList() - repeat(count) { - val entityEpisode: DbEpisode = makeDbEpisode() - entityEpisodes.add(entityEpisode) - } - return entityEpisodes - } - - internal fun makeApiEpisode(episodeId: Int = DataFactory.randomInt()): ApiEpisode = - ApiEpisode( - id = episodeId, - name = DataFactory.randomString(), - airDate = DataFactory.randomString(), - episode = DataFactory.randomString(), - characters = DataFactory.randomIntList(count = 5) - .map { "${DataFactory.randomString()}/$it" }, - url = DataFactory.randomString(), - created = DataFactory.randomString(), - ) - - internal fun makeApiEpisodes(count: Int): List { - val episodes: MutableList = ArrayList() - repeat(count) { - val episode: ApiEpisode = makeApiEpisode() - episodes.add(episode) - } - return episodes - } - - internal fun makeEpisode(episodeId: Int = DataFactory.randomInt()): ModelEpisode = - ModelEpisode( - id = episodeId, - name = DataFactory.randomString(), - airDate = DataFactory.randomString(), - episode = DataFactory.randomString(), - characterIds = DataFactory.randomIntList(count = 5), - url = DataFactory.randomString(), - created = DataFactory.randomString(), - ) - - internal fun makeEpisodes(count: Int): List { - val episodes: MutableList = ArrayList() - repeat(count) { - val episode: ModelEpisode = makeEpisode() - episodes.add(episode) - } - return episodes - } -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/LocationDataFactory.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/LocationDataFactory.kt deleted file mode 100644 index 0e13a06..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/LocationDataFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mohsenoid.rickandmorty.test - -import com.mohsenoid.rickandmorty.data.api.model.ApiLocation -import com.mohsenoid.rickandmorty.data.db.model.DbLocation -import com.mohsenoid.rickandmorty.domain.model.ModelLocation - -object LocationDataFactory { - - internal fun makeDbLocation(): DbLocation = - DbLocation( - name = DataFactory.randomString(), - url = DataFactory.randomString(), - ) - - internal fun makeApiLocation(): ApiLocation = - ApiLocation( - name = DataFactory.randomString(), - url = DataFactory.randomString(), - ) - - internal fun makeLocation(): ModelLocation = - ModelLocation( - name = DataFactory.randomString(), - url = DataFactory.randomString(), - ) -} diff --git a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/OriginDataFactory.kt b/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/OriginDataFactory.kt deleted file mode 100644 index 2285709..0000000 --- a/app/src/test/kotlin/com/mohsenoid/rickandmorty/test/OriginDataFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mohsenoid.rickandmorty.test - -import com.mohsenoid.rickandmorty.data.api.model.ApiOrigin -import com.mohsenoid.rickandmorty.data.db.model.DbOrigin -import com.mohsenoid.rickandmorty.domain.model.ModelOrigin - -object OriginDataFactory { - - internal fun makeDbOrigin(): DbOrigin = - DbOrigin( - name = DataFactory.randomString(), - url = DataFactory.randomString(), - ) - - internal fun makeApiOrigin(): ApiOrigin = - ApiOrigin( - name = DataFactory.randomString(), - url = DataFactory.randomString(), - ) - - internal fun makeOrigin(): ModelOrigin = - ModelOrigin( - name = DataFactory.randomString(), - url = DataFactory.randomString(), - ) -} diff --git a/build.gradle.kts b/build.gradle.kts index 8e0a4f6..7405bca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,26 +1,8 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath("com.android.tools.build:gradle:7.0.3") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") - classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5") - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - - -task("clean") { - delete = setOf(rootProject.buildDir) -} +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.jetbrainsCompose) apply false + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.kotlinMultiplatform) apply false +} \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts new file mode 100644 index 0000000..bdeb451 --- /dev/null +++ b/composeApp/build.gradle.kts @@ -0,0 +1,128 @@ +import org.jetbrains.compose.ExperimentalComposeLibrary +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.kotlinXSerialization) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + + sourceSets { + val desktopMain by getting + + androidMain.dependencies { + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.androidx.activity.compose) + + implementation(libs.ktor.client.okhttp) + } + + desktopMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.ktor.client.okhttp) + } + + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.animation) + implementation(compose.material3) + @OptIn(ExperimentalComposeLibrary::class) + implementation(compose.components.resources) + + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + + implementation(libs.moko.mvvm.core) + implementation(libs.moko.mvvm.compose) + + implementation(libs.kamel) + + implementation(libs.precompose) + } + + commonTest.dependencies { + } + } +} + +android { + namespace = "com.mohsenoid.rickandmorty" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].res.srcDirs("src/androidMain/res") + sourceSets["main"].resources.srcDirs("src/commonMain/resources") + + defaultConfig { + applicationId = "com.mohsenoid.rickandmorty" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + dependencies { + debugImplementation(libs.compose.ui.tooling) + } +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "com.mohsenoid.rickandmorty" + packageVersion = "1.0.0" + } + } +} diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b87d944 --- /dev/null +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/composeApp/src/androidMain/kotlin/com/mohsenoid/rickandmorty/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/mohsenoid/rickandmorty/MainActivity.kt new file mode 100644 index 0000000..24a2b5b --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/mohsenoid/rickandmorty/MainActivity.kt @@ -0,0 +1,36 @@ +package com.mohsenoid.rickandmorty + +import android.content.res.Configuration +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + App(darkTheme = isDarkModeOn()) + } + } + + private fun isDarkModeOn(): Boolean { + val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return currentNightMode == Configuration.UI_MODE_NIGHT_YES + } +} + + +@Preview +@Composable +fun AppAndroidDarkPreview() { + App(darkTheme = true) +} + +@Preview +@Composable +fun AppAndroidLightPreview() { + App(darkTheme = false) +} diff --git a/composeApp/src/androidMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.android.kt b/composeApp/src/androidMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.android.kt new file mode 100644 index 0000000..02c8c51 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.android.kt @@ -0,0 +1,21 @@ +package com.mohsenoid.rickandmorty.theme + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +@Composable +actual fun SetStatusBarColor(darkTheme: Boolean, color: Color) { + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = color.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } +} diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png differ diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png differ diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml new file mode 100644 index 0000000..b6c105f --- /dev/null +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -0,0 +1,3 @@ + + Rick and Morty + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/App.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/App.kt new file mode 100644 index 0000000..65226dd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/App.kt @@ -0,0 +1,138 @@ +package com.mohsenoid.rickandmorty + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.mohsenoid.rickandmorty.episode.data.EpisodeRemoteDataSource +import com.mohsenoid.rickandmorty.episode.data.EpisodeRepositoryImpl +import com.mohsenoid.rickandmorty.episode.presentation.EpisodeScreen +import com.mohsenoid.rickandmorty.episode.presentation.EpisodeViewModel +import com.mohsenoid.rickandmorty.episodes.data.EpisodesRemoteDataSource +import com.mohsenoid.rickandmorty.episodes.data.EpisodesRepositoryImpl +import com.mohsenoid.rickandmorty.episodes.presentation.EpisodesScreen +import com.mohsenoid.rickandmorty.episodes.presentation.EpisodesViewModel +import com.mohsenoid.rickandmorty.theme.Theme +import dev.icerock.moko.mvvm.compose.getViewModel +import dev.icerock.moko.mvvm.compose.viewModelFactory +import moe.tlaster.precompose.PreComposeApp +import moe.tlaster.precompose.navigation.NavHost +import moe.tlaster.precompose.navigation.rememberNavigator +import moe.tlaster.precompose.navigation.transition.NavTransition + +@Composable +fun App(darkTheme: Boolean) { + PreComposeApp { + Theme(darkTheme = darkTheme) { + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + Actionbar( + title = "Rick and Morty", + modifier = Modifier.fillMaxWidth(), + ) + }, + ) { paddingValues -> + val navigator = rememberNavigator() + NavHost( + modifier = Modifier.padding(paddingValues), + navigator = navigator, + navTransition = NavTransition(), + initialRoute = NavRoute.EpisodesScreen.route, + ) { + scene( + route = NavRoute.EpisodesScreen.route, + navTransition = NavTransition(), + ) { + val episodesViewModel = + getViewModel( + key = Unit, + factory = viewModelFactory { + EpisodesViewModel( + repository = EpisodesRepositoryImpl(remote = EpisodesRemoteDataSource()), + ) + }, + ) + val uiState by episodesViewModel.uiState.collectAsState() + LaunchedEffect(episodesViewModel) { + episodesViewModel.updateEpisodes() + } + + EpisodesScreen( + uiState = uiState, + modifier = Modifier.fillMaxSize(), + onEpisodeClicked = { + navigator.navigate(route = NavRoute.EpisodeScreen.route) + }, + ) + } + scene( + route = NavRoute.EpisodeScreen.route, + navTransition = NavTransition(), + ) { + val episodeViewModel = + getViewModel( + key = Unit, + factory = viewModelFactory { + EpisodeViewModel( + episodeId = 1, + repository = EpisodeRepositoryImpl(remote = EpisodeRemoteDataSource()), + ) + }, + ) + val uiState by episodeViewModel.uiState.collectAsState() + LaunchedEffect(episodeViewModel) { + episodeViewModel.updateEpisode() + } + + EpisodeScreen( + uiState = uiState, + modifier = Modifier.fillMaxSize(), + onCharacterClicked = { + navigator.navigate(route = NavRoute.CharacterScreen.route) + }, + ) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Actionbar(title: String, modifier: Modifier = Modifier) { + TopAppBar( + title = { + Text( + text = title, + color = Color.White, + style = MaterialTheme.typography.titleLarge, + ) + }, + modifier = modifier, + navigationIcon = { + }, + actions = { + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.secondary, + ), + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/NavRoute.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/NavRoute.kt new file mode 100644 index 0000000..914a134 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/NavRoute.kt @@ -0,0 +1,10 @@ +package com.mohsenoid.rickandmorty + +sealed class NavRoute(val route: String) { + + data object EpisodesScreen : NavRoute(route = "/episodes_screen") + + data object EpisodeScreen : NavRoute(route = "/episode_screen") + + data object CharacterScreen : NavRoute(route = "/character_screen") +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/EpisodeRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/EpisodeRemoteDataSource.kt new file mode 100644 index 0000000..a745649 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/EpisodeRemoteDataSource.kt @@ -0,0 +1,32 @@ +package com.mohsenoid.rickandmorty.episode.data + +import com.mohsenoid.rickandmorty.episode.data.model.CharacterResponse +import com.mohsenoid.rickandmorty.episode.data.model.EpisodeResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.serialization.kotlinx.json.json + +class EpisodeRemoteDataSource { + + private val httpClient = HttpClient { + install(ContentNegotiation) { + json() + } + } + + suspend fun fetchEpisode(episodeId: Int): EpisodeResponse = + httpClient + .get("https://rickandmortyapi.com/api/episode/$episodeId") + .body() + + suspend fun fetchCharacters(characterIds: List): List = + httpClient + .get("https://rickandmortyapi.com/api/character/${characterIds.joinToString(",")}") + .body>() + + fun close() { + httpClient.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/EpisodeRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/EpisodeRepositoryImpl.kt new file mode 100644 index 0000000..99b301e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/EpisodeRepositoryImpl.kt @@ -0,0 +1,51 @@ +package com.mohsenoid.rickandmorty.episode.data + +import com.mohsenoid.rickandmorty.episode.data.model.CharacterResponse +import com.mohsenoid.rickandmorty.episode.data.model.EpisodeResponse +import com.mohsenoid.rickandmorty.episode.domain.EpisodeRepository +import com.mohsenoid.rickandmorty.episode.domain.model.Character +import com.mohsenoid.rickandmorty.episode.domain.model.Episode + +internal class EpisodeRepositoryImpl( + private val remote: EpisodeRemoteDataSource, +) : EpisodeRepository { + + override suspend fun getEpisode(episodeId: Int): Episode { + val episodeResponse = remote.fetchEpisode(episodeId) + val characterIds: List = episodeResponse.characters.map { characterUrl -> + characterUrl.substringAfterLast("/").toInt() + } + val characters = remote.fetchCharacters(characterIds).map { characterResponse -> + characterResponse.toCharacter() + } + + return episodeResponse.toEpisode(characters) + } + + private fun EpisodeResponse.toEpisode(characters: List): Episode { + return Episode( + id = id, + name = name, + episode = episode, + characters = characters, + ) + + } + + private fun CharacterResponse.toCharacter() = Character( + id = id, + name = name, + status = status, + species = species, + type = type, + gender = gender, + origin = origin.name, + location = location.name, + image = image, + created = created, + ) + + override fun close() { + remote.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponse.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponse.kt new file mode 100644 index 0000000..2da34d6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponse.kt @@ -0,0 +1,33 @@ +package com.mohsenoid.rickandmorty.episode.data.model + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CharacterResponse( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + @SerialName("status") + val status: String, + @SerialName("species") + val species: String, + @SerialName("type") + val type: String, + @SerialName("gender") + val gender: String, + @SerialName("origin") + val origin: CharacterResponseOrigin, + @SerialName("location") + val location: CharacterResponseLocation, + @SerialName("image") + val image: String, + @SerialName("episode") + val episode: List, + @SerialName("url") + val url: String, + @SerialName("created") + val created: String, +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponseLocation.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponseLocation.kt new file mode 100644 index 0000000..2459da5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponseLocation.kt @@ -0,0 +1,13 @@ +package com.mohsenoid.rickandmorty.episode.data.model + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CharacterResponseLocation( + @SerialName("name") + val name: String, + @SerialName("url") + val url: String +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponseOrigin.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponseOrigin.kt new file mode 100644 index 0000000..9c324fe --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/CharacterResponseOrigin.kt @@ -0,0 +1,13 @@ +package com.mohsenoid.rickandmorty.episode.data.model + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CharacterResponseOrigin( + @SerialName("name") + val name: String, + @SerialName("url") + val url: String +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/EpisodeResponse.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/EpisodeResponse.kt new file mode 100644 index 0000000..cd170ad --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/data/model/EpisodeResponse.kt @@ -0,0 +1,23 @@ +package com.mohsenoid.rickandmorty.episode.data.model + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EpisodeResponse( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + @SerialName("air_date") + val airDate: String, + @SerialName("episode") + val episode: String, + @SerialName("characters") + val characters: List, + @SerialName("url") + val url: String, + @SerialName("created") + val created: String, +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/EpisodeRepository.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/EpisodeRepository.kt new file mode 100644 index 0000000..b4f4e55 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/EpisodeRepository.kt @@ -0,0 +1,8 @@ +package com.mohsenoid.rickandmorty.episode.domain + +import com.mohsenoid.rickandmorty.episode.domain.model.Episode + +interface EpisodeRepository { + suspend fun getEpisode(episodeId: Int): Episode + fun close() +} diff --git a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewCharacterDetails.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/model/Character.kt similarity index 57% rename from app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewCharacterDetails.kt rename to composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/model/Character.kt index 6698cea..d102b92 100644 --- a/app/src/main/kotlin/com/mohsenoid/rickandmorty/view/model/ViewCharacterDetails.kt +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/model/Character.kt @@ -1,14 +1,14 @@ -package com.mohsenoid.rickandmorty.view.model - -data class ViewCharacterDetails( - val id: Int, - val name: String, - val status: String, - val species: String, - val gender: String, - val origin: String, - val location: String, - val imageUrl: String, - val created: String, - val isKilledByUser: Boolean, -) +package com.mohsenoid.rickandmorty.episode.domain.model + +data class Character( + val id: Int, + val name: String, + val status: String, + val species: String, + val type: String, + val gender: String, + val origin: String, + val location: String, + val image: String, + val created: String, +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/model/Episode.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/model/Episode.kt new file mode 100644 index 0000000..ef89d39 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/domain/model/Episode.kt @@ -0,0 +1,8 @@ +package com.mohsenoid.rickandmorty.episode.domain.model + +data class Episode( + val id: Int, + val name: String, + val episode: String, + val characters: List, +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeScreen.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeScreen.kt new file mode 100644 index 0000000..4b4a254 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeScreen.kt @@ -0,0 +1,95 @@ +package com.mohsenoid.rickandmorty.episode.presentation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mohsenoid.rickandmorty.episode.domain.model.Character +import io.kamel.image.KamelImage +import io.kamel.image.asyncPainterResource +import io.ktor.http.Url + +@Composable +fun EpisodeScreen( + uiState: EpisodeUiState, + modifier: Modifier = Modifier, + onCharacterClicked: (characterId: Int) -> Unit, +) { + Column(modifier = modifier) { + when (uiState) { + EpisodeUiState.Loading -> { + AnimatedVisibility(visible = true) { + Text(text = "Loading...", style = MaterialTheme.typography.titleLarge) + } + } + + is EpisodeUiState.Success -> { + AnimatedVisibility(visible = true) { + LazyColumn( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(uiState.episode.characters) { character -> + CharacterItem( + character = character, + onCharacterClicked = onCharacterClicked, + ) + } + } + } + } + + is EpisodeUiState.Error -> { + AnimatedVisibility(visible = true) { + Text( + text = "Error: ${uiState.message}", + style = MaterialTheme.typography.titleLarge, + ) + } + } + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CharacterItem( + character: Character, + modifier: Modifier = Modifier, + onCharacterClicked: (characterId: Int) -> Unit, +) { + Card( + onClick = { onCharacterClicked(character.id) }, + modifier = modifier + .padding(8.dp) + .fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + ) { + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + ) { + KamelImage( + resource = asyncPainterResource(data = Url(character.image)), + modifier = Modifier.fillMaxWidth(), + contentDescription = character.name, + ) + Text(text = character.name, style = MaterialTheme.typography.titleSmall) + Text(text = character.created, style = MaterialTheme.typography.titleLarge) + Text(text = character.status, style = MaterialTheme.typography.titleMedium) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeUiState.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeUiState.kt new file mode 100644 index 0000000..1c5ee2a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeUiState.kt @@ -0,0 +1,9 @@ +package com.mohsenoid.rickandmorty.episode.presentation + +import com.mohsenoid.rickandmorty.episode.domain.model.Episode + +sealed interface EpisodeUiState { + data object Loading : EpisodeUiState + data class Success(val episode: Episode) : EpisodeUiState + data class Error(val message: String) : EpisodeUiState +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeViewModel.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeViewModel.kt new file mode 100644 index 0000000..b50ff79 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episode/presentation/EpisodeViewModel.kt @@ -0,0 +1,28 @@ +package com.mohsenoid.rickandmorty.episode.presentation + +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import com.mohsenoid.rickandmorty.episode.domain.EpisodeRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class EpisodeViewModel( + private val episodeId: Int, + private val repository: EpisodeRepository, +) : ViewModel() { + + private val _uiState: MutableStateFlow = MutableStateFlow(EpisodeUiState.Loading) + val uiState: StateFlow by ::_uiState + + fun updateEpisode() { + viewModelScope.launch { + val episode = repository.getEpisode(episodeId) + _uiState.value = EpisodeUiState.Success(episode) + } + } + + override fun onCleared() { + super.onCleared() + repository.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/EpisodesRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/EpisodesRemoteDataSource.kt new file mode 100644 index 0000000..bd2aa43 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/EpisodesRemoteDataSource.kt @@ -0,0 +1,29 @@ +package com.mohsenoid.rickandmorty.episodes.data + +import com.mohsenoid.rickandmorty.episodes.data.model.EpisodesResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.serialization.kotlinx.json.json + +class EpisodesRemoteDataSource { + + private val httpClient = HttpClient { + install(ContentNegotiation) { + json() + } + } + + suspend fun fetchEpisodes(page: Int): EpisodesResponse = + httpClient + .get("https://rickandmortyapi.com/api/episode") { + parameter("page", page) + } + .body() + + fun close() { + httpClient.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/EpisodesRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/EpisodesRepositoryImpl.kt new file mode 100644 index 0000000..5421683 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/EpisodesRepositoryImpl.kt @@ -0,0 +1,26 @@ +package com.mohsenoid.rickandmorty.episodes.data + +import com.mohsenoid.rickandmorty.episodes.domain.model.Episode +import com.mohsenoid.rickandmorty.episodes.domain.EpisodesRepository + +internal class EpisodesRepositoryImpl( + private val remote: EpisodesRemoteDataSource, +) : EpisodesRepository { + + override suspend fun getEpisodes(page: Int): List { + return remote.fetchEpisodes(page).episodesResponseResults.map { + Episode( + id = it.id, + name = it.name, + airDate = it.airDate, + episode = it.episode, + characters = it.characters, + created = it.created, + ) + } + } + + override fun close() { + remote.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponse.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponse.kt new file mode 100644 index 0000000..169472c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponse.kt @@ -0,0 +1,12 @@ +package com.mohsenoid.rickandmorty.episodes.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EpisodesResponse( + @SerialName("info") + val episodesResponseInfo: EpisodesResponseInfo, + @SerialName("results") + val episodesResponseResults: List +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponseInfo.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponseInfo.kt new file mode 100644 index 0000000..39a278d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponseInfo.kt @@ -0,0 +1,16 @@ +package com.mohsenoid.rickandmorty.episodes.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EpisodesResponseInfo( + @SerialName("count") + val count: Int, + @SerialName("pages") + val pages: Int, + @SerialName("next") + val next: String, + @SerialName("prev") + val prev: String? +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponseResult.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponseResult.kt new file mode 100644 index 0000000..fa052f0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/data/model/EpisodesResponseResult.kt @@ -0,0 +1,22 @@ +package com.mohsenoid.rickandmorty.episodes.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EpisodesResponseResult( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + @SerialName("air_date") + val airDate: String, + @SerialName("episode") + val episode: String, + @SerialName("characters") + val characters: List, + @SerialName("url") + val url: String, + @SerialName("created") + val created: String, +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/domain/EpisodesRepository.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/domain/EpisodesRepository.kt new file mode 100644 index 0000000..261be5d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/domain/EpisodesRepository.kt @@ -0,0 +1,8 @@ +package com.mohsenoid.rickandmorty.episodes.domain + +import com.mohsenoid.rickandmorty.episodes.domain.model.Episode + +interface EpisodesRepository { + suspend fun getEpisodes(page: Int): List + fun close() +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/domain/model/Episode.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/domain/model/Episode.kt new file mode 100644 index 0000000..e7d0ae8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/domain/model/Episode.kt @@ -0,0 +1,10 @@ +package com.mohsenoid.rickandmorty.episodes.domain.model + +data class Episode( + val id: Int, + val name: String, + val airDate: String, + val episode: String, + val characters: List, + val created: String +) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesScreen.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesScreen.kt new file mode 100644 index 0000000..161919e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesScreen.kt @@ -0,0 +1,101 @@ +package com.mohsenoid.rickandmorty.episodes.presentation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.mohsenoid.rickandmorty.episodes.domain.model.Episode + +@Composable +fun EpisodesScreen( + uiState: EpisodesUiState, + modifier: Modifier = Modifier, + onEpisodeClicked: (episodeId: Int) -> Unit, +) { + Column(modifier = modifier) { + when (uiState) { + EpisodesUiState.Loading -> { + AnimatedVisibility(visible = true) { + Text(text = "Loading...", style = MaterialTheme.typography.titleLarge) + } + } + + is EpisodesUiState.Success -> { + AnimatedVisibility(visible = uiState.episodes.isNotEmpty()) { + LazyColumn( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(uiState.episodes) { episode -> + EpisodeItem( + episode = episode, + onEpisodeClicked = onEpisodeClicked, + ) + } + } + } + } + + is EpisodesUiState.Error -> { + AnimatedVisibility(visible = true) { + Text( + text = "Error: ${uiState.message}", + style = MaterialTheme.typography.titleLarge, + ) + } + } + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EpisodeItem( + episode: Episode, + modifier: Modifier = Modifier, + onEpisodeClicked: (episodeId: Int) -> Unit, +) { + Card( + onClick = { onEpisodeClicked(episode.id) }, + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.small, + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + ) { + Column( + modifier = Modifier.padding(8.dp).fillMaxWidth(), + ) { + Text( + text = episode.episode, modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = episode.name, + modifier = Modifier.fillMaxWidth().padding(start = 0.dp, top = 4.dp, end = 0.dp, bottom = 4.dp), + color = MaterialTheme.colorScheme.inverseSurface, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = episode.airDate, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyLarge, + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesUiState.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesUiState.kt new file mode 100644 index 0000000..c07b63a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesUiState.kt @@ -0,0 +1,9 @@ +package com.mohsenoid.rickandmorty.episodes.presentation + +import com.mohsenoid.rickandmorty.episodes.domain.model.Episode + +sealed interface EpisodesUiState { + data object Loading : EpisodesUiState + data class Success(val episodes: List) : EpisodesUiState + data class Error(val message: String) : EpisodesUiState +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesViewModel.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesViewModel.kt new file mode 100644 index 0000000..77308f4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/episodes/presentation/EpisodesViewModel.kt @@ -0,0 +1,25 @@ +package com.mohsenoid.rickandmorty.episodes.presentation + +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import com.mohsenoid.rickandmorty.episodes.domain.EpisodesRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class EpisodesViewModel(private val repository: EpisodesRepository) : ViewModel() { + + private val _uiState: MutableStateFlow = MutableStateFlow(EpisodesUiState.Loading) + val uiState: StateFlow by ::_uiState + + fun updateEpisodes() { + viewModelScope.launch { + val episodes = repository.getEpisodes(1) + _uiState.value = EpisodesUiState.Success(episodes) + } + } + + override fun onCleared() { + super.onCleared() + repository.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Color.kt new file mode 100644 index 0000000..d985108 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Color.kt @@ -0,0 +1,10 @@ +@file:Suppress("MagicNumber") + +package com.mohsenoid.rickandmorty.theme + +import androidx.compose.ui.graphics.Color + +val colorPrimary = Color(0xFF00574B) +val colorSecondary = Color(0xFF008577) +val colorTertiary = Color(0xFFD81B60) +val colorSurface = Color(0xFF424242) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.kt new file mode 100644 index 0000000..3f49d94 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.kt @@ -0,0 +1,65 @@ +package com.mohsenoid.rickandmorty.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +private val DarkColorScheme = darkColorScheme( + primary = colorPrimary, + secondary = colorSecondary, + tertiary = colorTertiary, + surface = colorSurface, +) + +private val LightColorScheme = lightColorScheme( + primary = colorPrimary, + secondary = colorSecondary, + tertiary = colorTertiary, + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +val shapes = Shapes( + extraSmall = RoundedCornerShape(2.dp), + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(8.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(32.dp), +) + +@Composable +fun Theme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = when { + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + SetStatusBarColor(darkTheme, colorScheme.primary) + + MaterialTheme( + colorScheme = colorScheme, + shapes = shapes, + typography = Typography, + content = content, + ) +} + +@Composable +expect fun SetStatusBarColor(darkTheme: Boolean, color: Color) diff --git a/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Type.kt new file mode 100644 index 0000000..6be6b54 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/mohsenoid/rickandmorty/theme/Type.kt @@ -0,0 +1,34 @@ +package com.mohsenoid.rickandmorty.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) diff --git a/composeApp/src/commonMain/resources/compose-multiplatform.xml b/composeApp/src/commonMain/resources/compose-multiplatform.xml new file mode 100644 index 0000000..d7bf795 --- /dev/null +++ b/composeApp/src/commonMain/resources/compose-multiplatform.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/composeApp/src/desktopMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.jvm.kt b/composeApp/src/desktopMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.jvm.kt new file mode 100644 index 0000000..82de92c --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/mohsenoid/rickandmorty/theme/Theme.jvm.kt @@ -0,0 +1,9 @@ +package com.mohsenoid.rickandmorty.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +actual fun SetStatusBarColor(darkTheme: Boolean, color: Color) { + // no op +} diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt new file mode 100644 index 0000000..fe5fbc7 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/main.kt @@ -0,0 +1,17 @@ +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.mohsenoid.rickandmorty.App + +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + App(darkTheme = true) + } +} + +@Preview +@Composable +fun AppDesktopPreview() { + App(darkTheme = true) +} diff --git a/composeApp/src/iosMain/kotlin/com.mohsenoid.rickandmorty.theme/Theme.ios.kt b/composeApp/src/iosMain/kotlin/com.mohsenoid.rickandmorty.theme/Theme.ios.kt new file mode 100644 index 0000000..82de92c --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com.mohsenoid.rickandmorty.theme/Theme.ios.kt @@ -0,0 +1,9 @@ +package com.mohsenoid.rickandmorty.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +actual fun SetStatusBarColor(darkTheme: Boolean, color: Color) { + // no op +} diff --git a/composeApp/src/iosMain/kotlin/com/mohsenoid/rickandmorty/MainViewController.kt b/composeApp/src/iosMain/kotlin/com/mohsenoid/rickandmorty/MainViewController.kt new file mode 100644 index 0000000..4c5cae4 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/mohsenoid/rickandmorty/MainViewController.kt @@ -0,0 +1,6 @@ +package com.mohsenoid.rickandmorty + +import androidx.compose.ui.window.ComposeUIViewController +import com.mohsenoid.rickandmorty.App + +fun MainViewController() = ComposeUIViewController { App(darkTheme = true) } diff --git a/gradle.properties b/gradle.properties index aa522a6..9e52644 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,21 +1,19 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +kotlin.code.style=official + +#Gradle +org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" + + +#Compose +org.jetbrains.compose.experimental.uikit.enabled=true + +#Android android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true -# DataNetworkModule.kt: (22, 2): This class can only be used with the compiler argument '-Xopt-in=kotlin.RequiresOptIn' --Xopt-in=kotlin.RequiresOptIn +android.nonTransitiveRClass=true + +#MPP +kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.mpp.enableCInteropCommonization=true + +#Development +development=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..a73d757 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,57 @@ +[versions] +compose = "1.5.4" +compose-plugin = "1.5.11" +compose-compiler = "1.5.5" +agp = "8.1.4" +android-minSdk = "24" +android-compileSdk = "34" +android-targetSdk = "34" +androidx-activityCompose = "1.8.1" +androidx-core-ktx = "1.12.0" +androidx-appcompat = "1.6.1" +androidx-material = "1.10.0" +androidx-navigation-compose = "2.7.5" +androidx-constraintlayout = "2.1.4" +androidx-test-junit = "1.1.5" +androidx-espresso-core = "3.5.1" +kotlin = "1.9.21" +kotlinx-couroutines = "1.7.3" +junit = "4.13.2" +ktor = "2.3.6" +moko-mvvm = "0.16.1" +kamel = "0.8.3" +precompose = "1.5.8" + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } +androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } +androidx-material3 = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +moko-mvvm-core = { module = "dev.icerock.moko:mvvm-core", version.ref = "moko-mvvm" } +moko-mvvm-compose = { module = "dev.icerock.moko:mvvm-compose", version.ref = "moko-mvvm" } +kamel = { module = "media.kamel:kamel-image", version.ref = "kamel" } +precompose = { module = "moe.tlaster:precompose", version.ref = "precompose" } + +[plugins] +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinXSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c..033e24c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 29e4134..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..fcb6fca 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,98 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +129,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd3..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig new file mode 100644 index 0000000..7b7d0b8 --- /dev/null +++ b/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,3 @@ +TEAM_ID= +BUNDLE_ID=com.mohsenoid.rickandmorty.RickandMorty +APP_NAME=Rick and Morty \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9d88490 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,408 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 7555FF7B242A565900829871 /* .app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = .app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 058557D7273AAEEB004C7B11 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 42799AB246E5F90AF97AA0EF /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 734E0479B5BED29E27C27622 /* xcuserdata */ = { + isa = PBXGroup; + children = ( + 734E0628ABD2E3465859F65E /* mohsenoid.xcuserdatad */, + ); + name = xcuserdata; + path = iosApp.xcodeproj/xcuserdata; + sourceTree = ""; + }; + 734E0628ABD2E3465859F65E /* mohsenoid.xcuserdatad */ = { + isa = PBXGroup; + children = ( + 734E0F4C8C678DC04422D63E /* xcschemes */, + ); + path = mohsenoid.xcuserdatad; + sourceTree = ""; + }; + 734E0F4C8C678DC04422D63E /* xcschemes */ = { + isa = PBXGroup; + children = ( + ); + path = xcschemes; + sourceTree = ""; + }; + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + AB1DB47929225F7C00F7AF9C /* Configuration */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + 42799AB246E5F90AF97AA0EF /* Frameworks */, + 734E0479B5BED29E27C27622 /* xcuserdata */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* .app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF82242A565900829871 /* ContentView.swift */, + 7555FF8C242A565B00829871 /* Info.plist */, + 2152FB032600AC8F00CF470E /* iOSApp.swift */, + 058557D7273AAEEB004C7B11 /* Preview Content */, + ); + path = iosApp; + sourceTree = ""; + }; + AB1DB47929225F7C00F7AF9C /* Configuration */ = { + isa = PBXGroup; + children = ( + AB3632DC29227652001CCB65 /* Config.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, + 7555FF77242A565900829871 /* Sources */, + 7555FF79242A565900829871 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* .app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = orgName; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + 7555FF83242A565900829871 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + composeApp, + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + composeApp, + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} diff --git a/iosApp/iosApp.xcodeproj/xcuserdata/mohsenoid.xcuserdatad/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcuserdata/mohsenoid.xcuserdatad/xcschemes/iosApp.xcscheme new file mode 100644 index 0000000..921065c --- /dev/null +++ b/iosApp/iosApp.xcodeproj/xcuserdata/mohsenoid.xcuserdatad/xcschemes/iosApp.xcscheme @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/iosApp/iosApp.xcodeproj/xcuserdata/mohsenoid.xcuserdatad/xcschemes/xcschememanagement.plist b/iosApp/iosApp.xcodeproj/xcuserdata/mohsenoid.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..fa59f97 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/xcuserdata/mohsenoid.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + iosApp.xcscheme + + orderHint + 0 + + + + diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..ee7e3ca --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..8edf56e --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "app-icon-1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 0000000..53fc536 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..4aa7c53 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..3cd5c32 --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,21 @@ +import UIKit +import SwiftUI +import ComposeApp + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea(.keyboard) // Compose has own keyboard handler + } +} + + + diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 0000000..412e378 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..4aa7c53 --- /dev/null +++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..0648e86 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 5eceb93..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -include ':app' -rootProject.name = 'Rick and Morty' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..11ddb25 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,21 @@ +rootProject.name = "RickandMorty" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} + +include(":composeApp") \ No newline at end of file