Slide 1

Slide 1 text

Ubiratan Soares August / 2019 EXPLORING COROUTINES

Slide 2

Slide 2 text

How can we architect with Coroutines ?

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

DataSource Use case ViewModel collaborates with

Slide 6

Slide 6 text

DataSource Use case ViewModel External World User Interface Observable Observable Observable data flow format

Slide 7

Slide 7 text

DataSource Use case ViewModel External World User Interface Observable Observable LiveData data flow format

Slide 8

Slide 8 text

DataSource Use case ViewModel External World User Interface suspend fun (T) suspend fun (T) LiveData data flow format

Slide 9

Slide 9 text

DataSource Use case ViewModel External World User Interface suspend fun (T) suspend fun (T) LiveData data flow format

Slide 10

Slide 10 text

Unidirectional Data Flow

Slide 11

Slide 11 text

Activity( ) : ViewProtocol doSomething ( ) Presenter View Protocol fun handle(d : Data) fun showLoading( ) fun showError( ) fun doSomething( ) override fun handle(d : Data) override fun showLoading( ) override fun showError( ) doAnotherThing ( ) fun doAnotherThing( ) method call

Slide 12

Slide 12 text

Activity( ) doSomething ( ) ViewModel fun doSomething( ) : LiveData fun handle(d : Data) fun showLoading( ) fun showError( ) doAnotherThing ( ) fun doAnotherThing( ) : LiveData method call LiveData Observer

Slide 13

Slide 13 text

Activity( ) doSomething ( ) ViewModel fun doSomething( ) : LiveData fun handle(d : Data) fun showLoading( ) fun showError( ) doAnotherThing ( ) fun doAnotherThing( ) : LiveData method call fun justOneMoreThing( ) : LiveData justOneMoreThing ( ) LiveData Observer

Slide 14

Slide 14 text

Activity( ) doSomething ( ) ViewModel doAnotherThing ( ) method call justOneMoreThing ( ) LiveData Observer LiveData Observer LiveData Observer LiveData LiveData LiveData

Slide 15

Slide 15 text

Activity( ) doSomething ( ) ViewModel doAnotherThing ( ) method call justOneMoreThing ( ) LiveData Observer LiveData Observer LiveData Observer LiveData LiveData LiveData

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

DataSource Use case ViewModel External World User Interface suspend fun (T) suspend fun (T) suspend fun (T) data flow format

Slide 18

Slide 18 text

DataSource Use case ViewModel External World User Interface suspend fun (T) suspend fun (T) suspend fun (T) data flow format How can we push multiple states to the View level ???

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

DataSource Use case ViewModel External World User Interface suspend fun (T) suspend fun (T) Flow

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

Activity( ) handle(someInteraction) ViewModel method call FlowConsumer StateMachine fun handle(i : UserInteraction) bind( ) fun bind( ) : Flow fun handle(d : Data) fun showLoading( ) fun showError( ) data flow format Flow

Slide 28

Slide 28 text

Started bind( )

Slide 29

Slide 29 text

Started Loading interaction (suspend wrapper) bind( )

Slide 30

Slide 30 text

Started Loading Success interaction (suspend wrapper) bind( ) suspend fun resumed

Slide 31

Slide 31 text

Started Loading Error interaction (suspend wrapper) bind( ) suspend fun resumed

Slide 32

Slide 32 text

Started Loading Success Error interaction (suspend wrapper) bind( ) suspend fun resumed suspend fun resumed

Slide 33

Slide 33 text

Started Loading Success Error interaction (suspend wrapper) interaction (suspend wrapper) interaction (suspend wrapper) bind( ) suspend fun resumed suspend fun resumed

Slide 34

Slide 34 text

sealed class StateTransition { interface Parameters class Unparametrized internal constructor( val task: suspend () !" T ) : StateTransition() class Parametrized internal constructor( val task: suspend (Parameters) !" T, val parameters: Parameters ) : StateTransition() }

Slide 35

Slide 35 text

sealed class StateTransition { interface Parameters class Unparametrized internal constructor( val task: suspend () !" T ) : StateTransition() class Parametrized internal constructor( val task: suspend (Parameters) !" T, val parameters: Parameters ) : StateTransition() }

Slide 36

Slide 36 text

class StateMachine( private val container: StateContainer, private val executor: TaskExecutor ) { fun states() = container.observableStates() fun consume(execution: StateTransition) = executor.execute { wrapWithStates(execution) } // Continue on next slide }

Slide 37

Slide 37 text

class StateMachine(#$%) { private suspend fun wrapWithStates(execution: StateTransition) { val first = executionStarted() moveTo(first) val next = executeWith(execution) moveTo(next) } private suspend fun executeWith(transition: StateTransition) = try { val execution = when (transition) { is Unparametrized !" transition.task.invoke() is Parametrized !" with(transition) { this.task.invoke(parameters) } } Success(execution) } catch (error: Throwable) { Failed(error) } }

Slide 38

Slide 38 text

class StateMachine(#$%) { private suspend fun wrapWithStates(execution: StateTransition) { val first = executionStarted() moveTo(first) val next = executeWith(execution) moveTo(next) } private suspend fun executeWith(transition: StateTransition) = try { val execution = when (transition) { is Unparametrized !" transition.task.invoke() is Parametrized !" with(transition) { this.task.invoke(parameters) } } Success(execution) } catch (error: Throwable) { Failed(error) } }

Slide 39

Slide 39 text

class StateMachine(#$%) { private suspend fun wrapWithStates(execution: StateTransition) { val first = executionStarted() moveTo(first) val next = executeWith(execution) moveTo(next) } private suspend fun executeWith(transition: StateTransition) = try { val execution = when (transition) { is Unparametrized !" transition.task.invoke() is Parametrized !" with(transition) { this.task.invoke(parameters) } } Success(execution) } catch (error: Throwable) { Failed(error) } }

Slide 40

Slide 40 text

A complete data flow

Slide 41

Slide 41 text

interface ChuckNorrisDotIO { @GET("/jokes/categories") suspend fun categories(): RawCategories @GET("/jokes/search") suspend fun search(@Query("query") query: String): RawSearch companion object { const val API_URL = "https:&'api.chucknorris.io" } }

Slide 42

Slide 42 text

class FactsInfrastructure( private val rest: ChuckNorrisDotIO ) : RemoteFactsService { override suspend fun availableCategories() = managedExecution { rest.categories().asRelatedCategories() } override suspend fun fetchFacts(searchTerm: String) = managedExecution { rest.search(searchTerm).asChuckNorrisFacts() } }

Slide 43

Slide 43 text

class FetchFacts( private val factsService: RemoteFactsService ) { suspend fun search(term: String) = when { term.isEmpty() !" throw EmptyTerm else !" { val facts = factsService.fetchFacts(term) if (facts.isEmpty()) throw NoResultsFound else facts } } }

Slide 44

Slide 44 text

class FactsViewModel( private val factsFetcher: FetchFacts, private val queryManager: ManageSearchQuery, private val machine: StateMachine ) { fun bind() = machine.states() fun handle(interaction: UserInteraction) = interpret(interaction) .let { transition !" machine.consume(transition) } private fun interpret(interaction: UserInteraction) = when (interaction) { OpenedScreen, RequestedFreshContent !" StateTransition(()showFacts) else !" throw UnsupportedUserInteraction }

Slide 45

Slide 45 text

private fun interpret(interaction: UserInteraction) = when (interaction) { OpenedScreen, RequestedFreshContent !" StateTransition(()showFacts) else !" throw UnsupportedUserInteraction } private suspend fun showFacts() = queryManager.actualQuery().let { query !" factsFetcher .search(query) .map { FactDisplayRow(it) } .let { rows !" FactsPresentation(query, rows) } } }

Slide 46

Slide 46 text

private fun interpret(interaction: UserInteraction) = when (interaction) { OpenedScreen, RequestedFreshContent !" StateTransition(::showFacts) else !" throw UnsupportedUserInteraction } private suspend fun showFacts() = queryManager.actualQuery().let { query !" factsFetcher .search(query) .map { FactDisplayRow(it) } .let { rows !" FactsPresentation(query, rows) } } }

Slide 47

Slide 47 text

class FactsActivity : AppCompatActivity(), KodeinAware { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setup() } private fun setup() { setSupportActionBar(factsToolbar) factsRecyclerView.layoutManager = LinearLayoutManager(this) factsSwipeToRefresh.setOnRefreshListener { refresh() } lifecycleScope.launch { viewModel.bind().collect { renderState(it) } } }

Slide 48

Slide 48 text

private fun setup() { setSupportActionBar(factsToolbar) factsRecyclerView.layoutManager = LinearLayoutManager(this) factsSwipeToRefresh.setOnRefreshListener { refresh() } lifecycleScope.launch { viewModel.bind().collect { renderState(it) } } } private fun renderState(state: ViewState) = when (state) { is Failed !" handleError(state.reason) is Success !" showFacts(state.value) is Loading.FromEmpty !" startExecution() is Loading.FromPrevious !" showFacts(state.previous) is FirstLaunch !" loadFacts() } }

Slide 49

Slide 49 text

private fun renderState(state: ViewState) = when (state) { is Failed !" handleError(state.reason) is Success !" showFacts(state.value) is Loading.FromEmpty !" startExecution() is Loading.FromPrevious !" showFacts(state.previous) is FirstLaunch !" loadFacts() } } private fun loadFacts() { viewModel.handle(OpenedScreen) } }

Slide 50

Slide 50 text

Solving Lifecycles with Structured Concurrency

Slide 51

Slide 51 text

State Machine TaskExecutor StateContainer collaborates with class StateMachine( private val container: StateContainer, private val executor: TaskExecutor ) { // Implementation }

Slide 52

Slide 52 text

State Machine TaskExecutor StateContainer collaborates with The State Container registers the current state processed by the State Machine. The implementation is CoroutineScope-aware and uses a ConflatedBroadcastChannel as communication engine to propagate state to consumers. We can build a StateContainer instance with a CoroutineScope differente than the consumer’s scope

Slide 53

Slide 53 text

State Machine TaskExecutor StateContainer collaborates with The task executor defines a model of execution for the state transition : either Concurrent or Synchronous

Slide 54

Slide 54 text

interface TaskExecutor { fun execute(block: suspend TaskExecutor.() !" Unit): Job class Concurrent( private val scope: CoroutineScope, private val dispatcher: CoroutineDispatcher ) : TaskExecutor { override fun execute(block: suspend TaskExecutor.() !" Unit) = scope.launch(dispatcher) { block.invoke(this@Concurrent) } } class Synchronous(private val scope: CoroutineScope) : TaskExecutor { override fun execute(block: suspend TaskExecutor.() !" Unit) = runBlocking { scope.launch { block.invoke(this@Synchronous) } } } }

Slide 55

Slide 55 text

interface TaskExecutor { fun execute(block: suspend TaskExecutor.() !" Unit): Job class Concurrent( private val scope: CoroutineScope, private val dispatcher: CoroutineDispatcher ) : TaskExecutor { override fun execute(block: suspend TaskExecutor.() !" Unit) = scope.launch(dispatcher) { block.invoke(this@Concurrent) } } class Synchronous(private val scope: CoroutineScope) : TaskExecutor { override fun execute(block: suspend TaskExecutor.() !" Unit) = runBlocking { scope.launch { block.invoke(this@Synchronous) } } } }

Slide 56

Slide 56 text

Executor Coroutine (runs the suspend function) Consumer Coroutine (consume the states) Channel

Slide 57

Slide 57 text

Executor Coroutine (runs the suspend function Scope = AAC ViewModel Consumer Coroutine (consume the states) Scope = Activity Lifecycle Channel

Slide 58

Slide 58 text

class ConfigChangesAwareStateContainer : StateContainer, ViewModel() { private val broadcaster by lazy { ConflatedBroadcastChannel = broadcaster.value override suspend fun store(state: ViewState) { broadcaster.send(state) }

Slide 59

Slide 59 text

class ConfigChangesAwareStateContainer : StateContainer, ViewModel() { private val broadcaster by lazy { ConflatedBroadcastChannel = broadcaster.value override suspend fun store(state: ViewState) { broadcaster.send(state) } Cold flow over Hot Channel

Slide 60

Slide 60 text

class ConfigChangesAwareStateContainer : StateContainer, ViewModel() { private val broadcaster by lazy { ConflatedBroadcastChannel = broadcaster.value override suspend fun store(state: ViewState) { broadcaster.send(state) }

Slide 61

Slide 61 text

companion object { operator fun invoke(host: FragmentActivity): StateContainer { val f = object : ViewModelProvider.Factory { override fun create(klass: Class) = ConfigChangesAwareStateContainer() as Model } val keyClazz = ConfigChangesAwareStateContainer()class.java return ViewModelProviders.of(host, f)[keyClazz] as StateContainer } } }

Slide 62

Slide 62 text

Testing like a pro

Slide 63

Slide 63 text

class FetchCategoriesTests { private val categoriesCache = mock() private val remoteFacts = mock() private lateinit var usecase: FetchCategories private val categories = mutableListOf( RelatedCategory.Available("dev"), RelatedCategory.Available("code") ) @Before fun `before each test`() { usecase = FetchCategories(categoriesCache, remoteFacts) } @Test fun `should fetch from cache from available`() { runBlocking { `given that remote service not available`() `given that cache returns categories`() assertThat(usecase.execute()).isEqualTo(categories) } } }

Slide 64

Slide 64 text

internal class FactsInfrastructureTests { @get:Rule val rule = InfrastructureRule() private lateinit var infrastructure: FactsInfrastructure @Before fun `before each test`() { infrastructure = FactsInfrastructure(rule.api) } @Test fun `should fetch categories`() { rule.defineScenario( status = 200, response = loadFile("200_categories.json") )

Slide 65

Slide 65 text

val expected = listOf( Available("career"), Available("celebrity"), Available("dev") ) val categories = runBlocking { infrastructure.availableCategories() } assertThat(categories).isEqualTo(expected) } private fun simpleSearch() = runBlocking { infrastructure.fetchFacts("Norris") } }

Slide 66

Slide 66 text

class FactsViewModelTests { @get:Rule val helper = CoroutinesTestHelper() private val fetchFacts = mock() private val manageQuery = mock() private lateinit var viewModel: FactsViewModel @Before fun `before each test`() { val stateMachine = StateMachine( executor = TaskExecutor.Synchronous(helper.scope), container = StateContainer.Unbounded(helper.scope) ) viewModel = FactsViewModel(fetchFacts, manageQuery, stateMachine) } @Test fun `should report failure when fetching from remote`() {

Slide 67

Slide 67 text

@Test fun `should report failure when fetching from remote`() { &' Given flowTest(viewModel.bind()) { triggerEmissions { &' When whenever(fetchFacts.search(anyString())) .thenAnswer { throw UnexpectedResponse } &' And viewModel.handle(UserInteraction.OpenedScreen) } afterCollect { emissions !"

Slide 68

Slide 68 text

afterCollect { emissions !" &' Then val viewStates = listOf( FirstLaunch, Loading.FromEmpty, Failed(UnexpectedResponse) ) assertThat(emissions).isEqualTo(viewStates) } } } }

Slide 69

Slide 69 text

class FlowTest(private val parentJob: Job, private val emissions: List) { fun triggerEmissions(action: suspend () !" Job) { runBlocking { action().join() } } fun afterCollect(verification: (List) !" Unit) { parentJob.invokeOnCompletion { verification.invoke(emissions) } } companion object { fun flowTest( target: Flow, scope: CoroutineScope = GlobalScope, block: FlowTest.() !" Unit ) { target.test(scope, block) } } }

Slide 70

Slide 70 text

class FlowTest(private val parentJob: Job, private val emissions: List) { fun triggerEmissions(action: suspend () !" Job) { runBlocking { action().join() } } fun afterCollect(verification: (List) !" Unit) { parentJob.invokeOnCompletion { verification.invoke(emissions) } } companion object { fun flowTest( target: Flow, scope: CoroutineScope = GlobalScope, block: FlowTest.() !" Unit ) { target.test(scope, block) } } }

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

https://speakerdeck.com/ubiratansoares

Slide 73

Slide 73 text

UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy Google Developer Expert for Android / Kotlin Teacher, speaker, etc, etc

Slide 74

Slide 74 text

THANK YOU @ubiratanfsoares ubiratansoares.dev https://br.linkedin.com/in/ubiratanfsoares