diff --git a/MyApplication/.gitignore b/MyApplication/.gitignore new file mode 100644 index 000000000..2b75303ac --- /dev/null +++ b/MyApplication/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/MyApplication/.idea/codeStyles/Project.xml b/MyApplication/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..cb22ebb76 --- /dev/null +++ b/MyApplication/.idea/codeStyles/Project.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyApplication/.idea/codeStyles/codeStyleConfig.xml b/MyApplication/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/MyApplication/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/MyApplication/.idea/encodings.xml b/MyApplication/.idea/encodings.xml new file mode 100644 index 000000000..15a15b218 --- /dev/null +++ b/MyApplication/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MyApplication/.idea/gradle.xml b/MyApplication/.idea/gradle.xml new file mode 100644 index 000000000..7ac24c777 --- /dev/null +++ b/MyApplication/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/MyApplication/.idea/misc.xml b/MyApplication/.idea/misc.xml new file mode 100644 index 000000000..af0bbdde1 --- /dev/null +++ b/MyApplication/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyApplication/.idea/runConfigurations.xml b/MyApplication/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/MyApplication/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/MyApplication/.idea/vcs.xml b/MyApplication/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/MyApplication/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MyApplication/app/.gitignore b/MyApplication/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/MyApplication/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/MyApplication/app/build.gradle b/MyApplication/app/build.gradle new file mode 100644 index 000000000..7daeda822 --- /dev/null +++ b/MyApplication/app/build.gradle @@ -0,0 +1,66 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +androidExtensions { + experimental = true +} + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.buoy.codetest" + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + + //implementation 'android.arch.navigation:navigation-fragment:1.0.0' + //implementation 'android.arch.navigation:navigation-ui:1.0.0' + + // Koin for Android + implementation 'org.koin:koin-android:2.0.0-rc-2' + // or Koin for Lifecycle scoping + implementation 'org.koin:koin-androidx-scope:2.0.0-rc-2' + // or Koin for Android Architecture ViewModel + implementation 'org.koin:koin-androidx-viewmodel:2.0.0-rc-2' + + implementation 'com.squareup.retrofit2:retrofit:2.5.0' + implementation 'com.squareup.retrofit2:converter-gson:2.5.0' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0' + + implementation 'com.google.code.gson:gson:2.8.5' + + // rxandroid + implementation "io.reactivex.rxjava2:rxandroid:2.0.2" + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + implementation ("com.github.bumptech.glide:glide:4.9.0") { + exclude group: "com.android.support" + } + implementation "com.android.support:support-fragment:28.0.0" + + implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.0.0' + +} diff --git a/MyApplication/app/proguard-rules.pro b/MyApplication/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/MyApplication/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/MyApplication/app/src/androidTest/java/com/buoy/codetest/ExampleInstrumentedTest.kt b/MyApplication/app/src/androidTest/java/com/buoy/codetest/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..d395c041a --- /dev/null +++ b/MyApplication/app/src/androidTest/java/com/buoy/codetest/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.buoy.codetest + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("com.buoy.codetest", appContext.packageName) + } +} diff --git a/MyApplication/app/src/main/AndroidManifest.xml b/MyApplication/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..fac25922d --- /dev/null +++ b/MyApplication/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/CodeTestApplication.kt b/MyApplication/app/src/main/java/com/buoy/codetest/CodeTestApplication.kt new file mode 100644 index 000000000..7909d7302 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/CodeTestApplication.kt @@ -0,0 +1,23 @@ +package com.buoy.codetest + +import android.app.Application +import com.buoy.codetest.system.injection.networkModule +import com.buoy.codetest.system.injection.repositoryModule +import com.buoy.codetest.system.injection.viewModelModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin + +class CodeTestApplication: Application() { + + override fun onCreate() { + super.onCreate() + + startKoin{ + androidLogger() + androidContext(this@CodeTestApplication) + modules(listOf(repositoryModule, networkModule, viewModelModule)) + } + } + +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/model/api/CocktailApi.kt b/MyApplication/app/src/main/java/com/buoy/codetest/model/api/CocktailApi.kt new file mode 100644 index 000000000..71c328ee4 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/model/api/CocktailApi.kt @@ -0,0 +1,21 @@ +package com.buoy.codetest.model.api + +import com.buoy.codetest.model.domain.DrinksList +import io.reactivex.Single + +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface CocktailApi { + + @GET("api/json/v1/1/filter.php?g=Cocktail_glass") + fun getAllCocktails(): Single + + @GET("api/json/v1/1/lookup.php") + fun getCocktailDetail(@Query("i") id: String): Single + + companion object { + const val URL = "http://www.thecocktaildb.com" + } +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/model/domain/Drink.kt b/MyApplication/app/src/main/java/com/buoy/codetest/model/domain/Drink.kt new file mode 100644 index 000000000..b325d9fb3 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/model/domain/Drink.kt @@ -0,0 +1,40 @@ +package com.buoy.codetest.model.domain + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Drink(val idDrink: String?, + val strDrink: String?, + val strDrinkThumb: String?, + val strInstructions: String?, + val strIngredient1: String?, + val strIngredient2: String?, + val strIngredient3: String?, + val strIngredient4: String?, + val strIngredient5: String?, + val strIngredient6: String?, + val strIngredient7: String?, + val strIngredient8: String?, + val strIngredient9: String?, + val strIngredient10: String?, + val strIngredient11: String?, + val strIngredient12: String?, + val strIngredient13: String?, + val strIngredient14: String?, + val strIngredient15: String?, + val strMeasure1: String?, + val strMeasure2: String?, + val strMeasure3: String?, + val strMeasure4: String?, + val strMeasure5: String?, + val strMeasure6: String?, + val strMeasure7: String?, + val strMeasure8: String?, + val strMeasure9: String?, + val strMeasure10: String?, + val strMeasure11: String?, + val strMeasure12: String?, + val strMeasure13: String?, + val strMeasure14: String?, + val strMeasure15: String?): Parcelable \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/model/domain/DrinksList.kt b/MyApplication/app/src/main/java/com/buoy/codetest/model/domain/DrinksList.kt new file mode 100644 index 000000000..566c500a1 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/model/domain/DrinksList.kt @@ -0,0 +1,3 @@ +package com.buoy.codetest.model.domain + +data class DrinksList(val drinks: List?) \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/model/repository/CocktailRepository.kt b/MyApplication/app/src/main/java/com/buoy/codetest/model/repository/CocktailRepository.kt new file mode 100644 index 000000000..8ea33d575 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/model/repository/CocktailRepository.kt @@ -0,0 +1,13 @@ +package com.buoy.codetest.model.repository + +import com.buoy.codetest.model.domain.Drink +import io.reactivex.Single + + +interface CocktailRepository { + + fun getAllCocktails(): Single> + + fun getCocktailDetail(id: String): Single + +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/model/repository/CocktailRepositoryImpl.kt b/MyApplication/app/src/main/java/com/buoy/codetest/model/repository/CocktailRepositoryImpl.kt new file mode 100644 index 000000000..015ac8d35 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/model/repository/CocktailRepositoryImpl.kt @@ -0,0 +1,28 @@ +package com.buoy.codetest.model.repository + +import com.buoy.codetest.model.api.CocktailApi +import com.buoy.codetest.model.domain.Drink +import io.reactivex.Single + +class CocktailRepositoryImpl(val cocktailApi: CocktailApi): CocktailRepository { + + override fun getCocktailDetail(id: String): Single { + return cocktailApi.getCocktailDetail(id).map { drinkList -> + var drink: Drink? = null + drinkList.drinks?.let { drinks -> + if (drinks.isNotEmpty()) { + drink = drinks[0] + } + } + drink + } + + } + + override fun getAllCocktails(): Single> { + return cocktailApi.getAllCocktails().map { drinksList -> + drinksList.drinks + } + } + +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/system/extentions/Nullability.kt b/MyApplication/app/src/main/java/com/buoy/codetest/system/extentions/Nullability.kt new file mode 100644 index 000000000..1dd0fbde1 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/system/extentions/Nullability.kt @@ -0,0 +1,5 @@ +package com.buoy.codetest.system.extentions + +fun notEmptyStrings(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? { + return if (p1 != null && p1.isNotEmpty() && p2 != null && p2.isNotEmpty()) block(p1, p2) else null +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/system/extentions/Properties.kt b/MyApplication/app/src/main/java/com/buoy/codetest/system/extentions/Properties.kt new file mode 100644 index 000000000..b1d336592 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/system/extentions/Properties.kt @@ -0,0 +1,10 @@ +package com.buoy.codetest.system.extentions + +inline fun Any.getThroughReflection(propertyName: String): T? { + val getterName = "get" + propertyName.capitalize() + return try { + javaClass.getMethod(getterName).invoke(this) as? T + } catch (e: NoSuchMethodException) { + null + } +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/system/injection/GlobalModule.kt b/MyApplication/app/src/main/java/com/buoy/codetest/system/injection/GlobalModule.kt new file mode 100644 index 000000000..df050eb4a --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/system/injection/GlobalModule.kt @@ -0,0 +1,52 @@ +package com.buoy.codetest.system.injection + +import com.buoy.codetest.model.api.CocktailApi +import com.buoy.codetest.model.repository.CocktailRepository +import com.buoy.codetest.model.repository.CocktailRepositoryImpl +import com.buoy.codetest.ui.cocktaildetail.CocktailDetailViewModel +import com.buoy.codetest.ui.home.HomeViewModel +import com.google.gson.GsonBuilder +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import okhttp3.OkHttpClient + + + +private val retrofit: Retrofit = createNetworkClient() +private val cocktailApi: CocktailApi = retrofit.create(CocktailApi::class.java) + +const val DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" + + +val viewModelModule = module { + viewModel { CocktailDetailViewModel(get()) } + viewModel { HomeViewModel(get()) } +} + +val repositoryModule = module { + single { CocktailRepositoryImpl(cocktailApi = get()) as CocktailRepository } +} + +val networkModule = module { + single { cocktailApi } +} + + +fun createNetworkClient(): Retrofit { + val gson = GsonBuilder() + .setDateFormat(DATE_FORMAT) + .create() + + val okHttpClient = OkHttpClient().newBuilder().build() + + return Retrofit.Builder() + .baseUrl(CocktailApi.URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() +} + diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/MainActivity.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/MainActivity.kt new file mode 100644 index 000000000..d4dcf6199 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/MainActivity.kt @@ -0,0 +1,37 @@ +package com.buoy.codetest.ui + +import android.os.Bundle +import android.view.Window +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NavUtils +import androidx.navigation.Navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import com.buoy.codetest.R +import kotlinx.android.synthetic.main.activity_main.* + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + with(window) { + requestFeature(Window.FEATURE_CONTENT_TRANSITIONS) + } + + setContentView(R.layout.activity_main) + + } + + override fun onBackPressed() { + when(NavHostFragment.findNavController(nav_host_fragment).currentDestination?.id) { + R.id.cocktailDetailFragment -> { + NavHostFragment.findNavController(nav_host_fragment).popBackStack() + } + else -> { + super.onBackPressed() + } + } + } + +} diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/cocktaildetail/CocktailDetailFragment.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/cocktaildetail/CocktailDetailFragment.kt new file mode 100644 index 000000000..03041d210 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/cocktaildetail/CocktailDetailFragment.kt @@ -0,0 +1,92 @@ +package com.buoy.codetest.ui.cocktaildetail + +import android.os.Bundle +import android.transition.TransitionInflater +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.transition.ChangeBounds +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.buoy.codetest.R +import com.buoy.codetest.model.domain.Drink +import com.buoy.codetest.system.extentions.getThroughReflection +import com.buoy.codetest.system.extentions.notEmptyStrings +import com.buoy.codetest.ui.common.BaseFragment +import kotlinx.android.synthetic.main.fragment_cocktail_detail.* +import org.koin.androidx.viewmodel.ext.android.viewModel + +class CocktailDetailFragment: BaseFragment() { + + val model: CocktailDetailViewModel by viewModel() + + override fun getLayoutResource(): Int = R.layout.fragment_cocktail_detail + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (activity as AppCompatActivity).setSupportActionBar(toolbar) + + arguments?.let { args -> + if (args.containsKey(EXTRA_DRINK)) { + val drink: Drink = args.getParcelable(EXTRA_DRINK) + model.initialize(drink) + setCocktailImage(drink.strDrinkThumb) + } + } + + model.getDrink().observe(viewLifecycleOwner, Observer { drink -> onDrinkUpdate(drink) }) + } + + private fun setCocktailImage(strDrinkThumb: String?) { + Glide.with(this) + .load(strDrinkThumb) + .fitCenter() + .placeholder(R.drawable.placeholder) + .apply(RequestOptions.bitmapTransform(RoundedCorners(16))) + .into(image) + } + + private fun onDrinkUpdate(drink: Drink?) { + drink?.let { drink -> + setToolbarTitle(drink.strDrink, true) + setCocktailImage(drink.strDrinkThumb) + + ingredients.text = getIngredients(drink) + instructions.text = drink.strInstructions?.let { it } + } + } + + private fun getIngredients(drink: Drink): String { + var ingredients = "" + + for (i in 1..15) { + val ingredient = drink.getThroughReflection("strIngredient$i") + val measure = drink.getThroughReflection("strMeasure$i") + + notEmptyStrings(ingredient, measure) { ingredient, measure -> + ingredients += "$ingredient $measure \n" + } + + } + + return ingredients + } + + companion object { + const val EXTRA_DRINK = "extra_drink" + } +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/cocktaildetail/CocktailDetailViewModel.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/cocktaildetail/CocktailDetailViewModel.kt new file mode 100644 index 000000000..882f0e940 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/cocktaildetail/CocktailDetailViewModel.kt @@ -0,0 +1,39 @@ +package com.buoy.codetest.ui.cocktaildetail + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.buoy.codetest.model.domain.Drink +import com.buoy.codetest.model.repository.CocktailRepository +import com.buoy.codetest.ui.common.BaseViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class CocktailDetailViewModel(val cocktailRepository: CocktailRepository): BaseViewModel() { + + private val drink = MutableLiveData() + fun getDrink(): LiveData = drink + + fun initialize(drink: Drink) { + this.drink.value = drink + requestDrinkDetail(drink.idDrink) + } + + private fun requestDrinkDetail(cocktailId: String?) { + cocktailId?.let { id -> + setState(State.LOADING) + cocktailRepository.getCocktailDetail(id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe ({ result -> + drink.value = result + setState(State.SUCCESS) + }, { error -> + Log.e("HomeViewModel", "requestDrinks:", error) + setState(State.ERROR) + }) + + } + + } +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/common/BaseFragment.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/common/BaseFragment.kt new file mode 100644 index 000000000..355620d1c --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/common/BaseFragment.kt @@ -0,0 +1,42 @@ +package com.buoy.codetest.ui.common + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NavUtils +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.view_load_helper.* + +abstract class BaseFragment: Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(getLayoutResource(), container, false) + } + + abstract fun getLayoutResource(): Int + + fun setToolbarTitle(title: String?, backButton: Boolean) { + (activity as AppCompatActivity)?.let { + it.supportActionBar?.title = title + it.supportActionBar?.setHomeButtonEnabled(backButton) + it.supportActionBar?.setDisplayHomeAsUpEnabled(backButton) + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when(item?.itemId) { + android.R.id.home -> { + activity?.let { it.onBackPressed() } + return true + } + } + + return super.onOptionsItemSelected(item) + + } + +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/common/BaseViewModel.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/common/BaseViewModel.kt new file mode 100644 index 000000000..da206116a --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/common/BaseViewModel.kt @@ -0,0 +1,23 @@ +package com.buoy.codetest.ui.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +open class BaseViewModel: ViewModel() { + + enum class State { + LOADING, + SUCCESS, + ERROR + } + + private val state = MutableLiveData() + fun getState(): LiveData = state + + + protected fun setState(state: State) { + this.state.value = state + } + +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/CocktailsAdapter.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/CocktailsAdapter.kt new file mode 100644 index 000000000..3d0b93e0e --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/CocktailsAdapter.kt @@ -0,0 +1,67 @@ +package com.buoy.codetest.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.Navigation.findNavController +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.buoy.codetest.R +import com.buoy.codetest.model.domain.Drink +import com.buoy.codetest.ui.cocktaildetail.CocktailDetailFragment +import kotlinx.android.synthetic.main.listitem_cocktail.view.* + +class CocktailsAdapter: RecyclerView.Adapter() { + + private val drinks: MutableList = ArrayList() + + fun setDrinks(drinks: List) { + this.drinks.clear() + this.drinks.addAll(drinks) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CocktailItemViewHolder { + return CocktailItemViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.listitem_cocktail, parent, false)) + } + + override fun getItemCount(): Int = drinks.size + + override fun onBindViewHolder(holder: CocktailItemViewHolder, position: Int) { + holder.bind(drinks[position]) + } + + inner class CocktailItemViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + + fun bind(drink: Drink) = with(itemView) { + name.text = drink.strDrink + + Glide.with(context) + .load(drink.strDrinkThumb) + .placeholder(R.drawable.placeholder) + .fitCenter() + .apply(RequestOptions.bitmapTransform(RoundedCorners(16))) + .into(image) + + setOnClickListener { onDrinkSelected(drink, this) } + } + + private fun onDrinkSelected(drink: Drink, view: View) { + val bundle = Bundle() + bundle.putParcelable(CocktailDetailFragment.EXTRA_DRINK, drink) + + val extras = FragmentNavigatorExtras( + view.image to "cocktailTransitionName_to" + ) + + findNavController(view).navigate(R.id.action_homeFragment_to_cocktailDetailFragment, bundle, null, extras) + } + } + +} + diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/HomeFragment.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/HomeFragment.kt new file mode 100644 index 000000000..81714523b --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/HomeFragment.kt @@ -0,0 +1,97 @@ +package com.buoy.codetest.ui.home + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SearchView.OnQueryTextListener +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.buoy.codetest.R +import com.buoy.codetest.ui.common.BaseFragment +import com.buoy.codetest.ui.common.BaseViewModel +import kotlinx.android.synthetic.main.fragment_home.* +import kotlinx.android.synthetic.main.view_load_helper.* +import org.koin.androidx.viewmodel.ext.android.viewModel + + +class HomeFragment: BaseFragment() { + + private val model: HomeViewModel by viewModel() + private val adapter = CocktailsAdapter() + + override fun getLayoutResource(): Int = R.layout.fragment_home + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (activity as AppCompatActivity).setSupportActionBar(toolbar) + + cocktail_list.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + cocktail_list.adapter = adapter + + error_retry.setOnClickListener { onRetryClicked() } + + model.getDrinks().observe(viewLifecycleOwner, Observer { drinks -> + adapter.setDrinks(drinks) + }) + + model.getState().observe(viewLifecycleOwner, Observer { state -> + onStateChanged(state) + }) + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + super.onCreateOptionsMenu(menu, inflater) + inflater?.inflate(R.menu.menu_home, menu) + val menuItem = menu?.findItem(R.id.search) + (menuItem?.actionView as SearchView).setOnQueryTextListener( onQueryListener ) + } + + private fun onStateChanged(state: BaseViewModel.State?) { + + when(state) { + BaseViewModel.State.LOADING -> { + progress.visibility = View.VISIBLE + error_view.visibility = View.GONE + } + + BaseViewModel.State.SUCCESS -> { + progress.visibility = View.GONE + error_view.visibility = View.GONE + } + + BaseViewModel.State.ERROR -> { + progress.visibility = View.GONE + error_view.visibility = View.VISIBLE + } + } + } + + + private val onQueryListener = object: OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + model.filterList(newText) + return true + } + + } + + private fun onRetryClicked() { + model.requestDrinks() + } + + +} \ No newline at end of file diff --git a/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/HomeViewModel.kt b/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/HomeViewModel.kt new file mode 100644 index 000000000..5a7dcea53 --- /dev/null +++ b/MyApplication/app/src/main/java/com/buoy/codetest/ui/home/HomeViewModel.kt @@ -0,0 +1,72 @@ +package com.buoy.codetest.ui.home + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.buoy.codetest.model.domain.Drink +import com.buoy.codetest.model.repository.CocktailRepository +import com.buoy.codetest.ui.common.BaseViewModel +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class HomeViewModel(val cocktailRepository: CocktailRepository): BaseViewModel() { + + + private var drinks: List? = null + private val filterDrinks = MutableLiveData>() + + private var filterText: String? = null + + fun getDrinks(): LiveData> = filterDrinks + fun getFilter(): String? = filterText + + init { + requestDrinks() + } + + fun requestDrinks() { + setState(State.LOADING) + + cocktailRepository.getAllCocktails() + .flatMap { drinks -> + this.drinks = drinks + executeFilter() + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe ({ + setState(State.SUCCESS) + }, { error -> + Log.e("HomeViewModel", "requestDrinks:", error) + setState(State.ERROR) + }) + + + } + + fun filterList(newText: String?) { + filterText = newText + executeFilter() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe ({}, {}) + } + + private fun executeFilter(): Single { + return Single.create { emitter -> + drinks?.let { drinks -> + + val filter = filterText?.let { it.toLowerCase() } ?: "" + if (filter.isNotEmpty()) { + val filterList = drinks.filter { drink -> drink.strDrink?.toLowerCase()?.contains(filter) ?: false } + filterDrinks.postValue(filterList) + } else { + filterDrinks.postValue(drinks) + } + } + + emitter.onSuccess(true) + } + } +} \ No newline at end of file diff --git a/MyApplication/app/src/main/res/anim/slide_in_left.xml b/MyApplication/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 000000000..96995fa63 --- /dev/null +++ b/MyApplication/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/anim/slide_in_right.xml b/MyApplication/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 000000000..6fb52a3b7 --- /dev/null +++ b/MyApplication/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/anim/slide_out_left.xml b/MyApplication/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 000000000..020a1bee1 --- /dev/null +++ b/MyApplication/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/anim/slide_out_right.xml b/MyApplication/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 000000000..c4de80064 --- /dev/null +++ b/MyApplication/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/MyApplication/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..6348baae3 --- /dev/null +++ b/MyApplication/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/MyApplication/app/src/main/res/drawable-xhdpi/placeholder.png b/MyApplication/app/src/main/res/drawable-xhdpi/placeholder.png new file mode 100644 index 000000000..b2c6afe47 Binary files /dev/null and b/MyApplication/app/src/main/res/drawable-xhdpi/placeholder.png differ diff --git a/MyApplication/app/src/main/res/drawable/ic_launcher_background.xml b/MyApplication/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..a0ad202f9 --- /dev/null +++ b/MyApplication/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyApplication/app/src/main/res/layout/activity_main.xml b/MyApplication/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..03fccc4c6 --- /dev/null +++ b/MyApplication/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/layout/fragment_cocktail_detail.xml b/MyApplication/app/src/main/res/layout/fragment_cocktail_detail.xml new file mode 100644 index 000000000..eaa79dff5 --- /dev/null +++ b/MyApplication/app/src/main/res/layout/fragment_cocktail_detail.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyApplication/app/src/main/res/layout/fragment_home.xml b/MyApplication/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 000000000..365b8e7ff --- /dev/null +++ b/MyApplication/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/layout/listitem_cocktail.xml b/MyApplication/app/src/main/res/layout/listitem_cocktail.xml new file mode 100644 index 000000000..885e37c4b --- /dev/null +++ b/MyApplication/app/src/main/res/layout/listitem_cocktail.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyApplication/app/src/main/res/layout/view_load_helper.xml b/MyApplication/app/src/main/res/layout/view_load_helper.xml new file mode 100644 index 000000000..b173b542a --- /dev/null +++ b/MyApplication/app/src/main/res/layout/view_load_helper.xml @@ -0,0 +1,43 @@ + + + + + + + +