Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update lifecyle example #14

Merged
merged 3 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build (Compile, lint, checkApi, test, etc)
run: ./gradlew build
run: ./gradlew --no-daemon build
- name: Archive build reports artifacts
if: always()
uses: actions/upload-artifact@v2
Expand All @@ -42,7 +42,7 @@ jobs:
- name: Ensure documentation is up to date
id: doc-check
run: |
./gradlew dokka
./gradlew --no-daemon dokka
# the next command will list documentation files out of date. Please run ./gradlew dokka
git diff --exit-code --name-only || echo ::set-output name=status::failure
- name: Check which documentation files have changed
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build (Compile, lint, checkApi, test, etc)
run: ./gradlew -DVERSION_OVERRIDE=${{ steps.parse_tag.outputs.VERSION }} build
run: ./gradlew --no-daemon -DVERSION_OVERRIDE=${{ steps.parse_tag.outputs.VERSION }} build
- name: Archive build reports artifacts
if: always()
uses: actions/upload-artifact@v2
Expand All @@ -47,7 +47,7 @@ jobs:
- name: Ensure documentation is up to date
id: doc-check
run: |
./gradlew dokka
./gradlew --no-daemon dokka
# the next command will list documentation files out of date. Please run ./gradlew dokka
git diff --exit-code --name-only || echo ::set-output name=status::failure
- name: Check which documentation files have changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,106 +3,91 @@ package net.pedroloureiro.mvflow.samples.android.screens.lifecycle
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.broadcast
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import net.pedroloureiro.mvflow.MVFlow
import net.pedroloureiro.mvflow.samples.android.databinding.LifecycleActivityBinding
import net.pedroloureiro.mvflow.samples.android.screens.dummydialog.DummyDialogActivity
import net.pedroloureiro.mvflow.samples.android.screens.lifecycle.LifecycleMVFlow.Action
import net.pedroloureiro.mvflow.samples.android.screens.lifecycle.LifecycleMVFlow.Effect
import net.pedroloureiro.mvflow.samples.android.screens.lifecycle.LifecycleMVFlow.State

class LifecycleActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
logLifeCycle(if (savedInstanceState == null) "onCreate" else "onCreate with saved instance state")
title = "Advanced lifecycle"
val binding = LifecycleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)

val view = object : MVFlow.View<State, Action> {
override fun render(state: State) {
binding.normalCounter.text = state.normalCounter.toString()
binding.startedCounter.text = state.startedCounter.toString()
binding.resumedCounter.text = state.resumedCounter.toString()
/*
Note: this is not the normal way to use this library. This is just a contrived example to show the difference
between launch and launchWhenResumed (and other similar methods)
*/
val viewNormal = StateActionView(binding.normalCounter, "normal")
val mvFlowNormal = LifecycleMVFlow.create(lifecycleScope)
val viewStarted = StateActionView(binding.startedCounter, "started")
val mvFlowStarted = LifecycleMVFlow.create(lifecycleScope)
val viewResumed = StateActionView(binding.resumedCounter, "resumed")
val mvFlowResumed = LifecycleMVFlow.create(lifecycleScope)

binding.timersRunning.isChecked = state.timersRunning
val initialActions = listOf(Action.StartCounter)
lifecycleScope.launch {
mvFlowNormal.takeView(this, viewNormal, initialActions)
}
lifecycleScope.launchWhenStarted {
mvFlowStarted.takeView(this, viewStarted, initialActions)
}
lifecycleScope.launchWhenResumed {
mvFlowResumed.takeView(this, viewResumed, initialActions)
}

binding.delayedToggleTimers.isEnabled = state.delayedToggleWaiting.not()
binding.toggleProgressBar.visibility = if (state.delayedToggleWaiting) View.VISIBLE else View.INVISIBLE
}
binding.openDialog.setOnClickListener {
DummyDialogActivity.launch(this)
}
}

override fun actions(): Flow<Action> = callbackFlow {
binding.delayedToggleTimers.setOnClickListener {
offer(Action.ToggleTimersDelayed)
}
binding.openDialog.setOnClickListener {
offer(Action.OpenDialog)
}
binding.resetTimers.setOnClickListener {
offer(Action.ResetTimers)
}
binding.timersRunning.setOnCheckedChangeListener { _, checked ->
offer(Action.SetTimers(checked))
}
class StateActionView(private val textView: TextView, private val name: String) : MVFlow.View<State, Action> {
override fun render(state: State) {
Log.d("MYAPP", "lifecycle counter updated for $name with value ${state.counter}")
textView.text = state.counter.toString()
}

// these are not really actions coming from the view, but a proof of concept to allow to see the
// different ways you can use lifecycles to observe state updates how you want them.
//
// This sample highlighted some issues with the current API and we will address that soon.
val ticker = tickerBroadcastChannel()
lifecycleScope.launchWhenResumed {
ticker.openSubscription().consumeEach {
offer(Action.TickResumed)
}
}
override fun actions(): Flow<Action> = emptyFlow()
}

lifecycleScope.launch {
ticker.openSubscription().consumeEach {
offer(Action.TickNormal)
}
}
lifecycleScope.launchWhenStarted {
ticker.openSubscription().consumeEach {
offer(Action.TickStarted)
}
}
awaitClose()
}
}
override fun onStart() {
super.onStart()
logLifeCycle("onStart")
}

val mvFlow = LifecycleMVFlow.create(lifecycleScope)
lifecycleScope.launch {
mvFlow.takeView(this, view)
}
override fun onResume() {
super.onResume()
logLifeCycle("onResume")
}

lifecycleScope.launch {
mvFlow.observeEffects().filterIsInstance<Effect.OpenDialog>()
.collect {
DummyDialogActivity.launch(this@LifecycleActivity)
}
}
override fun onPause() {
logLifeCycle("onPause")
super.onPause()
}

override fun onStop() {
logLifeCycle("onStop")
super.onStop()
}

override fun onDestroy() {
logLifeCycle("onDestroy")
super.onDestroy()
}

@OptIn(ObsoleteCoroutinesApi::class)
private fun tickerBroadcastChannel(): BroadcastChannel<Unit> {
return ticker(
delayMillis = 1000,
initialDelayMillis = 0
).broadcast(capacity = Channel.CONFLATED)
private fun logLifeCycle(step: String) {
Log.d("MYAPP", "lifecycle step $step ${javaClass.simpleName}@${hashCode().toString(16)}")
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,92 +2,66 @@ package net.pedroloureiro.mvflow.samples.android.screens.lifecycle

import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import net.pedroloureiro.mvflow.HandlerWithEffects
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.broadcast
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.transform
import net.pedroloureiro.mvflow.Handler
import net.pedroloureiro.mvflow.MVFlow
import net.pedroloureiro.mvflow.Reducer

object LifecycleMVFlow {
data class State(
val normalCounter: Int = 0,
val startedCounter: Int = 0,
val resumedCounter: Int = 0,
val timersRunning: Boolean = false,
val delayedToggleWaiting: Boolean = false
val counter: Int = 0
)

sealed class Action {
data class SetTimers(val running: Boolean) : Action()
object ToggleTimersDelayed : Action()
object OpenDialog : Action()
object ResetTimers : Action()
object TickNormal : Action()
object TickStarted : Action()
object TickResumed : Action()
object StartCounter : Action()
}

sealed class Mutation {
data class SetTimersRunning(val running: Boolean) : Mutation()
data class DelayedToggleWaiting(val waiting: Boolean) : Mutation()

/**
* A tick, meant to increment the timers
*/
object TickNormal : Mutation()
object TickStarted : Mutation()
object TickResumed : Mutation()

object ResetTimers : Mutation()
object ToggleTimers : Mutation()
object Tick : Mutation()
}

sealed class Effect {
object OpenDialog : Effect()
}

val handler: HandlerWithEffects<State, Action, Mutation, Effect> = { _, action, effects ->
when (action) {
Action.OpenDialog -> flow {
effects.send(Effect.OpenDialog)
// empty flow - we will listen externally to this and act there
}
/*
Note: this is not the normal way to use this library. This is just a contrived example to show the difference
between launch and launchWhenResumed (and other similar methods)
*/

is Action.SetTimers -> flowOf(Mutation.SetTimersRunning(action.running))
Action.ToggleTimersDelayed -> flow {
emit(Mutation.DelayedToggleWaiting(true))
delay(5000)
emit(Mutation.ToggleTimers)
emit(Mutation.DelayedToggleWaiting(false))
fun createHandler(): Handler<State, Action, Mutation> =
{ _, action ->
when (action) {
Action.StartCounter ->
tickerBroadcastChannel
.openSubscription()
.consumeAsFlow()
.transform {
emit(Mutation.Tick)
}
}

Action.ResetTimers -> flowOf(Mutation.ResetTimers)

Action.TickNormal -> flowOf(Mutation.TickNormal)
Action.TickStarted -> flowOf(Mutation.TickStarted)
Action.TickResumed -> flowOf(Mutation.TickResumed)
}
}

@OptIn(ObsoleteCoroutinesApi::class)
private val tickerBroadcastChannel =
ticker(
delayMillis = 1000,
initialDelayMillis = 0
).broadcast(Channel.CONFLATED)

val reducer: Reducer<State, Mutation> = { state, mutation ->
when (mutation) {
is Mutation.SetTimersRunning -> state.copy(timersRunning = mutation.running)
is Mutation.DelayedToggleWaiting -> state.copy(delayedToggleWaiting = mutation.waiting)
Mutation.TickNormal -> state.copy(normalCounter = state.normalCounter + 1)
Mutation.TickStarted -> state.copy(startedCounter = state.startedCounter + 1)
Mutation.TickResumed -> state.copy(resumedCounter = state.resumedCounter + 1)
Mutation.ResetTimers -> state.copy(normalCounter = 0, startedCounter = 0, resumedCounter = 0)
Mutation.ToggleTimers -> state.copy(timersRunning = state.timersRunning.not())
Mutation.Tick -> state.copy(counter = state.counter + 1)
}
}

fun create(coroutineScope: CoroutineScope) =
MVFlow(
State(),
handler,
createHandler(),
reducer,
coroutineScope,
defaultLogger = { Log.d("MVFLOW", it) }
defaultLogger = { Log.d("MYAPP", it) }
)
}
Loading