From d0b2c21c2a5d16a01fd29144450021160e70fda2 Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Mon, 8 Aug 2022 21:29:25 +0100 Subject: [PATCH] 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 --- .circleci/config.yml | 81 ++++--- app/build.gradle | 80 ++++--- .../easycc/application/TestApplication.kt | 31 --- .../h_mal/easycc/application/TestRunner.kt | 9 +- .../application/modules/MockRepository.kt | 5 +- .../h_mal/easycc/robots/CurrencyRobot.kt | 4 +- .../h_mal/easycc/ui/main/MainActivityTest.kt | 40 +++- app/src/main/AndroidManifest.xml | 9 +- .../h_mal/easycc/application/AppClass.kt | 41 +--- .../h_mal/easycc/data/network/api/Api.kt | 3 + .../data/network/api/BackupCurrencyApi.kt | 22 +- .../easycc/data/network/api/CurrencyApi.kt | 24 +-- .../data/network/api/RemoteDataSource.kt | 23 ++ .../NetworkConnectionInterceptor.kt | 18 +- .../network/interceptors/QueryInterceptor.kt | 6 +- .../data/network/response/ResponseObject.kt | 14 +- .../easycc/data/prefs/PreferenceProvider.kt | 4 +- .../easycc/data/repository/RepositoryImpl.kt | 3 +- .../com/appttude/h_mal/easycc/di/AppModule.kt | 60 ++++++ .../h_mal/easycc/helper/CurrencyDataHelper.kt | 3 +- .../h_mal/easycc/helper/WidgetHelper.kt | 3 +- .../h_mal/easycc/ui/main/CustomDialogClass.kt | 7 +- .../h_mal/easycc/ui/main/MainActivity.kt | 83 ++++---- .../h_mal/easycc/ui/main/MainViewModel.kt | 9 +- .../easycc/ui/main/MainViewModelFactory.kt | 21 -- ...urrencyAppWidgetConfigureActivityKotlin.kt | 46 ++-- .../easycc/ui/widget/WidgetSubmitDialog.kt | 14 +- .../h_mal/easycc/ui/widget/WidgetViewModel.kt | 5 +- .../ui/widget/WidgetViewModelFactory.kt | 15 -- .../easycc/widget/CurrencyAppWidgetKotlin.kt | 15 +- .../easycc/widget/WidgetServiceIntent.kt | 22 +- app/src/main/res/layout-v26/activity_main.xml | 195 ++++++++--------- app/src/main/res/layout/activity_main.xml | 201 ++++++++---------- app/src/main/res/layout/confirm_dialog.xml | 6 +- .../layout/currency_app_widget_configure.xml | 121 +++++------ .../h_mal/easycc/ui/main/MainViewModelTest.kt | 5 + .../h_mal/easycc/utils/MainCoroutineRule.kt | 22 ++ build.gradle | 26 +-- gradle/wrapper/gradle-wrapper.properties | 6 +- settings.gradle | 15 ++ 40 files changed, 641 insertions(+), 676 deletions(-) delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestApplication.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/data/network/api/Api.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt create mode 100644 app/src/main/java/com/appttude/h_mal/easycc/di/AppModule.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelFactory.kt create mode 100644 app/src/test/java/com/appttude/h_mal/easycc/utils/MainCoroutineRule.kt diff --git a/.circleci/config.yml b/.circleci/config.yml index f435740..b9a076d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index c4821ee..a09cb07 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestApplication.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestApplication.kt deleted file mode 100644 index 35b9294..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestApplication.kt +++ /dev/null @@ -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()) } - } - -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestRunner.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestRunner.kt index 9b97a99..831415c 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestRunner.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/TestRunner.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt index de43efc..eaef8af 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt @@ -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() diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt index dace4cb..3142b42 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt @@ -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) { diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/ui/main/MainActivityTest.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/ui/main/MainActivityTest.kt index 19e298f..7c4771b 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/easycc/ui/main/MainActivityTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/ui/main/MainActivityTest.kt @@ -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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f2e6f5..a912b96 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,8 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/AppTheme"> - + @@ -30,12 +31,14 @@ android:resource="@xml/currency_app_widget_info" /> - + - + diff --git a/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt b/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt index f1a2a05..c8206a5 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/application/AppClass.kt @@ -1,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()) } - } - -} \ No newline at end of file +@HiltAndroidApp +class AppClass : Application() \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/Api.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/Api.kt new file mode 100644 index 0000000..52add23 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/Api.kt @@ -0,0 +1,3 @@ +package com.appttude.h_mal.easycc.data.network.api + +interface Api \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt index dbad89d..fd24c6f 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/BackupCurrencyApi.kt @@ -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 - 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) - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt index 862bf6a..c4da99f 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/CurrencyApi.kt @@ -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 - // 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) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt new file mode 100644 index 0000000..290eafc --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/api/RemoteDataSource.kt @@ -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 buildApi( + okkHttpclient: OkHttpClient, + baseUrl: String, + api: Class + ): Api { + + return Retrofit.Builder() + .client(okkHttpclient) + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(api) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/NetworkConnectionInterceptor.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/NetworkConnectionInterceptor.kt index 248f22f..bd99598 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/NetworkConnectionInterceptor.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/NetworkConnectionInterceptor.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt index d38b90a..8d82c3c 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/interceptors/QueryInterceptor.kt @@ -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 { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt index ab8742f..5bcaf32 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/network/response/ResponseObject.kt @@ -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? = null + @field:SerializedName("query") + var query: Any? = null, + @field:SerializedName("results") + var results: Map? = null ) : CurrencyModelInterface { override fun getCurrencyModel(): CurrencyModel { val res = results?.iterator()?.next()?.value return CurrencyModel( - res?.fr, - res?.to, - res?.value ?: 0.0 + res?.fr, + res?.to, + res?.value ?: 0.0 ) } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/prefs/PreferenceProvider.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/prefs/PreferenceProvider.kt index ce64169..f59fee9 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/prefs/PreferenceProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/prefs/PreferenceProvider.kt @@ -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 diff --git a/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt index 6faa0ef..7f63a32 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/data/repository/RepositoryImpl.kt @@ -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 diff --git a/app/src/main/java/com/appttude/h_mal/easycc/di/AppModule.kt b/app/src/main/java/com/appttude/h_mal/easycc/di/AppModule.kt new file mode 100644 index 0000000..c792203 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/easycc/di/AppModule.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt b/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt index 03aca04..89cce5d 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/helper/CurrencyDataHelper.kt @@ -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 ) { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt b/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt index e81126d..7713f91 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/helper/WidgetHelper.kt @@ -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 ) { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt index 829ab56..7b9de2b 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/CustomDialogClass.kt @@ -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(R.id.list_view) list_view.adapter = arrayAdapter // Edit text to filter @arrayAdapter - search_text.addTextChangedListener(object : TextWatcher { + findViewById(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) diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt index c81f544..3c4667c 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainActivity.kt @@ -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(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) } } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt index 73ba234..6370354 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModel.kt @@ -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( diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt deleted file mode 100644 index 8c5f3a5..0000000 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/main/MainViewModelFactory.kt +++ /dev/null @@ -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 create(modelClass: Class): T { - return MainViewModel(helper, repository) as T - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt index 2ce3050..f5de971 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/CurrencyAppWidgetConfigureActivityKotlin.kt @@ -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( - this, - R.layout.currency_app_widget_configure - ).apply { - viewmodel = viewModel - lifecycleOwner = this@CurrencyAppWidgetConfigureActivityKotlin } } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt index 668d14b..fb047c6 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetSubmitDialog.kt @@ -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() } } } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt index 84911b6..1d94263 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModel.kt @@ -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() { diff --git a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelFactory.kt deleted file mode 100644 index af5523e..0000000 --- a/app/src/main/java/com/appttude/h_mal/easycc/ui/widget/WidgetViewModelFactory.kt +++ /dev/null @@ -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 create(modelClass: Class): T { - return WidgetViewModel(repository) as T - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt b/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt index ad83dac..d4ce3fc 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/widget/CurrencyAppWidgetKotlin.kt @@ -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) } diff --git a/app/src/main/java/com/appttude/h_mal/easycc/widget/WidgetServiceIntent.kt b/app/src/main/java/com/appttude/h_mal/easycc/widget/WidgetServiceIntent.kt index 0070b06..0f2bc70 100644 --- a/app/src/main/java/com/appttude/h_mal/easycc/widget/WidgetServiceIntent.kt +++ b/app/src/main/java/com/appttude/h_mal/easycc/widget/WidgetServiceIntent.kt @@ -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 { diff --git a/app/src/main/res/layout-v26/activity_main.xml b/app/src/main/res/layout-v26/activity_main.xml index 41d762e..3fdae40 100644 --- a/app/src/main/res/layout-v26/activity_main.xml +++ b/app/src/main/res/layout-v26/activity_main.xml @@ -1,126 +1,113 @@ - + 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"> - + - - - - - - + android:orientation="vertical"> + + + + + + + + + + + + - + - - - - - - - + android:layout_margin="12dp" + android:tag="bottom" + android:textColor="@color/colour_five" + android:textSize="18sp" /> + - - - - - - - - - - - - + android:layout_marginTop="6dp" + android:layout_weight="7" + android:background="@drawable/round_edit_text" + android:ems="10" + android:hint="insert value two" + android:inputType="numberDecimal" + android:padding="12dp" + android:selectAllOnFocus="true" + android:tag="to" + android:textColorHighlight="#608d91" /> - - - + - + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9b778d7..a4515cc 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,128 +1,115 @@ - + 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"> - - - - - - + app:layout_constraintBottom_toTopOf="@id/middle" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintWidth_percent=".9"> - + - - - - - - - - - + android:layout_margin="12dp" + android:tag="top" + android:textColor="@color/colour_five" + android:textSize="18sp" /> + - - - - + 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" /> + - - - + android:layout_marginTop="6dp" + android:layout_weight="7" + android:background="@drawable/round_edit_text" + android:ems="10" + android:hint="insert value two" + android:inputType="numberDecimal" + android:padding="12dp" + android:selectAllOnFocus="true" + android:tag="to" + android:textColorHighlight="#608d91" /> - + + + + diff --git a/app/src/main/res/layout/confirm_dialog.xml b/app/src/main/res/layout/confirm_dialog.xml index a063840..339dcb0 100644 --- a/app/src/main/res/layout/confirm_dialog.xml +++ b/app/src/main/res/layout/confirm_dialog.xml @@ -19,7 +19,7 @@ android:orientation="vertical"> - - - - - - + + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:layout_margin="12dp"> - + android:orientation="vertical"> - + - - - - + + - + - - - - - - - - + + + + - + diff --git a/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt index 97945cf..3bc877c 100644 --- a/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt +++ b/app/src/test/java/com/appttude/h_mal/easycc/ui/main/MainViewModelTest.kt @@ -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 diff --git a/app/src/test/java/com/appttude/h_mal/easycc/utils/MainCoroutineRule.kt b/app/src/test/java/com/appttude/h_mal/easycc/utils/MainCoroutineRule.kt new file mode 100644 index 0000000..865c1c8 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/easycc/utils/MainCoroutineRule.kt @@ -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() + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8cd2633..1457a90 100644 --- a/build.gradle +++ b/build.gradle @@ -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) { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 02e4898..f63e218 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/settings.gradle b/settings.gradle index e7b4def..ed8e613 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "EasyCC" include ':app'