├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── kotlinc.xml ├── misc.xml ├── runConfigurations.xml ├── gradle.xml └── jarRepositories.xml ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── doilio.jpg │ │ │ │ ├── darkseid.jpg │ │ │ │ ├── placeholder.jpg │ │ │ │ ├── dc_landscape.jpg │ │ │ │ ├── ic_baseline_sort_24.xml │ │ │ │ ├── ic_baseline_filter_list_24.xml │ │ │ │ ├── ic_baseline_people_24.xml │ │ │ │ ├── ic_baseline_all_inclusive_24.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── integers.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── values-land │ │ │ │ └── integers.xml │ │ │ ├── values-sw600dp │ │ │ │ └── integers.xml │ │ │ ├── values-sw600dp-land │ │ │ │ └── integers.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── menu │ │ │ │ └── power_stats_menu.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── power_stat_item.xml │ │ │ │ ├── character_item.xml │ │ │ │ ├── filter_dialog_layout.xml │ │ │ │ ├── fragment_characters.xml │ │ │ │ └── fragment_character_detail.xml │ │ │ ├── navigation │ │ │ │ └── navigation.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── xml │ │ │ │ └── fragment_character_detail_scene.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── doiliomatsinhe │ │ │ │ └── dccharacters │ │ │ │ ├── model │ │ │ │ ├── Filters.kt │ │ │ │ └── Character.kt │ │ │ │ ├── utils │ │ │ │ ├── Constants.kt │ │ │ │ ├── Converters.kt │ │ │ │ └── ColorUtils.kt │ │ │ │ ├── network │ │ │ │ ├── ApiService.kt │ │ │ │ └── DataTransferObjects.kt │ │ │ │ ├── MyApplication.kt │ │ │ │ ├── database │ │ │ │ ├── CharactersDatabase.kt │ │ │ │ ├── CharacterDao.kt │ │ │ │ └── DatabaseEntities.kt │ │ │ │ ├── di │ │ │ │ ├── NetworkModule.kt │ │ │ │ └── DatabaseModule.kt │ │ │ │ ├── ui │ │ │ │ ├── characterdetail │ │ │ │ │ ├── CharacterDetailViewModelFactory.kt │ │ │ │ │ ├── CharacterDetailViewModel.kt │ │ │ │ │ └── CharacterDetailFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── character │ │ │ │ │ ├── CharactersViewModel.kt │ │ │ │ │ └── CharactersFragment.kt │ │ │ │ └── filter │ │ │ │ │ └── FilterFragment.kt │ │ │ │ ├── adapter │ │ │ │ ├── CharacterViewHolder.kt │ │ │ │ ├── CharacterAdapter.kt │ │ │ │ ├── BindingUtils.kt │ │ │ │ └── PowerStatAdapter.kt │ │ │ │ └── repository │ │ │ │ └── CharacterRepository.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── doiliomatsinhe │ │ │ └── dccharacters │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── doiliomatsinhe │ │ └── dccharacters │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | DC Vilains -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "DC Vilains" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/doilio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/drawable/doilio.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/darkseid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/drawable/darkseid.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/drawable/placeholder.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/dc_landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/drawable/dc_landscape.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doilio/DC-Characters/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-land/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp-land/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/model/Filters.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.model 2 | 3 | data class Filters(var order: String="", var race: String="", var gender: String="") -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.utils 2 | 3 | const val BASE_URL = "https://cdn.rawgit.com/akabab/superhero-api/0.2.0/api/" 4 | const val DB_NAME = "charactersDB" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/network/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.network 2 | 3 | import retrofit2.http.GET 4 | 5 | interface ApiService { 6 | 7 | @GET("all.json") 8 | suspend fun getCharacters(): List 9 | 10 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 27 23:21:00 CAT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | import timber.log.Timber 6 | 7 | @HiltAndroidApp 8 | class MyApplication : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | Timber.plant(Timber.DebugTree()) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/utils/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.utils 2 | 3 | import androidx.room.TypeConverter 4 | import com.google.gson.Gson 5 | 6 | class Converters { 7 | @TypeConverter 8 | fun listToJson(value: List): String = Gson().toJson(value) 9 | 10 | @TypeConverter 11 | fun jsonToList(value: String) = Gson().fromJson(value, Array::class.java).toList() 12 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_sort_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_filter_list_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/test/java/com/doiliomatsinhe/dccharacters/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/power_stats_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/database/CharactersDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.doiliomatsinhe.dccharacters.utils.Converters 7 | 8 | @Database(entities = [DatabaseCharacter::class], version = 1, exportSchema = false) 9 | @TypeConverters(Converters::class) 10 | abstract class CharactersDatabase : RoomDatabase() { 11 | abstract val characterDao: CharacterDao 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | 8 | #81b29a 9 | #e07a5f 10 | #3d405b 11 | #f2cc8f 12 | #7a6f9b 13 | #815e5b 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_people_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/database/CharacterDao.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.database 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.paging.PagingSource 5 | import androidx.room.* 6 | import androidx.sqlite.db.SupportSQLiteQuery 7 | 8 | @Dao 9 | interface CharacterDao { 10 | 11 | @RawQuery(observedEntities = [DatabaseCharacter::class]) 12 | fun getRawListOfCharacters(query: SupportSQLiteQuery): PagingSource 13 | 14 | @Insert(onConflict = OnConflictStrategy.REPLACE) 15 | fun insertAllCharacters(vararg characters: DatabaseCharacter) 16 | 17 | @Query("SELECT * FROM characters WHERE id= :id") 18 | fun getCharactersById(id: Int): LiveData 19 | 20 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/utils/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.utils 2 | 3 | import android.graphics.Color 4 | import kotlin.math.roundToInt 5 | 6 | object ColorUtils { 7 | /** 8 | * @param color is the given color. 9 | * @param factor number that makes the color darker. 10 | * @return's a darker color based the given color. 11 | */ 12 | fun manipulateColor(color: Int, factor: Float): Int { 13 | val a: Int = Color.alpha(color) 14 | val r = (Color.red(color) * factor).roundToInt() 15 | val g = (Color.green(color) * factor).roundToInt() 16 | val b = (Color.blue(color) * factor).roundToInt() 17 | return Color.argb( 18 | a, r.coerceAtMost(255), 19 | g.coerceAtMost(255), b.coerceAtMost(255) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/doiliomatsinhe/dccharacters/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.doiliomatsinhe.dcvilains", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.di 2 | 3 | import com.doiliomatsinhe.dccharacters.network.ApiService 4 | import com.doiliomatsinhe.dccharacters.utils.BASE_URL 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import javax.inject.Singleton 12 | 13 | @InstallIn(ApplicationComponent::class) 14 | @Module 15 | object NetworkModule { 16 | 17 | @Provides 18 | @Singleton 19 | fun provideApiService(): ApiService { 20 | return Retrofit.Builder() 21 | .addConverterFactory(GsonConverterFactory.create()) 22 | .baseUrl(BASE_URL) 23 | .build() 24 | .create(ApiService::class.java) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/ui/characterdetail/CharacterDetailViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.ui.characterdetail 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.doiliomatsinhe.dccharacters.repository.CharacterRepository 6 | import java.lang.IllegalArgumentException 7 | 8 | class CharacterDetailViewModelFactory( 9 | private val repository: CharacterRepository, 10 | private val villainId: Int 11 | ) : 12 | ViewModelProvider.Factory { 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(modelClass: Class): T { 16 | if (modelClass.isAssignableFrom(CharacterDetailViewModel::class.java)) { 17 | return CharacterDetailViewModel(repository, villainId) as T 18 | } 19 | throw IllegalArgumentException("Unknown ViewModel Class") 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_all_inclusive_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/adapter/CharacterViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.doiliomatsinhe.dccharacters.databinding.CharacterItemBinding 7 | import com.doiliomatsinhe.dccharacters.model.Character 8 | 9 | class CharacterViewHolder(private val binding: CharacterItemBinding) : 10 | RecyclerView.ViewHolder(binding.root) { 11 | 12 | fun bind(item: Character?, clickListener: CharacterClickListener) { 13 | binding.character = item 14 | binding.clickListener = clickListener 15 | binding.executePendingBindings() 16 | } 17 | 18 | companion object { 19 | fun from(parent: ViewGroup): CharacterViewHolder { 20 | val inflater = LayoutInflater.from(parent.context) 21 | val binding = CharacterItemBinding.inflate(inflater, parent, false) 22 | return CharacterViewHolder(binding) 23 | 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/ui/characterdetail/CharacterDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.ui.characterdetail 2 | 3 | import androidx.lifecycle.MediatorLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.doiliomatsinhe.dccharacters.model.Character 6 | import com.doiliomatsinhe.dccharacters.repository.CharacterRepository 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.launch 11 | 12 | class CharacterDetailViewModel( 13 | private val repository: CharacterRepository, 14 | private val characterId: Int 15 | ) : ViewModel() { 16 | 17 | private val viewModelJob = Job() 18 | private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) 19 | 20 | private val character = MediatorLiveData() 21 | 22 | fun getVillain() = character 23 | 24 | init { 25 | uiScope.launch { 26 | character.addSource(repository.getCharacterById(characterId), character::setValue) 27 | } 28 | 29 | } 30 | 31 | override fun onCleared() { 32 | super.onCleared() 33 | viewModelJob.cancel() 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.doiliomatsinhe.dccharacters.database.CharacterDao 6 | import com.doiliomatsinhe.dccharacters.database.CharactersDatabase 7 | import com.doiliomatsinhe.dccharacters.utils.DB_NAME 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.components.ApplicationComponent 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import javax.inject.Singleton 14 | 15 | @InstallIn(ApplicationComponent::class) 16 | @Module 17 | object DatabaseModule { 18 | 19 | @Provides 20 | fun provideCharactersDao(database: CharactersDatabase): CharacterDao { 21 | return database.characterDao 22 | } 23 | 24 | @Provides 25 | @Singleton 26 | fun provideCharactersDatabase(@ApplicationContext appContext: Context): CharactersDatabase { 27 | return Room.databaseBuilder( 28 | appContext, 29 | CharactersDatabase::class.java, 30 | DB_NAME 31 | ).fallbackToDestructiveMigration() 32 | .build() 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.ui 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import androidx.navigation.NavController 6 | import androidx.navigation.Navigation 7 | import androidx.navigation.ui.AppBarConfiguration 8 | import androidx.navigation.ui.navigateUp 9 | import androidx.navigation.ui.setupActionBarWithNavController 10 | import com.doiliomatsinhe.dccharacters.R 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private lateinit var appBarConfiguration: AppBarConfiguration 17 | private lateinit var navController: NavController 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_main) 22 | 23 | navController = Navigation.findNavController(this, R.id.nav_host_fragment) 24 | appBarConfiguration = AppBarConfiguration(navController.graph) 25 | setupActionBarWithNavController(navController, appBarConfiguration) 26 | } 27 | 28 | override fun onSupportNavigateUp(): Boolean { 29 | return navController.navigateUp(appBarConfiguration) || 30 | super.onSupportNavigateUp() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/adapter/CharacterAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.adapter 2 | 3 | 4 | import android.view.ViewGroup 5 | import androidx.paging.PagingDataAdapter 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.doiliomatsinhe.dccharacters.model.Character 9 | 10 | class CharacterAdapter (private val clickListener: CharacterClickListener) : 11 | PagingDataAdapter(CharacterDiffUtilCallback()) { 12 | 13 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 14 | (holder as CharacterViewHolder).bind(getItem(position), clickListener) 15 | } 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 18 | return CharacterViewHolder.from(parent) 19 | } 20 | 21 | } 22 | 23 | class CharacterDiffUtilCallback : DiffUtil.ItemCallback() { 24 | override fun areItemsTheSame(oldItem: Character, newItem: Character): Boolean { 25 | return oldItem.id == newItem.id 26 | } 27 | 28 | override fun areContentsTheSame(oldItem: Character, newItem: Character): Boolean { 29 | return oldItem == newItem 30 | } 31 | } 32 | 33 | class CharacterClickListener (val clickListener: (character: Character) -> Unit) { 34 | fun onClick(character: Character) = clickListener(character) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/model/Character.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.model 2 | 3 | data class Character( 4 | val id: Int, 5 | val name: String, 6 | val slug: String, 7 | val powerstats: Powerstats, 8 | val appearance: Appearance, 9 | val biography: Biography, 10 | val work: Work, 11 | val connections: Connections, 12 | val images: Images 13 | ) { 14 | 15 | var dominantcolor: Int = -12038056 16 | } 17 | 18 | data class Work( 19 | val occupation: String, 20 | val base: String 21 | ) 22 | 23 | data class Powerstats( 24 | val intelligence: Int = 0, 25 | val strength: Int = 0, 26 | val speed: Int = 0, 27 | val durability: Int = 0, 28 | val power: Int = 0, 29 | val combat: Int = 0 30 | ) 31 | 32 | data class Images( 33 | val xs: String, 34 | val sm: String, 35 | val md: String, 36 | val lg: String 37 | ) 38 | 39 | data class Connections( 40 | val groupAffiliation: String, 41 | val relatives: String 42 | ) 43 | 44 | data class Biography( 45 | val fullName: String, 46 | val alterEgos: String, 47 | val aliases: List, 48 | val placeOfBirth: String, 49 | val firstAppearance: String, 50 | val publisher: String?, 51 | val alignment: String 52 | ) 53 | 54 | data class Appearance( 55 | val gender: String, 56 | val race: String?, 57 | val height: List, 58 | val weight: List, 59 | val eyeColor: String, 60 | val hairColor: String 61 | ) -------------------------------------------------------------------------------- /app/src/main/res/navigation/navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 19 | 20 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/network/DataTransferObjects.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.network 2 | 3 | 4 | import com.doiliomatsinhe.dccharacters.database.DatabaseCharacter 5 | import com.doiliomatsinhe.dccharacters.model.* 6 | 7 | 8 | data class NetworkCharacter( 9 | val id: Int, 10 | val name: String, 11 | val slug: String, 12 | val powerstats: Powerstats, 13 | val appearance: Appearance, 14 | val biography: Biography, 15 | val work: Work, 16 | val connections: Connections, 17 | val images: Images 18 | ) 19 | 20 | /** 21 | * Converts Network results to Domain Objects 22 | */ 23 | fun List.asDomainModel(): List { 24 | return map { 25 | Character( 26 | id = it.id, 27 | name = it.name, 28 | slug = it.slug, 29 | powerstats = it.powerstats, 30 | appearance = it.appearance, 31 | biography = it.biography, 32 | work = it.work, 33 | connections = it.connections, 34 | images = it.images 35 | ) 36 | } 37 | } 38 | 39 | /** 40 | * Converts Network results to Database Objects 41 | */ 42 | fun List.asDatabaseModel(): Array { 43 | return map { 44 | DatabaseCharacter( 45 | id = it.id, 46 | name = it.name, 47 | slug = it.slug, 48 | powerstats = it.powerstats, 49 | appearance = it.appearance, 50 | biography = it.biography, 51 | work = it.work, 52 | connections = it.connections, 53 | images = it.images 54 | ) 55 | }.toTypedArray() 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/power_stat_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 28 | 29 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/xml/fragment_character_detail_scene.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 27 | 28 | 29 | 35 | 36 | 41 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/ui/character/CharactersViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.ui.character 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.paging.Pager 8 | import androidx.paging.PagingConfig 9 | import androidx.paging.PagingData 10 | import com.doiliomatsinhe.dccharacters.database.asDomainModel 11 | import com.doiliomatsinhe.dccharacters.model.Filters 12 | import com.doiliomatsinhe.dccharacters.model.Character 13 | import com.doiliomatsinhe.dccharacters.repository.CharacterRepository 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.SupervisorJob 17 | import kotlinx.coroutines.flow.Flow 18 | import kotlinx.coroutines.flow.map 19 | import kotlinx.coroutines.launch 20 | 21 | class CharactersViewModel @ViewModelInject constructor 22 | (private val repository: CharacterRepository) : ViewModel() { 23 | 24 | private val viewModelJob = SupervisorJob() 25 | private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) 26 | 27 | private val _navigateToCharacterDetail = MutableLiveData() 28 | val navigateToCharacterDetail: LiveData 29 | get() = _navigateToCharacterDetail 30 | 31 | var filters: Filters = Filters() 32 | private val pagingConfig = 33 | PagingConfig(pageSize = 55, enablePlaceholders = false, maxSize = 300) 34 | 35 | fun getList(filters: Filters): Flow> { 36 | return Pager(pagingConfig) { 37 | repository.getCharacters(filters) 38 | }.flow.map { 39 | it.asDomainModel() 40 | } 41 | } 42 | 43 | init { 44 | uiScope.launch { 45 | repository.refreshCharacters() 46 | } 47 | filters = Filters() 48 | } 49 | 50 | fun onCharacterClicked(character: Character) { 51 | _navigateToCharacterDetail.value = character 52 | } 53 | 54 | fun onCharacterDetailNavigated() { 55 | _navigateToCharacterDetail.value = null 56 | } 57 | 58 | override fun onCleared() { 59 | super.onCleared() 60 | viewModelJob.cancel() 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/database/DatabaseEntities.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.database 2 | 3 | 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.map 6 | import androidx.paging.PagingData 7 | import androidx.room.Embedded 8 | import androidx.room.Entity 9 | import androidx.room.PrimaryKey 10 | import com.doiliomatsinhe.dccharacters.model.* 11 | 12 | @Entity(tableName = "characters") 13 | data class DatabaseCharacter( 14 | @PrimaryKey 15 | val id: Int, 16 | val name: String, 17 | val slug: String, 18 | @Embedded val powerstats: Powerstats, 19 | @Embedded val appearance: Appearance, 20 | @Embedded val biography: Biography, 21 | @Embedded val work: Work, 22 | @Embedded val connections: Connections, 23 | @Embedded val images: Images 24 | ) 25 | 26 | /** 27 | * Converts Database results to Domain Objects 28 | */ 29 | fun List.asDomainModel(): List { 30 | 31 | return map { 32 | Character( 33 | id = it.id, 34 | name = it.name, 35 | slug = it.slug, 36 | powerstats = it.powerstats, 37 | appearance = it.appearance, 38 | biography = it.biography, 39 | work = it.work, 40 | connections = it.connections, 41 | images = it.images 42 | ) 43 | } 44 | } 45 | 46 | /** 47 | * Converts Database results to Domain Objects 48 | */ 49 | fun LiveData.asDomainModel(): LiveData { 50 | 51 | return map { 52 | Character( 53 | id = it.id, 54 | name = it.name, 55 | slug = it.slug, 56 | powerstats = it.powerstats, 57 | appearance = it.appearance, 58 | biography = it.biography, 59 | work = it.work, 60 | connections = it.connections, 61 | images = it.images 62 | ) 63 | } 64 | } 65 | 66 | fun PagingData.asDomainModel(): PagingData { 67 | 68 | return map { 69 | Character( 70 | id = it.id, 71 | name = it.name, 72 | slug = it.slug, 73 | powerstats = it.powerstats, 74 | appearance = it.appearance, 75 | biography = it.biography, 76 | work = it.work, 77 | connections = it.connections, 78 | images = it.images 79 | ) 80 | } 81 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/repository/CharacterRepository.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.repository 2 | 3 | 4 | import androidx.lifecycle.LiveData 5 | import androidx.paging.PagingSource 6 | import androidx.sqlite.db.SimpleSQLiteQuery 7 | import com.doiliomatsinhe.dccharacters.database.DatabaseCharacter 8 | import com.doiliomatsinhe.dccharacters.database.CharacterDao 9 | import com.doiliomatsinhe.dccharacters.database.asDomainModel 10 | import com.doiliomatsinhe.dccharacters.model.Filters 11 | import com.doiliomatsinhe.dccharacters.model.Character 12 | import com.doiliomatsinhe.dccharacters.network.ApiService 13 | import com.doiliomatsinhe.dccharacters.network.asDatabaseModel 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.withContext 16 | import timber.log.Timber 17 | import javax.inject.Inject 18 | 19 | class CharacterRepository @Inject constructor( 20 | private val service: ApiService, 21 | private val database: CharacterDao 22 | ) { 23 | 24 | fun getCharacters(filters: Filters): PagingSource { 25 | 26 | val builtQuery = if (filters.gender.isNotEmpty() && filters.race.isNotEmpty()) { 27 | SimpleSQLiteQuery( 28 | "SELECT * FROM characters WHERE publisher ='DC Comics' AND gender = ? AND race = ?", 29 | arrayOf(filters.gender, filters.race) 30 | ) 31 | } else if (filters.gender.isEmpty() && filters.race.isNotEmpty()) { 32 | SimpleSQLiteQuery( 33 | "SELECT * FROM characters WHERE publisher ='DC Comics' AND race = ?", 34 | arrayOf(filters.race) 35 | ) 36 | } else if (filters.race.isEmpty() && filters.gender.isNotEmpty()) { 37 | SimpleSQLiteQuery( 38 | "SELECT * FROM characters WHERE publisher ='DC Comics' AND gender = ?", 39 | arrayOf(filters.gender) 40 | ) 41 | } else { 42 | SimpleSQLiteQuery("SELECT * FROM characters WHERE publisher ='DC Comics'") 43 | } 44 | 45 | return database.getRawListOfCharacters(builtQuery) 46 | 47 | } 48 | 49 | suspend fun refreshCharacters() { 50 | withContext(Dispatchers.IO) { 51 | try { 52 | val listOfVillains = service.getCharacters() 53 | database.insertAllCharacters(*listOfVillains.asDatabaseModel()) 54 | } catch (e: Exception) { 55 | Timber.d("Error: ${e.message}") 56 | } 57 | } 58 | } 59 | 60 | suspend fun getCharacterById(id: Int): LiveData { 61 | return withContext(Dispatchers.IO) { 62 | val villain = database.getCharactersById(id) 63 | 64 | villain.asDomainModel() 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DC Characters 3 | Character image 4 | Couldn\'t load list! 5 | Sort by Name 6 | Any Race 7 | Any Gender 8 | Filter 9 | Sorting image 10 | Filter by race image 11 | Filter by gender image 12 | Apply 13 | DC Cover Image 14 | DC Character image 15 | Power Stats 16 | Coming Soon 17 | Appearance 18 | Biography 19 | Affiliations 20 | Relatives 21 | Oops!!!\nError Loading Data 22 | Retry 23 | No results found \nClear filters? 24 | Clear 25 | Strength 26 | Speed 27 | Power 28 | Durability 29 | Combat 30 | Intelligence 31 | 32 | 33 | 34 | 35 | @string/sort_by_name 36 | Sort by Combat Skill 37 | Sort by Durability 38 | Sort by Intelligence 39 | Sort by Power 40 | Sort by Speed 41 | Sort by Strength 42 | 43 | 44 | 45 | @string/any_race 46 | Alien 47 | Android 48 | Amazon 49 | Animal 50 | Atlantean 51 | Bizarro 52 | Bolovaxian 53 | Cyborg 54 | Czarnian 55 | Demi-God 56 | Demon 57 | God / Eternal 58 | Gorilla 59 | Human 60 | Human / Cosmic 61 | Human / Radiation 62 | Human-Vuldarian 63 | Korugaran 64 | Kryptonian 65 | Metahuman 66 | Martian 67 | New God 68 | Parademon 69 | Talokite 70 | Tamaranean 71 | Ungaran 72 | Zombie 73 | 74 | 75 | 76 | @string/any_gender 77 | Male 78 | Female 79 | Unknown 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/adapter/BindingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.adapter 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Color 5 | import android.view.View 6 | import android.widget.ImageView 7 | import android.widget.ProgressBar 8 | import android.widget.TextView 9 | import androidx.cardview.widget.CardView 10 | import androidx.constraintlayout.widget.ConstraintLayout 11 | import androidx.databinding.BindingAdapter 12 | import androidx.paging.LoadState 13 | import androidx.palette.graphics.Palette 14 | import androidx.recyclerview.widget.RecyclerView 15 | import com.bumptech.glide.Glide 16 | import com.bumptech.glide.load.DataSource 17 | import com.bumptech.glide.load.engine.DiskCacheStrategy 18 | import com.bumptech.glide.load.engine.GlideException 19 | import com.bumptech.glide.request.RequestListener 20 | import com.bumptech.glide.request.target.Target 21 | import com.doiliomatsinhe.dccharacters.model.Character 22 | 23 | 24 | @BindingAdapter(value = ["characterImage", "cardView"], requireAll = false) 25 | fun ImageView.setCharacterImage(item: Character, cardView: CardView) { 26 | 27 | Glide.with(context) 28 | .asBitmap() 29 | .load(item.images.md) 30 | .diskCacheStrategy(DiskCacheStrategy.ALL) 31 | .listener(object : RequestListener { 32 | override fun onLoadFailed( 33 | e: GlideException?, 34 | model: Any?, 35 | target: Target?, 36 | isFirstResource: Boolean 37 | ): Boolean { 38 | cardView.setCardBackgroundColor(item.dominantcolor) 39 | return false 40 | } 41 | 42 | override fun onResourceReady( 43 | resource: Bitmap?, 44 | model: Any?, 45 | target: Target?, 46 | dataSource: DataSource?, 47 | isFirstResource: Boolean 48 | ): Boolean { 49 | 50 | 51 | if (resource != null) { 52 | val p = Palette.from(resource).generate() 53 | // Use generated instance 54 | item.dominantcolor = p.getDarkMutedColor(Color.DKGRAY) 55 | cardView.setCardBackgroundColor(p.getDarkMutedColor(Color.DKGRAY)) 56 | } 57 | return false 58 | } 59 | }) 60 | .into(this) 61 | } 62 | 63 | @BindingAdapter("groupAffiliation") 64 | fun TextView.setGroupAffiliation(item: Character) { 65 | 66 | text = if (item.connections.groupAffiliation != "-") { 67 | item.connections.groupAffiliation 68 | } else { 69 | "No Affiliations" 70 | } 71 | 72 | } 73 | 74 | @BindingAdapter("characterName") 75 | fun TextView.setCharacterName(item: Character) { 76 | text = item.name 77 | } 78 | 79 | @BindingAdapter("visibility") 80 | fun ProgressBar.setVisibility(item: LoadState) { 81 | 82 | when (item) { 83 | is LoadState.Loading -> { 84 | this.visibility = View.VISIBLE 85 | } 86 | else -> this.visibility = View.GONE 87 | } 88 | } 89 | 90 | @BindingAdapter("visibility") 91 | fun ConstraintLayout.setVisibility(item: LoadState) { 92 | 93 | when (item) { 94 | is LoadState.Error -> { 95 | this.visibility = View.VISIBLE 96 | } 97 | else -> this.visibility = View.GONE 98 | } 99 | } 100 | 101 | @BindingAdapter("visibility") 102 | fun RecyclerView.setVisibility(item: LoadState) { 103 | 104 | when (item) { 105 | is LoadState.NotLoading -> { 106 | this.visibility = View.VISIBLE 107 | } 108 | else -> this.visibility = View.GONE 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DC-Characters 🦸‍♂️ 2 | Application that displays a list of DC Characters using Modern Android Application Development tools and API's. 3 | The App Makes a request to an [API](https://rosariopfernandes.github.io/dc-villains-api/) and parses the JSON response to display the results. 4 | 5 | [Video Demonstration](https://youtu.be/6rpBNI2BEf0) 6 | 7 | 8 | ![1](https://user-images.githubusercontent.com/38020305/87644247-2d51d100-c74c-11ea-837a-c57f3e0eb62e.png) 9 | ![2](https://user-images.githubusercontent.com/38020305/87644265-3478df00-c74c-11ea-851b-0027028553ec.png) 10 | ![6](https://user-images.githubusercontent.com/38020305/87727325-0f728380-c7c1-11ea-8de2-94496c2106a2.png) 11 | ![4](https://user-images.githubusercontent.com/38020305/87644274-36db3900-c74c-11ea-97e6-3fbfd7d30691.png) 12 | ![5](https://user-images.githubusercontent.com/38020305/87644280-380c6600-c74c-11ea-842f-3933e794d2d7.png) 13 | 14 | 15 | ## Built With 16 | 17 | * [Android Jetpack](https://developer.android.com/jetpack/?gclid=Cj0KCQjwhJrqBRDZARIsALhp1WQBmjQ4WUpnRT4ETGGR1T_rQG8VU3Ta_kVwiznZASR5y4fgPDRYFqkaAhtfEALw_wcB) - Suite of libraries, tools, and guidance to help developers write high-quality apps easier. 18 | * [Android KTX](https://developer.android.com/kotlin/ktx) 19 | * [Databinding](https://developer.android.com/jetpack/androidx/releases/databinding) 20 | * [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) 21 | * [MotionLayout](https://developer.android.com/jetpack/androidx/releases/constraintlayout) 22 | * [Navigation](https://developer.android.com/jetpack/androidx/releases/navigation) 23 | * [Paging](https://developer.android.com/jetpack/androidx/releases/paging) 24 | * [Palette](https://developer.android.com/jetpack/androidx/releases/palette) 25 | * [Room](https://developer.android.com/topic/libraries/architecture/room) 26 | * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 27 | * [Glide](https://github.com/bumptech/glide) - A fast and efficient open source media management and image loading framework for Android. 28 | * [GSON](https://github.com/google/gson) - Java library that can be used to convert Java Objects into their JSON representation. 29 | * [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) - Library that provides a standard way to incorporate Dagger dependency injection into an Android application. 30 | * [Kotlin Coroutines](https://developer.android.com/kotlin/coroutines) - Concurrency design pattern used on Android to simplify code that executes asynchronously. 31 | * [PercentageChartView](https://github.com/RamiJ3mli/PercentageChartView) - Custom view that displays the progress of a single given task. 32 | * [Retrofit 2](https://github.com/square/retrofit) - A type-safe HTTP client for Android and Java. 33 | * [Timber](https://github.com/JakeWharton/timber) - Logger with a small, extensible API which provides utility on top of Android's normal Log class. 34 | 35 | 36 | 37 | ## Author 38 | 39 | * **Doilio A. Matsinhe** 40 | - *Contact me on* 41 | - [Twitter](https://twitter.com/DoilioMatsinhe) 42 | - [Instagram](https://www.instagram.com/doiliomatsinhe/) 43 | - [LinkedIn](https://www.linkedin.com/in/doilio-matsinhe) 44 | 45 | 46 | ## License 47 | 48 | Copyright 2020 Doilio Abel Matsinhe 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); 51 | you may not use this file except in compliance with the License. 52 | You may obtain a copy of the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, 58 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 59 | See the License for the specific language governing permissions and 60 | limitations under the License. 61 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: "androidx.navigation.safeargs.kotlin" 6 | apply plugin: 'dagger.hilt.android.plugin' 7 | 8 | android { 9 | compileSdkVersion 30 10 | buildToolsVersion "30.0.0" 11 | 12 | defaultConfig { 13 | applicationId "com.doiliomatsinhe.dccharacters" 14 | minSdkVersion 19 15 | targetSdkVersion 30 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables.useSupportLibrary = true 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility = 1.8 32 | targetCompatibility = 1.8 33 | } 34 | kotlinOptions { 35 | jvmTarget = JavaVersion.VERSION_1_8 36 | } 37 | 38 | dataBinding { 39 | enabled = true 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation fileTree(dir: "libs", include: ["*.jar"]) 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 46 | implementation "androidx.core:core-ktx:1.3.1" 47 | implementation "androidx.fragment:fragment-ktx:1.2.5" 48 | implementation "androidx.appcompat:appcompat:1.2.0" 49 | implementation "androidx.legacy:legacy-support-v4:1.0.0" 50 | testImplementation "junit:junit:4.13" 51 | androidTestImplementation "androidx.test.ext:junit:1.1.1" 52 | androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" 53 | 54 | // ConstraintLayout 55 | implementation "androidx.constraintlayout:constraintlayout:$constraint_version" 56 | 57 | // ViewModel and LiveData 58 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$viewmodel_version" 59 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$viewmodel_version" 60 | 61 | // Room database 62 | // optional - Kotlin Extensions and Coroutines support for Room 63 | implementation "androidx.room:room-runtime:$room_version" 64 | kapt "androidx.room:room-compiler:$room_version" 65 | implementation "androidx.room:room-ktx:$room_version" 66 | 67 | // Paging 68 | implementation "androidx.paging:paging-runtime:$paging_version" 69 | 70 | // Retrofit & Gson Converter 71 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 72 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" 73 | 74 | // Coroutines 75 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" 76 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" 77 | 78 | // Navigation 79 | implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version" 80 | implementation "android.arch.navigation:navigation-ui-ktx:$nav_version" 81 | 82 | // Timber 83 | implementation "com.jakewharton.timber:timber:$timber_version" 84 | 85 | // Palette 86 | implementation "androidx.palette:palette:$palette_version" 87 | 88 | // Glide 89 | implementation "com.github.bumptech.glide:glide:$glide_version" 90 | kapt "com.github.bumptech.glide:compiler:$glide_version" 91 | 92 | // Hilt 93 | implementation "com.google.dagger:hilt-android:$hilt_version" 94 | kapt "com.google.dagger:hilt-android-compiler:$hilt_version" 95 | 96 | // Hilt Extension for ViewModels 97 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_ext_version" 98 | kapt "androidx.hilt:hilt-compiler:$hilt_ext_version" 99 | 100 | // Percentage ChartView 101 | implementation "com.ramijemli.percentagechartview:percentagechartview:$chartview_version" 102 | 103 | 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/adapter/PowerStatAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.core.content.ContextCompat 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.doiliomatsinhe.dccharacters.R 8 | import com.doiliomatsinhe.dccharacters.adapter.PowerStatAdapter.ViewHolder.Companion.from 9 | import com.doiliomatsinhe.dccharacters.databinding.PowerStatItemBinding 10 | import com.doiliomatsinhe.dccharacters.model.Powerstats 11 | 12 | class PowerStatAdapter : RecyclerView.Adapter() { 13 | 14 | var powerstats: Powerstats = Powerstats() 15 | set(value) { 16 | field = value 17 | notifyDataSetChanged() 18 | } 19 | 20 | class ViewHolder constructor(var binding: PowerStatItemBinding) : 21 | RecyclerView.ViewHolder(binding.root) { 22 | 23 | fun bind(powerstats: Powerstats, position: Int) { 24 | val context = binding.root.context 25 | when (position) { 26 | 0 -> { 27 | binding.powerstatValue.progressColor = 28 | ContextCompat.getColor(context, R.color.colorIntelligence) 29 | binding.powerstatText.text = context.getString(R.string.intelligence) 30 | binding.powerstatValue.setProgress(powerstats.intelligence.toFloat(), true) 31 | } 32 | 1 -> { 33 | binding.powerstatValue.progressColor = 34 | ContextCompat.getColor(context, R.color.colorCombat) 35 | binding.powerstatText.text = context.getString(R.string.combat) 36 | binding.powerstatValue.setProgress(powerstats.combat.toFloat(), true) 37 | } 38 | 2 -> { 39 | 40 | binding.powerstatValue.progressColor = 41 | ContextCompat.getColor(context, R.color.colorDurability) 42 | binding.powerstatText.text = context.getString(R.string.durability) 43 | binding.powerstatValue.setProgress(powerstats.durability.toFloat(), true) 44 | } 45 | 3 -> { 46 | 47 | binding.powerstatValue.progressColor = 48 | ContextCompat.getColor(context, R.color.colorPower) 49 | binding.powerstatText.text = context.getString(R.string.power) 50 | binding.powerstatValue.setProgress(powerstats.power.toFloat(), true) 51 | } 52 | 4 -> { 53 | 54 | binding.powerstatValue.progressColor = 55 | ContextCompat.getColor(context, R.color.colorSpeed) 56 | binding.powerstatText.text = context.getString(R.string.speed) 57 | binding.powerstatValue.setProgress(powerstats.speed.toFloat(), true) 58 | } 59 | 5 -> { 60 | 61 | binding.powerstatValue.progressColor = 62 | ContextCompat.getColor(context, R.color.colorStrength) 63 | binding.powerstatText.text = context.getString(R.string.strength) 64 | binding.powerstatValue.setProgress(powerstats.strength.toFloat(), true) 65 | } 66 | } 67 | } 68 | 69 | companion object { 70 | fun from(parent: ViewGroup): ViewHolder { 71 | val layoutInflater = LayoutInflater.from(parent.context) 72 | val binding = PowerStatItemBinding.inflate(layoutInflater, parent, false) 73 | 74 | return ViewHolder(binding) 75 | } 76 | } 77 | } 78 | 79 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 80 | return from(parent) 81 | 82 | } 83 | 84 | 85 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 86 | holder.bind(powerstats, position) 87 | } 88 | 89 | 90 | override fun getItemCount() = 6 91 | 92 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /app/src/main/res/layout/character_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 30 | 31 | 35 | 36 | 47 | 48 | 57 | 58 | 59 | 60 | 79 | 80 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/doiliomatsinhe/dccharacters/ui/character/CharactersFragment.kt: -------------------------------------------------------------------------------- 1 | package com.doiliomatsinhe.dccharacters.ui.character 2 | 3 | import android.graphics.drawable.ColorDrawable 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.* 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.content.ContextCompat 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.viewModels 11 | import androidx.lifecycle.Observer 12 | import androidx.lifecycle.lifecycleScope 13 | import androidx.navigation.fragment.findNavController 14 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 15 | import com.doiliomatsinhe.dccharacters.R 16 | import com.doiliomatsinhe.dccharacters.adapter.CharacterAdapter 17 | import com.doiliomatsinhe.dccharacters.adapter.CharacterClickListener 18 | import com.doiliomatsinhe.dccharacters.databinding.FragmentCharactersBinding 19 | import com.doiliomatsinhe.dccharacters.model.Filters 20 | import com.doiliomatsinhe.dccharacters.ui.filter.FilterFragment 21 | import dagger.hilt.android.AndroidEntryPoint 22 | import kotlinx.coroutines.flow.collectLatest 23 | import kotlinx.coroutines.launch 24 | 25 | @AndroidEntryPoint 26 | class CharactersFragment : Fragment(), 27 | FilterFragment.FilterDialogListener { 28 | 29 | private lateinit var binding: FragmentCharactersBinding 30 | private val charactersViewModel: CharactersViewModel by viewModels() 31 | private lateinit var characterAdapter: CharacterAdapter 32 | 33 | override fun onCreateView( 34 | inflater: LayoutInflater, 35 | container: ViewGroup?, 36 | savedInstanceState: Bundle? 37 | ): View? { 38 | // Inflate the layout for this fragment 39 | binding = FragmentCharactersBinding.inflate(inflater, container, false) 40 | 41 | setupActionBar() 42 | 43 | return binding.root 44 | } 45 | 46 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 47 | super.onViewCreated(view, savedInstanceState) 48 | 49 | initComponents() 50 | onFilter(charactersViewModel.filters) 51 | 52 | charactersViewModel.navigateToCharacterDetail.observe(viewLifecycleOwner, Observer { 53 | it?.let { 54 | findNavController().navigate( 55 | CharactersFragmentDirections.actionVillainsFragmentToVillainDetailFragment( 56 | it.id, 57 | it.name, 58 | it.dominantcolor 59 | ) 60 | ) 61 | charactersViewModel.onCharacterDetailNavigated() 62 | } 63 | }) 64 | 65 | } 66 | 67 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 68 | if (item.itemId == R.id.ic_filter) { 69 | openFilterDialog() 70 | } 71 | 72 | return true 73 | } 74 | 75 | private fun openFilterDialog() { 76 | val dialog = FilterFragment( 77 | charactersViewModel.filters 78 | ) 79 | dialog.setTargetFragment(this, 1) 80 | dialog.show(parentFragmentManager, "Filter Dialog") 81 | } 82 | 83 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 84 | super.onCreateOptionsMenu(menu, inflater) 85 | inflater.inflate(R.menu.power_stats_menu, menu) 86 | } 87 | 88 | private fun fetchData(filters: Filters) { 89 | lifecycleScope.launch { 90 | charactersViewModel.getList(filters).collectLatest { pagedList -> 91 | characterAdapter.submitData(pagedList) 92 | } 93 | } 94 | } 95 | 96 | private fun initComponents() { 97 | 98 | // Adapter 99 | characterAdapter = CharacterAdapter(CharacterClickListener { 100 | charactersViewModel.onCharacterClicked(it) 101 | }).apply { 102 | addLoadStateListener { 103 | binding.loadState = it.refresh 104 | } 105 | } 106 | 107 | binding.lifecycleOwner = viewLifecycleOwner 108 | 109 | 110 | binding.characterList.adapter = characterAdapter 111 | binding.characterList.hasFixedSize() 112 | binding.characterList.layoutManager = StaggeredGridLayoutManager( 113 | resources.getInteger(R.integer.span_count), 114 | StaggeredGridLayoutManager.VERTICAL 115 | ) 116 | 117 | // Reseting filters 118 | binding.buttonRetry.setOnClickListener { 119 | val filters = Filters() 120 | onFilter(filters) 121 | } 122 | 123 | } 124 | 125 | private fun setupActionBar() { 126 | ((activity as AppCompatActivity).supportActionBar)?.setBackgroundDrawable( 127 | ColorDrawable( 128 | ContextCompat.getColor( 129 | activity as AppCompatActivity, R.color.colorPrimary 130 | ) 131 | ) 132 | ) 133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 134 | ((activity as AppCompatActivity).window)?.statusBarColor = ContextCompat.getColor( 135 | activity as AppCompatActivity, R.color.colorPrimaryDark 136 | ) 137 | } 138 | 139 | setHasOptionsMenu(true) 140 | } 141 | 142 | override fun onFilter(filters: Filters) { 143 | fetchData(filters) 144 | 145 | charactersViewModel.filters = filters 146 | 147 | } 148 | 149 | 150 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/filter_dialog_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 29 | 30 | 42 | 43 | 54 | 55 | 67 | 68 | 79 | 80 | 92 | 93 |