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>();
Related
Im new to kotlin, and mvvm, but i was able to make it work in java, but when i made a new example mvvm-retrofit-corutines in kotlin, the view model gets called all the time on the OnCreate function is called, (which shouldn't happen according to docs and works fine in java).
MainActivity:
lateinit var viewModel : MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Here we can see the logs in every orientation changed in the emulator.
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.getMutableLiveDataModel().observe(this, Observer {
Log.d("zzzz","lamda executes onChanged method -> "+ it.otherValues). //element from model
})
}
MyViewModel:
class MyViewModel : ViewModel() {
private lateinit var objectTypeModel: MutableLiveData<MyTestModel>
fun getMutableLiveDataModel():MutableLiveData<MyTestModel>{
//Gets the model from a retrofit service call
objectTypeModel = MyRepository.getModelFromService()
return objectTypeModel
}
}
Am i doing something wrong? already tried convert 'viewModel' into local variable as suggested in other post.
Java Code, MainActivity
MyViewModel model;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
model = new ViewModelProvider(this).get(MyViewModel.class);
model.getUsers().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer users) {
Log.d("zzzz","updated value..")
}
});
}
Model
public class MyViewModel extends ViewModel {
private MutableLiveData<Integer> users;
public LiveData<Integer> getUsers() {
if (users == null) {
users = new MutableLiveData<Integer>();
users.setValue(10);
}
return users;
}
}
If you don't want to recreate view model declare your view model like this
private val model: MyViewModel by activityViewModels()
for more details refer ViewModel
I think the issue lies in your kotlin viewmodel class, if you are not getting the value(unless you have few more issues in other classes)
Fix your kotlin viewmodel class in which data is not set in MutableLiveData, you forgot to add a piece of code.
//Here it is like this
objectTypeModel.value= MyRepository.getModelFromService()
AFAIK onCreate() only gets called when activity is created. So its natural if your viewmodel is getting created again. You can also check it by init{} method in your viewmodel class.
Still if you are not satisfied move your api call from activity's onCreate() method to viewmodels init{} method and just observe the changes from Activity. Your getMutableLiveDataModel() will called once when viewmodel object gets created.
If your java viewmodel example is running as you expected. Then,try to convert the java class to kotlin and run it again(just paste the java code to a kotlin file, it will ask you to convert it), it should work.
I've tried the same concept and as expected, the functionality in Java and Kotlin is identical. In the LogCat, I expected that the log should be printed on every rotation and it does. Now, let me tell you why it happens.
So, as per the documentation ViewModel instance stays alive after the configuration change. Basically, ViewModel uses the same instance if your activity is re-creating numerous times but it's not getting destroyed (calling finish()). But it's not the magic of the ViewModel it's the magic of LiveData.
LiveData is an observable data view holder so it sends the latest preserved value to the active observers on every configuration change which you're observing in the onCreate().
Let me present you my code.
Java
// Activity
public class JavaActivity extends AppCompatActivity {
private static final String TAG = "JavaActivity";
private JavaViewModel javaViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java);
// Ignore this listener
findViewById(R.id.go_to_kotlin_activity).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
finish();
}
});
// Main
javaViewModel = new ViewModelProvider(this).get(JavaViewModel.class);
javaViewModel.getJavaLiveData().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer integer) {
Log.d(TAG, "onChanged: " + integer);
}
});
}
}
// ViewModel
public class JavaViewModel extends ViewModel {
private MutableLiveData<Integer> javaLiveData;
public LiveData<Integer> getJavaLiveData() {
if(javaLiveData == null) {
javaLiveData = new MutableLiveData<>();
javaLiveData.setValue(10);
}
return javaLiveData;
}
}
Kotlin
// Activity
class KotlinActivity : AppCompatActivity() {
companion object {
private const val TAG = "KotlinActivity"
}
private lateinit var kotlinViewModel: KotlinViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_kotlin)
// Ignore this listener
findViewById<Button>(R.id.go_to_java_activity_btn).setOnClickListener {
startActivity(Intent(this, JavaActivity::class.java))
}
// Main
kotlinViewModel = ViewModelProvider(this).get(KotlinViewModel::class.java)
kotlinViewModel.getKotlinLiveData().observe(this, Observer {
Log.d(TAG, "onCreate: $it")
})
}
}
// ViewModel
class KotlinViewModel : ViewModel() {
private lateinit var kotlinLiveData: MutableLiveData<Int>
fun getKotlinLiveData(): LiveData<Int> {
if (!::kotlinLiveData.isInitialized) {
kotlinLiveData = MutableLiveData()
kotlinLiveData.value = 10
}
return kotlinLiveData
}
}
If you have any follow-up questions, leave them in comments.
Thanks!
References
LiveData - Official Documentation
ViewModel - Official Documentation
This is a great article on how ViewModel works internally.
Do read this article as well
Try
MainActivity
lateinit var viewModel : MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.objectTypeModel.observe(this, Observer {
Log.d("zzzz","lamda executes onChanged method -> "+ it.otherValues).
//element from model
})
}
ViewModel
class MyViewModel : ViewModel() {
val objectTypeModel= MutableLiveData<MyTestModel>()
init {
objectTypeModel.value = MyRepository.getModelFromService()
}
}
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()
I'm trying to create a ViewModel in MainActivity, which observes to some data changes in some singleton component. The goal is to use that ViewModel in a few fragments of this activity. But so far even without involving the fragment yet it doesn't work. The app gets stuck at launch, printing :
java.lang.RuntimeException: Unable to start activity ComponentInfo{my_package.MainActivity}: java.lang.RuntimeException: Cannot create an instance of class my_package.MyViewModel
my_package.App cannot be cast to android.arch.lifecycle.LifecycleOwner
the problem seems to be in the line : MyCustomSingletonComponent.getInstance().getSomeDataLiveData().observe......
The Code :
public class MyCustomSingletonComponent
{
public MutableLiveData<CustomClass> someData = new MutableLiveData<>();
private static final MyCustomSingletonComponent instance = new MyCustomSingletonComponent();
private MyCustomSingletonComponent() {
someData = new MutableLiveData<>();
}
public static MyCustomSingletonComponent getInstance() {
return instance;
}
public LiveData<CustomClass> getDataLiveData()
{
return someData;
}
}
public class MyViewModel extends AndroidViewModel {
public MyViewModel(#NonNull Application application)
{
super(application);
MyCustomSingletonComponent.getInstance().getSomeDataLiveData().observe(getApplication(), myData -> {
...
});
}
}
public class MainActivity extends AppCompatActivity
{
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
....
}
}
First, an Application is not a LifecycleOwner, so you cannot pass it to observe() on a LiveData. Activities and fragments are standard lifecycle owners.
Second, IMHO, a ViewModel should not be observing anything. The thing that uses the ViewModel does the observing. MyViewModel might hold onto the LiveData, but then MainActivity is the one that observes.
MVVM architecture,
this is my View (Activity):
private MyApp app;
private MainActivityVM viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (MyApp) this.getApplication();
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
MainActivityVM.Factory factory = new MainActivityVM.Factory(app);
final MainActivityVM model = ViewModelProviders.of(this, factory)
.get(MainActivityVM.class);
viewModel = model;
binding.setVm(viewModel);
viewModel.onCreate();
and View Model:
public class MainActivityVM extends AndroidViewModel implements ViewModel {
public MainActivityVM(#NonNull Application application) {
super(application);
}
#Override public void onCreate() {
model = new MyService();
model.getData(); /* <-- how do i pass the activity here? */
}
#Override public void onPause() { }
#Override public void onResume() { }
#Override public void onDestroy() { }
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
public Factory(#NonNull Application application) {
mApplication = application;
}
#Override
public <T extends android.arch.lifecycle.ViewModel> T create(Class<T> modelClass) {
return (T) new MainActivityVM(mApplication);
}
}
}
and Model:
public class myService{
public getData(){
if(permissionacquired(){
getdata()
}else{
requestPermission();
}
}
private void requestPermission() {
PermissionKey permKey = new PermissionKey(HealthConstants.StepCount.HEALTH_DATA_TYPE, PermissionType.READ);
HealthPermissionManager pmsManager = new HealthPermissionManager(mStore);
try {
// Show user permission UI for allowing user to change options
/* BELOW CODE REQUIRE Activity reference to PASS */
pmsManager.requestPermissions(Collections.singleton(permKey), MainActivity.this).setResultListener(result -> {
/* ABOVE CODE REQUIRE Activity reference to PASS */
Log.d(APP_TAG, "Permission callback is received.");
Map<PermissionKey, Boolean> resultMap = result.getResultMap();
if (resultMap.containsValue(Boolean.FALSE)) {
updateStepCountView("");
showPermissionAlarmDialog();
} else {
// Get the current step count and display it
mReporter.start(mStepCountObserver);
}
});
} catch (Exception e) { Log.e(APP_TAG, "Permission setting fails.", e); }
}
}
EDIT: if you see my request permission in my Model, the API require activity to be pass - how can i pass activity reference to the request permission?
I have a get permission method that comes from Model. this get permission method from my service provider require activity e.g. requestPermission(Activity)
so in my ModelView, i have the model object which is the dataService from another source.
then, how I can reference Activity in my ViewModel so I can call: model.requestPermission(Activity); in my ViewModel?
understanding from here that:
Caution: A ViewModel must never reference a view, Lifecycle, or any
class that may hold a reference to the activity context.
As long as you require permission in onCreate() method you can just move logic with permission request into the activity, and pass request result into viewModel.
In my case I also added Activity into ViewModel for permissions and strings, but it's not a good idea. When I disabled location permission in one Fragment, an application crashed, because it restarted, then restored FragmentManager with fragment stack and later started MainActivity. So ViewModel got location status too early (in constructor) and threw an exception. But when I moved getting location status to a function, then the application restarted normally.
So, using Dagger, you can write something like:
AppModule:
#JvmStatic
#Provides
fun provideActivity(app: MainApplication): AppCompatActivity = app.mainActivity
In MainApplication hold mainActivity and in MainActivity set in onCreate:
application.mainActivity = this
In onDestroy:
application.mainActivity = null
In any ViewModel add:
class SomeViewModel #Inject constructor(
private val activity: Provider<AppCompatActivity>
)
Then use it: activity.get().getString(R.string.some_string).
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);