From fd4cd93f78c358d04494503906df29ff969f479c Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Sat, 27 Aug 2022 23:19:26 +0100 Subject: [PATCH 1/2] Issue resolved Took 2 hours 5 minutes --- .../data/network/api/BackupCurrencyApi.kt | 5 - .../easycc/data/network/api/CurrencyApi.kt | 8 +- .../data/network/api/RemoteDataSource.kt | 2 +- .../data/network/response/CurrencyResponse.kt | 24 ++--- .../easycc/data/repository/Repository.kt | 7 +- .../easycc/data/repository/RepositoryImpl.kt | 28 +++--- .../h_mal/easycc/helper/CurrencyDataHelper.kt | 19 ---- .../h_mal/easycc/helper/WidgetHelper.kt | 3 +- .../h_mal/easycc/models/CurrencyObject.kt | 16 ++-- .../appttude/h_mal/easycc/ui/BaseActivity.kt | 96 +++++++++++++++++++ .../appttude/h_mal/easycc/ui/BaseViewModel.kt | 22 +++++ .../h_mal/easycc/ui/main/CustomDialogClass.kt | 11 +-- .../h_mal/easycc/ui/main/MainActivity.kt | 54 ++++------- .../h_mal/easycc/ui/main/MainViewModel.kt | 37 ++----- ...urrencyAppWidgetConfigureActivityKotlin.kt | 38 +++----- .../easycc/ui/widget/WidgetSubmitDialog.kt | 2 - .../h_mal/easycc/ui/widget/WidgetViewModel.kt | 13 +-- .../com/appttude/h_mal/easycc/utils/Event.kt | 24 +++++ .../appttude/h_mal/easycc/utils/ViewState.kt | 7 ++ .../appttude/h_mal/easycc/utils/ViewUtils.kt | 29 ++++-- app/src/main/res/drawable/ic_background.xml | 6 +- app/src/main/res/layout/activity_main.xml | 13 --- app/src/main/res/layout/progress_layout.xml | 18 ++++ 23 files changed, 278 insertions(+), 204 deletions(-) delete mode 100644 app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/ui/BaseActivity.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/ui/BaseViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/utils/Event.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/utils/ViewState.kt create mode 100644 app/src/main/res/layout/progress_layout.xml diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt index fd24c6f..6f8fd91 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt @@ -1,12 +1,7 @@ package com.appttude.h_mal.easycc.data.network.api -import com.appttude.h_mal.easycc.data.network.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import retrofit2.http.Query diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt index c4da99f..40a1591 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt @@ -1,20 +1,14 @@ package com.appttude.h_mal.easycc.data.network.api -import com.appttude.h_mal.easycc.data.network.interceptors.NetworkConnectionInterceptor -import com.appttude.h_mal.easycc.data.network.interceptors.QueryInterceptor import com.appttude.h_mal.easycc.data.network.response.ResponseObject -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import retrofit2.http.Query /** * Retrofit Network class to currency api calls */ -interface CurrencyApi : Api{ +interface CurrencyApi : Api { // Get rate from server with arguments passed in Repository @GET("convert?") diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt index 290eafc..77be4d9 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt @@ -5,7 +5,7 @@ import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import javax.inject.Inject -class RemoteDataSource @Inject constructor(){ +class RemoteDataSource @Inject constructor() { fun buildApi( okkHttpclient: OkHttpClient, diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/CurrencyResponse.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/CurrencyResponse.kt index 91da559..7784450 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/CurrencyResponse.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/CurrencyResponse.kt @@ -5,22 +5,22 @@ import com.appttude.h_mal.easycc.models.CurrencyModelInterface import com.google.gson.annotations.SerializedName data class CurrencyResponse( - @field:SerializedName("date") - val date: String? = null, - @field:SerializedName("amount") - val amount: Double? = null, - @field:SerializedName("rates") - var rates: Map? = null, - @field:SerializedName("base") - val base: String? = null + @field:SerializedName("date") + val date: String? = null, + @field:SerializedName("amount") + val amount: Double? = null, + @field:SerializedName("rates") + var rates: Map? = null, + @field:SerializedName("base") + val base: String? = null ) : CurrencyModelInterface { override fun getCurrencyModel(): CurrencyModel { return CurrencyModel( - base, - rates?.iterator()?.next()?.key, - rates?.iterator()?.next()?.value ?: 0.0 - ) + base, + rates?.iterator()?.next()?.key, + rates?.iterator()?.next()?.value ?: 0.0 + ) } } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/repository/Repository.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/repository/Repository.kt index 37287e8..5650519 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/repository/Repository.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/repository/Repository.kt @@ -1,16 +1,13 @@ package com.appttude.h_mal.easycc.data.repository -import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse -import com.appttude.h_mal.easycc.data.network.response.ResponseObject +import com.appttude.h_mal.easycc.models.CurrencyModel /** * Main entry point for accessing currency data. */ interface Repository { - suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject - - suspend fun getBackupDataFromApi(fromCurrency: String, toCurrency: String): CurrencyResponse + suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): CurrencyModel fun getConversionPair(): Pair diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt index 7f63a32..292e4e7 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt @@ -5,10 +5,10 @@ import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.data.network.SafeApiRequest import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import com.appttude.h_mal.easycc.data.network.api.CurrencyApi -import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse -import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider +import com.appttude.h_mal.easycc.models.CurrencyModel import com.appttude.h_mal.easycc.utils.convertPairsListToString +import java.io.IOException import javax.inject.Inject /** @@ -23,17 +23,19 @@ class RepositoryImpl @Inject constructor( override suspend fun getDataFromApi( fromCurrency: String, toCurrency: String - ): ResponseObject { - // Set currency pairs as correct string for api query eg. AUD_GBP - val currencyPair = convertPairsListToString(fromCurrency, toCurrency) - return responseUnwrap { api.getCurrencyRate(currencyPair) } - } - - override suspend fun getBackupDataFromApi( - fromCurrency: String, - toCurrency: String - ): CurrencyResponse { - return responseUnwrap { backUpApi.getCurrencyRate(fromCurrency, toCurrency) } + ): CurrencyModel { + return try { + // Set currency pairs as correct string for api query eg. AUD_GBP + val currencyPair = convertPairsListToString(fromCurrency, toCurrency) + responseUnwrap { api.getCurrencyRate(currencyPair) }.getCurrencyModel() + } catch (e: IOException) { + responseUnwrap { + backUpApi.getCurrencyRate( + fromCurrency, + toCurrency + ) + }.getCurrencyModel() + } } override fun getConversionPair(): Pair { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt b/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt deleted file mode 100644 index 89cce5d..0000000 --- a/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.appttude.h_mal.easycc.helper - -import com.appttude.h_mal.easycc.data.repository.Repository -import com.appttude.h_mal.easycc.models.CurrencyModelInterface -import javax.inject.Inject - -class CurrencyDataHelper @Inject constructor( - val repository: Repository -) { - - suspend fun getDataFromApi(from: String, to: String): CurrencyModelInterface { - return try { - repository.getDataFromApi(from, to) - } catch (e: Exception) { - e.printStackTrace() - repository.getBackupDataFromApi(from, to) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt b/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt index 8fdbbf6..00863ab 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt @@ -6,7 +6,6 @@ import com.appttude.h_mal.easycc.utils.trimToThree import javax.inject.Inject class WidgetHelper @Inject constructor( - private val helper: CurrencyDataHelper, val repository: Repository ) { @@ -16,7 +15,7 @@ class WidgetHelper @Inject constructor( val s1 = pair.first?.trimToThree() ?: return null val s2 = pair.second?.trimToThree() ?: return null - return helper.getDataFromApi(s1, s2).getCurrencyModel() + return repository.getDataFromApi(s1, s2) } catch (e: Exception) { e.printStackTrace() return null diff --git a/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyObject.kt b/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyObject.kt index 4ca326c..9172181 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyObject.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyObject.kt @@ -3,12 +3,12 @@ package com.appttude.h_mal.easycc.models import com.google.gson.annotations.SerializedName data class CurrencyObject( - @SerializedName("id") - var id: String, - @SerializedName("fr") - var fr: String, - @SerializedName("to") - var to: String, - @SerializedName("val") - var value: Double + @SerializedName("id") + var id: String, + @SerializedName("fr") + var fr: String, + @SerializedName("to") + var to: String, + @SerializedName("val") + var value: Double ) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/BaseActivity.kt new file mode 100644 index 0000000..c46cb0c --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/BaseActivity.kt @@ -0,0 +1,96 @@ +package com.appttude.h_mal.easycc.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup.LayoutParams +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.appcompat.app.AppCompatActivity +import com.appttude.h_mal.easycc.R +import com.appttude.h_mal.easycc.utils.* + +abstract class BaseActivity : AppCompatActivity() { + + private lateinit var loadingView: View + + abstract val viewModel: V? + + private var loading: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + configureObserver() + } + + /** + * Creates a loading view which to be shown during async operations + * + * #setOnClickListener(null) is an ugly work around to prevent under being clicked during + * loading + */ + private fun instantiateLoadingView() { + loadingView = layoutInflater.inflate(R.layout.progress_layout, null) + loadingView.setOnClickListener(null) + addContentView(loadingView, LayoutParams(MATCH_PARENT, MATCH_PARENT)) + + loadingView.hide() + } + + override fun onStart() { + super.onStart() + instantiateLoadingView() + } + + fun startActivity(activity: Class) { + val intent = Intent(this, activity) + startActivity(intent) + } + + open fun onStarted() { + loadingView.fadeIn() + loading = true + } + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onSuccess(data: Any?) { + loadingView.fadeOut() + loading = false + } + + /** + * Called in case of failure or some error emitted from the liveData in viewModel + */ + open fun onFailure(error: String?) { + error?.let { displayToast(it) } + loadingView.fadeOut() + loading = false + } + + private fun configureObserver() { + viewModel?.uiState?.observe(this) { + when (it) { + is ViewState.HasStarted -> onStarted() + is ViewState.HasData<*> -> onSuccess(it.data.getContentIfNotHandled()) + is ViewState.HasError -> onFailure(it.error.getContentIfNotHandled()) + } + } + } + + private fun View.fadeIn() = apply { + show() + triggerAnimation(android.R.anim.fade_in) {} + } + + private fun View.fadeOut() = apply { + hide() + triggerAnimation(android.R.anim.fade_out) {} + } + + + override fun onBackPressed() { + if (!loading) super.onBackPressed() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/BaseViewModel.kt new file mode 100644 index 0000000..ed13dfd --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/BaseViewModel.kt @@ -0,0 +1,22 @@ +package com.appttude.h_mal.easycc.ui + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.appttude.h_mal.easycc.utils.Event +import com.appttude.h_mal.easycc.utils.ViewState + +abstract class BaseViewModel : ViewModel() { + open val uiState: MutableLiveData = MutableLiveData() + + fun onStart() { + uiState.postValue(ViewState.HasStarted) + } + + fun onSuccess(result: T) { + uiState.postValue(ViewState.HasData(Event(result))) + } + + protected fun onError(error: String) { + uiState.postValue(ViewState.HasError(Event(error))) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt index 7b9de2b..d95e307 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt @@ -9,7 +9,6 @@ import android.view.WindowManager import android.widget.ArrayAdapter import android.widget.ListView import android.widget.TextView -import androidx.appcompat.widget.SearchView import com.appttude.h_mal.easycc.R /** @@ -18,7 +17,7 @@ import com.appttude.h_mal.easycc.R @Suppress("DEPRECATION") class CustomDialogClass( context: Context, - private val clickListener: ClickListener + val onSelect: (String) -> Unit ) : Dialog(context) { override fun onCreate(savedInstanceState: Bundle?) { @@ -30,7 +29,6 @@ class CustomDialogClass( // Keyboard not to overlap dialog window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - // array adapter for list of currencies in R.Strings val arrayAdapter = ArrayAdapter.createFromResource( context, R.array.currency_arrays, @@ -52,13 +50,8 @@ class CustomDialogClass( // interface selection back to calling activity list_view.setOnItemClickListener { adapterView, _, i, _ -> - clickListener.onText(adapterView.getItemAtPosition(i).toString()) + onSelect.invoke(adapterView.getItemAtPosition(i).toString()) dismiss() } } -} - -// Interface to handle selection within dialog -interface ClickListener { - fun onText(currencyName: String) } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt index 3c4667c..b655924 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt @@ -7,18 +7,16 @@ import android.view.View import android.view.WindowManager import android.widget.TextView import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.databinding.ActivityMainBinding +import com.appttude.h_mal.easycc.models.CurrencyModel +import com.appttude.h_mal.easycc.ui.BaseActivity import com.appttude.h_mal.easycc.utils.clearEditText -import com.appttude.h_mal.easycc.utils.displayToast -import com.appttude.h_mal.easycc.utils.hideView import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MainActivity : AppCompatActivity(), View.OnClickListener { +class MainActivity : BaseActivity(), View.OnClickListener { - private val viewModel: MainViewModel by viewModels() + override val viewModel: MainViewModel by viewModels() private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -29,34 +27,22 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { * Prevent keyboard overlapping views */ window.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN or - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN ) viewModel.initiate(intent.extras) - binding.currencyOne.text = viewModel.rateIdTo - binding.currencyTwo.text = viewModel.rateIdFrom + binding.currencyOne.text = viewModel.rateIdFrom + binding.currencyTwo.text = viewModel.rateIdTo setUpListeners() - setUpObservers() } - private fun setUpObservers() { - viewModel.operationStartedListener.observe(this) { - binding.progressBar.hideView(false) - } - viewModel.operationFinishedListener.observe(this) { pair -> - // hide progress bar - binding.progressBar.hideView(true) - if (pair.first) { - // Operation was successful remove text in EditTexts - binding.bottomInsertValues.clearEditText() - binding.topInsertValue.clearEditText() - } else { - // Display Toast with error message returned from Viewmodel - pair.second?.let { displayToast(it) } - } + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is CurrencyModel) { + binding.bottomInsertValues.clearEditText() + binding.topInsertValue.clearEditText() } } @@ -69,25 +55,21 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } private fun showCustomDialog(view: View?) { - CustomDialogClass(this, object : ClickListener { - override fun onText(currencyName: String) { - (view as TextView).text = currencyName - viewModel.setCurrencyName(view.tag, currencyName) - } - - }).show() + CustomDialogClass(this) { + (view as TextView).text = it + viewModel.setCurrencyName(view.tag, it) + }.show() } override fun onClick(view: View?) { showCustomDialog(view) } - // Text watcher applied to EditText @topInsertValue private val textWatcherClass: TextWatcher = object : TextWatcher { override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) { // Remove text watcher on other text watcher to prevent infinite loop binding.bottomInsertValues.removeTextChangedListener(textWatcherClass2) - // Clear any values if current EditText is empty + if (binding.topInsertValue.text.isNullOrEmpty()) binding.bottomInsertValues.setText("") } @@ -102,7 +84,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { private val textWatcherClass2: TextWatcher = object : TextWatcher { override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) { - binding.topInsertValue.removeTextChangedListener(textWatcherClass) if (binding.bottomInsertValues.text.isNullOrEmpty()) binding.topInsertValue.clearEditText() @@ -115,4 +96,5 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } } + } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt index 6370354..3704d0c 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt @@ -1,17 +1,12 @@ package com.appttude.h_mal.easycc.ui.main import android.os.Bundle -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appttude.h_mal.easycc.data.repository.Repository -import com.appttude.h_mal.easycc.helper.CurrencyDataHelper +import com.appttude.h_mal.easycc.ui.BaseViewModel import com.appttude.h_mal.easycc.utils.toTwoDpString import com.appttude.h_mal.easycc.utils.trimToThree import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject @@ -21,60 +16,48 @@ import javax.inject.Inject */ @HiltViewModel class MainViewModel @Inject constructor( - private val currencyDataHelper: CurrencyDataHelper, private val repository: Repository -) : ViewModel() { +) : BaseViewModel() { private val conversionPairs by lazy { repository.getConversionPair() } - // Viewbinding to textviews in @activity_main.xml + // Viewbinded variables var rateIdFrom: String? = null var rateIdTo: String? = null - //operation results livedata based on outcome of operation - val operationStartedListener = MutableLiveData() - val operationFinishedListener = MutableLiveData>() - private var conversionRate: Double = 1.00 private fun getExchangeRate() { - operationStartedListener.postValue(true) - - // view binded exchange rates selected null checked + onStart() if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()) { - operationFinishedListener.postValue(Pair(false, "Select currencies")) + onError("Select both currencies") return } - // No need to call api as it will return exchange rate as 1 if (rateIdFrom == rateIdTo) { conversionRate = 1.00 - operationFinishedListener.postValue(Pair(true, null)) + onError("Currency selections are the same") return } viewModelScope.launch { try { // Non-null assertion (!!) as values have been null checked and have not changed - val exchangeResponse = currencyDataHelper.getDataFromApi( + val exchangeResponse = repository.getDataFromApi( rateIdFrom!!.trimToThree(), rateIdTo!!.trimToThree() ) - exchangeResponse.getCurrencyModel().let { + exchangeResponse.let { conversionRate = it.rate repository.setConversionPair(rateIdFrom!!, rateIdTo!!) - operationFinishedListener.postValue(Pair(true, null)) + onSuccess(it) return@launch } } catch (e: IOException) { - e.message?.let { - operationFinishedListener.postValue(Pair(false, it)) - return@launch - } + e.message?.let { onError(it) } } - operationFinishedListener.postValue(Pair(false, "Failed to retrieve rate")) } } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt index f5de971..70df0a8 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt @@ -7,11 +7,9 @@ import android.os.Bundle import android.view.View import android.widget.TextView import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding -import com.appttude.h_mal.easycc.ui.main.ClickListener +import com.appttude.h_mal.easycc.ui.BaseActivity import com.appttude.h_mal.easycc.ui.main.CustomDialogClass -import com.appttude.h_mal.easycc.utils.displayToast import com.appttude.h_mal.easycc.utils.transformIntToArray import com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin import dagger.hilt.android.AndroidEntryPoint @@ -20,16 +18,16 @@ import dagger.hilt.android.AndroidEntryPoint * The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget. */ @AndroidEntryPoint -class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), +class CurrencyAppWidgetConfigureActivityKotlin : BaseActivity(), View.OnClickListener { - val viewModel: WidgetViewModel by viewModels() + override val viewModel: WidgetViewModel by viewModels() private lateinit var binding: CurrencyAppWidgetConfigureBinding private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID - public override fun onCreate(icicle: Bundle?) { - super.onCreate(icicle) + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater) setContentView(binding.root) @@ -50,10 +48,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), finish() return } - viewModel.initiate(mAppWidgetId) - setupObserver() setupClickListener() } @@ -63,17 +59,9 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), binding.currencyTwo.setOnClickListener(this) } - private fun setupObserver() { - viewModel.operationFinishedListener.observe(this) { - - // it.first is a the success of the operation - if (it.first) { - displaySubmitDialog() - } else { - // failed operation - display toast with message from it.second - it.second?.let { message -> displayToast(message) } - } - } + override fun onSuccess(data: Any?) { + super.onSuccess(data) + displaySubmitDialog() } override fun onClick(view: View?) { @@ -98,12 +86,10 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), private fun showCustomDialog(view: View?) { - CustomDialogClass(this, object : ClickListener { - override fun onText(currencyName: String) { - (view as TextView).text = currencyName - viewModel.setCurrencyName(view.tag, currencyName) - } - }).show() + CustomDialogClass(this) { + (view as TextView).text = it + viewModel.setCurrencyName(view.tag, it) + }.show() } fun finishCurrencyWidgetActivity() { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt index fb047c6..6800341 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt @@ -3,8 +3,6 @@ package com.appttude.h_mal.easycc.ui.widget import android.app.Dialog import android.content.Context import android.os.Bundle -import com.appttude.h_mal.easycc.R -import com.appttude.h_mal.easycc.databinding.ActivityMainBinding import com.appttude.h_mal.easycc.databinding.ConfirmDialogBinding diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt index 1d94263..64d4c46 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt @@ -1,8 +1,7 @@ package com.appttude.h_mal.easycc.ui.widget -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import com.appttude.h_mal.easycc.data.repository.Repository +import com.appttude.h_mal.easycc.ui.BaseViewModel import com.appttude.h_mal.easycc.utils.trimToThree import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -10,7 +9,7 @@ import javax.inject.Inject @HiltViewModel class WidgetViewModel @Inject constructor( private val repository: Repository -) : ViewModel() { +) : BaseViewModel() { private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] } var appWidgetId: Int? = null @@ -20,7 +19,6 @@ class WidgetViewModel @Inject constructor( var rateIdTo: String? = null // Live data to feedback to @CurrencyAppWidgetConfigureActivityKotlin - val operationFinishedListener = MutableLiveData>() // Setup viewmodel app widget ID // Set default values for text views @@ -43,15 +41,14 @@ class WidgetViewModel @Inject constructor( fun submitSelectionOnClick() { if (rateIdTo == null || rateIdFrom == null) { - operationFinishedListener.value = Pair(false, "Selections incomplete") + onError("Selections incomplete") return } if (rateIdFrom == rateIdTo) { - operationFinishedListener.value = - Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}") + onError("Selected rates cannot be the same ${rateIdFrom}${rateIdTo}") return } - operationFinishedListener.value = Pair(true, null) + onSuccess(Unit) } fun setWidgetStored() { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/utils/Event.kt b/app/src/main/java/com/appttude/h_mal/easycc/utils/Event.kt new file mode 100644 index 0000000..e22afad --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/utils/Event.kt @@ -0,0 +1,24 @@ +package com.appttude.h_mal.easycc.utils + +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewState.kt b/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewState.kt new file mode 100644 index 0000000..dac521a --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewState.kt @@ -0,0 +1,7 @@ +package com.appttude.h_mal.easycc.utils + +sealed class ViewState { + object HasStarted : ViewState() + class HasData(val data: Event) : ViewState() + class HasError(val error: Event) : ViewState() +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewUtils.kt index 040378f..9520fb1 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewUtils.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/utils/ViewUtils.kt @@ -2,21 +2,34 @@ package com.appttude.h_mal.easycc.utils import android.content.Context import android.view.View +import android.view.animation.Animation +import android.view.animation.AnimationUtils import android.widget.EditText import android.widget.Toast +import androidx.annotation.AnimRes fun EditText.clearEditText() { this.setText("") } -fun View.hideView(vis: Boolean) { - visibility = if (vis) { - View.GONE - } else { - View.VISIBLE - } -} - fun Context.displayToast(message: String) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } + +fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) { + val animation = AnimationUtils.loadAnimation(context, id) + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation) + override fun onAnimationStart(a: Animation?) {} + override fun onAnimationRepeat(a: Animation?) {} + }) + startAnimation(animation) +} + +fun View.show() { + this.visibility = View.VISIBLE +} + +fun View.hide() { + this.visibility = View.GONE +} diff --git a/app/src/main/res/drawable/ic_background.xml b/app/src/main/res/drawable/ic_background.xml index 59ab491..21d6752 100644 --- a/app/src/main/res/drawable/ic_background.xml +++ b/app/src/main/res/drawable/ic_background.xml @@ -1,7 +1,7 @@ @@ -21,4 +21,4 @@ - + \ 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 a4515cc..da84114 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -99,17 +99,4 @@ - diff --git a/app/src/main/res/layout/progress_layout.xml b/app/src/main/res/layout/progress_layout.xml new file mode 100644 index 0000000..117f18c --- /dev/null +++ b/app/src/main/res/layout/progress_layout.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file From b6283340f203249f164306405d16c727455560be Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Sun, 28 Aug 2022 00:56:07 +0100 Subject: [PATCH 2/2] Issue resolved with unit tests Took 1 hour 31 minutes --- .../h_mal/easycc/ui/widget/WidgetViewModel.kt | 2 +- .../repository/RepositoryNetworkTest.kt | 10 +- .../h_mal/easycc/ui/BaseViewModelTest.kt | 22 ++++ .../h_mal/easycc/ui/main/MainViewModelTest.kt | 114 +++++++----------- .../easycc/ui/widget/WidgetViewModelTest.kt | 31 +++-- 5 files changed, 91 insertions(+), 88 deletions(-) create mode 100644 app/src/test/java/com/appttude/h_mal/easycc/ui/BaseViewModelTest.kt diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt index 64d4c46..60306a9 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt @@ -45,7 +45,7 @@ class WidgetViewModel @Inject constructor( return } if (rateIdFrom == rateIdTo) { - onError("Selected rates cannot be the same ${rateIdFrom}${rateIdTo}") + onError("Selected rates cannot be the same") return } onSuccess(Unit) diff --git a/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryNetworkTest.kt b/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryNetworkTest.kt index 3a67271..fdce986 100644 --- a/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryNetworkTest.kt +++ b/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryNetworkTest.kt @@ -1,11 +1,13 @@ package com.appttude.h_mal.easycc.repository +import com.appttude.h_mal.easycc.data.network.SafeApiRequest import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import com.appttude.h_mal.easycc.data.network.api.CurrencyApi import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.RepositoryImpl +import com.appttude.h_mal.easycc.models.CurrencyModel import com.appttude.h_mal.easycc.utils.convertPairsListToString import kotlinx.coroutines.runBlocking import okhttp3.ResponseBody @@ -22,7 +24,7 @@ import java.io.IOException import kotlin.test.assertFailsWith -class RepositoryNetworkTest { +class RepositoryNetworkTest : SafeApiRequest() { lateinit var repository: Repository @@ -49,15 +51,16 @@ class RepositoryNetworkTest { //create a successful retrofit response val mockCurrencyResponse = mock(ResponseObject::class.java) val re = Response.success(mockCurrencyResponse) + val currencyModel = mock(CurrencyModel::class.java) //WHEN - loginApiRequest to return a successful response val currencyPair = convertPairsListToString(s1, s2) Mockito.`when`(api.getCurrencyRate(currencyPair)).thenReturn(re) + Mockito.`when`(responseUnwrap { api.getCurrencyRate(currencyPair) }.getCurrencyModel()).thenReturn(currencyModel) //THEN - the unwrapped login response contains the correct values val currencyResponse = repository.getDataFromApi(s1, s2) - assertNotNull(currencyResponse) - assertEquals(currencyResponse, mockCurrencyResponse) + assertEquals(currencyResponse, currencyModel) } @Test @@ -74,6 +77,7 @@ class RepositoryNetworkTest { //WHEN val currencyPair = convertPairsListToString(s1, s2) Mockito.`when`(api.getCurrencyRate(currencyPair)).thenAnswer { re } + Mockito.`when`(apiBackup.getCurrencyRate(s1, s2)).thenAnswer { re } //THEN - assert exception is not null val ioExceptionReturned = assertFailsWith { diff --git a/app/src/test/java/com/appttude/h_mal/easycc/ui/BaseViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/easycc/ui/BaseViewModelTest.kt new file mode 100644 index 0000000..7e9f54b --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/easycc/ui/BaseViewModelTest.kt @@ -0,0 +1,22 @@ +package com.appttude.h_mal.easycc.ui + +import androidx.lifecycle.MutableLiveData +import com.appttude.h_mal.easycc.utils.ViewState + +abstract class BaseViewModelTest { + + abstract val viewModel: V? + + open fun setUp() { + viewModel?.uiState?.observeForever { + when (it) { + is ViewState.HasStarted -> Unit + is ViewState.HasData<*> -> dataPost.postValue(it.data.getContentIfNotHandled()) + is ViewState.HasError -> errorPost.postValue(it.error.getContentIfNotHandled()) + } + } + } + + var dataPost: MutableLiveData = MutableLiveData() + var errorPost: MutableLiveData = MutableLiveData() +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt index 49e7b98..c4d90a7 100644 --- a/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt +++ b/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt @@ -1,17 +1,17 @@ package com.appttude.h_mal.easycc.ui.main import android.os.Bundle -import android.util.Log import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.repository.Repository -import com.appttude.h_mal.easycc.helper.CurrencyDataHelper +import com.appttude.h_mal.easycc.models.CurrencyModel +import com.appttude.h_mal.easycc.ui.BaseViewModelTest import com.appttude.h_mal.easycc.utils.MainCoroutineRule import com.appttude.h_mal.easycc.utils.observeOnce import com.nhaarman.mockitokotlin2.doAnswer import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Rule import org.junit.Test @@ -22,7 +22,7 @@ import org.mockito.MockitoAnnotations import java.io.IOException @OptIn(ExperimentalCoroutinesApi::class) -class MainViewModelTest { +class MainViewModelTest : BaseViewModelTest(){ // Run tasks synchronously @get:Rule @@ -31,27 +31,27 @@ class MainViewModelTest { @get:Rule var mainCoroutineRule = MainCoroutineRule() - lateinit var viewModel: MainViewModel + override lateinit var viewModel: MainViewModel @Mock lateinit var repository: Repository - @Mock - lateinit var helper: CurrencyDataHelper + private val currencyOne = "AUD - Australian Dollar" + private val currencyTwo = "GBP - British Pound" @Before - fun setUp() { + override fun setUp() { MockitoAnnotations.initMocks(this) - viewModel = MainViewModel(helper, repository) + viewModel = MainViewModel(repository) + + super.setUp() } @Test fun initiate_validBundleValues_successResponse() = runBlocking { //GIVEN - val currencyOne = "AUD - Australian Dollar" - val currencyTwo = "GBP - British Pound" val bundle = mock(Bundle()::class.java) - val responseObject = mock(ResponseObject::class.java) + val responseObject = mock(CurrencyModel::class.java) //WHEN Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne) @@ -61,59 +61,50 @@ class MainViewModelTest { //THEN viewModel.initiate(bundle) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(true, it.first) - assertNull(it.second) + + dataPost.observeOnce { + assertEquals(it, responseObject) } } @Test fun initiate_invalidBundleValues_successfulResponse() = runBlocking { //GIVEN - val currencyOne = "corrupted data" - val currencyTwo = "corrupted data again" val bundle = mock(Bundle()::class.java) val error = "Corrupted data found" //WHEN Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne) Mockito.`when`(bundle.getString("parse_2")).thenReturn(currencyTwo) - Mockito.`when`(helper.getDataFromApi(currencyOne, currencyTwo)) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) .doAnswer { throw IOException(error) } //THEN viewModel.initiate(bundle) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(false, it.first) - assertEquals(it.second, error) + + errorPost.observeOnce { + assertEquals(error, it) } } @Test fun initiate_sameBundleValues_successfulResponse() = runBlocking { //GIVEN - val currencyOne = "AUD - Australian Dollar" val bundle = mock(Bundle()::class.java) + val responseObject = mock(CurrencyModel::class.java) //WHEN Mockito.`when`(bundle.getString("parse_1")).thenReturn(null) Mockito.`when`(bundle.getString("parse_2")).thenReturn(null) Mockito.`when`(repository.getConversionPair()).thenReturn(Pair(currencyOne, currencyOne)) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) + .thenReturn(responseObject) //THEN viewModel.initiate(bundle) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(true, it.first) - assertNull(it.second) + + dataPost.observeOnce { + assertEquals(responseObject, it) } } @@ -129,12 +120,9 @@ class MainViewModelTest { //THEN viewModel.initiate(bundle) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(false, it.first) - assertEquals("Select currencies", it.second) + + errorPost.observeOnce { + assertEquals("Select both currencies", it) } } @@ -142,11 +130,9 @@ class MainViewModelTest { @Test fun setCurrencyName_validValues_successResponse() = runBlocking { //GIVEN - val currencyOne = "AUD - Australian Dollar" - val currencyTwo = "GBP - British Pound" viewModel.rateIdTo = currencyTwo val tag = "top" - val responseObject = mock(ResponseObject::class.java) + val responseObject = mock(CurrencyModel::class.java) //WHEN Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) @@ -154,24 +140,18 @@ class MainViewModelTest { //THEN viewModel.setCurrencyName(tag, currencyOne) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(true, it.first) - Log.i("tag", "${it.first} ${it.second}") - assertNull(it.second) + + dataPost.observeOnce { + assertEquals(responseObject, it) } } @Test fun setCurrencyName_sameValues_successfulResponse() = runBlocking { //GIVEN - val currencyOne = "AUD - Australian Dollar" - val currencyTwo = "GBP - British Pound" viewModel.rateIdTo = currencyOne val tag = "top" - val responseObject = mock(ResponseObject::class.java) + val responseObject = mock(CurrencyModel::class.java) //WHEN Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) @@ -179,35 +159,27 @@ class MainViewModelTest { //THEN viewModel.setCurrencyName(tag, currencyOne) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(true, it.first) - assertNull(it.second) + dataPost.observeOnce { + assertEquals(responseObject, it) } } @Test fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking { //GIVEN - val currencyOne = "AUD - Australian Dollar" - val currencyTwo = "GBP - British Pound" + val error = "Data is corrupted" + viewModel.rateIdTo = "corrupted" val tag = "top" - val responseObject = mock(ResponseObject::class.java) + val responseObject = mock(CurrencyModel::class.java) //WHEN - Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) - .thenReturn(responseObject) + Mockito.`when`(repository.getDataFromApi(currencyOne, "corrupted")) + .doAnswer { throw IOException(error) } //THEN viewModel.setCurrencyName(tag, currencyOne) - viewModel.operationStartedListener.observeOnce { - assertEquals(true, it) - } - viewModel.operationFinishedListener.observeOnce { - assertEquals(false, it.first) - assertNotNull(it.second) + errorPost.observeOnce { + assertEquals(error, it) } } diff --git a/app/src/test/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelTest.kt index 9c8dfec..2fa6cbc 100644 --- a/app/src/test/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelTest.kt +++ b/app/src/test/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelTest.kt @@ -2,6 +2,7 @@ package com.appttude.h_mal.easycc.ui.widget import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.appttude.h_mal.easycc.data.repository.Repository +import com.appttude.h_mal.easycc.ui.BaseViewModelTest import com.appttude.h_mal.easycc.utils.observeOnce import org.junit.Assert.* import org.junit.Before @@ -15,21 +16,23 @@ import org.mockito.MockitoAnnotations private const val currencyOne = "AUD - Australian Dollar" private const val currencyTwo = "GBP - British Pound" -class WidgetViewModelTest { +class WidgetViewModelTest : BaseViewModelTest(){ // Run tasks synchronously @get:Rule val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() - lateinit var viewModel: WidgetViewModel + override lateinit var viewModel: WidgetViewModel @Mock lateinit var repository: Repository @Before - fun setUp() { + override fun setUp() { MockitoAnnotations.initMocks(this) viewModel = WidgetViewModel(repository) + + super.setUp() } @Test @@ -75,7 +78,6 @@ class WidgetViewModelTest { //THEN val dialogResult = viewModel.getSubmitDialogMessage() assertEquals(dialogResult, "Create widget for AUDGBP?") - } @Test @@ -86,9 +88,9 @@ class WidgetViewModelTest { //THEN viewModel.submitSelectionOnClick() - viewModel.operationFinishedListener.observeOnce { - assertEquals(it.first, true) - assertNull(it.second) + + dataPost.observeOnce { + assert(it is Unit) } } @@ -100,20 +102,23 @@ class WidgetViewModelTest { //THEN viewModel.submitSelectionOnClick() - viewModel.operationFinishedListener.observeOnce { - assertEquals(it.first, false) - assertNotNull(it.second) + + errorPost.observeOnce { + assertEquals("Selected rates cannot be the same", it) } } @Test fun submitSelectionOnClick_noInput_unsuccessfulResponse() { + //GIVEN + viewModel.rateIdFrom = null + viewModel.rateIdTo = null //THEN viewModel.submitSelectionOnClick() - viewModel.operationFinishedListener.observeOnce { - assertEquals(it.first, false) - assertNotNull(it.second) + + errorPost.observeOnce { + assertEquals("Selections incomplete", it) } } } \ No newline at end of file