Skip to content

Commit 9d5710d

Browse files
authored
Update to use the ViewModel. (#26)
* Migrate todo app to view model. * Add delay to initial task load. * Migrate fragment flow test to use view model. * Remove dependency. * Update ruby.
1 parent 1e48be0 commit 9d5710d

File tree

16 files changed

+77
-58
lines changed

16 files changed

+77
-58
lines changed

Gemfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
source 'https://rubygems.org'
22

3-
ruby '2.4.3'
3+
ruby '2.6.1'
44

55
gem "danger"
66
gem "danger-jacoco"

dependencies.gradle

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ ext {
2626
],
2727
constraintlayout: "androidx.constraintlayout:constraintlayout:1.1.3",
2828
lifecycle : [
29-
compiler: "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion",
30-
runtime : "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion"
29+
compiler : "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion",
30+
runtime : "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion",
31+
extensions: "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
3132
],
3233
recyclerview : "androidx.recyclerview:recyclerview:$androidXVersion",
3334
fragment : [

formula-integration-tests/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838

3939
implementation libraries.kotlin
4040
implementation libraries.androidx.appcompat
41+
implementation libraries.androidx.lifecycle.extensions
4142

4243
testImplementation libraries.junit
4344
testImplementation "com.google.truth:truth:$truthVersion"

formula-integration-tests/src/main/AndroidManifest.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
android:supportsRtl="true"
99
android:theme="@style/AppTheme">
1010

11-
<activity android:name=".TestActivity">
11+
<activity android:name=".TestFlowViewActivity">
1212
<intent-filter>
1313
<action android:name="android.intent.action.MAIN" />
1414
<category android:name="android.intent.category.LAUNCHER" />

formula-integration-tests/src/main/java/com/instacart/formula/TestActivity.kt formula-integration-tests/src/main/java/com/instacart/formula/TestFlowViewActivity.kt

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,26 @@ package com.instacart.formula
22

33
import android.os.Bundle
44
import androidx.fragment.app.FragmentActivity
5+
import androidx.lifecycle.ViewModelProviders
56
import com.instacart.formula.fragment.FormulaFragment
67
import com.instacart.formula.integration.FragmentFlowRenderView
78
import io.reactivex.disposables.CompositeDisposable
89

9-
class TestActivity : FragmentActivity() {
10+
class TestFlowViewActivity : FragmentActivity() {
1011
private val disposables = CompositeDisposable()
1112

1213
// Exposed for testing purposes.
1314
lateinit var fragmentFlowRenderView: FragmentFlowRenderView
14-
val component = TestComponent()
15+
lateinit var viewModel: TestFragmentFlowViewModel
1516

1617
override fun onCreate(savedInstanceState: Bundle?) {
17-
fragmentFlowRenderView = FragmentFlowRenderView(this, onLifecycleEvent = component::onLifecycleEvent)
18+
viewModel = ViewModelProviders.of(this).get(TestFragmentFlowViewModel::class.java)
19+
fragmentFlowRenderView = FragmentFlowRenderView(this, onLifecycleEvent = viewModel::onLifecycleEvent)
1820

1921
super.onCreate(savedInstanceState)
20-
setContentView(R.layout.basic_integration_activity)
22+
setContentView(R.layout.test_activity)
2123

22-
disposables.add(component.state.subscribe(fragmentFlowRenderView.renderer::render))
24+
disposables.add(viewModel.state.subscribe(fragmentFlowRenderView.renderer::render))
2325

2426
if (savedInstanceState == null) {
2527
val contract = TaskListContract()

formula-integration-tests/src/main/java/com/instacart/formula/TestFragmentComponent.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ object TestFragmentComponent {
1111
return FragmentComponent.create(
1212
renderView = object : RenderView<T> {
1313
override val renderer: Renderer<T> = Renderer.create {
14-
(view.context as TestActivity).component.renderCalls.add(Pair(contract, it))
14+
(view.context as TestFlowViewActivity).viewModel.renderCalls.add(Pair(contract, it))
1515
}
1616
},
1717
lifecycleCallbacks = object : FragmentLifecycleCallback {

formula-integration-tests/src/main/java/com/instacart/formula/TestComponent.kt formula-integration-tests/src/main/java/com/instacart/formula/TestFragmentFlowViewModel.kt

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.instacart.formula
22

3+
import androidx.lifecycle.ViewModel
34
import com.instacart.formula.fragment.FragmentContract
45
import com.instacart.formula.fragment.FragmentFlowState
56
import com.instacart.formula.fragment.FragmentFlowStore
67
import com.instacart.formula.fragment.FragmentLifecycleEvent
78
import com.jakewharton.rxrelay2.PublishRelay
89
import io.reactivex.BackpressureStrategy
910
import io.reactivex.Flowable
11+
import io.reactivex.disposables.CompositeDisposable
1012

11-
class TestComponent {
13+
class TestFragmentFlowViewModel : ViewModel() {
1214
val renderCalls = mutableListOf<Pair<FragmentContract<*>, *>>()
1315

1416
private val stateChangeRelay = PublishRelay.create<Pair<FragmentContract<*>, Any>>()
@@ -27,19 +29,24 @@ class TestComponent {
2729
}
2830
}
2931

32+
private val disposables = CompositeDisposable()
33+
3034
fun onLifecycleEvent(event: FragmentLifecycleEvent) {
3135
store.onLifecycleEffect(event)
3236
}
3337

3438
// Share state
35-
val state = store.state().replay(1).refCount()
39+
val state: Flowable<FragmentFlowState> = store.state().replay(1).apply {
40+
connect { disposables.add(it) }
41+
}
3642

3743
fun <T : Any> sendStateUpdate(contract: FragmentContract<T>, update: T) {
3844
stateChangeRelay.accept(Pair<FragmentContract<*>, Any>(contract, update))
3945
}
4046

41-
fun currentFragmentState(): FragmentFlowState {
42-
return state.test().values().last()
47+
override fun onCleared() {
48+
super.onCleared()
49+
disposables.clear()
4350
}
4451

4552
private fun stateChanges(contract: FragmentContract<*>): Flowable<Any> {

formula-integration-tests/src/test/java/com/instacart/formula/FragmentFlowRenderViewTest.kt

+19-15
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class FragmentFlowRenderViewTest {
1616

1717
class HeadlessFragment : Fragment()
1818

19-
@get:Rule val rule = ActivityTestRule(TestActivity::class.java)
19+
@get:Rule val rule = ActivityTestRule(TestFlowViewActivity::class.java)
2020

2121
@Test fun `add fragment lifecycle event`() {
2222
assertThat(currentBackstack()).containsExactly(TaskListContract())
@@ -49,32 +49,32 @@ class FragmentFlowRenderViewTest {
4949
}
5050

5151
@Test fun `render model is passed to visible fragment`() {
52-
val component = rule.activity.component
53-
component.sendStateUpdate(TaskListContract(), "update")
54-
assertThat(component.renderCalls).containsExactly(TaskListContract() to "update")
52+
val viewModel = viewModel()
53+
viewModel.sendStateUpdate(TaskListContract(), "update")
54+
assertThat(viewModel.renderCalls).containsExactly(TaskListContract() to "update")
5555
}
5656

5757
@Test fun `render model is not passed to not visible fragment`() {
5858
navigateToTaskDetail()
5959

60-
val component = rule.activity.component
61-
component.sendStateUpdate(TaskListContract(), "update")
62-
assertThat(component.renderCalls).isEqualTo(emptyList<Any>())
60+
val viewModel = viewModel()
61+
viewModel.sendStateUpdate(TaskListContract(), "update")
62+
assertThat(viewModel.renderCalls).isEqualTo(emptyList<Any>())
6363
}
6464

6565
@Test fun `visible fragments are updated when navigating`() {
6666
navigateToTaskDetail()
6767

6868
val contract = TaskDetailContract(1)
6969

70-
val component = rule.activity.component
71-
component.sendStateUpdate(contract, "update")
72-
assertThat(component.renderCalls).containsExactly(contract to "update")
70+
val viewModel = viewModel()
71+
viewModel.sendStateUpdate(contract, "update")
72+
assertThat(viewModel.renderCalls).containsExactly(contract to "update")
7373

7474
rule.activity.onBackPressed()
7575

76-
component.sendStateUpdate(contract, "update-two")
77-
assertThat(component.renderCalls).containsExactly(contract to "update")
76+
viewModel.sendStateUpdate(contract, "update-two")
77+
assertThat(viewModel.renderCalls).containsExactly(contract to "update")
7878
}
7979

8080
@Test fun `delegates back press to current render model`() {
@@ -83,8 +83,8 @@ class FragmentFlowRenderViewTest {
8383
var backPressed = 0
8484

8585
val contract = TaskDetailContract(1)
86-
val component = rule.activity.component
87-
component.sendStateUpdate(contract, object : BackCallback {
86+
val viewModel = viewModel()
87+
viewModel.sendStateUpdate(contract, object : BackCallback {
8888
override fun onBackPressed() {
8989
backPressed += 1
9090
}
@@ -96,6 +96,10 @@ class FragmentFlowRenderViewTest {
9696
assertThat(backPressed).isEqualTo(2)
9797
}
9898

99+
private fun viewModel(): TestFragmentFlowViewModel {
100+
return rule.activity.viewModel
101+
}
102+
99103
private fun navigateToTaskDetail() {
100104
val detail = TaskDetailContract(1)
101105
rule.activity.supportFragmentManager.beginTransaction()
@@ -106,6 +110,6 @@ class FragmentFlowRenderViewTest {
106110
}
107111

108112
private fun currentBackstack(): List<FragmentContract<*>> {
109-
return rule.activity.component.currentFragmentState().backStack.keys
113+
return rule.activity.viewModel.state.test().values().last().backStack.keys
110114
}
111115
}

formula-integration-tests/src/test/java/com/instacart/formula/TestFragmentActivity.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ class TestFragmentActivity : FragmentActivity() {
1111
private val disposables = CompositeDisposable()
1212

1313
lateinit var fragmentRenderView: FragmentFlowRenderView
14-
val component = TestComponent()
14+
val component = TestFragmentFlowViewModel()
1515
lateinit var contract: TestLifecycleContract
1616

1717
override fun onCreate(savedInstanceState: Bundle?) {
1818

1919
fragmentRenderView = FragmentFlowRenderView(this, onLifecycleEvent = component::onLifecycleEvent)
2020
super.onCreate(savedInstanceState)
21-
setContentView(R.layout.basic_integration_activity)
21+
setContentView(R.layout.test_activity)
2222

2323
disposables.add(component.state.subscribe(fragmentRenderView.renderer::render))
2424

Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.instacart.formula.integration
22

3-
3+
/**
4+
* Used to indicate that a screen render model
5+
* handles back presses.
6+
*/
47
interface BackCallback {
58
fun onBackPressed()
69
}

samples/todoapp/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ dependencies {
4343
implementation libraries.kotlin
4444
implementation libraries.rxrelays
4545

46+
implementation libraries.androidx.lifecycle.extensions
47+
4648
testImplementation libraries.junit
4749
testImplementation "com.google.truth:truth:$truthVersion"
4850

samples/todoapp/src/main/java/com/examples/todoapp/TodoActivity.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.examples.todoapp
33
import android.os.Bundle
44
import android.widget.Toast
55
import androidx.fragment.app.FragmentActivity
6+
import androidx.lifecycle.ViewModelProviders
67
import com.examples.todoapp.tasks.TaskListContract
78
import com.instacart.formula.fragment.FormulaFragment
89
import com.instacart.formula.integration.FragmentFlowRenderView
@@ -15,21 +16,21 @@ class TodoActivity : FragmentActivity() {
1516
private lateinit var fragmentRenderView: FragmentFlowRenderView
1617

1718
override fun onCreate(savedInstanceState: Bundle?) {
18-
val component = TodoApp.component(this)
19+
val viewModel = ViewModelProviders.of(this).get(TodoActivityViewModel::class.java)
20+
fragmentRenderView = FragmentFlowRenderView(this, onLifecycleEvent = viewModel::onLifecycleEvent)
1921

20-
fragmentRenderView = FragmentFlowRenderView(this, onLifecycleEvent = component::onLifecycleEvent)
2122
super.onCreate(savedInstanceState)
2223
setContentView(R.layout.todo_activity)
2324

24-
disposables.add(component.activityEffects().subscribe {
25+
disposables.add(viewModel.effects.subscribe {
2526
when (it) {
2627
is TodoActivityEffect.ShowToast -> {
2728
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
2829
}
2930
}
3031
})
3132

32-
disposables.add(component.state().subscribe(fragmentRenderView.renderer::render))
33+
disposables.add(viewModel.state.subscribe(fragmentRenderView.renderer::render))
3334

3435
if (savedInstanceState == null) {
3536
val contract = TaskListContract()
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
package com.examples.todoapp
22

3+
import androidx.lifecycle.ViewModel
34
import com.examples.todoapp.data.TaskRepo
45
import com.examples.todoapp.tasks.TaskListContract
56
import com.examples.todoapp.tasks.TaskListFormula
67
import com.instacart.formula.fragment.FragmentContract
78
import com.instacart.formula.fragment.FragmentFlowState
89
import com.instacart.formula.fragment.FragmentFlowStore
910
import com.instacart.formula.integration.LifecycleEvent
10-
import com.jakewharton.rxrelay2.BehaviorRelay
1111
import com.jakewharton.rxrelay2.PublishRelay
12-
import io.reactivex.BackpressureStrategy
1312
import io.reactivex.Flowable
1413
import io.reactivex.Observable
14+
import io.reactivex.disposables.CompositeDisposable
1515

16-
class TodoComponent {
16+
class TodoActivityViewModel : ViewModel() {
17+
// Should be injected
1718
private val repo: TaskRepo = TaskRepo()
1819

1920
private val activityEffectRelay: PublishRelay<TodoActivityEffect> = PublishRelay.create()
@@ -26,23 +27,22 @@ class TodoComponent {
2627
}
2728
}
2829

29-
// Using a relay here to survive configuration changes.
30-
val stateRelay: BehaviorRelay<FragmentFlowState> = BehaviorRelay.create()
30+
private val disposables = CompositeDisposable()
3131

32-
init {
33-
// This subscription should be added to some scope that outlives activity configuration changes.
34-
store.state().subscribe(stateRelay::accept)
32+
// We use replay + connect so this stream survives configuration changes.
33+
val state: Flowable<FragmentFlowState> = store.state().replay(1).apply {
34+
connect { disposables.add(it) }
3535
}
3636

37+
// We expose effects on the activity.
38+
val effects: Observable<TodoActivityEffect> = activityEffectRelay
39+
3740
fun onLifecycleEvent(event: LifecycleEvent<FragmentContract<*>>) {
3841
store.onLifecycleEffect(event)
3942
}
4043

41-
fun state(): Flowable<FragmentFlowState> {
42-
return stateRelay.toFlowable(BackpressureStrategy.LATEST)
43-
}
44-
45-
fun activityEffects(): Observable<TodoActivityEffect> {
46-
return activityEffectRelay
44+
override fun onCleared() {
45+
super.onCleared()
46+
disposables.clear()
4747
}
4848
}

samples/todoapp/src/main/java/com/examples/todoapp/TodoApp.kt

-7
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,4 @@ import com.instacart.formula.integration.LifecycleEvent
99
import io.reactivex.Flowable
1010

1111
class TodoApp : Application() {
12-
companion object {
13-
fun component(context: Context): TodoComponent {
14-
return (context.applicationContext as TodoApp).component
15-
}
16-
}
17-
18-
private val component: TodoComponent = TodoComponent()
1912
}

samples/todoapp/src/main/java/com/examples/todoapp/data/TaskRepo.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.examples.todoapp.data
33
import com.examples.todoapp.tasks.TaskCompletedEvent
44
import com.jakewharton.rxrelay2.BehaviorRelay
55
import io.reactivex.Observable
6+
import io.reactivex.android.schedulers.AndroidSchedulers
7+
import java.util.concurrent.TimeUnit
68

79
class TaskRepo {
810
private val localStore: BehaviorRelay<List<Task>> = BehaviorRelay.createDefault(
@@ -13,7 +15,10 @@ class TaskRepo {
1315
)
1416

1517
fun tasks(): Observable<List<Task>> {
16-
return localStore
18+
// Fake initial network request
19+
return Observable.timer(5, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).flatMap {
20+
localStore
21+
}
1722
}
1823

1924
fun onTaskCompleted(event: TaskCompletedEvent) {

0 commit comments

Comments
 (0)