MvvmCross fragments resolving - android

I'm using Xamarin with MvvmCross, and have problem with fragments usage.
I call ShowViewModel so:
public class MainViewModel : MvxViewModel
{
public override void Start()
{
ShowViewModel<MainMenuViewModel>();
}
}
Where MainMenuViewModel it's class:
public class MainMenuViewModel : MvxViewModel
{
}
Implemented fragment as follows:
[MvxFragment(typeof(MainMenuViewModel), Resource.Id.navigation_frame)]
[Register("mvvm.droid.views.MainMenuView")]
public class MainMenuView : MvxFragment<MainMenuViewModel>
{
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var ignore = base.OnCreateView(inflater, container, savedInstanceState);
return this.BindingInflate(Resource.Layout.MainMenuView, null);
}
}
But on runtime it throws error:
Android.Content.ActivityNotFoundException: Unable to find explicit
activity class
{Mvvm.Droid/md5f67dcc55ddb5809d2766dd0c42c8b3bb.MainMenuView};
have you declared this activity in your AndroidManifest.xml?
For figuring out this, i implemented CustomPresenter, taken from here.
And in Setup registered this presenter for fragments:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var mvxFragmentsPresenter = new MvxCustomFragmentsPresenter(AndroidViewAssemblies);
Mvx.RegisterSingleton<IMvxAndroidViewPresenter>(mvxFragmentsPresenter);
return mvxFragmentsPresenter;
}
It seems like presenter found fragments, but at Show(Intent) method call it's still crushing. In decompiled sources there is a strange check if it's an activity.
Tryed to implement drawerLayout based on many implementations, but the same result. What i'm missing?

The issue is in your MvxFragment attribute:
[MvxFragment(typeof(MainMenuViewModel), Resource.Id.navigation_frame)]
The first parameter needs to be the MvxViewModel associated to your Activity that you want to place the menu fragment in. In your case I believe this may be MainViewModel?
Mvvmcross description of MvxFragment attribute:
public MvxFragmentAttribute(
Type parentActivityViewModelType,
int fragmentContentId,
bool addToBackStack = false);

Related

Update ViewModel between a fragment and an activity

I just started using the architecture component of Android in my app, and I have a use case when:
A fragment is an observer of ViewModel's LiveData (which comes from a Room request)
This fragment starts an activity at some point
The activity needs to use the same data (as LiveData in the fragment) and update it
The user then returns to the fragment
I tried using the same ViewModel class and adding the fragment and activity as observers, thinking that somehow updating from the activity will affect the fragment when returning to it, but it doesn't work.
The two ViewModels seem independent.
How can I force refresh the fragment data when returning from the activity (in onActivityResult for example)?
Or is it possible to share the same ViewModel between the fragment and activity? (though I doubt it since the ViewModel is linked to the lifecycle of the observer)
Fragment
public void onAttach(#NonNull Context context) {
...
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Data>>() {
#Override
public void onChanged(List<Data> data) {
mAdapter.setData(data);
}
});
}
// in an onClick method
startActivity(intent); // go to the Activity
Activity
protected void onCreate(Bundle savedInstanceState) {
...
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Data>>() {
#Override
public void onChanged(List<Data> data) {
mPagerAdapter.setData(data);
}
});
}
Any help will be appreciated!
EDIT:
View Model
public class MyViewModel extends AndroidViewModel {
private Dao dao;
private ExecutorService executorService;
public MyViewModel (#NonNull Application application) {
super(application);
dao = AppDatabase.getInstance(application).getDao();
executorService = Executors.newSingleThreadExecutor();
}
public LiveData<Data> getData() {
return dao.getAllData();
}
// other methods to update and delete with executorService
}
I managed to get the result you want by seting a common lifecycle owner for the viewmodel, and using the same viewModel at the different fragments.
Inside my fragment, i get the view model like this:
INSIDE FRAGMENT:
var userViewModel = activity?.run{ViewModelProviders.of(this, SharedUserViewModel.MainActivityViewModelFactory(applicationContext))[SharedUserViewModel::class.java]}
Did you see i use the activity as the "ViewModelProviders.of" parameter?
This way, the view model have the same owner, its working alright for me.
Yes you can share ViewMode between activity and Fragment Using SharedViewModel.

Passing data between fragments through ViewModel

let say for example I have 1 Activity that contains 5 Fragments and those Fragments presents a 1 flow of payment process so each Fragment depends on the previous Fragment by passing data of what the user chooses
I'm planning to make 1 ViewModel in the Activity that handles the data between fragments but I've read that it is a bad idea to expose MutableLiveData outside of the view model. so I can't say viewModel.setdata(example) in the Activity the best solution was is to use navigation component with safe args and create a ViewModel for each Fragment and create ViewModelFactory for each fragment too.
but this will make me write too many classes.
is there an optimal way to pass data between views using 1 ViewModel without violating the MVVM architecture rules?
Yes, this is good decision to use ViewModel to share data between fragments. Look this
SharedViewModel
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
MasterFragment
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
DetailFragment
public class DetailFragment extends Fragment {
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
model.getSelected().observe(getViewLifecycleOwner(), item -> {
// Update the UI.
});
}
}
Here is the black magic:
val viewModel: MyViewModel by activityViewModels()

How to display R.xml files in a fragment?

I was having this doubt of how to display a R.xml file in a fragment.
public class initSettings extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
return inflater.inflate(R.layout.init_settings,container,false);
}
}
Now, if I wanted to display R.xml.init_settings getting an error
Expected resource of type layout.
Expected resource of type layout.
You are trying to inflate xml file which it should be layout in this case in here:
(R.layout.init_settings, container, false);
So try to inflate this in the layout directory (just like your current codes):
R.layout.init_settings
And use layout to inflate and not an xml.
And like I said in my comments, onCreateView() won't import xml files. Instead, use addPreferencesFromResource() to inflate xml settings:
public class SettingsActivity extends PreferenceActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.init_settings);
}
}
Read the documentation: https://developer.android.com/guide/topics/ui/settings#java
For the newest APIs (AndroidX) use PreferenceFragmentCompat().
Kotlin code:
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.settingspreference)
}
}

Mvvmcross fragments

I am trying to implement my navigation drawer with MVVMCross but I cannot get the fragments to show. (This is not a problem with the navigation drawer but with fragments and MVVMCross).
This is the code I have in my sample (found on the github of MVVMCross), see github links below!
I have one activity extending the MvxCachingFragmentCompatActivity<MainViewModel>, this is the MainActivity containing the FrameLayout (Called Resource.Id.content_frame)
I have an MvxFragment called FirstFragment:
[MvxFragment(typeof(MainViewModel), Resource.Id.content_frame, true)]
[Register(nameof(FirstFragment))]
public class FirstFragment : MvxFragment<FirstViewModel>
{
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.OnCreateView(inflater, container, savedInstanceState);
var view = this.BindingInflate(Resource.Layout.FirstView, container, false);
return view;
}
}
I also have the following code added in my MainViewModel:
public class MainViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
public MainViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
}
public override async Task Initialize()
{
await _navigationService.Navigate<FirstViewModel>();
}
}
Github links:
My sample is visible on the following github!
And the is the mvvmcross sample!
I found the issue => apparently the MvxNavigationService doesn't like MvxCachingFragmentCompatActivity for some reason.
First Sample (Working)
In the first sample I do RegisterAppStart<> directly on the MainViewModel which extend from MvxCachingFragmentCompatActivity. This is working perfectly
Some code samples (for full code see link)
public class App : MvvmCross.Core.ViewModels.MvxApplication
{
public override void Initialize()
{
RegisterAppStart<MainViewModel>();
}
}
MainViewModel : MvxViewModel
public MainViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
Init();
}
public async void Init()
{
await _navigationService.Navigate<FirstViewModel>();
}
Second Sample (Issue)
In the second sample I first call another activity(StartActivity) and then go to the MainActivity. This gives problems because the MainViewModel is not called with RegisterAppStart<> but with the IMvxNavigationService.Navigate<MainViewModel>()
Some code samples (for full code see link)
public class App : MvvmCross.Core.ViewModels.MvxApplication
{
public override void Initialize()
{
RegisterAppStart<StartViewModel>();
}
}
StartViewModel:
public class StartViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
public ICommand StartCommand => new MvxCommand(ExecuteStart);
public StartViewModel()
{
_navigationService = Mvx.Resolve<IMvxNavigationService>();
}
private async void ExecuteStart()
{
await _navigationService.Navigate<MainViewModel>();
}
}
Difference:
First ViewModel: RegisterAppStart<StartViewModel>();
Called from the navigation service: await _navigationService.Navigate<MainViewModel>();

Dagger can't inject a Presenter in my Activities

I'm refactoring an Android app to an MVP architecture, using Dagger 1.
My code is more or less the following:
public interface View {
Presenter getPresenter();
}
public abstract class Presenter {
// Dagger 1 can't have an abstract class without injectable members... :(
#Inject
Application dont_need_this;
protected View view;
public void takeView(View view) { ... }
}
The field in Presenter is not pretty, but what can we do?
Next, I have an Activity that's also a View.
public abstract class ActivityView extends Activity implements View {
#Inject
protected Presenter presenter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ObjectGraph scope;
// Looks for an "scoped" ObjectGraph hold in an retained fragment
if (/* fragment is null */) {
// This is not a rotation but the first time the activity is
// launched
MVPApplication app = (MVPApplication) getApplication();
ObjectGraph scope = app.createScope(this);
// Store in the fragment
}
scope.inject(this);
// setContentView, etc.
presenter.takeView(this);
}
}
All seems good for now...
public abstract class MVPApplication extends Application {
private ObjectGraph objectGraph;
#Override
public void onCreate() {
super.onCreate();
// Root scope
// createRootModules is defined in the subclass
objectGraph = ObjectGraph.create(createRootModules());
objectGraph.inject(this);
}
public abstract ObjectGraph createScope(View view);
}
With this, my application subclass has to:
define createRootModules() to instantiate every module in the root scope
check in createScope() the class of the concrete ActivityView, essentially using a HashMap<Class<? extends View>, Module> and do...
return objectGraph.plus(new ModuleForThisActivityViewClass());
For example, I have an CollectionActivityView. My app tries to...
return objectGraph.plus(new CollectionModule());
And I have the binding for this particular Presenter in this module:
#Module(addsTo = MyAppModule.class,
injects = {CollectionActivityView.class, CollectionPresenter.class}, complete=false)
public class CollectionModule {
#Provides
#Singleton
public Presenter providePresenter(CollectionPresenter presenter) {
return presenter;
}
}
But when doing the 'plus()` this error happens:
ComponentInfo{com.mycompany.myapp/com.mycompany.myapp.view.CollectionActivityView}:
java.lang.IllegalStateException: Unable to create binding for
com.mycompany.myapp.mvp.presenter.Presenter
...
Caused by: java.lang.IllegalStateException: Unable to create binding for
com.mycompany.myapp.mvp.presenter.Presenter
at dagger.internal.Linker.linkRequested(Linker.java:147)
at dagger.internal.Linker.linkAll(Linker.java:109)
at dagger.ObjectGraph$DaggerObjectGraph.linkEverything(ObjectGraph.java:244)
at dagger.ObjectGraph$DaggerObjectGraph.plus(ObjectGraph.java:203)
Dagger is trying to resolve the dangling dependency of the superclass Presenter in ActivityView before plusing the ObjectGraph with the new Module, who is in charge of providing the Presenter.
Is there a way to avoid this? Am I doing something terribly wrong? Do you have any suggestions for doing this properly?
Looking at it again, it was my fault.
I don't know why I did it (probably trying to avoid tagging the module as incomplete or library) but MyAppModule declared it was injecting CollectionActivityView, so the Presenter dependency had to be resolved in that module before plussing the other. Removed that from the annotation in MyAppModule and everything worked as intended.

Categories

Resources