Issue resolved

Took 2 hours 5 minutes
This commit is contained in:
2022-08-27 23:19:26 +01:00
parent 14c45b87ca
commit fd4cd93f78
23 changed files with 278 additions and 204 deletions

View File

@@ -1,12 +1,7 @@
package com.appttude.h_mal.easycc.data.network.api 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 com.appttude.h_mal.easycc.data.network.response.CurrencyResponse
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query

View File

@@ -1,13 +1,7 @@
package com.appttude.h_mal.easycc.data.network.api 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 com.appttude.h_mal.easycc.data.network.response.ResponseObject
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query

View File

@@ -1,16 +1,13 @@
package com.appttude.h_mal.easycc.data.repository package com.appttude.h_mal.easycc.data.repository
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.data.network.response.ResponseObject
/** /**
* Main entry point for accessing currency data. * Main entry point for accessing currency data.
*/ */
interface Repository { interface Repository {
suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): CurrencyModel
suspend fun getBackupDataFromApi(fromCurrency: String, toCurrency: String): CurrencyResponse
fun getConversionPair(): Pair<String?, String?> fun getConversionPair(): Pair<String?, String?>

View File

@@ -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.SafeApiRequest
import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi 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.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.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.utils.convertPairsListToString import com.appttude.h_mal.easycc.utils.convertPairsListToString
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
/** /**
@@ -23,17 +23,19 @@ class RepositoryImpl @Inject constructor(
override suspend fun getDataFromApi( override suspend fun getDataFromApi(
fromCurrency: String, fromCurrency: String,
toCurrency: String toCurrency: String
): ResponseObject { ): CurrencyModel {
return try {
// Set currency pairs as correct string for api query eg. AUD_GBP // Set currency pairs as correct string for api query eg. AUD_GBP
val currencyPair = convertPairsListToString(fromCurrency, toCurrency) val currencyPair = convertPairsListToString(fromCurrency, toCurrency)
return responseUnwrap { api.getCurrencyRate(currencyPair) } responseUnwrap { api.getCurrencyRate(currencyPair) }.getCurrencyModel()
} catch (e: IOException) {
responseUnwrap {
backUpApi.getCurrencyRate(
fromCurrency,
toCurrency
)
}.getCurrencyModel()
} }
override suspend fun getBackupDataFromApi(
fromCurrency: String,
toCurrency: String
): CurrencyResponse {
return responseUnwrap { backUpApi.getCurrencyRate(fromCurrency, toCurrency) }
} }
override fun getConversionPair(): Pair<String?, String?> { override fun getConversionPair(): Pair<String?, String?> {

View File

@@ -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)
}
}
}

View File

@@ -6,7 +6,6 @@ import com.appttude.h_mal.easycc.utils.trimToThree
import javax.inject.Inject import javax.inject.Inject
class WidgetHelper @Inject constructor( class WidgetHelper @Inject constructor(
private val helper: CurrencyDataHelper,
val repository: Repository val repository: Repository
) { ) {
@@ -16,7 +15,7 @@ class WidgetHelper @Inject constructor(
val s1 = pair.first?.trimToThree() ?: return null val s1 = pair.first?.trimToThree() ?: return null
val s2 = pair.second?.trimToThree() ?: return null val s2 = pair.second?.trimToThree() ?: return null
return helper.getDataFromApi(s1, s2).getCurrencyModel() return repository.getDataFromApi(s1, s2)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return null return null

View File

@@ -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<V : BaseViewModel> : 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 <A : AppCompatActivity> startActivity(activity: Class<A>) {
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()
}
}

View File

@@ -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<ViewState> = MutableLiveData()
fun onStart() {
uiState.postValue(ViewState.HasStarted)
}
fun <T : Any> onSuccess(result: T) {
uiState.postValue(ViewState.HasData(Event(result)))
}
protected fun onError(error: String) {
uiState.postValue(ViewState.HasError(Event(error)))
}
}

View File

@@ -9,7 +9,6 @@ import android.view.WindowManager
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ListView import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.SearchView
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
/** /**
@@ -18,7 +17,7 @@ import com.appttude.h_mal.easycc.R
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class CustomDialogClass( class CustomDialogClass(
context: Context, context: Context,
private val clickListener: ClickListener val onSelect: (String) -> Unit
) : Dialog(context) { ) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -30,7 +29,6 @@ class CustomDialogClass(
// Keyboard not to overlap dialog // Keyboard not to overlap dialog
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
// array adapter for list of currencies in R.Strings
val arrayAdapter = val arrayAdapter =
ArrayAdapter.createFromResource( ArrayAdapter.createFromResource(
context, R.array.currency_arrays, context, R.array.currency_arrays,
@@ -52,13 +50,8 @@ class CustomDialogClass(
// interface selection back to calling activity // interface selection back to calling activity
list_view.setOnItemClickListener { adapterView, _, i, _ -> list_view.setOnItemClickListener { adapterView, _, i, _ ->
clickListener.onText(adapterView.getItemAtPosition(i).toString()) onSelect.invoke(adapterView.getItemAtPosition(i).toString())
dismiss() dismiss()
} }
} }
} }
// Interface to handle selection within dialog
interface ClickListener {
fun onText(currencyName: String)
}

View File

@@ -7,18 +7,16 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels 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.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.clearEditText
import com.appttude.h_mal.easycc.utils.displayToast
import com.appttude.h_mal.easycc.utils.hideView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(), View.OnClickListener { class MainActivity : BaseActivity<MainViewModel>(), View.OnClickListener {
private val viewModel: MainViewModel by viewModels() override val viewModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -29,34 +27,22 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
* Prevent keyboard overlapping views * Prevent keyboard overlapping views
*/ */
window.setSoftInputMode( window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
) )
viewModel.initiate(intent.extras) viewModel.initiate(intent.extras)
binding.currencyOne.text = viewModel.rateIdTo binding.currencyOne.text = viewModel.rateIdFrom
binding.currencyTwo.text = viewModel.rateIdFrom binding.currencyTwo.text = viewModel.rateIdTo
setUpListeners() setUpListeners()
setUpObservers()
} }
private fun setUpObservers() { override fun onSuccess(data: Any?) {
viewModel.operationStartedListener.observe(this) { super.onSuccess(data)
binding.progressBar.hideView(false) if (data is CurrencyModel) {
}
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.bottomInsertValues.clearEditText()
binding.topInsertValue.clearEditText() binding.topInsertValue.clearEditText()
} else {
// Display Toast with error message returned from Viewmodel
pair.second?.let { displayToast(it) }
}
} }
} }
@@ -69,25 +55,21 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
private fun showCustomDialog(view: View?) { private fun showCustomDialog(view: View?) {
CustomDialogClass(this, object : ClickListener { CustomDialogClass(this) {
override fun onText(currencyName: String) { (view as TextView).text = it
(view as TextView).text = currencyName viewModel.setCurrencyName(view.tag, it)
viewModel.setCurrencyName(view.tag, currencyName) }.show()
}
}).show()
} }
override fun onClick(view: View?) { override fun onClick(view: View?) {
showCustomDialog(view) showCustomDialog(view)
} }
// Text watcher applied to EditText @topInsertValue
private val textWatcherClass: TextWatcher = object : TextWatcher { private val textWatcherClass: TextWatcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) { override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) {
// Remove text watcher on other text watcher to prevent infinite loop // Remove text watcher on other text watcher to prevent infinite loop
binding.bottomInsertValues.removeTextChangedListener(textWatcherClass2) binding.bottomInsertValues.removeTextChangedListener(textWatcherClass2)
// Clear any values if current EditText is empty
if (binding.topInsertValue.text.isNullOrEmpty()) if (binding.topInsertValue.text.isNullOrEmpty())
binding.bottomInsertValues.setText("") binding.bottomInsertValues.setText("")
} }
@@ -102,7 +84,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
private val textWatcherClass2: TextWatcher = object : TextWatcher { private val textWatcherClass2: TextWatcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) { override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) {
binding.topInsertValue.removeTextChangedListener(textWatcherClass) binding.topInsertValue.removeTextChangedListener(textWatcherClass)
if (binding.bottomInsertValues.text.isNullOrEmpty()) if (binding.bottomInsertValues.text.isNullOrEmpty())
binding.topInsertValue.clearEditText() binding.topInsertValue.clearEditText()
@@ -115,4 +96,5 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
} }
} }

View File

@@ -1,17 +1,12 @@
package com.appttude.h_mal.easycc.ui.main package com.appttude.h_mal.easycc.ui.main
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.appttude.h_mal.easycc.data.repository.Repository 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.toTwoDpString
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@@ -21,61 +16,49 @@ import javax.inject.Inject
*/ */
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val currencyDataHelper: CurrencyDataHelper,
private val repository: Repository private val repository: Repository
) : ViewModel() { ) : BaseViewModel() {
private val conversionPairs by lazy { repository.getConversionPair() } private val conversionPairs by lazy { repository.getConversionPair() }
// Viewbinding to textviews in @activity_main.xml // Viewbinded variables
var rateIdFrom: String? = null var rateIdFrom: String? = null
var rateIdTo: String? = null var rateIdTo: String? = null
//operation results livedata based on outcome of operation
val operationStartedListener = MutableLiveData<Boolean>()
val operationFinishedListener = MutableLiveData<Pair<Boolean, String?>>()
private var conversionRate: Double = 1.00 private var conversionRate: Double = 1.00
private fun getExchangeRate() { private fun getExchangeRate() {
operationStartedListener.postValue(true) onStart()
// view binded exchange rates selected null checked
if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()) { if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()) {
operationFinishedListener.postValue(Pair(false, "Select currencies")) onError("Select both currencies")
return return
} }
// No need to call api as it will return exchange rate as 1
if (rateIdFrom == rateIdTo) { if (rateIdFrom == rateIdTo) {
conversionRate = 1.00 conversionRate = 1.00
operationFinishedListener.postValue(Pair(true, null)) onError("Currency selections are the same")
return return
} }
viewModelScope.launch { viewModelScope.launch {
try { try {
// Non-null assertion (!!) as values have been null checked and have not changed // Non-null assertion (!!) as values have been null checked and have not changed
val exchangeResponse = currencyDataHelper.getDataFromApi( val exchangeResponse = repository.getDataFromApi(
rateIdFrom!!.trimToThree(), rateIdFrom!!.trimToThree(),
rateIdTo!!.trimToThree() rateIdTo!!.trimToThree()
) )
exchangeResponse.getCurrencyModel().let { exchangeResponse.let {
conversionRate = it.rate conversionRate = it.rate
repository.setConversionPair(rateIdFrom!!, rateIdTo!!) repository.setConversionPair(rateIdFrom!!, rateIdTo!!)
operationFinishedListener.postValue(Pair(true, null)) onSuccess(it)
return@launch return@launch
} }
} catch (e: IOException) { } catch (e: IOException) {
e.message?.let { e.message?.let { onError(it) }
operationFinishedListener.postValue(Pair(false, it))
return@launch
} }
} }
operationFinishedListener.postValue(Pair(false, "Failed to retrieve rate"))
}
} }
fun getConversion(fromValue: String): String? { fun getConversion(fromValue: String): String? {

View File

@@ -7,11 +7,9 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding 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.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.utils.transformIntToArray
import com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin import com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -20,16 +18,16 @@ import dagger.hilt.android.AndroidEntryPoint
* The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget. * The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), class CurrencyAppWidgetConfigureActivityKotlin : BaseActivity<WidgetViewModel>(),
View.OnClickListener { View.OnClickListener {
val viewModel: WidgetViewModel by viewModels() override val viewModel: WidgetViewModel by viewModels()
private lateinit var binding: CurrencyAppWidgetConfigureBinding private lateinit var binding: CurrencyAppWidgetConfigureBinding
private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
public override fun onCreate(icicle: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(icicle) super.onCreate(savedInstanceState)
binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater) binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -50,10 +48,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
finish() finish()
return return
} }
viewModel.initiate(mAppWidgetId) viewModel.initiate(mAppWidgetId)
setupObserver()
setupClickListener() setupClickListener()
} }
@@ -63,17 +59,9 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
binding.currencyTwo.setOnClickListener(this) binding.currencyTwo.setOnClickListener(this)
} }
private fun setupObserver() { override fun onSuccess(data: Any?) {
viewModel.operationFinishedListener.observe(this) { super.onSuccess(data)
// it.first is a the success of the operation
if (it.first) {
displaySubmitDialog() displaySubmitDialog()
} else {
// failed operation - display toast with message from it.second
it.second?.let { message -> displayToast(message) }
}
}
} }
override fun onClick(view: View?) { override fun onClick(view: View?) {
@@ -98,12 +86,10 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
private fun showCustomDialog(view: View?) { private fun showCustomDialog(view: View?) {
CustomDialogClass(this, object : ClickListener { CustomDialogClass(this) {
override fun onText(currencyName: String) { (view as TextView).text = it
(view as TextView).text = currencyName viewModel.setCurrencyName(view.tag, it)
viewModel.setCurrencyName(view.tag, currencyName) }.show()
}
}).show()
} }
fun finishCurrencyWidgetActivity() { fun finishCurrencyWidgetActivity() {

View File

@@ -3,8 +3,6 @@ package com.appttude.h_mal.easycc.ui.widget
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle 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 import com.appttude.h_mal.easycc.databinding.ConfirmDialogBinding

View File

@@ -1,8 +1,7 @@
package com.appttude.h_mal.easycc.ui.widget 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.data.repository.Repository
import com.appttude.h_mal.easycc.ui.BaseViewModel
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@@ -10,7 +9,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class WidgetViewModel @Inject constructor( class WidgetViewModel @Inject constructor(
private val repository: Repository private val repository: Repository
) : ViewModel() { ) : BaseViewModel() {
private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] } private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] }
var appWidgetId: Int? = null var appWidgetId: Int? = null
@@ -20,7 +19,6 @@ class WidgetViewModel @Inject constructor(
var rateIdTo: String? = null var rateIdTo: String? = null
// Live data to feedback to @CurrencyAppWidgetConfigureActivityKotlin // Live data to feedback to @CurrencyAppWidgetConfigureActivityKotlin
val operationFinishedListener = MutableLiveData<Pair<Boolean, String?>>()
// Setup viewmodel app widget ID // Setup viewmodel app widget ID
// Set default values for text views // Set default values for text views
@@ -43,15 +41,14 @@ class WidgetViewModel @Inject constructor(
fun submitSelectionOnClick() { fun submitSelectionOnClick() {
if (rateIdTo == null || rateIdFrom == null) { if (rateIdTo == null || rateIdFrom == null) {
operationFinishedListener.value = Pair(false, "Selections incomplete") onError("Selections incomplete")
return return
} }
if (rateIdFrom == rateIdTo) { if (rateIdFrom == rateIdTo) {
operationFinishedListener.value = onError("Selected rates cannot be the same ${rateIdFrom}${rateIdTo}")
Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}")
return return
} }
operationFinishedListener.value = Pair(true, null) onSuccess(Unit)
} }
fun setWidgetStored() { fun setWidgetStored() {

View File

@@ -0,0 +1,24 @@
package com.appttude.h_mal.easycc.utils
open class Event<out T>(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
}

View File

@@ -0,0 +1,7 @@
package com.appttude.h_mal.easycc.utils
sealed class ViewState {
object HasStarted : ViewState()
class HasData<T : Any>(val data: Event<T>) : ViewState()
class HasError(val error: Event<String>) : ViewState()
}

View File

@@ -2,21 +2,34 @@ package com.appttude.h_mal.easycc.utils
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AnimRes
fun EditText.clearEditText() { fun EditText.clearEditText() {
this.setText("") this.setText("")
} }
fun View.hideView(vis: Boolean) {
visibility = if (vis) {
View.GONE
} else {
View.VISIBLE
}
}
fun Context.displayToast(message: String) { fun Context.displayToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show() 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
}

View File

@@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="1080dp"
android:height="192dp" android:height="1920dp"
android:viewportWidth="1080" android:viewportWidth="1080"
android:viewportHeight="1920"> android:viewportHeight="1920">
<path android:pathData="M0,0L1080,0L1080,1920L0,1920L0,0Z"> <path android:pathData="M0,0L1080,0L1080,1920L0,1920L0,0Z">

View File

@@ -99,17 +99,4 @@
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateTint="@color/colour_four"
android:indeterminateTintMode="src_atop"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#4D000000">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="center"
android:elevation="0.2dp"
android:indeterminateTint="@color/colour_four"
android:indeterminateTintMode="src_atop"/>
</FrameLayout>