├── 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 | 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 | 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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 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 |