How to add paramaters to new livedata builder in the view model - mvvm

I'm trying to convert my view models to use the new livedata builder from live data2.0
In all the examples i've seen when you use this new builder pattern they omit how to set parameters take this view model for example, userId is not defined
class UserViewModel : ViewModel() {
private val repository = UserRepository()
val user: LiveData<Response<User>> = liveData {
val data = repository.getUser(userId) // loadUser is a suspend function.
emit(data)
}
}
looks pretty clean on concise, but where do I set the userId its not a function.
before i would expose a function that took a paramater then would update the livedata property.
I was thinking of something like this
class UserViewModel : ViewModel() {
private val repository = UserRepository()
var userId : String? = null
val user: LiveData<Response<User>> = liveData {
val data = repository.getUser(userId) // loadUser is a suspend function.
emit(data)
}
}
and the fragment can set the id, but what if I change the id and want to make another network call?

Related

android mvvm and livedata: list not observed in activity

I am developing an android app that has an activity with a recyclerview and an add button. When add button is clicked, a FragmentDialog is launched. the user enters the name of the book which will be stored in a greendao database. The list of books is displayed in the recyclerview.
I am using mvvm and livedata . the problem is that after adding the book in DialogFragment, the list is not updated, although the list is wrapped in livedata which is observed in activity.
DialogFragment:
// repository
val repository = Injection.provideRepository()
// viewmodel
val factory = CreateBookViewModelFactory(repository)
val viewModel =
ViewModelProvider(this, factory).get(BookViewModel::class.java)
builder.setView(binding.root)
// Add action buttons
.setPositiveButton(R.string.add,
DialogInterface.OnClickListener { dialog, id ->
// create book
book = Book(null, binding.name)
viewModel.onAddBook(book)
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener { dialog, id ->
getDialog()?.cancel()
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
viewModel:
class BookViewModel(val repository: Repository) : ViewModel() {
// Create a LiveData
val _books = MutableLiveData<List<Book>>()
val books = repository.books
fun init() :MutableLiveData<List<Book>> {
_books.postValue(books)
return _books
}
}
activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_books)
binding.setLifecycleOwner(this)
binding.recyclerview.adapter = adapter
binding.recyclerview.setHasFixedSize(false)
// repository
val repository = Injection.provideRepository()
val factory = CreateBookViewModelFactory(repository)
val viewModel =
ViewModelProvider(this, factory).get(BookViewModel::class.java)
viewModel.init().observe(this, Observer {
it?.let {
adapter.submitList(Mapper.mapToBookListDTO(it))
}
})
binding.addReturn.setOnClickListener { view ->
val createFragment: DialogFragment = BookDialog()
createFragment.show(supportFragmentManager, "new Book")
}
}
When I add a new book and click the add button, the list is not updated. But when I reopen the activity the list of books get updated, it means the list is not observed for changes.
I am using the same viewModel for the activity and the DialogFragment. I also tried using seperate viewmodels for each, but same result.
Thanks a lot for your help!

How to inject a ViewModel into a composable function using Hilt (Jetpack Compose)

I'm doing the same as shown in the documentation here.
I want to Inject the ViewModel into a Composable function (Screen), but I get this error:
Cannot create an instance of class
com.example.blotube.ui.later.LaterViewModel
My ViewModel:
#HiltViewModel
class LaterViewModel #Inject constructor(
private val database: Database
):ViewModel() {
val watchLater=database.videos().getAll()
}
My Composable Function (Screen):
#Composable
fun WatchLater(vm: LaterViewModel = viewModel()){
val videos=vm.watchLater.observeAsState()
val context= LocalContext.current
}
From version androidx.hilt:hilt-navigation-compose:1.0.0-alpha02
you can inject view model into Composable functions by:
hiltViewModel<ViewModelType>()
Example:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {}
LoginScreen(
viewModel = hiltViewModel<LoginViewModel>()
)
Android Developer Documentation compose and hilt
UPDATE:
import androidx.hilt.navigation.compose.hiltViewModel
#Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel()
){
val videos=vm.watchLater.observeAsState()
val context= LocalContext.current
}
I find the simplest way to do it is inside your composable function.
Add the dependency implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' then;
#Composable
fun Foo(){
val viewModel : Bar = hiltViewModel()
}
then you can use the viewmodel as usual.
You can use ViewModel directly inside Composable function via hiltViewModel()
#Composable
fun WatchLater(vm: LaterViewModel = hiltViewModel()) {
val videos = vm.watchLater.observeAsState()
val context = LocalContext.current
}
Please make sure to add following
Adding androidx.hilt:hilt-navigation-compose dependency to your module level Gradle file. Do check for latest version(tested on 1.0.0-alpha03).
#HiltViewModel to your ViewModel.
#AndroidEntryPoint for the owner using the Composable function.
This appears to be a bug in Jetpack Compose, will probably need to wait for an update on the Jetpack libraries to address it.
As a possible workaround, you could instantiate the viewmodel in your activity and pass it to your composable function
val viewModel: LaterViewModel = viewModel(
"later_viewmodel",
factory = defaultViewModelProviderFactory
)
WatchLater(viewModel)
if you are using the Nav graph component you can also scope your viewmodel to the nav graph using
val viewModel: LaterViewModel = hiltNavGraphViewModel<LaterViewModel>()
WatchLater(viewModel)
base document Inject Hilt In Composable Function
Note: notice to import class
Sample you can used viewModel()
Example:
...
import androidx.lifecycle.viewmodel.compose.viewModel
...
#Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) { /* ... */ }
Full Example:
...
import androidx.lifecycle.viewmodel.compose.viewModel
...
#HiltViewModel
class MyViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() { /* ... */ }
#Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) { /* ... */ }

MVVM without DataBinding

I started learning Kotlin to develop Android applications and for my first project I have to use MVVM as pattern to separate the presentation layer from the logic. All the articles I've found about MVVM use DataBinding library to bind the data of the models directly to the xml views. I worked a little with DataBinding in Java but I doesn't like it because it is very difficult to find errors when binding is wrong. My questions is if there is another way to use MVVM without DataBinding?
Sure it is. Just abstract example: imagine ViewModel:
class SomeViewModel : ViewModel() {
private lateinit var roadmap: RoleScreenRoadmap
private lateinit var uiScope: CoroutineScope
private val _nameRequired = MutableLiveData<Boolean>()
private val _userName = MutableLiveData<String>()
val nameRequired: LiveData<Boolean>
get() = _nameRequired
val userName: LiveData<String>
get() = _userName
...
}
And now Activity:
class AbstractActivity : AppCompatActivity() {
private lateinit var viewModel: RoleScreenViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_some)
initializeDependencies()
performBindings()
}
private fun initializeDependencies() {
viewModel = ViewModelProviders.of(this).get(AbstractViewModel::class.java)
}
private fun performBindings() {
val lifecycle = ::getLifecycle
viewModel.nameRequired.observe(lifecycle) { nameRequired: Boolean? ->
if (nameRequired!!) {
showNameInputWindow()
}
}
viewModel.userName.observe(lifecycle, ::setTitle)
}
...
}
You can just subscribe during android's lifecycle component initialization and no databinding is ever needed.

How to signal/notify super-controller of change in sub-controller?

In JavaFX, how do you model the following:
I show a List of Customers in a Scene. On the left side there is a table on the right side (contentPane) the currently select customer's details are shown.
(Relevant part of) Main-Controller:
#jfxf.FXML
protected def newButtonPressed(): Unit =
{
contentPane.getChildren.clear
contentPane.getChildren.add(FXMLLoader.load(GUILoader.load("customers/form.fxml")))
}
There is a Button to add a new Customer. Upon clicking this button instead of opening a Popup, I replace the "details"-part of the scene and add a form there.
Now for this form (designed - like the rest of the GUI - in the SceneBuilder and saved as .fxml) I use another controller.
class Form extends Main with jfxf.Initializable
{
#jfxf.FXML
private var foreNameTextField: jfxsc.TextField = _
#jfxf.FXML
private var lastNameTextField: jfxsc.TextField = _
#jfxf.FXML
private var ageTextField: jfxsc.TextField = _
override def initialize(url: URL, resourceBundle: ResourceBundle): Unit =
{
}
#jfxf.FXML
protected def ok(): Unit =
{
// TODO validation
val newPerson = new Person(-1, foreNameTextField.getText, lastNameTextField.getText, ageTextField.getText.toInt)
// Save to DB
// Close whole form
}
}
When I'm done with filling in a new customer's detail I click on another button (that calls ok()) and save it to a database.
What I want to do now is close the form and replace it with the detail-form.
Something like calling a protected method in the main-controller like:
protected def clearDetails(): Unit =
{
contentPane.getChildren.clear
contentPane.getChildren.add(savedOldDetails)
}
won't work of course. (Will throw a runtime-exception because there is no contentpane in the sub/form-controller (even if I make it protected)
In Qt (C++) I'd use signals/slots and connect them accordingly.
Seems like in JavaFX there is nothing the like. How am I supposed to share such information?
Do I need to create a "super-controller" for the contentPane?
(I don't know Scala, so I'll write this in Java, hope that is ok).
You can define an observable property in the Form controller, and observe it when you load the form from the main controller:
public class Form implements Initializable {
private final ObjectProperty<Person> person = new SimpleObjectProperty<>(null);
public ObjectProperty<Person> personProperty() {
return person ;
}
public final Person getPerson() {
return personProperty().get();
}
public final void setPerson(Person person) {
personProperty().set(person);
}
// ... code you had before...
#FXML
protected void ok() {
Person person = new Person(-1, foreNameTextField.getText(), lastNameTextField.getText(), ageTextField.getText());
// save to DB...
personProperty().set(person);
}
}
Now in your main controller, load the form as follows:
#FXML
protected void newButtonPressed() {
contentPane.getChildren().clear();
FXMLLoader loader = new FXMLLoader(getClass().getResource("customers/form.fxml"));
Parent form = loader.load();
Form formController = loader.getController();
formController.personProperty().addListener((obs, oldPerson, newPerson) {
if (newPerson != null) {
// clear form, etc, e.g.
clearDetails();
}
});
contentPane.getChildren().add(form);
}

Add filters to LiveGrid?

I am using gxt's LiveGrid. I want to add filters. I have added below few lines but data is not filtered. Am I missing any thing here?
GridFilters filters = new GridFilters();
filters.setLocal(true);
StringFilter nameFilter = new StringFilter("column name");
filters.addFilter(nameFilter);
filters.init(liveGrid);
liveGrid.addPlugin(filters);
From the filter javadoc :
To add a filter to a Grid column, create an instance of a concrete subclass of Filter, passing to the constructor the ValueProvider for the column, then add the filter to a GridFilters
Your code sample seems too restrinct and should probably be parameterized. The StringFilter should be given a ValueProvider for the property of the model object you wish to filter. Following is a simple overview of how to create a Grid with Filters.
Let's say you have a class User
public class User implements Serializable {
private String name;
private Integer id;
// Setters and getters
}
public interface UserProperties extends PropertyAccess<User> {
#Path("id")
ModelKeyProvider<User> key();
ValueProvider<User, String> name();
}
To create a grid that will display your users, you would do as follow
private static final UserProperties props = GWT.create(UserProperties.class);
...
// Create column config
ColumnConfig<User, String> nameCol = new ColumnConfig<User, String>(props.name(), 200, "Name");
// Create column model
List<ColumnConfig<User, ?>> l = new ArrayList<ColumnConfig<User, ?>>();
l.add(nameCol);
ColumnModel<User> cm = new ColumnModel<User>(l);
// Create User store
ListStore<User> store = new ListStore<User>(props.key());
// Create your grid
final LiveGridView<User> liveGridView = new LiveGridView<User>();
liveGridView.setForceFit(true);
Grid<User> view = new Grid<User>(store, cm) {
#Override
protected void onAfterFirstAttach() {
super.onAfterFirstAttach();
// Get grid data
}
};
// Create a String filter for the column
StringFilter<User> nameFilter = new StringFilter<User>(props.name());
// Create a GridFilters
GridFilters<User> filters = new GridFilters<User>();
filters.initPlugin(grid);
filters.setLocal(true);
filters.addFilter(nameFilter);
GridFilters filters = new GridFilters();
filters.setLocal(true);
StringFilter nameFilter = new StringFilter("column name");
filters.addFilter(nameFilter);
filters.init(liveGrid);
liveGrid.addPlugin(filters);
If u want to get the data after filter applied u need to overide reload() method of AbstractGridFilters.