Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
Merge branch 'release/2.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
trbnb committed Feb 24, 2021
2 parents ca87f2e + 4bc25a7 commit 7b080a1
Show file tree
Hide file tree
Showing 76 changed files with 676 additions and 330 deletions.
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ MvvmBase is available via JCenter. To use it put this in your `build.gradle`:

```gradle
dependencies {
// Always needed, library depends on ViewModelLazy
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// Necessary for SavedStateHandle
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"
def mvvmbaseVersion = "2.0.0"
def mvvmbaseVersion = "2.1.0"
[...]
implementation de.trbnb.mvvmbase:mvvmbase:$mvvmbaseVersion"
Expand Down Expand Up @@ -49,8 +44,15 @@ The library also uses some Kotlin reflection features. The AAR is exported with

# Features
This library comes with two major features:
* `MvvmActivity<VM>` / `MvvmFragment<VM>`
View components that have a `ViewModel` instance associated with them and keep it alive during the entire lifecycle.
* `MvvmView<VM, B>`
Interface for MVVM view components that have a `ViewModel` instance associated with them.
Included implementations are:
- `MvvmBindingActivity`
- `MvvmBindingFragment`
- `MvvmBindingDialogFragment`
- `MvvmBindingBottomSheetDialogFragment`

All of those come with a typealias that omits the "Binding" part from the name and the "`B`" type parameter (uses `ViewDataBinding`).

* `BindableProperty<T>`
A delegate property that calls `notfiyPropertyChanged(Int)` and finds the field ID in `BR.java` automatically.
Expand Down Expand Up @@ -120,7 +122,18 @@ It is also recommended to migrate from a `BaseViewModel` to a `BaseStateSavingVi
This will be called when the view model is "loaded". This is called after the view model has been created or retained in `onCreate`. The loading of the view model may not be synchronous, so don't try to access it in `onCreate`.

* `onViewModelPropertyChanged(viewModel: VM, fieldId: Int)`
This is called when the view model calls `notifyPropertyChanged`. The associated view model and the field ID that was used to call `notifyPropertyChanged` will be passed.
This is called when the view model calls `notifyPropertyChanged`. The associated view model and the field ID that was used to call `notifyPropertyChanged` will be passed.
**NOTE: This method is likely to be deprecated in the future.**
Use `KProperty0<T>.observe()` instead:
```kotlin
override fun onViewModelLoaded(viewModel: MainViewModel) {
super.onViewModelLoaded(viewModel)

viewModel::someProperty.observe { value ->
// Do something with new value
}
}
```

### ViewModel

Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ dependencies {

// Support library
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("com.google.android.material:material:1.1.0")
implementation("com.google.android.material:material:1.3.0")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
Expand Down
117 changes: 23 additions & 94 deletions app/src/main/java/de/trbnb/apptemplate/main/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package de.trbnb.apptemplate.main

import android.app.AlertDialog
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
Expand All @@ -17,16 +13,11 @@ import de.trbnb.apptemplate.list.ListActivity
import de.trbnb.apptemplate.resource.ResourceProviderImpl
import de.trbnb.apptemplate.second.SecondActivity
import de.trbnb.apptemplate.second.SecondController
import de.trbnb.mvvmbase.BR
import de.trbnb.mvvmbase.MvvmActivity
import de.trbnb.mvvmbase.events.Event
import de.trbnb.mvvmbase.viewmodel.viewModelProviderFactory

class MainActivity : MvvmActivity<MainViewModel>() {
private var dialog: Dialog? = null
private var snackbar: Snackbar? = null

override val layoutId: Int = R.layout.activity_main

class MainActivity : MvvmActivity<MainViewModel>(R.layout.activity_main) {
private lateinit var router: Router

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -38,94 +29,42 @@ class MainActivity : MvvmActivity<MainViewModel>() {
override fun onViewModelLoaded(viewModel: MainViewModel) {
super.onViewModelLoaded(viewModel)

// now that the view-model is loaded, we know whether or not we should know if we should
// show the dialog and snackbar
arrayOf(BR.showingDialog, BR.showSnackbar).forEach { onViewModelPropertyChanged(viewModel, it) }
}

/**
* will be called whenever the view-model calls notifyPropertyChanged
*/
override fun onViewModelPropertyChanged(viewModel: MainViewModel, fieldId: Int) {
when (fieldId) {
BR.showingDialog -> if (viewModel.isShowingDialog) showDialog() else dismissDialog()
BR.showSnackbar -> if (viewModel.showSnackbar) showSnackbar() else dismissSnackbar()
viewModel::textInput.observe { textInput ->
supportActionBar?.subtitle = textInput
}
}

override fun onDestroy() {
super.onDestroy()

// Dialogs should be dismissed in onDestroy so the window won't be leaked.
// This means we don't want to change the state in the view-model here.

// We dismiss and don't cancel here because we don't want to trigger the OnCancelListener that
// is attached to the dialog.
dialog?.dismiss()
}

/**
* create a new Dialog and show it
*/
private fun showDialog() {
dialog = AlertDialog.Builder(this)
.setTitle("Dialog title")
.setMessage("This is a sample dialog to show how to create a dialog via binding.")
.setOnCancelListener {
// Will only be called when the Dialog is canceled, not dismissed.
// This also changes the state in the view-model, so the dialog won't be shown
// again after rotation.
viewModel.isShowingDialog = false
}
.setPositiveButton(android.R.string.ok) { d, _ ->
// We cancel the Dialog here because the user got rid of it.
d.cancel()
}
.show()
}

/**
* dismiss a Dialog if one exists
*/
private fun dismissDialog() {
dialog?.dismiss()
dialog = null
AlertDialog.Builder(this)
.setTitle("Dialog title")
.setMessage("This is a sample dialog to show how to create a dialog via binding.")
.setPositiveButton(android.R.string.ok) { d, _ -> d.cancel() }
.show()
}

/**
* create a new Snackbar and show it
*/
private fun showSnackbar() {
snackbar = Snackbar.make(findViewById(android.R.id.content), "This is a sample Snackbar made with binding.", Snackbar.LENGTH_LONG).apply {

// if the snackbar is dismissed we want to update the state in the view-model
addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
// A Snackbar will dismiss itself if the containing Activity is destroyed.
// Because we don't want to change the state in the view-model we just return in that case.
if (isDestroyed) {
return
}

viewModel.showSnackbar = false
}
})

setAction("Hide") { viewModel.showSnackbar = false }

show()
}
private fun showSnackbar(text: String) {
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_LONG).apply {
setAction("Hide") { dismiss() }
}.show()
}

override fun onEvent(event: Event) {
super.onEvent(event)

when (event as? MainEvent ?: return) {
is MainEvent.ShowToast -> Toast.makeText(this, "Toast message!", Toast.LENGTH_SHORT).show()
is MainEvent.ShowMainActivityAgainEvent -> startActivity(Intent(this, MainActivity::class.java))
is MainEvent.ShowSecondActivityEvent -> startActivity(Intent(this, SecondActivity::class.java))
is MainEvent.ShowConductorEvent -> router.pushController(RouterTransaction.with(SecondController()))
is MainEvent.ShowListEvent -> startActivity(Intent(this, ListActivity::class.java))
when (event) {
MainEvent.ShowToast -> Toast.makeText(this, "Toast message!", Toast.LENGTH_SHORT).show()
MainEvent.ShowMainActivityAgainEvent -> startActivity(Intent(this, MainActivity::class.java))
MainEvent.ShowSecondActivityEvent -> startActivity(Intent(this, SecondActivity::class.java))
MainEvent.ShowConductorEvent -> router.pushController(RouterTransaction.with(SecondController()))
MainEvent.ShowListEvent -> startActivity(Intent(this, ListActivity::class.java))
MainEvent.ShowDialog -> showDialog()
is MainEvent.ShowSnackbar -> showSnackbar(event.text)
}
}

Expand All @@ -135,17 +74,7 @@ class MainActivity : MvvmActivity<MainViewModel>() {
}
}

/**
* dismiss a Snackbar if one exists
*/
private fun dismissSnackbar() {
snackbar?.dismiss()
snackbar = null
}

override fun getDefaultViewModelProviderFactory() = object : AbstractSavedStateViewModelFactory(this, intent?.extras) {
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
return MainViewModel(handle, ResourceProviderImpl(this@MainActivity)) as T
}
override fun getDefaultViewModelProviderFactory() = viewModelProviderFactory { handle ->
MainViewModel(handle, ResourceProviderImpl(this@MainActivity))
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/de/trbnb/apptemplate/main/MainEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ sealed class MainEvent : Event {
object ShowMainActivityAgainEvent : MainEvent()
object ShowConductorEvent : MainEvent()
object ShowListEvent : MainEvent()
object ShowDialog : MainEvent()
class ShowSnackbar(val text: String) : MainEvent()
}
22 changes: 6 additions & 16 deletions app/src/main/java/de/trbnb/apptemplate/main/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import androidx.databinding.Bindable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import de.trbnb.apptemplate.resource.ResourceProvider
import de.trbnb.mvvmbase.bindableproperty.bindableBoolean
import de.trbnb.mvvmbase.commands.ruleCommand
import de.trbnb.mvvmbase.bindableproperty.bindable
import de.trbnb.mvvmbase.bindableproperty.distinct
import de.trbnb.mvvmbase.commands.simpleCommand
import de.trbnb.mvvmbase.rxjava2.RxViewModel
import de.trbnb.mvvmbase.savedstate.BaseStateSavingViewModel
Expand All @@ -18,10 +18,8 @@ class MainViewModel(
resourceProvider: ResourceProvider
) : BaseStateSavingViewModel(savedStateHandle), RxViewModel {
@get:Bindable
var isShowingDialog by bindableBoolean(false)

@get:Bindable
var showSnackbar: Boolean by bindableBoolean(false)
var textInput by bindable("")
.distinct()

@get:Bindable
val title: String by Observable.create<String> { emitter ->
Expand All @@ -31,17 +29,9 @@ class MainViewModel(
}
}.toBindable(defaultValue = "foo")

val showDialogCommand = ruleCommand(
enabledRule = { !isShowingDialog },
action = { isShowingDialog = true },
dependentFields = listOf(::isShowingDialog)
)
val showDialogCommand = simpleCommand { eventChannel(MainEvent.ShowDialog) }

val showSnackbarCommand = ruleCommand(
action = { showSnackbar = true },
enabledRule = { !showSnackbar },
dependentFields = listOf(::showSnackbar)
)
val showSnackbarCommand = simpleCommand { eventChannel(MainEvent.ShowSnackbar("This is a sample Snackbar made with binding.")) }

val showToastCommand = simpleCommand {
eventChannel(MainEvent.ShowToast)
Expand Down
14 changes: 4 additions & 10 deletions app/src/main/java/de/trbnb/apptemplate/second/SecondController.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
package de.trbnb.apptemplate.second

import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import de.trbnb.apptemplate.R
import de.trbnb.apptemplate.resource.ResourceProviderImpl
import de.trbnb.mvvmbase.conductor.MvvmController
import de.trbnb.mvvmbase.conductor.viewModelProviderFactory

class SecondController : MvvmController<SecondViewModel>() {
override val layoutId: Int = R.layout.fragment_second

override val defaultViewModelProviderFactory = object : AbstractSavedStateViewModelFactory(this, defaultViewModelArgs) {
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
return SecondViewModel(handle, ResourceProviderImpl(activity ?: throw IllegalStateException())) as T
}
class SecondController : MvvmController<SecondViewModel>(layoutId = R.layout.fragment_second) {
override fun getDefaultViewModelProviderFactory() = viewModelProviderFactory { handle ->
SecondViewModel(handle, ResourceProviderImpl(activity ?: throw IllegalStateException()))
}
}
16 changes: 5 additions & 11 deletions app/src/main/java/de/trbnb/apptemplate/second/SecondFragment.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
package de.trbnb.apptemplate.second

import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import de.trbnb.apptemplate.R
import de.trbnb.apptemplate.resource.ResourceProviderImpl
import de.trbnb.mvvmbase.MvvmFragment
import de.trbnb.mvvmbase.viewmodel.viewModelProviderFactory

class SecondFragment : MvvmFragment<SecondViewModel>() {
override val layoutId: Int = R.layout.fragment_second
class SecondFragment : MvvmFragment<SecondViewModel>(R.layout.fragment_second) {
override val viewModelDelegate = activityViewModels<SecondViewModel> { defaultViewModelProviderFactory }

override val viewModelDelegate = activityViewModels<SecondViewModel>()

override fun getDefaultViewModelProviderFactory() = object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
return SecondViewModel(handle, ResourceProviderImpl(context ?: throw IllegalStateException())) as T
}
override fun getDefaultViewModelProviderFactory() = viewModelProviderFactory { handle ->
SecondViewModel(handle, ResourceProviderImpl(context ?: throw IllegalStateException()))
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="Foo bar"/>

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={vm.textInput}"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Expand Down
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath(group = "com.android.tools.build", name = "gradle", version = gradleToolsVersion)
classpath(group = "com.android.tools.build", name = "gradle", version = "4.1.1")
classpath(kotlin("gradle-plugin", version = kotlinVersion))

// NOTE: Do not place your application dependencies here; they belong
Expand All @@ -15,7 +15,7 @@ buildscript {

plugins {
id("com.jfrog.bintray") version "1.8.5" // jCenter
kotlin("jvm") version "1.3.72"
kotlin("jvm") version kotlinVersion
id("io.gitlab.arturbosch.detekt") version "1.9.1"
id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
}
Expand Down
8 changes: 4 additions & 4 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.extra

const val kotlinVersion = "1.3.72"
const val kotlinVersion = "1.4.21"

const val gradleToolsVersion = "4.0.1"
const val gradleToolsVersion = "4.1.1"

val javaVersion = JavaVersion.VERSION_1_8

Expand All @@ -18,8 +18,8 @@ object Android {
}

object Publishing {
const val versionName = "2.0.1"
const val versionCode = 41
const val versionName = "2.1.0"
const val versionCode = 42

const val url = "https://github.com/trbnb/MvvmBase"
const val gitUrl = "https://github.com/trbnb/MvvmBase.git"
Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Tue May 26 18:07:17 CEST 2020
#Tue Feb 16 14:42:44 CET 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
Loading

0 comments on commit 7b080a1

Please sign in to comment.