3

I'm trying to learn the Arrow library and improve my functional programming by transitioning some of my Android Kotlin code from more imperative style to functional style. I've been doing a type of MVI programming in the application to make testing simpler.

"Traditional" Method

ViewModel

My view model has a LiveData of the view's state plus a public method to pass user interactions from the view to the viewmodel so the view model can update state in whatever way is appropriate.

class MyViewModel: ViewModel() {
    val state = MutableLiveData(MyViewState()) // MyViewState is a data class with relevant data

    fun instruct(intent: MyIntent) { // MyIntent is a sealed class of data classes representing user interactions
        return when(intent) {
            is FirstIntent -> return viewModelScope.launch(Dispatchers.IO) {
                val result = myRoomRepository.suspendFunctionManipulatingDatabase(intent.myVal)
                updateStateWithResult(result)
            }.run { Unit }
            is SecondIntent -> return updateStateWithResult(intent.myVal)
        }
    }
}

Activity

The Activity subscribes to the LiveData and, on changes to state, it runs a render function using the state. The activity also passes user interactions to the view model as intents (not to be confused with Android's Intent class).

class MyActivity: AppCompatActivity() {
   private val viewModel = MyViewModel()

   override fun onCreateView() {
      viewModel.state.observe(this, Observer { render(it) })
      myWidget.onClickObserver = {
         viewModel.instruct(someIntent)
      }
   }

   private fun render(state: MyViewState) { /* update view with state */ }
}

Arrow.IO Functional Programming

I'm having trouble finding examples that aren't way over my head using Arrow's IO monad to make impure functions with side effects obvious and unit-testable.

View Model

So far I have turned my view model into:

class MyViewModel: ViewModel() {
    // ...

    fun instruct(intent: MyIntent): IO<Unit> {
        return when(intent) {
            is FirstIntent -> IO.fx {
                val (result) = effect { myRoomRepository.suspendFunctionManipulatingDatabase(intent.myVal) }
                updateStateWithResult(result)
            }
            is SecondIntent -> IO { updateStateWithResult(intent.myVal) }
        }
    }
}

I do not know how I am supposed to make this IO stuff run in Dispatcher.IO like I've been doing with viewModelScope.launch. I can't find an example for how to do this with Arrow. The ones that make API calls all seem to be something other than Android apps, so there is no guidance about Android UI vs IO threads.

View model unit test

Now, because one benefit I'm seeing to this is that when I write my view model's unit tests, I can have a test. If I mock the repository in order to check whether suspendFunctionManipulatingDatabase is called with the expected parameter.

@Test
fun myTest() {
    val result: IO<Unit> = viewModel.instruct(someIntent)
    result.unsafeRunSync()
    // verify suspendFunctionManipulatingDatabase argument was as expected
}

Activity

I do not know how to incorporate the above into my Activity.

class MyActivity: AppCompatActivity() {
   private val viewModel = MyViewModel()

   override fun onCreateView() {
      viewModel.state.observe(this, Observer { render(it) })
      myWidget.onClickObserver = {
         viewModel.instruct(someIntent).unsafeRunSync() // Is this how I should do it?
      }
   } 

   // ...
}

My understanding is anything in an IO block does not run right away (i.e., it's lazy). You have to call attempt() or unsafeRunSync() to get the contents to be evaluated.

  1. Calling viewModel.instruct from Activity means I need to create some scope and invoke in Dispatchers.IO right? Is this Bad(TM)? I was able to confine coroutines completely to the view model using the "traditional" method.

  2. Where do I incorporate Dispatchers.IO to replicate what I did with viewModelScope.launch(Dispatchers.IO)?

  3. Is this the way you're supposed to structure a unit test when using Arrow's IO?

user1713450
  • 1,307
  • 7
  • 18
  • 1
    Check this article I wrote to outline program structure for Android: https://www.pacoworks.com/2019/12/15/kotlin-coroutines-with-arrow-fx/ The section on "Program structure in fx" should give you some insight! – El Paco Dec 29 '19 at 20:23

1 Answers1

5

That's a really good post to read indeed. I'd also recommend digging into this sample app I wrote that is using ArrowFx also.

https://github.com/JorgeCastilloPrz/ArrowAndroidSamples

  • Note how we build the complete program using fx and returning Kind at all levels in our architecture. That makes the code polymorphic to the type F, so you can run it using different runtime data types for F at will, depending on the environment. In this case we end up running it using IO at the edges. That's the activity in this case, but could also be the application class or a fragment. Think about this as what'd be the entry points to your apps. If we were talking about jvm programs the equivalent would be main(). This is just an example of how to write polymorphic programs, but you could use IO.fx instead and return IO everywhere, if you want to stay simpler.
  • Note how we use continueOn() in the data source inside the fx block to leave and come back to the main thread. Coroutine context changes are explicit in ArrowFx, so the computation jumps to the passed thread right after the continueOn until you deliberately switch again to a different one. That intentionally makes thread changes explicit.
  • You could inject those dispatchers to use different ones in tests. Hopefully I can provide examples of this soon in the repo, but you can probably imagine how this would look.
  • For the syntax on how to write tests note that your program will return Kind (if you go polymorphic) or IO, so you would unsafeRunSync it from tests (vs unsafeRunAsync or unsafeRunAsyncCancellable in production code since Android needs it to be asynchronous). That is because we want our test to be synchronous and also blocking (for the latter we need to inject the proper dispatchers).
  • Current caveats: The solution proposed in the repo still doesn't care of cancellation, lifecycle or surviving config changes. That's something I'd like to address soon. Using ViewModels with a hybrid style might have a chance. This is Android so I'd not fear hybrid styles if that brings better productivity. Another alternative I've got in mind would maybe be something a bit more functional. ViewModels end up retaining themselves using the retain config state existing APIs under the hood by using the ViewModelStore. That ultimately sounds like a simple cache that is definitely a side effect and could be implemented wrapped into IO. I want to give a thought to this.

I would definitely also recommend reading the complete ArrowFx docs for better understanding: https://arrow-kt.io/docs/fx/ I think it would be helpful.

For more thoughts on approaches using Functional Programming and Arrow to Android you can take a look to my blog https://jorgecastillo.dev/ my plan is to write deep content around this starting 2020, since there's a lot of people interested.

In the other hand, you can find me or any other Arrow team maintainers in the Kotlinlang JetBrains Slack, where we could have more detailed conversations or try to resolve any doubts you can have https://kotlinlang.slack.com/

As a final clarification: Functional Programming is just a paradigm that resolves generic concerns like asynchrony, threading, concurrency, dependency injection, error handling, etc. Those problems can be found on any program, regardless of the platform. Even within an Android app. That is why FP is an option as valid for mobile as any other one, but we are still into explorations to provide the best APIs to fulfill the usual Android needs in a more ergonomic way. We are in the process of exploration in this sense, and 2020 is going to be a very promising year.

Hopefully this helped! Your thoughts seem to be well aligned with how things should work in this approach overall.

Jorge Castillo
  • 555
  • 4
  • 7