I am unable to bind ItemClick from MvxRecyclerView (or its Adapter) to a command on my ViewModel using Fluent API. It works if I put both ItemsSource and ItemClick in the XML so I am not interested in such solution.
I used this post as an excellent guideline (How to use the MvvmCross fluent API to bind a RecyclerView item's TextView to a property of its ViewModel on Android?) and all of that works except that I am unable to bind ItemClick on MvxRecyclerView (or the adapter) to a MainViewModel's command which will take me to the next fragment (ItemsSource works like a charm but its a property and not a command!).
For the sake of brevity, I will not be copying the code from the original post (How to use the MvvmCross fluent API to bind a RecyclerView item's TextView to a property of its ViewModel on Android?) so assume that the MainViewModel from that post has been enhanced with a command ShowItemCommand as such:
public class MainViewModel : MvxViewModel
{
private IEnumerable<ViewModelItem> _viewModelItems;
public IEnumerable<ViewModelItem> ViewModelItems
{
get { return _viewModelItems; }
set { SetProperty(ref _viewModelItems, value); }
}
public MvxCommand<ViewModelItem> ShowItemCommand
{
get
{
return new MvxCommand<ViewModelItem>(selectedItem =>
{
ShowViewModel<ViewModelItem>
(new { itemId = selectedItem.Id });
});
}
}
}
and everything else has been implemented as per the referenced post.
So now, in addition to ItemsSource, I want to wire up ItemClick on the MvxRecyclerView (or the Adapter) to the command. The reason these are interchangeable is that MvxRecyclerView just relays these commands to the Adapter.
Apparently, this should work...but it does not:
adapter.ItemClick = ViewModel.ShowItemCommand;
This does not work either:
set.Bind(recyclerView).For(v => v.ItemClick).To(vm => vm.ShowItemCommand);
When creating a custom MvxRecyclerViewHolder you need to make sure that you assign the Click command over to the ViewHolder. This is done in the OnCreateViewHolder override of your custom adapter.
Example for custom ViewHolder
public class MyAdapter : MvxRecyclerAdapter
{
public MyAdapter(IMvxAndroidBindingContext bindingContext)
: base(bindingContext)
{
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
var itemBindingContext = new MvxAndroidBindingContext(parent.Context, this.BindingContext.LayoutInflaterHolder);
var view = this.InflateViewForHolder(parent, viewType, itemBindingContext);
return new MyViewHolder(view, itemBindingContext)
{
Click = ItemClick,
LongClick = ItemLongClick
};
}
}
I can't reproduce your issue. I just created a new project, added a RecyclerView and added the following binding:
var set = this.CreateBindingSet<FirstView, FirstViewModel>();
set.Bind(recyclerView).For(v => v.ItemsSource).To(vm => vm.ViewModelItems);
set.Bind(recyclerView).For(v => v.ItemClick).To(vm => vm.ShowItemCommand);
set.Apply();
This works just as expected, where ItemClick triggers the ShowItemCommand. VM's look as follows:
public class ViewModelItem : MvxViewModel
{
public void Init(string itemId)
{
Mvx.Trace($"Showing {itemId}");
}
public string Id { get; set; }
}
public class FirstViewModel
: MvxViewModel
{
public FirstViewModel()
{
ViewModelItems = new ViewModelItem[] {
new ViewModelItem { Id = "Hello"},
new ViewModelItem { Id = "World"},
new ViewModelItem { Id = "Foo"},
new ViewModelItem { Id = "Bar"},
new ViewModelItem { Id = "Baz"}
};
}
private IEnumerable<ViewModelItem> _viewModelItems;
public IEnumerable<ViewModelItem> ViewModelItems
{
get { return _viewModelItems; }
set { SetProperty(ref _viewModelItems, value); }
}
public MvxCommand<ViewModelItem> ShowItemCommand =>
new MvxCommand<ViewModelItem>(DoShowItem);
private void DoShowItem(ViewModelItem item)
{
ShowViewModel<ViewModelItem>(new { itemId = item.Id });
}
}
Related
I`m using mvp with repository to update items on recyclerview when item at firestore database are changing.
As asked, here is some more code from SharedModelClass:
public LiveData<List<Task>> tasksListening() {
return repository.tasksListening(false);
}
which lead to:
public LiveData<List<Task>> tasksListening(boolean b) {
return new FirestoreTasksData(tasksReference, b);
}
Here is FirestoreTasksData extends LiveData<List>:
public class FirestoreTasksData extends LiveData<List<Task>> {
List<Task> tasks;
for (QueryDocumentSnapshot doc : value) {
Task item = doc.toObject(Task.class);
tasks.add(item);
}
setValue(tasks);
};
}
All works perfect except that list is updated in whole even when updating one item.
sharedViewModel.tasksListening().observe(getViewLifecycleOwner(), tasks -> {
tasksAdapter.submitList(tasks);
});
and here is adapter code:
public class MyTasksAdapter extends RecyclerView.Adapter<MyTasksAdapter.TaskHolder> {
private final AsyncListDiffer<Task> mDiffer = new AsyncListDiffer<>(this, DIFF_CALLBACK);
private static final DiffUtil.ItemCallback<Task> DIFF_CALLBACK = new DiffUtil.ItemCallback<Task>() {
#Override
public boolean areItemsTheSame(#NonNull Task oldItem, #NonNull Task newItem) {
return oldItem.getId().equals(newItem.getId());
}
#Override
public boolean areContentsTheSame(#NonNull Task oldItem, #NonNull Task newItem) {
return oldItem.geteDate().equals(newItem.geteDate()) && (new HashSet<>(oldItem.getRoles().values()).equals(new HashSet<>(newItem.getRoles().values())));
}
};
}
public void submitList(List<Task> list) {
mDiffer.submitList(list);
}
#Override
public int getItemCount() {
return mDiffer.getCurrentList().size();
}
Is this a bug or a feature? I was using Firestore UI recycler adapter before, just decided to refactor code.
That is expected behavior. When there's any change to the results of a query/collection, your code gets called with a QuerySnapshot object of all changes that match the query/collection.
If you want to see what has changed, you can look at the getDocumentChanges() of the snapshot to see those. For more on this (and an example) see the documentation on viewing change between snapshots.
Rather than implement custom adapter and DiffUtil Callback.
Take a look at LiveAdapter.
You just need to add the latest dependency in Gradle.
dependencies {
implementation 'com.github.RaviKoradiya:LiveAdapter:1.3.2-1608532016'
// kapt 'com.android.databinding:compiler:GRADLE_PLUGIN_VERSION' // this line only for Kotlin projects
}
and bind adapter with your RecyclerView
LiveAdapter(
data = liveListOfItems,
lifecycleOwner = this#MainActivity,
variable = BR.item )
.map<Header, ItemHeaderBinding>(R.layout.item_header) {
areContentsTheSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.map<Point, ItemPointBinding>(R.layout.item_point) {
areContentsTheSame { old: Point, new: Point ->
return#areContentsTheSame old.id == new.id
}
}
.into(recyclerview)
That's it. Not need to write extra code for adapter implementation, observe LiveData and notify the adapter.
Wanted to delete post but the rules.
I solved the problem. The problem is that all working good. Feel free to use code if needed.
I just realized that inside observer called another method, which is the reason I see rendering effect as if there were some bug with adapter.
sharedViewModel.tasksListening().observe(getViewLifecycleOwner(), tasks -> {
tasksAdapter.submitList(tasks);
checkEmpty(tasks.size());
});
private void checkEmpty(int size) {
if (size == 0) {
crossFade(noDataLayout, mTasksRecycler);
} else {
crossFade(mTasksRecycler, noDataLayout);
}
}
I fixed that and now all is fine.
We have an MvxTabbedPage and child MvxContentPages in our Xamarin.Forms project.
On Android, I'm finding that the ViewAppeared override on my first child page is not being called the first time the MvxTabbedPage is being shown.
When switching tabs, it subsequently is being called correctly.
I'm initialising the PageModels in the ViewAppearing for the MvxTabbedPage's PageModel as below:
public override async void ViewAppearing()
{
await ShowInitialViewModels();
base.ViewAppearing();
}
private bool viewModelsInitialised = false;
private async Task ShowInitialViewModels()
{
if (!viewModelsInitialised)
{
await _BusyManager.SetBusy();
var tasks = new List<Task>();
tasks.Add(_MvxNavigationService.Navigate<HomePageModel>());
tasks.Add(_MvxNavigationService.Navigate<MyBenefitsPageModel>());
tasks.Add(_MvxNavigationService.Navigate<ClaimsPageModel>());
tasks.Add(_MvxNavigationService.Navigate<ContactUsPageModel>());
tasks.Add(_MvxNavigationService.Navigate<SettingsPageModel>());
await Task.WhenAll(tasks);
viewModelsInitialised = true;
await _BusyManager.SetUnBusy();
}
}
Have others seen this behaviour, and/or should I be doing something differently?
Looks like it's this Forms bug:
https://github.com/xamarin/Xamarin.Forms/issues/3855
which is referenced by this MvvmCross issue
https://github.com/MvvmCross/MvvmCross/issues/2823
(thanks to Pedro for pointing me in this direction on Slack:)
Check the Playground project of mvvmcross. You should manage your tabs initializations separately in a viewmodel and the XF view code behind.
public class YourTabsViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
public YourTabsViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
ShowInitialViewModelsCommand = new MvxAsyncCommand(ShowInitialViewModels);
}
public IMvxAsyncCommand ShowInitialViewModelsCommand { get; private set; }
private async Task ShowInitialViewModels()
{
var tasks = new List<Task>
{
tasks.Add(_navigationService.Navigate<HomePageModel>();
tasks.Add(_navigationService.Navigate<MyBenefitsPageModel>());
tasks.Add(_navigationService.Navigate<ClaimsPageModel>());
tasks.Add(_navigationService.Navigate<ContactUsPageModel>());
tasks.Add(_navigationService.Navigate<SettingsPageModel>());
}
await Task.WhenAll(tasks);
}
}
And then on the code behind of your XF view
[MvxTabbedPagePresentation(TabbedPosition.Root, NoHistory = true)]
public partial class YourTabsPage : MvxTabbedPage<YourTabsViewModel>
{
public YourTabsPage()
{
InitializeComponent();
}
private bool _firstTime = true;
protected override void OnAppearing()
{
base.OnAppearing();
if (_firstTime)
{
ViewModel.ShowInitialViewModelsCommand.ExecuteAsync(null);
_firstTime = false;
}
}
}
There are many examples how to push new list to adapter on LiveData change.
I'm trying to update one row (e.g number of comments for post) in the huge list. It would be stupid to reset whole list to change only one field.
I am able to add observer onBindViewHolder, but I can't understand when should I remove observer
#Override
public void onBindViewHolder(ViewHolder vh, int position) {
Post post = getPost(position);
vh.itemView.setTag(post);
post.getLiveName().observeForever(vh.nameObserver);
...
}
Like #Lyla said, you should observe the whole list as LiveData in Fragment or Activity, when receive changes, you should set the whole list to the adapter by DiffUtil.
Fake code:
PostViewModel {
LiveData<List<Post>> posts; // posts comes from DAO or Webservice
}
MyFragment extends LifecycleFragment {
PostAdapter postAdapter;
...
void onActivityCreated() {
...
postViewModel.posts.observer(this, (postList) -> {
postAdapter.setPosts(postList);
}
}
}
PostAdapter {
void setPosts(List<Post> postList) {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {...}
...
}
}
Using DiffUtil might help with updating one row in a huge list. You can then have LiveData wrap the list of comments instead of a single comment or attribute of a comment.
Here's an example of using DiffUtil within a RecyclerView adapter and the list LiveData observation code in the fragment.
Use Transformations.switchMap() to swap the underlying Post object. Then there is no need to remove and re-add observers when the cell is recycled.
#Override
public void onBindViewHolder(PostViewHolder vh, int position) {
Post post = getPost(position);
vh.bind(post);
}
Then in your ViewHolder class
public class PostViewHolder extends RecyclerView.ViewHolder {
private final MutableLiveData<Post> post = new MutableLiveData<>();
public PostViewHolder(View itemView) {
super(itemView);
LiveData<String> name = Transformations.switchMap(post, new Function<Post, LiveData<String>>() {
#Override
public LiveData<String> apply(Post input) {
return input.getLiveName();
}
});
name.observeForever(new Observer<String>() {
#Override
public void onChanged(#Nullable String name) {
// use name
}
});
}
public void bind(Post post) {
post.setValue(post);
}
}
I think you should use LiveAdapter for RecyclerView Adapter instead of creating an extra class for the adapter.
It has DiffUtil implementation as well, so only single item will be updated.
and without calling notifyDatasetChange.
// Kotlin sample
LiveAdapter(
data = liveListOfItems,
lifecycleOwner = this#MainActivity,
variable = BR.item )
.map<Header, ItemHeaderBinding>(R.layout.item_header) {
onBind{
}
onClick{
}
areContentsTheSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.map<Point, ItemPointBinding>(R.layout.item_point) {
onBind{
}
onClick{
}
areContentsTheSame { old: Point, new: Point ->
return#areContentsTheSame old.id == new.id
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.into(recyclerview)
add context to your adapterClass construstor : AdpaterClass(context: Context)
then smart cast the context to AppCompactActivity
livedata.observe(context as AppCompatActivity, Observer {it ->
//perform action on it(livedata.value)
})
when calling the adapter from anywhere activity, fragment pass the context into the adpater
Is there a way to bind property of the VM to any kind of singleton property ( static resource property, property in the singleton service... ) in a way that we don't need to use IMessenger or to handle SingletonServiceResolved OnPropertyChanged?
It feels kind of dirty for me (even if it is in the base class) to have each activity to handle changes in my singleton Clock Property.
public class ClockService : ObservableObject, IClockService {
private DateTime _clock;
public DateTime Clock {
get{ return _clock;}
set { _clock = value; RaisePropertyChanged("Clock"); }
}
}
public class SomeViewModel : BaseViewModel {
private IClockService _clockService;
private IMvxMessenger _messenger;
public SomeViewModel(IClockService clockService, IMvxMessenger messenger) {
_clockService=clockService;
_messenger = messenger;
//trying to avoid
clockService.PropertyChanged += OnClockServicePropertyChanged;
}
public DateTime MyClock {
get{return _clockService.Clock;}
}
private OnClockServicePropertyChanged(...) {
if(e.PropertyName=="Clock") RaisePropertyChanged("Clock");
}
}
One way to bind to a singleton is to expose the singleton via a property on your ViewModel:
public Thing MySingleton
{
get
{
return Thing.Instance;
}
}
Update after comment that the singleton isn't constant:
You could use another constant singleton as a holder - and this can then implement INotifyPropertyChanged - e.g.:
public class ThingHolder : MvxNotifyPropertyChanged
{
public static readonly ThingHolder Instance = new ThingHolder();
private Thing _thing;
public Thing CurrentThing
{
get { return _thing; }
set { _thing = value; RaisePropertyChanged(() => CurrentThing); }
}
}
Your VMs can then properties like:
public ThingHolder ThingHolder
{
get
{
return ThingHolder.Instance;
}
}
Your views can then bind to expressions like ThingHolder.CurrentThing
Some times when the activity is destroyed (not sure why, memory pressure I presume), a new activity is created, but the old view model bound to the dead activity is reused.
Activity:
[Activity(
LaunchMode = LaunchMode.SingleTask,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class HomeView : MvxTabsFragmentActivity
{
protected override void OnCreate(Bundle bundle)
{
Log.Info("On HomeView created");
base.OnCreate(bundle);
}
protected override void OnDestroy()
{
base.OnDestroy();
Log.Info("On HomeView destroyed");
this.HomeViewModel.CleanUp();
}
}
ViewModel:
public class HomeViewModel : MvxViewModel
{
public HomeViewModel(
IMvxMessenger messenger,
IUserInteraction userInteraction,
DashboardViewModel dashboardViewModel,
AlertSettingsViewModel alertSettingsViewModel)
{
Log.Info("Building home view model");
}
public void CleanUp()
{
Log.Info("HomeViewModel => Clean-up");
}
}
App.cs:
public override void Initialize()
{
this.CreatableTypes().EndingWith("ViewModel").AsTypes().RegisterAsDynamic();
this.RegisterAppStart<HomeViewModel>();
TaskScheduler.UnobservedTaskException +=
(sender, eventArgs) =>
Log.Error("An Unobserved exception has been raised by a task", eventArgs.Exception);
}
Debug output:
On HomeView created
Building home view model
...
On HomeView destroyed
HomeViewModel => Clean-up
...
On HomeView created
[here: no "Building view model" message]
Maybe it is the SingleTask Activity ?
Is there a way (with IoC, or other) to get a fresh view model at every HomeView creation ?
EDIT:
I ran over this method on MvxActivityViewExtensions.cs
public static void OnViewCreate(this IMvxAndroidView androidView, Bundle bundle)
{
MvxActivityViewExtensions.EnsureSetupInitialized(androidView);
MvxActivityViewExtensions.OnLifetimeEvent(androidView, (Action<IMvxAndroidActivityLifetimeListener, Activity>) ((listener, activity) => listener.OnCreate(activity)));
IMvxViewModel cached = Mvx.Resolve<IMvxSingleViewModelCache>().GetAndClear(bundle);
IMvxView view = (IMvxView) androidView;
IMvxBundle savedState = MvxActivityViewExtensions.GetSavedStateFromBundle(bundle);
MvxViewExtensionMethods.OnViewCreate(view, (Func<IMvxViewModel>) (() => cached ?? MvxActivityViewExtensions.LoadViewModel(androidView, savedState)));
}
So would it mean my view model is cached ? How to disable this cache ?
This is covered in the Wiki in https://github.com/MvvmCross/MvvmCross/wiki/Customising-using-App-and-Setup#overriding-viewmodel-locationconstruction
By default, MvvmCross builds a new ViewModel every time one is requested and uses the CIRS sequence - Construction-Init-ReloadState-Start - to initialize that ViewModel.
If you want to override this behaviour for one or more ViewModel types, then you can do this in your App object by supplying your own IMvxViewModelLocator implementation.
For example, you could implement
public class MyViewModelLocator
: MvxDefaultViewModelLocator
{
private SpecialViewModel _special = new SpecialViewModel();
public override bool TryLoad(Type viewModelType, IDictionary<string, string> parameterValueLookup,
out IMvxViewModel model)
{
if (viewModelType == typeof(SpecialViewModel))
{
model = _special;
return true;
}
else if (viewModelType == typeof(FooViewModel))
{
model = new FooViewModel(_special);
return true;
}
return base.TryLoad(viewModelType, parameterValueLookup, out model);
}
}
and could then return this in App.cs:
protected override IMvxViewModelLocator CreateDefaultViewModelLocator()
{
return new MyViewModelLocator();
}