├── core ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ ├── de │ │ │ │ └── trbnb │ │ │ │ │ └── mvvmbase │ │ │ │ │ ├── events │ │ │ │ │ ├── Event.kt │ │ │ │ │ ├── EventChannelOwner.kt │ │ │ │ │ ├── EventChannelImpl.kt │ │ │ │ │ ├── ComposeUtils.kt │ │ │ │ │ └── EventChannel.kt │ │ │ │ │ ├── utils │ │ │ │ │ ├── ListUtils.kt │ │ │ │ │ ├── LifecycleUtils.kt │ │ │ │ │ ├── ReflectionUtils.kt │ │ │ │ │ └── SavedStateUtils.kt │ │ │ │ │ ├── commands │ │ │ │ │ ├── DisabledCommandInvocationException.kt │ │ │ │ │ ├── CommandUtils.kt │ │ │ │ │ ├── SimpleCommand.kt │ │ │ │ │ ├── Command.kt │ │ │ │ │ ├── BaseCommandImpl.kt │ │ │ │ │ └── RuleCommand.kt │ │ │ │ │ ├── DependsOn.kt │ │ │ │ │ ├── savedstate │ │ │ │ │ ├── BaseStateSavingViewModel.kt │ │ │ │ │ └── StateSavingViewModel.kt │ │ │ │ │ ├── OnPropertyChangedCallback.kt │ │ │ │ │ ├── compose │ │ │ │ │ ├── PropertyMutableState.kt │ │ │ │ │ └── ComposeUtils.kt │ │ │ │ │ ├── observable │ │ │ │ │ ├── ObservableUtils.kt │ │ │ │ │ ├── PropertyChangeRegistry.kt │ │ │ │ │ └── ObservableContainer.kt │ │ │ │ │ ├── observableproperty │ │ │ │ │ └── StateSaveOption.kt │ │ │ │ │ ├── MvvmBase.kt │ │ │ │ │ ├── ViewModelLifecycleOwner.kt │ │ │ │ │ ├── BaseViewModel.kt │ │ │ │ │ └── ViewModel.kt │ │ │ └── androidx │ │ │ │ └── lifecycle │ │ │ │ └── ViewModelUtils.kt │ │ └── res │ │ │ └── layout │ │ │ └── databinding_empty.xml │ └── test │ │ └── java │ │ └── de │ │ └── trbnb │ │ └── mvvmbase │ │ └── test │ │ ├── TestPropertyChangedCallback.kt │ │ ├── utils │ │ └── ReflectionUtilsTests.kt │ │ ├── events │ │ └── EventChannelImplTests.kt │ │ ├── ViewModelLifecycleTests.kt │ │ ├── bindableproperty │ │ └── BindablePropertySavedStateHandleTests.kt │ │ ├── NestedViewModelTests.kt │ │ ├── BaseViewModelTests.kt │ │ └── commands │ │ ├── CommandTests.kt │ │ └── SimpleCommandTests.kt ├── proguard-rules.pro └── build.gradle.kts ├── sample ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-w820dp │ │ │ └── dimens.xml │ │ ├── java │ │ └── de │ │ │ └── trbnb │ │ │ └── mvvmbase │ │ │ └── sample │ │ │ ├── list │ │ │ ├── Item.kt │ │ │ ├── ListViewModel.kt │ │ │ └── ListScreen.kt │ │ │ ├── app │ │ │ ├── App.kt │ │ │ ├── resource │ │ │ │ ├── ResourceProvider.kt │ │ │ │ └── ResourceProviderImpl.kt │ │ │ ├── Activity.kt │ │ │ ├── AppModule.kt │ │ │ ├── Theme.kt │ │ │ └── Navigation.kt │ │ │ ├── main │ │ │ ├── MainEvent.kt │ │ │ └── MainViewModel.kt │ │ │ └── second │ │ │ ├── SecondViewModel.kt │ │ │ └── SecondScreen.kt │ │ └── AndroidManifest.xml └── proguard-rules.pro ├── conductor ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── de │ │ └── trbnb │ │ └── mvvmbase │ │ └── conductor │ │ ├── MvvmController.kt │ │ ├── ConductorViewModelLazy.kt │ │ └── ViewModelProviderFactoryHelpers.kt ├── proguard-rules.pro └── build.gradle.kts ├── coroutines ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── trbnb │ │ │ └── mvvmbase │ │ │ └── coroutines │ │ │ ├── flow │ │ │ ├── Types.kt │ │ │ └── FlowBindable.kt │ │ │ └── CoroutineViewModel.kt │ └── test │ │ └── java │ │ └── de │ │ └── trbnb │ │ └── mvvmbase │ │ └── coroutines │ │ └── test │ │ ├── TestPropertyChangedCallback.kt │ │ └── CoroutinesViewModelTests.kt ├── proguard-rules.pro └── build.gradle.kts ├── databinding ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ ├── de │ │ │ │ └── trbnb │ │ │ │ │ └── mvvmbase │ │ │ │ │ └── databinding │ │ │ │ │ ├── utils │ │ │ │ │ ├── ListUtils.kt │ │ │ │ │ ├── ObservableUtils.kt │ │ │ │ │ └── ComposeUtils.kt │ │ │ │ │ ├── commands │ │ │ │ │ ├── DisabledCommandInvocationException.kt │ │ │ │ │ ├── SimpleCommand.kt │ │ │ │ │ ├── BaseCommandImpl.kt │ │ │ │ │ ├── CommandUtils.kt │ │ │ │ │ ├── Command.kt │ │ │ │ │ └── RuleCommand.kt │ │ │ │ │ ├── bindings │ │ │ │ │ ├── ViewBindings.kt │ │ │ │ │ └── CommandBindings.kt │ │ │ │ │ ├── savedstate │ │ │ │ │ └── BaseStateSavingViewModel.kt │ │ │ │ │ ├── bindableproperty │ │ │ │ │ └── ChildViewModelBindablePropertyProvider.kt │ │ │ │ │ ├── MvvmBaseDataBinding.kt │ │ │ │ │ ├── Typealiases.kt │ │ │ │ │ ├── recyclerview │ │ │ │ │ ├── BindingViewHolder.kt │ │ │ │ │ └── BindingListAdapter.kt │ │ │ │ │ ├── DataBindingViewModelLifecycleOwner.kt │ │ │ │ │ ├── viewmodel │ │ │ │ │ └── ViewModelProviderFactoryHelpers.kt │ │ │ │ │ ├── MvvmView.kt │ │ │ │ │ └── MvvmBindingActivity.kt │ │ │ └── androidx │ │ │ │ └── lifecycle │ │ │ │ └── DataBindingViewModelUtils.kt │ │ └── res │ │ │ └── layout │ │ │ └── databinding_empty.xml │ └── test │ │ └── java │ │ └── de │ │ └── trbnb │ │ └── mvvmbase │ │ └── databinding │ │ └── test │ │ ├── TestPropertyChangedCallback.kt │ │ ├── ObservableTests.kt │ │ ├── NestedViewModelTests.kt │ │ ├── bindableproperty │ │ └── BindablePropertySavedStateHandleTests.kt │ │ ├── commands │ │ └── CommandTests.kt │ │ └── ReflectionUtilsTests.kt ├── proguard-rules.pro └── build.gradle.kts ├── rxjava2 ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── trbnb │ │ │ └── mvvmbase │ │ │ └── rxjava2 │ │ │ ├── DisposableUtils.kt │ │ │ ├── ViewModelDisposable.kt │ │ │ ├── RxBindablePropertyBase.kt │ │ │ ├── CompletableBindableProperty.kt │ │ │ ├── SingleBindableProperty.kt │ │ │ ├── MaybeBindableProperty.kt │ │ │ ├── FlowableBindableProperty.kt │ │ │ └── ObservableBindableProperty.kt │ └── test │ │ └── java │ │ └── de │ │ └── trbnb │ │ └── mvvmbase │ │ └── rxjava2 │ │ └── test │ │ ├── TestPropertyChangedCallback.kt │ │ ├── DisposableTests.kt │ │ ├── CompletableBindingTests.kt │ │ ├── SingleBindingTests.kt │ │ └── MaybeBindingTests.kt ├── proguard-rules.pro └── build.gradle.kts ├── rxjava3 ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── trbnb │ │ │ └── mvvmbase │ │ │ └── rxjava3 │ │ │ ├── DisposableUtils.kt │ │ │ ├── ViewModelDisposable.kt │ │ │ ├── RxBindablePropertyBase.kt │ │ │ ├── CompletableBindableProperty.kt │ │ │ ├── SingleBindableProperty.kt │ │ │ ├── MaybeBindableProperty.kt │ │ │ ├── FlowableBindableProperty.kt │ │ │ └── ObservableBindableProperty.kt │ └── test │ │ └── java │ │ └── de │ │ └── trbnb │ │ └── mvvmbase │ │ └── rxjava3 │ │ └── test │ │ ├── TestPropertyChangedCallback.kt │ │ ├── DisposableTests.kt │ │ ├── CompletableBindingTests.kt │ │ ├── SingleBindingTests.kt │ │ └── MaybeBindingTests.kt ├── proguard-rules.pro └── build.gradle.kts ├── .editorconfig ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle.kts ├── gradle.properties └── gradlew.bat /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /conductor/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /databinding/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /rxjava2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /rxjava3/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | max_line_length=150 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbnb/MvvmBase/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbnb/MvvmBase/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbnb/MvvmBase/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbnb/MvvmBase/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbnb/MvvmBase/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbnb/MvvmBase/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /rxjava2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /rxjava3/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /conductor/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /coroutines/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | /.idea 10 | signing.gpg -------------------------------------------------------------------------------- /databinding/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/list/Item.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.list 2 | 3 | import java.util.UUID 4 | 5 | class Item(val id: UUID = UUID.randomUUID(), val text: String) 6 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/App.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() 8 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/events/Event.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.events 2 | 3 | /** 4 | * Base interface for all information that can be sent via [EventChannel]. 5 | * 6 | * @see EventChannel 7 | */ 8 | interface Event 9 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.buildFileName = "build.gradle.kts" 2 | 3 | include( 4 | ":sample", 5 | ":core", 6 | ":rxjava2", 7 | ":rxjava3", 8 | ":coroutines", 9 | ":databinding", 10 | ":conductor" 11 | ) 12 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/main/MainEvent.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.main 2 | 3 | import de.trbnb.mvvmbase.events.Event 4 | 5 | sealed class MainEvent : Event { 6 | class ShowToast(val text: String) : MainEvent() 7 | } 8 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/resource/ResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app.resource 2 | 3 | import androidx.annotation.StringRes 4 | 5 | interface ResourceProvider { 6 | fun getString(@StringRes resId: Int, vararg args: Any): String 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 19 10:51:21 CEST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/utils/ListUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.utils 2 | 3 | import de.trbnb.mvvmbase.ViewModel 4 | 5 | /** 6 | * Calls [ViewModel.destroy] on every element in the receiver collection. 7 | */ 8 | fun Collection.destroyAll() = forEach { it.destroy() } 9 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Compose MvvmBase Sample 3 | 4 | Hello Compose! 5 | 6 | Not restored. 7 | Restored. 8 | 9 | -------------------------------------------------------------------------------- /coroutines/src/main/java/de/trbnb/mvvmbase/coroutines/flow/Types.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.coroutines.flow 2 | 3 | import kotlinx.coroutines.flow.FlowCollector 4 | 5 | typealias OnException = suspend FlowCollector.(Throwable) -> Unit 6 | typealias OnCompletion = suspend FlowCollector.(Throwable?) -> Unit 7 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/commands/DisabledCommandInvocationException.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.commands 2 | 3 | /** 4 | * Exception that will only be thrown if [Command.invoke] has been called even though 5 | * [Command.isEnabled] was `false` at the same time. 6 | */ 7 | class DisabledCommandInvocationException : RuntimeException() 8 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/utils/ListUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.utils 2 | 3 | import de.trbnb.mvvmbase.databinding.ViewModel 4 | 5 | /** 6 | * Calls [ViewModel.destroy] on every element in the receiver collection. 7 | */ 8 | fun Collection.destroyAll() = forEach { it.destroy() } 9 | -------------------------------------------------------------------------------- /core/src/main/java/androidx/lifecycle/ViewModelUtils.kt: -------------------------------------------------------------------------------- 1 | package androidx.lifecycle 2 | 3 | internal fun ViewModel.getTagFromViewModel(key: String): T? = getTag(key) 4 | internal fun ViewModel.setTagIfAbsentForViewModel(key: String, newValue: T): T = setTagIfAbsent(key, newValue) 5 | 6 | internal fun ViewModel.destroyInternal() { 7 | clear() 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/events/EventChannelOwner.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.events 2 | 3 | /** 4 | * Marker interface for objects that contain an [EventChannel] 5 | */ 6 | interface EventChannelOwner { 7 | /** 8 | * Gets an EventChannel that can be used for sending one-time events. 9 | */ 10 | val eventChannel: EventChannel 11 | } 12 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/commands/DisabledCommandInvocationException.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.commands 2 | 3 | /** 4 | * Exception that will only be thrown if [Command.invoke] has been called even though 5 | * [Command.isEnabled] was `false` at the same time. 6 | */ 7 | class DisabledCommandInvocationException : RuntimeException() 8 | -------------------------------------------------------------------------------- /databinding/src/main/java/androidx/lifecycle/DataBindingViewModelUtils.kt: -------------------------------------------------------------------------------- 1 | package androidx.lifecycle 2 | 3 | internal fun ViewModel.getTagFromViewModel(key: String): T? = getTag(key) 4 | internal fun ViewModel.setTagIfAbsentForViewModel(key: String, newValue: T): T = setTagIfAbsent(key, newValue) 5 | 6 | internal fun ViewModel.destroyInternal() { 7 | clear() 8 | } 9 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/list/ListViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.list 2 | 3 | import de.trbnb.mvvmbase.BaseViewModel 4 | import de.trbnb.mvvmbase.observableproperty.observable 5 | import java.util.UUID 6 | 7 | class ListViewModel : BaseViewModel() { 8 | val items by observable(List(5) { Item(text = UUID.randomUUID().toString()) }) 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/utils/LifecycleUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.utils 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.LifecycleOwner 5 | 6 | /** 7 | * Returns `true` if [LifecycleOwner.getLifecycle] is in a destroyed state. 8 | */ 9 | val LifecycleOwner.isDestroyed: Boolean 10 | get() = lifecycle.currentState == Lifecycle.State.DESTROYED 11 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/DependsOn.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase 2 | 3 | /** 4 | * Describes an annotated property as observable. 5 | * Its getter may return a different value than before if another property has changed. 6 | * 7 | * @param value names of the dependency properties 8 | */ 9 | @Target(AnnotationTarget.PROPERTY) 10 | @Retention(AnnotationRetention.RUNTIME) 11 | annotation class DependsOn(vararg val value: String) 12 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/bindings/ViewBindings.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.bindings 2 | 3 | import android.view.View 4 | import androidx.databinding.BindingAdapter 5 | 6 | /** 7 | * Maps a visibility-boolean to either [View.VISIBLE] for `true` or [View.GONE] for `false`. 8 | */ 9 | @BindingAdapter("android:visible") 10 | fun View.setVisible(visible: Boolean) { 11 | visibility = if (visible) View.VISIBLE else View.GONE 12 | } 13 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/resource/ResourceProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app.resource 2 | 3 | import android.content.Context 4 | import dagger.hilt.android.qualifiers.ApplicationContext 5 | import javax.inject.Inject 6 | 7 | class ResourceProviderImpl @Inject constructor(@ApplicationContext private val context: Context) : ResourceProvider { 8 | override fun getString(resId: Int, vararg args: Any): String = context.getString(resId, *args) 9 | } 10 | -------------------------------------------------------------------------------- /conductor/src/main/java/de/trbnb/mvvmbase/conductor/MvvmController.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.conductor 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import de.trbnb.mvvmbase.ViewModel 5 | 6 | /** 7 | * Typealias for Controllers that don't need to specify the specific [ViewDataBinding] implementation. 8 | * 9 | * @param[VM] The type of the specific [ViewModel] implementation for this Controller. 10 | */ 11 | typealias MvvmController = MvvmBindingController 12 | -------------------------------------------------------------------------------- /rxjava2/src/test/java/de/trbnb/mvvmbase/rxjava2/test/TestPropertyChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.rxjava2.test 2 | 3 | import androidx.databinding.Observable 4 | 5 | class TestPropertyChangedCallback : Observable.OnPropertyChangedCallback() { 6 | var changedPropertyIds: List = emptyList() 7 | override fun onPropertyChanged(sender: Observable?, propertyId: Int) { 8 | changedPropertyIds = changedPropertyIds.toMutableList().apply { add(propertyId) } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rxjava3/src/test/java/de/trbnb/mvvmbase/rxjava3/test/TestPropertyChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.rxjava3.test 2 | 3 | import androidx.databinding.Observable 4 | 5 | class TestPropertyChangedCallback : Observable.OnPropertyChangedCallback() { 6 | var changedPropertyIds: List = emptyList() 7 | override fun onPropertyChanged(sender: Observable?, propertyId: Int) { 8 | changedPropertyIds = changedPropertyIds.toMutableList().apply { add(propertyId) } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/savedstate/BaseStateSavingViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.savedstate 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import de.trbnb.mvvmbase.BaseViewModel 5 | 6 | /** 7 | * Base implementation for [StateSavingViewModel]. 8 | * Receives the [SavedStateHandle] via construction parameter. 9 | */ 10 | abstract class BaseStateSavingViewModel( 11 | final override val savedStateHandle: SavedStateHandle 12 | ) : BaseViewModel(), StateSavingViewModel 13 | -------------------------------------------------------------------------------- /coroutines/src/test/java/de/trbnb/mvvmbase/coroutines/test/TestPropertyChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.coroutines.test 2 | 3 | import androidx.databinding.Observable 4 | 5 | class TestPropertyChangedCallback : Observable.OnPropertyChangedCallback() { 6 | var changedPropertyIds: List = emptyList() 7 | override fun onPropertyChanged(sender: Observable?, propertyId: Int) { 8 | changedPropertyIds = changedPropertyIds.toMutableList().apply { add(propertyId) } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/Activity.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class Activity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | setContent { Navigation() } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/savedstate/BaseStateSavingViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.savedstate 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import de.trbnb.mvvmbase.databinding.BaseViewModel 5 | import de.trbnb.mvvmbase.savedstate.StateSavingViewModel 6 | 7 | /** 8 | * Base implementation for [StateSavingViewModel]. 9 | * Receives the [SavedStateHandle] via construction parameter. 10 | */ 11 | abstract class BaseStateSavingViewModel( 12 | final override val savedStateHandle: SavedStateHandle 13 | ) : BaseViewModel(), StateSavingViewModel 14 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/AppModule.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import de.trbnb.mvvmbase.sample.app.resource.ResourceProvider 8 | import de.trbnb.mvvmbase.sample.app.resource.ResourceProviderImpl 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | interface AppModule { 14 | @Binds 15 | @Singleton 16 | fun resourceProvider(impl: ResourceProviderImpl): ResourceProvider 17 | } 18 | -------------------------------------------------------------------------------- /databinding/src/test/java/de/trbnb/mvvmbase/databinding/test/TestPropertyChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.test 2 | 3 | import androidx.databinding.Observable 4 | 5 | class TestPropertyChangedCallback : Observable.OnPropertyChangedCallback() { 6 | var changedPropertyIds: List = emptyList() 7 | private set 8 | 9 | override fun onPropertyChanged(sender: Observable?, propertyId: Int) { 10 | changedPropertyIds = changedPropertyIds.toMutableList().apply { add(propertyId) } 11 | } 12 | 13 | fun clear() { 14 | changedPropertyIds = emptyList() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/OnPropertyChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase 2 | 3 | import de.trbnb.mvvmbase.observable.ObservableContainer 4 | 5 | /** 6 | * Defines a simple callback for [ObservableContainer]. 7 | */ 8 | fun interface OnPropertyChangedCallback { 9 | /** 10 | * Called by an ObservableContainer whenever an observable property changes. 11 | * 12 | * @param sender The ObservableContainer that contains the property. 13 | * @param propertyName The name of the changed property. 14 | */ 15 | fun onPropertyChanged(sender: ObservableContainer, propertyName: String) 16 | } 17 | -------------------------------------------------------------------------------- /rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/DisposableUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.rxjava2 2 | 3 | import androidx.lifecycle.Lifecycle.Event.ON_DESTROY 4 | import androidx.lifecycle.LifecycleEventObserver 5 | import androidx.lifecycle.LifecycleOwner 6 | import io.reactivex.disposables.Disposable 7 | 8 | /** 9 | * Disposes a [Disposable] when the given lifecycle has emitted the [ON_DESTROY] event. 10 | */ 11 | fun Disposable.autoDispose(lifecycleOwner: LifecycleOwner) { 12 | lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> 13 | if (event == ON_DESTROY) { 14 | dispose() 15 | } 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/java/de/trbnb/mvvmbase/test/TestPropertyChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.test 2 | 3 | import de.trbnb.mvvmbase.OnPropertyChangedCallback 4 | import de.trbnb.mvvmbase.observable.ObservableContainer 5 | 6 | class TestPropertyChangedCallback : OnPropertyChangedCallback { 7 | var changedPropertyIds: List = emptyList() 8 | private set 9 | 10 | override fun onPropertyChanged(sender: ObservableContainer, propertyName: String) { 11 | changedPropertyIds = changedPropertyIds.toMutableList().apply { add(propertyName) } 12 | } 13 | 14 | fun clear() { 15 | changedPropertyIds = emptyList() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/DisposableUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.rxjava3 2 | 3 | import androidx.lifecycle.Lifecycle.Event.ON_DESTROY 4 | import androidx.lifecycle.LifecycleEventObserver 5 | import androidx.lifecycle.LifecycleOwner 6 | import io.reactivex.rxjava3.disposables.Disposable 7 | 8 | /** 9 | * Disposes a [Disposable] when the given lifecycle has emitted the [ON_DESTROY] event. 10 | */ 11 | fun Disposable.autoDispose(lifecycleOwner: LifecycleOwner) { 12 | lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> 13 | if (event == ON_DESTROY) { 14 | dispose() 15 | } 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/res/layout/databinding_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /databinding/src/main/res/layout/databinding_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/Theme.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | @Composable 10 | fun AppTheme( 11 | darkMode: Boolean = isSystemInDarkTheme(), 12 | content: @Composable () -> Unit 13 | ) { 14 | MaterialTheme( 15 | colors = if (darkMode) darkColors() else lightColors(), 16 | typography = MaterialTheme.typography, 17 | shapes = MaterialTheme.shapes, 18 | content = content 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/compose/PropertyMutableState.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.compose 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import kotlin.reflect.KMutableProperty0 6 | 7 | /** 8 | * Simple implementation of a mutable state derived from an observable property. 9 | */ 10 | class PropertyMutableState( 11 | private val state: State, 12 | private val property: KMutableProperty0 13 | ) : MutableState { 14 | override var value: T 15 | get() = state.value 16 | set(value) = property.set(value) 17 | 18 | override fun component1(): T = value 19 | override fun component2(): (T) -> Unit = { value = it } 20 | } 21 | -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Users\Thorben\Documents\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keep @interface kotlin.Metadata { *; } -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/app/Navigation.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.app 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.compose.NavHost 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.compose.rememberNavController 7 | import de.trbnb.mvvmbase.sample.list.ListScreen 8 | import de.trbnb.mvvmbase.sample.main.MainScreen 9 | import de.trbnb.mvvmbase.sample.second.SecondScreen 10 | 11 | @Composable 12 | fun Navigation() { 13 | val navController = rememberNavController() 14 | NavHost(navController, startDestination = "main") { 15 | composable("main") { MainScreen(navController) } 16 | composable("second") { SecondScreen() } 17 | composable("list") { ListScreen() } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/bindableproperty/ChildViewModelBindablePropertyProvider.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.bindableproperty 2 | 3 | import de.trbnb.mvvmbase.databinding.ViewModel 4 | import de.trbnb.mvvmbase.observableproperty.StateSaveOption 5 | 6 | /** 7 | * Helper function to migrate from `childrenBindable`. 8 | * 9 | * @see ViewModel.asChildren 10 | */ 11 | @Deprecated("Use asChildren() instead.", ReplaceWith("bindable(defaultValue, fieldId, stateSaveOption).asChildren()")) 12 | inline fun > ViewModel.childrenBindable( 13 | defaultValue: C, 14 | fieldId: Int? = null, 15 | stateSaveOption: StateSaveOption? = null 16 | ): BindableProperty.Provider = bindable(defaultValue, fieldId, stateSaveOption).asChildren() 17 | -------------------------------------------------------------------------------- /conductor/src/main/java/de/trbnb/mvvmbase/conductor/ConductorViewModelLazy.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.conductor 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelLazy 6 | import androidx.lifecycle.ViewModelProvider 7 | import androidx.lifecycle.ViewModelStoreOwner 8 | import com.bluelinelabs.conductor.Controller 9 | 10 | /** 11 | * @see [androidx.fragment.app.Fragment.activityViewModels] 12 | */ 13 | @MainThread 14 | inline fun Controller.activityViewModels( 15 | noinline factoryProducer: () -> ViewModelProvider.Factory 16 | ) = ViewModelLazy( 17 | viewModelClass = VM::class, 18 | storeProducer = { (activity as ViewModelStoreOwner).viewModelStore }, 19 | factoryProducer = factoryProducer 20 | ) 21 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/savedstate/StateSavingViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.savedstate 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import de.trbnb.mvvmbase.MvvmBase 5 | import de.trbnb.mvvmbase.ViewModel 6 | import de.trbnb.mvvmbase.observableproperty.StateSaveOption 7 | 8 | /** 9 | * Specification for [ViewModel]s that support saving state via [SavedStateHandle]. 10 | */ 11 | interface StateSavingViewModel { 12 | /** 13 | * Used to store/read values if a new instance has to be created. 14 | */ 15 | val savedStateHandle: SavedStateHandle 16 | 17 | /** 18 | * Defines what default [StateSaveOption] will be used for bindable properties. 19 | */ 20 | val defaultStateSaveOption: StateSaveOption 21 | get() = MvvmBase.defaultStateSaveOption 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/commands/CommandUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.commands 2 | 3 | import de.trbnb.mvvmbase.observable.ObservableContainer 4 | 5 | internal fun RuleCommand<*, *>.dependsOn(observable: ObservableContainer, dependencyPropertyNames: List?) { 6 | if (dependencyPropertyNames.isNullOrEmpty()) return 7 | observable.addOnPropertyChangedCallback { _, propertyName -> 8 | if (propertyName in dependencyPropertyNames) { 9 | onEnabledChanged() 10 | } 11 | } 12 | } 13 | 14 | /** 15 | * Invokes the command with the parameter [Unit]. 16 | */ 17 | operator fun Command.invoke() = invoke(Unit) 18 | 19 | /** 20 | * Invokes the command safely with the parameter [Unit]. 21 | */ 22 | fun Command.invokeSafely() = invokeSafely(Unit) 23 | -------------------------------------------------------------------------------- /rxjava2/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /rxjava3/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /conductor/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /coroutines/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /rxjava3/src/main/java/de/trbnb/mvvmbase/rxjava3/ViewModelDisposable.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.rxjava3 2 | 3 | import de.trbnb.mvvmbase.databinding.ViewModel 4 | import io.reactivex.rxjava3.disposables.CompositeDisposable 5 | import java.io.Closeable 6 | 7 | private const val COMPOSITE_DISPOSABLE_KEY = "de.trbnb.mvvmbase.rxjava3.CompositeDisposable" 8 | 9 | /** 10 | * Gets [CompositeDisposable] that will immediately be disposed if the ViewModel is destroyed. 11 | */ 12 | val ViewModel.compositeDisposable: CompositeDisposable 13 | get() = (get(COMPOSITE_DISPOSABLE_KEY) ?: initTag(COMPOSITE_DISPOSABLE_KEY, ViewModelDisposableContainer())).disposable 14 | 15 | internal class ViewModelDisposableContainer : Closeable { 16 | internal val disposable = CompositeDisposable() 17 | override fun close() { 18 | disposable.dispose() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Users\Thorben\Documents\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | #-dontobfuscate 19 | #-keepattributes Signature, InnerClasses, EnclosingMethod, RuntimeVisibleAnnotations 20 | -------------------------------------------------------------------------------- /rxjava2/src/main/java/de/trbnb/mvvmbase/rxjava2/ViewModelDisposable.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.rxjava2 2 | 3 | import de.trbnb.mvvmbase.databinding.ViewModel 4 | import io.reactivex.disposables.CompositeDisposable 5 | import java.io.Closeable 6 | 7 | private const val COMPOSITE_DISPOSABLE_KEY = "de.trbnb.mvvmbase.databinding.rxjava2.CompositeDisposable" 8 | 9 | /** 10 | * Gets [CompositeDisposable] that will immediately be disposed if the ViewModel is destroyed. 11 | */ 12 | val ViewModel.compositeDisposable: CompositeDisposable 13 | get() = (get(COMPOSITE_DISPOSABLE_KEY) ?: initTag(COMPOSITE_DISPOSABLE_KEY, ViewModelDisposableContainer())).disposable 14 | 15 | internal class ViewModelDisposableContainer : Closeable { 16 | internal val disposable = CompositeDisposable() 17 | override fun close() { 18 | disposable.dispose() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/observable/ObservableUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.observable 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.LifecycleEventObserver 5 | import androidx.lifecycle.LifecycleOwner 6 | import de.trbnb.mvvmbase.OnPropertyChangedCallback 7 | 8 | /** 9 | * Adds an [OnPropertyChangedCallback] and removes it when the lifecycle of [lifecycleOwner] is destroyed. 10 | */ 11 | fun ObservableContainer.addOnPropertyChangedCallback(lifecycleOwner: LifecycleOwner, callback: OnPropertyChangedCallback) { 12 | addOnPropertyChangedCallback(callback) 13 | lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver { 14 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 15 | if (event == Lifecycle.Event.ON_DESTROY) { 16 | removeOnPropertyChangedCallback(callback) 17 | } 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/utils/ObservableUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.utils 2 | 3 | import androidx.databinding.Observable 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleEventObserver 6 | import androidx.lifecycle.LifecycleOwner 7 | 8 | /** 9 | * Adds an [Observable.OnPropertyChangedCallback] and removes it when the lifecycle of [lifecycleOwner] is destroyed. 10 | */ 11 | fun Observable.addOnPropertyChangedCallback(lifecycleOwner: LifecycleOwner, callback: Observable.OnPropertyChangedCallback) { 12 | addOnPropertyChangedCallback(callback) 13 | lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver { 14 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 15 | if (event == Lifecycle.Event.ON_DESTROY) { 16 | removeOnPropertyChangedCallback(callback) 17 | } 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /databinding/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Users\Thorben\Documents\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -keep class de.trbnb.mvvmbase.databinding.BR { *; } 19 | 20 | -keep @interface kotlin.Metadata { *; } 21 | 22 | -keep class ** extends de.trbnb.mvvmbase.databinding.BaseViewModel { 23 | @androidx.databinding.Bindable public *; 24 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | android.useAndroidX=true 20 | android.enableJetifier=false 21 | -------------------------------------------------------------------------------- /core/src/test/java/de/trbnb/mvvmbase/test/utils/ReflectionUtilsTests.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.test.utils 2 | 3 | import de.trbnb.mvvmbase.BaseViewModel 4 | import de.trbnb.mvvmbase.MvvmBase 5 | import de.trbnb.mvvmbase.observableproperty.observable 6 | import de.trbnb.mvvmbase.utils.observe 7 | import org.junit.jupiter.api.Assertions 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | 11 | class ReflectionUtilsTests { 12 | @BeforeEach 13 | fun setup() { 14 | MvvmBase.disableViewModelLifecycleThreadConstraints() 15 | } 16 | 17 | @Test 18 | fun `observe property`() { 19 | val observable = object : BaseViewModel() { 20 | var foo by observable("") 21 | } 22 | 23 | var wasTriggered = false 24 | 25 | observable::foo.observe { 26 | wasTriggered = true 27 | } 28 | 29 | observable.foo = "kd" 30 | 31 | Assertions.assertEquals(true, wasTriggered) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /conductor/src/main/java/de/trbnb/mvvmbase/conductor/ViewModelProviderFactoryHelpers.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.conductor 2 | 3 | import androidx.lifecycle.AbstractSavedStateViewModelFactory 4 | import androidx.lifecycle.SavedStateHandle 5 | import androidx.lifecycle.ViewModelProvider 6 | import de.trbnb.mvvmbase.databinding.ViewModel 7 | 8 | /** 9 | * Convenience function to create a [ViewModelProvider.Factory] for an [de.trbnb.mvvmbase.databinding.MvvmBindingActivity]. 10 | * Can be useful for overriding [MvvmBindingController.getDefaultViewModelProviderFactory]. 11 | */ 12 | fun MvvmBindingController.viewModelProviderFactory(factory: (handle: SavedStateHandle) -> VM): ViewModelProvider.Factory 13 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel = object : AbstractSavedStateViewModelFactory(this, args) { 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { 16 | return factory(handle) as T 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/observable/PropertyChangeRegistry.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.observable 2 | 3 | import de.trbnb.mvvmbase.OnPropertyChangedCallback 4 | 5 | internal class PropertyChangeRegistry( 6 | private val dependencyPairs: List>> 7 | ) : CallbackRegistry( 8 | object : NotifierCallback() { 9 | override fun onNotifyCallback(callback: OnPropertyChangedCallback, sender: ObservableContainer, arg: String) { 10 | callback.onPropertyChanged(sender, arg) 11 | } 12 | } 13 | ) { 14 | fun notifyChange(sender: ObservableContainer, propertyName: String) { 15 | notifyCallbacks(sender, propertyName) 16 | 17 | dependencyPairs.forEach { (dependentProperty, source) -> 18 | if (propertyName in source) { 19 | notifyChange(sender, dependentProperty) 20 | return@forEach 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/observableproperty/StateSaveOption.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.observableproperty 2 | 3 | import kotlin.reflect.KProperty 4 | 5 | /** 6 | * Represents the options for state saving mechanisms that can be used by BindableProperties. 7 | */ 8 | sealed class StateSaveOption { 9 | /** 10 | * The state of a property should not be saved. 11 | */ 12 | object None : StateSaveOption() 13 | 14 | /** 15 | * The state of a property should be saved and the key for it should be figured out automatically. 16 | */ 17 | object Automatic : StateSaveOption() 18 | 19 | /** 20 | * The state of a property should be saved and the key will be [key]. 21 | */ 22 | class Manual(internal val key: String) : StateSaveOption() 23 | } 24 | 25 | /** 26 | * Resolves a key for [androidx.lifecycle.SavedStateHandle]. 27 | */ 28 | fun StateSaveOption.resolveKey(property: KProperty<*>): String? = when (this) { 29 | StateSaveOption.Automatic -> property.name 30 | is StateSaveOption.Manual -> key 31 | StateSaveOption.None -> null 32 | } 33 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/second/SecondViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.second 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import de.trbnb.mvvmbase.observableproperty.observable 6 | import de.trbnb.mvvmbase.sample.R 7 | import de.trbnb.mvvmbase.sample.app.resource.ResourceProvider 8 | import de.trbnb.mvvmbase.savedstate.BaseStateSavingViewModel 9 | import javax.inject.Inject 10 | 11 | @HiltViewModel 12 | class SecondViewModel @Inject constructor( 13 | savedStateHandle: SavedStateHandle, 14 | resourceProvider: ResourceProvider 15 | ) : BaseStateSavingViewModel(savedStateHandle) { 16 | var text by observable(resourceProvider.getString(R.string.not_restored)) 17 | 18 | var progress by observable(0) 19 | .distinct() 20 | .validate { _, new -> new.coerceAtMost(100) } 21 | 22 | init { 23 | val key = "restored" 24 | if (key in savedStateHandle) { 25 | text = resourceProvider.getString(R.string.restored) 26 | } 27 | savedStateHandle[key] = true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/MvvmBase.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase 2 | 3 | import de.trbnb.mvvmbase.observableproperty.StateSaveOption 4 | 5 | /** 6 | * Object for containing library configurations. 7 | */ 8 | object MvvmBase { 9 | var defaultStateSaveOption: StateSaveOption = StateSaveOption.Automatic 10 | private set 11 | 12 | var enforceViewModelLifecycleMainThread = true 13 | private set 14 | 15 | /** 16 | * Sets the default [StateSaveOption] that will be used for bindable properties in [de.trbnb.mvvmbase.savedstate.StateSavingViewModel]. 17 | */ 18 | fun defaultStateSaveOption(stateSaveOption: StateSaveOption) = apply { 19 | defaultStateSaveOption = stateSaveOption 20 | } 21 | 22 | /** 23 | * Starting with Androidx Lifecycle version 2.3.0 all Lifecycles are thread-safe (only usable from main-thread). 24 | * This can be deactivated for [ViewModel.getLifecycle] to allow for initialization of ViewModels on other threads. 25 | */ 26 | fun disableViewModelLifecycleThreadConstraints(): MvvmBase = apply { 27 | enforceViewModelLifecycleMainThread = false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/list/ListScreen.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.list 2 | 3 | import androidx.compose.foundation.lazy.LazyColumn 4 | import androidx.compose.foundation.lazy.items 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.State 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import androidx.hilt.navigation.compose.hiltViewModel 11 | import de.trbnb.mvvmbase.compose.observeAsState 12 | import de.trbnb.mvvmbase.sample.app.AppTheme 13 | 14 | @Composable 15 | fun ListScreen() { 16 | val viewModel = hiltViewModel() 17 | ListScreenTemplate(viewModel::items.observeAsState()) 18 | } 19 | 20 | @Preview 21 | @Composable 22 | fun ListScreenTemplate( 23 | items: State> = mutableStateOf(listOf( 24 | Item(text = "One"), 25 | Item(text = "Two"), 26 | Item(text = "Three"), 27 | Item(text = "Four"), 28 | Item(text = "Five"), 29 | )) 30 | ) = AppTheme { 31 | LazyColumn { 32 | items(items.value, { it.id }) { 33 | Text(it.text) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/observable/ObservableContainer.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.observable 2 | 3 | import de.trbnb.mvvmbase.OnPropertyChangedCallback 4 | import kotlin.reflect.KProperty 5 | 6 | /** 7 | * Interface that describes basic functionality for classes that contain observable properties. 8 | */ 9 | interface ObservableContainer { 10 | /** 11 | * Adds a callback that will be notified when a propertys value has changed. 12 | */ 13 | fun addOnPropertyChangedCallback(callback: OnPropertyChangedCallback) 14 | 15 | /** 16 | * Removes a callback. 17 | * 18 | * @see addOnPropertyChangedCallback 19 | */ 20 | fun removeOnPropertyChangedCallback(callback: OnPropertyChangedCallback) 21 | 22 | /** 23 | * Notifies all callbacks that a propertys value may have changed. 24 | * 25 | * @param propertyName The name of the property. 26 | */ 27 | fun notifyPropertyChanged(propertyName: String) 28 | 29 | /** 30 | * Notifies all callbacks that a propertys value may have changed. 31 | * 32 | * @param property A reference of the property. 33 | */ 34 | fun notifyPropertyChanged(property: KProperty<*>) = notifyPropertyChanged(property.name) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/events/EventChannelImpl.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.events 2 | 3 | /** 4 | * [EventChannel] implementation. 5 | * 6 | * @param memorizeNotReceivedEvents Defines if events that can't be received by listeners because none are registered are sent later 7 | * when a listener is registered. 8 | */ 9 | class EventChannelImpl(memorizeNotReceivedEvents: Boolean = true) : EventChannel { 10 | private val listeners = mutableListOf() 11 | private val notReceivedEvents = if (memorizeNotReceivedEvents) mutableListOf() else null 12 | 13 | override operator fun invoke(event: Event) { 14 | if (listeners.isEmpty()) { 15 | notReceivedEvents?.add(event) 16 | } else { 17 | listeners.forEach { it.invoke(event) } 18 | } 19 | } 20 | 21 | override fun addListener(eventListener: EventListener): EventListener { 22 | listeners += eventListener 23 | notReceivedEvents?.apply { 24 | forEach(eventListener) 25 | clear() 26 | } 27 | return eventListener 28 | } 29 | 30 | override fun removeListener(eventListener: EventListener) { 31 | listeners.removeAll { it == eventListener } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample/src/main/java/de/trbnb/mvvmbase/sample/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.sample.main 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import de.trbnb.mvvmbase.commands.ruleCommand 7 | import de.trbnb.mvvmbase.observableproperty.observable 8 | import de.trbnb.mvvmbase.savedstate.BaseStateSavingViewModel 9 | import io.reactivex.Observable 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class MainViewModel @Inject constructor( 16 | savedStateHandle: SavedStateHandle 17 | ) : BaseStateSavingViewModel(savedStateHandle) { 18 | var textInput by observable("") 19 | .distinct() 20 | 21 | val title = Observable.create { emitter -> 22 | emitter.onNext("foo") 23 | viewModelScope.launch { 24 | delay(5000) 25 | emitter.onNext("bar") 26 | } 27 | } 28 | 29 | val showToastCommand = ruleCommand( 30 | action = { eventChannel(MainEvent.ShowToast(textInput)) }, 31 | enabledRule = { textInput.isNotEmpty() }, 32 | dependencyProperties = listOf(::textInput) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/commands/SimpleCommand.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.commands 2 | 3 | /** 4 | * A [Command] implementation that can simply be set as en-/disabled with a boolean value. 5 | * 6 | * @param action The initial action that will be run when the Command is executed. 7 | * @param isEnabled Has to be `true` if this Command should be enabled, otherwise `false`. 8 | */ 9 | class SimpleCommand 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 create(modelClass: Class): T =factory() as T 42 | } 43 | 44 | /** 45 | * Converts a [Provider] to a [ViewModelProvider.Factory]. 46 | * Useful if a [Provider] for a [ViewModel] is injected and is intended to be used for overriding getDefaultViewModelProviderFactory. 47 | */ 48 | fun Provider.asViewModelProviderFactory(): ViewModelProvider.Factory 49 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel = viewModelProviderFactory(::get) 50 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/utils/ComposeUtils.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.utils 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.DisposableEffect 7 | import androidx.compose.runtime.MutableState 8 | import androidx.compose.runtime.State 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalLifecycleOwner 13 | import androidx.compose.ui.viewinterop.AndroidViewBinding 14 | import androidx.databinding.ViewDataBinding 15 | import androidx.lifecycle.LifecycleOwner 16 | import de.trbnb.mvvmbase.compose.PropertyMutableState 17 | import de.trbnb.mvvmbase.databinding.BR 18 | import de.trbnb.mvvmbase.databinding.ViewModel 19 | import kotlin.reflect.KMutableProperty0 20 | import kotlin.reflect.KProperty0 21 | 22 | /** 23 | * Observes an observable property as Compose state. 24 | */ 25 | @Composable 26 | fun KProperty0.observeBindableAsState(): State { 27 | val state = remember { mutableStateOf(get()) } 28 | val lifecycleOwner = LocalLifecycleOwner.current 29 | DisposableEffect(key1 = this, key2 = lifecycleOwner) { 30 | val dispose = observeBindable(lifecycleOwner, false) { state.value = it } 31 | onDispose(dispose::invoke) 32 | } 33 | return state 34 | } 35 | 36 | /** 37 | * Observes an observable property as mutable Compose state. 38 | */ 39 | @Composable 40 | fun KMutableProperty0.observeBindableAsMutableState(): MutableState { 41 | return PropertyMutableState(observeBindableAsState(), this) 42 | } 43 | 44 | /** 45 | * Helper function to use [AndroidViewBinding] with [ViewDataBinding]s that only have a single variable: a ViewModel. 46 | * 47 | * @see AndroidViewBinding 48 | */ 49 | @Composable 50 | fun AndroidViewDataBinding( 51 | viewModel: VM, 52 | factory: (inflater: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> B, 53 | modifier: Modifier = Modifier, 54 | fieldId: Int = BR.vm, 55 | update: B.(viewModel: VM) -> Unit = {}, 56 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current 57 | ) where VM : ViewModel, VM : androidx.lifecycle.ViewModel, B : ViewDataBinding { 58 | AndroidViewBinding( 59 | factory = { inflater, parent, attachToParent -> 60 | factory(inflater, parent, attachToParent).apply { 61 | this.lifecycleOwner = lifecycleOwner 62 | setVariable(fieldId, viewModel) 63 | viewModel.onBind() 64 | } 65 | }, 66 | modifier = modifier, 67 | update = { update(viewModel) } 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /core/src/test/java/de/trbnb/mvvmbase/test/commands/CommandTests.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.test.commands 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.MvvmBase 8 | import de.trbnb.mvvmbase.commands.Command 9 | import de.trbnb.mvvmbase.commands.SimpleCommand 10 | import de.trbnb.mvvmbase.commands.ruleCommand 11 | import de.trbnb.mvvmbase.commands.simpleCommand 12 | import de.trbnb.mvvmbase.observableproperty.observable 13 | import de.trbnb.mvvmbase.utils.observe 14 | import org.junit.jupiter.api.Assertions.assertEquals 15 | import org.junit.jupiter.api.BeforeAll 16 | import org.junit.jupiter.api.Test 17 | 18 | class CommandTests { 19 | companion object { 20 | @BeforeAll 21 | @JvmStatic 22 | fun setup() { 23 | MvvmBase.disableViewModelLifecycleThreadConstraints() 24 | } 25 | } 26 | 27 | @Test 28 | fun `enabledListener with Lifecycle`() { 29 | val command: Command<*, *> = SimpleCommand { } 30 | val lifecycleOwner = object : LifecycleOwner { 31 | private val lifecycle = LifecycleRegistry.createUnsafe(this).apply { 32 | currentState = Lifecycle.State.STARTED 33 | } 34 | override fun getLifecycle() = lifecycle 35 | fun destroy() { lifecycle.currentState = Lifecycle.State.DESTROYED } 36 | } 37 | 38 | var listenerWasTriggered = false 39 | command::isEnabled.observe(lifecycleOwner) { listenerWasTriggered = true } 40 | lifecycleOwner.destroy() 41 | assert(!listenerWasTriggered) 42 | } 43 | 44 | @Test 45 | fun `observeLifecycle works`() { 46 | val viewModel = object : BaseViewModel() { 47 | val command = simpleCommand { } 48 | } 49 | 50 | var amountListenerWasCalled = 0 51 | viewModel.command::isEnabled.observe { amountListenerWasCalled++ } 52 | viewModel.command.isEnabled = !viewModel.command.isEnabled 53 | assertEquals(amountListenerWasCalled, 1) 54 | 55 | viewModel.command.isEnabled = !viewModel.command.isEnabled 56 | assertEquals(amountListenerWasCalled, 2) 57 | } 58 | 59 | @Test 60 | fun `dependsOn works`() { 61 | val viewModel = DependsOnViewModel() 62 | assert(!viewModel.command.isEnabled) 63 | 64 | viewModel.foo = Any() 65 | assert(viewModel.command.isEnabled) 66 | } 67 | 68 | class DependsOnViewModel : BaseViewModel() { 69 | var foo by observable() 70 | 71 | val command = ruleCommand( 72 | enabledRule = { foo != null }, 73 | action = {}, 74 | dependencyProperties = listOf(::foo) 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /databinding/src/test/java/de/trbnb/mvvmbase/databinding/test/commands/CommandTests.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.test.commands 2 | 3 | import androidx.databinding.Bindable 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.LifecycleRegistry 7 | import de.trbnb.mvvmbase.MvvmBase 8 | import de.trbnb.mvvmbase.databinding.BaseViewModel 9 | import de.trbnb.mvvmbase.databinding.bindableproperty.bindable 10 | import de.trbnb.mvvmbase.databinding.commands.Command 11 | import de.trbnb.mvvmbase.databinding.commands.SimpleCommand 12 | import de.trbnb.mvvmbase.databinding.commands.addEnabledListener 13 | import de.trbnb.mvvmbase.databinding.commands.ruleCommand 14 | import de.trbnb.mvvmbase.databinding.commands.simpleCommand 15 | import de.trbnb.mvvmbase.databinding.initDataBinding 16 | import de.trbnb.mvvmbase.databinding.resetDataBinding 17 | import de.trbnb.mvvmbase.databinding.test.BR 18 | import org.junit.jupiter.api.Test 19 | 20 | class CommandTests { 21 | @Test 22 | fun `enabledListener with Lifecycle`() { 23 | val command: Command<*, *> = SimpleCommand { } 24 | val lifecycleOwner = object : LifecycleOwner { 25 | private val lifecycle = LifecycleRegistry.createUnsafe(this).apply { 26 | currentState = Lifecycle.State.STARTED 27 | } 28 | override fun getLifecycle() = lifecycle 29 | fun destroy() { lifecycle.currentState = Lifecycle.State.DESTROYED } 30 | } 31 | 32 | var listenerWasTriggered = false 33 | command.addEnabledListener(lifecycleOwner) { listenerWasTriggered = true } 34 | lifecycleOwner.destroy() 35 | assert(!listenerWasTriggered) 36 | } 37 | 38 | @Test 39 | fun `observeLifecycle works`() { 40 | val viewModel = object : BaseViewModel() { 41 | val command = simpleCommand { } 42 | } 43 | 44 | var amountListenerWasCalled = 0 45 | viewModel.command.addEnabledListenerForView { amountListenerWasCalled++ } 46 | viewModel.onBind() 47 | viewModel.command.isEnabled = !viewModel.command.isEnabled 48 | assert(amountListenerWasCalled == 1) 49 | 50 | viewModel.onUnbind() 51 | viewModel.command.isEnabled = !viewModel.command.isEnabled 52 | assert(amountListenerWasCalled == 1) 53 | } 54 | 55 | @Test 56 | fun `dependsOn works`() { 57 | MvvmBase.initDataBinding() 58 | val viewModel = DependsOnViewModel() 59 | assert(!viewModel.command.isEnabled) 60 | 61 | viewModel.foo = Any() 62 | assert(viewModel.command.isEnabled) 63 | MvvmBase.resetDataBinding() 64 | } 65 | 66 | class DependsOnViewModel : BaseViewModel() { 67 | @get:Bindable 68 | var foo by bindable() 69 | 70 | val command = ruleCommand( 71 | enabledRule = { foo != null }, 72 | action = { }, 73 | dependentFieldIds = intArrayOf(BR.foo) 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rxjava2/src/test/java/de/trbnb/mvvmbase/rxjava2/test/SingleBindingTests.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_VARIABLE", "unused") 2 | 3 | package de.trbnb.mvvmbase.rxjava2.test 4 | 5 | import de.trbnb.mvvmbase.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.Single 11 | import io.reactivex.subjects.SingleSubject 12 | import org.junit.jupiter.api.BeforeAll 13 | import org.junit.jupiter.api.Test 14 | 15 | class SingleBindingTests { 16 | companion object { 17 | @BeforeAll 18 | @JvmStatic 19 | fun setup() { 20 | MvvmBase.initDataBinding().disableViewModelLifecycleThreadConstraints() 21 | } 22 | } 23 | 24 | @Test 25 | fun `is the given default value used`() { 26 | val single: Single = SingleSubject.create() 27 | 28 | val viewModel = object : BaseViewModel(), RxViewModel { 29 | val property by single.toBindable(defaultValue = 3) 30 | } 31 | 32 | assert(viewModel.property == 3) 33 | } 34 | 35 | @Test 36 | fun `is null the default default value`() { 37 | val single: Single = SingleSubject.create() 38 | 39 | val viewModel = object : BaseViewModel(), RxViewModel { 40 | val property by single.toBindable() 41 | } 42 | 43 | assert(viewModel.property == null) 44 | } 45 | 46 | @Test 47 | fun `is onError called`() { 48 | val single: SingleSubject = SingleSubject.create() 49 | var isOnErrorCalled = false 50 | val onError = { _: Throwable -> isOnErrorCalled = true } 51 | 52 | val viewModel = object : BaseViewModel(), RxViewModel { 53 | val property by single.toBindable(onError = onError) 54 | } 55 | 56 | single.onError(RuntimeException()) 57 | assert(isOnErrorCalled) 58 | } 59 | 60 | @Test 61 | fun `are new values received`() { 62 | val single: SingleSubject = SingleSubject.create() 63 | 64 | val viewModel = object : BaseViewModel(), RxViewModel { 65 | val property by single.toBindable(defaultValue = 3) 66 | } 67 | 68 | val newValue = 55 69 | single.onSuccess(newValue) 70 | assert(viewModel.property == newValue) 71 | } 72 | 73 | @Test 74 | fun `is notifyPropertyChanged() called (automatic field ID)`() { 75 | val single: SingleSubject = SingleSubject.create() 76 | 77 | val viewModel = ViewModelWithBindable(single) 78 | 79 | val propertyChangedCallback = TestPropertyChangedCallback() 80 | viewModel.addOnPropertyChangedCallback(propertyChangedCallback) 81 | 82 | val newValue = 55 83 | single.onSuccess(newValue) 84 | assert(BR.property in propertyChangedCallback.changedPropertyIds) 85 | } 86 | 87 | class ViewModelWithBindable(single: Single) : BaseViewModel(), RxViewModel { 88 | @get:Bindable 89 | val property by single.toBindable(defaultValue = 3) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /rxjava3/src/test/java/de/trbnb/mvvmbase/rxjava3/test/SingleBindingTests.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.databinding.BaseViewModel 7 | import de.trbnb.mvvmbase.MvvmBase 8 | import de.trbnb.mvvmbase.databinding.initDataBinding 9 | import de.trbnb.mvvmbase.rxjava3.RxViewModel 10 | import io.reactivex.rxjava3.core.Single 11 | import io.reactivex.rxjava3.subjects.SingleSubject 12 | import org.junit.jupiter.api.BeforeAll 13 | import org.junit.jupiter.api.Test 14 | 15 | class SingleBindingTests { 16 | companion object { 17 | @BeforeAll 18 | @JvmStatic 19 | fun setup() { 20 | MvvmBase.initDataBinding().disableViewModelLifecycleThreadConstraints() 21 | } 22 | } 23 | 24 | @Test 25 | fun `is the given default value used`() { 26 | val single: Single = SingleSubject.create() 27 | 28 | val viewModel = object : BaseViewModel(), RxViewModel { 29 | val property by single.toBindable(defaultValue = 3) 30 | } 31 | 32 | assert(viewModel.property == 3) 33 | } 34 | 35 | @Test 36 | fun `is null the default default value`() { 37 | val single: Single = SingleSubject.create() 38 | 39 | val viewModel = object : BaseViewModel(), RxViewModel { 40 | val property by single.toBindable() 41 | } 42 | 43 | assert(viewModel.property == null) 44 | } 45 | 46 | @Test 47 | fun `is onError called`() { 48 | val single: SingleSubject = SingleSubject.create() 49 | var isOnErrorCalled = false 50 | val onError = { _: Throwable -> isOnErrorCalled = true } 51 | 52 | val viewModel = object : BaseViewModel(), RxViewModel { 53 | val property by single.toBindable(onError = onError) 54 | } 55 | 56 | single.onError(RuntimeException()) 57 | assert(isOnErrorCalled) 58 | } 59 | 60 | @Test 61 | fun `are new values received`() { 62 | val single: SingleSubject = SingleSubject.create() 63 | 64 | val viewModel = object : BaseViewModel(), RxViewModel { 65 | val property by single.toBindable(defaultValue = 3) 66 | } 67 | 68 | val newValue = 55 69 | single.onSuccess(newValue) 70 | assert(viewModel.property == newValue) 71 | } 72 | 73 | @Test 74 | fun `is notifyPropertyChanged() called (automatic field ID)`() { 75 | val single: SingleSubject = SingleSubject.create() 76 | 77 | val viewModel = ViewModelWithBindable(single) 78 | 79 | val propertyChangedCallback = TestPropertyChangedCallback() 80 | viewModel.addOnPropertyChangedCallback(propertyChangedCallback) 81 | 82 | val newValue = 55 83 | single.onSuccess(newValue) 84 | assert(BR.property in propertyChangedCallback.changedPropertyIds) 85 | } 86 | 87 | class ViewModelWithBindable(single: Single) : BaseViewModel(), RxViewModel { 88 | @get:Bindable 89 | val property by single.toBindable(defaultValue = 3) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /core/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 | consumerProguardFile("proguard-rules.pro") 18 | } 19 | 20 | buildTypes { 21 | named("release").configure { 22 | isMinifyEnabled = false 23 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 24 | } 25 | } 26 | 27 | compileOptions { 28 | sourceCompatibility = Versions.java 29 | targetCompatibility = Versions.java 30 | } 31 | 32 | kotlin { 33 | explicitApi() 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = Versions.java.toString() 38 | } 39 | 40 | buildFeatures { 41 | compose = true 42 | } 43 | 44 | composeOptions { 45 | kotlinCompilerExtensionVersion = Versions.composeCompiler 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation(fileTree("dir" to "libs", "include" to listOf("*.jar"))) 51 | 52 | // Support library 53 | implementation("androidx.appcompat:appcompat:1.5.1") 54 | implementation("com.google.android.material:material:1.6.1") 55 | implementation("androidx.fragment:fragment-ktx:1.5.3") 56 | implementation("androidx.activity:activity-ktx:1.6.0") 57 | implementation("androidx.recyclerview:recyclerview:1.2.1") 58 | 59 | implementation("androidx.compose.runtime:runtime:${Versions.compose}") 60 | implementation("androidx.compose.ui:ui:${Versions.compose}") 61 | 62 | // Testing 63 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0") 64 | androidTestImplementation("androidx.test:runner:1.4.0") 65 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 66 | 67 | // Kotlin 68 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}") 69 | implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}") 70 | 71 | // Lifecycle architecture components 72 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") 73 | api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1") 74 | 75 | // Java inject API for dependency injection 76 | api("javax.inject:javax.inject:1") 77 | } 78 | 79 | repositories { 80 | mavenCentral() 81 | google() 82 | } 83 | 84 | val sourcesJar = task("sourcesJar") { 85 | archiveClassifier.set("sources") 86 | from(android.sourceSets["main"].java.srcDirs) 87 | } 88 | 89 | signing { 90 | sign(publishing.publications) 91 | } 92 | 93 | afterEvaluate { 94 | publishing { 95 | repositories { 96 | mavenCentralUpload(project) 97 | } 98 | publications { 99 | create(Publication.CORE, this@afterEvaluate) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/commands/RuleCommand.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.commands 2 | 3 | import androidx.databinding.Bindable 4 | import de.trbnb.mvvmbase.databinding.ViewModel 5 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId 6 | import kotlin.reflect.KProperty 7 | 8 | /** 9 | * A [Command] that determines if it is enabled via a predicate. 10 | * This predicate, or "rule", is set during initialization. 11 | 12 | * The predicates result will be cached. A refresh can be triggered by calling [onEnabledChanged]. 13 | * 14 | * @param action The initial action that will be run when the Command is executed. 15 | * @param enabledRule The initial rule that determines if this Command is enabled. 16 | */ 17 | class RuleCommand internal constructor( 18 | action: (P) -> R, 19 | private val enabledRule: () -> Boolean 20 | ) : BaseCommandImpl(action) { 21 | @get:Bindable 22 | override var isEnabled: Boolean = enabledRule() 23 | private set(value) { 24 | if (field == value) return 25 | 26 | field = value 27 | triggerEnabledChangedListener() 28 | } 29 | 30 | /** 31 | * This method has to be called when the result of the rule might have changed. 32 | */ 33 | fun onEnabledChanged() { 34 | isEnabled = enabledRule() 35 | } 36 | } 37 | 38 | /** 39 | * Helper function to create a [RuleCommand] that clears all it's listeners automatically when 40 | * [ViewModel.onUnbind] is called. 41 | */ 42 | @JvmName("parameterizedRuleCommand") 43 | fun ViewModel.ruleCommand( 44 | action: (P) -> R, 45 | enabledRule: () -> Boolean, 46 | dependentFieldIds: IntArray? = null 47 | ): RuleCommand = RuleCommand(action, enabledRule).apply { 48 | observeLifecycle(this@ruleCommand) 49 | dependsOn(this@ruleCommand, dependentFieldIds) 50 | } 51 | 52 | /** 53 | * Helper function to create a [RuleCommand] that clears all it's listeners automatically when 54 | * [ViewModel.onUnbind] is called. 55 | */ 56 | @JvmName("parameterizedRuleCommand") 57 | fun ViewModel.ruleCommand( 58 | action: (P) -> R, 59 | enabledRule: () -> Boolean, 60 | dependentFields: List> 61 | ): RuleCommand = ruleCommand(action, enabledRule, dependentFields.map { it.resolveFieldId() }.toIntArray()) 62 | 63 | /** 64 | * Helper function to create a parameter-less [RuleCommand] that clears all it's listeners automatically when 65 | * [ViewModel.onUnbind] is called. 66 | */ 67 | fun ViewModel.ruleCommand( 68 | action: (Unit) -> R, 69 | enabledRule: () -> Boolean, 70 | dependentFieldIds: IntArray? = null 71 | ): RuleCommand = ruleCommand(action, enabledRule, dependentFieldIds) 72 | 73 | /** 74 | * Helper function to create a parameter-less [RuleCommand] that clears all it's listeners automatically when 75 | * [ViewModel.onUnbind] is called. 76 | */ 77 | fun ViewModel.ruleCommand( 78 | action: (Unit) -> R, 79 | enabledRule: () -> Boolean, 80 | dependentFields: List> 81 | ): RuleCommand = ruleCommand(action, enabledRule, dependentFields) 82 | -------------------------------------------------------------------------------- /databinding/src/test/java/de/trbnb/mvvmbase/databinding/test/ReflectionUtilsTests.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding.test 2 | 3 | import androidx.databinding.Bindable 4 | import de.trbnb.mvvmbase.MvvmBase 5 | import de.trbnb.mvvmbase.databinding.BaseViewModel 6 | import de.trbnb.mvvmbase.databinding.initDataBinding 7 | import de.trbnb.mvvmbase.databinding.resetDataBinding 8 | import de.trbnb.mvvmbase.databinding.utils.brFieldName 9 | import de.trbnb.mvvmbase.databinding.utils.findGenericSuperclass 10 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId 11 | import org.junit.jupiter.api.BeforeEach 12 | import org.junit.jupiter.api.Test 13 | 14 | class ReflectionUtilsTests { 15 | open class A 16 | class B : A() 17 | 18 | @BeforeEach 19 | fun setup() { 20 | MvvmBase.disableViewModelLifecycleThreadConstraints() 21 | } 22 | 23 | @Test 24 | fun `findGenericSuperclass() for objects`() { 25 | assert(Any().findGenericSuperclass() == null) 26 | assert(Any().findGenericSuperclass>() == null) 27 | assert(B().findGenericSuperclass>()?.rawType == A::class.java) 28 | assert(B().findGenericSuperclass>()?.actualTypeArguments?.first() == String::class.java) 29 | } 30 | 31 | @Test 32 | fun `findGenericSuperclass() for types`() { 33 | assert(Any::class.java.findGenericSuperclass(Any::class.java) == null) 34 | assert(Any::class.java.findGenericSuperclass(List::class.java) == null) 35 | assert(B::class.java.findGenericSuperclass(A::class.java)?.rawType == A::class.java) 36 | assert(B::class.java.findGenericSuperclass(A::class.java)?.actualTypeArguments?.first() == String::class.java) 37 | } 38 | 39 | @Test 40 | fun `BR field name from property`() { 41 | val viewModel = TestViewModel() 42 | 43 | assert(viewModel::amount.brFieldName() == "amount") 44 | assert(viewModel::isAmount.brFieldName() == "isAmount") 45 | assert(viewModel::isLoading.brFieldName() == "loading") 46 | assert(viewModel::loading.brFieldName() == "loading") 47 | assert(viewModel::isLoadingNullable.brFieldName() == "isLoadingNullable") 48 | } 49 | 50 | @Test 51 | fun `BR field integer from property`() { 52 | MvvmBase.initDataBinding() 53 | val viewModel = TestViewModel() 54 | 55 | assert(viewModel::amount.resolveFieldId() == BR.amount) 56 | assert(viewModel::isAmount.resolveFieldId() == BR._all) 57 | assert(viewModel::isLoading.resolveFieldId() == BR.loading) 58 | assert(viewModel::loading.resolveFieldId() == BR.loading) 59 | assert(viewModel::isLoadingNullable.resolveFieldId() == BR._all) 60 | 61 | // reset MvvmBase for other tests 62 | MvvmBase.resetDataBinding() 63 | } 64 | 65 | class TestViewModel : BaseViewModel() { 66 | @get:Bindable val amount = 4 67 | /* No bindable as it violates JavaBeans convention */ val isAmount = 4 68 | @get:Bindable val isLoading = false 69 | @get:Bindable val loading = false 70 | /* No bindable as it violates JavaBeans convention */ val isLoadingNullable: Boolean? = null 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/src/test/java/de/trbnb/mvvmbase/test/commands/SimpleCommandTests.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.test.commands 2 | 3 | import de.trbnb.mvvmbase.commands.DisabledCommandInvocationException 4 | import de.trbnb.mvvmbase.commands.SimpleCommand 5 | import de.trbnb.mvvmbase.commands.invoke 6 | import de.trbnb.mvvmbase.commands.invokeSafely 7 | import de.trbnb.mvvmbase.utils.observe 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.assertThrows 10 | 11 | class SimpleCommandTests { 12 | @Test 13 | fun `invocation works when enabled`() { 14 | val command = SimpleCommand { _: Unit -> 4 } 15 | assert(command() == 4) 16 | assert(command.invokeSafely() == 4) 17 | } 18 | 19 | @Test 20 | fun `disabled command doesn't work`() { 21 | val command = SimpleCommand(isEnabled = false) { _: Unit -> 4 } 22 | assertThrows { command() } 23 | assert(command.invokeSafely() == null) 24 | } 25 | 26 | @Test 27 | fun `enabledListener is triggered`() { 28 | val command = SimpleCommand(isEnabled = true) { } 29 | var wasDisabled = false 30 | var wasReEnabled = false 31 | command::isEnabled.observe { enabled -> 32 | when { 33 | enabled -> wasReEnabled = true 34 | else -> wasDisabled = true 35 | } 36 | } 37 | 38 | command.isEnabled = false 39 | assert(wasDisabled) 40 | command.isEnabled = true 41 | assert(wasReEnabled) 42 | } 43 | 44 | @Test 45 | fun `enabledListener is not triggered when isEnabled is set to previous value`() = booleanArrayOf(true, false).forEach { bool -> 46 | val command = SimpleCommand(isEnabled = bool) { } 47 | 48 | var listenerWasTriggered = false 49 | command::isEnabled.observe { listenerWasTriggered = true } 50 | command.isEnabled = bool 51 | 52 | assert(!listenerWasTriggered) 53 | } 54 | 55 | @Test 56 | fun `removing enabledListener works`() { 57 | val command = SimpleCommand { } 58 | 59 | var listenerWasTriggered = false 60 | command::isEnabled.observe { listenerWasTriggered = true }() 61 | command.isEnabled = !command.isEnabled 62 | 63 | assert(!listenerWasTriggered) 64 | } 65 | 66 | @Test 67 | fun `clearing enabledListeners works`() { 68 | val command = SimpleCommand { } 69 | 70 | fun newListener() = object : (Boolean) -> Unit { 71 | var wasTriggered = false 72 | private set 73 | 74 | override fun invoke(enabled: Boolean) { 75 | wasTriggered = true 76 | } 77 | } 78 | 79 | val listeners = List(3) { newListener() }.map { it to command::isEnabled.observe(action = it) } 80 | 81 | listeners.forEach { it.second() } 82 | command.isEnabled = !command.isEnabled 83 | 84 | assert(listeners.all { !it.first.wasTriggered }) 85 | } 86 | 87 | @Test 88 | fun `changing enabled invokes notifyPropertyChanged`() { 89 | val command = SimpleCommand(false) { } 90 | 91 | var callbackWasTriggered = false 92 | command::isEnabled.observe { callbackWasTriggered = true } 93 | 94 | command.isEnabled = true 95 | assert(callbackWasTriggered) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase 2 | 3 | import androidx.annotation.CallSuper 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.destroyInternal 7 | import androidx.lifecycle.getTagFromViewModel 8 | import androidx.lifecycle.setTagIfAbsentForViewModel 9 | import de.trbnb.mvvmbase.events.EventChannel 10 | import de.trbnb.mvvmbase.events.EventChannelImpl 11 | import de.trbnb.mvvmbase.observable.PropertyChangeRegistry 12 | import kotlin.reflect.KProperty 13 | import kotlin.reflect.full.findAnnotation 14 | import kotlin.reflect.full.memberProperties 15 | import androidx.lifecycle.ViewModel as ArchitectureViewModel 16 | 17 | /** 18 | * Simple base implementation of the [ViewModel]. 19 | */ 20 | abstract class BaseViewModel : ArchitectureViewModel(), ViewModel, LifecycleOwner { 21 | /** 22 | * Callback registry for [de.trbnb.mvvmbase.observable.ObservableContainer]. 23 | */ 24 | private val callbacks: PropertyChangeRegistry 25 | 26 | /** 27 | * [EventChannel] implementation that can be used to send non-state information to a view component. 28 | */ 29 | override val eventChannel: EventChannel by lazy { EventChannelImpl(memorizeNotReceivedEvents) } 30 | 31 | /** 32 | * Gets if events that are raised when no listeners are registered are raised later when a listener is registered. 33 | */ 34 | protected open val memorizeNotReceivedEvents: Boolean 35 | get() = true 36 | 37 | /** 38 | * @see ViewModelLifecycleOwner 39 | */ 40 | private val lifecycleOwner = ViewModelLifecycleOwner(MvvmBase.enforceViewModelLifecycleMainThread) 41 | 42 | init { 43 | val pairs = javaClass.kotlin.memberProperties.mapNotNull { property -> 44 | when (val annotation = property.findAnnotation()) { 45 | null -> null 46 | else -> property.name to annotation.value 47 | } 48 | } 49 | 50 | callbacks = PropertyChangeRegistry(pairs) 51 | } 52 | 53 | final override fun addOnPropertyChangedCallback(callback: OnPropertyChangedCallback) { 54 | callbacks.add(callback) 55 | } 56 | 57 | final override fun removeOnPropertyChangedCallback(callback: OnPropertyChangedCallback) { 58 | callbacks.remove(callback) 59 | } 60 | 61 | final override fun notifyPropertyChanged(propertyName: String) { 62 | callbacks.notifyChange(this, propertyName) 63 | } 64 | 65 | final override fun notifyPropertyChanged(property: KProperty<*>) { 66 | notifyPropertyChanged(property.name) 67 | } 68 | 69 | final override fun destroy() { 70 | destroyInternal() 71 | } 72 | 73 | /** 74 | * Is called when this instance is about to be destroyed. 75 | * Any references that could cause memory leaks should be cleared here. 76 | */ 77 | @CallSuper 78 | protected open fun onDestroy() { 79 | super.onCleared() 80 | lifecycleOwner.onEvent(ViewModelLifecycleOwner.Event.DESTROYED) 81 | } 82 | 83 | final override fun onCleared() { 84 | onDestroy() 85 | } 86 | 87 | final override operator fun get(key: String): T? = getTagFromViewModel(key) 88 | 89 | final override fun initTag(key: String, newValue: T): T = setTagIfAbsentForViewModel(key, newValue) 90 | 91 | override fun getLifecycle(): Lifecycle = lifecycleOwner.lifecycle 92 | } 93 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/MvvmView.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding 2 | 3 | import androidx.annotation.CallSuper 4 | import androidx.annotation.LayoutRes 5 | import androidx.databinding.DataBindingComponent 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.databinding.ViewDataBinding 8 | import androidx.lifecycle.ViewModelStoreOwner 9 | import androidx.savedstate.SavedStateRegistryOwner 10 | import de.trbnb.mvvmbase.events.Event 11 | 12 | /** 13 | * Contract for view components that want to support MVVM with a [ViewModel] bound to a [ViewDataBinding]. 14 | * 15 | * Implementation should have an instance in [binding] what will bind the [viewModel] with the [viewModelBindingId]. 16 | * Only a layout resource has to be specified via [layoutId] to create the binding. 17 | * Specifying a [DataBindingComponent] via [dataBindingComponent] is optional. 18 | * 19 | * The [ViewModel] will be instantiated via the ViewModel API by Android X. 20 | */ 21 | interface MvvmView : ViewModelStoreOwner, SavedStateRegistryOwner 22 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel { 23 | /** 24 | * The [ViewDataBinding] implementation for a specific layout. 25 | * Nullable due to possible lifecycle circumstances. 26 | */ 27 | val binding: B? 28 | 29 | /** 30 | * Delegate for [viewModel]. 31 | * 32 | * Can be overridden to make use of `activityViewModels()` or `navGraphViewModels()`. 33 | */ 34 | val viewModelDelegate: Lazy 35 | 36 | /** 37 | * The [ViewModel] that is used for data binding. 38 | * 39 | * @see viewModelDelegate 40 | */ 41 | val viewModel: VM 42 | get() = viewModelDelegate.value 43 | 44 | /** 45 | * Gets the class of the view model that an implementation uses. 46 | */ 47 | val viewModelClass: Class 48 | 49 | /** 50 | * The [de.trbnb.mvvmbase.BR] value that is used as parameter for the view model in the binding. 51 | * Is always [de.trbnb.mvvmbase.BR.vm]. 52 | */ 53 | val viewModelBindingId: Int 54 | get() = BR.vm 55 | 56 | /** 57 | * The layout resource ID for this Activity. 58 | * Is used to create the [ViewDataBinding]. 59 | */ 60 | @get:LayoutRes 61 | val layoutId: Int 62 | 63 | /** 64 | * Defines which [DataBindingComponent] will be used with [DataBindingUtil.inflate]. 65 | * Default is `null` and will lead to usage of [DataBindingUtil.getDefaultComponent]. 66 | */ 67 | val dataBindingComponent: DataBindingComponent? 68 | get() = null 69 | 70 | /** 71 | * Called when the view model is loaded and is set as [viewModel]. 72 | * 73 | * @param[viewModel] The [ViewModel] instance that was loaded. 74 | */ 75 | fun onViewModelLoaded(viewModel: VM) 76 | 77 | /** 78 | * Called when the view model notifies listeners that a property has changed. 79 | * 80 | * @param[viewModel] The [ViewModel] instance whose property has changed. 81 | * @param[fieldId] The ID of the field in the BR file that indicates which property in the view model has changed. 82 | */ 83 | @CallSuper 84 | @Deprecated("Use KProperty.observe() instead", level = DeprecationLevel.ERROR) 85 | fun onViewModelPropertyChanged(viewModel: VM, fieldId: Int) { } 86 | 87 | /** 88 | * Is called when the ViewModel sends an [Event]. 89 | */ 90 | fun onEvent(event: Event) { } 91 | } 92 | -------------------------------------------------------------------------------- /rxjava3/src/test/java/de/trbnb/mvvmbase/rxjava3/test/MaybeBindingTests.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_VARIABLE", "unused") 2 | 3 | package de.trbnb.mvvmbase.rxjava3.test 4 | 5 | import de.trbnb.mvvmbase.MvvmBase 6 | import de.trbnb.mvvmbase.databinding.BaseViewModel 7 | import de.trbnb.mvvmbase.databinding.initDataBinding 8 | import de.trbnb.mvvmbase.rxjava3.RxViewModel 9 | import io.reactivex.rxjava3.core.Maybe 10 | import io.reactivex.rxjava3.subjects.MaybeSubject 11 | import org.junit.jupiter.api.BeforeAll 12 | import org.junit.jupiter.api.Test 13 | 14 | class MaybeBindingTests { 15 | companion object { 16 | @BeforeAll 17 | @JvmStatic 18 | fun setup() { 19 | MvvmBase.initDataBinding().disableViewModelLifecycleThreadConstraints() 20 | } 21 | } 22 | 23 | @Test 24 | fun `is the given default value used`() { 25 | val maybe: Maybe = MaybeSubject.create() 26 | 27 | val viewModel = object : BaseViewModel(), RxViewModel { 28 | val property by maybe.toBindable(defaultValue = 3) 29 | } 30 | 31 | assert(viewModel.property == 3) 32 | } 33 | 34 | @Test 35 | fun `is null the default default value`() { 36 | val maybe: Maybe = MaybeSubject.create() 37 | 38 | val viewModel = object : BaseViewModel(), RxViewModel { 39 | val property by maybe.toBindable() 40 | } 41 | 42 | assert(viewModel.property == null) 43 | } 44 | 45 | @Test 46 | fun `is onError called`() { 47 | val maybe: MaybeSubject = MaybeSubject.create() 48 | var isOnErrorCalled = false 49 | val onError = { _: Throwable -> isOnErrorCalled = true } 50 | 51 | val viewModel = object : BaseViewModel(), RxViewModel { 52 | val property by maybe.toBindable(onError = onError) 53 | } 54 | 55 | maybe.onError(RuntimeException()) 56 | assert(isOnErrorCalled) 57 | } 58 | 59 | @Test 60 | fun `is onComplete called`() { 61 | val maybe: MaybeSubject = MaybeSubject.create() 62 | var isOnCompleteCalled = false 63 | val onComplete = { isOnCompleteCalled = true } 64 | 65 | val viewModel = object : BaseViewModel(), RxViewModel { 66 | val property by maybe.toBindable(onComplete = onComplete) 67 | } 68 | 69 | maybe.onComplete() 70 | assert(isOnCompleteCalled) 71 | } 72 | 73 | @Test 74 | fun `are new values received`() { 75 | val maybe: MaybeSubject = MaybeSubject.create() 76 | 77 | val viewModel = object : BaseViewModel(), RxViewModel { 78 | val property by maybe.toBindable(defaultValue = 3) 79 | } 80 | 81 | val newValue = 55 82 | maybe.onSuccess(newValue) 83 | assert(viewModel.property == newValue) 84 | } 85 | 86 | @Test 87 | fun `is notifyPropertyChanged() called (automatic field ID)`() { 88 | val maybe: MaybeSubject = MaybeSubject.create() 89 | 90 | val viewModel = ViewModelWithBindable(maybe) 91 | 92 | val propertyChangedCallback = TestPropertyChangedCallback() 93 | viewModel.addOnPropertyChangedCallback(propertyChangedCallback) 94 | 95 | val newValue = 55 96 | maybe.onSuccess(newValue) 97 | assert(BR.property in propertyChangedCallback.changedPropertyIds) 98 | } 99 | 100 | class ViewModelWithBindable(maybe: Maybe) : BaseViewModel(), RxViewModel { 101 | val property by maybe.toBindable(defaultValue = 3) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /rxjava2/src/test/java/de/trbnb/mvvmbase/rxjava2/test/MaybeBindingTests.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_VARIABLE", "unused") 2 | 3 | package de.trbnb.mvvmbase.rxjava2.test 4 | 5 | import de.trbnb.mvvmbase.Bindable 6 | import de.trbnb.mvvmbase.databinding.BaseViewModel 7 | import de.trbnb.mvvmbase.MvvmBase 8 | import de.trbnb.mvvmbase.databinding.initDataBinding 9 | import de.trbnb.mvvmbase.rxjava2.RxViewModel 10 | import io.reactivex.Maybe 11 | import io.reactivex.subjects.MaybeSubject 12 | import org.junit.jupiter.api.BeforeAll 13 | import org.junit.jupiter.api.Test 14 | 15 | class MaybeBindingTests { 16 | companion object { 17 | @BeforeAll 18 | @JvmStatic 19 | fun setup() { 20 | MvvmBase.initDataBinding().disableViewModelLifecycleThreadConstraints() 21 | } 22 | } 23 | 24 | @Test 25 | fun `is the given default value used`() { 26 | val maybe: Maybe = MaybeSubject.create() 27 | 28 | val viewModel = object : BaseViewModel(), RxViewModel { 29 | val property by maybe.toBindable(defaultValue = 3) 30 | } 31 | 32 | assert(viewModel.property == 3) 33 | } 34 | 35 | @Test 36 | fun `is null the default default value`() { 37 | val maybe: Maybe = MaybeSubject.create() 38 | 39 | val viewModel = object : BaseViewModel(), RxViewModel { 40 | val property by maybe.toBindable() 41 | } 42 | 43 | assert(viewModel.property == null) 44 | } 45 | 46 | @Test 47 | fun `is onError called`() { 48 | val maybe: MaybeSubject = MaybeSubject.create() 49 | var isOnErrorCalled = false 50 | val onError = { _: Throwable -> isOnErrorCalled = true } 51 | 52 | val viewModel = object : BaseViewModel(), RxViewModel { 53 | val property by maybe.toBindable(onError = onError) 54 | } 55 | 56 | maybe.onError(RuntimeException()) 57 | assert(isOnErrorCalled) 58 | } 59 | 60 | @Test 61 | fun `is onComplete called`() { 62 | val maybe: MaybeSubject = MaybeSubject.create() 63 | var isOnCompleteCalled = false 64 | val onComplete = { isOnCompleteCalled = true } 65 | 66 | val viewModel = object : BaseViewModel(), RxViewModel { 67 | val property by maybe.toBindable(onComplete = onComplete) 68 | } 69 | 70 | maybe.onComplete() 71 | assert(isOnCompleteCalled) 72 | } 73 | 74 | @Test 75 | fun `are new values received`() { 76 | val maybe: MaybeSubject = MaybeSubject.create() 77 | 78 | val viewModel = object : BaseViewModel(), RxViewModel { 79 | val property by maybe.toBindable(defaultValue = 3) 80 | } 81 | 82 | val newValue = 55 83 | maybe.onSuccess(newValue) 84 | assert(viewModel.property == newValue) 85 | } 86 | 87 | @Test 88 | fun `is notifyPropertyChanged() called (automatic field ID)`() { 89 | val maybe: MaybeSubject = MaybeSubject.create() 90 | 91 | val viewModel = ViewModelWithBindable(maybe) 92 | 93 | val propertyChangedCallback = TestPropertyChangedCallback() 94 | viewModel.addOnPropertyChangedCallback(propertyChangedCallback) 95 | 96 | val newValue = 55 97 | maybe.onSuccess(newValue) 98 | assert(BR.property in propertyChangedCallback.changedPropertyIds) 99 | } 100 | 101 | class ViewModelWithBindable(maybe: Maybe) : BaseViewModel(), RxViewModel { 102 | @get:Bindable 103 | val property by maybe.toBindable(defaultValue = 3) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /databinding/src/main/java/de/trbnb/mvvmbase/databinding/MvvmBindingActivity.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.databinding 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.CallSuper 5 | import androidx.annotation.LayoutRes 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.lifecycle.ViewModelLazy 10 | import de.trbnb.mvvmbase.databinding.utils.findGenericSuperclass 11 | import de.trbnb.mvvmbase.events.Event 12 | import de.trbnb.mvvmbase.events.addListener 13 | 14 | /** 15 | * Reference implementation of an [MvvmView] with [android.app.Activity]. 16 | * 17 | * This creates the binding and the ViewModel during [onCreate]. 18 | */ 19 | abstract class MvvmBindingActivity(@LayoutRes override val layoutId: Int) : AppCompatActivity(), MvvmView 20 | where VM : ViewModel, VM : androidx.lifecycle.ViewModel, B : ViewDataBinding { 21 | override lateinit var binding: B 22 | 23 | /** 24 | * Is called when the ViewModel sends an [Event]. 25 | * Will only call [onEvent]. 26 | * 27 | * @see onEvent 28 | */ 29 | private val eventListener: (Event) -> Unit = { event -> 30 | runOnUiThread { onEvent(event) } 31 | } 32 | 33 | @Suppress("LeakingThis") 34 | override val viewModelDelegate: Lazy = ViewModelLazy( 35 | viewModelClass = viewModelClass.kotlin, 36 | storeProducer = { viewModelStore }, 37 | factoryProducer = { defaultViewModelProviderFactory }, 38 | extrasProducer = { defaultViewModelCreationExtras } 39 | ) 40 | 41 | @Suppress("UNCHECKED_CAST") 42 | override val viewModelClass: Class 43 | get() = findGenericSuperclass>() 44 | ?.actualTypeArguments 45 | ?.firstOrNull() as? Class 46 | ?: throw IllegalStateException("viewModelClass does not equal Class") 47 | 48 | constructor() : this(0) 49 | 50 | /** 51 | * Called by the lifecycle. 52 | * Creates the [ViewDataBinding] and loads the view model. 53 | */ 54 | @Suppress("UNCHECKED_CAST") 55 | override fun onCreate(savedInstanceState: Bundle?) { 56 | super.onCreate(savedInstanceState) 57 | binding = initBinding() 58 | } 59 | 60 | /** 61 | * Calls [onViewModelLoaded]. This happens here and not in [onCreate] so that initializations can finish before event callbacks like 62 | * [onEvent] and [onViewModelPropertyChanged] are can access those initilaized components. 63 | */ 64 | override fun onPostCreate(savedInstanceState: Bundle?) { 65 | super.onPostCreate(savedInstanceState) 66 | onViewModelLoaded(viewModel) 67 | } 68 | 69 | /** 70 | * Creates the [ViewDataBinding]. 71 | * 72 | * @return The new [ViewDataBinding] instance that fits this Activity. 73 | */ 74 | private fun initBinding(): B { 75 | val binding: B = when (val dataBindingComponent = dataBindingComponent) { 76 | null -> DataBindingUtil.setContentView(this, layoutId) 77 | else -> DataBindingUtil.setContentView(this, layoutId, dataBindingComponent) 78 | } 79 | return binding.apply { 80 | lifecycleOwner = this@MvvmBindingActivity 81 | setVariable(viewModelBindingId, viewModel) 82 | viewModel.onBind() 83 | } 84 | } 85 | 86 | @CallSuper 87 | override fun onViewModelLoaded(viewModel: VM) { 88 | viewModel.eventChannel.addListener(this, eventListener) 89 | } 90 | 91 | @Suppress("EmptyFunctionBlock") 92 | override fun onEvent(event: Event) { 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/java/de/trbnb/mvvmbase/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.LifecycleEventObserver 5 | import androidx.lifecycle.LifecycleOwner 6 | import de.trbnb.mvvmbase.events.EventChannelOwner 7 | import de.trbnb.mvvmbase.events.addListener 8 | import de.trbnb.mvvmbase.observable.ObservableContainer 9 | import de.trbnb.mvvmbase.observableproperty.BeforeSet 10 | import de.trbnb.mvvmbase.observableproperty.ObservableProperty 11 | import de.trbnb.mvvmbase.utils.destroyAll 12 | 13 | /** 14 | * Base interface that defines basic functionality for all view models. 15 | * 16 | * It extends the [ObservableContainer] interface provided by the Android data binding library. This means 17 | * that implementations have to handle [OnPropertyChangedCallback]s.. 18 | */ 19 | interface ViewModel : ObservableContainer, LifecycleOwner, EventChannelOwner { 20 | /** 21 | * Is called when this instance is about to be removed from memory. 22 | * This means that this object is no longer bound to a view and will never be. It is about to 23 | * be garbage collected. 24 | * Implementations should provide a method to deregister from callbacks, etc. 25 | * 26 | * @see [BaseViewModel.onDestroy] 27 | */ 28 | fun destroy() 29 | 30 | /** 31 | * @see [androidx.lifecycle.ViewModel.getTag] 32 | */ 33 | operator fun get(key: String): T? 34 | 35 | /** 36 | * @see [androidx.lifecycle.ViewModel.setTagIfAbsent] 37 | */ 38 | fun initTag(key: String, newValue: T): T 39 | 40 | /** 41 | * Destroys all ViewModels in that list when the containing ViewModel is destroyed. 42 | */ 43 | fun > C.autoDestroy(): C = onEach { it.autoDestroy() } 44 | 45 | /** 46 | * Destroys the receiver ViewModel when the containing ViewModel is destroyed. 47 | */ 48 | fun VM.autoDestroy(): VM = also { child -> 49 | val parentLifecycleObserver = LifecycleEventObserver { _, event -> 50 | if (event == Lifecycle.Event.ON_DESTROY) { 51 | child.destroy() 52 | } 53 | }.also(this@ViewModel.lifecycle::addObserver) 54 | 55 | // If the child is destroyed for any reason it's listener to the parents lifecycle is removed to avoid leaks. 56 | child.lifecycle.addObserver(LifecycleEventObserver { _, event -> 57 | if (event == Lifecycle.Event.ON_DESTROY) { 58 | this@ViewModel.lifecycle.removeObserver(parentLifecycleObserver) 59 | } 60 | }) 61 | } 62 | 63 | /** 64 | * Sends all the events of a given list of (receiver type) ViewModels through the event channel of the ViewModel where this function is called in. 65 | */ 66 | fun > C.bindEvents(): C = onEach { it.bindEvents() } 67 | 68 | /** 69 | * Sends all the events of a given (receiver type) ViewModel through the event channel of the ViewModel where this function is called in. 70 | */ 71 | fun VM.bindEvents(): VM = also { child -> 72 | child.eventChannel.addListener(this@ViewModel) { event -> this@ViewModel.eventChannel.invoke(event) } 73 | } 74 | 75 | /** 76 | * Sets [ObservableProperty.beforeSet] to a given function and returns that instance. 77 | */ 78 | fun > ObservableProperty.Provider.asChildren(beforeSet: BeforeSet? = null) = apply { 79 | beforeSet { old, new -> 80 | old.destroyAll() 81 | new.autoDestroy().bindEvents() 82 | beforeSet?.invoke(old, new) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /coroutines/src/main/java/de/trbnb/mvvmbase/coroutines/flow/FlowBindable.kt: -------------------------------------------------------------------------------- 1 | package de.trbnb.mvvmbase.coroutines.flow 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.BindableProperty 7 | import de.trbnb.mvvmbase.databinding.bindableproperty.BindablePropertyBase 8 | import de.trbnb.mvvmbase.databinding.bindableproperty.Validate 9 | import de.trbnb.mvvmbase.databinding.utils.resolveFieldId 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.ExperimentalCoroutinesApi 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.catch 14 | import kotlinx.coroutines.flow.launchIn 15 | import kotlinx.coroutines.flow.onCompletion 16 | import kotlinx.coroutines.flow.onEach 17 | import kotlin.properties.ReadOnlyProperty 18 | import kotlin.reflect.KProperty 19 | 20 | /** 21 | * Bindable delegate property that collects the emitted values of a given [Flow] and uses them for [getValue]. 22 | * Uses `defaultValue` if no value has been emitted. 23 | */ 24 | @ExperimentalCoroutinesApi 25 | class FlowBindable private constructor( 26 | private val viewModel: ViewModel, 27 | private val fieldId: Int, 28 | defaultValue: T, 29 | flow: Flow, 30 | onException: OnException?, 31 | onCompletion: OnCompletion?, 32 | coroutineScope: CoroutineScope, 33 | distinct: Boolean, 34 | afterSet: AfterSet?, 35 | beforeSet: BeforeSet?, 36 | validate: Validate? 37 | ) : BindablePropertyBase(distinct, afterSet, beforeSet, validate), ReadOnlyProperty { 38 | private var value: T = defaultValue 39 | set(value) { 40 | if (distinct && value === field) return 41 | 42 | val oldValue = field 43 | beforeSet?.invoke(oldValue, value) 44 | field = when (val validate = validate) { 45 | null -> value 46 | else -> validate(oldValue, value) 47 | } 48 | 49 | viewModel.notifyPropertyChanged(fieldId) 50 | afterSet?.invoke(oldValue, field) 51 | } 52 | 53 | init { 54 | flow.onEach { value = it } 55 | .run { onCompletion(onCompletion ?: return@run this) } 56 | .run { catch(onException ?: return@run this) } 57 | .launchIn(coroutineScope) 58 | } 59 | 60 | override fun getValue(thisRef: ViewModel, property: KProperty<*>): T = value 61 | 62 | /** 63 | * Property delegate provider for [FlowBindable]. 64 | * Needed so that reflection via [KProperty] is only necessary once, during delegate initialization. 65 | * 66 | * @see BindableProperty 67 | */ 68 | class Provider( 69 | private val flow: Flow, 70 | private val onException: OnException?, 71 | private val onCompletion: OnCompletion?, 72 | private val coroutineScope: CoroutineScope, 73 | private val defaultValue: T 74 | ) : BindablePropertyBase.Provider() { 75 | override operator fun provideDelegate(thisRef: ViewModel, property: KProperty<*>) = FlowBindable( 76 | viewModel = thisRef, 77 | flow = flow, 78 | fieldId = property.resolveFieldId(), 79 | onException = onException, 80 | onCompletion = onCompletion, 81 | coroutineScope = coroutineScope, 82 | defaultValue = defaultValue, 83 | distinct = distinct, 84 | afterSet = afterSet, 85 | beforeSet = beforeSet, 86 | validate = validate 87 | ) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /databinding/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 | consumerProguardFile("proguard-rules.pro") 18 | } 19 | 20 | buildTypes { 21 | named("release").configure { 22 | isMinifyEnabled = false 23 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 24 | } 25 | } 26 | 27 | buildFeatures { 28 | dataBinding = true 29 | compose = true 30 | } 31 | 32 | composeOptions { 33 | kotlinCompilerExtensionVersion = Versions.composeCompiler 34 | } 35 | 36 | dataBinding { 37 | // This is necessary to allow the data binding annotation processor to generate 38 | // the BR fields from Bindable annotations 39 | testOptions.unitTests.isIncludeAndroidResources = true 40 | 41 | isEnabledForTests = true 42 | } 43 | 44 | compileOptions { 45 | sourceCompatibility = Versions.java 46 | targetCompatibility = Versions.java 47 | } 48 | 49 | kotlin { 50 | explicitApi() 51 | } 52 | 53 | kotlinOptions { 54 | jvmTarget = Versions.java.toString() 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation(fileTree("dir" to "libs", "include" to listOf("*.jar"))) 60 | 61 | // Support library 62 | implementation("androidx.appcompat:appcompat:1.5.1") 63 | implementation("com.google.android.material:material:1.6.1") 64 | implementation("androidx.fragment:fragment-ktx:1.5.3") 65 | implementation("androidx.recyclerview:recyclerview:1.2.1") 66 | 67 | testAnnotationProcessor("androidx.databinding:databinding-compiler:${Versions.gradleTools}") 68 | kaptTest("androidx.databinding:databinding-compiler:${Versions.gradleTools}") 69 | 70 | implementation("androidx.compose.runtime:runtime:${Versions.compose}") 71 | implementation("androidx.compose.ui:ui:${Versions.compose}") 72 | implementation("androidx.compose.ui:ui-viewbinding:1.2.1") 73 | 74 | // Testing 75 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0") 76 | androidTestImplementation("androidx.test:runner:1.4.0") 77 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 78 | 79 | // Kotlin 80 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}") 81 | implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}") 82 | 83 | api(project(":core")) 84 | 85 | // Lifecycle architecture components 86 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") 87 | api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1") 88 | 89 | // Java inject API for dependency injection 90 | api("javax.inject:javax.inject:1") 91 | } 92 | 93 | repositories { 94 | mavenCentral() 95 | google() 96 | } 97 | 98 | val sourcesJar = task("sourcesJar") { 99 | archiveClassifier.set("sources") 100 | from(android.sourceSets["main"].java.srcDirs) 101 | } 102 | 103 | signing { 104 | sign(publishing.publications) 105 | } 106 | 107 | afterEvaluate { 108 | publishing { 109 | repositories { 110 | mavenCentralUpload(project) 111 | } 112 | publications { 113 | create(Publication.DATABINDING, this@afterEvaluate) 114 | } 115 | } 116 | } 117 | --------------------------------------------------------------------------------