Merge pull request #4 from hakodeveloper/feature/add-acronyms-feature

Acronyms Module
This commit is contained in:
Carlos Martinez
2021-06-17 13:47:35 -04:00
committed by GitHub
62 changed files with 706 additions and 271 deletions

View File

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

View File

@@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:name="dev.carlos.acronyms.MainApplication"
android:allowBackup="true"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
@@ -14,11 +14,12 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/Theme.Acronyms"
android:networkSecurityConfig="@xml/network_security_config"
android:hardwareAccelerated="true"
>
<activity
android:name=".views.MainActivity"
android:name="dev.carlos.acronyms.views.MainActivity"
android:launchMode="singleTask"
>
<intent-filter>

View File

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

View File

@@ -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() {

View File

@@ -5,7 +5,8 @@
android:layout_height="match_parent"
>
<androidx.fragment.app.FragmentContainerView
<!-- Still usign fragment because of this [https://issuetracker.google.com/issues/142847973] -->
<fragment
android:id="@+id/main_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"

View File

@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_navigation"
app:startDestination="@id/shortform_navigation"
>
<include app:graph="@navigation/shortform_navigation" />
</navigation>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">www.nactem.ac.uk</domain>
</domain-config>
</network-security-config>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package dev.carlos.core.domain.network
sealed class RequestState {
class Success<out T>(val data: T) : RequestState()
object Loading : RequestState()
class Error(val type: RequestError) : RequestState()
object Empty : RequestState()
}

View File

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

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) {
observe(owner, Observer {
observe(owner, {
it?.let {
func(it)
}

View File

@@ -0,0 +1,3 @@
package dev.carlos.core.extensions
fun Int.notEmpty() = this > 0

View File

@@ -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 <T> Observable<T>.runOnIo(scheduler: Scheduler): Observable<T> =
subscribeOn(scheduler.io()).observeOn(scheduler.ui())
fun <T> Single<T>.runOnIo(scheduler: Scheduler): Single<T> =
subscribeOn(scheduler.io()).observeOn(scheduler.ui())
fun <T> Flowable<T>.runOnIo(scheduler: Scheduler): Flowable<T> =
subscribeOn(scheduler.io()).observeOn(scheduler.ui())
fun Completable.runOnIo(scheduler: Scheduler): Completable =
subscribeOn(scheduler.io()).observeOn(scheduler.ui())

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
>
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="49.59793"
android:startX="42.9492"
android:endY="92.4963"
android:endX="85.84757"
android:type="linear"
>
<item
android:color="#44000000"
android:offset="0.0"
/>
<item
android:color="#00000000"
android:offset="1.0"
/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeWidth="1"
android:strokeColor="#00000000"
/>
</vector>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape >
<solid android:color="@color/card_background" />
<corners android:radius="4dp" />
</shape>
</item>
</layer-list>

View File

@@ -1,204 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
>
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"
/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"
/>
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -7,4 +7,7 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
<color name="soft_background">#EEEEEE</color>
<color name="transparent">#00000000</color>
<color name="card_background">#FFFFFF</color>
</resources>

View File

@@ -1,3 +1,5 @@
<resources>
<string name="app_name">core</string>
</resources>
<string name="network_error_bad_response">Respuesta inesperada</string>
<string name="network_error_no_network">Sin conexión</string>
<string name="network_error_unknown">Error desconocido</string>
</resources>

View File

@@ -1,2 +1,2 @@
rootProject.name = "Acronyms"
include ':app', ':core'
include ':app', ':core', ':shortform'

1
shortform/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

16
shortform/build.gradle Normal file
View File

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

21
shortform/proguard-rules.pro vendored Normal file
View File

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

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.carlos.shortform" />

View File

@@ -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<ShortformModel> {
return remoteDatasource.getShortformDefinition(acronym).map { it.single().toShortformModel() }
}
}

View File

@@ -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<List<ShortformRemote>>
}

View File

@@ -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<LongformRemote>
) : Parcelable {
@Parcelize
data class LongformRemote(
@SerializedName("lf") val value: String,
@SerializedName("freq") val corpusFrequency: Int,
@SerializedName("since") val since: Int
) : Parcelable
}

View File

@@ -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<List<ShortformRemote>> {
return shortformService.getShortformDefinition(acronym)
}
}

View File

@@ -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<List<ShortformRemote>>
}

View File

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

View File

@@ -0,0 +1,12 @@
package dev.carlos.shortform.data.models
data class ShortformModel(
val value: String,
val results: List<LongformModel>
) {
data class LongformModel(
val value: String,
val corpusFrequency: Int,
val since: Int
)
}

View File

@@ -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<Scheduler> { SchedulerProvider() }
single {
get<RemoteClient>().getClient(ShortformService::class.java)
}
factory<ShortformRemoteSource> { ShortformRemoteDatasource(get()) }
factory<ShortformRepository> { ShortformDataRepository(get()) }
factory { GetShortformDefinition(get(), get()) }
viewModel { ShortformViewmodel(get()) }
}

View File

@@ -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<ShortformModel> {
return shortformRepository.getShortformDefinition(shortform).runOnIo(scheduler)
}
}

View File

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

View File

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

View File

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

View File

@@ -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<RecyclerView.ViewHolder>() {
private var items by Delegates.observable(emptyList<ShortformModel.LongformModel>()) { _, 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<ShortformModel.LongformModel>) {
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)
}
}
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:hint="@string/shortform_definition_search"
android:textSize="18sp"
android:autofillHints="no"
android:id="@+id/shortform_definition_search_field"
android:layout_weight="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/shortform_definition_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/shortform_definition_search_field"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/shortform_definition_error"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/white"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/shortform_definition_search_field"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
>
<TextView
android:id="@+id/shortform_definition_error_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
tools:text="Error"
/>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:visibility="gone"
android:id="@+id/shortform_definition_loading"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/white"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/shortform_definition_search_field"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
>
<ProgressBar
android:id="@+id/shortform_definition_loading_bar"
android:indeterminate="true"
android:indeterminateBehavior="repeat"
android:layout_width="36dp"
android:layout_height="36dp"
/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_longform_container"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_card"
android:elevation="2dp"
android:orientation="vertical">
<TextView
android:id="@+id/item_longform_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:textSize="18sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Adenovirus receptor" />
<TextView
android:id="@+id/item_longform_since"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@+id/item_longform_name"
app:layout_constraintTop_toBottomOf="@+id/item_longform_name"
tools:text="Since: 1997" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/shortform_navigation"
app:startDestination="@id/shortformFragment"
>
<fragment
android:id="@+id/shortformFragment"
android:name="dev.carlos.shortform.feature.ShortformFragment"
tools:layout="@layout/fragment_shortform_definition"
android:label="Acronyms"
/>
</navigation>

View File

@@ -0,0 +1,19 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Acronyms"
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor"
tools:targetApi="l">?attr/colorPrimaryVariant
</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,5 @@
<resources>
<string name="item_longform_since">Desde %1$d</string>
<string name="shortform_definition_search">Buscar acrónimo</string>
<string name="shortform_definition_no_definition">No existe una definición</string>
</resources>

View File

@@ -0,0 +1,19 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Acronyms"
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor"
tools:targetApi="l">?attr/colorPrimaryVariant
</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

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