internal constructor(isEnabled: Boolean = true, action: (P) -> R) : BaseCommandImpl(action) {
10 | override var isEnabled: Boolean = isEnabled
11 | set(value) {
12 | if (field == value) return
13 | field = value
14 | triggerEnabledChangedListener()
15 | }
16 | }
17 |
18 | /**
19 | * Helper function to create a [SimpleCommand].
20 | */
21 | @JvmName("parameterizedSimpleCommand")
22 | fun
simpleCommand(
23 | isEnabled: Boolean = true,
24 | action: (P) -> R
25 | ): SimpleCommand
= SimpleCommand(isEnabled, action)
26 |
27 | /**
28 | * Helper function to create a parameter-less [SimpleCommand].
29 | */
30 | fun simpleCommand(
31 | isEnabled: Boolean = true,
32 | action: (Unit) -> R
33 | ): SimpleCommand = simpleCommand(isEnabled, action)
34 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/MvvmBaseDataBinding.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import de.trbnb.mvvmbase.MvvmBase
5 |
6 | /**
7 | * Object for containing library configurations.
8 | */
9 | internal object MvvmBaseDataBinding {
10 | private var brFieldIds: Map = emptyMap()
11 |
12 | /**
13 | * Get data binding field ID for given property name.
14 | *
15 | * @see initDataBinding
16 | */
17 | fun lookupFieldIdByName(name: String): Int? = brFieldIds[name]
18 |
19 | internal fun retrieveFieldIds(brClass: Class<*>) {
20 | brFieldIds = brClass.fields.asSequence()
21 | .filter { it.type == Int::class.java }
22 | .map { it.name to it.getInt(null) }
23 | .toMap()
24 | }
25 | }
26 |
27 | /**
28 | * Initializes the library with the BR class from itself (which will be expanded by the databinding compiler and so will contain every field id).
29 | */
30 | fun MvvmBase.initDataBinding() = apply {
31 | MvvmBaseDataBinding.retrieveFieldIds(BR::class.java)
32 | }
33 |
34 | @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
35 | internal fun MvvmBase.resetDataBinding() = apply {
36 | MvvmBaseDataBinding.retrieveFieldIds(Unit::class.java)
37 | }
38 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/ViewModelLifecycleOwner.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleOwner
6 | import androidx.lifecycle.LifecycleRegistry
7 |
8 | /**
9 | * The custom lifecycle owner for ViewModels.
10 | *
11 | * Its lifecycle state is:
12 | * - After initialization: [Lifecycle.State.RESUMED].
13 | * - After being destroyed: [Lifecycle.State.DESTROYED].
14 | */
15 | internal class ViewModelLifecycleOwner(enforceMainThread: Boolean) : LifecycleOwner {
16 | @SuppressLint("VisibleForTests")
17 | private val registry = when (enforceMainThread) {
18 | true -> LifecycleRegistry(this)
19 | false -> LifecycleRegistry.createUnsafe(this)
20 | }
21 |
22 | init {
23 | onEvent(Event.INITIALIZED)
24 | }
25 |
26 | fun onEvent(event: Event) {
27 | registry.currentState = when (event) {
28 | Event.INITIALIZED -> Lifecycle.State.RESUMED
29 | Event.DESTROYED -> Lifecycle.State.DESTROYED
30 | }
31 | }
32 |
33 | override fun getLifecycle() = registry
34 |
35 | /**
36 | * Enum for the specific Lifecycle of ViewModels.
37 | */
38 | internal enum class Event {
39 | INITIALIZED,
40 | DESTROYED
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/commands/Command.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.commands
2 |
3 | import de.trbnb.mvvmbase.observable.ObservableContainer
4 |
5 | /**
6 | * The basic contract for command implementations.
7 | *
8 | * @param P The parameter type for invocation. An instance of this has to be used to call [invoke]. [Unit] may be used if no parameter is neccessary.
9 | * @param R The return type for invocation. An instance of this has to be returned from [invoke].
10 | */
11 | interface Command : ObservableContainer {
12 | /**
13 | * Determines whether this Command is enabled or not.
14 | *
15 | * @return Returns `true` if this Command is enabled, otherwise `false`.
16 | */
17 | val isEnabled: Boolean
18 |
19 | /**
20 | * Invokes the Command.
21 | *
22 | * @throws de.trbnb.mvvmbase.commands.DisabledCommandInvocationException If [isEnabled] returns `false`.
23 | * @return A return type instance.
24 | */
25 | operator fun invoke(param: P): R
26 |
27 | /**
28 | * Invokes the Command only if [isEnabled] equals `true`.
29 | *
30 | * @return A return type instance if [isEnabled] equals `true` before invocation, otherwise `null`.
31 | */
32 | fun invokeSafely(param: P): R? {
33 | return if (isEnabled) invoke(param) else null
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/events/ComposeUtils.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.events
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.neverEqualPolicy
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.platform.LocalLifecycleOwner
10 |
11 | /**
12 | * Helper function to use handle one-off events in Compose.
13 | */
14 | @Composable
15 | fun EventChannelOwner.OnEvent(action: @Composable (event: Event) -> Unit) {
16 | val lastEvent = lastEventAsState()
17 | lastEvent.value?.let { action(it) }
18 | }
19 |
20 | /**
21 | * Observes the events from [EventChannelOwner.eventChannel] as Compose state.
22 | * If no event has been observed the State contains `null`.
23 | */
24 | @Composable
25 | fun EventChannelOwner.lastEventAsState(): State {
26 | val state = remember { mutableStateOf(null, neverEqualPolicy()) }
27 | val lifecycleOwner = LocalLifecycleOwner.current
28 | DisposableEffect(key1 = this, lifecycleOwner) {
29 | val action = { event: Event -> state.value = event }
30 | eventChannel.addListener(lifecycleOwner, action)
31 | onDispose { eventChannel.removeListener(action) }
32 | }
33 | return state
34 | }
35 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/Typealiases.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding
2 |
3 | import androidx.databinding.ViewDataBinding
4 |
5 | /**
6 | * Typealias for Fragments that don't need to specify the specific [ViewDataBinding] implementation.
7 | *
8 | * @param[VM] The type of the specific [ViewModel] implementation for this Fragment.
9 | */
10 | typealias MvvmFragment = MvvmBindingFragment
11 |
12 | /**
13 | * Typealias for DialogFragments that don't need to specify the specific [ViewDataBinding] implementation.
14 | *
15 | * @param[VM] The type of the specific [ViewModel] implementation for this Activity.
16 | */
17 | typealias MvvmDialogFragment = MvvmBindingDialogFragment
18 |
19 | /**
20 | * Typealias for BottomSheetDialogFragments that don't need to specify the specific [ViewDataBinding] implementation.
21 | *
22 | * @param[VM] The type of the specific [ViewModel] implementation for this Activity.
23 | */
24 | typealias MvvmBottomSheetDialogFragment = MvvmBindingBottomSheetDialogFragment
25 |
26 | /**
27 | * Typealias for Activities that don't need to specify the specific [ViewDataBinding] implementation.
28 | *
29 | * @param[VM] The type of the specific [ViewModel] implementation for this Activity.
30 | */
31 | typealias MvvmActivity = MvvmBindingActivity
32 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/compose/ComposeUtils.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.compose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.MutableState
6 | import androidx.compose.runtime.State
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.platform.LocalLifecycleOwner
10 | import de.trbnb.mvvmbase.utils.observe
11 | import kotlin.reflect.KMutableProperty0
12 | import kotlin.reflect.KProperty0
13 |
14 | /**
15 | * Observes an observable property as Compose state.
16 | */
17 | @Composable
18 | fun KProperty0.observeAsState(): State {
19 | val state = remember { mutableStateOf(get()) }
20 | val lifecycleOwner = LocalLifecycleOwner.current
21 | DisposableEffect(key1 = this, key2 = lifecycleOwner) {
22 | val dispose = observe(lifecycleOwner, false) { state.value = it }
23 | onDispose(dispose::invoke)
24 | }
25 | return state
26 | }
27 |
28 | /**
29 | * Observes an observable property as mutable Compose state.
30 | */
31 | @Composable
32 | fun KMutableProperty0.observeAsMutableState(): MutableState {
33 | return PropertyMutableState(observeAsState(), this)
34 | }
35 |
36 | /**
37 | * Gets the setter function in an explicit way.
38 | */
39 | inline val MutableState.setter: (T) -> Unit get() = component2()
40 |
--------------------------------------------------------------------------------
/databinding/src/test/java/de/trbnb/mvvmbase/databinding/test/ObservableTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.test
2 |
3 | import androidx.databinding.BaseObservable
4 | import androidx.databinding.Observable
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.lifecycle.LifecycleRegistry
8 | import de.trbnb.mvvmbase.databinding.utils.addOnPropertyChangedCallback
9 | import org.junit.jupiter.api.Test
10 |
11 | class ObservableTests {
12 | @Test
13 | fun `property changed callback with Lifecycle`() {
14 | val observable = BaseObservable()
15 |
16 | val lifecycleOwner = object : LifecycleOwner {
17 | private val lifecycle = LifecycleRegistry.createUnsafe(this).apply {
18 | currentState = Lifecycle.State.STARTED
19 | }
20 | override fun getLifecycle() = lifecycle
21 | fun destroy() { lifecycle.currentState = Lifecycle.State.DESTROYED }
22 | }
23 |
24 | var callbackWasTriggered = false
25 | observable.addOnPropertyChangedCallback(lifecycleOwner, object : Observable.OnPropertyChangedCallback() {
26 | override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
27 | callbackWasTriggered = true
28 | }
29 | })
30 |
31 | lifecycleOwner.destroy()
32 | observable.notifyPropertyChanged(BR.enabled)
33 | assert(!callbackWasTriggered)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/commands/BaseCommandImpl.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.commands
2 |
3 | import de.trbnb.mvvmbase.OnPropertyChangedCallback
4 | import de.trbnb.mvvmbase.observable.PropertyChangeRegistry
5 |
6 | /**
7 | * Base class for standard [Command] implementations.
8 |
9 | * An implementation of the [Command.isEnabled] is not given.
10 | *
11 | * @param action The initial action that will be run when the Command is executed.
12 | */
13 | abstract class BaseCommandImpl(private val action: (P) -> R) : Command {
14 | private val registry = PropertyChangeRegistry(emptyList())
15 |
16 | override fun addOnPropertyChangedCallback(callback: OnPropertyChangedCallback) {
17 | registry.add(callback)
18 | }
19 |
20 | override fun removeOnPropertyChangedCallback(callback: OnPropertyChangedCallback) {
21 | registry.remove(callback)
22 | }
23 |
24 | override fun notifyPropertyChanged(propertyName: String) {
25 | registry.notifyChange(this, propertyName)
26 | }
27 |
28 | final override fun invoke(param: P): R {
29 | if (!isEnabled) {
30 | throw DisabledCommandInvocationException()
31 | }
32 |
33 | return action(param)
34 | }
35 |
36 | /**
37 | * This method should be called when the result of [isEnabled] might have changed.
38 | */
39 | protected fun triggerEnabledChangedListener() {
40 | notifyPropertyChanged(::isEnabled)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/utils/ReflectionUtils.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.utils
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import de.trbnb.mvvmbase.OnPropertyChangedCallback
5 | import de.trbnb.mvvmbase.observable.ObservableContainer
6 | import de.trbnb.mvvmbase.observable.addOnPropertyChangedCallback
7 | import kotlin.jvm.internal.CallableReference
8 | import kotlin.reflect.KProperty0
9 |
10 | /**
11 | * Invokes [action] everytime notifyPropertyChanged is called for the receiver property.
12 | */
13 | inline fun KProperty0.observe(
14 | lifecycleOwner: LifecycleOwner? = null,
15 | invokeImmediately: Boolean = false,
16 | crossinline action: (T) -> Unit
17 | ): () -> Unit {
18 | val observableContainer = (this as? CallableReference)?.boundReceiver?.let { it as? ObservableContainer }
19 | ?: throw IllegalArgumentException("Property receiver is not an Observable")
20 |
21 | val onPropertyChangedCallback = OnPropertyChangedCallback { _, propertyName ->
22 | if (propertyName == name) {
23 | action(get())
24 | }
25 | }
26 |
27 | if (lifecycleOwner != null) {
28 | observableContainer.addOnPropertyChangedCallback(lifecycleOwner, onPropertyChangedCallback)
29 | } else {
30 | observableContainer.addOnPropertyChangedCallback(onPropertyChangedCallback)
31 | }
32 |
33 | if (invokeImmediately) {
34 | action(get())
35 | }
36 |
37 | return { observableContainer.removeOnPropertyChangedCallback(onPropertyChangedCallback) }
38 | }
39 |
--------------------------------------------------------------------------------
/rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/RxBindablePropertyBase.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import kotlin.properties.ReadOnlyProperty
9 | import kotlin.reflect.KProperty
10 |
11 | /**
12 | * Base class for RxKotlin related BindableProperties.
13 | */
14 | open class RxBindablePropertyBase protected constructor(
15 | private val viewModel: ViewModel,
16 | defaultValue: T,
17 | private val fieldId: Int,
18 | distinct: Boolean,
19 | afterSet: AfterSet?,
20 | beforeSet: BeforeSet?,
21 | validate: Validate?
22 | ) : BindablePropertyBase(distinct, afterSet, beforeSet, validate), ReadOnlyProperty {
23 | protected var value: T = defaultValue
24 | set(value) {
25 | if (distinct && value === field) return
26 |
27 | val oldValue = field
28 | beforeSet?.invoke(oldValue, value)
29 | field = when (val validate = validate) {
30 | null -> value
31 | else -> validate(oldValue, value)
32 | }
33 |
34 | viewModel.notifyPropertyChanged(fieldId)
35 | afterSet?.invoke(oldValue, field)
36 | }
37 |
38 | final override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
39 | }
40 |
--------------------------------------------------------------------------------
/rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/RxBindablePropertyBase.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import kotlin.properties.ReadOnlyProperty
9 | import kotlin.reflect.KProperty
10 |
11 | /**
12 | * Base class for RxKotlin related BindableProperties.
13 | */
14 | open class RxBindablePropertyBase protected constructor(
15 | private val viewModel: ViewModel,
16 | defaultValue: T,
17 | private val fieldId: Int,
18 | distinct: Boolean,
19 | afterSet: AfterSet?,
20 | beforeSet: BeforeSet?,
21 | validate: Validate?
22 | ) : BindablePropertyBase(distinct, afterSet, beforeSet, validate), ReadOnlyProperty {
23 | protected var value: T = defaultValue
24 | set(value) {
25 | if (distinct && value === field) return
26 |
27 | val oldValue = field
28 | beforeSet?.invoke(oldValue, value)
29 | field = when (val validate = validate) {
30 | null -> value
31 | else -> validate(oldValue, value)
32 | }
33 |
34 | viewModel.notifyPropertyChanged(fieldId)
35 | afterSet?.invoke(oldValue, field)
36 | }
37 |
38 | final override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
39 | }
40 |
--------------------------------------------------------------------------------
/core/src/test/java/de/trbnb/mvvmbase/test/events/EventChannelImplTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.test.events
2 |
3 | import de.trbnb.mvvmbase.events.Event
4 | import de.trbnb.mvvmbase.events.EventChannel
5 | import de.trbnb.mvvmbase.events.EventChannelImpl
6 | import org.junit.jupiter.api.Assertions
7 | import org.junit.jupiter.api.Test
8 |
9 | class EventChannelImplTests {
10 | @Test
11 | fun `events are forwarded`() {
12 | val eventChannel: EventChannel = EventChannelImpl()
13 | var eventWasReceived = false
14 | eventChannel.addListener {
15 | eventWasReceived = true
16 | }
17 | eventChannel(TestEvent())
18 | Assertions.assertEquals(eventWasReceived, true)
19 | }
20 |
21 | @Test
22 | fun `memorized events are forwarded`() {
23 | val eventChannel: EventChannel = EventChannelImpl(memorizeNotReceivedEvents = true)
24 | var eventWasReceived = false
25 | eventChannel(TestEvent())
26 |
27 | eventChannel.addListener {
28 | eventWasReceived = true
29 | }
30 |
31 | Assertions.assertEquals(eventWasReceived, true)
32 | }
33 |
34 | @Test
35 | fun `event aren't forwarding if memorizing is deactivated`() {
36 | val eventChannel: EventChannel = EventChannelImpl(memorizeNotReceivedEvents = false)
37 | var eventWasReceived = false
38 | eventChannel(TestEvent())
39 |
40 | eventChannel.addListener {
41 | eventWasReceived = true
42 | }
43 |
44 | Assertions.assertEquals(eventWasReceived, false)
45 | }
46 |
47 | class TestEvent : Event
48 | }
49 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/commands/SimpleCommand.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.commands
2 |
3 | import androidx.databinding.Bindable
4 | import de.trbnb.mvvmbase.databinding.ViewModel
5 |
6 | /**
7 | * A [Command] implementation that can simply be set as en-/disabled with a boolean value.
8 | *
9 | * @param action The initial action that will be run when the Command is executed.
10 | * @param isEnabled Has to be `true` if this Command should be enabled, otherwise `false`.
11 | */
12 | class SimpleCommand internal constructor(isEnabled: Boolean = true, action: (P) -> R) : BaseCommandImpl(action) {
13 | @get:Bindable
14 | override var isEnabled: Boolean = isEnabled
15 | set(value) {
16 | if (field == value) return
17 | field = value
18 | triggerEnabledChangedListener()
19 | }
20 | }
21 |
22 | /**
23 | * Helper function to create a [SimpleCommand] that clears all it's listeners automatically when
24 | * [ViewModel.onUnbind] is called.
25 | */
26 | @JvmName("parameterizedSimpleCommand")
27 | fun
ViewModel.simpleCommand(
28 | isEnabled: Boolean = true,
29 | action: (P) -> R
30 | ): SimpleCommand
= SimpleCommand(isEnabled, action).apply {
31 | observeLifecycle(this@simpleCommand)
32 | }
33 |
34 | /**
35 | * Helper function to create a parameter-less [SimpleCommand] that clears all it's listeners automatically when
36 | * [ViewModel.onUnbind] is called.
37 | */
38 | fun ViewModel.simpleCommand(
39 | isEnabled: Boolean = true,
40 | action: (Unit) -> R
41 | ): SimpleCommand = simpleCommand(isEnabled, action)
42 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/recyclerview/BindingViewHolder.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.recyclerview
2 |
3 | import androidx.databinding.ViewDataBinding
4 | import androidx.recyclerview.widget.RecyclerView
5 | import de.trbnb.mvvmbase.databinding.BR
6 | import de.trbnb.mvvmbase.databinding.ViewModel
7 |
8 | /**
9 | * [RecyclerView.ViewHolder] implementation for item-ViewModels.
10 | * Setting the item for this ViewHolder should only happen via [bind] so calling of [ViewModel.onUnbind], [ViewModel.onBind]
11 | * and [ViewDataBinding.executePendingBindings] is ensured.
12 | *
13 | * @param binding Binding containing the view associated with this ViewHolder.
14 | */
15 | open class BindingViewHolder(
16 | val binding: B,
17 | private val viewModelFieldId: Int = BR.vm
18 | ) : RecyclerView.ViewHolder(binding.root) {
19 | /**
20 | * Gets the current ViewModel associated with the [binding].
21 | */
22 | var viewModel: ViewModel? = null
23 | private set(value) {
24 | field?.onUnbind()
25 | field = value
26 | binding.setVariable(viewModelFieldId, value)
27 | binding.executePendingBindings()
28 | value?.onBind()
29 | }
30 |
31 | /**
32 | * Sets the ViewModel as [binding] variable.
33 | */
34 | fun bind(viewModel: ViewModel) {
35 | this.viewModel = viewModel
36 | onBound(viewModel)
37 | }
38 |
39 | /**
40 | * Called when [viewModel] was bound to [binding].
41 | * The ViewModel with specific type can be accessed via [binding].
42 | */
43 | open fun onBound(viewModel: ViewModel) {}
44 | }
45 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/bindings/CommandBindings.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.bindings
2 |
3 | import android.view.View
4 | import androidx.databinding.BindingAdapter
5 | import de.trbnb.mvvmbase.databinding.commands.Command
6 |
7 | /**
8 | * Binds the given [Command] as command that will be invoked when the View has been clicked.
9 | * This will also bind the [View.isEnabled] property to the [Command.isEnabled] property.
10 | */
11 | @BindingAdapter("clickCommand")
12 | fun View.bindClickCommand(command: Command?) {
13 | if (command == null) {
14 | setOnClickListener(null)
15 | return
16 | }
17 | bindEnabled(command)
18 |
19 | setOnClickListener {
20 | command.invokeSafely(Unit)
21 | }
22 | }
23 |
24 | /**
25 | * Binds the [View.isEnabled] property to the [Command.isEnabled] property of the given instances.
26 | */
27 | fun View.bindEnabled(command: Command<*, *>?) {
28 | command ?: return
29 | isEnabled = command.isEnabled
30 |
31 | command.addEnabledListenerForView { enabled ->
32 | post {
33 | isEnabled = enabled
34 | }
35 | }
36 | }
37 |
38 | /**
39 | * Binds the given [Command] as command that will be invoked when the View has been long-clicked.
40 | */
41 | @BindingAdapter("longClickCommand")
42 | fun View.bindLongClickCommand(command: Command?) {
43 | if (command == null) {
44 | setOnLongClickListener(null)
45 | return
46 | }
47 |
48 | setOnLongClickListener {
49 | if (command.isEnabled) {
50 | command.invoke(Unit) as? Boolean ?: true
51 | } else {
52 | false
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/utils/SavedStateUtils.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.utils
2 |
3 | import android.os.Binder
4 | import android.os.Bundle
5 | import android.os.Parcelable
6 | import android.util.Size
7 | import android.util.SizeF
8 | import java.io.Serializable
9 |
10 | /**
11 | * Function that specifies which types allow for saving state in [de.trbnb.mvvmbase.observableproperty.BindableProperty].
12 | */
13 | inline fun savingStateInBindableSupports(): Boolean {
14 | val clazz = T::class.java
15 | return when {
16 | BooleanArray::class.java.isAssignableFrom(clazz) ||
17 | ByteArray::class.java.isAssignableFrom(clazz) ||
18 | CharArray::class.java.isAssignableFrom(clazz) ||
19 | CharSequence::class.java.isAssignableFrom(clazz) ||
20 | Array::class.java.isAssignableFrom(clazz) ||
21 | DoubleArray::class.java.isAssignableFrom(clazz) ||
22 | FloatArray::class.java.isAssignableFrom(clazz) ||
23 | IntArray::class.java.isAssignableFrom(clazz) ||
24 | LongArray::class.java.isAssignableFrom(clazz) ||
25 | ShortArray::class.java.isAssignableFrom(clazz) ||
26 | String::class.java.isAssignableFrom(clazz) ||
27 | Array::class.java.isAssignableFrom(clazz) ||
28 | Binder::class.java.isAssignableFrom(clazz) ||
29 | Bundle::class.java.isAssignableFrom(clazz) ||
30 | Parcelable::class.java.isAssignableFrom(clazz) ||
31 | Array::class.java.isAssignableFrom(clazz) ||
32 | Serializable::class.java.isAssignableFrom(clazz) ||
33 | Size::class.java.isAssignableFrom(clazz) ||
34 | SizeF::class.java.isAssignableFrom(clazz) -> true
35 |
36 | else -> false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sample/src/main/java/de/trbnb/mvvmbase/sample/second/SecondScreen.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.sample.second
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Slider
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.MutableState
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import androidx.compose.ui.unit.dp
16 | import androidx.hilt.navigation.compose.hiltViewModel
17 | import de.trbnb.mvvmbase.compose.observeAsMutableState
18 | import de.trbnb.mvvmbase.compose.observeAsState
19 |
20 | @Composable
21 | fun SecondScreen() {
22 | val viewModel = hiltViewModel()
23 | val text by viewModel::text.observeAsState()
24 |
25 | SecondScreenTemplate(
26 | text = text,
27 | progress = viewModel::progress.observeAsMutableState()
28 | )
29 | }
30 |
31 | @Preview(showSystemUi = true)
32 | @Composable
33 | internal fun SecondScreenTemplate(
34 | text: String = "Foo bar",
35 | progress: MutableState = remember { mutableStateOf(50) }
36 | ) {
37 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
38 | Text(text = text, Modifier.padding(4.dp))
39 | Text(text = progress.value.toString(), Modifier.padding(4.dp))
40 | Slider(
41 | value = progress.value.toFloat(),
42 | valueRange = 0f..100f,
43 | onValueChange = { progress.value = it.toInt() }
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/commands/BaseCommandImpl.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.commands
2 |
3 | import androidx.databinding.BaseObservable
4 | import de.trbnb.mvvmbase.databinding.BR
5 |
6 | /**
7 | * Base class for standard [Command] implementations.
8 |
9 | * An implementation of the [Command.isEnabled] is not given.
10 | *
11 | * @param action The initial action that will be run when the Command is executed.
12 | */
13 | abstract class BaseCommandImpl(private val action: (P) -> R) : BaseObservable(), Command {
14 | private val listeners = mutableListOf<(Boolean) -> Unit>()
15 |
16 | final override fun invoke(param: P): R {
17 | if (!isEnabled) {
18 | throw DisabledCommandInvocationException()
19 | }
20 |
21 | return action(param)
22 | }
23 |
24 | /**
25 | * This method should be called when the result of [isEnabled] might have changed.
26 | */
27 | protected fun triggerEnabledChangedListener() {
28 | notifyPropertyChanged(BR.enabled)
29 | listeners.forEach { it(isEnabled) }
30 | }
31 |
32 | override fun addEnabledListener(listener: EnabledListener) {
33 | listeners.add(listener)
34 | }
35 |
36 | override fun removeEnabledListener(listener: EnabledListener) {
37 | listeners.remove(listener)
38 | }
39 |
40 | override fun clearEnabledListeners() {
41 | listeners.clear()
42 | }
43 |
44 | override fun addEnabledListenerForView(listener: EnabledListener) {
45 | listeners.add(ViewListener(listener))
46 | }
47 |
48 | override fun clearEnabledListenersForViews() {
49 | listeners.removeAll { it is ViewListener }
50 | }
51 |
52 | private class ViewListener(private val listener: EnabledListener) : EnabledListener {
53 | override fun invoke(enabled: Boolean) = listener(enabled)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/rxjava2/src/test/java/de/trbnb/mvvmbase/rxjava2/test/DisposableTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2.test
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import androidx.lifecycle.ViewModelStore
6 | import de.trbnb.mvvmbase.databinding.BaseViewModel
7 | import de.trbnb.mvvmbase.rxjava2.autoDispose
8 | import de.trbnb.mvvmbase.rxjava2.compositeDisposable
9 | import io.reactivex.disposables.CompositeDisposable
10 | import org.junit.jupiter.api.Test
11 |
12 | class DisposableTests {
13 | @Test
14 | fun `compositeDisposable extension is the same for a ViewModel`() {
15 | val viewModel = object : BaseViewModel() {}
16 | val firstCompositeDisposable = viewModel.compositeDisposable
17 | val secondCompositeDisposable = viewModel.compositeDisposable
18 |
19 | assert(firstCompositeDisposable === secondCompositeDisposable)
20 | }
21 |
22 | @Test
23 | fun `compositeDisposable is disposed when ViewModelStore is cleared`() {
24 | val viewModelStore = ViewModelStore()
25 | val viewModel = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {
26 | @Suppress("UNCHECKED_CAST")
27 | override fun create(modelClass: Class) = object : BaseViewModel() {} as T
28 | })[BaseViewModel::class.java]
29 | val compositeDisposable = viewModel.compositeDisposable
30 | viewModel.destroy()
31 | viewModelStore.clear()
32 | assert(compositeDisposable.isDisposed)
33 | }
34 |
35 | @Test
36 | fun `autoDispose disposes in onDestroy`() {
37 | val compositeDisposable = CompositeDisposable()
38 | val viewModel = object : BaseViewModel() {
39 | init {
40 | compositeDisposable.autoDispose(this)
41 | }
42 | }
43 | viewModel.destroy()
44 | assert(compositeDisposable.isDisposed)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/events/EventChannel.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.events
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleEventObserver
5 | import androidx.lifecycle.LifecycleOwner
6 |
7 | typealias EventListener = (event: Event) -> Unit
8 |
9 | /**
10 | * Base interface that defines interaction for not-state information between [de.trbnb.mvvmbase.ViewModel]
11 | * and MVVM view components.
12 | */
13 | interface EventChannel {
14 | /**
15 | * Invokes all listeners with given [event].
16 | */
17 | operator fun invoke(event: Event)
18 |
19 | /**
20 | * Registers a new listener.
21 | */
22 | fun addListener(eventListener: EventListener): EventListener
23 |
24 | /**
25 | * Removes a listener.
26 | */
27 | fun removeListener(eventListener: EventListener)
28 |
29 | /**
30 | * Registers a new listener.
31 | *
32 | * @see [addListener]
33 | */
34 | operator fun plusAssign(eventListener: EventListener) {
35 | addListener(eventListener)
36 | }
37 |
38 | /**
39 | * Removes a listener.
40 | *
41 | * @see [removeListener]
42 | */
43 | operator fun minusAssign(eventListener: EventListener) = removeListener(eventListener)
44 | }
45 |
46 | /**
47 | * Adds a listener and will remove it when the [Lifecycle] of [lifecycleOwner] is [Lifecycle.State.DESTROYED].
48 | */
49 | fun EventChannel.addListener(lifecycleOwner: LifecycleOwner, eventListener: EventListener) {
50 | addListener(eventListener)
51 |
52 | lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
53 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
54 | if (event == Lifecycle.Event.ON_DESTROY) {
55 | removeListener(eventListener)
56 | lifecycleOwner.lifecycle.removeObserver(this)
57 | }
58 | }
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/rxjava3/src/test/java/de/trbnb/mvvmbase/rxjava3/test/DisposableTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3.test
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import androidx.lifecycle.ViewModelStore
6 | import de.trbnb.mvvmbase.databinding.BaseViewModel
7 | import de.trbnb.mvvmbase.rxjava3.autoDispose
8 | import de.trbnb.mvvmbase.rxjava3.compositeDisposable
9 | import io.reactivex.rxjava3.disposables.CompositeDisposable
10 | import org.junit.jupiter.api.Test
11 |
12 | class DisposableTests {
13 | @Test
14 | fun `compositeDisposable extension is the same for a ViewModel`() {
15 | val viewModel = object : BaseViewModel() {}
16 | val firstCompositeDisposable = viewModel.compositeDisposable
17 | val secondCompositeDisposable = viewModel.compositeDisposable
18 |
19 | assert(firstCompositeDisposable === secondCompositeDisposable)
20 | }
21 |
22 | @Test
23 | fun `compositeDisposable is disposed when ViewModelStore is cleared`() {
24 | val viewModelStore = ViewModelStore()
25 | val viewModel = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {
26 | @Suppress("UNCHECKED_CAST")
27 | override fun create(modelClass: Class) = object : BaseViewModel() {} as T
28 | })[BaseViewModel::class.java]
29 | val compositeDisposable = viewModel.compositeDisposable
30 | viewModel.destroy()
31 | viewModelStore.clear()
32 | assert(compositeDisposable.isDisposed)
33 | }
34 |
35 | @Test
36 | fun `autoDispose disposes in onDestroy`() {
37 | val compositeDisposable = CompositeDisposable()
38 | val viewModel = object : BaseViewModel() {
39 | init {
40 | compositeDisposable.autoDispose(this)
41 | }
42 | }
43 | viewModel.destroy()
44 | assert(compositeDisposable.isDisposed)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/src/test/java/de/trbnb/mvvmbase/test/ViewModelLifecycleTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.test
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleEventObserver
5 | import androidx.lifecycle.LifecycleOwner
6 | import de.trbnb.mvvmbase.BaseViewModel
7 | import de.trbnb.mvvmbase.MvvmBase
8 | import org.junit.jupiter.api.Assertions.assertEquals
9 | import org.junit.jupiter.api.Test
10 |
11 | class ViewModelLifecycleTests {
12 | class ViewModel : BaseViewModel()
13 |
14 | @Test
15 | fun `initial state`() {
16 | MvvmBase.disableViewModelLifecycleThreadConstraints()
17 | val observer = LifecycleObserver()
18 | val viewModel = ViewModel().apply {
19 | lifecycle.addObserver(observer)
20 | }
21 | assert(viewModel.lifecycle.currentState == Lifecycle.State.RESUMED)
22 | }
23 |
24 | @Test
25 | fun `after onDestroy() without binding`() {
26 | MvvmBase.disableViewModelLifecycleThreadConstraints()
27 | val observer = LifecycleObserver()
28 | val viewModel = ViewModel().apply {
29 | lifecycle.addObserver(observer)
30 | }
31 | viewModel.destroy()
32 | assert(viewModel.lifecycle.currentState == Lifecycle.State.DESTROYED)
33 |
34 | assertEquals(Lifecycle.Event.ON_CREATE, observer.events[0])
35 | assertEquals(Lifecycle.Event.ON_START, observer.events[1])
36 | assertEquals(Lifecycle.Event.ON_RESUME, observer.events[2])
37 | assertEquals(Lifecycle.Event.ON_PAUSE, observer.events[3])
38 | assertEquals(Lifecycle.Event.ON_STOP, observer.events[4])
39 | assertEquals(Lifecycle.Event.ON_DESTROY, observer.events[5])
40 | }
41 |
42 | class LifecycleObserver : LifecycleEventObserver {
43 | val events = mutableListOf()
44 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
45 | events += event
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/coroutines/src/main/java/de/trbnb/mvvmbase/coroutines/CoroutineViewModel.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.coroutines
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import de.trbnb.mvvmbase.coroutines.flow.FlowBindable
5 | import de.trbnb.mvvmbase.coroutines.flow.OnCompletion
6 | import de.trbnb.mvvmbase.coroutines.flow.OnException
7 | import de.trbnb.mvvmbase.databinding.ViewModel
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | /**
13 | * Interface that defines coroutines extensions for [ViewModel].
14 | */
15 | interface CoroutineViewModel : ViewModel {
16 | /**
17 | * Gets a [CoroutineScope] that is cancelled when the ViewModel is destroyed.
18 | */
19 | val viewModelScope: CoroutineScope
20 | get() = (this as? androidx.lifecycle.ViewModel)?.viewModelScope ?: throw RuntimeException(
21 | "ViewModel doesn't extend androidx.lifecycle.ViewModel and has to implement viewModelScope manually."
22 | )
23 |
24 | /**
25 | * Creates a new FlowBindable.Provider instance.
26 | *
27 | * @param defaultValue Value of the property from the start.
28 | */
29 | @ExperimentalCoroutinesApi
30 | fun Flow.toBindable(
31 | defaultValue: T,
32 | onException: OnException? = null,
33 | onCompletion: OnCompletion? = null,
34 | scope: CoroutineScope = viewModelScope
35 | ): FlowBindable.Provider = FlowBindable.Provider(this, onException, onCompletion, scope, defaultValue)
36 |
37 | /**
38 | * Creates a new FlowBindable.Provider instance with `null` as default value.
39 | */
40 | @ExperimentalCoroutinesApi
41 | fun Flow.toBindable(
42 | onException: OnException? = null,
43 | onCompletion: OnCompletion? = null,
44 | scope: CoroutineScope = viewModelScope
45 | ): FlowBindable.Provider = toBindable(null, onException, onCompletion, scope)
46 | }
47 |
--------------------------------------------------------------------------------
/databinding/src/test/java/de/trbnb/mvvmbase/databinding/test/NestedViewModelTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.test
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import de.trbnb.mvvmbase.BaseViewModel
5 | import de.trbnb.mvvmbase.events.Event
6 | import org.junit.jupiter.api.Test
7 | import java.io.Closeable
8 |
9 | class NestedViewModelTests {
10 | @Test
11 | fun `autoDestroy() destroys ViewModels in a parent ViewModel`() {
12 | val childViewModel = SimpleViewModel()
13 | val parentViewModel = object : BaseViewModel() {
14 | val items = listOf(childViewModel).autoDestroy()
15 | }
16 |
17 | parentViewModel.destroy()
18 | assert(childViewModel.lifecycle.currentState == Lifecycle.State.DESTROYED)
19 | }
20 |
21 | @Test
22 | fun `bindEvents() redirects events from child ViewModels to eventChannel of parent ViewModel`() {
23 | val event = object : Event {}
24 | val childViewModel = SimpleViewModel()
25 | val parentViewModel = object : BaseViewModel() {
26 | val items = listOf(childViewModel).bindEvents()
27 | }
28 |
29 | var eventWasReceived = false
30 |
31 | parentViewModel.eventChannel.addListener { newEvent ->
32 | if (event == newEvent) {
33 | eventWasReceived = true
34 | }
35 | }
36 |
37 | childViewModel.eventChannel(event)
38 | assert(eventWasReceived)
39 | }
40 |
41 | @Test
42 | fun `destroy() closes all tags`() {
43 | var tagIsClosed = false
44 | val viewModel = object : BaseViewModel() {
45 | init {
46 | initTag("foo", object : Closeable {
47 | override fun close() {
48 | tagIsClosed = true
49 | }
50 | })
51 | }
52 | }
53 |
54 | viewModel.destroy()
55 | assert(tagIsClosed)
56 | }
57 | }
58 |
59 | class SimpleViewModel : BaseViewModel()
60 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/DataBindingViewModelLifecycleOwner.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleOwner
6 | import androidx.lifecycle.LifecycleRegistry
7 |
8 | /**
9 | * The custom lifecycle owner for ViewModels.
10 | *
11 | * Its lifecycle state is:
12 | * - After initialization/being unbound: [Lifecycle.State.STARTED].
13 | * - After being bound: [Lifecycle.State.RESUMED].
14 | * - After being destroyed: [Lifecycle.State.DESTROYED].
15 | */
16 | internal class DataBindingViewModelLifecycleOwner(enforceMainThread: Boolean) : LifecycleOwner {
17 | @SuppressLint("VisibleForTests")
18 | private val registry = when (enforceMainThread) {
19 | true -> LifecycleRegistry(this)
20 | false -> LifecycleRegistry.createUnsafe(this)
21 | }
22 |
23 | init {
24 | onEvent(Event.INITIALIZED)
25 | }
26 |
27 | fun onEvent(event: Event) {
28 | registry.currentState = when (event) {
29 | Event.INITIALIZED -> Lifecycle.State.STARTED
30 | Event.BOUND -> Lifecycle.State.RESUMED
31 | Event.UNBOUND -> Lifecycle.State.STARTED
32 | Event.DESTROYED -> Lifecycle.State.DESTROYED
33 | }
34 | }
35 |
36 | override fun getLifecycle() = registry
37 |
38 | internal fun getInternalState() = when (registry.currentState) {
39 | Lifecycle.State.DESTROYED -> State.DESTROYED
40 | Lifecycle.State.RESUMED -> State.BOUND
41 | else -> State.INITIALIZED
42 | }
43 |
44 | /**
45 | * Enum for the specific Lifecycle of ViewModels.
46 | */
47 | internal enum class State {
48 | INITIALIZED,
49 | BOUND,
50 | DESTROYED
51 | }
52 |
53 | /**
54 | * Enum for the specific Lifecycle of ViewModels.
55 | */
56 | internal enum class Event {
57 | INITIALIZED,
58 | BOUND,
59 | UNBOUND,
60 | DESTROYED
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/commands/CommandUtils.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.commands
2 |
3 | import androidx.databinding.Observable
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleEventObserver
6 | import androidx.lifecycle.LifecycleOwner
7 |
8 | internal fun Command<*, *>.observeLifecycle(lifecycleOwner: LifecycleOwner) {
9 | lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
10 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
11 | if (event == Lifecycle.Event.ON_PAUSE) {
12 | clearEnabledListenersForViews()
13 | }
14 | }
15 | })
16 | }
17 |
18 | internal fun RuleCommand<*, *>.dependsOn(observable: Observable, dependentFieldIds: IntArray?) {
19 | if (dependentFieldIds?.isEmpty() != false) return
20 | observable.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
21 | override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
22 | if (propertyId in dependentFieldIds) {
23 | onEnabledChanged()
24 | }
25 | }
26 | })
27 | }
28 |
29 | /**
30 | * Calls [Command.addEnabledListener] and removes the listener if the lifecycle of [lifecycleOwner] is destroyed.
31 | */
32 | fun Command<*, *>.addEnabledListener(lifecycleOwner: LifecycleOwner, listener: (enabled: Boolean) -> Unit) {
33 | addEnabledListener(listener)
34 | lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
35 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
36 | if (event == Lifecycle.Event.ON_DESTROY) {
37 | removeEnabledListener(listener)
38 | }
39 | }
40 | })
41 | }
42 |
43 | /**
44 | * Invokes the command with the parameter [Unit].
45 | */
46 | operator fun Command.invoke() = invoke(Unit)
47 |
48 | /**
49 | * Invokes the command safely with the parameter [Unit].
50 | */
51 | fun Command.invokeSafely() = invokeSafely(Unit)
52 |
--------------------------------------------------------------------------------
/core/src/test/java/de/trbnb/mvvmbase/test/bindableproperty/BindablePropertySavedStateHandleTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.test.bindableproperty
2 |
3 | import android.util.Size
4 | import android.util.SizeF
5 | import androidx.lifecycle.SavedStateHandle
6 | import de.trbnb.mvvmbase.MvvmBase
7 | import de.trbnb.mvvmbase.observableproperty.observable
8 | import de.trbnb.mvvmbase.savedstate.BaseStateSavingViewModel
9 | import de.trbnb.mvvmbase.utils.savingStateInBindableSupports
10 | import org.junit.jupiter.api.BeforeEach
11 | import org.junit.jupiter.api.Test
12 |
13 | class BindablePropertySavedStateHandleTests {
14 | @BeforeEach
15 | fun setup() {
16 | MvvmBase.disableViewModelLifecycleThreadConstraints()
17 | }
18 |
19 | class TestViewModel(savedStateHandle: SavedStateHandle = SavedStateHandle()) : BaseStateSavingViewModel(savedStateHandle) {
20 | var text: String by observable("foo")
21 | var userSetting: Boolean by observable(false)
22 | var nullableBoolean: Boolean? by observable()
23 | var isDone: Boolean by observable(false)
24 | var property: String? by observable()
25 | }
26 |
27 | @Test
28 | fun `saved state integration in bindable properties`() {
29 | val handle = SavedStateHandle().apply {
30 | set("text", "Meh")
31 | set("isDone", true)
32 | }
33 |
34 | val viewModel = TestViewModel(handle)
35 | assert(viewModel.text == "Meh")
36 | assert(viewModel.userSetting.not())
37 | assert(viewModel.nullableBoolean == null)
38 | assert(viewModel.isDone)
39 | }
40 |
41 | @Test
42 | fun `supported state saving types`() {
43 | assert(savingStateInBindableSupports())
44 | assert(savingStateInBindableSupports())
45 | assert(savingStateInBindableSupports>())
46 | assert(!savingStateInBindableSupports>())
47 | assert(savingStateInBindableSupports>())
48 |
49 | assert(savingStateInBindableSupports())
50 | assert(savingStateInBindableSupports())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/conductor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | `maven-publish`
6 | signing
7 | }
8 |
9 | android {
10 | compileSdk = Android.compileSdk
11 |
12 | defaultConfig {
13 | minSdk = Android.minSdk
14 | targetSdk = Android.compileSdk
15 |
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | named("release").configure {
21 | isMinifyEnabled = false
22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
23 | }
24 | }
25 |
26 | buildFeatures {
27 | dataBinding = true
28 | }
29 |
30 | compileOptions {
31 | sourceCompatibility = Versions.java
32 | targetCompatibility = Versions.java
33 | }
34 |
35 | kotlin {
36 | explicitApi()
37 | }
38 |
39 | kotlinOptions {
40 | jvmTarget = Versions.java.toString()
41 | }
42 | }
43 |
44 | dependencies {
45 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}")
46 |
47 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
48 | implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
49 |
50 | implementation(project(":databinding"))
51 |
52 | api("com.bluelinelabs:conductor:3.1.0")
53 | api("com.bluelinelabs:conductor-archlifecycle:3.1.0")
54 |
55 | testImplementation("junit:junit:4.13.2")
56 | androidTestImplementation("androidx.test:runner:1.4.0")
57 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
58 | }
59 |
60 | val sourcesJar = task("sourcesJar") {
61 | archiveClassifier.set("sources")
62 | from(android.sourceSets["main"].java.srcDirs)
63 | }
64 |
65 | signing {
66 | sign(publishing.publications)
67 | }
68 |
69 | afterEvaluate {
70 | publishing {
71 | repositories {
72 | mavenCentralUpload(project)
73 | }
74 | publications {
75 | create(Publication.CONDUCTOR, this@afterEvaluate)
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/recyclerview/BindingListAdapter.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.recyclerview
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.databinding.BindingAdapter
7 | import androidx.databinding.DataBindingUtil
8 | import androidx.databinding.ViewDataBinding
9 | import androidx.recyclerview.widget.DiffUtil
10 | import androidx.recyclerview.widget.ListAdapter
11 | import androidx.recyclerview.widget.RecyclerView
12 | import de.trbnb.mvvmbase.databinding.ViewModel
13 |
14 | /**
15 | * Basic [ListAdapter] implementation for ViewModel lists.
16 | *
17 | * Uses referential equality for [DiffUtil.ItemCallback.areContentsTheSame]
18 | * and [ViewModel.equals] for [DiffUtil.ItemCallback.areItemsTheSame] by default.
19 | *
20 | * @param layoutId Layout resource ID of the item layout.
21 | */
22 | open class BindingListAdapter(
23 | val layoutId: Int,
24 | diffItemCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() {
25 | @SuppressLint("DiffUtilEquals")
26 | override fun areContentsTheSame(oldItem: VM, newItem: VM) = oldItem == newItem
27 | override fun areItemsTheSame(oldItem: VM, newItem: VM) = oldItem === newItem
28 | }
29 | ) : ListAdapter>(diffItemCallback) {
30 | override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
31 | holder.bind(getItem(position))
32 | }
33 |
34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = BindingViewHolder(
35 | DataBindingUtil.inflate(LayoutInflater.from(parent.context), layoutId, parent, false)
36 | )
37 | }
38 |
39 | /**
40 | * Binding adapter function to make use of [BindingListAdapter].
41 | */
42 | @BindingAdapter("items", "itemLayout")
43 | fun RecyclerView.setItems(items: List, itemLayout: Int) {
44 | @Suppress("UNCHECKED_CAST")
45 | (adapter as? BindingListAdapter
46 | ?: BindingListAdapter(itemLayout).also { this.adapter = it }).submitList(items)
47 | }
48 |
--------------------------------------------------------------------------------
/rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/CompletableBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.Completable
10 | import io.reactivex.rxkotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * BindableProperty implementation for [Completable]s.
15 | */
16 | class CompletableBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | fieldId: Int,
19 | completable: Completable,
20 | onError: (Throwable) -> Unit,
21 | distinct: Boolean,
22 | afterSet: AfterSet?,
23 | beforeSet: BeforeSet?,
24 | validate: Validate?
25 | ) : RxBindablePropertyBase(viewModel, false, fieldId, distinct, afterSet, beforeSet, validate) {
26 | init {
27 | viewModel.compositeDisposable += completable.subscribe({ value = true }, onError)
28 | }
29 |
30 | /**
31 | * Property delegate provider for [CompletableBindableProperty].
32 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
33 | *
34 | * @see CompletableBindableProperty
35 | */
36 | class Provider internal constructor(
37 | private val completable: Completable,
38 | private val onError: (Throwable) -> Unit
39 | ) : BindablePropertyBase.Provider() {
40 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = CompletableBindableProperty(
41 | viewModel = thisRef,
42 | fieldId = property.resolveFieldId(),
43 | completable = completable,
44 | onError = onError,
45 | distinct = distinct,
46 | afterSet = afterSet,
47 | beforeSet = beforeSet,
48 | validate = validate
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/CompletableBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.rxjava3.core.Completable
10 | import io.reactivex.rxjava3.kotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * BindableProperty implementation for [Completable]s.
15 | */
16 | class CompletableBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | fieldId: Int,
19 | completable: Completable,
20 | onError: (Throwable) -> Unit,
21 | distinct: Boolean,
22 | afterSet: AfterSet?,
23 | beforeSet: BeforeSet?,
24 | validate: Validate?
25 | ) : RxBindablePropertyBase(viewModel, false, fieldId, distinct, afterSet, beforeSet, validate) {
26 | init {
27 | viewModel.compositeDisposable += completable.subscribe({ value = true }, onError)
28 | }
29 |
30 | /**
31 | * Property delegate provider for [CompletableBindableProperty].
32 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
33 | *
34 | * @see CompletableBindableProperty
35 | */
36 | class Provider internal constructor(
37 | private val completable: Completable,
38 | private val onError: (Throwable) -> Unit
39 | ) : BindablePropertyBase.Provider() {
40 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = CompletableBindableProperty(
41 | viewModel = thisRef,
42 | fieldId = property.resolveFieldId(),
43 | completable = completable,
44 | onError = onError,
45 | distinct = distinct,
46 | afterSet = afterSet,
47 | beforeSet = beforeSet,
48 | validate = validate
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/core/src/test/java/de/trbnb/mvvmbase/test/NestedViewModelTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.test
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import de.trbnb.mvvmbase.BaseViewModel
5 | import de.trbnb.mvvmbase.MvvmBase
6 | import de.trbnb.mvvmbase.events.Event
7 | import org.junit.jupiter.api.BeforeAll
8 | import org.junit.jupiter.api.Test
9 | import java.io.Closeable
10 |
11 | class NestedViewModelTests {
12 | companion object {
13 | @BeforeAll
14 | @JvmStatic
15 | fun setup() {
16 | MvvmBase.disableViewModelLifecycleThreadConstraints()
17 | }
18 | }
19 |
20 | @Test
21 | fun `autoDestroy() destroys ViewModels in a parent ViewModel`() {
22 | val childViewModel = SimpleViewModel()
23 | val parentViewModel = object : BaseViewModel() {
24 | val items = listOf(childViewModel).autoDestroy()
25 | }
26 |
27 | parentViewModel.destroy()
28 | assert(childViewModel.lifecycle.currentState == Lifecycle.State.DESTROYED)
29 | }
30 |
31 | @Test
32 | fun `bindEvents() redirects events from child ViewModels to eventChannel of parent ViewModel`() {
33 | val event = object : Event {}
34 | val childViewModel = SimpleViewModel()
35 | val parentViewModel = object : BaseViewModel() {
36 | val items = listOf(childViewModel).bindEvents()
37 | }
38 |
39 | var eventWasReceived = false
40 |
41 | parentViewModel.eventChannel.addListener { newEvent ->
42 | if (event == newEvent) {
43 | eventWasReceived = true
44 | }
45 | }
46 |
47 | childViewModel.eventChannel(event)
48 | assert(eventWasReceived)
49 | }
50 |
51 | @Test
52 | fun `destroy() closes all tags`() {
53 | var tagIsClosed = false
54 | val viewModel = object : BaseViewModel() {
55 | init {
56 | initTag("foo", object : Closeable {
57 | override fun close() {
58 | tagIsClosed = true
59 | }
60 | })
61 | }
62 | }
63 |
64 | viewModel.destroy()
65 | assert(tagIsClosed)
66 | }
67 | }
68 |
69 | class SimpleViewModel : BaseViewModel()
70 |
--------------------------------------------------------------------------------
/coroutines/src/test/java/de/trbnb/mvvmbase/coroutines/test/CoroutinesViewModelTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.coroutines.test
2 |
3 | import androidx.databinding.Observable
4 | import de.trbnb.mvvmbase.databinding.BaseViewModel
5 | import de.trbnb.mvvmbase.MvvmBase
6 | import de.trbnb.mvvmbase.coroutines.CoroutineViewModel
7 | import de.trbnb.mvvmbase.events.EventChannel
8 | import org.junit.jupiter.api.Assertions
9 | import org.junit.jupiter.api.BeforeAll
10 | import org.junit.jupiter.api.Test
11 |
12 | class CoroutinesViewModelTests {
13 | companion object {
14 | @BeforeAll
15 | @JvmStatic
16 | fun setup() {
17 | MvvmBase.disableViewModelLifecycleThreadConstraints()
18 | }
19 | }
20 |
21 | @Test
22 | fun `viewModelScope is resolved correctly`() {
23 | val viewModel = object : BaseViewModel(), CoroutineViewModel {}
24 |
25 | viewModel.viewModelScope
26 | }
27 |
28 | @Test
29 | fun `exception thrown if viewModelScope cannot be resolved`() {
30 | val viewModel = object : CoroutineViewModel {
31 | override fun notifyChange() = TODO("Not yet implemented")
32 | override fun notifyPropertyChanged(fieldId: Int) = TODO("Not yet implemented")
33 | override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) = TODO("Not yet implemented")
34 | override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) = TODO("Not yet implemented")
35 | override fun onBind() = TODO("Not yet implemented")
36 | override fun onUnbind() = TODO("Not yet implemented")
37 | override fun onDestroy() = TODO("Not yet implemented")
38 | override fun destroy() = TODO("Not yet implemented")
39 | override fun get(key: String) = TODO("Not yet implemented")
40 | override fun initTag(key: String, newValue: T) = TODO("Not yet implemented")
41 | override fun getLifecycle() = TODO("Not yet implemented")
42 | override val eventChannel: EventChannel get() = TODO("Not yet implemented")
43 | }
44 |
45 | Assertions.assertThrows(RuntimeException::class.java) { viewModel.viewModelScope }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/SingleBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.Single
10 | import io.reactivex.rxkotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Single] or `defaultValue` if no value has been emitted.
15 | */
16 | class SingleBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | single: Single,
21 | onError: (Throwable) -> Unit,
22 | distinct: Boolean,
23 | afterSet: AfterSet?,
24 | beforeSet: BeforeSet?,
25 | validate: Validate?
26 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
27 | init {
28 | viewModel.compositeDisposable += single.subscribe({ value = it }, onError)
29 | }
30 |
31 | /**
32 | * Property delegate provider for [SingleBindableProperty].
33 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
34 | *
35 | * @see SingleBindableProperty
36 | */
37 | class Provider internal constructor(
38 | private val defaultValue: T,
39 | private val single: Single,
40 | private val onError: (Throwable) -> Unit
41 | ) : BindablePropertyBase.Provider() {
42 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = SingleBindableProperty(
43 | viewModel = thisRef,
44 | fieldId = property.resolveFieldId(),
45 | defaultValue = defaultValue,
46 | single = single,
47 | onError = onError,
48 | distinct = distinct,
49 | afterSet = afterSet,
50 | beforeSet = beforeSet,
51 | validate = validate
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/SingleBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.rxjava3.core.Single
10 | import io.reactivex.rxjava3.kotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Single] or `defaultValue` if no value has been emitted.
15 | */
16 | class SingleBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | single: Single,
21 | onError: (Throwable) -> Unit,
22 | distinct: Boolean,
23 | afterSet: AfterSet?,
24 | beforeSet: BeforeSet?,
25 | validate: Validate?
26 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
27 | init {
28 | viewModel.compositeDisposable += single.subscribe({ value = it }, onError)
29 | }
30 |
31 | /**
32 | * Property delegate provider for [SingleBindableProperty].
33 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
34 | *
35 | * @see SingleBindableProperty
36 | */
37 | class Provider internal constructor(
38 | private val defaultValue: T,
39 | private val single: Single,
40 | private val onError: (Throwable) -> Unit
41 | ) : BindablePropertyBase.Provider() {
42 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = SingleBindableProperty(
43 | viewModel = thisRef,
44 | fieldId = property.resolveFieldId(),
45 | defaultValue = defaultValue,
46 | single = single,
47 | onError = onError,
48 | distinct = distinct,
49 | afterSet = afterSet,
50 | beforeSet = beforeSet,
51 | validate = validate
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/core/src/test/java/de/trbnb/mvvmbase/test/BaseViewModelTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.test
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.LifecycleRegistry
6 | import de.trbnb.mvvmbase.BaseViewModel
7 | import de.trbnb.mvvmbase.DependsOn
8 | import de.trbnb.mvvmbase.MvvmBase
9 | import de.trbnb.mvvmbase.observable.addOnPropertyChangedCallback
10 | import de.trbnb.mvvmbase.observableproperty.observable
11 | import de.trbnb.mvvmbase.utils.observe
12 | import org.junit.jupiter.api.Assertions
13 | import org.junit.jupiter.api.BeforeEach
14 | import org.junit.jupiter.api.Test
15 |
16 | class BaseViewModelTests {
17 | @BeforeEach
18 | fun setup() {
19 | MvvmBase.disableViewModelLifecycleThreadConstraints()
20 | }
21 |
22 | @Test
23 | fun `property changed callback with Lifecycle`() {
24 | val observable = object : BaseViewModel() {}
25 |
26 | val lifecycleOwner = object : LifecycleOwner {
27 | private val lifecycle = LifecycleRegistry.createUnsafe(this).apply {
28 | currentState = Lifecycle.State.STARTED
29 | }
30 | override fun getLifecycle() = lifecycle
31 | fun destroy() { lifecycle.currentState = Lifecycle.State.DESTROYED }
32 | }
33 |
34 | var callbackWasTriggered = false
35 | observable.addOnPropertyChangedCallback(lifecycleOwner) { _, _ -> callbackWasTriggered = true }
36 |
37 | lifecycleOwner.destroy()
38 | observable.notifyPropertyChanged("")
39 | assert(!callbackWasTriggered)
40 | }
41 |
42 | @Test
43 | fun `@DependsOn`() {
44 | val viewModel = object : BaseViewModel() {
45 | var foo by observable("")
46 |
47 | @DependsOn("foo")
48 | val bar: String
49 | get() = "${foo}bar"
50 | }
51 |
52 | var barChanged = false
53 | var amountOfNotify = 0
54 |
55 | viewModel.addOnPropertyChangedCallback { _, _ ->
56 | amountOfNotify++
57 | }
58 |
59 | viewModel::bar.observe {
60 | barChanged = true
61 | }
62 |
63 | viewModel.foo = "foo"
64 |
65 | Assertions.assertEquals(true, barChanged)
66 | Assertions.assertEquals(2, amountOfNotify)
67 | Assertions.assertEquals("foobar", viewModel.bar)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/MaybeBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.Maybe
10 | import io.reactivex.rxkotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Maybe] or `defaultValue` if no value has been emitted.
15 | */
16 | class MaybeBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | maybe: Maybe,
21 | onError: (Throwable) -> Unit,
22 | onComplete: () -> Unit,
23 | distinct: Boolean,
24 | afterSet: AfterSet?,
25 | beforeSet: BeforeSet?,
26 | validate: Validate?
27 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
28 | init {
29 | viewModel.compositeDisposable += maybe.subscribe({ value = it }, onError, onComplete)
30 | }
31 |
32 | /**
33 | * Property delegate provider for [MaybeBindableProperty].
34 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
35 | *
36 | * @see MaybeBindableProperty
37 | */
38 | class Provider internal constructor(
39 | private val defaultValue: T,
40 | private val maybe: Maybe,
41 | private val onError: (Throwable) -> Unit,
42 | private val onComplete: () -> Unit
43 | ) : BindablePropertyBase.Provider() {
44 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = MaybeBindableProperty(
45 | viewModel = thisRef,
46 | fieldId = property.resolveFieldId(),
47 | defaultValue = defaultValue,
48 | maybe = maybe,
49 | onError = onError,
50 | onComplete = onComplete,
51 | distinct = distinct,
52 | afterSet = afterSet,
53 | beforeSet = beforeSet,
54 | validate = validate
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rxjava2/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | `maven-publish`
6 | signing
7 | }
8 |
9 | android {
10 | compileSdk = Android.compileSdk
11 |
12 | defaultConfig {
13 | minSdk = Android.minSdk
14 | targetSdk = Android.compileSdk
15 |
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | named("release").configure {
21 | isMinifyEnabled = false
22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
23 | }
24 | }
25 |
26 | buildFeatures {
27 | dataBinding = true
28 | }
29 |
30 | dataBinding {
31 | // This is necessary to allow the data binding annotation processor to generate
32 | // the BR fields from Bindable annotations
33 | testOptions.unitTests.isIncludeAndroidResources = true
34 |
35 | isEnabledForTests = true
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility = Versions.java
40 | targetCompatibility = Versions.java
41 | }
42 |
43 | kotlin {
44 | explicitApi()
45 | }
46 |
47 | kotlinOptions {
48 | jvmTarget = Versions.java.toString()
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}")
54 |
55 | implementation(project(":databinding"))
56 |
57 | testAnnotationProcessor("androidx.databinding:databinding-compiler:${Versions.gradleTools}")
58 | kaptTest("androidx.databinding:databinding-compiler:${Versions.gradleTools}")
59 |
60 | implementation("io.reactivex.rxjava2:rxkotlin:2.4.0")
61 |
62 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0")
63 | androidTestImplementation("androidx.test:runner:1.4.0")
64 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
65 | }
66 |
67 | val sourcesJar = task("sourcesJar") {
68 | archiveClassifier.set("sources")
69 | from(android.sourceSets["main"].java.srcDirs)
70 | }
71 |
72 | signing {
73 | sign(publishing.publications)
74 | }
75 |
76 | afterEvaluate {
77 | publishing {
78 | repositories {
79 | mavenCentralUpload(project)
80 | }
81 | publications {
82 | create(Publication.RX_JAVA_2, this@afterEvaluate)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/rxjava3/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | `maven-publish`
6 | signing
7 | }
8 |
9 | android {
10 | compileSdk = Android.compileSdk
11 |
12 | defaultConfig {
13 | minSdk = Android.minSdk
14 | targetSdk = Android.compileSdk
15 |
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | named("release").configure {
21 | isMinifyEnabled = false
22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
23 | }
24 | }
25 |
26 | buildFeatures {
27 | dataBinding = true
28 | }
29 |
30 | dataBinding {
31 | // This is necessary to allow the data binding annotation processor to generate
32 | // the BR fields from Bindable annotations
33 | testOptions.unitTests.isIncludeAndroidResources = true
34 |
35 | isEnabledForTests = true
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility = Versions.java
40 | targetCompatibility = Versions.java
41 | }
42 |
43 | kotlin {
44 | explicitApi()
45 | }
46 |
47 | kotlinOptions {
48 | jvmTarget = Versions.java.toString()
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}")
54 |
55 | implementation(project(":databinding"))
56 |
57 | testAnnotationProcessor("androidx.databinding:databinding-compiler:${Versions.gradleTools}")
58 | kaptTest("androidx.databinding:databinding-compiler:${Versions.gradleTools}")
59 |
60 | implementation("io.reactivex.rxjava3:rxkotlin:3.0.0")
61 |
62 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0")
63 | androidTestImplementation("androidx.test:runner:1.4.0")
64 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
65 | }
66 |
67 | val sourcesJar = task("sourcesJar") {
68 | archiveClassifier.set("sources")
69 | from(android.sourceSets["main"].java.srcDirs)
70 | }
71 |
72 | signing {
73 | sign(publishing.publications)
74 | }
75 |
76 | afterEvaluate {
77 | publishing {
78 | repositories {
79 | mavenCentralUpload(project)
80 | }
81 | publications {
82 | create(Publication.RX_JAVA_3, this@afterEvaluate)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/MaybeBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.rxjava3.core.Maybe
10 | import io.reactivex.rxjava3.kotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Maybe] or `defaultValue` if no value has been emitted.
15 | */
16 | class MaybeBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | maybe: Maybe,
21 | onError: (Throwable) -> Unit,
22 | onComplete: () -> Unit,
23 | distinct: Boolean,
24 | afterSet: AfterSet?,
25 | beforeSet: BeforeSet?,
26 | validate: Validate?
27 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
28 | init {
29 | viewModel.compositeDisposable += maybe.subscribe({ value = it }, onError, onComplete)
30 | }
31 |
32 | /**
33 | * Property delegate provider for [MaybeBindableProperty].
34 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
35 | *
36 | * @see MaybeBindableProperty
37 | */
38 | class Provider internal constructor(
39 | private val defaultValue: T,
40 | private val maybe: Maybe,
41 | private val onError: (Throwable) -> Unit,
42 | private val onComplete: () -> Unit
43 | ) : BindablePropertyBase.Provider() {
44 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = MaybeBindableProperty(
45 | viewModel = thisRef,
46 | fieldId = property.resolveFieldId(),
47 | defaultValue = defaultValue,
48 | maybe = maybe,
49 | onError = onError,
50 | onComplete = onComplete,
51 | distinct = distinct,
52 | afterSet = afterSet,
53 | beforeSet = beforeSet,
54 | validate = validate
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/FlowableBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.Flowable
10 | import io.reactivex.rxkotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Flowable] or `defaultValue` if no value has been emitted.
15 | */
16 | class FlowableBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | flowable: Flowable,
21 | onError: (Throwable) -> Unit,
22 | onComplete: () -> Unit,
23 | distinct: Boolean,
24 | afterSet: AfterSet?,
25 | beforeSet: BeforeSet?,
26 | validate: Validate?
27 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
28 | init {
29 | viewModel.compositeDisposable += flowable.subscribe({ value = it }, onError, onComplete)
30 | }
31 |
32 | /**
33 | * Property delegate provider for [FlowableBindableProperty].
34 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
35 | *
36 | * @see FlowableBindableProperty
37 | */
38 | class Provider internal constructor(
39 | private val defaultValue: T,
40 | private val flowable: Flowable,
41 | private val onError: (Throwable) -> Unit,
42 | private val onComplete: () -> Unit
43 | ) : BindablePropertyBase.Provider() {
44 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = FlowableBindableProperty(
45 | viewModel = thisRef,
46 | fieldId = property.resolveFieldId(),
47 | defaultValue = defaultValue,
48 | flowable = flowable,
49 | onError = onError,
50 | onComplete = onComplete,
51 | distinct = distinct,
52 | afterSet = afterSet,
53 | beforeSet = beforeSet,
54 | validate = validate
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/FlowableBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.rxjava3.core.Flowable
10 | import io.reactivex.rxjava3.kotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Flowable] or `defaultValue` if no value has been emitted.
15 | */
16 | class FlowableBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | flowable: Flowable,
21 | onError: (Throwable) -> Unit,
22 | onComplete: () -> Unit,
23 | distinct: Boolean,
24 | afterSet: AfterSet?,
25 | beforeSet: BeforeSet?,
26 | validate: Validate?
27 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
28 | init {
29 | viewModel.compositeDisposable += flowable.subscribe({ value = it }, onError, onComplete)
30 | }
31 |
32 | /**
33 | * Property delegate provider for [FlowableBindableProperty].
34 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
35 | *
36 | * @see FlowableBindableProperty
37 | */
38 | class Provider internal constructor(
39 | private val defaultValue: T,
40 | private val flowable: Flowable,
41 | private val onError: (Throwable) -> Unit,
42 | private val onComplete: () -> Unit
43 | ) : BindablePropertyBase.Provider() {
44 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = FlowableBindableProperty(
45 | viewModel = thisRef,
46 | fieldId = property.resolveFieldId(),
47 | defaultValue = defaultValue,
48 | flowable = flowable,
49 | onError = onError,
50 | onComplete = onComplete,
51 | distinct = distinct,
52 | afterSet = afterSet,
53 | beforeSet = beforeSet,
54 | validate = validate
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/ObservableBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava2
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.Observable
10 | import io.reactivex.rxkotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Observable] or `defaultValue` if no value has been emitted.
15 | */
16 | class ObservableBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | observable: Observable,
21 | onError: (Throwable) -> Unit,
22 | onComplete: () -> Unit,
23 | distinct: Boolean,
24 | afterSet: AfterSet?,
25 | beforeSet: BeforeSet?,
26 | validate: Validate?
27 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
28 | init {
29 | viewModel.compositeDisposable += observable.subscribe({ value = it }, onError, onComplete)
30 | }
31 |
32 | /**
33 | * Property delegate provider for [ObservableBindableProperty].
34 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
35 | *
36 | * @see ObservableBindableProperty
37 | */
38 | class Provider internal constructor(
39 | private val defaultValue: T,
40 | private val observable: Observable,
41 | private val onError: (Throwable) -> Unit,
42 | private val onComplete: () -> Unit
43 | ) : BindablePropertyBase.Provider() {
44 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = ObservableBindableProperty(
45 | viewModel = thisRef,
46 | fieldId = property.resolveFieldId(),
47 | defaultValue = defaultValue,
48 | observable = observable,
49 | onError = onError,
50 | onComplete = onComplete,
51 | distinct = distinct,
52 | afterSet = afterSet,
53 | beforeSet = beforeSet,
54 | validate = validate
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/databinding/src/test/java/de/trbnb/mvvmbase/databinding/test/bindableproperty/BindablePropertySavedStateHandleTests.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.test.bindableproperty
2 |
3 | import android.util.Size
4 | import android.util.SizeF
5 | import androidx.databinding.Bindable
6 | import androidx.lifecycle.SavedStateHandle
7 | import de.trbnb.mvvmbase.MvvmBase
8 | import de.trbnb.mvvmbase.databinding.bindableproperty.bindable
9 | import de.trbnb.mvvmbase.databinding.bindableproperty.bindableBoolean
10 | import de.trbnb.mvvmbase.databinding.savedstate.BaseStateSavingViewModel
11 | import de.trbnb.mvvmbase.utils.savingStateInBindableSupports
12 | import org.junit.jupiter.api.BeforeEach
13 | import org.junit.jupiter.api.Test
14 |
15 | class BindablePropertySavedStateHandleTests {
16 | @BeforeEach
17 | fun setup() {
18 | MvvmBase.disableViewModelLifecycleThreadConstraints()
19 | }
20 |
21 | class TestViewModel(savedStateHandle: SavedStateHandle = SavedStateHandle()) : BaseStateSavingViewModel(savedStateHandle) {
22 | @get:Bindable
23 | var text: String by bindable("foo")
24 | @get:Bindable
25 | var userSetting: Boolean by bindable(false)
26 | @get:Bindable
27 | var nullableBoolean: Boolean? by bindable()
28 | @get:Bindable
29 | var isDone: Boolean by bindableBoolean()
30 | @get:Bindable
31 | var isDoneTwo: Boolean by bindable(false)
32 | var property: String? by bindable()
33 | }
34 |
35 | @Test
36 | fun `saved state integration in bindable properties`() {
37 | val handle = SavedStateHandle().apply {
38 | set("text", "Meh")
39 | set("isDone", true)
40 | }
41 |
42 | val viewModel = TestViewModel(handle)
43 | assert(viewModel.text == "Meh")
44 | assert(viewModel.userSetting.not())
45 | assert(viewModel.nullableBoolean == null)
46 | assert(viewModel.isDone)
47 | assert(!viewModel.isDoneTwo)
48 | }
49 |
50 | @Test
51 | fun `supported state saving types`() {
52 | assert(savingStateInBindableSupports())
53 | assert(savingStateInBindableSupports())
54 | assert(savingStateInBindableSupports>())
55 | assert(!savingStateInBindableSupports>())
56 | assert(savingStateInBindableSupports>())
57 |
58 | assert(savingStateInBindableSupports())
59 | assert(savingStateInBindableSupports())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/ObservableBindableProperty.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.rxjava3
2 |
3 | import de.trbnb.mvvmbase.databinding.ViewModel
4 | import de.trbnb.mvvmbase.databinding.bindableproperty.AfterSet
5 | import de.trbnb.mvvmbase.databinding.bindableproperty.BeforeSet
6 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase
7 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate
8 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId
9 | import io.reactivex.rxjava3.core.Observable
10 | import io.reactivex.rxjava3.kotlin.plusAssign
11 | import kotlin.reflect.KProperty
12 |
13 | /**
14 | * Read-only bindable property delegate that has last emitted value from a [Observable] or `defaultValue` if no value has been emitted.
15 | */
16 | class ObservableBindableProperty private constructor(
17 | viewModel: ViewModel,
18 | defaultValue: T,
19 | fieldId: Int,
20 | observable: Observable,
21 | onError: (Throwable) -> Unit,
22 | onComplete: () -> Unit,
23 | distinct: Boolean,
24 | afterSet: AfterSet?,
25 | beforeSet: BeforeSet?,
26 | validate: Validate?
27 | ) : RxBindablePropertyBase(viewModel, defaultValue, fieldId, distinct, afterSet, beforeSet, validate) {
28 | init {
29 | viewModel.compositeDisposable += observable.subscribe({ value = it }, onError, onComplete)
30 | }
31 |
32 | /**
33 | * Property delegate provider for [ObservableBindableProperty].
34 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization.
35 | *
36 | * @see ObservableBindableProperty
37 | */
38 | class Provider internal constructor(
39 | private val defaultValue: T,
40 | private val observable: Observable,
41 | private val onError: (Throwable) -> Unit,
42 | private val onComplete: () -> Unit
43 | ) : BindablePropertyBase.Provider() {
44 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = ObservableBindableProperty(
45 | viewModel = thisRef,
46 | fieldId = property.resolveFieldId(),
47 | defaultValue = defaultValue,
48 | observable = observable,
49 | onError = onError,
50 | onComplete = onComplete,
51 | distinct = distinct,
52 | afterSet = afterSet,
53 | beforeSet = beforeSet,
54 | validate = validate
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/commands/Command.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.commands
2 |
3 | import androidx.databinding.Bindable
4 | import androidx.databinding.Observable
5 |
6 | typealias EnabledListener = (enabled: Boolean) -> Unit
7 |
8 | /**
9 | * The basic contract for command implementations.
10 | *
11 | * @param P The parameter type for invocation. An instance of this has to be used to call [invoke]. [Unit] may be used if no parameter is neccessary.
12 | * @param R The return type for invocation. An instance of this has to be returned from [invoke].
13 | */
14 | interface Command : Observable {
15 | /**
16 | * Determines whether this Command is enabled or not.
17 | *
18 | * @return Returns `true` if this Command is enabled, otherwise `false`.
19 | */
20 | @get:Bindable
21 | val isEnabled: Boolean
22 |
23 | /**
24 | * Invokes the Command.
25 | *
26 | * @throws de.trbnb.mvvmbase.commands.DisabledCommandInvocationException If [isEnabled] returns `false`.
27 | * @return A return type instance.
28 | */
29 | operator fun invoke(param: P): R
30 |
31 | /**
32 | * Invokes the Command only if [isEnabled] equals `true`.
33 | *
34 | * @return A return type instance if [isEnabled] equals `true` before invocation, otherwise `null`.
35 | */
36 | fun invokeSafely(param: P): R? {
37 | return if (isEnabled) invoke(param) else null
38 | }
39 |
40 | /**
41 | * Adds a listener that is notified when the value of [isEnabled] might have changed.
42 | */
43 | fun addEnabledListener(listener: EnabledListener)
44 |
45 | /**
46 | * Adds a listener for a view component.
47 | * These will be removed via [clearEnabledListenersForViews] and [observeLifecycle] .
48 | */
49 | fun addEnabledListenerForView(listener: EnabledListener)
50 |
51 | /**
52 | * Removes a listener that is used for listening to changes to [isEnabled].
53 | * A listener that is passed to this method will not be notified anymore.
54 | */
55 | fun removeEnabledListener(listener: EnabledListener)
56 |
57 | /**
58 | * Removes all listeners that are used for listening to changes to [isEnabled].
59 | * No previously added listeners will be notified anymore.
60 | */
61 | fun clearEnabledListeners()
62 |
63 | /**
64 | * Removes all listeners that were added via [addEnabledListenerForView].
65 | */
66 | fun clearEnabledListenersForViews()
67 | }
68 |
--------------------------------------------------------------------------------
/coroutines/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | `maven-publish`
6 | signing
7 | }
8 |
9 | android {
10 | compileSdk = Android.compileSdk
11 |
12 | defaultConfig {
13 | minSdk = Android.minSdk
14 | targetSdk = Android.compileSdk
15 |
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | named("release").configure {
21 | isMinifyEnabled = false
22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
23 | }
24 | }
25 |
26 | buildFeatures {
27 | dataBinding = true
28 | }
29 |
30 | dataBinding {
31 | // This is necessary to allow the data binding annotation processor to generate
32 | // the BR fields from Bindable annotations
33 | testOptions.unitTests.isIncludeAndroidResources = true
34 |
35 | isEnabledForTests = true
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility = Versions.java
40 | targetCompatibility = Versions.java
41 | }
42 |
43 | kotlin {
44 | explicitApi()
45 | }
46 |
47 | kotlinOptions {
48 | jvmTarget = Versions.java.toString()
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}")
54 |
55 | implementation(project(":databinding"))
56 |
57 | testAnnotationProcessor("androidx.databinding:databinding-compiler:${Versions.gradleTools}")
58 | kaptTest("androidx.databinding:databinding-compiler:${Versions.gradleTools}")
59 |
60 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
61 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
62 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
63 |
64 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0")
65 | androidTestImplementation("androidx.test:runner:1.4.0")
66 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
67 | }
68 |
69 | val sourcesJar = task("sourcesJar") {
70 | archiveClassifier.set("sources")
71 | from(android.sourceSets["main"].java.srcDirs)
72 | }
73 |
74 | signing {
75 | sign(publishing.publications)
76 | }
77 |
78 | afterEvaluate {
79 | publishing {
80 | repositories {
81 | mavenCentralUpload(project)
82 | }
83 | publications {
84 | create(Publication.COROUTINES, this@afterEvaluate)
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/core/src/main/java/de/trbnb/mvvmbase/commands/RuleCommand.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.commands
2 |
3 | import de.trbnb.mvvmbase.ViewModel
4 | import kotlin.reflect.KProperty
5 |
6 | /**
7 | * A [Command] that determines if it is enabled via a predicate.
8 | * This predicate, or "rule", is set during initialization.
9 |
10 | * The predicates result will be cached. A refresh can be triggered by calling [onEnabledChanged].
11 | *
12 | * @param action The initial action that will be run when the Command is executed.
13 | * @param enabledRule The initial rule that determines if this Command is enabled.
14 | */
15 | class RuleCommand internal constructor(
16 | action: (P) -> R,
17 | private val enabledRule: () -> Boolean
18 | ) : BaseCommandImpl(action) {
19 | override var isEnabled: Boolean = enabledRule()
20 | private set(value) {
21 | if (field == value) return
22 |
23 | field = value
24 | triggerEnabledChangedListener()
25 | }
26 |
27 | /**
28 | * This method has to be called when the result of the rule might have changed.
29 | */
30 | fun onEnabledChanged() {
31 | isEnabled = enabledRule()
32 | }
33 | }
34 |
35 | /**
36 | * Helper function to create a [RuleCommand].
37 | */
38 | @JvmName("parameterizedRuleCommand0")
39 | fun
ViewModel.ruleCommand(
40 | action: (P) -> R,
41 | enabledRule: () -> Boolean,
42 | dependencyPropertyNames: List? = null
43 | ): RuleCommand = RuleCommand(action, enabledRule).apply {
44 | dependsOn(this@ruleCommand, dependencyPropertyNames)
45 | }
46 |
47 | /**
48 | * Helper function to create a [RuleCommand].
49 | */
50 | @JvmName("parameterizedRuleCommand1")
51 | fun
ViewModel.ruleCommand(
52 | action: (P) -> R,
53 | enabledRule: () -> Boolean,
54 | dependencyProperties: List>
55 | ): RuleCommand = ruleCommand(action, enabledRule, dependencyProperties.map { it.name })
56 |
57 | /**
58 | * Helper function to create a parameter-less [RuleCommand].
59 | */
60 | @JvmName("parameterizedRuleCommand2")
61 | fun ViewModel.ruleCommand(
62 | action: (Unit) -> R,
63 | enabledRule: () -> Boolean,
64 | dependencyPropertyNames: List? = null
65 | ): RuleCommand = ruleCommand(action, enabledRule, dependencyPropertyNames)
66 |
67 | /**
68 | * Helper function to create a parameter-less [RuleCommand].
69 | */
70 | fun ViewModel.ruleCommand(
71 | action: (Unit) -> R,
72 | enabledRule: () -> Boolean,
73 | dependencyProperties: List>
74 | ): RuleCommand = ruleCommand(action, enabledRule, dependencyProperties)
75 |
--------------------------------------------------------------------------------
/rxjava2/src/test/java/de/trbnb/mvvmbase/rxjava2/test/CompletableBindingTests.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNUSED_VARIABLE", "unused")
2 |
3 | package de.trbnb.mvvmbase.rxjava2.test
4 |
5 | import androidx.databinding.Bindable
6 | import de.trbnb.mvvmbase.MvvmBase
7 | import de.trbnb.mvvmbase.databinding.BaseViewModel
8 | import de.trbnb.mvvmbase.databinding.initDataBinding
9 | import de.trbnb.mvvmbase.rxjava2.RxViewModel
10 | import io.reactivex.Completable
11 | import io.reactivex.subjects.CompletableSubject
12 | import org.junit.jupiter.api.BeforeAll
13 | import org.junit.jupiter.api.Test
14 |
15 | class CompletableBindingTests {
16 | companion object {
17 | @BeforeAll
18 | @JvmStatic
19 | fun setup() {
20 | MvvmBase.initDataBinding().disableViewModelLifecycleThreadConstraints()
21 | }
22 | }
23 |
24 | @Test
25 | fun `is false the default default value`() {
26 | val completable = CompletableSubject.create()
27 |
28 | val viewModel = object : BaseViewModel(), RxViewModel {
29 | val property by completable.toBindable()
30 | }
31 |
32 | assert(!viewModel.property)
33 | }
34 |
35 | @Test
36 | fun `is onError called`() {
37 | val completable = CompletableSubject.create()
38 | var isOnErrorCalled = false
39 | val onError = { _: Throwable -> isOnErrorCalled = true }
40 |
41 | val viewModel = object : BaseViewModel(), RxViewModel {
42 | val property by completable.toBindable(onError = onError)
43 | }
44 |
45 | completable.onError(RuntimeException())
46 | assert(isOnErrorCalled)
47 | }
48 |
49 | @Test
50 | fun `is onComplete called`() {
51 | val completable = CompletableSubject.create()
52 |
53 | val viewModel = object : BaseViewModel(), RxViewModel {
54 | val property by completable.toBindable()
55 | }
56 |
57 | completable.onComplete()
58 | assert(viewModel.property)
59 | }
60 |
61 | @Test
62 | fun `is notifyPropertyChanged() called (automatic field ID)`() {
63 | val completable = CompletableSubject.create()
64 |
65 | val viewModel = ViewModelWithBindable(completable)
66 |
67 | val propertyChangedCallback = TestPropertyChangedCallback()
68 | viewModel.addOnPropertyChangedCallback(propertyChangedCallback)
69 |
70 | completable.onComplete()
71 | assert(BR.property in propertyChangedCallback.changedPropertyIds)
72 | }
73 |
74 | class ViewModelWithBindable(completable: Completable) : BaseViewModel(), RxViewModel {
75 | @get:Bindable
76 | val property by completable.toBindable()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/rxjava3/src/test/java/de/trbnb/mvvmbase/rxjava3/test/CompletableBindingTests.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNUSED_VARIABLE", "unused")
2 |
3 | package de.trbnb.mvvmbase.rxjava3.test
4 |
5 | import androidx.databinding.Bindable
6 | import de.trbnb.mvvmbase.MvvmBase
7 | import de.trbnb.mvvmbase.databinding.BaseViewModel
8 | import de.trbnb.mvvmbase.databinding.initDataBinding
9 | import de.trbnb.mvvmbase.rxjava3.RxViewModel
10 | import io.reactivex.rxjava3.core.Completable
11 | import io.reactivex.rxjava3.subjects.CompletableSubject
12 | import org.junit.jupiter.api.BeforeAll
13 | import org.junit.jupiter.api.Test
14 |
15 | class CompletableBindingTests {
16 | companion object {
17 | @BeforeAll
18 | @JvmStatic
19 | fun setup() {
20 | MvvmBase.initDataBinding().disableViewModelLifecycleThreadConstraints()
21 | }
22 | }
23 |
24 | @Test
25 | fun `is false the default default value`() {
26 | val completable = CompletableSubject.create()
27 |
28 | val viewModel = object : BaseViewModel(), RxViewModel {
29 | val property by completable.toBindable()
30 | }
31 |
32 | assert(!viewModel.property)
33 | }
34 |
35 | @Test
36 | fun `is onError called`() {
37 | val completable = CompletableSubject.create()
38 | var isOnErrorCalled = false
39 | val onError = { _: Throwable -> isOnErrorCalled = true }
40 |
41 | val viewModel = object : BaseViewModel(), RxViewModel {
42 | val property by completable.toBindable(onError = onError)
43 | }
44 |
45 | completable.onError(RuntimeException())
46 | assert(isOnErrorCalled)
47 | }
48 |
49 | @Test
50 | fun `is onComplete called`() {
51 | val completable = CompletableSubject.create()
52 |
53 | val viewModel = object : BaseViewModel(), RxViewModel {
54 | val property by completable.toBindable()
55 | }
56 |
57 | completable.onComplete()
58 | assert(viewModel.property)
59 | }
60 |
61 | @Test
62 | fun `is notifyPropertyChanged() called (automatic field ID)`() {
63 | val completable = CompletableSubject.create()
64 |
65 | val viewModel = ViewModelWithBindable(completable)
66 |
67 | val propertyChangedCallback = TestPropertyChangedCallback()
68 | viewModel.addOnPropertyChangedCallback(propertyChangedCallback)
69 |
70 | completable.onComplete()
71 | assert(BR.property in propertyChangedCallback.changedPropertyIds)
72 | }
73 |
74 | class ViewModelWithBindable(completable: Completable) : BaseViewModel(), RxViewModel {
75 | @get:Bindable
76 | val property by completable.toBindable()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/databinding/src/main/java/de/trbnb/mvvmbase/databinding/viewmodel/ViewModelProviderFactoryHelpers.kt:
--------------------------------------------------------------------------------
1 | package de.trbnb.mvvmbase.databinding.viewmodel
2 |
3 | import androidx.lifecycle.AbstractSavedStateViewModelFactory
4 | import androidx.lifecycle.SavedStateHandle
5 | import androidx.lifecycle.ViewModelProvider
6 | import de.trbnb.mvvmbase.databinding.MvvmBindingActivity
7 | import de.trbnb.mvvmbase.databinding.MvvmBindingFragment
8 | import de.trbnb.mvvmbase.databinding.ViewModel
9 | import javax.inject.Provider
10 |
11 | /**
12 | * Convenience function to create a [ViewModelProvider.Factory] for an [MvvmBindingFragment].
13 | * Can be useful for overriding [MvvmBindingActivity.getDefaultViewModelProviderFactory].
14 | */
15 | fun MvvmBindingFragment.viewModelProviderFactory(factory: (handle: SavedStateHandle) -> VM): ViewModelProvider.Factory
16 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel = object : AbstractSavedStateViewModelFactory(this, arguments) {
17 | @Suppress("UNCHECKED_CAST")
18 | override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T {
19 | return factory(handle) as T
20 | }
21 | }
22 |
23 | /**
24 | * Convenience function to create a [ViewModelProvider.Factory] for an [MvvmBindingActivity].
25 | * Can be useful for overriding [MvvmBindingActivity.getDefaultViewModelProviderFactory].
26 | */
27 | fun MvvmBindingActivity.viewModelProviderFactory(factory: (handle: SavedStateHandle) -> VM): ViewModelProvider.Factory
28 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel = object : AbstractSavedStateViewModelFactory(this, intent?.extras) {
29 | @Suppress("UNCHECKED_CAST")
30 | override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T {
31 | return factory(handle) as T
32 | }
33 | }
34 |
35 | /**
36 | * Convenience function to create a [ViewModelProvider.Factory].
37 | */
38 | fun viewModelProviderFactory(factory: () -> VM): ViewModelProvider.Factory
39 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel = object : ViewModelProvider.Factory {
40 | @Suppress("UNCHECKED_CAST")
41 | override fun