diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9836993e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/* +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/README.md b/README.md index c0193b60..082174ac 100644 --- a/README.md +++ b/README.md @@ -12,36 +12,36 @@ http://docs.starwarsfavorites.apiary.io/# ### Lista de Personagens -Para obter os personagens, sua aplicação deverá utilizar o recurso `people` da Swapi (documentação disponível no topo do documento). A aplicação deve exibir todos os 87 personagens e permitir pesquisar o personagem pelo nome. Sugerimos exibir as primeiras páginas enquanto carrega as outras, em um formato de scroll infinito. +Para obter os personagens, sua aplicação deverá utilizar o recurso `people` da Swapi (documentação disponível no topo do documento). A aplicação deve exibir todos os 87 personagens e permitir pesquisar o personagem pelo nome. Sugerimos exibir as primeiras páginas enquanto carrega as outras, em um formato de scroll infinito. ✅ A lista de itens deve exibir as seguintes informações: -+ Nome [name] -+ Altura [height] -+ Genero [gender] -+ Peso [mass] ++ Nome [name] ✔ ++ Altura [height] ✔ ++ Genero [gender] ✔ ++ Peso [mass] ✔ -Os dados devem ser salvos em banco de dados local para acesso offline e atualizados sempre que a tela for aberta. +Os dados devem ser salvos em banco de dados local para acesso offline e atualizados sempre que a tela for aberta. ✔ ### Detalhes do Personagem Ao clicar em um item da lista o seu app deve mostrar as informações abaixo: -+ name -+ height -+ mass -+ hair_color -+ skin_color -+ eye_color -+ birth_year -+ gender -+ Nome do Planeta Natal -+ Nome da Espécie ++ name ✔ ++ height ✔ ++ mass ✔ ++ hair_color ✔ ++ skin_color ✔ ++ eye_color ✔ ++ birth_year ✔ ++ gender ✔ ++ Nome do Planeta Natal ✔ ++ Nome da Espécie ✔ -A busca pelo nome do planeta e da espécie deve ser feita em paralelo. +A busca pelo nome do planeta e da espécie deve ser feita em paralelo. ✔ ### Favoritos -Na lista e nos detalhes deve ser possível adicionar e remover um personagem a sua lista de favoritos. Tambem deve ser possível filtrar quais personagens foram favoritados na lista principal. +Na lista e nos detalhes deve ser possível adicionar e remover um personagem a sua lista de favoritos. ✔ Tambem deve ser possível filtrar quais personagens foram favoritados na lista principal. ✔ ##### Adição e Remoção de Favoritos @@ -49,14 +49,31 @@ URL BASE: http://private-782d3-starwarsfavorites.apiary-mock.com/ Ao adicionar um favorito a aplicação deve fazer um request para a api starwarsfavorites (documentação disponível no topo do documento). A aplicação deve: -+ Exibir a mensagem de retorno da API em caso de sucesso ou erro. -+ Reenviar a requisição da próxima vez que o app for aberto em caso de erro. -+ Salvar no banco de dados local quais personagens foram favoritados. -+ Tratar a remoção de favoritos apenas no banco de dados local. ++ Exibir a mensagem de retorno da API em caso de sucesso ou erro. ✔ ++ Reenviar a requisição da próxima vez que o app for aberto em caso de erro. ✔ ++ Salvar no banco de dados local quais personagens foram favoritados. ✔ ++ Tratar a remoção de favoritos apenas no banco de dados local. ✔ -Em metade das requisições enviadas para a api starwarsfavorites a aplicação deve adicionar o header `Prefer` com o valor `status=400`. +Em metade das requisições enviadas para a api starwarsfavorites a aplicação deve adicionar o header `Prefer` com o valor `status=400`. ✔ -P.S.: O candidato deve escolher o ID. +P.S.: O candidato deve escolher o ID. ✔ + +# Implementações diferenciadas 🚀 + ++ O aplicativo também fica bonito quando está na horizontal. ✅ ++ O aplicativo pode mudar de tema, podendo implementar mais temas de cores diferentes facilmente. ✅ ++ O aplicativo tem animações de views compartilhadas. ✅ ++ O aplicativo tem animações na recyclerview. ✅ ++ O aplicativo tem animações na tela de detalhes. ✅ + +##### Bugs na implementação 👀 + ++ Mostra os itens favoritados quando é feito uma pesquisa, mesmo se o item não tiver nada haver com a pesquisa. + +###### Possiveis soluções + ++ Ao invés de usar o RemoteMediator para pesquisar e ver todos os itens, pode-se separar e usar um PageSource e verificar se há algum favorito na lista ++ Usar tabelas diferentes --- #### LICENSE diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..805d3866 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,132 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' + id 'androidx.navigation.safeargs' +} + +android { + compileSdk 31 + + defaultConfig { + applicationId "com.arthurgonzaga.wikistarwars" + minSdk 23 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + // Exporting room schema + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + viewBinding true + dataBinding true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.6.0' + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + + def retrofit_verion = "2.9.0" + def gson_version = "2.9.0" + def paging_version = "3.0.1" + def room_version = "2.3.0" + def lifecycle_version = "2.2.0" + def rxjava_version = "3.0.0" + def rv_animators_version = "4.0.2" + + // Retrofit + + implementation "com.squareup.retrofit2:retrofit:$retrofit_verion" + implementation "com.squareup.retrofit2:converter-gson:$gson_version" + implementation "com.github.akarnokd:rxjava3-retrofit-adapter:$rxjava_version" + + // Navigation Component + + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + + // Paging 3 + + implementation "androidx.paging:paging-runtime-ktx:$paging_version" + implementation "androidx.paging:paging-rxjava3:$paging_version" + + // Room + + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + + // Hilt + + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + + + // Lifecycle + + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" + + + //RxJava + implementation "io.reactivex.rxjava3:rxjava:$rxjava_version" + implementation "io.reactivex.rxjava3:rxandroid:$rxjava_version" + + // RecyclerView Animators + + implementation "jp.wasabeef:recyclerview-animators:$rv_animators_version" + + // Testing + + def mockito_version = "2.21.0" + def arch_version = "2.1.0" + def fragment_version = "1.4.0-alpha09" + def truth_version = "1.1.3" + + + testImplementation 'junit:junit:4.13.2' + testImplementation "androidx.paging:paging-common-ktx:$paging_version" + testImplementation "androidx.room:room-testing:$room_version" + testImplementation "org.mockito:mockito-core:$mockito_version" + testImplementation "androidx.arch.core:core-testing:$arch_version" + testImplementation "com.google.truth:truth:$truth_version" + debugImplementation "androidx.fragment:fragment-testing:$fragment_version" + + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + androidTestImplementation "com.google.truth:truth:$truth_version" + androidTestImplementation "org.mockito:mockito-android:$mockito_version" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/app/schemas/com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB/1.json b/app/schemas/com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB/1.json new file mode 100644 index 00000000..ab1e09de --- /dev/null +++ b/app/schemas/com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB/1.json @@ -0,0 +1,138 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "606fe7d41fc767aace460ba4b8b7991a", + "entities": [ + { + "tableName": "characters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `height` TEXT NOT NULL, `weight` TEXT NOT NULL, `hair_color` TEXT NOT NULL, `skin_color` TEXT NOT NULL, `eye_color` TEXT NOT NULL, `birth_year` TEXT NOT NULL, `gender` TEXT NOT NULL, `specie_name` TEXT, `home_world_name` TEXT, `is_favorite` INTEGER NOT NULL, `is_synchronized_with_backend` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hairColor", + "columnName": "hair_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "skinColor", + "columnName": "skin_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eyeColor", + "columnName": "eye_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthYear", + "columnName": "birth_year", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "specieName", + "columnName": "specie_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeWoldName", + "columnName": "home_world_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSynchronizedWithBackend", + "columnName": "is_synchronized_with_backend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "remote_keys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`characterId` INTEGER NOT NULL, `prevKey` INTEGER, `nextKey` INTEGER, PRIMARY KEY(`characterId`))", + "fields": [ + { + "fieldPath": "characterId", + "columnName": "characterId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prevKey", + "columnName": "prevKey", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextKey", + "columnName": "nextKey", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "characterId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '606fe7d41fc767aace460ba4b8b7991a')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..31932ec5 --- /dev/null +++ b/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.arthurgonzaga.wikistarwars + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.arthurgonzaga.wikistarwars", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ui/fragments/FavoriteListFragmentTest.kt b/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ui/fragments/FavoriteListFragmentTest.kt new file mode 100644 index 00000000..1cab3234 --- /dev/null +++ b/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ui/fragments/FavoriteListFragmentTest.kt @@ -0,0 +1,42 @@ +package com.arthurgonzaga.wikistarwars.ui.fragments + +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.navigation.NavController +import androidx.navigation.Navigation +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack +import com.arthurgonzaga.wikistarwars.R +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class FavoriteListFragmentTest { + + lateinit var navController: NavController + lateinit var scenario: FragmentScenario + + @Before + fun setup() { + navController = mock(NavController::class.java) + scenario = launchFragmentInContainer( + themeResId = R.style.Theme_WikiStarWars_Yellow + ) + + scenario.onFragment{ + Navigation.setViewNavController(it.requireView(), navController) + } + } + + @Test + fun testPopupBackStack() { + + pressBack() + + verify(navController).popBackStack() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ui/fragments/HomeFragmentTest.kt b/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ui/fragments/HomeFragmentTest.kt new file mode 100644 index 00000000..f3724783 --- /dev/null +++ b/app/src/androidTest/java/com/arthurgonzaga/wikistarwars/ui/fragments/HomeFragmentTest.kt @@ -0,0 +1,57 @@ +package com.arthurgonzaga.wikistarwars.ui.fragments + +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.navigation.NavController +import androidx.navigation.Navigation +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import com.arthurgonzaga.wikistarwars.R +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class HomeFragmentTest { + + lateinit var navController: NavController + lateinit var scenario: FragmentScenario + + @Before + fun setup(){ + // Create a TestNavHostController + navController = mock(NavController::class.java) + + // Create a graphical FragmentScenario + scenario = launchFragmentInContainer( + themeResId = R.style.Theme_WikiStarWars_Yellow + ) + + scenario.onFragment { fragment -> + Navigation.setViewNavController(fragment.requireView(), navController) + } + } + + + @Test + fun isFavoriteButtonDisplayed() { + onView(withId(R.id.favorite_list_button)).check(matches(isDisplayed())) + } + + @Test + fun testNavigateToFavoriteListFragment() { + onView(withId(R.id.favorite_list_button)).perform(click()) + verify(navController).navigate(R.id.goToFavoritesListFragment) + } + + @Test + fun testNavigateToDetailFragment() { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6f3857e3 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/CharacterResponse.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/CharacterResponse.kt new file mode 100644 index 00000000..39e00583 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/CharacterResponse.kt @@ -0,0 +1,41 @@ +package com.arthurgonzaga.wikistarwars.api.responses + + +import android.util.Log +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.google.gson.annotations.SerializedName + +data class CharacterResponse( + val name: String = "", + val height: String = "", + @SerializedName("mass") + val weight: String = "", + @SerializedName("hair_color") + val hairColor: String = "", + @SerializedName("skin_color") + val skinColor: String = "", + @SerializedName("eye_color") + val eyeColor: String = "", + @SerializedName("birth_year") + val birthYear: String = "", + val gender: String = "", + @SerializedName("homeworld") + private val homeWorldUrl: String = "", + @SerializedName("species") + private val speciesUrls: List = listOf(), + private val url: String +) { + + fun getId(): Int{ + return url.split("people/")[1].replace("/","").toInt() + } + + fun getHomeWorldId(): Int{ + return homeWorldUrl.split("planets/")[1].replace("/","").toInt() + } + + fun getSpecieId(): Int? { + return speciesUrls.firstOrNull()?.split("species/")?.get(1)?.replace("/","")?.toIntOrNull() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/HomeWorldResponse.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/HomeWorldResponse.kt new file mode 100644 index 00000000..0fa3b253 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/HomeWorldResponse.kt @@ -0,0 +1,3 @@ +package com.arthurgonzaga.wikistarwars.api.responses + +data class HomeWorldResponse(val name: String) \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/PageResponse.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/PageResponse.kt new file mode 100644 index 00000000..a1ae9daf --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/PageResponse.kt @@ -0,0 +1,21 @@ +package com.arthurgonzaga.wikistarwars.api.responses + +data class PageResponse( + val count: Int, + private val next: String?, + private val previous: String?, + val results: List = listOf() +){ + + fun getNextPageIndex(): Int? { + return next?.split("page=")?.get(1)?.toIntOrNull() + } + + fun getPreviousIndex(): Int? { + return previous?.split("page=")?.get(1)?.toIntOrNull() + } + + override fun toString(): String { + return "PageResponse(count=$count, next=${getNextPageIndex()}, previous=${getPreviousIndex()}, result=$results)" + } +} diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/SpecieResponse.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/SpecieResponse.kt new file mode 100644 index 00000000..70e4de58 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/responses/SpecieResponse.kt @@ -0,0 +1,3 @@ +package com.arthurgonzaga.wikistarwars.api.responses + +data class SpecieResponse(val name: String) \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/FavoriteService.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/FavoriteService.kt new file mode 100644 index 00000000..7bf14627 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/FavoriteService.kt @@ -0,0 +1,18 @@ +package com.arthurgonzaga.wikistarwars.api.services + +import retrofit2.Call +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path + +interface FavoriteService { + + @POST("favorite/{id}") + @Headers( + "Prefer: status=200", + "Content-Type:application/json" + ) + fun setFavorite( + @Path("id") id: Int + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/HomeWorldService.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/HomeWorldService.kt new file mode 100644 index 00000000..03757b0f --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/HomeWorldService.kt @@ -0,0 +1,17 @@ +package com.arthurgonzaga.wikistarwars.api.services + +import com.arthurgonzaga.wikistarwars.api.responses.HomeWorldResponse +import com.arthurgonzaga.wikistarwars.api.responses.SpecieResponse +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +interface HomeWorldService { + + @GET("planets/{id}") + fun getHomeWorld( + @Path("id") id: Int + ): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/PeopleService.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/PeopleService.kt new file mode 100644 index 00000000..748010ad --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/PeopleService.kt @@ -0,0 +1,18 @@ +package com.arthurgonzaga.wikistarwars.api.services + +import com.arthurgonzaga.wikistarwars.api.responses.PageResponse +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +interface PeopleService { + + @GET("people") + fun getPeoplePage( + @Query("page") page: Int, + @Query("search") query: String = "" + ): Single + +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/SpeciesService.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/SpeciesService.kt new file mode 100644 index 00000000..01cfb1cb --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/api/services/SpeciesService.kt @@ -0,0 +1,19 @@ +package com.arthurgonzaga.wikistarwars.api.services + +import com.arthurgonzaga.wikistarwars.api.responses.PageResponse +import com.arthurgonzaga.wikistarwars.api.responses.SpecieResponse +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface SpeciesService { + + @GET("species/{id}") + fun getSpecie( + @Path("id") id: Int + ): Single + +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/data/Constants.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/Constants.kt new file mode 100644 index 00000000..6d602de5 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/Constants.kt @@ -0,0 +1,7 @@ +package com.arthurgonzaga.wikistarwars.data + +object Constants { + + const val BASE_URL = "https://swapi.dev/api/" + const val FAVORITE_API_BASE_URL = "https://private-782d3-starwarsfavorites.apiary-mock.com" +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/data/WikiStarWarsDB.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/WikiStarWarsDB.kt new file mode 100644 index 00000000..4810ed36 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/WikiStarWarsDB.kt @@ -0,0 +1,19 @@ +package com.arthurgonzaga.wikistarwars.data + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.arthurgonzaga.wikistarwars.data.dao.CharacterDAO +import com.arthurgonzaga.wikistarwars.data.dao.RemoteKeysDAO +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.data.model.RemoteKeys + +@Database(entities = [CharacterEntity::class, RemoteKeys::class], version = 1) +abstract class WikiStarWarsDB: RoomDatabase() { + + abstract fun charactersDAO(): CharacterDAO + abstract fun remoteKeysDAO(): RemoteKeysDAO + + companion object { + const val NAME = "wiki_starwars_db" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/data/dao/CharacterDAO.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/dao/CharacterDAO.kt new file mode 100644 index 00000000..fbe7cad0 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/dao/CharacterDAO.kt @@ -0,0 +1,60 @@ +package com.arthurgonzaga.wikistarwars.data.dao + +import androidx.paging.PagingData +import androidx.paging.PagingSource +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity + +import androidx.room.* + + +@Dao +interface CharacterDAO { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertAll(characterEntity: List): List + + + @Query("SELECT id FROM characters WHERE is_favorite = 1") + suspend fun getAllFavoritesIds(): List + + @Query("SELECT id FROM characters WHERE is_favorite = 1 AND is_synchronized_with_backend = 0") + suspend fun getAllFavoritesIdsNotInSync(): List + + @Query("UPDATE characters SET is_synchronized_with_backend = 1 WHERE id= :id") + suspend fun setInSync(id: Int) + + + @Query("SELECT id FROM characters WHERE is_favorite = 1") + fun getAllFavoritesIdsUpsert(): List + + @Query("SELECT * FROM CHARACTERS WHERE is_favorite = 1") + fun getAllFavoriteCharacters(): PagingSource + + @Query("UPDATE characters SET is_favorite = 1, is_synchronized_with_backend = :isSynchronized WHERE id= :id") + suspend fun favorite(id: Int, isSynchronized: Boolean = false) + + @Query("UPDATE characters SET is_favorite = 0 WHERE id = :id") + suspend fun unFavorite(id: Int) + + @Query("SELECT * FROM characters") + fun getAllCharacters(): PagingSource + + @Query("DELETE FROM characters WHERE is_favorite = 0") + fun clearCharacters() + + @Query("UPDATE characters SET is_favorite = 1 WHERE id IN (:ids)") + fun update( + ids: List + ) + + + @Transaction + fun upsert(obj: List) { + val favoriteIds = getAllFavoritesIdsUpsert() + clearCharacters() + + insertAll(obj); + + update(favoriteIds) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/data/dao/RemoteKeysDAO.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/dao/RemoteKeysDAO.kt new file mode 100644 index 00000000..980ad283 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/dao/RemoteKeysDAO.kt @@ -0,0 +1,33 @@ +package com.arthurgonzaga.wikistarwars.data.dao + +import androidx.room.* +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.data.model.RemoteKeys + +@Dao +interface RemoteKeysDAO { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertAll(remoteKey: List): List + + @Update(onConflict = OnConflictStrategy.IGNORE) + fun updateAll(remoteKey: List) + + + @Query("SELECT * FROM remote_keys WHERE characterId = :id") + fun remoteKeysById(id: Int): RemoteKeys? + + @Query("DELETE FROM remote_keys") + fun clearRemoteKeys() + + + + @Transaction + fun upsert(remoteKey: List) { + val longs: List = insertAll(remoteKey) + + if (longs.contains(-1)) { + updateAll(remoteKey) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/data/model/CharacterEntity.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/model/CharacterEntity.kt new file mode 100644 index 00000000..e1daf1a2 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/model/CharacterEntity.kt @@ -0,0 +1,34 @@ +package com.arthurgonzaga.wikistarwars.data.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.arthurgonzaga.wikistarwars.util.toEntity +import io.reactivex.rxjava3.core.Observable +import java.io.Serializable + +@Entity(tableName = "characters") +data class CharacterEntity( + @PrimaryKey + val id: Int, + val name: String = "", + val height: String = "", + val weight: String = "", + @ColumnInfo(name = "hair_color") + val hairColor: String = "", + @ColumnInfo(name = "skin_color") + val skinColor: String = "", + @ColumnInfo(name = "eye_color") + val eyeColor: String = "", + @ColumnInfo(name = "birth_year") + val birthYear: String = "", + val gender: String = "", + @ColumnInfo(name = "specie_name") + val specieName: String? = null, + @ColumnInfo(name = "home_world_name") + val homeWoldName: String? = null, + @ColumnInfo(name = "is_favorite") + var isFavorite: Boolean = false, + @ColumnInfo(name = "is_synchronized_with_backend") + val isSynchronizedWithBackend: Boolean = false, +): Serializable diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/data/model/RemoteKeys.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/model/RemoteKeys.kt new file mode 100644 index 00000000..16955cd8 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/data/model/RemoteKeys.kt @@ -0,0 +1,12 @@ +package com.arthurgonzaga.wikistarwars.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "remote_keys") +data class RemoteKeys( + @PrimaryKey + val characterId: Int, + val prevKey: Int?, + val nextKey: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/di/AppApplication.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/AppApplication.kt new file mode 100644 index 00000000..76755a20 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/AppApplication.kt @@ -0,0 +1,7 @@ +package com.arthurgonzaga.wikistarwars.di + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class AppApplication: Application() \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/di/ApplicationModule.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/ApplicationModule.kt new file mode 100644 index 00000000..d290be31 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/ApplicationModule.kt @@ -0,0 +1,70 @@ +package com.arthurgonzaga.wikistarwars.di + +import android.app.Application +import androidx.paging.ExperimentalPagingApi +import androidx.room.Room +import com.arthurgonzaga.wikistarwars.api.services.FavoriteService +import com.arthurgonzaga.wikistarwars.api.services.HomeWorldService +import com.arthurgonzaga.wikistarwars.api.services.PeopleService +import com.arthurgonzaga.wikistarwars.api.services.SpeciesService +import com.arthurgonzaga.wikistarwars.data.Constants +import com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB +import com.arthurgonzaga.wikistarwars.data.dao.CharacterDAO +import com.arthurgonzaga.wikistarwars.repository.HomeRepositoryImpl +import com.arthurgonzaga.wikistarwars.repository.MainRepositoryImpl +import com.arthurgonzaga.wikistarwars.repository.interfaces.HomeRepository +import com.arthurgonzaga.wikistarwars.repository.interfaces.MainRepository +import com.arthurgonzaga.wikistarwars.util.getRetrofitInstance +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ApplicationModule { + + @Provides + @Singleton + fun providePeopleService(): PeopleService = getRetrofitInstance() + + @Provides + @Singleton + fun provideFavoriteService(): FavoriteService = + getRetrofitInstance(Constants.FAVORITE_API_BASE_URL) + + @Provides + @Singleton + fun provideSpeciesService(): SpeciesService = getRetrofitInstance() + + @Provides + @Singleton + fun provideHomeWorldService(): HomeWorldService = getRetrofitInstance() + + @Provides + @Singleton + fun provideRoomDatabase( + application: Application + ): WikiStarWarsDB = Room.databaseBuilder( + application, + WikiStarWarsDB::class.java, + WikiStarWarsDB.NAME + ).fallbackToDestructiveMigration().build() + + @Provides + @Singleton + fun provideCharacterDAO( + database: WikiStarWarsDB + ): CharacterDAO = database.charactersDAO() + + + @Provides + @Singleton + fun provideMainRepository( + favoriteService: FavoriteService, + dao: CharacterDAO + ): MainRepository = MainRepositoryImpl(favoriteService, dao) + +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/di/FavoritesComponent.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/FavoritesComponent.kt new file mode 100644 index 00000000..1e7e6930 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/FavoritesComponent.kt @@ -0,0 +1,26 @@ +package com.arthurgonzaga.wikistarwars.di + +import androidx.paging.ExperimentalPagingApi +import com.arthurgonzaga.wikistarwars.api.services.PeopleService +import com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB +import com.arthurgonzaga.wikistarwars.repository.FavoritesRepositoryImpl +import com.arthurgonzaga.wikistarwars.repository.HomeRepositoryImpl +import com.arthurgonzaga.wikistarwars.repository.interfaces.FavoritesRepository +import com.arthurgonzaga.wikistarwars.repository.interfaces.HomeRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +object FavoritesComponent { + + @ExperimentalPagingApi + @Provides + @ViewModelScoped + fun provideFavoritesRepository( + db: WikiStarWarsDB + ): FavoritesRepository = FavoritesRepositoryImpl(db) +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/di/HomeComponent.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/HomeComponent.kt new file mode 100644 index 00000000..312f7d46 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/di/HomeComponent.kt @@ -0,0 +1,29 @@ +package com.arthurgonzaga.wikistarwars.di + +import androidx.paging.ExperimentalPagingApi +import com.arthurgonzaga.wikistarwars.api.services.HomeWorldService +import com.arthurgonzaga.wikistarwars.api.services.PeopleService +import com.arthurgonzaga.wikistarwars.api.services.SpeciesService +import com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB +import com.arthurgonzaga.wikistarwars.repository.HomeRepositoryImpl +import com.arthurgonzaga.wikistarwars.repository.interfaces.HomeRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +object HomeComponent { + + @ExperimentalPagingApi + @Provides + @ViewModelScoped + fun provideHomeRepository( + peopleService: PeopleService, + speciesService: SpeciesService, + homeWorldService: HomeWorldService, + db: WikiStarWarsDB + ): HomeRepository = HomeRepositoryImpl(peopleService, speciesService, homeWorldService, db) +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/FavoritesRepositoryImpl.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/FavoritesRepositoryImpl.kt new file mode 100644 index 00000000..a9353a98 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/FavoritesRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.arthurgonzaga.wikistarwars.repository + +import androidx.lifecycle.LiveData +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.liveData +import com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB +import com.arthurgonzaga.wikistarwars.data.dao.CharacterDAO +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.repository.interfaces.FavoritesRepository +import javax.inject.Inject + +class FavoritesRepositoryImpl @Inject constructor( + val db: WikiStarWarsDB +) : FavoritesRepository{ + + override fun getFavoriteCharacters(): LiveData> { + return Pager( + config = PagingConfig(pageSize = 10, enablePlaceholders = false) + ){ + db.charactersDAO().getAllFavoriteCharacters() + }.liveData + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/HomeRepositoryImpl.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/HomeRepositoryImpl.kt new file mode 100644 index 00000000..5f31531b --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/HomeRepositoryImpl.kt @@ -0,0 +1,40 @@ +package com.arthurgonzaga.wikistarwars.repository + +import androidx.lifecycle.LiveData +import androidx.paging.* +import com.arthurgonzaga.wikistarwars.api.services.HomeWorldService +import com.arthurgonzaga.wikistarwars.api.services.PeopleService +import com.arthurgonzaga.wikistarwars.api.services.SpeciesService +import com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.repository.interfaces.HomeRepository +import com.arthurgonzaga.wikistarwars.repository.paging.CharacterRemoteMediator +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import javax.inject.Inject + + +@ExperimentalPagingApi +class HomeRepositoryImpl @Inject constructor( + private val peopleService: PeopleService, + private val speciesService: SpeciesService, + private val homeWorldService: HomeWorldService, + private val database: WikiStarWarsDB +) : HomeRepository { + + override fun getCharacters(query: String): LiveData> { + val pagingSourceFactory = { database.charactersDAO().getAllCharacters() } + return Pager( + config = PagingConfig(pageSize = 10, enablePlaceholders = false), + pagingSourceFactory = pagingSourceFactory, + initialKey = 1, + remoteMediator = CharacterRemoteMediator( + peopleService, + speciesService, + homeWorldService, + database, + query + ) + ).liveData + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/MainRepositoryImpl.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/MainRepositoryImpl.kt new file mode 100644 index 00000000..fc2f619f --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/MainRepositoryImpl.kt @@ -0,0 +1,47 @@ +package com.arthurgonzaga.wikistarwars.repository + +import android.util.Log +import com.arthurgonzaga.wikistarwars.api.services.FavoriteService +import com.arthurgonzaga.wikistarwars.api.services.PeopleService +import com.arthurgonzaga.wikistarwars.data.dao.CharacterDAO +import com.arthurgonzaga.wikistarwars.repository.interfaces.MainRepository +import com.arthurgonzaga.wikistarwars.viewmodel.HomeViewModel +import retrofit2.HttpException +import retrofit2.await +import retrofit2.awaitResponse +import javax.inject.Inject + +class MainRepositoryImpl @Inject constructor( + private val favoriteService: FavoriteService, + private val dao: CharacterDAO +): MainRepository { + + override suspend fun favoriteCharacter(characterId: Int): Boolean { + return try { + dao.favorite(characterId, false) + val isSuccessful = favoriteService.setFavorite(characterId).awaitResponse().isSuccessful + dao.favorite(characterId, isSuccessful) + + isSuccessful + }catch (e: Exception){ + false + } + } + + override suspend fun unFavoriteCharacter(characterId: Int) { + dao.unFavorite(characterId) + } + + override suspend fun synchronizeFavoriteCharacters(): Boolean { + val ids = dao.getAllFavoritesIdsNotInSync() + + val syncResults = mutableListOf() + ids.map { + val isSuccessful = favoriteService.setFavorite(it).awaitResponse().isSuccessful + dao.setInSync(it) + syncResults.add(isSuccessful) + } + return !syncResults.contains(false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/FavoritesRepository.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/FavoritesRepository.kt new file mode 100644 index 00000000..2bf2e0f4 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/FavoritesRepository.kt @@ -0,0 +1,10 @@ +package com.arthurgonzaga.wikistarwars.repository.interfaces + +import androidx.lifecycle.LiveData +import androidx.paging.PagingData +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity + +interface FavoritesRepository { + + fun getFavoriteCharacters(): LiveData> +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/HomeRepository.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/HomeRepository.kt new file mode 100644 index 00000000..8540c438 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/HomeRepository.kt @@ -0,0 +1,12 @@ +package com.arthurgonzaga.wikistarwars.repository.interfaces + +import androidx.lifecycle.LiveData +import androidx.paging.PagingData +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import kotlinx.coroutines.flow.Flow + +interface HomeRepository { + + fun getCharacters(query: String): LiveData> + +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/MainRepository.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/MainRepository.kt new file mode 100644 index 00000000..f57b2844 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/interfaces/MainRepository.kt @@ -0,0 +1,10 @@ +package com.arthurgonzaga.wikistarwars.repository.interfaces + +interface MainRepository { + + suspend fun favoriteCharacter(characterId: Int): Boolean + + suspend fun unFavoriteCharacter(characterId: Int) + + suspend fun synchronizeFavoriteCharacters(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/paging/CharacterRemoteMediator.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/paging/CharacterRemoteMediator.kt new file mode 100644 index 00000000..add3ca3a --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/repository/paging/CharacterRemoteMediator.kt @@ -0,0 +1,197 @@ +package com.arthurgonzaga.wikistarwars.repository.paging + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.paging.rxjava3.RxRemoteMediator +import androidx.room.withTransaction +import com.arthurgonzaga.wikistarwars.api.responses.CharacterResponse +import com.arthurgonzaga.wikistarwars.api.responses.SpecieResponse +import com.arthurgonzaga.wikistarwars.api.services.HomeWorldService +import com.arthurgonzaga.wikistarwars.api.services.PeopleService +import com.arthurgonzaga.wikistarwars.api.services.SpeciesService +import com.arthurgonzaga.wikistarwars.data.WikiStarWarsDB +import com.arthurgonzaga.wikistarwars.data.dao.CharacterDAO +import com.arthurgonzaga.wikistarwars.data.dao.RemoteKeysDAO +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.data.model.RemoteKeys +import com.arthurgonzaga.wikistarwars.util.toEntity +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import retrofit2.HttpException +import retrofit2.await +import java.io.IOException +import java.io.InvalidObjectException + + +@ExperimentalPagingApi +class CharacterRemoteMediator( + private val peopleService: PeopleService, + private val speciesService: SpeciesService, + private val homeWorldService: HomeWorldService, + private val database: WikiStarWarsDB, + private val query: String, +) : RxRemoteMediator() { + + override fun initializeSingle(): Single { + return Single.just(InitializeAction.LAUNCH_INITIAL_REFRESH) + } + + override fun loadSingle( + loadType: LoadType, + state: PagingState + ): Single { + return Single.just(loadType) + .subscribeOn(Schedulers.io()) + .map { + when (it) { + LoadType.REFRESH -> { + Log.i(TAG, "LoadType.REFRESH") + val remoteKeys = getClosestRemoteKey(state) + remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX + } + LoadType.APPEND -> { + Log.i(TAG, "LoadType.APPEND") + val remoteKeys = getLastRemoteKey(state) + ?: throw InvalidObjectException("Remote key should not be null for $it") + remoteKeys.nextKey ?: INVALID_PAGE + } + LoadType.PREPEND -> { + Log.i(TAG, "LoadType.PREPEND") + val remoteKeys = getFirstRemoteKey(state) + ?: throw InvalidObjectException("Invalid state, key should not be null") + //end of list condition reached + remoteKeys.prevKey ?: INVALID_PAGE + } + } + }.flatMap { page -> + if (page == INVALID_PAGE) { + Single.just(MediatorResult.Success(endOfPaginationReached = true)) + } else { + getCharactersPage(page).map { insertOnDB(page, loadType, it) } + .map { MediatorResult.Success(endOfPaginationReached = it.isEmpty()) } + .onErrorReturn { MediatorResult.Error(it) } + } + }.onErrorReturn { MediatorResult.Error(it) } + } + + + + @Suppress("DEPRECATION") + fun insertOnDB( + page: Int, + loadType: LoadType, + characters: List, + ) : List{ + database.beginTransaction() + + try { + if (loadType == LoadType.REFRESH) { + // clear all the rows in the database + database.remoteKeysDAO().clearRemoteKeys() + + // clear all the rows with isFavorite = false + // update all the rows with isFavorite = true + database.charactersDAO().upsert(characters) + // FIXME: 9/28/2021 (need to separate the favorites from the CharacterEntity model) + // BUG: it shows the search and the favorites, even if they has anything to do with the query + } + + val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1 + val nextKey = if (characters.isEmpty()) null else page + 1 + + val keys = characters.map { + RemoteKeys(characterId = it.id, prevKey = prevKey, nextKey = nextKey) + } + + + database.remoteKeysDAO().insertAll(keys) + database.charactersDAO().insertAll(characters) + database.setTransactionSuccessful() + }finally { + database.endTransaction() + } + + return characters + } + + fun getCharactersPage(page: Int): Single> { + return peopleService.getPeoplePage(page, query).map { it.results } + .flattenAsObservable { list -> + list + } + .flatMap { characterResponse -> + + val homeWorld = + homeWorldService.getHomeWorld(characterResponse.getHomeWorldId()) + val specie = + characterResponse.getSpecieId()?.let { speciesService.getSpecie(it) } + ?.subscribeOn(Schedulers.newThread()) ?: Single.just( + SpecieResponse( + "Human" + ) + ) + + Observable.zip( + homeWorld.toObservable(), + specie.toObservable() + ) { homeWorldResponse, specieResponse -> + characterResponse.toEntity( + homeWorldName = homeWorldResponse.name, + specieName = specieResponse.name + ) + } + } + .toList() + } + + /** + * get the last remote key inserted which had the data + */ + private fun getLastRemoteKey(state: PagingState): RemoteKeys? { + return state.pages + .lastOrNull { it.data.isNotEmpty() } + ?.data?.lastOrNull() + ?.let { character -> database.remoteKeysDAO().remoteKeysById(character.id) } + ?: RemoteKeys(0, null, 2) + } + + /** + * get the first remote key inserted which had the data + */ + private fun getFirstRemoteKey(state: PagingState): RemoteKeys? { + return state.pages + .firstOrNull() { it.data.isNotEmpty() } + ?.data?.firstOrNull() + ?.let { character -> database.remoteKeysDAO().remoteKeysById(character.id) } + ?: RemoteKeys(0, null, 2) + } + + /** + * get the closest remote key inserted which had the data + */ + private fun getClosestRemoteKey(state: PagingState): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.id?.let { id -> + database.remoteKeysDAO().remoteKeysById(id) + } + } + } + + + companion object { + private const val STARTING_PAGE_INDEX = 1 + private const val TAG = "CharacterRemoteMediator" + private const val INVALID_PAGE = -1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/MainActivity.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/MainActivity.kt new file mode 100644 index 00000000..3c4d8389 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/MainActivity.kt @@ -0,0 +1,41 @@ +package com.arthurgonzaga.wikistarwars.ui + +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.annotation.IdRes +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.ui.components.MyThemes +import com.arthurgonzaga.wikistarwars.viewmodel.MainViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + + private val vm : MainViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTheme(getMyTheme()) + setContentView(R.layout.activity_main) + + vm.inSync.observe(this){ + if(it == true){ + Toast.makeText(this, R.string.sync_success, Toast.LENGTH_SHORT).show() + } + } + } + + private fun getMyTheme(): Int{ + val sharedPref = this.getSharedPreferences(getString(R.string.theme_key), Context.MODE_PRIVATE) + + return when(sharedPref.getInt("theme", MyThemes.YELLOW.ordinal)){ + MyThemes.YELLOW.ordinal -> R.style.Theme_WikiStarWars_Yellow + MyThemes.BLUE.ordinal -> R.style.Theme_WikiStarWars_Blue + MyThemes.RED.ordinal -> R.style.Theme_WikiStarWars_Red + else -> R.style.Theme_WikiStarWars_Yellow + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/adapters/CharacterAdapter.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/adapters/CharacterAdapter.kt new file mode 100644 index 00000000..3ad06517 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/adapters/CharacterAdapter.kt @@ -0,0 +1,102 @@ +package com.arthurgonzaga.wikistarwars.ui.adapters + +import android.app.Application +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.databinding.RvCharacterItemBinding +import com.arthurgonzaga.wikistarwars.util.setImage +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class CharacterAdapter( + private val context: Context, + private val navigateToDetail: (CharacterEntity, TextView, ImageButton, ViewGroup) -> Unit, + private val favoriteCharacter: (characterId: Int, isFavorite: Boolean) -> Unit +) : PagingDataAdapter(DiffUtilCallback) { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val binding = + RvCharacterItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return VH(binding) + } + + override fun onBindViewHolder(holder: VH, position: Int) { + val currentItem = getItem(position) + + currentItem?.let { character -> + holder.binding.gender.text = character.gender + holder.binding.subtitle.text = context.getString(R.string.rv_subtitle, character.height, character.weight) + holder.binding.title.apply { + text = character.name + transitionName = "heading_small$position"; + } + holder.binding.item.transitionName = "rv_background$position" + holder.binding.root.apply { + transitionName = "rv_background$position" + setOnClickListener { _ -> + navigateToDetail( + character, + holder.binding.title, + holder.binding.favoriteButton, + holder.binding.item + ) + } + } + + holder.binding.favoriteButton.apply { + setImage(character.isFavorite) + transitionName = "rv_favorite_btn$position" + setOnClickListener { _ -> + favoriteCharacter(character.id, !character.isFavorite) + } + + setOnTouchListener { view, motionEvent -> + when (motionEvent.action) { + // User touched + MotionEvent.ACTION_DOWN -> { + setImage(!character.isFavorite) + } + // User has canceled the touch + MotionEvent.ACTION_CANCEL -> { + setImage(character.isFavorite) + } + // User has canceled release the touch + MotionEvent.ACTION_UP -> { + performClick() // this calls setOnClickListener + } + } + true + } + } + } + } + + + class VH(val binding: RvCharacterItemBinding) : RecyclerView.ViewHolder(binding.root) + + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CharacterEntity, newItem: CharacterEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: CharacterEntity, + newItem: CharacterEntity + ): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/CharacterInfo.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/CharacterInfo.kt new file mode 100644 index 00000000..89beaaf4 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/CharacterInfo.kt @@ -0,0 +1,103 @@ +package com.arthurgonzaga.wikistarwars.ui.components + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.databinding.CharacterInfoLayoutBinding + + +private const val TAG = "CharacterInfo" + +class CharacterInfo @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + private lateinit var titleStr: String + private lateinit var subtitleStr: String + + lateinit var binding: CharacterInfoLayoutBinding + + private var shortAnimationDuration: Int = 0 + + + init { + shortAnimationDuration = resources.getInteger(android.R.integer.config_mediumAnimTime) + + context?.let { + // Get the attribute values + it.theme.obtainStyledAttributes( + attrs, R.styleable.CharacterInfo, + 0, 0 + ).apply { + try { + titleStr = this.getString(R.styleable.CharacterInfo_title) ?: "" + subtitleStr = this.getString(R.styleable.CharacterInfo_subtitle) ?: "" + } finally { + recycle() + } + } + + binding = CharacterInfoLayoutBinding.inflate(LayoutInflater.from(it)) + + binding.title.text = titleStr.replaceFirstChar(Char::uppercase) + binding.subtitle.text = subtitleStr.replaceFirstChar(Char::uppercase) + addView(binding.root) + } + } + + fun setTitle(text: String) { + binding.title.text = text + } + + fun setSubtitle(text: String) { + binding.subtitle.text = text + } + + /** + * Show the view with a fading in animation + */ + fun show() { + Log.i(TAG, "showing $titleStr") + this.apply { + // Set the content view to 0% opacity but visible, so that it is visible + // (but fully transparent) during the animation. + alpha = 0f + visibility = View.VISIBLE + + // Animate the content view to 100% opacity, and clear any animation + // listener set on the view. + animate() + .alpha(1f) + .setDuration(shortAnimationDuration.toLong()) + .setListener(null) + } + } + + /** + * Hide the view with a fading out animation + */ + fun hide() { + binding.root.apply { + animate() + .alpha(0f) + .setDuration(shortAnimationDuration.toLong()) + .setListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + visibility = View.GONE + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/MyThemes.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/MyThemes.kt new file mode 100644 index 00000000..259d38a9 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/MyThemes.kt @@ -0,0 +1,7 @@ +package com.arthurgonzaga.wikistarwars.ui.components + +enum class MyThemes { + YELLOW, + BLUE, + RED +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/SpacingItemDecoration.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/SpacingItemDecoration.kt new file mode 100644 index 00000000..df8b4e57 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/components/SpacingItemDecoration.kt @@ -0,0 +1,38 @@ +package com.arthurgonzaga.wikistarwars.ui.components + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class SpacingItemDecoration( + private val spanCount: Int, + private val spacing: Int, + private val headerNum: Int = 0 +) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + val position = parent.getChildAdapterPosition(view) - headerNum // item position + + if (position >= 0) { + val column = position % spanCount // item column + + outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing) + outRect.right = + spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing) + if (position >= spanCount) { + outRect.top = spacing // item top + } + } else { + outRect.left = 0 + outRect.right = 0 + outRect.top = 0 + outRect.bottom = 0 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/DetailFragment.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/DetailFragment.kt new file mode 100644 index 00000000..51632d70 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/DetailFragment.kt @@ -0,0 +1,102 @@ +package com.arthurgonzaga.wikistarwars.ui.fragments + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.os.trace +import androidx.core.view.forEach +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.navArgs +import androidx.transition.TransitionInflater +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.databinding.FragmentDetailBinding +import com.arthurgonzaga.wikistarwars.databinding.FragmentHomeBinding +import com.arthurgonzaga.wikistarwars.ui.components.CharacterInfo +import com.arthurgonzaga.wikistarwars.util.setImage +import com.arthurgonzaga.wikistarwars.viewmodel.DetailViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + + +/** + * + * This fragment will display full information about a character + * + */ +@AndroidEntryPoint +class DetailFragment : Fragment() { + + private lateinit var binding: FragmentDetailBinding + + private val vm: DetailViewModel by viewModels() + private val args: DetailFragmentArgs by navArgs() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentDetailBinding.inflate(inflater) + + binding.character = args.character + + val characterId = args.character.id + binding.favoriteButton.setOnClickListener { + if (args.character.isFavorite) { + vm.unFavoriteCharacter(characterId) + binding.favoriteButton.setImage(false) + args.character.isFavorite = false + } else { + vm.favoriteCharacter(characterId) + binding.favoriteButton.setImage(true) + args.character.isFavorite = true + } + } + + setupAnimations() + + return binding.root + } + + + fun setupAnimations() { + val animation = TransitionInflater.from(requireContext()).inflateTransition( + android.R.transition.move + ) + sharedElementEnterTransition = animation + sharedElementReturnTransition = animation + + lifecycleScope.launch { + showInfosWithAnimation() + } + } + + private suspend fun showInfosWithAnimation() { + delay(260) + binding.gridLayout.forEach { info -> + (info as CharacterInfo) + info.show() + delay(25) + } + } + + fun observeChanges(){ + vm.newFavorite.observe(viewLifecycleOwner){ isSuccessful -> + Log.d(TAG, "isSuccessful: $isSuccessful") + if(isSuccessful == true){ + Toast.makeText(requireContext(), R.string.favorite_success, Toast.LENGTH_SHORT).show() + } + } + } + + companion object { + private const val TAG = "DetailFragment" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/FavoriteListFragment.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/FavoriteListFragment.kt new file mode 100644 index 00000000..ccffce07 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/FavoriteListFragment.kt @@ -0,0 +1,107 @@ +package com.arthurgonzaga.wikistarwars.ui.fragments + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.core.os.bundleOf +import androidx.core.view.isEmpty +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.databinding.FragmentDetailBinding +import com.arthurgonzaga.wikistarwars.databinding.FragmentFavoriteListBinding +import com.arthurgonzaga.wikistarwars.databinding.FragmentHomeBinding +import com.arthurgonzaga.wikistarwars.ui.adapters.CharacterAdapter +import com.arthurgonzaga.wikistarwars.ui.components.SpacingItemDecoration +import com.arthurgonzaga.wikistarwars.ui.util.navigateToDetailFragment +import com.arthurgonzaga.wikistarwars.viewmodel.FavoritesViewModel +import com.arthurgonzaga.wikistarwars.viewmodel.HomeViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + + +/** + * + * This fragment will display the list of favorites characters + * + */ +@AndroidEntryPoint +class FavoriteListFragment : Fragment() { + + private lateinit var binding: FragmentFavoriteListBinding + private val vm: FavoritesViewModel by viewModels() + + private lateinit var characterAdapter: CharacterAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + binding = FragmentFavoriteListBinding.inflate(inflater) + + setupRecyclerView() + observeChanges() + + return binding.root + } + + private fun setupRecyclerView() { + characterAdapter = + CharacterAdapter( + context = requireContext(), + navigateToDetail = ::navigateToDetailFragment, + favoriteCharacter = { id, isFavorite -> + if (!isFavorite) vm.unFavoriteCharacter(id) + } + ) + + binding.recyclerView.apply { + adapter = characterAdapter + postponeEnterTransition() + viewTreeObserver.addOnPreDrawListener { + startPostponedEnterTransition() + true + } + + val space = requireContext().resources.getDimensionPixelSize(R.dimen.normal) + val spanCount = requireContext().resources.getInteger(R.integer.rv_column_count) + + addItemDecoration(SpacingItemDecoration(spanCount = spanCount, spacing = space)) + } + } + + private fun observeChanges() { + vm.favoritesCharacters.observe(viewLifecycleOwner) { pagingData -> + viewLifecycleOwner.lifecycleScope.launch { + characterAdapter.submitData(pagingData) + + } + } + } + + + private fun navigateToDetailFragment( + characterEntity: CharacterEntity, + textView: TextView, + imageButton: ImageButton, + viewGroup: ViewGroup + ) = navigateToDetailFragment( + characterEntity, + textView, + imageButton, + viewGroup, + R.id.goToDetailFragmentFromFavorites + ) + + companion object { + private const val TAG = "FavoriteListFragment" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/HomeFragment.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/HomeFragment.kt new file mode 100644 index 00000000..60d3fc40 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/fragments/HomeFragment.kt @@ -0,0 +1,164 @@ +package com.arthurgonzaga.wikistarwars.ui.fragments + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.ImageButton +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.databinding.FragmentHomeBinding +import com.arthurgonzaga.wikistarwars.ui.adapters.CharacterAdapter +import com.arthurgonzaga.wikistarwars.viewmodel.HomeViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import com.arthurgonzaga.wikistarwars.ui.components.MyThemes +import com.arthurgonzaga.wikistarwars.ui.components.SpacingItemDecoration +import com.arthurgonzaga.wikistarwars.ui.util.navigateToDetailFragment +import jp.wasabeef.recyclerview.animators.ScaleInAnimator + + +/** + * + * This fragment is the initial fragment of the backstack. + * + * It will display a at the top header and a recyclerview containing + * some information about the characters + * + */ +@AndroidEntryPoint +class HomeFragment : Fragment() { + + private lateinit var binding: FragmentHomeBinding + private val vm: HomeViewModel by viewModels() + + lateinit var characterAdapter: CharacterAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentHomeBinding.inflate(inflater) + + setupRecyclerView() + setupSearchBar() + setupThemeChanger() + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeChanges() + } + + private fun setupRecyclerView(){ + characterAdapter = + CharacterAdapter( + context = requireContext(), + navigateToDetail = ::navigateToDetailFragment, + favoriteCharacter = vm::favoriteCharacter + ) + + binding.recyclerView.apply { + adapter = characterAdapter + postponeEnterTransition() + viewTreeObserver.addOnPreDrawListener { + startPostponedEnterTransition() + true + } + itemAnimator = ScaleInAnimator() + + val space = requireContext().resources.getDimensionPixelSize(R.dimen.normal) + val spanCount = requireContext().resources.getInteger(R.integer.rv_column_count) + + addItemDecoration(SpacingItemDecoration(spanCount = spanCount, spacing = space)) + } + } + + private fun setupSearchBar(){ + binding.row.searchBar.setOnEditorActionListener { textView, action, _ -> + if(action == EditorInfo.IME_ACTION_SEARCH){ + + vm.search(textView.text.toString()) + + return@setOnEditorActionListener true + } + false + } + + binding.row.searchBarLayout.setEndIconOnClickListener { _ -> + binding.row.searchBar.setText("") + vm.search("") + } + + binding.row.favoriteListButton.setOnClickListener { _ -> + navigateToFavoritesFragment() + } + } + + private fun setupThemeChanger(){ + val sharedPref = requireContext().getSharedPreferences(getString(R.string.theme_key), Context.MODE_PRIVATE) + + val nextTheme = when(binding.header.changeColor.backgroundTintList?.defaultColor){ + requireContext().getColor(R.color.yellow) -> MyThemes.YELLOW + requireContext().getColor(R.color.blue) -> MyThemes.BLUE + requireContext().getColor(R.color.red) -> MyThemes.RED + else -> MyThemes.YELLOW + } + + binding.header.changeColor.setOnClickListener { _ -> + with(sharedPref.edit()){ + putInt("theme", nextTheme.ordinal) + commit() + } + requireActivity().recreate() + } + } + + private fun observeChanges(){ + vm.characters.observe(viewLifecycleOwner) { pagingData -> + viewLifecycleOwner.lifecycleScope.launch { + characterAdapter.submitData(pagingData) + } + } + + vm.newFavorite.observe(viewLifecycleOwner){ isSuccessful -> + Log.d(TAG, "isSuccessful: $isSuccessful") + if(isSuccessful == true){ + Toast.makeText(requireContext(), R.string.favorite_success, Toast.LENGTH_SHORT).show() + } + } + } + + private fun navigateToFavoritesFragment() { + findNavController().navigate(R.id.goToFavoritesListFragment) + } + + private fun navigateToDetailFragment( + characterEntity: CharacterEntity, + textView: TextView, + imageButton: ImageButton, + viewGroup: ViewGroup + ) = navigateToDetailFragment( + characterEntity, + textView, + imageButton, + viewGroup, + R.id.goToDetailFragmentFromHome + ) + + + companion object { + private const val TAG = "HomeFragment" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/util/navigateToDetailFragment.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/util/navigateToDetailFragment.kt new file mode 100644 index 00000000..33f38338 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/ui/util/navigateToDetailFragment.kt @@ -0,0 +1,35 @@ +package com.arthurgonzaga.wikistarwars.ui.util + +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.FragmentNavigatorExtras +import com.arthurgonzaga.wikistarwars.R +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity +import androidx.navigation.fragment.findNavController + +fun Fragment.navigateToDetailFragment( + characterEntity: CharacterEntity, + textView: TextView, + imageButton: ImageButton, + viewGroup: ViewGroup, + @IdRes actionId: Int +) { + + val extras = FragmentNavigatorExtras( + textView to "heading_big", + imageButton to "favorite_btn_big", + viewGroup to "background" + ) + + val args = bundleOf("character" to characterEntity) + findNavController().navigate( + actionId, + args, + null, + extras + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/util/CharacterMappers.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/util/CharacterMappers.kt new file mode 100644 index 00000000..3c6c3834 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/util/CharacterMappers.kt @@ -0,0 +1,39 @@ +package com.arthurgonzaga.wikistarwars.util + +import android.util.Log +import com.arthurgonzaga.wikistarwars.api.responses.CharacterResponse +import com.arthurgonzaga.wikistarwars.data.model.CharacterEntity + +fun CharacterResponse.toEntity( + homeWorldName: String, + specieName: String?, +) = CharacterEntity( + id = this.getId(), + name= this.name, + height= this.height, + weight= this.weight, + hairColor= this.hairColor, + skinColor= this.skinColor, + eyeColor= this.eyeColor, + birthYear= this.birthYear, + gender= this.gender, + specieName = specieName ?: "Human", + homeWoldName = homeWorldName, +) + +fun CharacterResponse.toEntity(): CharacterEntity{ + Log.d("Mappers", "toEntity: id: ${this.getId()}") + return CharacterEntity( + id = this.getId(), + name= this.name, + height= this.height, + weight= this.weight, + hairColor= this.hairColor, + skinColor= this.skinColor, + eyeColor= this.eyeColor, + birthYear= this.birthYear, + gender= this.gender, + specieName = null, + homeWoldName = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/util/GetRetrofitInstance.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/util/GetRetrofitInstance.kt new file mode 100644 index 00000000..76038a27 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/util/GetRetrofitInstance.kt @@ -0,0 +1,17 @@ +package com.arthurgonzaga.wikistarwars.util + +import com.arthurgonzaga.wikistarwars.data.Constants +import hu.akarnokd.rxjava3.retrofit.RxJava3CallAdapterFactory +import io.reactivex.rxjava3.schedulers.Schedulers +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +inline fun getRetrofitInstance(baseUrl: String = Constants.BASE_URL): T{ + val retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())) + .build() + + return retrofit.create(T::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/util/SetImage.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/util/SetImage.kt new file mode 100644 index 00000000..bfdd04f7 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/util/SetImage.kt @@ -0,0 +1,14 @@ +package com.arthurgonzaga.wikistarwars.util + +import android.widget.ImageButton +import androidx.core.content.res.ResourcesCompat +import com.arthurgonzaga.wikistarwars.R + +fun ImageButton.setImage(isFavorite: Boolean) { + val drawable = ResourcesCompat.getDrawable( + context.resources, + if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border, + context.theme + ) + this.setImageDrawable(drawable) +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/DetailViewModel.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/DetailViewModel.kt new file mode 100644 index 00000000..84a6b930 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/DetailViewModel.kt @@ -0,0 +1,34 @@ +package com.arthurgonzaga.wikistarwars.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.arthurgonzaga.wikistarwars.repository.interfaces.MainRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DetailViewModel @Inject constructor( + val mainRepository: MainRepository +): ViewModel() { + + + private val _newFavorite = MutableLiveData() + val newFavorite: LiveData = _newFavorite + + + fun unFavoriteCharacter(id: Int){ + viewModelScope.launch { + mainRepository.unFavoriteCharacter(id) + } + } + + fun favoriteCharacter(id: Int){ + viewModelScope.launch { + _newFavorite.value = mainRepository.favoriteCharacter(id) + _newFavorite.value = null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/FavoritesViewModel.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/FavoritesViewModel.kt new file mode 100644 index 00000000..3deeee28 --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/FavoritesViewModel.kt @@ -0,0 +1,27 @@ +package com.arthurgonzaga.wikistarwars.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.arthurgonzaga.wikistarwars.repository.interfaces.FavoritesRepository +import com.arthurgonzaga.wikistarwars.repository.interfaces.HomeRepository +import com.arthurgonzaga.wikistarwars.repository.interfaces.MainRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class FavoritesViewModel @Inject constructor( + favoritesRepository: FavoritesRepository, + private val mainRepository: MainRepository +): ViewModel() { + + val favoritesCharacters = favoritesRepository.getFavoriteCharacters().cachedIn(viewModelScope) + + fun unFavoriteCharacter(id: Int){ + viewModelScope.launch { + mainRepository.unFavoriteCharacter(id) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/HomeViewModel.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/HomeViewModel.kt new file mode 100644 index 00000000..d225842f --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/HomeViewModel.kt @@ -0,0 +1,52 @@ +package com.arthurgonzaga.wikistarwars.viewmodel + +import android.util.Log +import androidx.lifecycle.* +import androidx.paging.cachedIn +import com.arthurgonzaga.wikistarwars.repository.interfaces.HomeRepository +import com.arthurgonzaga.wikistarwars.repository.interfaces.MainRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import retrofit2.HttpException +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + val repository: HomeRepository, + val mainRepository: MainRepository +) : ViewModel(), LifecycleObserver { + + + private val _currentSearchText = MutableLiveData("") + + val characters = _currentSearchText.switchMap { query -> + if(query.isBlank()){ + repository.getCharacters("").cachedIn(viewModelScope) + }else { + repository.getCharacters(query).cachedIn(viewModelScope) + } + } + + private val _newFavorite = MutableLiveData() + val newFavorite: LiveData = _newFavorite + + + fun favoriteCharacter(characterId: Int, favorite: Boolean){ + viewModelScope.launch { + if(favorite){ + _newFavorite.value = mainRepository.favoriteCharacter(characterId) + _newFavorite.value = null + }else { + mainRepository.unFavoriteCharacter(characterId) + } + } + } + + fun search(text: String) { + _currentSearchText.value = text + } + + companion object { + const val TAG = "HomeViewModel" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/MainViewModel.kt b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/MainViewModel.kt new file mode 100644 index 00000000..71fc67ae --- /dev/null +++ b/app/src/main/java/com/arthurgonzaga/wikistarwars/viewmodel/MainViewModel.kt @@ -0,0 +1,30 @@ +package com.arthurgonzaga.wikistarwars.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.arthurgonzaga.wikistarwars.repository.interfaces.MainRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class MainViewModel @Inject constructor( + private val mainRepository: MainRepository +): ViewModel() { + + private val _inSync = MutableLiveData() + val inSync: LiveData = _inSync + + init { + viewModelScope.launch { + synchronizeWithBackend() + } + } + + private suspend fun synchronizeWithBackend(){ + _inSync.value = mainRepository.synchronizeFavoriteCharacters() + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 00000000..f3729dbd --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 00000000..305df144 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 00000000..1ad55f86 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 00000000..546ed705 --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/icon_text_input_tint.xml b/app/src/main/res/color/icon_text_input_tint.xml new file mode 100644 index 00000000..fce9e5b7 --- /dev/null +++ b/app/src/main/res/color/icon_text_input_tint.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/outline_color.xml b/app/src/main/res/color/outline_color.xml new file mode 100644 index 00000000..6626417f --- /dev/null +++ b/app/src/main/res/color/outline_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/border_background.xml b/app/src/main/res/drawable/border_background.xml new file mode 100644 index 00000000..593b050d --- /dev/null +++ b/app/src/main/res/drawable/border_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 00000000..7d8f0dfd --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_background.xml b/app/src/main/res/drawable/divider_background.xml new file mode 100644 index 00000000..d4150a74 --- /dev/null +++ b/app/src/main/res/drawable/divider_background.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml b/app/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml new file mode 100644 index 00000000..af4fdc3d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..87296030 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml new file mode 100644 index 00000000..8a997503 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_border.xml b/app/src/main/res/drawable/ic_favorite_border.xml new file mode 100644 index 00000000..c55e379c --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_border.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml new file mode 100644 index 00000000..0d7db32c --- /dev/null +++ b/app/src/main/res/drawable/ic_list.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..57ee9496 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/item_rv_background.xml b/app/src/main/res/drawable/item_rv_background.xml new file mode 100644 index 00000000..32a6c879 --- /dev/null +++ b/app/src/main/res/drawable/item_rv_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/montserrat_extra_bold.ttf b/app/src/main/res/font/montserrat_extra_bold.ttf new file mode 100644 index 00000000..80ea8061 Binary files /dev/null and b/app/src/main/res/font/montserrat_extra_bold.ttf differ diff --git a/app/src/main/res/font/montserrat_regular.ttf b/app/src/main/res/font/montserrat_regular.ttf new file mode 100644 index 00000000..8d443d5d Binary files /dev/null and b/app/src/main/res/font/montserrat_regular.ttf differ diff --git a/app/src/main/res/font/montserrat_semi_bold.ttf b/app/src/main/res/font/montserrat_semi_bold.ttf new file mode 100644 index 00000000..f8a43f2b Binary files /dev/null and b/app/src/main/res/font/montserrat_semi_bold.ttf differ diff --git a/app/src/main/res/font/starwars_hol.ttf b/app/src/main/res/font/starwars_hol.ttf new file mode 100644 index 00000000..3c06b481 Binary files /dev/null and b/app/src/main/res/font/starwars_hol.ttf differ diff --git a/app/src/main/res/font/starwars_jedi.ttf b/app/src/main/res/font/starwars_jedi.ttf new file mode 100644 index 00000000..2ac5bb1b Binary files /dev/null and b/app/src/main/res/font/starwars_jedi.ttf differ diff --git a/app/src/main/res/font/ubunto_mono.ttf b/app/src/main/res/font/ubunto_mono.ttf new file mode 100644 index 00000000..fdd309d7 Binary files /dev/null and b/app/src/main/res/font/ubunto_mono.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..deaefc43 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/character_info_layout.xml b/app/src/main/res/layout/character_info_layout.xml new file mode 100644 index 00000000..5c1f835f --- /dev/null +++ b/app/src/main/res/layout/character_info_layout.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/divider_layout.xml b/app/src/main/res/layout/divider_layout.xml new file mode 100644 index 00000000..cfdce6d3 --- /dev/null +++ b/app/src/main/res/layout/divider_layout.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml new file mode 100644 index 00000000..3b1f2988 --- /dev/null +++ b/app/src/main/res/layout/fragment_detail.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favorite_list.xml b/app/src/main/res/layout/fragment_favorite_list.xml new file mode 100644 index 00000000..e52ff4f3 --- /dev/null +++ b/app/src/main/res/layout/fragment_favorite_list.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 00000000..9e3b7bad --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/home_header_layout.xml b/app/src/main/res/layout/home_header_layout.xml new file mode 100644 index 00000000..078feadd --- /dev/null +++ b/app/src/main/res/layout/home_header_layout.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/home_row_layout.xml b/app/src/main/res/layout/home_row_layout.xml new file mode 100644 index 00000000..b57a9515 --- /dev/null +++ b/app/src/main/res/layout/home_row_layout.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/rv_character_item.xml b/app/src/main/res/layout/rv_character_item.xml new file mode 100644 index 00000000..aa00baae --- /dev/null +++ b/app/src/main/res/layout/rv_character_item.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ 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 new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..0b6f43e2 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 00000000..7fe31292 --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,4 @@ + + + 60dp + \ No newline at end of file diff --git a/app/src/main/res/values-land/integers.xml b/app/src/main/res/values-land/integers.xml new file mode 100644 index 00000000..f17a5521 --- /dev/null +++ b/app/src/main/res/values-land/integers.xml @@ -0,0 +1,5 @@ + + + 5 + 2 + \ No newline at end of file diff --git a/app/src/main/res/values-long-land/integers.xml b/app/src/main/res/values-long-land/integers.xml new file mode 100644 index 00000000..f90a1ea1 --- /dev/null +++ b/app/src/main/res/values-long-land/integers.xml @@ -0,0 +1,4 @@ + + + 6 + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..5f2ecde1 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f9673d7d --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFFFE919 + #FF4EADFF + #FFEF233C + #FF000000 + #80000000 + #FFEDF2F4 + #FF8D99AE + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..a52835e5 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + 4dp + 8dp + 16dp + 24dp + 32dp + 40dp + 52dp + + + 116dp + + + 16sp + 14sp + 44sp + 44sp + + + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 00000000..a0abc4ae --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,5 @@ + + + 2 + 1 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..af8929cf --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,29 @@ + + WikiStarWars + the characters wiki + + Star\nWars + + Hello blank fragment + Favorite + Height + Weight + Hair color + Birth year + Eye color + Skin color + Gender + Homeworld + Specie + Favorites + You have not favorite\nany character yet + Search + Favorite list + %1$s cm - %2$s kg + %1$s cm + %1$s kg + + Character was favorited successfully + Favorite characters was synchronized successfully + theme_key + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5bc7aa4b --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..092ae1cc --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/arthurgonzaga/wikistarwars/ExampleUnitTest.kt b/app/src/test/java/com/arthurgonzaga/wikistarwars/ExampleUnitTest.kt new file mode 100644 index 00000000..90370e43 --- /dev/null +++ b/app/src/test/java/com/arthurgonzaga/wikistarwars/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.arthurgonzaga.wikistarwars + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/arthurgonzaga/wikistarwars/api/responses/CharacterResponseTest.kt b/app/src/test/java/com/arthurgonzaga/wikistarwars/api/responses/CharacterResponseTest.kt new file mode 100644 index 00000000..54b77796 --- /dev/null +++ b/app/src/test/java/com/arthurgonzaga/wikistarwars/api/responses/CharacterResponseTest.kt @@ -0,0 +1,81 @@ +package com.arthurgonzaga.wikistarwars.api.responses + +import com.google.common.truth.Truth.assertThat +import com.google.gson.annotations.SerializedName +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class CharacterResponseTest { + + @Test + fun `should return null when the list is empty`() { + + // Given + val characterResponse = CharacterResponse( + speciesUrls = listOf(), + homeWorldUrl = "planets/1/", + url = "people/1/" + ) + + // When + val specieId = characterResponse.getSpecieId() + + // Then + assertThat(specieId).isEqualTo(null) + } + + @Test + fun `should return the id of the first index when the species list is not empty`() { + + val url1 = "https://swapi.dev/api/species/1/" + val url2 = "https://swapi.dev/api/species/33/" + + // Given + val characterResponse = CharacterResponse( + speciesUrls = listOf(url1, url2), + homeWorldUrl = "planets/1/", + url = "people/1/" + ) + + // When + val specieId = characterResponse.getSpecieId() + + // Then + assertThat(specieId).isEqualTo(1) + } + + + @Test + fun `should return the id of the planet`() { + // Given + val characterResponse = + CharacterResponse( + homeWorldUrl = "https://swapi.dev/api/planets/10/", + url = "people/1/" + ) + + // When + val planetId = characterResponse.getHomeWorldId() + + // Then + assertThat(planetId).isEqualTo(10) + } + + @Test + fun `should return the id of the character`() { + // Given + val characterResponse = + CharacterResponse( + homeWorldUrl = "planets/1/", + url = "https://swapi.dev/api/people/9/" + ) + + // When + val planetId = characterResponse.getId() + + // Then + assertThat(planetId).isEqualTo(45) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/arthurgonzaga/wikistarwars/api/responses/PageResponseTest.kt b/app/src/test/java/com/arthurgonzaga/wikistarwars/api/responses/PageResponseTest.kt new file mode 100644 index 00000000..2bfe42e2 --- /dev/null +++ b/app/src/test/java/com/arthurgonzaga/wikistarwars/api/responses/PageResponseTest.kt @@ -0,0 +1,79 @@ +package com.arthurgonzaga.wikistarwars.api.responses + +import com.google.common.truth.Truth.assertThat +import junit.framework.TestCase +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class PageResponseTest { + + @Test + fun `should return null when next page is null`() { + // Given + val pageResponse = PageResponse( + count = 87, + next = null, + previous = "https://swapi.dev/api/people/?page=8", + results = listOf() + ) + + // When + val nextPageIndex = pageResponse.getNextPageIndex() + + // Then + assertThat(nextPageIndex).isEqualTo(null) + } + + @Test + fun `should return next page index when it has a next Page`() { + // Given + val pageResponse = PageResponse( + count = 87, + next = "https://swapi.dev/api/people/?page=9", + previous = "https://swapi.dev/api/people/?page=7", + results = listOf() + ) + + // When + val nextPageIndex = pageResponse.getNextPageIndex() + + // Then + assertThat(nextPageIndex).isEqualTo(9) + } + + @Test + fun `should return null when previous page is null`() { + // Given + val pageResponse = PageResponse( + count = 87, + next = "https://swapi.dev/api/people/?page=2", + previous = null, + results = listOf() + ) + + // When + val previousPageIndex = pageResponse.getPreviousIndex() + + // Then + assertThat(previousPageIndex).isEqualTo(null) + } + + @Test + fun `should return previous page index when it has a previous Page`() { + // Given + val pageResponse = PageResponse( + count = 87, + next = "https://swapi.dev/api/people/?page=3", + previous = "https://swapi.dev/api/people/?page=1", + results = listOf() + ) + + // When + val previousPageIndex = pageResponse.getPreviousIndex() + + // Then + assertThat(previousPageIndex).isEqualTo(1) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..abf2edd0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + + ext { + hilt_version = "2.37" + nav_version = "2.3.5" + } + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.0.1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30" + + // Hilt + + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" + + + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..98bed167 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# 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=-Xmx2048m -Dfile.encoding=UTF-8 +# 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 +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..06624b5d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 23 17:55:34 GFT 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 +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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +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. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +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 +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 + +# 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 + # 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\"" + fi + i=`expr $i + 1` + 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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..0c10d828 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + jcenter() // Warning: this repository is going to shut down soon + } +} +rootProject.name = "WikiStarWars" +include ':app'