├── .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 |
4 |
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 |
5 |
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 |
8 |
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 |
--------------------------------------------------------------------------------
/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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | 
9 | 
10 | 
11 | 
12 | 
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 |
5 |
6 |
7 |
8 |
9 |
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 |
120 |
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 |
106 |
107 |
118 |
119 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/doiliomatsinhe/dccharacters/ui/filter/FilterFragment.kt:
--------------------------------------------------------------------------------
1 | package com.doiliomatsinhe.dccharacters.ui.filter
2 |
3 | import android.app.AlertDialog
4 | import android.app.Dialog
5 | import android.content.Context
6 | import android.os.Bundle
7 | import android.view.View
8 | import android.widget.*
9 | import androidx.appcompat.app.AppCompatDialogFragment
10 | import com.doiliomatsinhe.dccharacters.R
11 | import com.doiliomatsinhe.dccharacters.model.Filters
12 | import timber.log.Timber
13 |
14 | class FilterFragment(private val recoveredFilters: Filters) : AppCompatDialogFragment() {
15 |
16 | private lateinit var spinnerSortBy: Spinner
17 | private lateinit var spinnerFilterRace: Spinner
18 | private lateinit var spinnerFilterGender: Spinner
19 | private lateinit var cancelButton: Button
20 | private lateinit var applyButton: Button
21 | private lateinit var filterListener: FilterDialogListener
22 | private lateinit var sortAdapter: ArrayAdapter
23 | private lateinit var raceAdapter: ArrayAdapter
24 | private lateinit var genderAdapter: ArrayAdapter
25 |
26 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
27 | val builder = AlertDialog.Builder(requireActivity(), R.style.AlertDialogTheme)
28 | val inflater = requireActivity().layoutInflater
29 | val view: View = inflater.inflate(R.layout.filter_dialog_layout, null)
30 |
31 | // Init components
32 | initComponents(view)
33 |
34 | configureSpinners()
35 |
36 | recoverSpinnerPositions()
37 |
38 | builder.setView(view)
39 | return builder.create()
40 | }
41 |
42 | private fun recoverSpinnerPositions() {
43 | val sortSpinnerPosition = sortAdapter.getPosition(recoveredFilters.order)
44 | spinnerSortBy.setSelection(sortSpinnerPosition)
45 | spinnerSortBy.isEnabled = false //TODO Enable after finding a clean way to Implement sorting
46 |
47 | val genderSpinnerPosition = if (recoveredFilters.gender == "-") {
48 | genderAdapter.getPosition("Unknown")
49 | } else {
50 | genderAdapter.getPosition(recoveredFilters.gender)
51 | }
52 | spinnerFilterGender.setSelection(genderSpinnerPosition)
53 |
54 | val raceSpinnerPosition = raceAdapter.getPosition(recoveredFilters.race)
55 | spinnerFilterRace.setSelection(raceSpinnerPosition)
56 | }
57 |
58 | private fun configureSpinners() {
59 | // Sort Spinner
60 | val sortList = resources.getStringArray(R.array.sort_by)
61 | sortAdapter = ArrayAdapter(
62 | requireActivity(),
63 | android.R.layout.simple_spinner_item,
64 | sortList
65 | )
66 | sortAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
67 | spinnerSortBy.adapter = sortAdapter
68 |
69 |
70 | // Race Spinner
71 | val raceList = resources.getStringArray(R.array.filter_by_race)
72 | raceAdapter = ArrayAdapter(
73 | requireActivity(),
74 | android.R.layout.simple_spinner_item,
75 | raceList
76 | )
77 | raceAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
78 | spinnerFilterRace.adapter = raceAdapter
79 |
80 | // Gender Spinner
81 | val genderList = resources.getStringArray(R.array.filter_by_gender)
82 | genderAdapter = ArrayAdapter(
83 | requireActivity(),
84 | android.R.layout.simple_spinner_item, genderList
85 | )
86 | genderAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
87 | spinnerFilterGender.adapter = genderAdapter
88 |
89 | }
90 |
91 | override fun onAttach(context: Context) {
92 | super.onAttach(context)
93 | filterListener = try {
94 | (targetFragment as FilterDialogListener)
95 | } catch (e: ClassCastException) {
96 | throw ClassCastException("$context must implement FilterDialogListener")
97 | }
98 |
99 | }
100 |
101 | private fun onApplyClicked() {
102 | filterListener.onFilter(getFilters())
103 | }
104 |
105 | interface FilterDialogListener {
106 | fun onFilter(filters: Filters)
107 | }
108 |
109 | private fun initComponents(view: View) {
110 | spinnerSortBy = view.findViewById(R.id.spinner_stats)
111 | spinnerFilterRace = view.findViewById(R.id.filter_race)
112 | spinnerFilterGender = view.findViewById(R.id.filter_gender)
113 | cancelButton = view.findViewById(R.id.filter_cancel)
114 | applyButton = view.findViewById(R.id.fitler_apply)
115 |
116 | // Set Click Listeners
117 | cancelButton.setOnClickListener { dismiss() }
118 | applyButton.setOnClickListener {
119 | onApplyClicked()
120 | dismiss()
121 | }
122 | }
123 |
124 | private fun getSortOrder(): String {
125 | val selected = spinnerSortBy.selectedItem as String
126 | Timber.d("Selected order $selected")
127 | return selected
128 | }
129 |
130 | private fun getGender(): String {
131 | val gender = when (val selected = spinnerFilterGender.selectedItem as String) {
132 | "Any Gender" -> ""
133 | "Unknown" -> "-"
134 | else -> selected
135 | }
136 |
137 | Timber.d("Selected gender $gender")
138 | return gender
139 | }
140 |
141 | private fun getRace(): String {
142 | val selected = spinnerFilterRace.selectedItem as String
143 | val race = if (selected == "Any Race") {
144 | ""
145 | } else {
146 | selected
147 | }
148 | Timber.d("Selected race $race")
149 | return race
150 | }
151 |
152 | private fun getFilters(): Filters {
153 | val filters = Filters()
154 | filters.order = getSortOrder()
155 | filters.gender = getGender()
156 | filters.race = getRace()
157 |
158 | return filters
159 | }
160 |
161 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_characters.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
19 |
20 |
37 |
38 |
48 |
49 |
66 |
67 |
81 |
82 |
83 |
100 |
101 |
115 |
116 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/app/src/main/java/com/doiliomatsinhe/dccharacters/ui/characterdetail/CharacterDetailFragment.kt:
--------------------------------------------------------------------------------
1 | package com.doiliomatsinhe.dccharacters.ui.characterdetail
2 |
3 | import android.graphics.drawable.ColorDrawable
4 | import android.os.Build
5 | import android.os.Bundle
6 | import androidx.fragment.app.Fragment
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.lifecycle.Observer
12 | import androidx.lifecycle.ViewModelProvider
13 | import com.bumptech.glide.Glide
14 | import com.doiliomatsinhe.dccharacters.R
15 | import com.doiliomatsinhe.dccharacters.adapter.PowerStatAdapter
16 | import com.doiliomatsinhe.dccharacters.databinding.FragmentCharacterDetailBinding
17 | import com.doiliomatsinhe.dccharacters.model.Character
18 | import com.doiliomatsinhe.dccharacters.repository.CharacterRepository
19 | import com.doiliomatsinhe.dccharacters.utils.ColorUtils
20 | import dagger.hilt.android.AndroidEntryPoint
21 | import javax.inject.Inject
22 |
23 | @AndroidEntryPoint
24 | class CharacterDetailFragment : Fragment() {
25 |
26 | private lateinit var binding: FragmentCharacterDetailBinding
27 | private lateinit var viewModel: CharacterDetailViewModel
28 | private lateinit var arguments: CharacterDetailFragmentArgs
29 | private lateinit var adapter: PowerStatAdapter
30 |
31 | @Inject
32 | lateinit var repository: CharacterRepository
33 |
34 | override fun onCreateView(
35 | inflater: LayoutInflater, container: ViewGroup?,
36 | savedInstanceState: Bundle?
37 | ): View? {
38 | // Inflate the layout for this fragment
39 | binding = FragmentCharacterDetailBinding.inflate(inflater, container, false)
40 |
41 | arguments = CharacterDetailFragmentArgs.fromBundle(requireArguments())
42 |
43 | setupActionBar(arguments)
44 |
45 |
46 | return binding.root
47 | }
48 |
49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
50 | super.onViewCreated(view, savedInstanceState)
51 |
52 | initComponents()
53 |
54 | viewModel.getVillain().observe(viewLifecycleOwner, Observer {
55 | it?.let {
56 | populateUI(it)
57 | }
58 | })
59 |
60 | }
61 |
62 | private fun populateUI(character: Character) {
63 | Glide.with(this).load(character.images.sm).placeholder(R.drawable.placeholder)
64 | .error(R.drawable.placeholder).into(binding.profileCharacter)
65 |
66 | // Populate Appearance
67 | val appearance = character.appearance
68 | val villainRace = when (appearance.race) {
69 | null -> " with unknown race"
70 | else -> " ${appearance.race}"
71 | }
72 | val villainHair = when (appearance.hairColor) {
73 | "-" -> "."
74 | "No Hair" -> " and ${appearance.hairColor}."
75 | else -> " and ${appearance.hairColor} hair."
76 | }
77 | val villainEyeColor = when (appearance.eyeColor) {
78 | "-" -> ""
79 | else -> ", has ${appearance.eyeColor} eyes"
80 | }
81 |
82 | val appearanceText =
83 | "${character.name} is a ${appearance.gender}$villainRace, who is ${appearance.height[1]} tall, weights ${appearance.weight[1]}$villainEyeColor$villainHair"
84 | binding.textAppearance.text = appearanceText
85 |
86 | // Populate Biography
87 | val biography = character.biography
88 | val work = character.work
89 |
90 | val fullName = when (biography.fullName) {
91 | "" -> "Fullname is unknown.\n"
92 | else -> "Fullname is ${biography.fullName}.\n"
93 | }
94 | val placeOfBirth = when (biography.placeOfBirth) {
95 | "-" -> "Place of birth is unknown.\n"
96 | else -> "Born in ${biography.placeOfBirth}.\n"
97 | }
98 |
99 | val aliasesText: String
100 | if (biography.aliases[0] == "-") {
101 | aliasesText = ""
102 | } else {
103 | var aliases = ""
104 | for (name in biography.aliases) {
105 | aliases += "* $name\n"
106 | }
107 | aliasesText = "Usually goes by:\n$aliases\n"
108 | }
109 |
110 | val occupation = when (work.occupation) {
111 | "-" -> "Unknown\n"
112 | else -> "${work.occupation}\n"
113 |
114 | }
115 |
116 | val base = when (work.base) {
117 | "-" -> "Unknown"
118 | else -> work.base
119 |
120 | }
121 |
122 | val biographyText =
123 | "$fullName$placeOfBirth${aliasesText}First appeared in ${biography.firstAppearance} published by ${biography.publisher}.\nOcupation: ${occupation}Based at: $base".trim()
124 | binding.textBiography.text = biographyText
125 |
126 | // Populate Affiliations
127 |
128 | val connections = character.connections
129 |
130 | val affiliationsText = when (connections.groupAffiliation) {
131 | "-" -> "No known affiliations."
132 | else -> {
133 |
134 | var result = ""
135 | if (connections.groupAffiliation.contains("; ")) {
136 | for (affiliation in connections.groupAffiliation.split("; ")) {
137 | result += "* $affiliation\n"
138 | }
139 | } else {
140 | for (affiliation in connections.groupAffiliation.split(", ")) {
141 | result += "* $affiliation\n"
142 | }
143 | }
144 | "Affiliated with: \n$result"
145 | }
146 | }
147 |
148 | binding.textAffiliations.text = affiliationsText.trim()
149 |
150 | // Populate Relatives
151 |
152 | val relativesText = when (connections.relatives) {
153 | "-" -> "No known relatives.\n"
154 | else -> {
155 |
156 | var result = ""
157 | if (connections.relatives.contains("; ")) {
158 | for (relative in connections.relatives.split("; ")) {
159 | result += "* $relative\n"
160 | }
161 | } else {
162 | for (relative in connections.relatives.split(", ")) {
163 | result += "* $relative\n"
164 | }
165 | }
166 | result
167 | }
168 | }
169 |
170 | binding.textRelatives.text = relativesText.trim()
171 |
172 | // Populate Power Stats
173 | adapter.powerstats = character.powerstats
174 | binding.powerstatsView.adapter = adapter
175 |
176 | }
177 |
178 |
179 | private fun setupActionBar(arguments: CharacterDetailFragmentArgs) {
180 | ((activity as AppCompatActivity).supportActionBar)?.title = arguments.villainName
181 | ((activity as AppCompatActivity).supportActionBar)?.setBackgroundDrawable(
182 | ColorDrawable(
183 | arguments.cardColor
184 | )
185 | )
186 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
187 | ((activity as AppCompatActivity).window)?.statusBarColor = ColorUtils.manipulateColor(
188 | arguments.cardColor, 0.50f
189 | )
190 | }
191 |
192 | binding.apply {
193 | titleAffiliations.setTextColor(arguments.cardColor)
194 | titleAppearance.setTextColor(arguments.cardColor)
195 | titleBiography.setTextColor(arguments.cardColor)
196 | titlePowerStats.setTextColor(arguments.cardColor)
197 | titleRelatives.setTextColor(arguments.cardColor)
198 |
199 | }
200 |
201 | }
202 |
203 | private fun initComponents() {
204 | val factory = CharacterDetailViewModelFactory(repository, arguments.villainId)
205 | viewModel = ViewModelProvider(this, factory).get(CharacterDetailViewModel::class.java)
206 |
207 | binding.powerstatsView.hasFixedSize()
208 | adapter = PowerStatAdapter()
209 | }
210 |
211 |
212 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_character_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
19 |
20 |
31 |
32 |
43 |
44 |
50 |
51 |
52 |
53 |
62 |
63 |
66 |
67 |
81 |
82 |
88 |
89 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
118 |
119 |
124 |
125 |
133 |
134 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
159 |
160 |
165 |
166 |
174 |
175 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
201 |
202 |
207 |
208 |
220 |
221 |
233 |
234 |
235 |
236 |
237 |
238 |
250 |
251 |
256 |
257 |
265 |
266 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
--------------------------------------------------------------------------------