diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index 7944245..d87fd9a 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -33,9 +33,10 @@ diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 0899032..13229d6 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 88ea3aa..3cc336b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,22 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index d04819c..99e86f7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -39,7 +39,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index 2a2ba60..60f4be0 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,8 +2,8 @@ - - + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b0436be..99c9da3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,13 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId "com.appttude.h_mal.easycc" - minSdkVersion 23 - targetSdkVersion 29 - versionCode 2 - versionName "1.1" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 5 + versionName "4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -24,6 +24,15 @@ android { } viewBinding.enabled = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + } dependencies { @@ -40,8 +49,10 @@ dependencies { testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" //Retrofit and GSON - implementation 'com.squareup.retrofit2:retrofit:2.6.0' - implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + def retrofit_ver = "2.8.1" + implementation "com.squareup.retrofit2:retrofit:$retrofit_ver" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" //Kotlin Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/MainActivityTest.java b/app/src/androidTest/java/com/appttude/h_mal/easycc/MainActivityTest.java deleted file mode 100644 index dff6430..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/easycc/MainActivityTest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.appttude.h_mal.easycc; - - -public class MainActivityTest { - - - -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt b/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt index 1634132..af25f94 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt @@ -1,11 +1,15 @@ package com.appttude.h_mal.easycc.application import android.app.Application +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.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.easycc.data.network.interceptors.QueryInterceptor +import com.appttude.h_mal.easycc.data.network.interceptors.loggingInterceptor import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.repository.RepositoryImpl +import com.appttude.h_mal.easycc.helper.CurrencyDataHelper +import com.appttude.h_mal.easycc.helper.WidgetHelper import com.appttude.h_mal.easycc.ui.main.MainViewModelFactory import com.appttude.h_mal.easycc.ui.widget.WidgetViewModelFactory import org.kodein.di.Kodein @@ -18,17 +22,20 @@ import org.kodein.di.generic.singleton class AppClass : Application(), KodeinAware { - // Kodein Dependecy Injection created in Application class + // Kodein Dependecy Injection singletons and providers created override val kodein by Kodein.lazy { import(androidXModule(this@AppClass)) - // instance() can be context or other binding created bind() from singleton { NetworkConnectionInterceptor(instance()) } + bind() from singleton { loggingInterceptor() } bind() from singleton { QueryInterceptor(instance()) } - bind() from singleton { CurrencyApi(instance(),instance()) } + bind() from singleton { CurrencyApi(instance(), instance(), instance()) } + bind() from singleton { BackupCurrencyApi(instance(),instance()) } bind() from singleton { PreferenceProvider(instance()) } bind() from singleton { RepositoryImpl(instance(), instance(), instance()) } - bind() from provider { MainViewModelFactory(instance()) } + bind() from singleton { CurrencyDataHelper(instance()) } + bind() from singleton { WidgetHelper(instance(), instance()) } + bind() from provider { MainViewModelFactory(instance(), instance()) } bind() from provider { WidgetViewModelFactory(instance()) } } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/SafeApiRequest.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/SafeApiRequest.kt index 601f2a4..d152aca 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/SafeApiRequest.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/SafeApiRequest.kt @@ -43,6 +43,8 @@ abstract class SafeApiRequest { .append(errorMessageString) .toString() } + print(log) + // Log.e("Api Response Error", log) //return error message @@ -58,7 +60,7 @@ abstract class SafeApiRequest { //extract ["error"] from error body return JSONObject(it).getString("error") } catch (e: JSONException) { - Log.e(TAG, e.message) + e.printStackTrace() } } return null 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 new file mode 100644 index 0000000..20a0224 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt @@ -0,0 +1,49 @@ +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 + +/** + * Retrofit2 Network class to create network requests + */ +interface BackupCurrencyApi { + + // Get rate from server with arguments passed in Repository + @GET("latest?") + suspend fun getCurrencyRate( + @Query("from") currencyFrom: String, + @Query("to") currencyTo: String + ): Response + + // interface invokation to be used in application class + companion object{ + operator fun invoke( + networkConnectionInterceptor: NetworkConnectionInterceptor, + interceptor: HttpLoggingInterceptor + ) : BackupCurrencyApi{ + + val okkHttpclient = OkHttpClient.Builder() + .addInterceptor(interceptor) + .addNetworkInterceptor(networkConnectionInterceptor) + .build() + + // Build retrofit + return Retrofit.Builder() + .client(okkHttpclient) + .baseUrl("https://api.frankfurter.app/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(BackupCurrencyApi::class.java) + } + } + + + +} \ No newline at end of file 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 6ead166..e08daa9 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 @@ -4,6 +4,7 @@ import com.appttude.h_mal.easycc.data.network.interceptors.NetworkConnectionInte 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 @@ -22,26 +23,25 @@ interface CurrencyApi { // interface invokation to be used in application class companion object{ operator fun invoke( - networkConnectionInterceptor: NetworkConnectionInterceptor, - queryInterceptor: QueryInterceptor + networkConnectionInterceptor: NetworkConnectionInterceptor, + queryInterceptor: QueryInterceptor, + interceptor: HttpLoggingInterceptor ) : CurrencyApi{ // okkHttpclient with injected interceptors val okkHttpclient = OkHttpClient.Builder() - .addInterceptor(queryInterceptor) - .addNetworkInterceptor(networkConnectionInterceptor) - .build() + .addInterceptor(interceptor) + .addInterceptor(queryInterceptor) + .addNetworkInterceptor(networkConnectionInterceptor) + .build() // Build retrofit return Retrofit.Builder() - .client(okkHttpclient) - .baseUrl("https://free.currencyconverterapi.com/api/v3/") - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(CurrencyApi::class.java) + .client(okkHttpclient) + .baseUrl("https://free.currencyconverterapi.com/api/v3/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(CurrencyApi::class.java) } } - - - } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/LoggingInterceptor.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/LoggingInterceptor.kt new file mode 100644 index 0000000..115ebfa --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/LoggingInterceptor.kt @@ -0,0 +1,10 @@ +package com.appttude.h_mal.easycc.data.network.interceptors + +import okhttp3.logging.HttpLoggingInterceptor + +fun loggingInterceptor() = run { + val httpLoggingInterceptor = HttpLoggingInterceptor() + httpLoggingInterceptor.apply { + httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt index 166d70e..5a3b06b 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt @@ -18,7 +18,7 @@ class QueryInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val original: Request = chain.request() - val originalHttpUrl: HttpUrl = original.url() + val originalHttpUrl: HttpUrl = original.url val url = originalHttpUrl.newBuilder() .addQueryParameter("apiKey", context.getString(R.string.apiKey)) 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 new file mode 100644 index 0000000..f1efcc0 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/CurrencyResponse.kt @@ -0,0 +1,30 @@ +package com.appttude.h_mal.easycc.data.network.response + +import com.appttude.h_mal.easycc.models.CurrencyModel +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 +): CurrencyModelInterface { + + override fun getCurrencyModel(): CurrencyModel { + return CurrencyModel( + base, + rates?.iterator()?.next()?.key, + rates?.iterator()?.next()?.value ?: 0.0 + ) + } + +} diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt index 0d301e5..862d615 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt @@ -1,11 +1,24 @@ package com.appttude.h_mal.easycc.data.network.response +import com.appttude.h_mal.easycc.models.CurrencyModel +import com.appttude.h_mal.easycc.models.CurrencyModelInterface import com.appttude.h_mal.easycc.models.CurrencyObject import com.google.gson.annotations.SerializedName -class ResponseObject( - @SerializedName("query") - var query : Any, - @SerializedName("results") - var results : Map? -) \ No newline at end of file +data class ResponseObject( + @field:SerializedName("query") + var query : Any? = null, + @field:SerializedName("results") + var results : Map? = null +): CurrencyModelInterface { + + override fun getCurrencyModel(): CurrencyModel { + val res = results?.iterator()?.next()?.value + return CurrencyModel( + res?.fr, + res?.to, + res?.value ?: 0.0 + ) + } + +} \ No newline at end of file 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 48931b7..37287e8 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,5 +1,6 @@ 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 /** @@ -7,13 +8,15 @@ import com.appttude.h_mal.easycc.data.network.response.ResponseObject */ interface Repository { - suspend fun getData(fromCurrency: String, toCurrency: String): ResponseObject + suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject + + suspend fun getBackupDataFromApi(fromCurrency: String, toCurrency: String): CurrencyResponse fun getConversionPair(): Pair fun setConversionPair(fromCurrency: String, toCurrency: String) - fun getArrayList(): Array + fun getCurrenciesList(): Array fun getWidgetConversionPairs(appWidgetId: Int): 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 e08e90d..68e1e75 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 @@ -1,9 +1,11 @@ package com.appttude.h_mal.easycc.data.repository -import android.content.Context +import android.content.res.Resources 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.utils.convertPairsListToString @@ -12,20 +14,27 @@ import com.appttude.h_mal.easycc.utils.convertPairsListToString * Default implementation of [Repository]. Single entry point for managing currency' data. */ class RepositoryImpl ( - private val api: CurrencyApi, - private val prefs: PreferenceProvider, - context: Context + private val api: CurrencyApi, + private val backUpApi: BackupCurrencyApi, + private val prefs: PreferenceProvider ):Repository, SafeApiRequest(){ - private val appContext = context.applicationContext - - override suspend fun getData(fromCurrency: String, toCurrency: String + 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)} + } + override fun getConversionPair(): Pair { return prefs.getConversionPair() } @@ -34,8 +43,8 @@ class RepositoryImpl ( prefs.saveConversionPair(fromCurrency, toCurrency) } - override fun getArrayList(): Array = - appContext.resources.getStringArray(R.array.currency_arrays) + override fun getCurrenciesList(): Array = + Resources.getSystem().getStringArray(R.array.currency_arrays) override fun getWidgetConversionPairs(appWidgetId: Int): Pair = prefs.getWidgetConversionPair(appWidgetId) 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 new file mode 100644 index 0000000..d2b0027 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt @@ -0,0 +1,19 @@ +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 java.lang.Exception + +class CurrencyDataHelper ( + 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 new file mode 100644 index 0000000..e6b0230 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt @@ -0,0 +1,30 @@ +package com.appttude.h_mal.easycc.helper + +import com.appttude.h_mal.easycc.data.repository.Repository +import com.appttude.h_mal.easycc.models.CurrencyModel +import com.appttude.h_mal.easycc.utils.trimToThree + +import kotlin.Exception + +class WidgetHelper ( + val helper: CurrencyDataHelper, + val repository: Repository +){ + + suspend fun getWidgetData(): CurrencyModel? { + try { + val pair = repository.getConversionPair() + val s1 = pair.first?.trimToThree() ?: return null + val s2 = pair.second?.trimToThree() ?: return null + + return helper.getDataFromApi(s1, s2).getCurrencyModel() + }catch (e: Exception){ + e.printStackTrace() + return null + } + } + + fun removeWidgetData(id: Int){ + repository.removeWidgetConversionPairs(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyModel.kt new file mode 100644 index 0000000..5030c33 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/models/CurrencyModel.kt @@ -0,0 +1,11 @@ +package com.appttude.h_mal.easycc.models + +data class CurrencyModel( + val from: String?, + val to: String?, + var rate: Double = 0.0 +) + +interface CurrencyModelInterface{ + fun getCurrencyModel(): CurrencyModel +} \ 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 a8c6902..0f25586 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 @@ -22,31 +22,30 @@ import org.kodein.di.generic.instance class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener { - override val kodein by kodein() // Retrieve MainViewModelFactory via dependency injection + override val kodein by kodein() private val factory: MainViewModelFactory by instance() - companion object { - lateinit var viewModel: MainViewModel - } + lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Keyboard is not overlapping views - this.window.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN or - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN or + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + ) viewModel = ViewModelProviders.of(this, factory) - .get(MainViewModel::class.java) + .get(MainViewModel::class.java) // Bind viewmodel to layout with view binding DataBindingUtil - .setContentView(this, R.layout.activity_main) - .apply { - viewmodel = viewModel - lifecycleOwner = this@MainActivity - } + .setContentView(this, R.layout.activity_main) + .apply { + viewmodel = viewModel + lifecycleOwner = this@MainActivity + } viewModel.initiate(intent.extras) @@ -56,24 +55,23 @@ class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener { private fun setUpObservers() { viewModel.operationStartedListener.observe(this, Observer { - // Show progress bar progressBar.hideView(false) }) viewModel.operationFinishedListener.observe(this, Observer { pair -> // hide progress bar progressBar.hideView(true) - if (pair.first){ + if (pair.first) { // Operation was successful remove text in EditTexts bottomInsertValues.clearEditText() topInsertValue.clearEditText() - }else{ + } else { // Display Toast with error message returned from Viewmodel pair.second?.let { displayToast(it) } } }) } - private fun setUpListeners(){ + private fun setUpListeners() { topInsertValue.addTextChangedListener(textWatcherClass) bottomInsertValues.addTextChangedListener(textWatcherClass2) 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 ad99ad6..1be09d6 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,11 +1,12 @@ package com.appttude.h_mal.easycc.ui.main import android.os.Bundle -import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.appttude.h_mal.easycc.data.repository.Repository +import com.appttude.h_mal.easycc.helper.CurrencyDataHelper import com.appttude.h_mal.easycc.utils.toTwoDpString +import com.appttude.h_mal.easycc.utils.trimToThree import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -14,9 +15,8 @@ import java.io.IOException /** * ViewModel for the task Main Activity Screen */ -private const val TAG = "MainViewModel" class MainViewModel( - // Repository injected via Viewmodel factory + private val currencyDataHelper: CurrencyDataHelper, private val repository: Repository ) : ViewModel(){ @@ -35,7 +35,7 @@ class MainViewModel( private fun getExchangeRate(){ operationStartedListener.postValue(true) - // Null check on currency values + // view binded exchange rates selected null checked if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()){ operationFinishedListener.postValue(Pair(false, "Select currencies")) return @@ -48,18 +48,19 @@ class MainViewModel( return } - // Open Coroutine on IO thread to carry out async task CoroutineScope(Dispatchers.IO).launch { try { // Non-null assertion (!!) as values have been null checked and have not changed - val exchangeResponse = repository.getData(rateIdFrom!!, rateIdTo!!) + val exchangeResponse = currencyDataHelper.getDataFromApi( + rateIdFrom!!.trimToThree(), + rateIdTo!!.trimToThree() + ) - exchangeResponse.results?.iterator()?.next()?.value?.let { - // Response Successful and contains @param CurrencyObject + exchangeResponse.getCurrencyModel().let { + conversionRate = it.rate repository.setConversionPair(rateIdFrom!!, rateIdTo!!) operationFinishedListener.postValue(Pair(true, null)) - conversionRate = it.value return@launch } }catch(e: IOException){ diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt index 15a2d4f..242e7e8 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt @@ -3,6 +3,7 @@ package com.appttude.h_mal.easycc.ui.main import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.appttude.h_mal.easycc.data.repository.RepositoryImpl +import com.appttude.h_mal.easycc.helper.CurrencyDataHelper /** * Viewmodel factory for [MainViewModel] @@ -10,10 +11,11 @@ import com.appttude.h_mal.easycc.data.repository.RepositoryImpl */ @Suppress("UNCHECKED_CAST") class MainViewModelFactory ( - private val repository: RepositoryImpl + private val repository: RepositoryImpl, + private val helper: CurrencyDataHelper ): ViewModelProvider.NewInstanceFactory(){ override fun create(modelClass: Class): T { - return MainViewModel(repository) as T + return MainViewModel(helper, repository) as T } } \ No newline at end of file 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 1443614..831aa30 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 @@ -9,7 +9,7 @@ class WidgetViewModel( private val repository: Repository ) : ViewModel(){ - private val defaultCurrency: String by lazy { repository.getArrayList()[0] } + private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] } var appWidgetId: Int? = null // data binding to @R.layout.currency_app_widget_configure diff --git a/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt b/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt index 6215cf8..b78e062 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt @@ -1,5 +1,6 @@ package com.appttude.h_mal.easycc.widget +import android.app.Activity import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider @@ -8,9 +9,8 @@ import android.content.Context import android.content.Intent import android.util.Log import android.widget.RemoteViews -import android.widget.Toast import com.appttude.h_mal.easycc.R -import com.appttude.h_mal.easycc.data.repository.RepositoryImpl +import com.appttude.h_mal.easycc.helper.WidgetHelper import com.appttude.h_mal.easycc.ui.main.MainActivity import com.appttude.h_mal.easycc.utils.transformIntToArray import kotlinx.coroutines.CoroutineScope @@ -19,7 +19,6 @@ import kotlinx.coroutines.launch import org.kodein.di.KodeinAware import org.kodein.di.LateInitKodein import org.kodein.di.generic.instance -import java.io.IOException /** @@ -27,15 +26,21 @@ import java.io.IOException * App Widget Configuration implemented in [CurrencyAppWidgetConfigureActivityKotlin] */ private const val TAG = "CurrencyAppWidgetKotlin" -class CurrencyAppWidgetKotlin : AppWidgetProvider() { + +class CurrencyAppWidgetKotlin : AppWidgetProvider() { //DI with kodein to use in CurrencyAppWidgetKotlin private val kodein = LateInitKodein() - private val repository : RepositoryImpl by kodein.instance() + private val repository: WidgetHelper by kodein.instance() //update trigger either on timed update or from from first start - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - Log.i(TAG,"onUpdate() appWidgetIds = ${appWidgetIds.size}") + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + kodein.baseKodein = (context.applicationContext as KodeinAware).kodein + Log.i(TAG, "onUpdate() appWidgetIds = ${appWidgetIds.size}") // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) @@ -44,75 +49,66 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() { } override fun onDeleted(context: Context, appWidgetIds: IntArray) { + kodein.baseKodein = (context.applicationContext as KodeinAware).kodein // When the user deletes the widget, delete the preference associated with it. for (appWidgetId in appWidgetIds) { - repository.removeWidgetConversionPairs(appWidgetId) + repository.removeWidgetData(appWidgetId) } super.onDeleted(context, appWidgetIds) } override fun onEnabled(context: Context) { + kodein.baseKodein = (context.applicationContext as KodeinAware).kodein // Enter relevant functionality for when the first widget is created AppWidgetManager.getInstance(context).apply { - val thisAppWidget = ComponentName(context.packageName, CurrencyAppWidgetKotlin::class.java.name) + val thisAppWidget = + ComponentName(context.packageName, CurrencyAppWidgetKotlin::class.java.name) val appWidgetIds = getAppWidgetIds(thisAppWidget) onUpdate(context, this, appWidgetIds) } super.onEnabled(context) } - override fun onReceive(context: Context?, intent: Intent?) { - if (context == null){ return } - kodein.baseKodein = (context.applicationContext as KodeinAware).kodein - - super.onReceive(context, intent) - } - override fun onDisabled(context: Context) { kodein.baseKodein = (context.applicationContext as KodeinAware).kodein // Enter relevant functionality for when the last widget is disabled } - private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { - val stringList = repository.getWidgetConversionPairs(appWidgetId) - val s1 = stringList.first - val s2 = stringList.second - + private fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { // Construct the RemoteViews object val views = RemoteViews(context.packageName, R.layout.currency_app_widget) CoroutineScope(Dispatchers.Main).launch { - try { - val response = repository.getData(s1!!.substring(0,3),s2!!.substring(0,3)) + val exchangeResponse = repository.getWidgetData() - response.results?.iterator()?.next()?.value?.let { - val titleString = "${it.fr}${it.to}" - views.setTextViewText(R.id.exchangeName, titleString) - views.setTextViewText(R.id.exchangeRate, it.value.toString()) - } - }catch (io : IOException){ - Log.i("WidgetClass",io.message ?: "Failed") - Toast.makeText(context,io.message, Toast.LENGTH_LONG).show() - }finally { - setUpdateIntent(context, appWidgetId).let { + exchangeResponse?.let { + val titleString = "${it.from}${it.to}" + views.setTextViewText(R.id.exchangeName, titleString) + views.setTextViewText(R.id.exchangeRate, it.rate.toString()) + + setUpdateIntent(context, appWidgetId).let { intent -> //set the pending intent to the icon views.setImageViewResource(R.id.refresh_icon, R.drawable.ic_refresh_white_24dp) - views.setOnClickPendingIntent(R.id.refresh_icon, it) + views.setOnClickPendingIntent(R.id.refresh_icon, intent) } - val clickIntentTemplate = clickingIntent(context, s1, s2) + val clickIntentTemplate = clickingIntent(context) val configPendingIntent = - PendingIntent.getActivity( + PendingIntent.getActivity( context, appWidgetId, clickIntentTemplate, - PendingIntent.FLAG_UPDATE_CURRENT) - + PendingIntent.FLAG_UPDATE_CURRENT + ) views.setOnClickPendingIntent(R.id.widget_view, configPendingIntent) - - // Instruct the widget manager to update the widget - appWidgetManager.updateAppWidget(appWidgetId, views) } + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views) } } @@ -120,22 +116,26 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() { private fun setUpdateIntent(context: Context, appWidgetId: Int): PendingIntent? { //Create update intent for refresh icon val updateIntent = Intent( - context, CurrencyAppWidgetKotlin::class.java).apply { + context, CurrencyAppWidgetKotlin::class.java + ).apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, transformIntToArray(appWidgetId)) } //add previous intent to this pending intent return PendingIntent.getBroadcast( - context, - appWidgetId, - updateIntent, - PendingIntent.FLAG_UPDATE_CURRENT) + context, + appWidgetId, + updateIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) } private fun clickingIntent( - context: Context, - s1: String?, s2: String? + context: Context ): Intent { + val pair = repository.repository.getConversionPair() + val s1 = pair.first + val s2 = pair.second return Intent(context, MainActivity::class.java).apply { action = Intent.ACTION_MAIN addCategory(Intent.CATEGORY_LAUNCHER) @@ -144,5 +144,27 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) } } + + private fun clickingIntent( + context: Context, + activity: Class, + vararg argPairs: Pair + ): Intent { + + return Intent(context, activity::class.java).apply { + action = Intent.ACTION_MAIN + addCategory(Intent.CATEGORY_LAUNCHER) + argPairs.forEach { + putExtra(it.first, it.second) + } + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + } + + private fun Intent.putExtra(s: String, second: T?) { + when(second){ + is String -> putExtra(s,second) + } + } } diff --git a/app/src/main/res/drawable/ic_background.xml b/app/src/main/res/drawable/ic_background.xml new file mode 100644 index 0000000..e50aa13 --- /dev/null +++ b/app/src/main/res/drawable/ic_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout-v26/activity_main.xml b/app/src/main/res/layout-v26/activity_main.xml new file mode 100644 index 0000000..41d762e --- /dev/null +++ b/app/src/main/res/layout-v26/activity_main.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b04fe20..9b778d7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,119 +1,128 @@ + - - + app:layout_constraintWidth_percent=".9" + android:layout_marginBottom="9dp" + android:orientation="vertical" + app:layout_constraintBottom_toTopOf="@id/middle" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent"> - + + + + + + + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:background="@drawable/round_edit_text" + android:ems="10" + android:hint="insert value one" + android:inputType="numberDecimal" + android:padding="12dp" + android:selectAllOnFocus="true" + android:tag="from" + android:textColorHighlight="#608d91" /> - + + + + + + + + + + - - - - + android:layout_margin="12dp" + android:tag="bottom" + android:text="@={viewmodel.rateIdTo}" + android:textColor="@color/colour_five" + android:textSize="18sp" /> + - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="6dp" + android:layout_weight="7" + android:background="@drawable/round_edit_text" + android:ems="10" + android:hint="insert value two" + android:inputType="numberDecimal" + android:padding="12dp" + android:selectAllOnFocus="true" + android:tag="to" + android:textColorHighlight="#608d91" /> + + + + + diff --git a/app/src/main/res/layout/currency_app_widget.xml b/app/src/main/res/layout/currency_app_widget.xml index 42be33e..393e531 100644 --- a/app/src/main/res/layout/currency_app_widget.xml +++ b/app/src/main/res/layout/currency_app_widget.xml @@ -7,17 +7,17 @@ android:orientation="vertical" android:background="#4D000000"> + + + android:layout_height="wrap_content"> + tools:text="0.52646215" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 17566b6..015a0af 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,7 +6,7 @@ @color/colour_two @color/colour_two @color/colour_two - @drawable/gradient_colour + @drawable/ic_background false true @color/colour_five 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 fd5fb81..44319e6 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,7 +1,8 @@ package com.appttude.h_mal.easycc.repository -import android.content.Context +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.data.repository.Repository @@ -29,31 +30,31 @@ class RepositoryNetworkTest{ @Mock lateinit var api: CurrencyApi @Mock - lateinit var prefs: PreferenceProvider + lateinit var apiBackup: BackupCurrencyApi @Mock - lateinit var context: Context + lateinit var prefs: PreferenceProvider @Before fun setUp() { MockitoAnnotations.initMocks(this) - repository = RepositoryImpl(api, prefs, context) + repository = RepositoryImpl(api, apiBackup, prefs) } @Test fun getRateFromApi_positiveResponse() = runBlocking { //GIVEN - Create query string - val s1 = "AUD - Australian Dollar" - val s2 = "GBP - British Pound" - val query = convertPairsListToString(s1, s2) + val s1 = "AUD" + val s2 = "GBP" //create a successful retrofit response val mockCurrencyResponse = mock(ResponseObject::class.java) val re = Response.success(mockCurrencyResponse) //WHEN - loginApiRequest to return a successful response - Mockito.`when`(api.getCurrencyRate(query)).thenReturn(re) + val currencyPair = convertPairsListToString(s1, s2) + Mockito.`when`(api.getCurrencyRate(currencyPair)).thenReturn(re) //THEN - the unwrapped login response contains the correct values - val currencyResponse = repository.getData(s1,s2) + val currencyResponse = repository.getDataFromApi(s1,s2) assertNotNull(currencyResponse) assertEquals(currencyResponse, mockCurrencyResponse) } @@ -61,20 +62,21 @@ class RepositoryNetworkTest{ @Test fun loginUser_negativeResponse() = runBlocking { //GIVEN - val s1 = "AUD - Australian Dollar" - val s2 = "GBP - British Pound" - val query = convertPairsListToString(s1, s2) + val s1 = "AUD" + val s2 = "GBP" + //mock retrofit error response val mockBody = mock(ResponseBody::class.java) val mockRaw = mock(okhttp3.Response::class.java) val re = Response.error(mockBody, mockRaw) //WHEN - Mockito.`when`(api.getCurrencyRate(query)).thenAnswer { re } + val currencyPair = convertPairsListToString(s1, s2) + Mockito.`when`(api.getCurrencyRate(currencyPair)).thenAnswer { re } //THEN - assert exception is not null val ioExceptionReturned = assertFailsWith { - repository.getData(s1, s2) + repository.getDataFromApi(s1, s2) } assertNotNull(ioExceptionReturned) assertNotNull(ioExceptionReturned.message) diff --git a/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryStorageTest.kt b/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryStorageTest.kt index 1e2a630..0b39a44 100644 --- a/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryStorageTest.kt +++ b/app/src/test/java/com/appttude/h_mal/easycc/repository/RepositoryStorageTest.kt @@ -1,6 +1,7 @@ package com.appttude.h_mal.easycc.repository import android.content.Context +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.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.repository.Repository @@ -19,14 +20,14 @@ class RepositoryStorageTest { @Mock lateinit var api: CurrencyApi @Mock - lateinit var prefs: PreferenceProvider + lateinit var apiBackup: BackupCurrencyApi @Mock - lateinit var context: Context + lateinit var prefs: PreferenceProvider @Before fun setUp() { MockitoAnnotations.initMocks(this) - repository = RepositoryImpl(api, prefs, context) + repository = RepositoryImpl(api, apiBackup, prefs) } @Test 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 32a2bc0..4b8378d 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 @@ -5,7 +5,7 @@ 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.models.CurrencyObject +import com.appttude.h_mal.easycc.helper.CurrencyDataHelper import kotlinx.coroutines.runBlocking import org.junit.Before import com.appttude.h_mal.easycc.utils.observeOnce @@ -16,7 +16,6 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations -import kotlin.time.seconds class MainViewModelTest { @@ -29,10 +28,13 @@ class MainViewModelTest { @Mock lateinit var repository: Repository + @Mock + lateinit var helper: CurrencyDataHelper + @Before fun setUp() { MockitoAnnotations.initMocks(this) - viewModel = MainViewModel(repository) + viewModel = MainViewModel(helper, repository) } @Test @@ -46,7 +48,7 @@ class MainViewModelTest { //WHEN Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne) Mockito.`when`(bundle.getString("parse_2")).thenReturn(currencyTwo) - Mockito.`when`(repository.getData(currencyOne, currencyTwo)).thenReturn(responseObject) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) //THEN viewModel.initiate(bundle) @@ -55,7 +57,6 @@ class MainViewModelTest { } viewModel.operationFinishedListener.observeOnce { assertEquals(true, it.first) - Log.i("tag", "${it.first} ${it.second}") assertNull(it.second) } } @@ -70,7 +71,7 @@ class MainViewModelTest { //WHEN Mockito.`when`(repository.getConversionPair()).thenReturn(pair) - Mockito.`when`(repository.getData(currencyOne, currencyTwo)).thenReturn(responseObject) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) //THEN viewModel.initiate(null) @@ -138,7 +139,7 @@ class MainViewModelTest { val responseObject = mock(ResponseObject::class.java) //WHEN - Mockito.`when`(repository.getData(currencyOne, currencyTwo)).thenReturn(responseObject) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) //THEN viewModel.setCurrencyName(tag, currencyOne) @@ -162,7 +163,7 @@ class MainViewModelTest { val responseObject = mock(ResponseObject::class.java) //WHEN - Mockito.`when`(repository.getData(currencyOne, currencyTwo)).thenReturn(responseObject) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) //THEN viewModel.setCurrencyName(tag, currencyOne) @@ -184,7 +185,7 @@ class MainViewModelTest { val responseObject = mock(ResponseObject::class.java) //WHEN - Mockito.`when`(repository.getData(currencyOne, currencyTwo)).thenReturn(responseObject) + Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) //THEN viewModel.setCurrencyName(tag, currencyOne) 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 cb2a155..7bb9d8a 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 @@ -57,7 +57,7 @@ class WidgetViewModelTest { Mockito.`when`(repository.getWidgetConversionPairs(appId)).thenAnswer { pair } Mockito.`when`(repository.getWidgetConversionPairs(appId).first).thenReturn(null) Mockito.`when`(repository.getWidgetConversionPairs(appId).second).thenReturn(null) - Mockito.`when`(repository.getArrayList()).thenReturn(array) + Mockito.`when`(repository.getCurrenciesList()).thenReturn(array) //THEN viewModel.initiate(123)