├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── 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
│ │ │ ├── drawable
│ │ │ │ ├── transformers_team_placeholder.png
│ │ │ │ ├── selector_white.xml
│ │ │ │ ├── ic_add_24dp.xml
│ │ │ │ ├── ic_close_24dp.xml
│ │ │ │ ├── btn_rounded_white.xml
│ │ │ │ ├── btn_rounded_accent.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── zero_state.xml
│ │ │ │ ├── toolbar_button.xml
│ │ │ │ ├── activity_trans_list.xml
│ │ │ │ ├── item_transformer.xml
│ │ │ │ ├── layout_item_stats.xml
│ │ │ │ └── activity_create_transformer.xml
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── dimens.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── xyz
│ │ │ │ └── derekcsm
│ │ │ │ └── transformers
│ │ │ │ ├── model
│ │ │ │ ├── TransformersResponse.kt
│ │ │ │ └── Transformer.kt
│ │ │ │ ├── base
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── CoroutinesViewModel.kt
│ │ │ │ └── SharedPref.kt
│ │ │ │ ├── ui
│ │ │ │ ├── create_transformer
│ │ │ │ │ ├── CreateTransformerView.kt
│ │ │ │ │ ├── CreateTransformerViewModel.kt
│ │ │ │ │ └── CreateTransformerActivity.kt
│ │ │ │ └── transformers_list
│ │ │ │ │ ├── TransListView.kt
│ │ │ │ │ ├── adapter
│ │ │ │ │ ├── TransListAdapter.kt
│ │ │ │ │ └── TransformerViewHolder.kt
│ │ │ │ │ ├── TransListViewModel.kt
│ │ │ │ │ └── TransListActivity.kt
│ │ │ │ ├── persistence
│ │ │ │ ├── AppDatabase.kt
│ │ │ │ └── TransformersDao.kt
│ │ │ │ ├── TransformersApp.kt
│ │ │ │ ├── network
│ │ │ │ ├── RequestInterceptor.kt
│ │ │ │ ├── ApiService.kt
│ │ │ │ └── ApiAuthenticator.kt
│ │ │ │ ├── di
│ │ │ │ ├── PersistenceModule.kt
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ └── NetworkModule.kt
│ │ │ │ ├── repository
│ │ │ │ ├── TransformersRepository.kt
│ │ │ │ ├── Repository.kt
│ │ │ │ └── CreateEditTransformerRepository.kt
│ │ │ │ └── utils
│ │ │ │ └── ConnectivityUtils.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── xyz
│ │ │ └── derekcsm
│ │ │ └── transformers
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── xyz
│ │ └── derekcsm
│ │ └── transformers
│ │ ├── CreateTransformerTest.kt
│ │ ├── BaseTestRobot.kt
│ │ └── CreateFlowTestRobot.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── runConfigurations.xml
├── checkstyle-idea.xml
├── jarRepositories.xml
└── misc.xml
├── gradle.properties
├── README.md
├── .gitignore
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "Transformers"
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/transformers_team_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/derekcsm/Transformers/master/app/src/main/res/drawable/transformers_team_placeholder.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/model/TransformersResponse.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.model
2 |
3 | data class TransformersResponse(
4 | var transformers: List
5 | )
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/base/Constants.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.base
2 |
3 | object Constants {
4 | val SHARED_PREF_TOKEN = "token"
5 | val TEAM_DECEPTICONS = "D"
6 | val TEAM_AUTOBOTS = "A"
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jun 17 12:01:51 PDT 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/drawable/selector_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/create_transformer/CreateTransformerView.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.create_transformer
2 |
3 | import android.content.Context
4 |
5 | interface CreateTransformerView {
6 | fun onRequestCompleted()
7 | fun showLoading()
8 | fun hideLoading()
9 | fun getContext() : Context
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_rounded_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_rounded_accent.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/transformers_list/TransListView.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.transformers_list
2 |
3 | import android.content.Context
4 | import xyz.derekcsm.transformers.model.Transformer
5 |
6 | interface TransListView {
7 | fun populateList(transformersList: List)
8 | fun getItemCount(): Int
9 | fun showLoading()
10 | fun hideLoading()
11 | fun getContext(): Context
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/persistence/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.persistence
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import xyz.derekcsm.transformers.model.Transformer
6 |
7 | @Database(entities = [Transformer::class], version = 1, exportSchema = true)
8 | abstract class AppDatabase : RoomDatabase() {
9 |
10 | abstract fun transformersDao(): TransformersDao
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/test/java/xyz/derekcsm/transformers/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers
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/java/xyz/derekcsm/transformers/TransformersApp.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers
2 |
3 | import android.app.Application
4 | import com.facebook.drawee.backends.pipeline.Fresco
5 | import com.facebook.stetho.Stetho
6 | import dagger.hilt.android.HiltAndroidApp
7 |
8 | @HiltAndroidApp
9 | class TransformersApp : Application() {
10 |
11 | override fun onCreate() {
12 | super.onCreate()
13 | Fresco.initialize(this)
14 | Stetho.initializeWithDefaults(this)
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/zero_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/model/Transformer.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import com.google.gson.annotations.SerializedName
6 |
7 | @Entity
8 | data class Transformer(
9 | @PrimaryKey
10 | var id: String,
11 | var name: String,
12 | var team: String,
13 | var strength: Int,
14 | var intelligence: Int,
15 | var speed: Int,
16 | var endurance: Int,
17 | var rank: Int,
18 | var courage: Int,
19 | var firepower: Int,
20 | var skill: Int,
21 | @SerializedName("team_icon")
22 | var teamIcon: String?
23 | )
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | @color/white
6 | #03A9F4
7 |
8 | #fff
9 | #000
10 | #80000000
11 | #F6707B
12 |
13 |
14 | #27ffffff
15 |
16 | #4C47AE
17 | #DE0012
18 |
19 |
--------------------------------------------------------------------------------
/.idea/checkstyle-idea.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/network/RequestInterceptor.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.network
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 | import xyz.derekcsm.transformers.base.Constants
6 | import xyz.derekcsm.transformers.base.SharedPref
7 | import java.io.IOException
8 |
9 | class RequestInterceptor(private val sharedPref: SharedPref) : Interceptor {
10 |
11 | @Throws(IOException::class)
12 | override fun intercept(chain: Interceptor.Chain): Response {
13 | val builder = chain.request().newBuilder()
14 | builder.addHeader("Content-Type", "application/json")
15 |
16 | val token = sharedPref.read(Constants.SHARED_PREF_TOKEN, "")
17 | builder.addHeader("Authorization", "Bearer " + token)
18 |
19 | return chain.proceed(builder.build())
20 | }
21 | }
--------------------------------------------------------------------------------
/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/xyz/derekcsm/transformers/base/CoroutinesViewModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.base
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.liveData
6 | import androidx.lifecycle.viewModelScope
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.SupervisorJob
10 |
11 | abstract class CoroutinesViewModel : ViewModel() {
12 |
13 | val viewModelJob = SupervisorJob()
14 | val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
15 |
16 | inline fun launchOnViewModelScope(crossinline block: suspend () -> LiveData): LiveData {
17 | return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
18 | emitSource(block())
19 | }
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/network/ApiService.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.network
2 |
3 | import kotlinx.coroutines.Deferred
4 | import retrofit2.Response
5 | import retrofit2.http.*
6 | import xyz.derekcsm.transformers.model.Transformer
7 | import xyz.derekcsm.transformers.model.TransformersResponse
8 |
9 | interface ApiService {
10 |
11 | @GET("allspark")
12 | fun getAuthentication(): Deferred>
13 |
14 | @GET("transformers")
15 | suspend fun getTransformers(): TransformersResponse
16 |
17 | @POST("transformers")
18 | suspend fun createTransformer(@Body transformer: Transformer): Transformer
19 |
20 | @PUT("transformers")
21 | suspend fun updateTransformer(@Body transformer: Transformer): Transformer
22 |
23 | @DELETE("transformers/{transformerId}")
24 | suspend fun deleteTransformer(@Path("transformerId") id: String): Response
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/persistence/TransformersDao.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.persistence
2 |
3 | import androidx.room.*
4 | import xyz.derekcsm.transformers.model.Transformer
5 |
6 | @Dao
7 | interface TransformersDao {
8 |
9 | @Insert(onConflict = OnConflictStrategy.REPLACE)
10 | fun insertTransformersList(pokemonList: List)
11 |
12 | @Query("SELECT * FROM Transformer")
13 | fun getTransformersList(): List
14 |
15 | @Query("SELECT * FROM Transformer WHERE id =:id")
16 | fun getTransformer(id: String): Transformer
17 |
18 | @Query("DELETE FROM Transformer WHERE id =:id")
19 | fun deleteTransformer(id: String)
20 |
21 |
22 | @Transaction
23 | fun upsert(transformer: Transformer) {
24 | val id = insert(transformer)
25 | if (id == -1L) {
26 | update(transformer)
27 | }
28 | }
29 |
30 | @Insert(onConflict = OnConflictStrategy.IGNORE)
31 | abstract fun insert(obj: Transformer): Long
32 |
33 | @Update
34 | abstract fun update(obj: Transformer)
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/di/PersistenceModule.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.di
2 |
3 | import android.app.Application
4 | import androidx.room.Room
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.components.ApplicationComponent
9 | import xyz.derekcsm.transformers.persistence.AppDatabase
10 | import xyz.derekcsm.transformers.persistence.TransformersDao
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(ApplicationComponent::class)
15 | object PersistenceModule {
16 |
17 | @Provides
18 | @Singleton
19 | fun provideAppDatabase(application: Application): AppDatabase {
20 | return Room
21 | .databaseBuilder(application, AppDatabase::class.java, "Transformers.db")
22 | .fallbackToDestructiveMigration()
23 | .allowMainThreadQueries()
24 | .build()
25 | }
26 |
27 | @Provides
28 | @Singleton
29 | fun provideTransformersDao(appDatabase: AppDatabase): TransformersDao {
30 | return appDatabase.transformersDao()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/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/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Transformers
3 |
4 | Autobot
5 | Decepticon
6 |
7 | Name
8 | Team
9 | Strength
10 | Intelligence
11 | Speed
12 | Endurance
13 | Rank
14 | Courage
15 | Firepower
16 | Skill
17 |
18 | %1$s: %2$s
19 | Fight!
20 | Save
21 | Delete
22 |
23 | Your Transformer needs a name!
24 |
25 | You haven\'t created any Transformers yet
26 | Network error
27 | No internet connection
28 | Error unknown
29 | Error timeout
30 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.android.components.ActivityRetainedComponent
7 | import dagger.hilt.android.scopes.ActivityRetainedScoped
8 | import xyz.derekcsm.transformers.network.ApiService
9 | import xyz.derekcsm.transformers.persistence.TransformersDao
10 | import xyz.derekcsm.transformers.repository.CreateEditTransformerRepository
11 | import xyz.derekcsm.transformers.repository.TransformersRepository
12 |
13 | @Module
14 | @InstallIn(ActivityRetainedComponent::class)
15 | object RepositoryModule {
16 |
17 | @Provides
18 | @ActivityRetainedScoped
19 | fun provideTransformersRepository(
20 | apiService: ApiService,
21 | transformersDao: TransformersDao
22 | ): TransformersRepository {
23 | return TransformersRepository(apiService, transformersDao)
24 | }
25 |
26 | @Provides
27 | @ActivityRetainedScoped
28 | fun provideCreateEditRepository(
29 | apiService: ApiService,
30 | transformersDao: TransformersDao
31 | ): CreateEditTransformerRepository {
32 | return CreateEditTransformerRepository(apiService, transformersDao)
33 | }
34 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Transformers
2 | ## A Demo project by @derekcsm
3 |
4 | Hey there review team! Please consider this project a reasonable approximation of my skills following a mix of new patterns and some old-school K.I.S.S. methodology.
5 |
6 | Given that I'm quite busy these days, and that I'm confident you'll see my qualifications from this project, I felt it was best to just focus on the *core* of the application, and not build the fighting feature - I hope you can understand. Doing this gave me more time to refactor and consider things as I normally would while working.
7 |
8 | ### Technologies/libraries used:
9 |
10 | - MVVM + Repository Architecture
11 | - Kotlin
12 | - Hilt (Dagger)
13 | - Okhttp3 + Retrofit2
14 | - Coroutines
15 | - AndroidX Lifecycle
16 | - Gson
17 | - Room DB
18 | - Stetho for debugging
19 | - Espresso (using robot builder pattern)
20 |
21 | ### Features:
22 |
23 | - Automatic authorization system using OkHttp Authenticator + RequestInterceptor (that could easily be adapter to automatically refresh an expired token)
24 | - Create a Transformer
25 | - Edit existing Transformers
26 | - Delete Transformers from the list directly
27 | - Pull to refresh transformers list, with a zero state implementation
28 | - Request and network error handling throughout
29 |
30 | Looking forward to talking with you :) - Derek
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
13 |
14 |
17 |
18 |
21 |
22 |
25 |
26 |
30 |
31 |
35 |
--------------------------------------------------------------------------------
/.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 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16dp
5 | 8dp
6 | 4dp
7 | 72dp
8 | 36dp
9 | 24dp
10 |
11 |
12 | 112sp
13 | 56sp
14 | 45sp
15 | 34sp
16 | 30sp
17 | 24sp
18 | 20sp
19 | 16sp
20 | 14sp
21 | 12sp
22 |
23 |
24 | 36dp
25 | 24dp
26 |
27 |
28 | 56dp
29 | 52dp
30 | 20dp
31 | 60dp
32 | 42dp
33 | 4dp
34 |
35 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/xyz/derekcsm/transformers/CreateTransformerTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.filters.LargeTest
5 | import androidx.test.rule.ActivityTestRule
6 | import org.junit.Rule
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import xyz.derekcsm.transformers.ui.transformers_list.TransListActivity
10 |
11 | @RunWith(AndroidJUnit4::class)
12 | @LargeTest
13 | class CreateTransformerTest : CreateFlowTestRobot() {
14 |
15 | @get:Rule
16 | var activityRule: ActivityTestRule =
17 | ActivityTestRule(TransListActivity::class.java)
18 |
19 | @Test
20 | fun test_create_transformer_1() {
21 | val transformerName = "Transformer-" + (0..10000).random()
22 | pressCreate()
23 | typeTransformerName(transformerName)
24 | saveTransformer()
25 | }
26 |
27 | @Test
28 | fun test_edit_transformer_1() {
29 | val transformerName = "Transformer-" + (0..10000).random() + "-Edited"
30 | clickFirstTransformerInRecycler()
31 | typeTransformerName(transformerName)
32 | saveTransformer()
33 | checkTransformerNameInRecyclerView(0, transformerName)
34 | }
35 |
36 | /*
37 | Could easily compose more actions here based on this pattern, just keeping it short and simple
38 | */
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/base/SharedPref.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.base
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 |
7 | class SharedPref(private val context: Context) {
8 |
9 | private var mSharedPref: SharedPreferences? = null
10 |
11 | init {
12 | if (mSharedPref == null) mSharedPref =
13 | context.getSharedPreferences(context.packageName, Activity.MODE_PRIVATE)
14 | }
15 |
16 | fun read(key: String?, defValue: String?): String? {
17 | return mSharedPref!!.getString(key, defValue)
18 | }
19 |
20 | fun write(key: String?, value: String?) {
21 | val prefsEditor = mSharedPref!!.edit()
22 | prefsEditor.putString(key, value)
23 | prefsEditor.apply()
24 | }
25 |
26 | fun read(key: String?, defValue: Boolean): Boolean {
27 | return mSharedPref!!.getBoolean(key, defValue)
28 | }
29 |
30 | fun write(key: String?, value: Boolean) {
31 | val prefsEditor = mSharedPref!!.edit()
32 | prefsEditor.putBoolean(key, value)
33 | prefsEditor.apply()
34 | }
35 |
36 | fun read(key: String?, defValue: Int): Int? {
37 | return mSharedPref!!.getInt(key, defValue)
38 | }
39 |
40 | fun write(key: String?, value: Int?) {
41 | val prefsEditor = mSharedPref!!.edit()
42 | prefsEditor.putInt(key, value!!).apply()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/xyz/derekcsm/transformers/BaseTestRobot.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers
2 |
3 | import android.view.View
4 | import androidx.annotation.IdRes
5 | import androidx.recyclerview.widget.RecyclerView
6 | import androidx.test.espresso.Espresso
7 | import androidx.test.espresso.ViewAssertion
8 | import androidx.test.espresso.ViewInteraction
9 | import androidx.test.espresso.action.ViewActions
10 | import androidx.test.espresso.assertion.ViewAssertions
11 | import androidx.test.espresso.contrib.RecyclerViewActions
12 | import androidx.test.espresso.matcher.ViewMatchers
13 | import junit.framework.Assert.assertNotNull
14 | import junit.framework.Assert.assertTrue
15 | import org.hamcrest.Matcher
16 |
17 | open class BaseTestRobot {
18 |
19 | fun clickView(resId: Int): ViewInteraction = Espresso.onView((ViewMatchers.withId(resId)))
20 | .perform(ViewActions.click())
21 |
22 | fun textView(resId: Int): ViewInteraction = Espresso.onView(ViewMatchers.withId(resId))
23 |
24 | fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
25 | .check(ViewAssertions.matches(ViewMatchers.withText(text)))
26 |
27 | fun matchText(resId: Int, text: String): ViewInteraction = matchText(textView(resId), text)
28 |
29 | fun typeTextIntoEditText(text: String, resId: Int): ViewInteraction =
30 | Espresso.onView(ViewMatchers.withId(resId)).perform(ViewActions.typeText(text))
31 |
32 | fun clearEditText(resId: Int): ViewInteraction =
33 | Espresso.onView(ViewMatchers.withId(resId)).perform(ViewActions.clearText())
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/repository/TransformersRepository.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.repository
2 |
3 | import xyz.derekcsm.transformers.model.Transformer
4 | import xyz.derekcsm.transformers.model.TransformersResponse
5 | import xyz.derekcsm.transformers.network.ApiService
6 | import xyz.derekcsm.transformers.persistence.TransformersDao
7 | import javax.inject.Inject
8 |
9 | class TransformersRepository @Inject constructor(
10 | private val apiService: ApiService,
11 | private val transformersDao: TransformersDao
12 | ) : Repository() {
13 |
14 | private val TAG = "TransformersRepository"
15 |
16 | fun fetchTransformersFromDB(): List {
17 | return transformersDao.getTransformersList()
18 | }
19 |
20 | suspend fun fetchTransformersFromNetwork(errorEmitter: RemoteErrorEmitter): List? {
21 | var transformersList: List = listOf()
22 |
23 | val transformersListResponse: TransformersResponse? = safeApiCall(errorEmitter) {
24 | apiService.getTransformers()
25 | }
26 |
27 | if (transformersListResponse != null) {
28 | transformersList = transformersListResponse.transformers
29 | transformersDao.insertTransformersList(transformersList)
30 | return transformersList
31 | }
32 |
33 | return null
34 | }
35 |
36 | suspend fun deleteTransformer(errorEmitter: RemoteErrorEmitter, id: String) {
37 | transformersDao.deleteTransformer(id)
38 | safeApiCall(errorEmitter) {
39 | apiService.deleteTransformer(id)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/repository/Repository.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.repository
2 |
3 | import android.util.Log
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import okhttp3.ResponseBody
7 | import org.json.JSONObject
8 | import retrofit2.HttpException
9 | import java.io.IOException
10 | import java.net.SocketTimeoutException
11 |
12 | open class Repository {
13 |
14 | suspend inline fun safeApiCall(
15 | emitter: RemoteErrorEmitter,
16 | crossinline responseFunction: suspend () -> T
17 | ): T? {
18 | return try {
19 | val response = withContext(Dispatchers.IO) { responseFunction.invoke() }
20 | response
21 | } catch (e: Exception) {
22 | withContext(Dispatchers.Main) {
23 | e.printStackTrace()
24 | Log.e("ApiCalls", "Call error: ${e.localizedMessage}", e.cause)
25 | when (e) {
26 | is HttpException -> {
27 | emitter.onError(""+ e.code() + " Error")
28 | }
29 | is SocketTimeoutException -> emitter.onError(ErrorType.TIMEOUT)
30 | is IOException -> emitter.onError(ErrorType.NETWORK)
31 | else -> emitter.onError(ErrorType.UNKNOWN)
32 | }
33 | }
34 | null
35 | }
36 | }
37 | }
38 |
39 | interface RemoteErrorEmitter {
40 | fun onError(msg: String)
41 | fun onError(errorType: ErrorType)
42 | }
43 |
44 | enum class ErrorType {
45 | NETWORK, // IO
46 | TIMEOUT, // Socket
47 | UNKNOWN //Anything else
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/utils/ConnectivityUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.utils
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.os.Build
7 |
8 | class ConnectivityUtils {
9 |
10 | fun isInternetAvailable(context: Context): Boolean {
11 | var result = false
12 | val connectivityManager =
13 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
14 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
15 | val networkCapabilities = connectivityManager.activeNetwork ?: return false
16 | val actNw =
17 | connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
18 | result = when {
19 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
20 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
21 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
22 | else -> false
23 | }
24 | } else {
25 | connectivityManager.run {
26 | connectivityManager.activeNetworkInfo?.run {
27 | result = when (type) {
28 | ConnectivityManager.TYPE_WIFI -> true
29 | ConnectivityManager.TYPE_MOBILE -> true
30 | ConnectivityManager.TYPE_ETHERNET -> true
31 | else -> false
32 | }
33 |
34 | }
35 | }
36 | }
37 |
38 | return result
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/repository/CreateEditTransformerRepository.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.repository
2 |
3 | import xyz.derekcsm.transformers.model.Transformer
4 | import xyz.derekcsm.transformers.model.TransformersResponse
5 | import xyz.derekcsm.transformers.network.ApiService
6 | import xyz.derekcsm.transformers.persistence.TransformersDao
7 | import javax.inject.Inject
8 |
9 | class CreateEditTransformerRepository @Inject constructor(
10 | private val apiService: ApiService,
11 | private val transformersDao: TransformersDao
12 | ) : Repository() {
13 |
14 | private val TAG = "CreateEditRepository"
15 |
16 | fun getTransformerFromDB(id: String): Transformer {
17 | return transformersDao.getTransformer(id)
18 | }
19 |
20 | suspend fun createTransformer(errorEmitter: RemoteErrorEmitter, transformer: Transformer): Transformer? {
21 |
22 | val transformerResponse: Transformer? = safeApiCall(errorEmitter) {
23 | apiService.createTransformer(transformer)
24 | }
25 |
26 | transformerResponse?.let { transformersDao.upsert(it) }
27 | return transformerResponse
28 | }
29 |
30 | suspend fun updateTransformer(errorEmitter: RemoteErrorEmitter, transformer: Transformer): Transformer? {
31 |
32 | val transformerResponse: Transformer? = safeApiCall(errorEmitter) {
33 | apiService.updateTransformer(transformer)
34 | }
35 |
36 | transformerResponse?.let { transformersDao.upsert(it) }
37 | return transformerResponse
38 | }
39 | }
40 |
41 | data class CreateUpdateTransformerRepositoryResponse(
42 | var transformer: Transformer?,
43 | var errorMessage: String?
44 | )
45 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/transformers_list/adapter/TransListAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.transformers_list.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import xyz.derekcsm.transformers.R
7 | import xyz.derekcsm.transformers.model.Transformer
8 |
9 | class TransListAdapter(val listener: TransListAdapterListener) :
10 | RecyclerView.Adapter() {
11 |
12 | private var adapterItems = mutableListOf()
13 |
14 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
15 | return TransformerViewHolder(
16 | LayoutInflater.from(parent.context).inflate(R.layout.item_transformer, parent, false)
17 | )
18 | }
19 |
20 | override fun getItemCount(): Int = adapterItems.size
21 |
22 | override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
23 | val transformerViewHolder = viewHolder as TransformerViewHolder
24 | transformerViewHolder.bindView(listener, adapterItems[position])
25 | }
26 |
27 | fun setItems(adapterItems: List) {
28 | this.adapterItems.clear()
29 | this.adapterItems.addAll(adapterItems)
30 | notifyDataSetChanged()
31 | }
32 |
33 | fun removeTransformer(id: String) {
34 | adapterItems.forEachIndexed { index, value ->
35 | if (value.id.equals(id)) {
36 | adapterItems.removeAt(index)
37 | notifyItemRemoved(index)
38 | return
39 | }
40 | }
41 | }
42 | }
43 |
44 | interface TransListAdapterListener {
45 | fun onTransformerClicked(transformer: Transformer)
46 | fun onDeleteTransformerClicked(transformer: Transformer)
47 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## taken from https://github.com/github/gitignore/blob/master/Android.gitignore
2 | # Built application files
3 | *.apk
4 | *.aar
5 | *.ap_
6 | *.aab
7 |
8 | # Files for the ART/Dalvik VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # Generated files
15 | bin/
16 | gen/
17 | out/
18 | # Uncomment the following line in case you need and you don't have the release build type files in your app
19 | # release/
20 |
21 | # Gradle files
22 | .gradle/
23 | build/
24 |
25 | # Local configuration file (sdk path, etc)
26 | local.properties
27 |
28 | # Proguard folder generated by Eclipse
29 | proguard/
30 |
31 | # Log Files
32 | *.log
33 |
34 | # Android Studio Navigation editor temp files
35 | .navigation/
36 |
37 | # Android Studio captures folder
38 | captures/
39 |
40 | # IntelliJ
41 | *.iml
42 | .idea/workspace.xml
43 | .idea/tasks.xml
44 | .idea/gradle.xml
45 | .idea/assetWizardSettings.xml
46 | .idea/dictionaries
47 | .idea/libraries
48 | # Android Studio 3 in .gitignore file.
49 | .idea/caches
50 | .idea/modules.xml
51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
52 | .idea/navEditor.xml
53 |
54 | # Keystore files
55 | # Uncomment the following lines if you do not want to check your keystore files in.
56 | #*.jks
57 | #*.keystore
58 |
59 | # External native build folder generated in Android Studio 2.2 and later
60 | .externalNativeBuild
61 | .cxx/
62 |
63 | # Google Services (e.g. APIs or Firebase)
64 | # google-services.json
65 |
66 | # Freeline
67 | freeline.py
68 | freeline/
69 | freeline_project_description.json
70 |
71 | # fastlane
72 | fastlane/report.xml
73 | fastlane/Preview.html
74 | fastlane/screenshots
75 | fastlane/test_output
76 | fastlane/readme.md
77 |
78 | # Version control
79 | vcs.xml
80 |
81 | # lint
82 | lint/intermediates/
83 | lint/generated/
84 | lint/outputs/
85 | lint/tmp/
86 | # lint/reports/
--------------------------------------------------------------------------------
/app/src/androidTest/java/xyz/derekcsm/transformers/CreateFlowTestRobot.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers
2 |
3 | import androidx.recyclerview.widget.RecyclerView
4 | import androidx.test.espresso.Espresso.onView
5 | import androidx.test.espresso.action.ViewActions.click
6 | import androidx.test.espresso.assertion.ViewAssertions.matches
7 | import androidx.test.espresso.contrib.RecyclerViewActions
8 | import androidx.test.espresso.matcher.ViewMatchers
9 | import androidx.test.espresso.matcher.ViewMatchers.withId
10 | import it.xabaras.android.espresso.recyclerviewchildactions.RecyclerViewChildActions.Companion.childOfViewAtPositionWithMatcher
11 | import xyz.derekcsm.transformers.ui.transformers_list.adapter.TransformerViewHolder
12 | import org.hamcrest.core.AllOf.allOf
13 |
14 |
15 | open class CreateFlowTestRobot : BaseTestRobot() {
16 |
17 | fun pressCreate() {
18 | clickView(R.id.fab)
19 | }
20 |
21 | fun typeTransformerName(name: String) {
22 | clearEditText(R.id.et_transformer_name)
23 | typeTextIntoEditText(name, R.id.et_transformer_name)
24 | }
25 |
26 | fun saveTransformer() {
27 | clickView(R.id.btn_toolbar)
28 | }
29 |
30 | fun clickFirstTransformerInRecycler() {
31 | onView(withId(R.id.rv_transformers))
32 | .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()))
33 | }
34 |
35 | fun checkTransformerNameInRecyclerView(index: Int, name: String) {
36 | onView(withId(R.id.rv_transformers))
37 | .perform(RecyclerViewActions.scrollToPosition(index))
38 | .check(
39 | matches(
40 | allOf(
41 | childOfViewAtPositionWithMatcher(
42 | R.id.tv_name,
43 | index,
44 | ViewMatchers.withText(name)
45 | )
46 | )
47 | )
48 | )
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
27 |
28 |
34 |
35 |
39 |
40 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_trans_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
22 |
23 |
28 |
29 |
36 |
37 |
38 |
39 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/transformers_list/TransListViewModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.transformers_list
2 |
3 | import android.util.Log
4 | import android.widget.Toast
5 | import androidx.hilt.lifecycle.ViewModelInject
6 | import kotlinx.coroutines.launch
7 | import xyz.derekcsm.transformers.R
8 | import xyz.derekcsm.transformers.base.CoroutinesViewModel
9 | import xyz.derekcsm.transformers.repository.ErrorType
10 | import xyz.derekcsm.transformers.repository.RemoteErrorEmitter
11 | import xyz.derekcsm.transformers.repository.TransformersRepository
12 |
13 | class TransListViewModel @ViewModelInject constructor(
14 | private val transformersRepository: TransformersRepository
15 | ) : CoroutinesViewModel(), RemoteErrorEmitter {
16 |
17 | private val TAG = "TransListViewModel"
18 |
19 | var view: TransListView? = null
20 | fun connectViewInterface(view: TransListView) {
21 | this.view = view
22 | }
23 |
24 | init {
25 | Log.d(TAG, "initialized!")
26 | }
27 |
28 | fun deleteTransformer(id: String) {
29 | uiScope.launch {
30 | transformersRepository.deleteTransformer(this@TransListViewModel, id)
31 | }
32 | }
33 |
34 | fun fetchTransformers() {
35 | view!!.showLoading()
36 | view!!.populateList(transformersRepository.fetchTransformersFromDB())
37 |
38 | uiScope.launch {
39 | val response =
40 | transformersRepository.fetchTransformersFromNetwork(this@TransListViewModel)
41 |
42 | if (response != null) {
43 | view!!.populateList(response)
44 | }
45 | view!!.hideLoading()
46 | }
47 | }
48 |
49 | override fun onError(msg: String) {
50 | Toast.makeText(view!!.getContext(), msg, Toast.LENGTH_LONG).show()
51 | }
52 |
53 | override fun onError(errorType: ErrorType) {
54 | when (errorType) {
55 | ErrorType.UNKNOWN -> {
56 | onError(view!!.getContext().getString(R.string.error_unknown))
57 | }
58 | ErrorType.TIMEOUT -> {
59 | onError(view!!.getContext().getString(R.string.error_timeout))
60 | }
61 | ErrorType.NETWORK -> {
62 | onError(view!!.getContext().getString(R.string.network_error))
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/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/res/layout/item_transformer.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
22 |
23 |
29 |
30 |
35 |
36 |
43 |
44 |
45 |
46 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/network/ApiAuthenticator.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.network
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.runBlocking
9 | import okhttp3.Authenticator
10 | import okhttp3.Request
11 | import okhttp3.Response
12 | import okhttp3.Route
13 | import xyz.derekcsm.transformers.base.Constants
14 | import xyz.derekcsm.transformers.base.SharedPref
15 | import xyz.derekcsm.transformers.di.NetworkModule
16 | import kotlin.coroutines.CoroutineContext
17 |
18 | class ApiAuthenticator(val context: Context, val sharedPref: SharedPref) : Authenticator,
19 | CoroutineScope {
20 |
21 | private val TAG = "ApiAuthenticator"
22 | private val RETRY_TAG = "transReqRetryCount"
23 |
24 | override val coroutineContext: CoroutineContext
25 | get() = Dispatchers.Main + Job()
26 |
27 | override fun authenticate(route: Route?, response: Response): Request? {
28 | Log.d(TAG, "authenticate() called with: route = $route, response = $response")
29 |
30 | val previousRetryCount = retryCount(response)
31 |
32 | if (previousRetryCount > 1) {
33 | return response.request
34 | }
35 |
36 | val newRequest = reAuthenticateRequestUsingRefreshToken(
37 | response.request,
38 | previousRetryCount + 1
39 | )
40 |
41 | return newRequest
42 | }
43 |
44 | private fun retryCount(response: Response): Int {
45 | return response.header(RETRY_TAG)?.toInt() ?: 0
46 | }
47 |
48 | @Synchronized
49 | private fun reAuthenticateRequestUsingRefreshToken(
50 | staleRequest: Request,
51 | retryCount: Int
52 | ): Request? {
53 | val authToken = renewToken().body()
54 | if (authToken != null) {
55 | sharedPref.write(Constants.SHARED_PREF_TOKEN, authToken.toString())
56 | return rewriteRequest(staleRequest, retryCount, authToken.toString())
57 | } else {
58 | return rewriteRequest(staleRequest, retryCount,"")
59 | }
60 | }
61 |
62 | private fun renewToken() = runBlocking {
63 | val apiService = NetworkModule.buildAuthenticatorApiServiceInstance(context, sharedPref)
64 | apiService.getAuthentication().await()
65 | }
66 |
67 | private fun rewriteRequest(
68 | staleRequest: Request, retryCount: Int, authToken: String
69 | ): Request? {
70 | return staleRequest.newBuilder()
71 | .removeHeader("Authorization")
72 | .header("Authorization", "Bearer $authToken")
73 | .header(RETRY_TAG, "$retryCount")
74 | .build()
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/create_transformer/CreateTransformerViewModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.create_transformer
2 |
3 | import android.util.Log
4 | import android.widget.Toast
5 | import androidx.hilt.lifecycle.ViewModelInject
6 | import kotlinx.coroutines.launch
7 | import xyz.derekcsm.transformers.R
8 | import xyz.derekcsm.transformers.base.CoroutinesViewModel
9 | import xyz.derekcsm.transformers.model.Transformer
10 | import xyz.derekcsm.transformers.repository.CreateEditTransformerRepository
11 | import xyz.derekcsm.transformers.repository.ErrorType
12 | import xyz.derekcsm.transformers.repository.RemoteErrorEmitter
13 |
14 | class CreateTransformerViewModel @ViewModelInject constructor(
15 | private val createEditRepository: CreateEditTransformerRepository
16 | ) : CoroutinesViewModel(), RemoteErrorEmitter {
17 |
18 | private val TAG = "CreateTransViewModel"
19 |
20 | var view: CreateTransformerView? = null
21 | fun connectViewInterface(view: CreateTransformerView) {
22 | this.view = view
23 | }
24 |
25 | init {
26 | Log.d(TAG, "initialized!")
27 | }
28 |
29 | fun getTransformerFromDB(transformerId: String): Transformer {
30 | return createEditRepository.getTransformerFromDB(transformerId)
31 | }
32 |
33 | fun createTransformer(transformer: Transformer) {
34 | view!!.showLoading()
35 | uiScope.launch {
36 | val transformerResponse = createEditRepository.createTransformer(this@CreateTransformerViewModel, transformer)
37 |
38 | if (transformerResponse != null) {
39 | view!!.onRequestCompleted()
40 | } else {
41 | view!!.hideLoading()
42 | }
43 | }
44 | }
45 |
46 | fun updateTransformer(transformer: Transformer) {
47 | view!!.showLoading()
48 | uiScope.launch {
49 | val transformerResponse = createEditRepository.updateTransformer(this@CreateTransformerViewModel, transformer)
50 |
51 | if (transformerResponse != null) {
52 | view!!.onRequestCompleted()
53 | } else {
54 | view!!.hideLoading()
55 | }
56 | }
57 | }
58 |
59 | override fun onError(msg: String) {
60 | Toast.makeText(view!!.getContext(), msg, Toast.LENGTH_LONG).show()
61 | }
62 |
63 | override fun onError(errorType: ErrorType) {
64 | when (errorType) {
65 | ErrorType.UNKNOWN -> {
66 | onError(view!!.getContext().getString(R.string.error_unknown))
67 | }
68 | ErrorType.TIMEOUT -> {
69 | onError(view!!.getContext().getString(R.string.error_timeout))
70 | }
71 | ErrorType.NETWORK -> {
72 | onError(view!!.getContext().getString(R.string.network_error))
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.di
2 |
3 | import android.content.Context
4 | import com.facebook.stetho.okhttp3.StethoInterceptor
5 | import com.google.gson.GsonBuilder
6 | import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.components.ApplicationComponent
11 | import dagger.hilt.android.qualifiers.ApplicationContext
12 | import okhttp3.OkHttpClient
13 | import okhttp3.logging.HttpLoggingInterceptor
14 | import retrofit2.Retrofit
15 | import retrofit2.converter.gson.GsonConverterFactory
16 | import xyz.derekcsm.transformers.base.SharedPref
17 | import xyz.derekcsm.transformers.network.ApiAuthenticator
18 | import xyz.derekcsm.transformers.network.ApiService
19 | import xyz.derekcsm.transformers.network.RequestInterceptor
20 | import javax.inject.Singleton
21 |
22 |
23 | @Module
24 | @InstallIn(ApplicationComponent::class)
25 | object NetworkModule {
26 |
27 | @Provides
28 | @Singleton
29 | fun provideSharedPref(@ApplicationContext context: Context): SharedPref {
30 | return SharedPref(context)
31 | }
32 |
33 | @Provides
34 | @Singleton
35 | fun provideOkHttpClient(
36 | @ApplicationContext context: Context,
37 | sharedPref: SharedPref
38 | ): OkHttpClient {
39 | val logging = HttpLoggingInterceptor()
40 | logging.setLevel(HttpLoggingInterceptor.Level.BODY) // TODO for dev purposes only
41 |
42 | return OkHttpClient.Builder()
43 | .addInterceptor(logging)
44 | .addNetworkInterceptor(StethoInterceptor())
45 | .addInterceptor(RequestInterceptor(sharedPref))
46 | .authenticator(ApiAuthenticator(context, sharedPref))
47 | .build()
48 | }
49 |
50 | @Provides
51 | @Singleton
52 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
53 | val gson = GsonBuilder()
54 | .setLenient()
55 | .create()
56 | return Retrofit.Builder()
57 | .client(okHttpClient)
58 | .baseUrl("https://transformers-api.firebaseapp.com/")
59 | .addCallAdapterFactory(CoroutineCallAdapterFactory())
60 | .addConverterFactory(GsonConverterFactory.create(gson))
61 | .build()
62 | }
63 |
64 | @Provides
65 | @Singleton
66 | fun provideApiService(retrofit: Retrofit): ApiService {
67 | return retrofit.create(ApiService::class.java)
68 | }
69 |
70 | /*
71 | For one time use only in ApiAuthenticator
72 | */
73 | fun buildAuthenticatorApiServiceInstance(context: Context, sharedPref: SharedPref): ApiService {
74 | return provideApiService(
75 | provideRetrofit(
76 | provideOkHttpClient(context, sharedPref)
77 | )
78 | )
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/transformers_list/adapter/TransformerViewHolder.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.transformers_list.adapter
2 |
3 | import android.view.View
4 | import android.widget.Toast
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.facebook.drawee.view.SimpleDraweeView
7 | import kotlinx.android.synthetic.main.item_transformer.view.*
8 | import kotlinx.android.synthetic.main.layout_item_stats.view.*
9 | import xyz.derekcsm.transformers.R
10 | import xyz.derekcsm.transformers.model.Transformer
11 | import xyz.derekcsm.transformers.utils.ConnectivityUtils
12 |
13 | class TransformerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
14 |
15 | val context = itemView.context
16 | lateinit var sdvTeamIcon: SimpleDraweeView
17 |
18 | fun bindView(listener: TransListAdapterListener, transformer: Transformer) {
19 | itemView.card_transformer.setOnClickListener {
20 | listener.onTransformerClicked(transformer)
21 | }
22 | itemView.tv_delete.setOnClickListener {
23 | if (ConnectivityUtils().isInternetAvailable(context)) {
24 | listener.onDeleteTransformerClicked(transformer)
25 | } else {
26 | Toast.makeText(context, R.string.no_internet_connection, Toast.LENGTH_SHORT).show()
27 | }
28 | }
29 |
30 | sdvTeamIcon = itemView.findViewById(R.id.sdv_team_icon)
31 | sdvTeamIcon.setImageURI(transformer.teamIcon)
32 |
33 | itemView.tv_name.text = transformer.name
34 | itemView.tv_courage.text = context.getString(
35 | R.string.stat,
36 | context.getString(R.string.courage),
37 | transformer.courage.toString()
38 | )
39 | itemView.tv_endurance.text = context.getString(
40 | R.string.stat,
41 | context.getString(R.string.endurance),
42 | transformer.endurance.toString()
43 | )
44 | itemView.tv_firepower.text = context.getString(
45 | R.string.stat,
46 | context.getString(R.string.firepower),
47 | transformer.firepower.toString()
48 | )
49 | itemView.tv_intelligence.text = context.getString(
50 | R.string.stat,
51 | context.getString(R.string.intelligence),
52 | transformer.intelligence.toString()
53 | )
54 | itemView.tv_rank.text = context.getString(
55 | R.string.stat,
56 | context.getString(R.string.rank),
57 | transformer.rank.toString()
58 | )
59 | itemView.tv_skill.text = context.getString(
60 | R.string.stat,
61 | context.getString(R.string.skill),
62 | transformer.skill.toString()
63 | )
64 | itemView.tv_speed.text = context.getString(
65 | R.string.stat,
66 | context.getString(R.string.speed),
67 | transformer.speed.toString()
68 | )
69 | itemView.tv_strength.text = context.getString(
70 | R.string.stat,
71 | context.getString(R.string.strength),
72 | transformer.strength.toString()
73 | )
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_item_stats.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
21 |
22 |
29 |
30 |
37 |
38 |
45 |
46 |
47 |
52 |
53 |
60 |
61 |
68 |
69 |
76 |
77 |
84 |
85 |
--------------------------------------------------------------------------------
/.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/java/xyz/derekcsm/transformers/ui/transformers_list/TransListActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.transformers_list
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.lifecycle.Lifecycle
9 | import androidx.lifecycle.LifecycleObserver
10 | import androidx.lifecycle.OnLifecycleEvent
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
13 | import dagger.hilt.android.AndroidEntryPoint
14 | import kotlinx.android.synthetic.main.activity_trans_list.*
15 | import kotlinx.android.synthetic.main.zero_state.*
16 | import xyz.derekcsm.transformers.R
17 | import xyz.derekcsm.transformers.model.Transformer
18 | import xyz.derekcsm.transformers.ui.create_transformer.CreateTransformerActivity
19 | import xyz.derekcsm.transformers.ui.transformers_list.adapter.TransListAdapter
20 | import xyz.derekcsm.transformers.ui.transformers_list.adapter.TransListAdapterListener
21 |
22 | @AndroidEntryPoint
23 | class TransListActivity : AppCompatActivity(),
24 | TransListView, TransListAdapterListener, LifecycleObserver {
25 |
26 | private val TAG = "TransListActivity"
27 | val viewModel by viewModels()
28 | private val transListAdapter = TransListAdapter(this)
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | viewModel.connectViewInterface(this)
33 | setContentView(R.layout.activity_trans_list)
34 | lifecycle.addObserver(this)
35 |
36 | rv_transformers.apply {
37 | layoutManager = LinearLayoutManager(context)
38 | adapter = transListAdapter
39 | }
40 |
41 | swipe_refresh_layout.setOnRefreshListener {
42 | showLoading()
43 | reload()
44 | }
45 |
46 | fab.setOnClickListener {
47 | startActivity(CreateTransformerActivity.activityIntent(this, null))
48 | }
49 |
50 | transListAdapter.registerAdapterDataObserver(object : AdapterDataObserver() {
51 | override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
52 | super.onItemRangeRemoved(positionStart, itemCount)
53 | checkShowZeroState()
54 | }
55 |
56 | override fun onChanged() {
57 | super.onChanged()
58 | checkShowZeroState()
59 | }
60 | })
61 | }
62 |
63 | @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
64 | fun reload() {
65 | viewModel.fetchTransformers()
66 | }
67 |
68 | override fun populateList(transformersList: List) {
69 | transListAdapter.setItems(transformersList)
70 | }
71 |
72 | override fun onTransformerClicked(transformer: Transformer) {
73 | startActivity(CreateTransformerActivity.activityIntent(this, transformer.id))
74 | }
75 |
76 | override fun onDeleteTransformerClicked(transformer: Transformer) {
77 | transListAdapter.removeTransformer(transformer.id)
78 | viewModel.deleteTransformer(transformer.id)
79 | }
80 |
81 | override fun getItemCount(): Int {
82 | return transListAdapter.itemCount
83 | }
84 |
85 | override fun showLoading() {
86 | swipe_refresh_layout.isRefreshing = true
87 | }
88 |
89 | override fun hideLoading() {
90 | swipe_refresh_layout.isRefreshing = false
91 | }
92 |
93 |
94 | fun checkShowZeroState() {
95 | if (transListAdapter.itemCount > 0) {
96 | hideZeroState()
97 | } else {
98 | showZeroState()
99 | }
100 | }
101 |
102 | fun hideZeroState() {
103 | zero_state.visibility = View.GONE
104 | }
105 |
106 | fun showZeroState() {
107 | zero_state.visibility = View.VISIBLE
108 | tv_zero_state.text = getString(R.string.transformers_zero_state)
109 | }
110 |
111 | override fun getContext(): Context {
112 | return this
113 | }
114 | }
--------------------------------------------------------------------------------
/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: "de.mannodermaus.android-junit5"
6 | apply plugin: 'dagger.hilt.android.plugin'
7 |
8 | android {
9 | compileSdkVersion 29
10 |
11 | defaultConfig {
12 | applicationId "xyz.derekcsm.transformers"
13 | minSdkVersion 21
14 | targetSdkVersion 29
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = JavaVersion.VERSION_1_8.toString()
32 | }
33 | buildFeatures {
34 | dataBinding = true
35 | }
36 | }
37 |
38 | dependencies {
39 | def lifecycle_version = "2.2.0"
40 |
41 | implementation fileTree(dir: 'libs', include: ['*.jar'])
42 |
43 | // Kotlin
44 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
45 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
46 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
47 |
48 | // AndroidX
49 | implementation 'androidx.core:core-ktx:1.3.0'
50 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
51 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
52 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
53 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
54 | implementation "androidx.fragment:fragment-ktx:1.2.5"
55 | implementation 'androidx.appcompat:appcompat:1.1.0'
56 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
57 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
58 | implementation "androidx.cardview:cardview:1.0.0"
59 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
60 | implementation "androidx.recyclerview:recyclerview:1.1.0"
61 |
62 | // images
63 | implementation 'com.facebook.fresco:fresco:2.2.0'
64 |
65 | //base level UI (AKA breaking the rules)
66 | implementation 'com.google.android.material:material:1.1.0'
67 | implementation "com.github.duanhong169:drawabletoolbox:1.0.7"
68 | implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
69 |
70 | // Dagger Hilt
71 | implementation "com.google.dagger:hilt-android:$hilt_version"
72 | androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
73 | kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
74 | kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
75 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01"
76 | implementation "androidx.hilt:hilt-common:1.0.0-alpha01"
77 | kapt "androidx.hilt:hilt-compiler:1.0.0-alpha01"
78 | implementation "com.google.dagger:dagger:2.28"
79 |
80 | // network & JSON
81 | implementation 'com.google.code.gson:gson:2.8.6'
82 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
83 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
84 | implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
85 | implementation("com.squareup.okhttp3:okhttp:4.7.2")
86 | implementation("com.squareup.okhttp3:logging-interceptor:4.7.2")
87 |
88 | // debugging
89 | implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
90 | implementation 'com.facebook.stetho:stetho-okhttp:1.5.1'
91 |
92 | // persistence
93 | implementation "androidx.room:room-runtime:2.2.5"
94 | implementation 'androidx.room:room-ktx:2.2.5'
95 | kapt "androidx.room:room-compiler:2.2.5"
96 | androidTestImplementation "androidx.room:room-testing:2.2.5"
97 |
98 | testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.0"
99 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.6.0"
100 | testImplementation "org.junit.jupiter:junit-jupiter-params:5.6.0"
101 | testImplementation("org.assertj:assertj-core:3.15.0")
102 |
103 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
104 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
105 | androidTestImplementation 'androidx.test:rules:1.2.0'
106 | androidTestImplementation 'androidx.test:runner:1.2.0'
107 | androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
108 | androidTestImplementation "it.xabaras.android.espresso:recyclerview-child-actions:1.0"
109 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.idea/misc.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 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/derekcsm/transformers/ui/create_transformer/CreateTransformerActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.derekcsm.transformers.ui.create_transformer
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.graphics.drawable.Drawable
6 | import android.os.Bundle
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.view.Window
10 | import android.view.WindowManager
11 | import android.widget.Toast
12 | import androidx.activity.viewModels
13 | import androidx.annotation.VisibleForTesting
14 | import androidx.appcompat.app.AppCompatActivity
15 | import androidx.core.content.ContextCompat
16 | import androidx.core.view.forEach
17 | import dagger.hilt.android.AndroidEntryPoint
18 | import kotlinx.android.synthetic.main.activity_create_transformer.*
19 | import kotlinx.android.synthetic.main.toolbar_button.*
20 | import top.defaults.drawabletoolbox.DrawableBuilder
21 | import xyz.derekcsm.transformers.R
22 | import xyz.derekcsm.transformers.base.Constants
23 | import xyz.derekcsm.transformers.model.Transformer
24 |
25 | @AndroidEntryPoint
26 | class CreateTransformerActivity : AppCompatActivity(), CreateTransformerView {
27 |
28 | companion object {
29 | val EXTRA_TRANSFORMER_ID = "transformer_id"
30 |
31 | fun activityIntent(
32 | context: Context,
33 | transformerId: String?
34 | ): Intent? {
35 | val intent = Intent(context, CreateTransformerActivity::class.java)
36 | if (transformerId != null) {
37 | intent.putExtra(EXTRA_TRANSFORMER_ID, transformerId)
38 | }
39 | return intent
40 | }
41 | }
42 |
43 | private val TAG = "CreateTransformerActivity"
44 | private var transformerId: String = ""
45 |
46 | @VisibleForTesting
47 | val viewModel by viewModels()
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | viewModel.connectViewInterface(this)
52 | setContentView(R.layout.activity_create_transformer)
53 |
54 | if (intent.extras != null && intent.extras!!.containsKey(EXTRA_TRANSFORMER_ID)) {
55 | transformerId = intent!!.extras!!.get(EXTRA_TRANSFORMER_ID).toString()
56 | }
57 |
58 | if (transformerId != "") {
59 | /*
60 | Populate fields from existing Transformer for updating
61 | */
62 | val transformerToEdit = viewModel.getTransformerFromDB(transformerId)
63 | selectTeam(transformerToEdit.team)
64 | et_transformer_name.setText(transformerToEdit.name)
65 | sb_strength.setProgress(transformerToEdit.strength - 1)
66 | sb_intelligence.setProgress(transformerToEdit.intelligence - 1)
67 | sb_speed.setProgress(transformerToEdit.speed - 1)
68 | sb_endurance.setProgress(transformerToEdit.endurance - 1)
69 | sb_rank.setProgress(transformerToEdit.rank - 1)
70 | sb_courage.setProgress(transformerToEdit.courage - 1)
71 | sb_firepower.setProgress(transformerToEdit.firepower - 1)
72 | sb_skill.setProgress(transformerToEdit.skill - 1)
73 | }
74 |
75 | iv_close.setOnClickListener {
76 | super.onBackPressed()
77 | }
78 | toolbar.elevation = 0f
79 | btn_toolbar.text = getString(R.string.save)
80 | btn_toolbar.setOnClickListener {
81 | formatTransformerFromInputsAndCreate()
82 | }
83 |
84 | setupTeamListener()
85 | }
86 |
87 | private var selectedTeam: String = Constants.TEAM_AUTOBOTS
88 | private fun setupTeamListener() {
89 |
90 | // default for creation
91 | if (transformerId == "") {
92 | selectTeam(Constants.TEAM_AUTOBOTS)
93 | }
94 |
95 | tv_team_autobots.setOnClickListener {
96 | selectTeam(Constants.TEAM_AUTOBOTS)
97 | }
98 |
99 | tv_team_decepticons.setOnClickListener {
100 | selectTeam(Constants.TEAM_DECEPTICONS)
101 | }
102 | }
103 |
104 | private fun selectTeam(team: String) {
105 | selectedTeam = team
106 | var color: Int = 0
107 | when (selectedTeam) {
108 | Constants.TEAM_DECEPTICONS -> {
109 | color = ContextCompat.getColor(this, R.color.decepticon)
110 | toolbar.setBackgroundColor(color)
111 | ll_name.setBackgroundColor(color)
112 | updateStatusbarColor(color)
113 | tv_team_decepticons.setBackground(getTeamBackgroundDrawable())
114 | tv_team_autobots.setBackgroundColor(
115 | ContextCompat.getColor(
116 | this,
117 | android.R.color.transparent
118 | )
119 | )
120 | }
121 | Constants.TEAM_AUTOBOTS -> {
122 | color = ContextCompat.getColor(this, R.color.autobot)
123 | toolbar.setBackgroundColor(color)
124 | ll_name.setBackgroundColor(color)
125 | updateStatusbarColor(color)
126 | tv_team_autobots.setBackground(getTeamBackgroundDrawable())
127 | tv_team_decepticons.setBackgroundColor(
128 | ContextCompat.getColor(
129 | this,
130 | android.R.color.transparent
131 | )
132 | )
133 | }
134 | }
135 | }
136 |
137 | private fun getTeamBackgroundDrawable(): Drawable {
138 | var solidColor: Int = 0
139 | when (selectedTeam) {
140 | Constants.TEAM_DECEPTICONS -> {
141 | solidColor = ContextCompat.getColor(this, R.color.decepticon)
142 | }
143 | Constants.TEAM_AUTOBOTS -> {
144 | solidColor = ContextCompat.getColor(this, R.color.autobot)
145 | }
146 | }
147 |
148 | return DrawableBuilder()
149 | .rectangle()
150 | .rounded()
151 | .solidColor(solidColor)
152 | .ripple()
153 | .build()
154 | }
155 |
156 | private fun updateStatusbarColor(color: Int) {
157 | val window: Window = getWindow()
158 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
159 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
160 | window.setStatusBarColor(color)
161 | }
162 |
163 | private fun formatTransformerFromInputsAndCreate() {
164 |
165 | if (et_transformer_name.text.toString().isEmpty()) {
166 | Toast.makeText(this, R.string.error_must_have_name, Toast.LENGTH_LONG).show()
167 | return
168 | }
169 |
170 | val transformer = Transformer(
171 | transformerId,
172 | et_transformer_name.text.toString(),
173 | selectedTeam,
174 | sb_strength.progress + 1,
175 | sb_intelligence.progress + 1,
176 | sb_speed.progress + 1,
177 | sb_endurance.progress + 1,
178 | sb_rank.progress + 1,
179 | sb_courage.progress + 1,
180 | sb_firepower.progress + 1,
181 | sb_skill.progress + 1,
182 | null
183 | )
184 |
185 | if (transformerId != "") {
186 | viewModel.updateTransformer(transformer)
187 | } else {
188 | viewModel.createTransformer(transformer)
189 | }
190 | }
191 |
192 | override fun onRequestCompleted() {
193 | super.onBackPressed()
194 | }
195 |
196 | override fun showLoading() {
197 | btn_toolbar.isEnabled = false
198 | scrollview.deepForEach { isEnabled = false }
199 | progress_horizontal.visibility = View.VISIBLE
200 | }
201 |
202 | override fun hideLoading() {
203 | btn_toolbar.isEnabled = true
204 | scrollview.deepForEach { isEnabled = true }
205 | progress_horizontal.visibility = View.GONE
206 | }
207 |
208 | fun ViewGroup.deepForEach(function: View.() -> Unit) {
209 | this.forEach { child ->
210 | child.function()
211 | if (child is ViewGroup) {
212 | child.deepForEach(function)
213 | }
214 | }
215 | }
216 |
217 | override fun getContext(): Context {
218 | return this
219 | }
220 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_create_transformer.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
16 |
17 |
21 |
22 |
29 |
30 |
40 |
41 |
42 |
43 |
48 |
49 |
57 |
58 |
61 |
62 |
70 |
71 |
72 |
73 |
78 |
79 |
85 |
86 |
91 |
92 |
97 |
98 |
103 |
104 |
109 |
110 |
115 |
116 |
121 |
122 |
127 |
128 |
133 |
134 |
139 |
140 |
145 |
146 |
151 |
152 |
157 |
158 |
163 |
164 |
169 |
170 |
175 |
176 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------