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: jobs:
build: # Below is the definition of your job to build and test your app, you can rename and customize it as you want.
working_directory: ~/code build-and-test:
docker: # These next lines define the Android machine image executor.
- image: circleci/android:api-29 # See: https://circleci.com/docs/2.0/executor-types/
environment: executor:
- CIRCLE_COMPARE_URL: https://github.com/hmalik144/EasyCC_Master/compare/7e995468c9fdc5528a6d1a5489ba301bb9f14c00...14293254ec6d68b93bba50eea79df8808aec9de9 name: android/android-machine
- JVM_OPTS: -Xmx3200m
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run: - run:
name: Chmod permissions name: Chmod permissions
command: sudo chmod +x ./gradlew command: sudo chmod +x ./gradlew
- run: - run:
name: Download Dependencies name: Download Dependencies
command: ./gradlew androidDependencies command: ./gradlew androidDependencies
- save_cache: - save_cache:
paths: paths:
- ~/.gradle - ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run: - run:
name: Run Tests name: Run Tests
command: ./gradlew lint test command: ./gradlew lint test
- store_artifacts: - store_artifacts:
path: app/build/reports path: app/build/reports
destination: reports destination: reports
- store_test_results: - store_test_results:
path: app/build/test-results path: app/build/test-results
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows: workflows:
version: 2 sample: # This is the name of the workflow, feel free to change it to better match your workflow.
workflow: # Inside the workflow, you define the jobs you want to run.
jobs: jobs:
- build - build-and-test

View File

@@ -1,14 +1,16 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' id 'com.android.application'
apply plugin: 'kotlin-android-extensions' id 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-kapt' id 'com.google.dagger.hilt.android'
id 'kotlin-kapt'
}
android { android {
compileSdkVersion 30 compileSdkVersion 32
defaultConfig { defaultConfig {
applicationId "com.appttude.h_mal.easycc" applicationId "com.appttude.h_mal.easycc"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 32
versionCode 5 versionCode 5
versionName "4.1" versionName "4.1"
@@ -20,35 +22,36 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
dataBinding {
enabled = true
}
viewBinding.enabled = true
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
} }
} }
dependencies { dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.core:core-ktx:1.8.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.annotation:annotation:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
testImplementation 'junit:junit:4.12' implementation 'androidx.activity:activity-ktx:1.5.1'
androidTestImplementation 'androidx.test:runner:1.2.0' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0-beta02' 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 //Retrofit and GSON
def retrofit_ver = "2.8.1" def retrofit_ver = "2.8.1"
@@ -56,32 +59,27 @@ dependencies {
implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver" implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" 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 // ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//New Material Design //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 // Hilt dependency injection
implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1" def hilt_ver = "2.43.2"
implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1" 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 //mockito and livedata testing
testImplementation 'org.mockito:mockito-inline:2.13.0' testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'androidx.arch.core:core-testing:2.1.0' testImplementation 'androidx.arch.core:core-testing:2.1.0'
//Android Room implementation "androidx.preference:preference-ktx:1.2.0"
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"
//mock websever for testing retrofit responses //mock websever for testing retrofit responses
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0" testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.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.app.Application
import android.content.Context import android.content.Context
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.runner.AndroidJUnitRunner import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class TestRunner : AndroidJUnitRunner() { class TestRunner : AndroidJUnitRunner() {
companion object {
val idlingResources = CountingIdlingResource("Data_loader")
}
@Throws( @Throws(
InstantiationException::class, InstantiationException::class,
IllegalAccessException::class, IllegalAccessException::class,
@@ -17,6 +24,6 @@ class TestRunner : AndroidJUnitRunner() {
className: String?, className: String?,
context: Context? context: Context?
): Application { ): 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 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.CurrencyResponse
import com.appttude.h_mal.easycc.data.network.response.ResponseObject 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.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyObject import com.appttude.h_mal.easycc.models.CurrencyObject
import kotlinx.coroutines.delay 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 { override suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject {
idlingResources.increment() idlingResources.increment()

View File

@@ -18,11 +18,11 @@ fun currencyRobot(func: CurrencyRobot.() -> Unit) = CurrencyRobot()
class CurrencyRobot { class CurrencyRobot {
fun clickOnTopList() { 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() { 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) { fun searchInCurrencyList(search: String) {

View File

@@ -2,23 +2,47 @@
package com.appttude.h_mal.easycc.ui.main package com.appttude.h_mal.easycc.ui.main
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule 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 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.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
@LargeTest @HiltAndroidTest
@RunWith(AndroidJUnit4::class) @UninstallModules(AppModule::class)
class MainActivityTest { class MainActivityTest {
@Rule @get:Rule(order = 0)
@JvmField var hiltAndroidRule = HiltAndroidRule(this)
@get:Rule(order = 1)
var mActivityTestRule = ActivityTestRule(MainActivity::class.java) 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 @Test
fun mainActivityTest() { fun mainActivityTest() {
currencyRobot { currencyRobot {

View File

@@ -20,7 +20,8 @@
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> 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> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter> </intent-filter>
@@ -30,12 +31,14 @@
android:resource="@xml/currency_app_widget_info" /> android:resource="@xml/currency_app_widget_info" />
</receiver> </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> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
</activity> </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> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -1,42 +1,7 @@
package com.appttude.h_mal.easycc.application package com.appttude.h_mal.easycc.application
import android.app.Application import android.app.Application
import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import dagger.hilt.android.HiltAndroidApp
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
class AppClass : Application(), KodeinAware { @HiltAndroidApp
class AppClass : Application()
// 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()) }
}
}

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 * Retrofit Network class to currency api calls
*/ */
interface BackupCurrencyApi { interface BackupCurrencyApi : Api {
@GET("latest?") @GET("latest?")
suspend fun getCurrencyRate( suspend fun getCurrencyRate(
@@ -21,24 +21,4 @@ interface BackupCurrencyApi {
@Query("to") currencyTo: String @Query("to") currencyTo: String
): Response<CurrencyResponse> ): 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 * Retrofit Network class to currency api calls
*/ */
interface CurrencyApi { interface CurrencyApi : Api{
// Get rate from server with arguments passed in Repository // Get rate from server with arguments passed in Repository
@GET("convert?") @GET("convert?")
suspend fun getCurrencyRate(@Query("q") currency: String): Response<ResponseObject> 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.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
import javax.inject.Inject
/** /**
* Interceptor used in network classes to check network status * Interceptor used in network classes to check network status
* *
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class NetworkConnectionInterceptor( class NetworkConnectionInterceptor @Inject constructor(
context: Context @ApplicationContext context: Context
) : Interceptor { ) : Interceptor {
private val applicationContext = context.applicationContext private val applicationContext = context.applicationContext
@@ -27,13 +29,12 @@ class NetworkConnectionInterceptor(
} }
private fun isInternetAvailable(): Boolean { private fun isInternetAvailable(): Boolean {
var result = false
val connectivityManager = val connectivityManager =
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let { return connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply { it.getNetworkCapabilities(connectivityManager.activeNetwork)?.run {
result = when { when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
else -> false else -> false
@@ -41,7 +42,7 @@ class NetworkConnectionInterceptor(
} }
} else { } else {
it.activeNetworkInfo?.run { it.activeNetworkInfo?.run {
result = when (type) { when (type) {
ConnectivityManager.TYPE_WIFI -> true ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true ConnectivityManager.TYPE_ETHERNET -> true
@@ -49,8 +50,7 @@ class NetworkConnectionInterceptor(
} }
} }
} }
} } ?: false
return result
} }
} }

View File

@@ -2,17 +2,19 @@ package com.appttude.h_mal.easycc.data.network.interceptors
import android.content.Context import android.content.Context
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import javax.inject.Inject
/** /**
* Interceptor used in CurrencyApi * Interceptor used in CurrencyApi
* Adds apiKey to query parameters * Adds apiKey to query parameters
*/ */
class QueryInterceptor( class QueryInterceptor @Inject constructor(
val context: Context @ApplicationContext val context: Context
) : Interceptor { ) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { 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 import com.google.gson.annotations.SerializedName
data class ResponseObject( data class ResponseObject(
@field:SerializedName("query") @field:SerializedName("query")
var query: Any? = null, var query: Any? = null,
@field:SerializedName("results") @field:SerializedName("results")
var results: Map<String, CurrencyObject>? = null var results: Map<String, CurrencyObject>? = null
) : CurrencyModelInterface { ) : CurrencyModelInterface {
override fun getCurrencyModel(): CurrencyModel { override fun getCurrencyModel(): CurrencyModel {
val res = results?.iterator()?.next()?.value val res = results?.iterator()?.next()?.value
return CurrencyModel( return CurrencyModel(
res?.fr, res?.fr,
res?.to, res?.to,
res?.value ?: 0.0 res?.value ?: 0.0
) )
} }
} }

View File

@@ -4,6 +4,8 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.appttude.h_mal.easycc.R 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 * 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_ONE = "conversion_one"
private const val CURRENCY_TWO = "conversion_two" private const val CURRENCY_TWO = "conversion_two"
class PreferenceProvider(context: Context) { class PreferenceProvider @Inject constructor(@ApplicationContext context: Context) {
private val appContext = context.applicationContext 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.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.utils.convertPairsListToString import com.appttude.h_mal.easycc.utils.convertPairsListToString
import javax.inject.Inject
/** /**
* Default implementation of [Repository]. Single entry point for managing currency' data. * Default implementation of [Repository]. Single entry point for managing currency' data.
*/ */
class RepositoryImpl( class RepositoryImpl @Inject constructor(
private val api: CurrencyApi, private val api: CurrencyApi,
private val backUpApi: BackupCurrencyApi, private val backUpApi: BackupCurrencyApi,
private val prefs: PreferenceProvider 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.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyModelInterface import com.appttude.h_mal.easycc.models.CurrencyModelInterface
import javax.inject.Inject
class CurrencyDataHelper( class CurrencyDataHelper @Inject constructor(
val repository: Repository 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.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyModel import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
import javax.inject.Inject
class WidgetHelper( class WidgetHelper @Inject constructor(
private val helper: CurrencyDataHelper, private val helper: CurrencyDataHelper,
val repository: Repository val repository: Repository
) { ) {

View File

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

View File

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

View File

@@ -2,20 +2,25 @@ package com.appttude.h_mal.easycc.ui.main
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
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.helper.CurrencyDataHelper
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject
/** /**
* ViewModel for the task Main Activity Screen * ViewModel for the task Main Activity Screen
*/ */
class MainViewModel( @HiltViewModel
class MainViewModel @Inject constructor(
private val currencyDataHelper: CurrencyDataHelper, private val currencyDataHelper: CurrencyDataHelper,
private val repository: Repository private val repository: Repository
) : ViewModel() { ) : ViewModel() {
@@ -48,7 +53,7 @@ class MainViewModel(
return return
} }
CoroutineScope(Dispatchers.IO).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 = 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.os.Bundle
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity 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.databinding.CurrencyAppWidgetConfigureBinding
import com.appttude.h_mal.easycc.ui.main.ClickListener import com.appttude.h_mal.easycc.ui.main.ClickListener
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.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 kotlinx.android.synthetic.main.currency_app_widget_configure.* import dagger.hilt.android.AndroidEntryPoint
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.generic.instance
/** /**
* The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget. * The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget.
*/ */
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAware, @AndroidEntryPoint
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
View.OnClickListener { View.OnClickListener {
override val kodein by kodein() val viewModel: WidgetViewModel by viewModels()
private val factory: WidgetViewModelFactory by instance()
var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID private lateinit var binding: CurrencyAppWidgetConfigureBinding
private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
companion object {
lateinit var viewModel: WidgetViewModel
}
public override fun onCreate(icicle: Bundle?) { public override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle) super.onCreate(icicle)
binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root)
// Set the result to CANCELED. This will cause the widget host to cancel // 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. // out of the widget placement if the user presses the back button.
@@ -57,23 +51,20 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
return return
} }
// ViewModel setup
viewModel = ViewModelProviders.of(this, factory).get(WidgetViewModel::class.java)
viewModel.initiate(mAppWidgetId) viewModel.initiate(mAppWidgetId)
setupDataBinding()
setupObserver() setupObserver()
setupClickListener() setupClickListener()
} }
private fun setupClickListener() { private fun setupClickListener() {
submit_widget.setOnClickListener(this) binding.submitWidget.setOnClickListener(this)
currency_one.setOnClickListener(this) binding.currencyOne.setOnClickListener(this)
currency_two.setOnClickListener(this) binding.currencyTwo.setOnClickListener(this)
} }
private fun setupObserver() { private fun setupObserver() {
viewModel.operationFinishedListener.observe(this, { viewModel.operationFinishedListener.observe(this) {
// it.first is a the success of the operation // it.first is a the success of the operation
if (it.first) { if (it.first) {
@@ -82,17 +73,6 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
// failed operation - display toast with message from it.second // failed operation - display toast with message from it.second
it.second?.let { message -> displayToast(message) } 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.content.Context
import android.os.Bundle import android.os.Bundle
import com.appttude.h_mal.easycc.R 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 private val dialogInterface: DialogSubmit
) : Dialog(context) { ) : Dialog(context) {
private lateinit var binding: ConfirmDialogBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.confirm_dialog) binding = ConfirmDialogBinding.inflate(layoutInflater)
setContentView(binding.root)
// layer behind dialog to be transparent // layer behind dialog to be transparent
window!!.setBackgroundDrawableResource(android.R.color.transparent) window!!.setBackgroundDrawableResource(android.R.color.transparent)
// Dialog cannot be cancelled by clicking away // Dialog cannot be cancelled by clicking away
setCancelable(false) setCancelable(false)
confirm_text.text = messageString binding.confirmText.text = messageString
// handle dialog buttons // handle dialog buttons
confirm_yes.setOnClickListener { dialogInterface.onSubmit() } binding.confirmYes.setOnClickListener { dialogInterface.onSubmit() }
confirm_no.setOnClickListener { dismiss() } binding.confirmNo.setOnClickListener { dismiss() }
} }
} }

View File

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

View File

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

View File

@@ -1,126 +1,113 @@
<?xml version="1.0" encoding="utf-8"?> <?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: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 <LinearLayout
name="viewmodel" android:id="@+id/whole_view"
type="com.appttude.h_mal.easycc.ui.main.MainViewModel" /> android:layout_width="match_parent"
</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"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="12dp" android:orientation="vertical">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" <LinearLayout
app:layout_constraintRight_toRightOf="parent" android:layout_width="match_parent"
app:layout_constraintTop_toTopOf="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 <LinearLayout
android:id="@+id/whole_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <androidx.cardview.widget.CardView style="@style/cardview_theme">
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/currencyTwo"
<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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp" android:layout_margin="12dp"
android:layout_marginBottom="6dp" android:tag="bottom"
android:background="@drawable/round_edit_text" android:textColor="@color/colour_five"
android:ems="10" android:textSize="18sp" />
android:hint="insert value one" </androidx.cardview.widget.CardView>
android:inputType="numberDecimal"
android:padding="12dp"
android:selectAllOnFocus="true"
android:tag="from"
android:textColorHighlight="#608d91" />
</LinearLayout>
<LinearLayout <EditText
android:id="@+id/bottomInsertValues"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:layout_marginTop="6dp"
android:layout_weight="7"
<androidx.cardview.widget.CardView style="@style/cardview_theme"> android:background="@drawable/round_edit_text"
android:ems="10"
<TextView android:hint="insert value two"
android:id="@+id/currency_two" android:inputType="numberDecimal"
android:layout_width="match_parent" android:padding="12dp"
android:layout_height="wrap_content" android:selectAllOnFocus="true"
android:layout_margin="12dp" android:tag="to"
android:tag="bottom" android:textColorHighlight="#608d91" />
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>
</LinearLayout> </LinearLayout>
<ProgressBar </LinearLayout>
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>
</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"?> <?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: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> <LinearLayout
android:layout_width="0dp"
<variable android:layout_height="wrap_content"
name="viewmodel" android:layout_marginBottom="9dp"
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" 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 <androidx.cardview.widget.CardView style="@style/cardview_theme">
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"> <TextView
android:id="@+id/currencyOne"
<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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp" android:layout_margin="12dp"
android:layout_marginBottom="6dp" android:tag="top"
android:background="@drawable/round_edit_text" android:textColor="@color/colour_five"
android:ems="10" android:textSize="18sp" />
android:hint="insert value one" </androidx.cardview.widget.CardView>
android:inputType="numberDecimal"
android:padding="12dp"
android:selectAllOnFocus="true"
android:tag="from"
android:textColorHighlight="#608d91" />
</LinearLayout>
<android.widget.Space <EditText
android:id="@+id/middle" android:id="@+id/topInsertValue"
android:layout_width="match_parent" 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_height="wrap_content"
app:layout_constraintWidth_percent=".9" android:layout_marginTop="6dp"
android:layout_marginTop="9dp" android:layout_marginBottom="6dp"
android:orientation="vertical" android:background="@drawable/round_edit_text"
app:layout_constraintLeft_toLeftOf="parent" android:ems="10"
app:layout_constraintRight_toRightOf="parent" android:hint="insert value one"
app:layout_constraintTop_toBottomOf="@id/middle"> android:inputType="numberDecimal"
android:padding="12dp"
android:selectAllOnFocus="true"
android:tag="from"
android:textColorHighlight="#608d91" />
<androidx.cardview.widget.CardView style="@style/cardview_theme"> </LinearLayout>
<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.widget.Space
android:id="@+id/bottomInsertValues" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp" android:layout_margin="12dp"
android:layout_weight="7" android:tag="bottom"
android:background="@drawable/round_edit_text" android:textColor="@color/colour_five"
android:ems="10" android:textSize="18sp" />
android:hint="insert value two" </androidx.cardview.widget.CardView>
android:inputType="numberDecimal"
android:padding="12dp"
android:selectAllOnFocus="true"
android:tag="to"
android:textColorHighlight="#608d91" />
</LinearLayout>
<ProgressBar <EditText
android:id="@+id/progressBar" android:id="@+id/bottomInsertValues"
style="?android:attr/progressBarStyle" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:layout_marginTop="6dp"
android:indeterminateTint="@color/colour_four" android:layout_weight="7"
android:indeterminateTintMode="src_atop" android:background="@drawable/round_edit_text"
android:visibility="gone" android:ems="10"
app:layout_constraintBottom_toBottomOf="parent" android:hint="insert value two"
app:layout_constraintLeft_toLeftOf="parent" android:inputType="numberDecimal"
app:layout_constraintRight_toRightOf="parent" android:padding="12dp"
app:layout_constraintTop_toTopOf="parent" /> android:selectAllOnFocus="true"
</androidx.constraintlayout.widget.ConstraintLayout> 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"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/confirm_text" android:id="@+id/confirmText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
@@ -36,7 +36,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/confirm_yes" android:id="@+id/confirmYes"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@@ -48,7 +48,7 @@
android:textColor="@color/colour_five" /> android:textColor="@color/colour_five" />
<TextView <TextView
android:id="@+id/confirm_no" android:id="@+id/confirmNo"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"

View File

@@ -1,83 +1,68 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
<data> android:layout_height="match_parent"
android:focusable="false"
<variable android:focusableInTouchMode="true"
name="viewmodel" android:orientation="vertical"
type="com.appttude.h_mal.easycc.ui.widget.WidgetViewModel" /> tools:context=".ui.widget.CurrencyAppWidgetConfigureActivityKotlin">
</data>
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_centerInParent="true"
android:focusable="false" android:layout_margin="12dp">
android:focusableInTouchMode="true"
tools:context=".ui.widget.CurrencyAppWidgetConfigureActivityKotlin">
<RelativeLayout <LinearLayout
android:id="@+id/whole_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:orientation="vertical">
android:layout_margin="12dp">
<LinearLayout <androidx.cardview.widget.CardView
android:layout_width="match_parent" style="@style/cardview_theme"
android:layout_height="wrap_content" android:layout_margin="11dp">
android:orientation="vertical"
android:id="@+id/whole_view">
<androidx.cardview.widget.CardView <TextView
style="@style/cardview_theme" android:id="@+id/currencyOne"
android:layout_margin="11dp"> android:layout_width="match_parent"
android:layout_height="wrap_content"
<TextView android:layout_margin="12dp"
android:id="@+id/currency_one" android:tag="top"
android:layout_width="match_parent" android:textColor="@color/colour_five"
android:layout_height="wrap_content" android:textSize="18sp"
android:layout_margin="12dp" tools:text="Currency One" />
android:tag="top" </androidx.cardview.widget.CardView>
android:text="@={viewmodel.rateIdFrom}"
android:textColor="@color/colour_five"
android:textSize="18sp"
tools:text="Currency One" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
style="@style/cardview_theme" style="@style/cardview_theme"
android:layout_margin="11dp"> android:layout_margin="11dp">
<TextView <TextView
android:id="@+id/currency_two" android:id="@+id/currencyTwo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="12dp" android:layout_margin="12dp"
android:tag="bottom" android:tag="bottom"
android:text="@={viewmodel.rateIdTo}" android:textColor="@color/colour_five"
android:textColor="@color/colour_five" android:textSize="18sp"
android:textSize="18sp" tools:text="Currency Two" />
tools:text="Currency Two" /> </androidx.cardview.widget.CardView>
</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>
</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> </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.network.response.ResponseObject
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.helper.CurrencyDataHelper
import com.appttude.h_mal.easycc.utils.MainCoroutineRule
import com.appttude.h_mal.easycc.utils.observeOnce import com.appttude.h_mal.easycc.utils.observeOnce
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before import org.junit.Before
@@ -23,6 +25,9 @@ class MainViewModelTest {
@get:Rule @get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
lateinit var viewModel: MainViewModel lateinit var viewModel: MainViewModel
@Mock @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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
buildscript { id 'com.android.application' version '7.2.2' apply false
ext.kotlin_version = '1.4.10' id 'com.android.library' version '7.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
repositories { id 'com.google.dagger.hilt.android' version '2.43.2' apply false
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()
}
} }
task clean(type: Delete) { 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 distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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' include ':app'