Skip to content

Commit

Permalink
refactor: replace state machine (#52)
Browse files Browse the repository at this point in the history
* refactor: replace state machine

* fix: contributing web url

* fix: add missing ViewModel inheritance

* fix: tests
  • Loading branch information
adrielcafe authored Oct 11, 2020
1 parent 7190f38 commit 14b13a6
Show file tree
Hide file tree
Showing 31 changed files with 187 additions and 925 deletions.
7 changes: 5 additions & 2 deletions buildSrc/src/main/java/dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ object Versions {
const val assertJ29 = "2.9.1"
const val espresso = "3.1.0"
const val androidJUnit = "1.1.0"
const val mockitoKT = "2.0.0-RC1"
const val mockito = "2.19.0"
const val mockitoKT = "2.2.0"
const val mockito = "3.5.13"

const val kodeinDI = "6.0.1"
const val slf4j = "1.7.25"
const val fabric = "1.26.1"
const val coroutine = "1.3.2"
const val dalek = "1.0.2"
}

object Dependencies {
Expand Down Expand Up @@ -88,6 +89,8 @@ object Dependencies {
val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutine}"
val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutine}"
val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutine}"

val dalek = "com.github.adrielcafe:dalek:${Versions.dalek}"
}

object BuildPlugins {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.jcaique.dialetus.domain.regions

import com.jcaique.dialetus.domain.errors.GatewayIntegrationIssues
import com.jcaique.dialetus.domain.errors.UnexpectedResponse
import com.jcaique.dialetus.domain.models.Region

interface RegionsService {

@Throws(UnexpectedResponse::class, GatewayIntegrationIssues::class)
suspend fun fetchRegions(): List<Region>
}
1 change: 1 addition & 0 deletions presentation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {

implementation Dependencies.coroutines
implementation Dependencies.coroutinesAndroid
implementation Dependencies.dalek

AndroidModule.main.forEach { implementation it }
AndroidModule.unitTesting.forEach { testImplementation it }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.jcaique.dialetus.presentation.contributing

internal interface ContributingInteraction {
object OpenAndroid : ContributingInteraction
object OpenWeb : ContributingInteraction
internal sealed class ContributingInteraction(val url: String) {
object OpenAndroid : ContributingInteraction(ContributingConst.ANDROID)
object OpenWeb : ContributingInteraction(ContributingConst.API)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.jcaique.dialetus.presentation.R
import com.jcaique.dialetus.utils.dataflow.UnsupportedUserInteraction
import kotlinx.android.synthetic.main.regions_menu_view.*

internal class ContributingNavigation : BottomSheetDialogFragment() {
Expand All @@ -33,17 +32,8 @@ internal class ContributingNavigation : BottomSheetDialogFragment() {
}
}

private fun handleClick(interaction: ContributingInteraction) {
when (interaction) {
ContributingInteraction.OpenAndroid -> navigate(
ContributingConst.ANDROID
)
ContributingInteraction.OpenWeb -> navigate(
ContributingConst.API
)
else -> throw UnsupportedUserInteraction
}
}
private fun handleClick(interaction: ContributingInteraction) =
navigate(interaction.url)

private fun navigate(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package com.jcaique.dialetus.presentation.di

import com.jcaique.dialetus.presentation.dialects.DialectsPresentation
import com.jcaique.dialetus.presentation.dialects.DialectsViewModel
import com.jcaique.dialetus.presentation.regions.RegionsPresentation
import com.jcaique.dialetus.presentation.regions.RegionsViewModel
import com.jcaique.dialetus.utils.KodeinTags
import com.jcaique.dialetus.utils.extensions.newStateContainer
import com.jcaique.dialetus.utils.extensions.newStateMachine
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
Expand All @@ -15,21 +10,13 @@ import org.kodein.di.generic.provider
val presentationModule = Kodein.Module(name = "presentation") {

bind() from provider {
val stateContainer = newStateContainer<RegionsPresentation>(KodeinTags.hostActivity)
val stateMachine = newStateMachine(stateContainer)

RegionsViewModel(
service = instance(),
machine = stateMachine
service = instance()
)
}

bind() from provider {
val stateContainer = newStateContainer<DialectsPresentation>(KodeinTags.hostActivity)
val stateMachine = newStateMachine(stateContainer)

DialectsViewModel(
machine = stateMachine,
service = instance()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.dalek.DalekEvent
import cafe.adriel.dalek.Failure
import cafe.adriel.dalek.Finish
import cafe.adriel.dalek.Start
import cafe.adriel.dalek.Success
import cafe.adriel.dalek.collectIn
import com.jcaique.dialetus.domain.models.Dialect
import com.jcaique.dialetus.domain.models.Region
import com.jcaique.dialetus.presentation.R
import com.jcaique.dialetus.presentation.contributing.ContributingConst
import com.jcaique.dialetus.utils.dataflow.ViewState
import com.jcaique.dialetus.presentation.ktx.value
import com.jcaique.dialetus.utils.extensions.selfInject
import com.jcaique.dialetus.utils.extensions.share
import com.jcaique.dialetus.utils.ui.DividerItemDecoration
Expand All @@ -22,16 +28,14 @@ import kotlinx.android.synthetic.main.activity_regions.emptyStateView
import kotlinx.android.synthetic.main.activity_regions.errorStateView
import kotlinx.android.synthetic.main.activity_regions.loadingStateView
import kotlinx.android.synthetic.main.error_state_layout.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance

class DialectsActivity : AppCompatActivity(), KodeinAware {

companion object {
const val EXTRA_REGION = "region"

fun newInstance(activity: Activity, region: Region) = activity.run {
startActivity(
Intent(this, DialectsActivity::class.java)
Expand All @@ -41,73 +45,68 @@ class DialectsActivity : AppCompatActivity(), KodeinAware {
}

override val kodein = selfInject()

private val viewModel by kodein.instance<DialectsViewModel>()

private val region by lazy { intent.getSerializableExtra(EXTRA_REGION) as Region }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dialects)
init()
setupToolbar()
setupViews()
showDialects()
}

private fun init() {
viewModel.handle(ShowDialects(region))

lifecycleScope.launch {
viewModel.bind().collect { handle(it) }
private fun setupViews() {
dialectsToolBar.apply {
title = region.name.capitalize()
setNavigationOnClickListener { finish() }
}

dialectsList.run {
layoutManager = LinearLayoutManager(this@DialectsActivity)
addItemDecoration(
DividerItemDecoration(this@DialectsActivity)
.also {
.also {
ContextCompat
.getDrawable(this@DialectsActivity, R.drawable.divider)
?.let(it::setDrawable)
?.let(it::setDrawable)
}
)
}

dialectsFilter.addTextChangedListener { filterDialects() }
}

private fun setupToolbar() {
dialectsToolBar.apply {
title = region.name.capitalize()
setNavigationOnClickListener { finish() }
}
}

private fun handle(state: ViewState<DialectsPresentation>) {
controlVisibilities(state)
private fun showDialects() {
viewModel
.getDialects(region)
.collectIn(lifecycleScope, ::handleResult)
}

when (state) {
is ViewState.Success -> setupContent(state.value)
is ViewState.Failed -> setupRetry()
}
private fun filterDialects() {
viewModel
.filterDialects(query = dialectsFilter.value)
.collectIn(lifecycleScope, ::handleResult)
}

private fun setupRetry() {
errorStateView.let {
tryAgainBtn.setOnClickListener {
viewModel.handle(ShowDialects(region))
}
private suspend fun handleResult(event: DalekEvent<DialectsPresentation>) {
controlVisibilities(event)

when (event) {
is Success -> setupContent(event.value)
is Failure -> setupRetry()
}
}

private fun setupContent(value: DialectsPresentation) {
dialectsList.adapter =
DialectAdapter(value, ::shareDialect)
private fun setupContent(presentation: DialectsPresentation) {
dialectsList.adapter = DialectAdapter(presentation, ::shareDialect)
}

private fun filterDialects() {
dialectsFilter.text
?.toString()
?.let(::FilterDialects)
?.let(viewModel::handle)

private fun setupRetry() {
tryAgainBtn.setOnClickListener {
showDialects()
}
}

private fun shareDialect(dialect: Dialect) = dialect.run {
Expand All @@ -126,11 +125,13 @@ class DialectsActivity : AppCompatActivity(), KodeinAware {
.trimMargin()
.share(this@DialectsActivity)
}

private fun controlVisibilities(state: ViewState<DialectsPresentation>) {
loadingStateView.isVisible = state is ViewState.Loading
emptyStateView.isVisible = state is ViewState.Success && state.value.dialects.isEmpty()
errorStateView.isVisible = state is ViewState.Failed
dialectsList.isVisible = state is ViewState.Success && state.value.dialects.isNotEmpty()

private fun controlVisibilities(event: DalekEvent<DialectsPresentation>) {
if (event is Finish) return

loadingStateView.isVisible = event is Start
emptyStateView.isVisible = event is Success && event.value.dialects.isEmpty()
errorStateView.isVisible = event is Failure
dialectsList.isVisible = event is Success && event.value.dialects.isNotEmpty()
}
}
Original file line number Diff line number Diff line change
@@ -1,65 +1,33 @@
package com.jcaique.dialetus.presentation.dialects

import androidx.lifecycle.ViewModel
import cafe.adriel.dalek.Dalek
import cafe.adriel.dalek.DalekEvent
import com.jcaique.dialetus.domain.dialects.DialectsService
import com.jcaique.dialetus.domain.models.Dialect
import com.jcaique.dialetus.utils.dataflow.StateMachine
import com.jcaique.dialetus.utils.dataflow.StateTransition
import com.jcaique.dialetus.utils.dataflow.UnsupportedUserInteraction
import com.jcaique.dialetus.utils.dataflow.UserInteraction
import com.jcaique.dialetus.utils.extensions.normalize
import kotlinx.coroutines.coroutineScope
import com.jcaique.dialetus.domain.models.Region
import com.jcaique.dialetus.presentation.ktx.matches
import kotlinx.coroutines.flow.Flow

internal class DialectsViewModel(
private val machine: StateMachine<DialectsPresentation>,
private val service: DialectsService
) {
) : ViewModel() {

// TODO cache dialects elsewhere
private var dialects = emptyList<Dialect>()

fun bind() = machine.states()

fun handle(interaction: UserInteraction) {
interpret(interaction)
.let(machine::consume)
}

private fun interpret(interaction: UserInteraction) =
when (interaction) {
is ShowDialects -> StateTransition(
::showDialects,
interaction
)
is FilterDialects -> StateTransition(
::filterDialects,
interaction
)
else -> throw UnsupportedUserInteraction

fun getDialects(region: Region): Flow<DalekEvent<DialectsPresentation>> =
Dalek {
service
.getDialectsBy(region.name.toLowerCase())
.also(::dialects::set)
.let(::DialectsPresentation)
}

private suspend fun showDialects(
parameters: StateTransition.Parameters
): DialectsPresentation = coroutineScope {
val interaction = parameters as ShowDialects

service
.getDialectsBy(interaction.region.name.toLowerCase())
.let(::DialectsPresentation)
}

private suspend fun filterDialects(
parameters: StateTransition.Parameters
): DialectsPresentation = coroutineScope {
val interaction = parameters as FilterDialects

dialects
.filter {
it.dialect
.normalize()
.contains(
other = interaction.query.normalize(),
ignoreCase = true
)
}
.let(::DialectsPresentation)
}
fun filterDialects(query: String): Flow<DalekEvent<DialectsPresentation>> =
Dalek {
dialects
.filter { it.matches(query) }
.let(::DialectsPresentation)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.jcaique.dialetus.presentation.ktx

import com.jcaique.dialetus.domain.models.Dialect
import com.jcaique.dialetus.utils.extensions.normalize

internal fun Dialect.matches(query: String): Boolean =
dialect
.normalize()
.contains(other = query.normalize(), ignoreCase = true)
Loading

0 comments on commit 14b13a6

Please sign in to comment.