Update gradle dependencies (#7)

- Synthetic views replaced with latest view binding
- Hilt DI replaces kodein
- Viewmodel factories removed
- Updated to latest android version
- Migrated to Hilt instrumentation testing
This commit is contained in:
2022-08-08 21:29:25 +01:00
committed by GitHub
parent f9aac8b755
commit d0b2c21c2a
40 changed files with 641 additions and 676 deletions

View File

@@ -1,36 +1,53 @@
version: 2
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
# For a detailed guide to building and testing on Android, read the docs:
# https://circleci.com/docs/2.0/language-android/ for more details.
version: 2.1
# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
# See: https://circleci.com/docs/2.0/orb-intro/
orbs:
android: circleci/android@1.0.3
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
build:
working_directory: ~/code
docker:
- image: circleci/android:api-29
environment:
- CIRCLE_COMPARE_URL: https://github.com/hmalik144/EasyCC_Master/compare/7e995468c9fdc5528a6d1a5489ba301bb9f14c00...14293254ec6d68b93bba50eea79df8808aec9de9
- JVM_OPTS: -Xmx3200m
# Below is the definition of your job to build and test your app, you can rename and customize it as you want.
build-and-test:
# These next lines define the Android machine image executor.
# See: https://circleci.com/docs/2.0/executor-types/
executor:
name: android/android-machine
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Chmod permissions
command: sudo chmod +x ./gradlew
- run:
name: Download Dependencies
command: ./gradlew androidDependencies
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Run Tests
command: ./gradlew lint test
- store_artifacts:
path: app/build/reports
destination: reports
- store_test_results:
path: app/build/test-results
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Chmod permissions
command: sudo chmod +x ./gradlew
- run:
name: Download Dependencies
command: ./gradlew androidDependencies
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Run Tests
command: ./gradlew lint test
- store_artifacts:
path: app/build/reports
destination: reports
- store_test_results:
path: app/build/test-results
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
version: 2
workflow:
sample: # This is the name of the workflow, feel free to change it to better match your workflow.
# Inside the workflow, you define the jobs you want to run.
jobs:
- build
- build-and-test

View File

@@ -1,14 +1,16 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.dagger.hilt.android'
id 'kotlin-kapt'
}
android {
compileSdkVersion 30
compileSdkVersion 32
defaultConfig {
applicationId "com.appttude.h_mal.easycc"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 32
versionCode 5
versionName "4.1"
@@ -20,35 +22,36 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dataBinding {
enabled = true
}
viewBinding.enabled = true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
androidTestImplementation 'androidx.test:rules:1.4.0-beta02'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.annotation:annotation:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.activity:activity-ktx:1.5.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
implementation 'org.jetbrains.kotlin:kotlin-test:1.7.10'
// Coroutines
def coroutines_version = "1.6.2"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
//Retrofit and GSON
def retrofit_ver = "2.8.1"
@@ -56,32 +59,27 @@ dependencies {
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"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//New Material Design
implementation 'com.google.android.material:material:1.1.0-alpha10'
implementation 'com.google.android.material:material:1.6.1'
//Kodein Dependency Injection
implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1"
implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1"
// Hilt dependency injection
def hilt_ver = "2.43.2"
implementation "com.google.dagger:hilt-android:$hilt_ver"
kapt "com.google.dagger:hilt-compiler:$hilt_ver"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_ver"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_ver"
//mockito and livedata testing
testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'androidx.arch.core:core-testing:2.1.0'
//Android Room
implementation "androidx.room:room-runtime:2.2.0-rc01"
implementation "androidx.room:room-ktx:2.2.0-rc01"
kapt "androidx.room:room-compiler:2.2.0-rc01"
implementation "androidx.preference:preference-ktx:1.1.0"
implementation "androidx.preference:preference-ktx:1.2.0"
//mock websever for testing retrofit responses
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
implementation "org.jetbrains.kotlin:kotlin-test:1.7.10"
}

View File

@@ -1,31 +0,0 @@
package com.appttude.h_mal.easycc.application
import android.app.Application
import androidx.test.espresso.idling.CountingIdlingResource
import com.appttude.h_mal.easycc.application.modules.MockRepository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
import com.appttude.h_mal.easycc.ui.main.MainViewModelFactory
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class TestApplication : Application(), KodeinAware {
companion object {
val idlingResources = CountingIdlingResource("Data_loader")
}
// KODEIN DI components declaration
override val kodein by Kodein.lazy {
import(androidXModule(this@TestApplication))
bind() from singleton { MockRepository() }
bind() from singleton { CurrencyDataHelper(instance()) }
bind() from provider { MainViewModelFactory(instance(), instance()) }
}
}

View File

@@ -2,11 +2,18 @@ package com.appttude.h_mal.easycc.application
import android.app.Application
import android.content.Context
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class TestRunner : AndroidJUnitRunner() {
companion object {
val idlingResources = CountingIdlingResource("Data_loader")
}
@Throws(
InstantiationException::class,
IllegalAccessException::class,
@@ -17,6 +24,6 @@ class TestRunner : AndroidJUnitRunner() {
className: String?,
context: Context?
): Application {
return super.newApplication(cl, TestApplication::class.java.name, context)
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

View File

@@ -1,13 +1,14 @@
package com.appttude.h_mal.easycc.application.modules
import com.appttude.h_mal.easycc.application.TestApplication.Companion.idlingResources
import com.appttude.h_mal.easycc.application.TestRunner.Companion.idlingResources
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.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyObject
import kotlinx.coroutines.delay
import javax.inject.Inject
class MockRepository : Repository {
class MockRepository @Inject constructor() : Repository {
override suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject {
idlingResources.increment()

View File

@@ -18,11 +18,11 @@ fun currencyRobot(func: CurrencyRobot.() -> Unit) = CurrencyRobot()
class CurrencyRobot {
fun clickOnTopList() {
Espresso.onView(ViewMatchers.withId(R.id.currency_one)).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.currencyOne)).perform(ViewActions.click())
}
fun clickOnBottomList() {
Espresso.onView(ViewMatchers.withId(R.id.currency_two)).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.currencyTwo)).perform(ViewActions.click())
}
fun searchInCurrencyList(search: String) {

View File

@@ -2,23 +2,47 @@
package com.appttude.h_mal.easycc.ui.main
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import com.appttude.h_mal.easycc.application.modules.MockRepository
import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.data.repository.RepositoryImpl
import com.appttude.h_mal.easycc.di.AppModule
import com.appttude.h_mal.easycc.robots.currencyRobot
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import dagger.hilt.components.SingletonComponent
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
@UninstallModules(AppModule::class)
class MainActivityTest {
@Rule
@JvmField
@get:Rule(order = 0)
var hiltAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
var mActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Before
fun setUp() {
hiltAndroidRule.inject()
}
@Module
@InstallIn(SingletonComponent::class)
object TestAppModule {
@Provides
fun provideRepository(impl: MockRepository): Repository {
return impl
}
}
@Test
fun mainActivityTest() {
currencyRobot {

View File

@@ -20,7 +20,8 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<receiver android:name="com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin">
<receiver android:name="com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@@ -30,12 +31,14 @@
android:resource="@xml/currency_app_widget_info" />
</receiver>
<activity android:name="com.appttude.h_mal.easycc.ui.widget.CurrencyAppWidgetConfigureActivityKotlin">
<activity android:name="com.appttude.h_mal.easycc.ui.widget.CurrencyAppWidgetConfigureActivityKotlin"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity android:name="com.appttude.h_mal.easycc.ui.main.MainActivity">
<activity android:name="com.appttude.h_mal.easycc.ui.main.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -1,42 +1,7 @@
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
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
import dagger.hilt.android.HiltAndroidApp
class AppClass : Application(), KodeinAware {
// KODEIN DI components declaration
override val kodein by Kodein.lazy {
import(androidXModule(this@AppClass))
bind() from singleton { NetworkConnectionInterceptor(instance()) }
bind() from singleton { loggingInterceptor() }
bind() from singleton { QueryInterceptor(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 singleton { CurrencyDataHelper(instance()) }
bind() from singleton { WidgetHelper(instance(), instance()) }
bind() from provider { MainViewModelFactory(instance(), instance()) }
bind() from provider { WidgetViewModelFactory(instance()) }
}
}
@HiltAndroidApp
class AppClass : Application()

View File

@@ -0,0 +1,3 @@
package com.appttude.h_mal.easycc.data.network.api
interface Api

View File

@@ -13,7 +13,7 @@ import retrofit2.http.Query
/**
* Retrofit Network class to currency api calls
*/
interface BackupCurrencyApi {
interface BackupCurrencyApi : Api {
@GET("latest?")
suspend fun getCurrencyRate(
@@ -21,24 +21,4 @@ interface BackupCurrencyApi {
@Query("to") currencyTo: String
): Response<CurrencyResponse>
companion object {
operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor,
interceptor: HttpLoggingInterceptor
): BackupCurrencyApi {
val okkHttpclient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.addNetworkInterceptor(networkConnectionInterceptor)
.build()
return Retrofit.Builder()
.client(okkHttpclient)
.baseUrl("https://api.frankfurter.app/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(BackupCurrencyApi::class.java)
}
}
}

View File

@@ -14,32 +14,10 @@ import retrofit2.http.Query
/**
* Retrofit Network class to currency api calls
*/
interface CurrencyApi {
interface CurrencyApi : Api{
// Get rate from server with arguments passed in Repository
@GET("convert?")
suspend fun getCurrencyRate(@Query("q") currency: String): Response<ResponseObject>
// interface invokation to be used in application class
companion object {
operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor,
queryInterceptor: QueryInterceptor,
interceptor: HttpLoggingInterceptor
): CurrencyApi {
val okkHttpclient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(queryInterceptor)
.addNetworkInterceptor(networkConnectionInterceptor)
.build()
return Retrofit.Builder()
.client(okkHttpclient)
.baseUrl("https://free.currencyconverterapi.com/api/v3/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CurrencyApi::class.java)
}
}
}

View File

@@ -0,0 +1,23 @@
package com.appttude.h_mal.easycc.data.network.api
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject
class RemoteDataSource @Inject constructor(){
fun <Api> buildApi(
okkHttpclient: OkHttpClient,
baseUrl: String,
api: Class<Api>
): Api {
return Retrofit.Builder()
.client(okkHttpclient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(api)
}
}

View File

@@ -4,17 +4,19 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import javax.inject.Inject
/**
* Interceptor used in network classes to check network status
*
*/
@Suppress("DEPRECATION")
class NetworkConnectionInterceptor(
context: Context
class NetworkConnectionInterceptor @Inject constructor(
@ApplicationContext context: Context
) : Interceptor {
private val applicationContext = context.applicationContext
@@ -27,13 +29,12 @@ class NetworkConnectionInterceptor(
}
private fun isInternetAvailable(): Boolean {
var result = false
val connectivityManager =
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
return connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.run {
when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
else -> false
@@ -41,7 +42,7 @@ class NetworkConnectionInterceptor(
}
} else {
it.activeNetworkInfo?.run {
result = when (type) {
when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
@@ -49,8 +50,7 @@ class NetworkConnectionInterceptor(
}
}
}
}
return result
} ?: false
}
}

View File

@@ -2,17 +2,19 @@ package com.appttude.h_mal.easycc.data.network.interceptors
import android.content.Context
import com.appttude.h_mal.easycc.R
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import javax.inject.Inject
/**
* Interceptor used in CurrencyApi
* Adds apiKey to query parameters
*/
class QueryInterceptor(
val context: Context
class QueryInterceptor @Inject constructor(
@ApplicationContext val context: Context
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {

View File

@@ -6,18 +6,18 @@ import com.appttude.h_mal.easycc.models.CurrencyObject
import com.google.gson.annotations.SerializedName
data class ResponseObject(
@field:SerializedName("query")
var query: Any? = null,
@field:SerializedName("results")
var results: Map<String, CurrencyObject>? = null
@field:SerializedName("query")
var query: Any? = null,
@field:SerializedName("results")
var results: Map<String, CurrencyObject>? = null
) : CurrencyModelInterface {
override fun getCurrencyModel(): CurrencyModel {
val res = results?.iterator()?.next()?.value
return CurrencyModel(
res?.fr,
res?.to,
res?.value ?: 0.0
res?.fr,
res?.to,
res?.value ?: 0.0
)
}
}

View File

@@ -4,6 +4,8 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.appttude.h_mal.easycc.R
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
/**
* Shared prefs class used for storing conversion name values as pairs
@@ -13,7 +15,7 @@ import com.appttude.h_mal.easycc.R
private const val CURRENCY_ONE = "conversion_one"
private const val CURRENCY_TWO = "conversion_two"
class PreferenceProvider(context: Context) {
class PreferenceProvider @Inject constructor(@ApplicationContext context: Context) {
private val appContext = context.applicationContext

View File

@@ -9,11 +9,12 @@ 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
import javax.inject.Inject
/**
* Default implementation of [Repository]. Single entry point for managing currency' data.
*/
class RepositoryImpl(
class RepositoryImpl @Inject constructor(
private val api: CurrencyApi,
private val backUpApi: BackupCurrencyApi,
private val prefs: PreferenceProvider

View File

@@ -0,0 +1,60 @@
package com.appttude.h_mal.easycc.di
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.RemoteDataSource
import com.appttude.h_mal.easycc.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.data.repository.RepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor()
@Provides
fun provideOkHttpclient(
interceptor: HttpLoggingInterceptor,
networkConnectionInterceptor: NetworkConnectionInterceptor,
) = OkHttpClient.Builder()
.addInterceptor(interceptor)
.addNetworkInterceptor(networkConnectionInterceptor)
.build()
@Provides
fun provideCurrencyApi(
remoteDataSource: RemoteDataSource,
okHttpClient: OkHttpClient
): CurrencyApi {
return remoteDataSource.buildApi(
okHttpClient,
"https://free.currencyconverterapi.com/api/v3/",
CurrencyApi::class.java
)
}
@Provides
fun provideBackupCurrencyApi(
remoteDataSource: RemoteDataSource,
okHttpClient: OkHttpClient
): BackupCurrencyApi {
return remoteDataSource.buildApi(
okHttpClient,
"https://api.frankfurter.app/",
BackupCurrencyApi::class.java
)
}
@Provides
fun provideRepository(impl: RepositoryImpl): Repository {
return impl
}
}

View File

@@ -2,8 +2,9 @@ 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(
class CurrencyDataHelper @Inject constructor(
val repository: Repository
) {

View File

@@ -3,8 +3,9 @@ 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 javax.inject.Inject
class WidgetHelper(
class WidgetHelper @Inject constructor(
private val helper: CurrencyDataHelper,
val repository: Repository
) {

View File

@@ -7,8 +7,10 @@ import android.text.Editable
import android.text.TextWatcher
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
import kotlinx.android.synthetic.main.custom_dialog.*
/**
* Custom dialog when selecting currencies from list with filter
@@ -35,10 +37,11 @@ class CustomDialogClass(
android.R.layout.simple_list_item_1
)
val list_view = findViewById<ListView>(R.id.list_view)
list_view.adapter = arrayAdapter
// Edit text to filter @arrayAdapter
search_text.addTextChangedListener(object : TextWatcher {
findViewById<TextView>(R.id.search_text).addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
arrayAdapter.filter.filter(charSequence)

View File

@@ -6,77 +6,66 @@ import android.text.TextWatcher
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProviders
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.ActivityMainBinding
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 kotlinx.android.synthetic.main.activity_main.*
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.generic.instance
import dagger.hilt.android.AndroidEntryPoint
@Suppress("DEPRECATION")
class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener {
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), View.OnClickListener {
// Retrieve MainViewModelFactory via dependency injection
override val kodein by kodein()
private val factory: MainViewModelFactory by instance()
lateinit var viewModel: MainViewModel
private val viewModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keyboard is not overlapping views
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
/*
* Prevent keyboard overlapping views
*/
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)
// Bind viewmodel to layout with view binding
DataBindingUtil
.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
.apply {
viewmodel = viewModel
lifecycleOwner = this@MainActivity
}
viewModel.initiate(intent.extras)
binding.currencyOne.text = viewModel.rateIdTo
binding.currencyTwo.text = viewModel.rateIdFrom
setUpListeners()
setUpObservers()
}
private fun setUpObservers() {
viewModel.operationStartedListener.observe(this, {
progressBar.hideView(false)
})
viewModel.operationFinishedListener.observe(this, { pair ->
viewModel.operationStartedListener.observe(this) {
binding.progressBar.hideView(false)
}
viewModel.operationFinishedListener.observe(this) { pair ->
// hide progress bar
progressBar.hideView(true)
binding.progressBar.hideView(true)
if (pair.first) {
// Operation was successful remove text in EditTexts
bottomInsertValues.clearEditText()
topInsertValue.clearEditText()
binding.bottomInsertValues.clearEditText()
binding.topInsertValue.clearEditText()
} else {
// Display Toast with error message returned from Viewmodel
pair.second?.let { displayToast(it) }
}
})
}
}
private fun setUpListeners() {
topInsertValue.addTextChangedListener(textWatcherClass)
bottomInsertValues.addTextChangedListener(textWatcherClass2)
binding.topInsertValue.addTextChangedListener(textWatcherClass)
binding.bottomInsertValues.addTextChangedListener(textWatcherClass2)
currency_one.setOnClickListener(this)
currency_two.setOnClickListener(this)
binding.currencyOne.setOnClickListener(this)
binding.currencyTwo.setOnClickListener(this)
}
private fun showCustomDialog(view: View?) {
@@ -97,32 +86,32 @@ class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener {
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
bottomInsertValues.removeTextChangedListener(textWatcherClass2)
binding.bottomInsertValues.removeTextChangedListener(textWatcherClass2)
// Clear any values if current EditText is empty
if (topInsertValue.text.isNullOrEmpty())
bottomInsertValues.setText("")
if (binding.topInsertValue.text.isNullOrEmpty())
binding.bottomInsertValues.setText("")
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable) {
bottomInsertValues.setText(viewModel.getConversion(s.toString()))
binding.bottomInsertValues.setText(viewModel.getConversion(s.toString()))
// add Text watcher back as it is safe to do so
bottomInsertValues.addTextChangedListener(textWatcherClass2)
binding.bottomInsertValues.addTextChangedListener(textWatcherClass2)
}
}
private val textWatcherClass2: TextWatcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) {
topInsertValue.removeTextChangedListener(textWatcherClass)
if (bottomInsertValues.text.isNullOrEmpty())
topInsertValue.clearEditText()
binding.topInsertValue.removeTextChangedListener(textWatcherClass)
if (binding.bottomInsertValues.text.isNullOrEmpty())
binding.topInsertValue.clearEditText()
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable) {
topInsertValue.setText(viewModel.getReciprocalConversion(s.toString()))
topInsertValue.addTextChangedListener(textWatcherClass)
binding.topInsertValue.setText(viewModel.getReciprocalConversion(s.toString()))
binding.topInsertValue.addTextChangedListener(textWatcherClass)
}
}

View File

@@ -2,20 +2,25 @@ 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.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
/**
* ViewModel for the task Main Activity Screen
*/
class MainViewModel(
@HiltViewModel
class MainViewModel @Inject constructor(
private val currencyDataHelper: CurrencyDataHelper,
private val repository: Repository
) : ViewModel() {
@@ -48,7 +53,7 @@ class MainViewModel(
return
}
CoroutineScope(Dispatchers.IO).launch {
viewModelScope.launch {
try {
// Non-null assertion (!!) as values have been null checked and have not changed
val exchangeResponse = currencyDataHelper.getDataFromApi(

View File

@@ -1,21 +0,0 @@
package com.appttude.h_mal.easycc.ui.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
/**
* Viewmodel factory for [MainViewModel]
* inject repository into viewmodel
*/
@Suppress("UNCHECKED_CAST")
class MainViewModelFactory(
private val repository: Repository,
private val helper: CurrencyDataHelper
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(helper, repository) as T
}
}

View File

@@ -6,38 +6,32 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProviders
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding
import com.appttude.h_mal.easycc.ui.main.ClickListener
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 kotlinx.android.synthetic.main.currency_app_widget_configure.*
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.generic.instance
import dagger.hilt.android.AndroidEntryPoint
/**
* The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget.
*/
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAware,
@AndroidEntryPoint
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
View.OnClickListener {
override val kodein by kodein()
private val factory: WidgetViewModelFactory by instance()
val viewModel: WidgetViewModel by viewModels()
var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
companion object {
lateinit var viewModel: WidgetViewModel
}
private lateinit var binding: CurrencyAppWidgetConfigureBinding
private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
public override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)
binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root)
// Set the result to CANCELED. This will cause the widget host to cancel
// out of the widget placement if the user presses the back button.
@@ -57,23 +51,20 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
return
}
// ViewModel setup
viewModel = ViewModelProviders.of(this, factory).get(WidgetViewModel::class.java)
viewModel.initiate(mAppWidgetId)
setupDataBinding()
setupObserver()
setupClickListener()
}
private fun setupClickListener() {
submit_widget.setOnClickListener(this)
currency_one.setOnClickListener(this)
currency_two.setOnClickListener(this)
binding.submitWidget.setOnClickListener(this)
binding.currencyOne.setOnClickListener(this)
binding.currencyTwo.setOnClickListener(this)
}
private fun setupObserver() {
viewModel.operationFinishedListener.observe(this, {
viewModel.operationFinishedListener.observe(this) {
// it.first is a the success of the operation
if (it.first) {
@@ -82,17 +73,6 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
// failed operation - display toast with message from it.second
it.second?.let { message -> displayToast(message) }
}
})
}
private fun setupDataBinding() {
// data binding to @R.layout.currency_app_widget_configure
DataBindingUtil.setContentView<CurrencyAppWidgetConfigureBinding>(
this,
R.layout.currency_app_widget_configure
).apply {
viewmodel = viewModel
lifecycleOwner = this@CurrencyAppWidgetConfigureActivityKotlin
}
}

View File

@@ -4,7 +4,8 @@ import android.app.Dialog
import android.content.Context
import android.os.Bundle
import com.appttude.h_mal.easycc.R
import kotlinx.android.synthetic.main.confirm_dialog.*
import com.appttude.h_mal.easycc.databinding.ActivityMainBinding
import com.appttude.h_mal.easycc.databinding.ConfirmDialogBinding
/**
@@ -17,19 +18,22 @@ class WidgetSubmitDialog(
private val dialogInterface: DialogSubmit
) : Dialog(context) {
private lateinit var binding: ConfirmDialogBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.confirm_dialog)
binding = ConfirmDialogBinding.inflate(layoutInflater)
setContentView(binding.root)
// layer behind dialog to be transparent
window!!.setBackgroundDrawableResource(android.R.color.transparent)
// Dialog cannot be cancelled by clicking away
setCancelable(false)
confirm_text.text = messageString
binding.confirmText.text = messageString
// handle dialog buttons
confirm_yes.setOnClickListener { dialogInterface.onSubmit() }
confirm_no.setOnClickListener { dismiss() }
binding.confirmYes.setOnClickListener { dialogInterface.onSubmit() }
binding.confirmNo.setOnClickListener { dismiss() }
}
}

View File

@@ -4,8 +4,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.utils.trimToThree
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class WidgetViewModel(
@HiltViewModel
class WidgetViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {

View File

@@ -1,15 +0,0 @@
package com.appttude.h_mal.easycc.ui.widget
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.easycc.data.repository.RepositoryImpl
@Suppress("UNCHECKED_CAST")
class WidgetViewModelFactory(
private val repository: RepositoryImpl
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return WidgetViewModel(repository) as T
}
}

View File

@@ -6,21 +6,19 @@ import android.content.Context
import android.content.Intent
import com.appttude.h_mal.easycc.helper.WidgetHelper
import com.appttude.h_mal.easycc.widget.WidgetServiceIntent.Companion.enqueueWork
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Implementation of App Widget functionality.
* App Widget Configuration implemented in [CurrencyAppWidgetKotlin]
*/
@AndroidEntryPoint
class CurrencyAppWidgetKotlin : AppWidgetProvider() {
//DI with kodein to use in CurrencyAppWidgetKotlin
private val kodein = LateInitKodein()
private val repository: WidgetHelper by kodein.instance()
@Inject
lateinit var helper: WidgetHelper
//update trigger either on timed update or from from first start
override fun onUpdate(
@@ -33,10 +31,9 @@ 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.removeWidgetData(appWidgetId)
helper.removeWidgetData(appWidgetId)
}
super.onDeleted(context, appWidgetIds)
}

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.easycc.widget
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
@@ -11,22 +13,20 @@ import com.appttude.h_mal.easycc.R
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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
import javax.inject.Inject
@AndroidEntryPoint
class WidgetServiceIntent : JobIntentService() {
//DI with kodein to use in CurrencyAppWidgetKotlin
private val kodein = LateInitKodein()
private val repository: WidgetHelper by kodein.instance()
@Inject
lateinit var helper: WidgetHelper
override fun onHandleWork(intent: Intent) {
kodein.baseKodein = (application as KodeinAware).kodein
val appWidgetManager = AppWidgetManager.getInstance(this)
val thisAppWidget = ComponentName(packageName, CurrencyAppWidgetKotlin::class.java.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
@@ -45,7 +45,7 @@ class WidgetServiceIntent : JobIntentService() {
val views = RemoteViews(context.packageName, R.layout.currency_app_widget)
CoroutineScope(Dispatchers.Main).launch {
val exchangeResponse = repository.getWidgetData()
val exchangeResponse = helper.getWidgetData()
exchangeResponse?.let {
val titleString = "${it.from}${it.to}"
@@ -63,7 +63,7 @@ class WidgetServiceIntent : JobIntentService() {
val configPendingIntent =
PendingIntent.getActivity(
context, appWidgetId, clickIntentTemplate,
PendingIntent.FLAG_UPDATE_CURRENT
PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_view, configPendingIntent)
}
@@ -87,14 +87,14 @@ class WidgetServiceIntent : JobIntentService() {
context,
appWidgetId,
updateIntent,
PendingIntent.FLAG_UPDATE_CURRENT
FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
)
}
private fun clickingIntent(
context: Context
): Intent {
val pair = repository.repository.getConversionPair()
val pair = helper.repository.getConversionPair()
val s1 = pair.first
val s2 = pair.second
return Intent(context, MainActivity::class.java).apply {

View File

@@ -1,126 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_background"
android:focusable="false"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".ui.main.MainActivity">
<data>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<variable
name="viewmodel"
type="com.appttude.h_mal.easycc.ui.main.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_background"
android:focusable="false"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".ui.main.MainActivity">
<RelativeLayout
android:layout_width="0dp"
<LinearLayout
android:id="@+id/whole_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="18dp"
android:orientation="vertical">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<TextView
android:id="@+id/currencyOne"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:autoSizeMaxTextSize="12dp"
android:tag="top"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
<EditText
android:id="@+id/topInsertValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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" />
</LinearLayout>
<LinearLayout
android:id="@+id/whole_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="18dp"
android:orientation="vertical">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<TextView
android:id="@+id/currency_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:autoSizeMaxTextSize="12dp"
android:tag="top"
android:text="@={viewmodel.rateIdFrom}"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
<EditText
android:id="@+id/topInsertValue"
<TextView
android:id="@+id/currencyTwo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<LinearLayout
<EditText
android:id="@+id/bottomInsertValues"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<TextView
android:id="@+id/currency_two"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="bottom"
android:text="@={viewmodel.rateIdTo}"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
<EditText
android:id="@+id/bottomInsertValues"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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" />
</LinearLayout>
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" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/colour_four"
android:indeterminateTintMode="src_atop"
android:visibility="gone" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</layout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/colour_four"
android:indeterminateTintMode="src_atop"
android:visibility="gone" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,128 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_background"
android:focusable="false"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".ui.main.MainActivity">
<data>
<variable
name="viewmodel"
type="com.appttude.h_mal.easycc.ui.main.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_background"
android:focusable="false"
android:focusableInTouchMode="true"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="9dp"
android:orientation="vertical"
tools:context=".ui.main.MainActivity">
app:layout_constraintBottom_toTopOf="@id/middle"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintWidth_percent=".9">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
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">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<TextView
android:id="@+id/currency_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="top"
android:text="@={viewmodel.rateIdFrom}"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
<EditText
android:id="@+id/topInsertValue"
<TextView
android:id="@+id/currencyOne"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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" />
</LinearLayout>
android:layout_margin="12dp"
android:tag="top"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
<android.widget.Space
android:id="@+id/middle"
<EditText
android:id="@+id/topInsertValue"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintWidth_percent=".9"
android:layout_marginTop="9dp"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/middle">
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" />
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<TextView
android:id="@+id/currency_two"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="bottom"
android:text="@={viewmodel.rateIdTo}"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<EditText
android:id="@+id/bottomInsertValues"
<android.widget.Space
android:id="@+id/middle"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/middle"
app:layout_constraintWidth_percent=".9">
<androidx.cardview.widget.CardView style="@style/cardview_theme">
<TextView
android:id="@+id/currencyTwo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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" />
android:layout_margin="12dp"
android:tag="bottom"
android:textColor="@color/colour_five"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
<EditText
android:id="@+id/bottomInsertValues"
android:layout_width="match_parent"
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>
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" />
</layout>
</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>

View File

@@ -19,7 +19,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/confirm_text"
android:id="@+id/confirmText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
@@ -36,7 +36,7 @@
android:orientation="horizontal">
<TextView
android:id="@+id/confirm_yes"
android:id="@+id/confirmYes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
@@ -48,7 +48,7 @@
android:textColor="@color/colour_five" />
<TextView
android:id="@+id/confirm_no"
android:id="@+id/confirmNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"

View File

@@ -1,83 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.appttude.h_mal.easycc.ui.widget.WidgetViewModel" />
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="false"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".ui.widget.CurrencyAppWidgetConfigureActivityKotlin">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:focusable="false"
android:focusableInTouchMode="true"
tools:context=".ui.widget.CurrencyAppWidgetConfigureActivityKotlin">
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="12dp">
<RelativeLayout
<LinearLayout
android:id="@+id/whole_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="12dp">
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/whole_view">
<androidx.cardview.widget.CardView
style="@style/cardview_theme"
android:layout_margin="11dp">
<androidx.cardview.widget.CardView
style="@style/cardview_theme"
android:layout_margin="11dp">
<TextView
android:id="@+id/currency_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="top"
android:text="@={viewmodel.rateIdFrom}"
android:textColor="@color/colour_five"
android:textSize="18sp"
tools:text="Currency One" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/currencyOne"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="top"
android:textColor="@color/colour_five"
android:textSize="18sp"
tools:text="Currency One" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/cardview_theme"
android:layout_margin="11dp">
<androidx.cardview.widget.CardView
style="@style/cardview_theme"
android:layout_margin="11dp">
<TextView
android:id="@+id/currency_two"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="bottom"
android:text="@={viewmodel.rateIdTo}"
android:textColor="@color/colour_five"
android:textSize="18sp"
tools:text="Currency Two" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<TextView
android:layout_marginEnd="22dp"
android:id="@+id/submit_widget"
android:tag="submit"
android:padding="12dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/whole_view"
android:layout_alignParentEnd="true"
android:textColor="@color/colour_five"
android:text="Submit" />
</RelativeLayout>
<TextView
android:id="@+id/currencyTwo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:tag="bottom"
android:textColor="@color/colour_five"
android:textSize="18sp"
tools:text="Currency Two" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<TextView
android:id="@+id/submitWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/whole_view"
android:layout_alignParentEnd="true"
android:layout_marginEnd="22dp"
android:padding="12dp"
android:tag="submit"
android:text="Submit"
android:textColor="@color/colour_five" />
</RelativeLayout>
</layout>
</RelativeLayout>

View File

@@ -6,7 +6,9 @@ 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.utils.MainCoroutineRule
import com.appttude.h_mal.easycc.utils.observeOnce
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Before
@@ -23,6 +25,9 @@ class MainViewModelTest {
@get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
lateinit var viewModel: MainViewModel
@Mock

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.easycc.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.rules.TestWatcher
import org.junit.runner.Description
@ExperimentalCoroutinesApi
class MainCoroutineRule(private val dispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}

View File

@@ -1,25 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.10'
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }
}
allprojects {
repositories {
jcenter()
google()
}
plugins {
id 'com.android.application' version '7.2.2' apply false
id 'com.android.library' version '7.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
id 'com.google.dagger.hilt.android' version '2.43.2' apply false
}
task clean(type: Delete) {

View File

@@ -1,6 +1,6 @@
#Sat Jun 12 22:27:25 BST 2021
#Thu Aug 04 22:17:29 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "EasyCC"
include ':app'