How to update activity views from a fragment recyclerView - android-activity

I have MainActivity which has AppBar (with a "cart" TextView) and FrameLayout (with Fragment which contain a RecyclerView). This RecyclerView has a Button to increase Cart Value count. And I want that count to update Cart value in MainActivity.
I found this one but without full explanation "Updating views of Activity or Fragment from a RecyclerView adapter".

No better solution than this ViewModel
First of all add this dependency in build.gradle : implementation 'android.arch.lifecycle:extensions:1.1.1'
Your ViewModel class will look like this :
class MyViewModel: ViewModel() {
val mCartInfo: MutableLiveData<Int> = MutableLiveData()
private var cnt = 0
fun addToCartCounter() {
//TODO Apply your logic here
mCartInfo.value = cnt++
}
}
What is LiveData? See here
After that you've to initialize your ViewModel and observe the changed data, all these things are going to be done in your AppBar's Activity :
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
//These will observe any changes happen in ViewModel's MutableLiveData(it'll directly jump inside Observer)
//Observer is main important thing
myViewModel.mCartInfo.observe(this, Observer {
//tvCartCounter is Toolbar's TextView
tvCartCounter.text = it.toString()
})
}
}
Get ViewModel in your Fragment and your button click which calls a ViewModel's method:
class BlankFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val rootView = inflater.inflate(R.layout.fragment_blank, container, false)
//Get your Activity's ViewModel
val myViewModel = ViewModelProviders.of(requireActivity()).get(MyViewModel::class.java)
//Button click
rootView.btnAddToCart.setOnClickListener {
//It'll call ViewModel's method
myViewModel.addToCartCounter()
}
return rootView
}
}
Whenever addToCartCounter() calls on your button click it'll add value in MutableLiveData soon after it'll triggered your observer.

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 do I pass a function from a ViewModel to my scaffold's floating action button in Jetpack Compose?

I'm building an Android app using purely Jetpack Compose. My entire application is wrapped under one scaffold, and has a ViewModel for each "screen" (which are composables) in my app. Because of that, I have some conditional statements in my scaffold to determine the floating action button (FAB) based on the route. However, one of the FABs needs access to a function in a ViewModel, which is only created when I navigate to the route that holds that composable, and I'm at a loss on the best way to give the FAB access to that viewmodel function.
Take the following example (based off my code), and note the FAB for the route "route3".
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
#Composable
fun MyApp() {
val navController = rememberNavController()
val backstackEntry = navController.currentBackStackEntryAsState()
val scaffoldState = rememberScaffoldState()
Surface(color = MaterialTheme.colors.background) {
Scaffold(
topBar = { ... },
bottomBar = { ... },
floatingActionButton = {
when (backstackEntry.value?.destination?.route) {
"route2" -> FAB2(navController)
"route3" -> FAB3(navController) // Needs to access function from viewModel3
}
},
scaffoldState = scaffoldState,
) {
MyNavHost(navController, scaffoldState)
}
}
}
#Composable
fun MyNavHost(navController: NavHostController, scaffoldState: ScaffoldState) {
NavHost(
navController = navController,
startDestination = "route1"
) {
composable("route1") { Destination1() }
composable("route2") { Destination2() }
composable("route3") { Destination3() }
}
}
#Composable
fun Destination1() { ...}
#Composable
fun Destination2() { ... }
#Composable
fun Destination3() {
val viewModel3: CustomViewModel = hiltViewModel()
Screen3(viewModel3)
}
}
So my main question is, if the FAB3 variable needs to access a function from viewModel3, how would I go about doing it?
I decided to just switch over to using a scaffold for every screen.
Honestly, this makes managing routes a lot easier, because in the single scaffold scenario, it was getting difficult managing all of the possible routes for things like the TopBar and FAB in large when() blocks.
But if anyone has a solution to the original question that would be appreciated!

Change a ViewModel Property from a different class and update the View - MVVM

I need to change the Visibility of a Button on a View from method call from within a class.
I have tried accessing the VeiwModel by exposing it in the class, and then had success in changing the Property "ShowRedHat" from true to false, but this does not update the Visibility of the Button in the View. This also double loads the ViewModel, which is not acceptable in my solution.
Any help is appreciated.
The class:
public class HatEngine
{
public void SetShowRedHat()
{
????.ShowRedHat = false;
}
}
The Property in the ViewModel:
public class MyViewModel : ObservableObject
{
private bool _showRedHat;
public bool ShowRedHat
{
get { return _showRedHat; }
set
{
OnPropertyChanged(ref _showRedHat, value);
}
}
}
The Button in the View:
<Button Content="Red Hat"
Command="{Binding RedHatCommand}"
Visibility="{Binding ShowRedHat, Converter={StaticResource BoolToVis}}"/>
If the purpose of HatEngine is to be a service that is used by MyViewModel, then something like the following be the start of getting what you need.
This example uses dependency injection via the constructor; this is common in MVVM and if you're not familiar with it, I would highly recommend looking into it further.
// define delegate for event to be fired from HatEngine instances
public delegate void HatEngineNotifyEventHandler(object sender, bool shouldShow);
// interface declaration for HatEngine - this is important for injecting mocks for unit testing
public interface IHatEngine
{
event HatEngineNotifyEventHandler Notify;
void SetShowRedHat(bool show);
}
// simple IHatEngine implementation
public sealed class HatEngine : IHatEngine
{
public event HatEngineNotifyEventHandler Notify;
public void SetShowRedHat(bool show) => OnNotify(show);
private void OnNotify(bool shouldShow) =>
Notify?.Invoke(this, shouldShow);
}
public class MyViewModel : ObservableObject
{
private readonly IHatEngine _hatEngine;
private bool _showRedHat;
// MyViewModel consumes an IHatEngine instance and subscribes to its Notify event
public MyViewModel(IHatEngine hatEngine = null)
{
// many MVVM frameworks include a DI container that should be used here
// to resolve an IHatEngine instance; however, for simplicity for this
// example just create HatEngine() directly
_hatEngine = hatEngine ?? new HatEngine();
// when the event is received, update ShowRedHat accordingly
_hatEngine.Notify += (_, shouldShow) => ShowRedHat = shouldShow;
}
public bool ShowRedHat
{
get => _showRedHat;
set => OnPropertyChanged(ref _showRedHat, value);
}
}
You can just bind an integer since Visibility is an Enum, check documentation since in some versions Hidden option is not available and Collapsed becomes 1, however normally you can just use these below:
Visible [0] - Display the element.
Hidden [1] Do not display the element, but reserve space for the
element in layout.
Collapsed [2] Do not display the element, and do not reserve space for
it in layout.

how to update control on one view from another view in mvvm

Hi I have three region based views in my MVVM application. I am new to MVVM and I want to update DataGrid on click on button from different view.
One view have one button and second view have datagrid. I would like to update datagrid result or bind datagrid when button on other view is pressed. I saw few post on doing it with eventService but I am not sure how. cany anyone give me some example to do as I am new so not sure. Thanks in advance.
a simple solution is to use the same ViewModel in both views:
ViewModel:
public class MyModel : ViewModel
{
static myModel;
public static MyModel Current { get { if(myModel==null) myModel=new MyModel(); return myModel; } }
public IEnumerable<T> Data { get { ... } set { /* Notification */ }}
public ICommand SetData {get { return new DelegateCommand(()=>Data= /* return the data */); }
}
Button view:
<Button Command={Binding SetData} />
DataGrid view:
<DataGrid ItemsSource={Binding Data} />
In the code-behind of both views, add to the constructor:
this.DataContext = MyModel.Current;

MVVM using Page Navigation On Windows Phone 7

The Navigation framework in Windows Phone 7 is a cut down version of what is in Silverlight. You can only navigate to a Uri and not pass in a view. Since the NavigationService is tied to the View, how do people get this to fit into MVVM. For example:
public class ViewModel : IViewModel
{
private IUnityContainer container;
private IView view;
public ViewModel(IUnityContainer container, IView view)
{
this.container = container;
this.view = view;
}
public ICommand GoToNextPageCommand { get { ... } }
public IView { get { return this.view; } }
public void GoToNextPage()
{
// What do I put here.
}
}
public class View : PhoneApplicationPage, IView
{
...
public void SetModel(IViewModel model) { ... }
}
I am using the Unity IOC container. I have to resolve my view model first and then use the View property to get hold of the view and then show it. However using the NavigationService, I have to pass in a view Uri. There is no way for me to create the view model first. Is there a way to get around this.
Instead of passing the view through the constructor. You could construct the view first via the NavigationService and pass it into the view-model. Like so:
public class ViewModel : IViewModel
{
private IUnityContainer container;
private IView view;
public ViewModel(IUnityContainer container)
{
this.container = container;
}
public ICommand GoToNextPageCommand { get { ... } }
public IView
{
get { return this.view; }
set { this.view = value; this.view.SetModel(this); }
}
public void GoToNextPage()
{
// What do I put here.
}
}
PhoneApplicationFrame frame = Application.Current.RootVisual;
bool success = frame.Navigate(new Uri("View Uri"));
if (success)
{
// I'm not sure if the frame's Content property will give you the current view.
IView view = (IView)frame.Content;
IViewModel viewModel = this.unityContainer.Resolve<IViewModel>();
viewModel.View = view;
}
If you are using Mvvm Light you could try:
Windows Phone 7 — Navigation between pages using MVVM Light Messaging
(See similar post: Silverlight Navigation using Mvvm-light(oobe)+MEF?)
My opinion is that the view-model should be created and registered at application startup. By placing it inside the root DataContext all pages will automatically get a reference to it without any code-behind or IoC tricks.
// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
m_ViewModel = new PrimaryViewModel(RootFrame) ;
RootFrame.DataContext = m_ViewModel;
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
m_ViewModel = new PrimaryViewModel(RootFrame) ;
m_ViewModel.Activated(PhoneApplicationService.Current.State);
RootFrame.DataContext = m_ViewModel;
}
If you are using MVVM architecture,then you can pass navigationPage after registering using Messenger. Create a model class (say NavigateToPageMessage) with a string(say PageName) variable. You want to pass string from homepage.xaml to newpage.xaml,then in Homepage viewmodel just send the message like this under the command you binded (say HomeNavigationCommand)
private void HomeNavigationCommandHandler()
{
Messenger.Default.Send(new NavigateToPageMessage {PageName = "newpage"});
}
In the newpage Viewmodel,you should register the messenger like this,
Messenger.Default.Register<NavigateToPageMessage>(this, (action) => ReceiveMessage(action));
private object ReceiveMessage(NavigateToPageMessage action)
{
var page = string.Format("/Views/{0}.xaml", action.PageName);
NavigationService.Navigate(new System.Uri(page,System.UriKind.Relative));
return null;
}
//Assuming your views are in View Folder