Skip to content

Commit 91c2a56

Browse files
authored
Merge branch 'feature/flowmvvm' into lusa/safecollect
2 parents 07e5ba1 + 70c66b3 commit 91c2a56

File tree

11 files changed

+277
-28
lines changed

11 files changed

+277
-28
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.monstarlab.extensions
2+
3+
import android.view.LayoutInflater
4+
import androidx.appcompat.app.AppCompatActivity
5+
import androidx.lifecycle.Lifecycle
6+
import androidx.lifecycle.LifecycleObserver
7+
import androidx.lifecycle.OnLifecycleEvent
8+
import androidx.viewbinding.ViewBinding
9+
import kotlin.properties.ReadOnlyProperty
10+
import kotlin.reflect.KProperty
11+
12+
class ActivityViewBindingDelegate<T : ViewBinding>(
13+
private val activity: AppCompatActivity,
14+
private val viewBinder: (LayoutInflater) -> T,
15+
private val beforeSetContent: () -> Unit = {}
16+
) : ReadOnlyProperty<AppCompatActivity, T>, LifecycleObserver {
17+
18+
private var activityBinding: T? = null
19+
20+
init {
21+
activity.lifecycle.addObserver(this)
22+
}
23+
24+
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
25+
fun createBinding() {
26+
initialize()
27+
beforeSetContent()
28+
activity.setContentView(activityBinding?.root)
29+
activity.lifecycle.removeObserver(this)
30+
}
31+
32+
private fun initialize() {
33+
if (activityBinding == null) {
34+
activityBinding = viewBinder(activity.layoutInflater)
35+
}
36+
}
37+
38+
override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
39+
ensureMainThread()
40+
41+
initialize()
42+
return activityBinding!!
43+
}
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.monstarlab.extensions
2+
3+
import androidx.fragment.app.Fragment
4+
import androidx.lifecycle.LifecycleCoroutineScope
5+
import androidx.lifecycle.lifecycleScope
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.flow.Flow
8+
import kotlinx.coroutines.flow.collect
9+
import kotlinx.coroutines.flow.combine
10+
import kotlinx.coroutines.launch
11+
12+
fun <T1, T2> CoroutineScope.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, collectBlock: (suspend (T1, T2) -> Unit)) {
13+
launch {
14+
flow1.combine(flow2) { v1, v2 ->
15+
collectBlock.invoke(v1, v2)
16+
}.collect {
17+
// Empty collect block to trigger ^
18+
}
19+
}
20+
}
21+
22+
fun <T1, T2, T3> CoroutineScope.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, collectBlock: (suspend (T1, T2, T3) -> Unit)) {
23+
launch {
24+
combine(flow1, flow2, flow3) { v1, v2, v3 ->
25+
collectBlock.invoke(v1, v2, v3)
26+
}.collect {
27+
// Empty collect block to trigger ^
28+
}
29+
}
30+
}
31+
32+
fun <T1, T2, T3, T4> CoroutineScope.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, flow4: Flow<T4>, collectBlock: (suspend (T1, T2, T3, T4) -> Unit)) {
33+
launch {
34+
combine(flow1, flow2, flow3, flow4) { v1, v2, v3, v4 ->
35+
collectBlock.invoke(v1, v2, v3, v4)
36+
}.collect {
37+
// Empty collect block to trigger ^
38+
}
39+
}
40+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.monstarlab.extensions
2+
3+
import android.view.View
4+
import androidx.fragment.app.Fragment
5+
import androidx.lifecycle.*
6+
import androidx.viewbinding.ViewBinding
7+
import kotlin.properties.ReadOnlyProperty
8+
import kotlin.reflect.KProperty
9+
10+
class FragmentViewBindingDelegate<T : ViewBinding>(
11+
private val fragment: Fragment,
12+
private val viewBinder: (View) -> T,
13+
private val disposeEvents: T.() -> Unit = {}
14+
) : ReadOnlyProperty<Fragment, T>, LifecycleObserver {
15+
16+
private inline fun Fragment.observeLifecycleOwnerThroughLifecycleCreation(crossinline viewOwner: LifecycleOwner.() -> Unit) {
17+
lifecycle.addObserver(object : DefaultLifecycleObserver {
18+
override fun onCreate(owner: LifecycleOwner) {
19+
viewLifecycleOwnerLiveData.observe(
20+
this@observeLifecycleOwnerThroughLifecycleCreation,
21+
Observer { viewLifecycleOwner ->
22+
viewLifecycleOwner.viewOwner()
23+
})
24+
}
25+
})
26+
}
27+
28+
private var fragmentBinding: T? = null
29+
30+
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
31+
fun disposeBinding() {
32+
fragmentBinding?.disposeEvents()
33+
fragmentBinding = null
34+
}
35+
36+
init {
37+
fragment.observeLifecycleOwnerThroughLifecycleCreation {
38+
lifecycle.addObserver(this@FragmentViewBindingDelegate)
39+
}
40+
}
41+
42+
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
43+
44+
ensureMainThread()
45+
46+
val binding = fragmentBinding
47+
if (binding != null) {
48+
return binding
49+
}
50+
51+
val lifecycle = fragment.viewLifecycleOwner.lifecycle
52+
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
53+
throw IllegalStateException("Fragment views are destroyed.")
54+
}
55+
return viewBinder(thisRef.requireView()).also { fragmentBinding = it }
56+
}
57+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.monstarlab.extensions
2+
3+
import android.view.View
4+
import androidx.viewbinding.ViewBinding
5+
import kotlin.properties.ReadOnlyProperty
6+
import kotlin.reflect.KProperty
7+
8+
class GlobalViewBindingDelegate<T : ViewBinding>(val viewBinder: (View) -> T) :
9+
ReadOnlyProperty<View, T> {
10+
11+
override fun getValue(thisRef: View, property: KProperty<*>): T {
12+
return viewBinder(thisRef)
13+
}
14+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.monstarlab.extensions
2+
3+
import android.app.Activity
4+
import android.os.Looper
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.fragment.app.Fragment
9+
import androidx.viewbinding.ViewBinding
10+
11+
inline fun <T : ViewBinding> Activity.viewBinder(crossinline bindingInflater: (LayoutInflater) -> T) =
12+
lazy(LazyThreadSafetyMode.NONE) {
13+
bindingInflater.invoke(layoutInflater)
14+
}
15+
16+
fun <T : ViewBinding> AppCompatActivity.viewBinding(
17+
bindingInflater: (LayoutInflater) -> T,
18+
beforeSetContent: () -> Unit = {}
19+
) =
20+
ActivityViewBindingDelegate(this, bindingInflater, beforeSetContent)
21+
22+
fun <T : ViewBinding> Fragment.viewBinding(
23+
viewBindingFactory: (View) -> T,
24+
disposeEvents: T.() -> Unit = {}
25+
) =
26+
FragmentViewBindingDelegate(this, viewBindingFactory, disposeEvents)
27+
28+
fun <T : ViewBinding> globalViewBinding(viewBindingFactory: (View) -> T) =
29+
GlobalViewBindingDelegate(viewBindingFactory)
30+
31+
internal fun ensureMainThread() {
32+
if (Looper.myLooper() != Looper.getMainLooper()) {
33+
throw IllegalThreadStateException("View can be accessed only on the main thread.")
34+
}
35+
}

app/src/main/java/com/monstarlab/extensions/ViewExtensions.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlinx.coroutines.launch
1212

1313
fun <T> Fragment.collectFlow(targetFlow: Flow<T>, collectBlock: ((T) -> Unit)) {
1414
safeViewCollect {
15-
viewLifecycleOwner.lifecycleScope.launch {
15+
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
1616
targetFlow.collect {
1717
collectBlock.invoke(it)
1818
}
@@ -33,7 +33,7 @@ private inline fun Fragment.safeViewCollect(crossinline viewOwner: LifecycleOwne
3333
}
3434

3535
fun <T1, T2> Fragment.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, collectBlock: ((T1, T2) -> Unit)) {
36-
viewLifecycleOwner.lifecycleScope.launch {
36+
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
3737
flow1.combine(flow2) { v1, v2 ->
3838
collectBlock.invoke(v1, v2)
3939
}.collect {
@@ -43,7 +43,7 @@ fun <T1, T2> Fragment.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, collectBloc
4343
}
4444

4545
fun <T1, T2, T3> Fragment.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, collectBlock: ((T1, T2, T3) -> Unit)) {
46-
viewLifecycleOwner.lifecycleScope.launch {
46+
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
4747
combine(flow1, flow2, flow3) { v1, v2, v3 ->
4848
collectBlock.invoke(v1, v2, v3)
4949
}.collect {
@@ -53,7 +53,7 @@ fun <T1, T2, T3> Fragment.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3:
5353
}
5454

5555
fun <T1, T2, T3, T4> Fragment.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, flow4: Flow<T4>, collectBlock: ((T1, T2, T3, T4) -> Unit)) {
56-
viewLifecycleOwner.lifecycleScope.launch {
56+
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
5757
combine(flow1, flow2, flow3, flow4) { v1, v2, v3, v4 ->
5858
collectBlock.invoke(v1, v2, v3, v4)
5959
}.collect {
@@ -63,7 +63,7 @@ fun <T1, T2, T3, T4> Fragment.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flo
6363
}
6464

6565
fun <T1, T2> Fragment.zipFlows(flow1: Flow<T1>, flow2: Flow<T2>, collectBlock: ((T1, T2) -> Unit)) {
66-
viewLifecycleOwner.lifecycleScope.launch {
66+
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
6767
flow1.zip(flow2) { v1, v2 ->
6868
collectBlock.invoke(v1, v2)
6969
}.collect {
Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,20 @@
11
package com.monstarlab.features.sample
22

33
import android.os.Bundle
4-
import android.view.LayoutInflater
54
import android.view.View
6-
import android.view.ViewGroup
7-
import androidx.fragment.app.Fragment
85
import com.monstarlab.R
96
import com.monstarlab.base.BaseFragment
107
import com.monstarlab.databinding.FragmentSampleBinding
118
import com.monstarlab.extensions.collectFlow
9+
import com.monstarlab.extensions.viewBinding
10+
import kotlinx.coroutines.flow.collect
11+
import kotlinx.coroutines.flow.combine
1212

13-
class SampleFragment: BaseFragment(R.layout.fragment_sample) {
13+
class SampleFragment : BaseFragment(R.layout.fragment_sample) {
1414

15-
val viewModel by viewModel<SampleViewModel>()
15+
private val viewModel by viewModel<SampleViewModel>()
1616

17-
private var _binding: FragmentSampleBinding? = null
18-
private val binding get() = _binding!!
19-
20-
override fun onCreateView(
21-
inflater: LayoutInflater,
22-
container: ViewGroup?,
23-
savedInstanceState: Bundle?
24-
): View? {
25-
_binding = FragmentSampleBinding.inflate(inflater, container, false)
26-
val view = binding.root
27-
return view
28-
}
29-
30-
override fun onDestroyView() {
31-
super.onDestroyView()
32-
_binding = null
33-
}
17+
private val binding by viewBinding(FragmentSampleBinding::bind)
3418

3519
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
3620
super.onViewCreated(view, savedInstanceState)
@@ -39,8 +23,14 @@ class SampleFragment: BaseFragment(R.layout.fragment_sample) {
3923
binding.valueTextView.text = "Clicked $clicks times"
4024
}
4125

26+
collectFlow(viewModel.textFlow) { newText ->
27+
binding.asyncTextView.text = newText
28+
}
29+
4230
binding.theButton.setOnClickListener {
4331
viewModel.clickedButton()
4432
}
33+
34+
viewModel.fetchString()
4535
}
4636
}
Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
package com.monstarlab.features.sample
22

33
import androidx.lifecycle.ViewModel
4-
import kotlinx.coroutines.flow.MutableStateFlow
4+
import androidx.lifecycle.viewModelScope
5+
import com.monstarlab.extensions.combineFlows
6+
import com.monstarlab.mock.MockFlows
7+
import com.monstarlab.mock.MockSuspends
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.flow.*
10+
import kotlinx.coroutines.launch
11+
import kotlinx.coroutines.withContext
512
import javax.inject.Inject
613

714
class SampleViewModel @Inject constructor(
815

916
): ViewModel() {
1017

1118
val clickFlow: MutableStateFlow<Int> = MutableStateFlow(0)
19+
val textFlow: MutableStateFlow<String> = MutableStateFlow("Nothing yet")
1220

1321
fun clickedButton() {
1422
clickFlow.value++
1523
}
1624

25+
fun fetchString() {
26+
viewModelScope.combineFlows(MockFlows.mockString(), MockFlows.mockFlag()) { text, flag ->
27+
textFlow.value = "text: $text - flag: $flag"
28+
}
29+
30+
31+
MockFlows.mockString()
32+
.onEach { result -> textFlow.value = result }
33+
.catch { _ -> /* Do something with the exception */ }
34+
.launchIn(viewModelScope)
35+
36+
viewModelScope.launch {
37+
val result = withContext(Dispatchers.IO) { MockSuspends.fetchString() }
38+
textFlow.value = result
39+
}
40+
}
41+
1742
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.monstarlab.mock
2+
3+
import kotlinx.coroutines.delay
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.flow
6+
7+
object MockFlows {
8+
9+
fun mockString(): Flow<String> = flow {
10+
delay(2000)
11+
emit("Hello world from Flow")
12+
}
13+
14+
fun mockFlag(): Flow<Boolean> = flow {
15+
delay(1000)
16+
emit(false)
17+
delay(2000)
18+
emit(true)
19+
}
20+
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.monstarlab.mock
2+
3+
import kotlinx.coroutines.delay
4+
5+
object MockSuspends {
6+
7+
suspend fun fetchString(): String {
8+
delay(5000)
9+
return "Hello world from suspend"
10+
}
11+
12+
}

0 commit comments

Comments
 (0)