diff --git a/app/build.gradle b/app/build.gradle index d08b275..c808dd7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ apply from: '../versions.gradle' apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' android { compileSdkVersion build_versions.target_sdk @@ -15,7 +16,7 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - buildConfigField "String", "BASE_ENDPOINT", '"http://www.nactem.ac.uk/software/acromine/"' + buildConfigField "String", "BASE_ENDPOINT", '"http://www.nactem.ac.uk"' } buildTypes { @@ -57,4 +58,5 @@ android { dependencies { implementation project(":core") + implementation project(':shortform') } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b2d252b..e4ecfed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ diff --git a/app/src/main/java/dev/carlos/acronyms/MainApplication.kt b/app/src/main/java/dev/carlos/acronyms/MainApplication.kt index ea7d95c..8c7e978 100644 --- a/app/src/main/java/dev/carlos/acronyms/MainApplication.kt +++ b/app/src/main/java/dev/carlos/acronyms/MainApplication.kt @@ -1,6 +1,7 @@ package dev.carlos.acronyms import android.app.Application +import dev.carlos.shortform.di.shortformModule import dev.carlos.acronyms.di.appModules import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -24,7 +25,8 @@ class MainApplication : Application() { modules( listOf( - appModules + appModules, + shortformModule ) ) } diff --git a/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt b/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt index 8d5b26b..415fd1c 100644 --- a/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt +++ b/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt @@ -5,15 +5,11 @@ import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.ui.NavigationUI import dev.carlos.acronyms.R -import dev.carlos.acronyms.di.appModules import dev.carlos.acronyms.viewmodel.NavigationViewmodel -import dev.carlos.core.extensions.findNavHostFragment import dev.carlos.core.extensions.observeNonNull import dev.carlos.core.navigation.NavigationRouter import org.koin.android.ext.android.inject -import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.context.startKoin class MainActivity : AppCompatActivity() { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a21b3e1..f35bcca 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,8 @@ android:layout_height="match_parent" > - + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..754ccf0 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + www.nactem.ac.uk + + diff --git a/base.gradle b/base.gradle index 6b9694a..3ace927 100644 --- a/base.gradle +++ b/base.gradle @@ -2,6 +2,7 @@ apply from: '../versions.gradle' apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' android { compileSdkVersion build_versions.target_sdk diff --git a/core/build.gradle b/core/build.gradle index 6372e2c..f47fd63 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,6 +3,7 @@ apply from: '../base.gradle' apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' dependencies { api deps.google.kotlin.std_lib @@ -15,6 +16,7 @@ dependencies { api deps.google.androidx.navigation_fragment api deps.google.androidx.navigation_ui api deps.google.material.core + api deps.google.gson.core api deps.core.okhttp.logging_interceptor api deps.core.retrofit.runtime api deps.core.retrofit.gson diff --git a/core/src/main/java/dev/carlos/core/domain/network/NetworkErrors.kt b/core/src/main/java/dev/carlos/core/domain/network/NetworkErrors.kt new file mode 100644 index 0000000..39c676e --- /dev/null +++ b/core/src/main/java/dev/carlos/core/domain/network/NetworkErrors.kt @@ -0,0 +1,10 @@ +package dev.carlos.core.domain.network + +import androidx.annotation.StringRes +import dev.carlos.core.R + +enum class RequestError(@StringRes val message: Int) { + NO_NETWORK(R.string.network_error_no_network), + BAD_RESPONSE(R.string.network_error_bad_response), + UNKNOWN_PROBLEM(R.string.network_error_unknown) +} diff --git a/core/src/main/java/dev/carlos/core/domain/network/RequestState.kt b/core/src/main/java/dev/carlos/core/domain/network/RequestState.kt new file mode 100644 index 0000000..b710558 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/domain/network/RequestState.kt @@ -0,0 +1,8 @@ +package dev.carlos.core.domain.network + +sealed class RequestState { + class Success(val data: T) : RequestState() + object Loading : RequestState() + class Error(val type: RequestError) : RequestState() + object Empty : RequestState() +} diff --git a/core/src/main/java/dev/carlos/core/domain/network/RequestStatus.kt b/core/src/main/java/dev/carlos/core/domain/network/RequestStatus.kt deleted file mode 100644 index 1c8cafc..0000000 --- a/core/src/main/java/dev/carlos/core/domain/network/RequestStatus.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.carlos.core.domain.network - -sealed class RequestStatus { - object Ready : RequestStatus() - object Loading : RequestStatus() - object Errored : RequestStatus() - object Empty : RequestStatus() -} diff --git a/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt index 1176e23..c9b501b 100644 --- a/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt +++ b/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer fun LiveData.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) { - observe(owner, Observer { + observe(owner, { it?.let { func(it) } diff --git a/core/src/main/java/dev/carlos/core/extensions/NumbersExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/NumbersExtensions.kt new file mode 100644 index 0000000..3a428da --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/NumbersExtensions.kt @@ -0,0 +1,3 @@ +package dev.carlos.core.extensions + +fun Int.notEmpty() = this > 0 diff --git a/core/src/main/java/dev/carlos/core/extensions/SchedulerExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/SchedulerExtensions.kt new file mode 100644 index 0000000..7ac530c --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/SchedulerExtensions.kt @@ -0,0 +1,19 @@ +package dev.carlos.core.extensions + +import dev.carlos.core.scheduler.Scheduler +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single + +fun Observable.runOnIo(scheduler: Scheduler): Observable = + subscribeOn(scheduler.io()).observeOn(scheduler.ui()) + +fun Single.runOnIo(scheduler: Scheduler): Single = + subscribeOn(scheduler.io()).observeOn(scheduler.ui()) + +fun Flowable.runOnIo(scheduler: Scheduler): Flowable = + subscribeOn(scheduler.io()).observeOn(scheduler.ui()) + +fun Completable.runOnIo(scheduler: Scheduler): Completable = + subscribeOn(scheduler.io()).observeOn(scheduler.ui()) diff --git a/core/src/main/java/dev/carlos/core/extensions/StringsExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/StringsExtensions.kt new file mode 100644 index 0000000..e61c927 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/StringsExtensions.kt @@ -0,0 +1,5 @@ +package dev.carlos.core.extensions + +import java.util.* + +fun String.capitalize() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } diff --git a/core/src/main/java/dev/carlos/core/scheduler/Scheduler.kt b/core/src/main/java/dev/carlos/core/scheduler/Scheduler.kt new file mode 100644 index 0000000..9a57637 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/scheduler/Scheduler.kt @@ -0,0 +1,10 @@ +package dev.carlos.core.scheduler + +import io.reactivex.Scheduler + +interface Scheduler { + fun io(): Scheduler + fun computation(): Scheduler + fun newThread(): Scheduler + fun ui(): Scheduler +} diff --git a/core/src/main/java/dev/carlos/core/scheduler/SchedulerProvider.kt b/core/src/main/java/dev/carlos/core/scheduler/SchedulerProvider.kt new file mode 100644 index 0000000..f04fb75 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/scheduler/SchedulerProvider.kt @@ -0,0 +1,15 @@ +package dev.carlos.core.scheduler + +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class SchedulerProvider : Scheduler { + + override fun io() = Schedulers.io() + + override fun computation() = Schedulers.computation() + + override fun newThread() = Schedulers.newThread() + + override fun ui(): io.reactivex.Scheduler = AndroidSchedulers.mainThread() +} diff --git a/core/src/main/java/dev/carlos/core/viewmodel/RxViewModel.kt b/core/src/main/java/dev/carlos/core/viewmodel/RxViewModel.kt new file mode 100644 index 0000000..d6a8e8b --- /dev/null +++ b/core/src/main/java/dev/carlos/core/viewmodel/RxViewModel.kt @@ -0,0 +1,17 @@ +package dev.carlos.core.viewmodel + +import androidx.lifecycle.ViewModel +import io.reactivex.disposables.CompositeDisposable + +abstract class RxViewModel : ViewModel() { + protected val compositeDisposable by lazy { CompositeDisposable() } + + override fun onCleared() { + super.onCleared() + clearCompositeDisposable() + } + + private fun clearCompositeDisposable() { + compositeDisposable.clear() + } +} diff --git a/core/src/main/res/drawable-v24/ic_launcher_foreground.xml b/core/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 1448aba..0000000 --- a/core/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/core/src/main/res/drawable/bg_card.xml b/core/src/main/res/drawable/bg_card.xml new file mode 100644 index 0000000..f686445 --- /dev/null +++ b/core/src/main/res/drawable/bg_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/src/main/res/drawable/ic_launcher_background.xml b/core/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 445d0d5..0000000 --- a/core/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 03eed25..0000000 --- a/core/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/core/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 03eed25..0000000 --- a/core/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher.png b/core/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a571e60..0000000 Binary files a/core/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_round.png b/core/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 61da551..0000000 Binary files a/core/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher.png b/core/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index c41dd28..0000000 Binary files a/core/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/core/src/main/res/mipmap-mdpi/ic_launcher_round.png b/core/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index db5080a..0000000 Binary files a/core/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher.png b/core/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 6dba46d..0000000 Binary files a/core/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/core/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/core/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index da31a87..0000000 Binary files a/core/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher.png b/core/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 15ac681..0000000 Binary files a/core/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/core/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/core/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index b216f2d..0000000 Binary files a/core/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index f25a419..0000000 Binary files a/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/core/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index e96783c..0000000 Binary files a/core/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index 09837df..9c8f149 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ #FF018786 #FF000000 #FFFFFFFF - \ No newline at end of file + #EEEEEE + #00000000 + #FFFFFF + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 75f77d2..696b69b 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ - core - \ No newline at end of file + Respuesta inesperada + Sin conexión + Error desconocido + diff --git a/settings.gradle b/settings.gradle index e89e7cc..51e8e82 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = "Acronyms" -include ':app', ':core' +include ':app', ':core', ':shortform' diff --git a/shortform/.gitignore b/shortform/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/shortform/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/shortform/build.gradle b/shortform/build.gradle new file mode 100644 index 0000000..92d439c --- /dev/null +++ b/shortform/build.gradle @@ -0,0 +1,16 @@ +apply from: '../versions.gradle' +apply from: '../base.gradle' +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' + +android { + defaultConfig { + buildConfigField "String", "DB_NAME", '"acronyms.db"' + } +} + +dependencies { + implementation project(':core') +} diff --git a/shortform/proguard-rules.pro b/shortform/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/shortform/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/shortform/src/main/AndroidManifest.xml b/shortform/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7b5a50f --- /dev/null +++ b/shortform/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/shortform/src/main/java/dev/carlos/shortform/data/ShortformDataRepository.kt b/shortform/src/main/java/dev/carlos/shortform/data/ShortformDataRepository.kt new file mode 100644 index 0000000..d4ca8ef --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/ShortformDataRepository.kt @@ -0,0 +1,16 @@ +package dev.carlos.shortform.data + +import dev.carlos.shortform.data.cloud.ShortformRemoteSource +import dev.carlos.shortform.data.models.ShortformModel +import dev.carlos.shortform.data.models.toShortformModel +import dev.carlos.shortform.domain.ShortformRepository +import io.reactivex.Single + +class ShortformDataRepository( + private val remoteDatasource: ShortformRemoteSource +) : ShortformRepository { + + override fun getShortformDefinition(acronym: String): Single { + return remoteDatasource.getShortformDefinition(acronym).map { it.single().toShortformModel() } + } +} diff --git a/shortform/src/main/java/dev/carlos/shortform/data/cloud/ShortformRemoteSource.kt b/shortform/src/main/java/dev/carlos/shortform/data/cloud/ShortformRemoteSource.kt new file mode 100644 index 0000000..8c073d2 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/cloud/ShortformRemoteSource.kt @@ -0,0 +1,8 @@ +package dev.carlos.shortform.data.cloud + +import dev.carlos.shortform.data.cloud.model.ShortformRemote +import io.reactivex.Single + +interface ShortformRemoteSource { + fun getShortformDefinition(acronym: String): Single> +} diff --git a/shortform/src/main/java/dev/carlos/shortform/data/cloud/model/ShortformRemote.kt b/shortform/src/main/java/dev/carlos/shortform/data/cloud/model/ShortformRemote.kt new file mode 100644 index 0000000..6fb68c8 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/cloud/model/ShortformRemote.kt @@ -0,0 +1,18 @@ +package dev.carlos.shortform.data.cloud.model + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ShortformRemote( + @SerializedName("sf") val value: String, + @SerializedName("lfs") val results: List +) : Parcelable { + @Parcelize + data class LongformRemote( + @SerializedName("lf") val value: String, + @SerializedName("freq") val corpusFrequency: Int, + @SerializedName("since") val since: Int + ) : Parcelable +} diff --git a/shortform/src/main/java/dev/carlos/shortform/data/cloud/retrofit/ShortformRemoteDatasource.kt b/shortform/src/main/java/dev/carlos/shortform/data/cloud/retrofit/ShortformRemoteDatasource.kt new file mode 100644 index 0000000..b72e388 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/cloud/retrofit/ShortformRemoteDatasource.kt @@ -0,0 +1,11 @@ +package dev.carlos.shortform.data.cloud.retrofit + +import dev.carlos.shortform.data.cloud.ShortformRemoteSource +import dev.carlos.shortform.data.cloud.model.ShortformRemote +import io.reactivex.Single + +class ShortformRemoteDatasource(private val shortformService: ShortformService) : ShortformRemoteSource { + override fun getShortformDefinition(acronym: String): Single> { + return shortformService.getShortformDefinition(acronym) + } +} diff --git a/shortform/src/main/java/dev/carlos/shortform/data/cloud/retrofit/ShortformService.kt b/shortform/src/main/java/dev/carlos/shortform/data/cloud/retrofit/ShortformService.kt new file mode 100644 index 0000000..f5dd805 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/cloud/retrofit/ShortformService.kt @@ -0,0 +1,11 @@ +package dev.carlos.shortform.data.cloud.retrofit + +import dev.carlos.shortform.data.cloud.model.ShortformRemote +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Query + +interface ShortformService { + @GET("/software/acromine/dictionary.py") + fun getShortformDefinition(@Query("sf") acronym: String): Single> +} diff --git a/shortform/src/main/java/dev/carlos/shortform/data/models/Mappers.kt b/shortform/src/main/java/dev/carlos/shortform/data/models/Mappers.kt new file mode 100644 index 0000000..6031961 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/models/Mappers.kt @@ -0,0 +1,7 @@ +package dev.carlos.shortform.data.models + +import dev.carlos.shortform.data.cloud.model.ShortformRemote + +fun ShortformRemote.toShortformModel() = ShortformModel(this.value, this.results.map { + ShortformModel.LongformModel(it.value, it.corpusFrequency, it.since) +}) diff --git a/shortform/src/main/java/dev/carlos/shortform/data/models/ShortformModel.kt b/shortform/src/main/java/dev/carlos/shortform/data/models/ShortformModel.kt new file mode 100644 index 0000000..e36aa13 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/data/models/ShortformModel.kt @@ -0,0 +1,12 @@ +package dev.carlos.shortform.data.models + +data class ShortformModel( + val value: String, + val results: List +) { + data class LongformModel( + val value: String, + val corpusFrequency: Int, + val since: Int + ) +} diff --git a/shortform/src/main/java/dev/carlos/shortform/di/ShortformModule.kt b/shortform/src/main/java/dev/carlos/shortform/di/ShortformModule.kt new file mode 100644 index 0000000..d5e8295 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/di/ShortformModule.kt @@ -0,0 +1,29 @@ +package dev.carlos.shortform.di + +import dev.carlos.core.domain.network.RemoteClient +import dev.carlos.core.scheduler.Scheduler +import dev.carlos.core.scheduler.SchedulerProvider +import dev.carlos.shortform.data.ShortformDataRepository +import dev.carlos.shortform.data.cloud.ShortformRemoteSource +import dev.carlos.shortform.data.cloud.retrofit.ShortformRemoteDatasource +import dev.carlos.shortform.data.cloud.retrofit.ShortformService +import dev.carlos.shortform.domain.ShortformRepository +import dev.carlos.shortform.domain.GetShortformDefinition +import dev.carlos.shortform.viewmodels.ShortformViewmodel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val shortformModule = module { + single { SchedulerProvider() } + + single { + get().getClient(ShortformService::class.java) + } + + factory { ShortformRemoteDatasource(get()) } + factory { ShortformDataRepository(get()) } + + factory { GetShortformDefinition(get(), get()) } + + viewModel { ShortformViewmodel(get()) } +} diff --git a/shortform/src/main/java/dev/carlos/shortform/domain/GetShortformDefinition.kt b/shortform/src/main/java/dev/carlos/shortform/domain/GetShortformDefinition.kt new file mode 100644 index 0000000..fee307e --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/domain/GetShortformDefinition.kt @@ -0,0 +1,15 @@ +package dev.carlos.shortform.domain + +import dev.carlos.shortform.data.models.ShortformModel +import dev.carlos.core.extensions.runOnIo +import dev.carlos.core.scheduler.Scheduler +import io.reactivex.Single + +class GetShortformDefinition( + private val shortformRepository: ShortformRepository, + private val scheduler: Scheduler +) { + fun getShortformDefinition(shortform: String): Single { + return shortformRepository.getShortformDefinition(shortform).runOnIo(scheduler) + } +} diff --git a/shortform/src/main/java/dev/carlos/shortform/domain/ShortformRepository.kt b/shortform/src/main/java/dev/carlos/shortform/domain/ShortformRepository.kt new file mode 100644 index 0000000..76e6b0a --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/domain/ShortformRepository.kt @@ -0,0 +1,9 @@ +package dev.carlos.shortform.domain + +import dev.carlos.shortform.data.models.ShortformModel +import io.reactivex.Flowable +import io.reactivex.Single + +interface ShortformRepository { + fun getShortformDefinition(acronym: String): Single +} diff --git a/shortform/src/main/java/dev/carlos/shortform/feature/ShortformFragment.kt b/shortform/src/main/java/dev/carlos/shortform/feature/ShortformFragment.kt new file mode 100644 index 0000000..79855aa --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/feature/ShortformFragment.kt @@ -0,0 +1,112 @@ +package dev.carlos.shortform.feature + +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import dev.carlos.core.domain.network.RequestError +import dev.carlos.core.domain.network.RequestState +import dev.carlos.core.extensions.gone +import dev.carlos.core.extensions.observeNonNull +import dev.carlos.core.extensions.visible +import dev.carlos.shortform.R +import dev.carlos.shortform.data.models.ShortformModel +import dev.carlos.shortform.databinding.FragmentShortformDefinitionBinding +import dev.carlos.shortform.viewmodels.ShortformViewmodel +import dev.carlos.shortform.widgets.LongformAdapter +import org.koin.androidx.viewmodel.ext.android.viewModel + +class ShortformFragment : Fragment() { + private val viewModel: ShortformViewmodel by viewModel() + private val listAdapter by lazy { LongformAdapter() } + private lateinit var binding: FragmentShortformDefinitionBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_shortform_definition, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setBinding(view) + setRecycler() + setObservers() + } + + private fun setBinding(view: View) { + binding = FragmentShortformDefinitionBinding.bind(view) + } + + private fun setObservers() { + viewModel.acronymDefinition.observeNonNull(this) { + handleResult(it) + } + + binding.shortformDefinitionSearchField.setOnKeyListener(object : View.OnKeyListener { + override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN && + keyCode == KeyEvent.KEYCODE_ENTER + ) { + onSearch() + return true + } + return false + } + }) + } + + private fun onSearch() { + fetchAcronym(binding.shortformDefinitionSearchField.text.toString()) + binding.shortformDefinitionRecycler.requestFocus() + } + + private fun fetchAcronym(acronym: String) { + viewModel.fetchAcronymDefinition(acronym) + } + + private fun handleResult(state: RequestState) { + when (state) { + is RequestState.Success<*> -> handleSuccess(state.data as ShortformModel) + is RequestState.Empty -> handleEmpty() + is RequestState.Error -> handleError(state.type) + is RequestState.Loading -> handleLoading() + } + } + + private fun handleLoading() { + binding.shortformDefinitionLoading.visible() + binding.shortformDefinitionError.gone() + } + + private fun handleSuccess(shortform: ShortformModel) { + binding.shortformDefinitionError.gone() + binding.shortformDefinitionLoading.gone() + listAdapter.addAll(shortform.results) + } + + private fun handleEmpty() { + binding.shortformDefinitionErrorLabel.text = getString(R.string.shortform_definition_no_definition) + binding.shortformDefinitionError.visible() + binding.shortformDefinitionLoading.gone() + } + + private fun handleError(error: RequestError) { + binding.shortformDefinitionErrorLabel.text = getString(error.message) + binding.shortformDefinitionError.visible() + binding.shortformDefinitionLoading.gone() + } + + private fun setRecycler() { + binding.shortformDefinitionRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = listAdapter.apply { + onItemClick = { + Toast.makeText(context, it.value, Toast.LENGTH_SHORT).show() + } + } + } + } +} \ No newline at end of file diff --git a/shortform/src/main/java/dev/carlos/shortform/viewmodels/ShortformViewmodel.kt b/shortform/src/main/java/dev/carlos/shortform/viewmodels/ShortformViewmodel.kt new file mode 100644 index 0000000..35e36d7 --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/viewmodels/ShortformViewmodel.kt @@ -0,0 +1,37 @@ +package dev.carlos.shortform.viewmodels + +import androidx.lifecycle.MutableLiveData +import dev.carlos.core.domain.network.RequestError +import dev.carlos.core.domain.network.RequestState +import dev.carlos.core.viewmodel.RxViewModel +import dev.carlos.shortform.data.models.ShortformModel +import dev.carlos.shortform.domain.GetShortformDefinition +import retrofit2.HttpException +import java.net.UnknownHostException + +class ShortformViewmodel( + private val getAcronymsDefinition: GetShortformDefinition +) : RxViewModel() { + + val acronymDefinition = MutableLiveData() + + fun fetchAcronymDefinition(acronym: String) { + val disposable = getAcronymsDefinition.getShortformDefinition(acronym) + .doOnSubscribe { acronymDefinition.postValue(RequestState.Loading) } + .subscribe(::handleSuccess, ::handleError) + compositeDisposable.add(disposable) + } + + private fun handleSuccess(definition: ShortformModel) { + acronymDefinition.postValue(RequestState.Success(definition)) + } + + private fun handleError(exception: Throwable) { + when (exception) { + is NoSuchElementException -> acronymDefinition.postValue(RequestState.Empty) + is UnknownHostException -> acronymDefinition.postValue(RequestState.Error(RequestError.NO_NETWORK)) + is HttpException -> acronymDefinition.postValue(RequestState.Error(RequestError.BAD_RESPONSE)) + else -> acronymDefinition.postValue(RequestState.Error(RequestError.UNKNOWN_PROBLEM)) + } + } +} \ No newline at end of file diff --git a/shortform/src/main/java/dev/carlos/shortform/widgets/LongformAdapter.kt b/shortform/src/main/java/dev/carlos/shortform/widgets/LongformAdapter.kt new file mode 100644 index 0000000..6543abe --- /dev/null +++ b/shortform/src/main/java/dev/carlos/shortform/widgets/LongformAdapter.kt @@ -0,0 +1,60 @@ +package dev.carlos.shortform.widgets + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import dev.carlos.core.extensions.autoNotify +import dev.carlos.core.extensions.capitalize +import dev.carlos.shortform.R +import dev.carlos.shortform.data.models.ShortformModel +import dev.carlos.shortform.databinding.ItemLongformCardBinding +import kotlin.properties.Delegates + +class LongformAdapter : RecyclerView.Adapter() { + + private var items by Delegates.observable(emptyList()) { _, oldList, newList -> + autoNotify(oldList, newList) { old, new -> old.value == new.value } + notifyDataSetChanged() + } + + var onItemClick: (ShortformModel.LongformModel) -> Unit = { } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + LongformViewHolder( + LayoutInflater + .from(parent.context) + .inflate(R.layout.item_longform_card, parent, false), + onItemClick + ) + + fun getItem(position: Int) = items[position] + + fun addAll(list: List) { + items = list + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder(viewholder: RecyclerView.ViewHolder, position: Int) = + when (viewholder) { + is LongformViewHolder -> viewholder.bind(items[position]) + else -> throw NoWhenBranchMatchedException("Undefined viewholder") + } +} + +class LongformViewHolder( + private val view: View, + private val onItemClick: (ShortformModel.LongformModel) -> Unit +) : RecyclerView.ViewHolder(view) { + + private val binding = ItemLongformCardBinding.bind(view) + + fun bind(longform: ShortformModel.LongformModel) = with(view) { + binding.itemLongformName.text = longform.value.capitalize() + binding.itemLongformSince.text = view.resources.getString(R.string.item_longform_since, longform.since) + binding.itemLongformContainer.setOnClickListener { + onItemClick(longform) + } + } +} \ No newline at end of file diff --git a/shortform/src/main/res/layout/fragment_shortform_definition.xml b/shortform/src/main/res/layout/fragment_shortform_definition.xml new file mode 100644 index 0000000..0958fae --- /dev/null +++ b/shortform/src/main/res/layout/fragment_shortform_definition.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/shortform/src/main/res/layout/item_longform_card.xml b/shortform/src/main/res/layout/item_longform_card.xml new file mode 100644 index 0000000..e580f44 --- /dev/null +++ b/shortform/src/main/res/layout/item_longform_card.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/shortform/src/main/res/navigation/shortform_navigation.xml b/shortform/src/main/res/navigation/shortform_navigation.xml new file mode 100644 index 0000000..2ed5d1b --- /dev/null +++ b/shortform/src/main/res/navigation/shortform_navigation.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/shortform/src/main/res/values-night/themes.xml b/shortform/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..7558bbb --- /dev/null +++ b/shortform/src/main/res/values-night/themes.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/shortform/src/main/res/values/colors.xml b/shortform/src/main/res/values/colors.xml new file mode 100644 index 0000000..09837df --- /dev/null +++ b/shortform/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/shortform/src/main/res/values/strings.xml b/shortform/src/main/res/values/strings.xml new file mode 100644 index 0000000..be206d9 --- /dev/null +++ b/shortform/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Desde %1$d + Buscar acrónimo + No existe una definición + \ No newline at end of file diff --git a/shortform/src/main/res/values/themes.xml b/shortform/src/main/res/values/themes.xml new file mode 100644 index 0000000..a6cea39 --- /dev/null +++ b/shortform/src/main/res/values/themes.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/versions.gradle b/versions.gradle index 9aab55f..ed5da03 100644 --- a/versions.gradle +++ b/versions.gradle @@ -11,6 +11,7 @@ versions.androidx_lifecycle = "2.2.0" versions.androidx_recycler_view = "1.2.0" versions.androidx_navigation = "2.3.5" versions.material_core = "1.3.0" +versions.gson = "2.8.7" versions.okhttp_interceptor = "4.9.1" versions.retrofit = "2.9.0" versions.timber = "4.7.1" @@ -53,8 +54,12 @@ androidx.navigation_ui = "androidx.navigation:navigation-ui-ktx:$versions.androi def material = [:] material.core = "com.google.android.material:material:$versions.material_core" +def gson = [:] +gson.core = "com.google.code.gson:gson:$versions.gson" + def google = [:] google.kotlin = kotlin +google.gson = gson google.androidx = androidx google.material = material deps.google = google