From 159a2cc9825d08ee1ceba68eed2b8ef1f1cffa7b Mon Sep 17 00:00:00 2001 From: Prince Ikede Date: Sun, 26 Jul 2020 17:09:15 -0400 Subject: [PATCH 1/3] Luminary Code Challenge --- .gitignore | 12 ++ .idea/.gitignore | 2 + .idea/codeStyles/Project.xml | 116 ++++++++++++++++++ .idea/gradle.xml | 19 +++ .idea/jarRepositories.xml | 45 +++++++ .idea/misc.xml | 4 + .idea/vcs.xml | 6 + app/build.gradle | 38 +++++- app/src/main/AndroidManifest.xml | 2 +- .../java/cvdevelopers/takehome/Adapter.kt | 53 ++++++++ .../takehome/LuminaryTakeHomeApplication.kt | 22 ---- .../cvdevelopers/takehome/MainActivity.kt | 7 +- .../cvdevelopers/takehome/UsersFragment.kt | 84 +++++++++++++ .../takehome/api/RandomUserApiEndpoint.kt | 5 +- .../takehome/api/RecipeRetrofit.kt | 14 +++ .../takehome/dagger/ApplicationComponent.kt | 16 --- .../takehome/dagger/ApplicationModule.kt | 16 --- .../takehome/dagger/NetworkClientModule.kt | 31 ----- .../takehome/database/ClientDatabase.kt | 30 +++++ .../takehome/database/ClientDictionaryDao.kt | 18 +++ .../takehome/database/DatabaseEntity.kt | 11 ++ .../takehome/diKoin/RecipeApplication.kt | 15 +++ .../takehome/diKoin/RecipesModule.kt | 16 +++ .../takehome/models/ApiResponse.kt | 7 +- .../cvdevelopers/takehome/models/Client.kt | 8 +- .../java/cvdevelopers/takehome/models/Id.kt | 6 +- .../java/cvdevelopers/takehome/models/Name.kt | 6 +- .../cvdevelopers/takehome/models/Picture.kt | 6 +- .../takehome/repository/Repository.kt | 64 ++++++++++ .../cvdevelopers/takehome/utils/GsonUtil.kt | 13 ++ .../takehome/utils/ItemDiffCallback.kt | 21 ++++ .../takehome/viewmodel/UsersViewModel.kt | 28 +++++ app/src/main/res/layout/activity_main.xml | 3 +- app/src/main/res/layout/fragment_users.xml | 32 +++++ app/src/main/res/layout/users.xml | 32 +++++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/styles.xml | 3 + build.gradle | 10 ++ gradle.properties | 1 + local.properties | 4 +- 40 files changed, 714 insertions(+), 117 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 app/src/main/java/cvdevelopers/takehome/Adapter.kt delete mode 100644 app/src/main/java/cvdevelopers/takehome/LuminaryTakeHomeApplication.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/UsersFragment.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/api/RecipeRetrofit.kt delete mode 100644 app/src/main/java/cvdevelopers/takehome/dagger/ApplicationComponent.kt delete mode 100644 app/src/main/java/cvdevelopers/takehome/dagger/ApplicationModule.kt delete mode 100644 app/src/main/java/cvdevelopers/takehome/dagger/NetworkClientModule.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/database/ClientDatabase.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/database/ClientDictionaryDao.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/database/DatabaseEntity.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/diKoin/RecipeApplication.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/diKoin/RecipesModule.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/repository/Repository.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/utils/GsonUtil.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/utils/ItemDiffCallback.kt create mode 100644 app/src/main/java/cvdevelopers/takehome/viewmodel/UsersViewModel.kt create mode 100644 app/src/main/res/layout/fragment_users.xml create mode 100644 app/src/main/res/layout/users.xml diff --git a/.gitignore b/.gitignore index 9955973..d53cdb2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,15 @@ /.gradle/4.4/fileChanges/last-build.bin /.gradle/4.10.1/fileChanges/last-build.bin /.gradle/buildOutputCleanup/outputFiles.bin +*.iml +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e7e9d11 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..440480e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..0a801ef --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f5c6d9e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7593dde..9dde2ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 @@ -13,6 +14,12 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + dataBinding { + enabled = true + } + lintOptions { + disable 'GoogleAppIndexingWarning' + } buildTypes { release { minifyEnabled false @@ -38,6 +45,9 @@ repositories { dependencies { api fileTree(dir: 'libs', include: ['*.jar']) api "com.jakewharton:butterknife:$butterknife_version" + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' kapt "com.jakewharton:butterknife-compiler:$butterknife_version" api "com.google.dagger:dagger-android-support:$daggerVersion" @@ -51,15 +61,33 @@ dependencies { exclude group: 'com.google.code.findbugs' }) + testImplementation 'junit:junit:4.12' api "androidx.appcompat:appcompat:$supportLibraryVersion" api "androidx.recyclerview:recyclerview:$supportLibraryVersion" -// api "androidx.legacy:legacy-support-v4:$supportLibraryVersion" - api 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' api 'com.squareup.retrofit2:retrofit:2.6.0' api 'com.squareup.retrofit2:adapter-rxjava2:2.6.0' api 'com.squareup.retrofit2:converter-gson:2.3.0' - api 'com.squareup.picasso:picasso:2.5.2' - api 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'com.squareup.picasso:picasso:2.71828' - testImplementation 'junit:junit:4.12' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' + implementation 'io.reactivex.rxjava2:rxjava:2.2.2' + + // UI + implementation "com.google.android.material:material:$rootProject.materialVersion" + + // ViewModel Kotlin support + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion" + + // Room components + implementation "androidx.room:room-runtime:$rootProject.roomVersion" + implementation "androidx.room:room-ktx:$rootProject.roomVersion" + kapt "androidx.room:room-compiler:$rootProject.roomVersion" + androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion" + + // Koin + api "org.koin:koin-android:$koin_version" + api "org.koin:koin-androidx-scope:$koin_version" + api "org.koin:koin-androidx-viewmodel:$koin_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b687906..ee7d022 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,12 +5,12 @@ diff --git a/app/src/main/java/cvdevelopers/takehome/Adapter.kt b/app/src/main/java/cvdevelopers/takehome/Adapter.kt new file mode 100644 index 0000000..0552ac1 --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/Adapter.kt @@ -0,0 +1,53 @@ +package cvdevelopers.takehome + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.squareup.picasso.Picasso +import cvdevelopers.githubstalker.R +import cvdevelopers.takehome.models.Client +import cvdevelopers.takehome.utils.ItemDiffCallback +import cvdevelopers.takehome.utils.image.CircleTransformation +import kotlinx.android.synthetic.main.users.view.* + +typealias ClickListener = (Client) -> Unit + +class Adapter(private val clickListener: ClickListener) : + RecyclerView.Adapter() { + + private var client: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate( + R.layout.users, parent, false + ) as LinearLayout + + val viewHolder = ViewHolder(view) + view.setOnClickListener { clickListener(client[viewHolder.adapterPosition]) } + return viewHolder + } + + override fun getItemCount(): Int = client.count() + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(client[position]) + } + + fun updateClient(client: List) { + val diffResult = DiffUtil.calculateDiff(ItemDiffCallback(this.client, client)) + this.client = client + diffResult.dispatchUpdatesTo(this) + } + + class ViewHolder(view: ViewGroup) : RecyclerView.ViewHolder(view) { + fun bind(client: Client) { + val picture = client.picture.medium + + Picasso.get().load(picture).transform(CircleTransformation()).into(itemView.img) + itemView.user_first.text = client.name.first + itemView.user_last.text = client.name.last + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/LuminaryTakeHomeApplication.kt b/app/src/main/java/cvdevelopers/takehome/LuminaryTakeHomeApplication.kt deleted file mode 100644 index adce9b7..0000000 --- a/app/src/main/java/cvdevelopers/takehome/LuminaryTakeHomeApplication.kt +++ /dev/null @@ -1,22 +0,0 @@ -package cvdevelopers.takehome - -import android.app.Application -import cvdevelopers.takehome.dagger.ApplicationComponent -import cvdevelopers.takehome.dagger.ApplicationModule -import cvdevelopers.takehome.dagger.DaggerApplicationComponent - -class LuminaryTakeHomeApplication : Application() { - - val appComponent: ApplicationComponent by lazy { - DaggerApplicationComponent - .builder() - .applicationModule(ApplicationModule(this)) - .build() - } - - override fun onCreate() { - super.onCreate() - appComponent.inject(this); - } - -} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/MainActivity.kt b/app/src/main/java/cvdevelopers/takehome/MainActivity.kt index e1c3bb4..a47aaba 100644 --- a/app/src/main/java/cvdevelopers/takehome/MainActivity.kt +++ b/app/src/main/java/cvdevelopers/takehome/MainActivity.kt @@ -9,8 +9,13 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - (application as LuminaryTakeHomeApplication).appComponent.inject(this) + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.activity_fragment_container, UsersFragment.newInstance()) + .addToBackStack(null) + .commit() + } } } diff --git a/app/src/main/java/cvdevelopers/takehome/UsersFragment.kt b/app/src/main/java/cvdevelopers/takehome/UsersFragment.kt new file mode 100644 index 0000000..5bca8d8 --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/UsersFragment.kt @@ -0,0 +1,84 @@ +package cvdevelopers.takehome + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import cvdevelopers.githubstalker.R +import cvdevelopers.takehome.models.Client +import cvdevelopers.takehome.viewmodel.UsersViewModel +import kotlinx.android.synthetic.main.fragment_users.* +import org.koin.androidx.viewmodel.ext.android.viewModel + +class UsersFragment : Fragment() { + + private val clickListener: ClickListener = this::onClientClicked + private val viewModel: UsersViewModel by viewModel() + private var mAdapter = Adapter(clickListener) + private var dataSet = emptyList() + + companion object { + fun newInstance() = UsersFragment() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_users, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setUpSwipeToRefresh() + setupRecyclerView() + + getUsers() + + viewModel.usersList.observe(viewLifecycleOwner, Observer { + Log.d("observing::", "observing") + renderClients(it) + }) + } + + private fun setUpSwipeToRefresh() { + simpleSwipeRefreshLayout.setOnRefreshListener { + Toast.makeText(requireContext(), "Swipe detected Fetching users", Toast.LENGTH_SHORT).show() + clearDatabase() + renderClients(emptyList()) + getUsers() + simpleSwipeRefreshLayout.isRefreshing = false + } + } + + private fun getUsers() { + viewModel.getUsers() + } + + private fun clearDatabase() { + viewModel.clearCache() + } + + private fun renderClients(clients: List) { + Log.d("rendering clients::", "display client users") + loadingIndicator.visibility = View.GONE + dataSet = clients + mAdapter.updateClient(clients) + recyclerview.visibility = View.VISIBLE + } + + private fun setupRecyclerView() { + recyclerview.layoutManager = LinearLayoutManager(requireContext()) + recyclerview.adapter = mAdapter + recyclerview.setHasFixedSize(true) + } + + private fun onClientClicked(client: Client) { + view?.let { Snackbar.make(it, client.name.first, Snackbar.LENGTH_LONG).show() } + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/api/RandomUserApiEndpoint.kt b/app/src/main/java/cvdevelopers/takehome/api/RandomUserApiEndpoint.kt index 3e4eb8a..7d07e64 100644 --- a/app/src/main/java/cvdevelopers/takehome/api/RandomUserApiEndpoint.kt +++ b/app/src/main/java/cvdevelopers/takehome/api/RandomUserApiEndpoint.kt @@ -1,16 +1,15 @@ package cvdevelopers.takehome.api import cvdevelopers.takehome.models.ApiResponse -import io.reactivex.Single import retrofit2.http.GET import retrofit2.http.Query interface RandomUserApiEndpoint { @GET("/api/") - fun getClient(@Query("page") page: String, @Query("results") results: String = "15"): Single + suspend fun getClient(@Query("page") page: String, @Query("results") results: String = "15"): ApiResponse companion object { - val SERVER = "https://randomuser.me" + const val SERVER = "https://randomuser.me" } } \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/api/RecipeRetrofit.kt b/app/src/main/java/cvdevelopers/takehome/api/RecipeRetrofit.kt new file mode 100644 index 0000000..057c08e --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/api/RecipeRetrofit.kt @@ -0,0 +1,14 @@ +package cvdevelopers.takehome.api + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class RecipeRetrofit { + + companion object { + var userApiEndpoint: RandomUserApiEndpoint = Retrofit.Builder() + .baseUrl(RandomUserApiEndpoint.SERVER) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(RandomUserApiEndpoint::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/dagger/ApplicationComponent.kt b/app/src/main/java/cvdevelopers/takehome/dagger/ApplicationComponent.kt deleted file mode 100644 index 7f4144d..0000000 --- a/app/src/main/java/cvdevelopers/takehome/dagger/ApplicationComponent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cvdevelopers.takehome.dagger - -import cvdevelopers.takehome.MainActivity -import cvdevelopers.takehome.LuminaryTakeHomeApplication -import dagger.Component -import javax.inject.Singleton - -/** - * Created by CamiloVega on 10/7/17. - */ -@Singleton -@Component(modules = arrayOf(ApplicationModule::class, NetworkClientModule::class)) -interface ApplicationComponent { - fun inject(app: LuminaryTakeHomeApplication) - fun inject(target: MainActivity) -} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/dagger/ApplicationModule.kt b/app/src/main/java/cvdevelopers/takehome/dagger/ApplicationModule.kt deleted file mode 100644 index 337d977..0000000 --- a/app/src/main/java/cvdevelopers/takehome/dagger/ApplicationModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cvdevelopers.takehome.dagger - -import android.app.Application -import cvdevelopers.takehome.LuminaryTakeHomeApplication -import dagger.Module -import dagger.Provides -import javax.inject.Singleton - -@Module -class ApplicationModule(private val app: LuminaryTakeHomeApplication) { - - @Provides - @Singleton - fun provideApplication(): Application = app - -} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/dagger/NetworkClientModule.kt b/app/src/main/java/cvdevelopers/takehome/dagger/NetworkClientModule.kt deleted file mode 100644 index 47c5ade..0000000 --- a/app/src/main/java/cvdevelopers/takehome/dagger/NetworkClientModule.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cvdevelopers.takehome.dagger - -import com.google.gson.FieldNamingPolicy -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import cvdevelopers.takehome.api.RandomUserApiEndpoint -import dagger.Module -import dagger.Provides -import io.reactivex.schedulers.Schedulers -import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Singleton - -@Module -class NetworkClientModule { - - @Provides - @Singleton - fun provideGson() = GsonBuilder() - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .create() - - @Provides - @Singleton - fun provideRandomUserEndpoint(gson: Gson): RandomUserApiEndpoint = Retrofit.Builder() - .baseUrl(RandomUserApiEndpoint.SERVER) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build().create(RandomUserApiEndpoint::class.java) -} diff --git a/app/src/main/java/cvdevelopers/takehome/database/ClientDatabase.kt b/app/src/main/java/cvdevelopers/takehome/database/ClientDatabase.kt new file mode 100644 index 0000000..1146174 --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/database/ClientDatabase.kt @@ -0,0 +1,30 @@ +package cvdevelopers.takehome.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [DatabaseEntity::class], version = 1, exportSchema = false) +abstract class ClientDatabase : RoomDatabase() { + + abstract fun clientDao(): ClientDictionaryDao + + companion object { + private const val databaseName = "client" + + @Volatile + private var INSTANCE: ClientDatabase? = null + + fun getDataBase(context: Context): ClientDatabase = + INSTANCE ?: synchronized(this) { + INSTANCE ?: buildDatabase(context).also { INSTANCE = it } + } + + private fun buildDatabase(context: Context) = Room.databaseBuilder( + context.applicationContext, + ClientDatabase::class.java, + databaseName + ).build() + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/database/ClientDictionaryDao.kt b/app/src/main/java/cvdevelopers/takehome/database/ClientDictionaryDao.kt new file mode 100644 index 0000000..edeb210 --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/database/ClientDictionaryDao.kt @@ -0,0 +1,18 @@ +package cvdevelopers.takehome.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface ClientDictionaryDao { + @Query("SELECT * FROM DatabaseEntity") + suspend fun getClientsByName(): List + + @Query("DELETE FROM DatabaseEntity") + suspend fun clearDatabase() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertWordDefinition(vararg databaseEntity: DatabaseEntity) +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/database/DatabaseEntity.kt b/app/src/main/java/cvdevelopers/takehome/database/DatabaseEntity.kt new file mode 100644 index 0000000..ed53f3b --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/database/DatabaseEntity.kt @@ -0,0 +1,11 @@ +package cvdevelopers.takehome.database + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class DatabaseEntity( + @PrimaryKey val lastName: String, + @ColumnInfo(name = "json") val json: String +) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/diKoin/RecipeApplication.kt b/app/src/main/java/cvdevelopers/takehome/diKoin/RecipeApplication.kt new file mode 100644 index 0000000..5215009 --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/diKoin/RecipeApplication.kt @@ -0,0 +1,15 @@ +package cvdevelopers.takehome.diKoin + +import android.app.Application +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class RecipeApplication : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@RecipeApplication) + modules(ClientModule) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/diKoin/RecipesModule.kt b/app/src/main/java/cvdevelopers/takehome/diKoin/RecipesModule.kt new file mode 100644 index 0000000..ec7257a --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/diKoin/RecipesModule.kt @@ -0,0 +1,16 @@ +package cvdevelopers.takehome.diKoin + +import cvdevelopers.takehome.database.ClientDatabase +import cvdevelopers.takehome.repository.Repository +import cvdevelopers.takehome.api.RecipeRetrofit +import cvdevelopers.takehome.viewmodel.UsersViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val ClientModule = module { + single { ClientDatabase.getDataBase(androidContext()) } + single { RecipeRetrofit() } + single { Repository(get()) } + viewModel { UsersViewModel(get()) } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/models/ApiResponse.kt b/app/src/main/java/cvdevelopers/takehome/models/ApiResponse.kt index 60edba6..a91ec49 100644 --- a/app/src/main/java/cvdevelopers/takehome/models/ApiResponse.kt +++ b/app/src/main/java/cvdevelopers/takehome/models/ApiResponse.kt @@ -1,6 +1,5 @@ package cvdevelopers.takehome.models -data class ApiResponse ( - val results: List -){ -} \ No newline at end of file +data class ApiResponse( + val results: List +) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/models/Client.kt b/app/src/main/java/cvdevelopers/takehome/models/Client.kt index ae3d816..e360d3e 100644 --- a/app/src/main/java/cvdevelopers/takehome/models/Client.kt +++ b/app/src/main/java/cvdevelopers/takehome/models/Client.kt @@ -1,8 +1,8 @@ package cvdevelopers.takehome.models data class Client( - val email: String, - val id: Id, - val name: Name, - val picture: Picture + val email: String, + val id: Id, + val name: Name, + val picture: Picture ) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/models/Id.kt b/app/src/main/java/cvdevelopers/takehome/models/Id.kt index 0e9e153..70b82c4 100644 --- a/app/src/main/java/cvdevelopers/takehome/models/Id.kt +++ b/app/src/main/java/cvdevelopers/takehome/models/Id.kt @@ -1,9 +1,7 @@ package cvdevelopers.takehome.models -import com.google.gson.annotations.SerializedName - data class Id( - val name: String, - val value: String + val name: String, + val value: String ) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/models/Name.kt b/app/src/main/java/cvdevelopers/takehome/models/Name.kt index 064fdcc..a688292 100644 --- a/app/src/main/java/cvdevelopers/takehome/models/Name.kt +++ b/app/src/main/java/cvdevelopers/takehome/models/Name.kt @@ -1,7 +1,7 @@ package cvdevelopers.takehome.models data class Name( - val first: String, - val last: String, - val title: String + val first: String, + val last: String, + val title: String ) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/models/Picture.kt b/app/src/main/java/cvdevelopers/takehome/models/Picture.kt index f3a4d6e..03e24ea 100644 --- a/app/src/main/java/cvdevelopers/takehome/models/Picture.kt +++ b/app/src/main/java/cvdevelopers/takehome/models/Picture.kt @@ -1,7 +1,7 @@ package cvdevelopers.takehome.models data class Picture( - val large: String, - val medium: String, - val thumbnail: String + val large: String, + val medium: String, + val thumbnail: String ) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/repository/Repository.kt b/app/src/main/java/cvdevelopers/takehome/repository/Repository.kt new file mode 100644 index 0000000..52615a6 --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/repository/Repository.kt @@ -0,0 +1,64 @@ +package cvdevelopers.takehome.repository + +import android.util.Log +import cvdevelopers.takehome.database.ClientDatabase +import cvdevelopers.takehome.database.DatabaseEntity +import cvdevelopers.takehome.utils.listFromJson +import cvdevelopers.takehome.utils.listToJson +import cvdevelopers.takehome.api.RecipeRetrofit +import cvdevelopers.takehome.models.Client +import java.util.* + +class Repository(private val clientDatabase: ClientDatabase) { + private var isRetrofitRequestInProgress = false + private var mDataset: List = emptyList() + + suspend fun clearDatabase() { + clientDatabase.clientDao().clearDatabase() + } + + suspend fun getSearchResults(page: String): List { + try { + Log.d("getting search::", "ma") + mDataset = getClientsFromDatabase(page) + } catch (e: Exception) { + Log.e("ERROR GETTING DATA ", e.message ?: "") + getClientsFromDatabase(page) + } + return mDataset + } + + private suspend fun getClientsFromDatabase(page: String): List { + val databaseData = clientDatabase.clientDao().getClientsByName() + return if (databaseData.isNotEmpty()) { + Log.d("get data from db::", "db is not empty") + databaseData[0].run { listFromJson(this.json) } + } else { + Log.d("db is empty::", "db is empty") + getDefinitionsFromServiceCall(page) + insertUsersIntoDatabase() + mDataset + } + } + + private suspend fun getDefinitionsFromServiceCall(page: String) { + if (isRetrofitRequestInProgress) return + else { + isRetrofitRequestInProgress = true + Log.d("fetch data from api::", "fetching data") + mDataset = RecipeRetrofit.userApiEndpoint.getClient(page).results + isRetrofitRequestInProgress = false + } + } + + private fun insertUsersIntoDatabase() { + if (mDataset.isEmpty()) return + else { + val lastName = mDataset[0].name.last.toLowerCase(Locale.getDefault()) + val jsonStringData = listToJson(mDataset) + Log.d("insert data to db::", "inserting users") + clientDatabase.clientDao() + .insertWordDefinition(DatabaseEntity(lastName, jsonStringData)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/utils/GsonUtil.kt b/app/src/main/java/cvdevelopers/takehome/utils/GsonUtil.kt new file mode 100644 index 0000000..036df8b --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/utils/GsonUtil.kt @@ -0,0 +1,13 @@ +package cvdevelopers.takehome.utils + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import cvdevelopers.takehome.models.Client +import java.lang.reflect.Type +import java.util.ArrayList + +val listType: Type = object : TypeToken>() {}.type +val gson = Gson() + +fun listToJson(list: List): String = gson.toJson(list, listType) +fun listFromJson(string: String): List = gson.fromJson(string, listType) \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/utils/ItemDiffCallback.kt b/app/src/main/java/cvdevelopers/takehome/utils/ItemDiffCallback.kt new file mode 100644 index 0000000..51894fa --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/utils/ItemDiffCallback.kt @@ -0,0 +1,21 @@ +package cvdevelopers.takehome.utils + +import androidx.recyclerview.widget.DiffUtil +import cvdevelopers.takehome.models.Client + +class ItemDiffCallback( + private val old: List, + private val new: List +) : DiffUtil.Callback() { + override fun getOldListSize() = old.size + + override fun getNewListSize() = new.size + + override fun areItemsTheSame(oldIndex: Int, newIndex: Int): Boolean { + return old[oldIndex].name == new[newIndex].name + } + + override fun areContentsTheSame(oldIndex: Int, newIndex: Int): Boolean { + return old[oldIndex] == new[newIndex] + } +} \ No newline at end of file diff --git a/app/src/main/java/cvdevelopers/takehome/viewmodel/UsersViewModel.kt b/app/src/main/java/cvdevelopers/takehome/viewmodel/UsersViewModel.kt new file mode 100644 index 0000000..b161f8f --- /dev/null +++ b/app/src/main/java/cvdevelopers/takehome/viewmodel/UsersViewModel.kt @@ -0,0 +1,28 @@ +package cvdevelopers.takehome.viewmodel + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cvdevelopers.takehome.repository.Repository +import cvdevelopers.takehome.models.Client +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class UsersViewModel( + private val repository: Repository +) : ViewModel() { + + val usersList = MutableLiveData>() + + fun getUsers(page: String = "1") = viewModelScope.launch(Dispatchers.IO) { + Log.d("get users::", "getting users") + usersList.postValue(repository.getSearchResults(page)) + } + + fun clearCache() { + viewModelScope.launch(Dispatchers.IO) { + repository.clearDatabase() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f577e9f..e472196 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,7 +13,6 @@ + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/fragment_users.xml b/app/src/main/res/layout/fragment_users.xml new file mode 100644 index 0000000..994e66e --- /dev/null +++ b/app/src/main/res/layout/fragment_users.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/users.xml b/app/src/main/res/layout/users.xml new file mode 100644 index 0000000..08a092b --- /dev/null +++ b/app/src/main/res/layout/users.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8f8350..918322a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,4 +11,9 @@ Fetching User Search + User Image + Search By First Name + Error Loading Clients + First Name + Last Name diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b18e852..954d40c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -3,6 +3,9 @@