I am being using mvvmcross to develop a cross platform app, it's helps us in most prominent way.
I am facing an minor issue on Loading indicator display.
I have a property named IsLoading in my BaseViewModel
public abstract class BaseViewModel : MvxViewModel
{
#region IsLoading
private bool _IsLoading;
public bool IsLoading
{
get { return _IsLoading; }
set
{
_IsLoading = value;
RaisePropertyChanged(() => IsLoading);
}
}
#endregion
}
Now, I have MainViewModel which create instance of two viewmodel inside it like below.
public class MainViewModel : BaseViewModel
{
public DashBoardViewModel DashboardVM
{
get { return _DashBoardVM; }
set
{
_DashBoardVM = value;
RaisePropertyChanged(() => DashboardVM);
}
}
public AccountViewModel AccountVM
{
get { return _AccountVM; }
set
{
_AccountVM = value;
RaisePropertyChanged(() => AccountVM);
}
}
public MainViewModel()
{
//To Do
}
public async Task Init()
{
DashboardVM = new DashBoardViewModel();
AccountVM = new AccountViewModel();
await Task.Delay(0);
}
}
Below is my DashBoardViewModel
public class DashBoardViewModel : BaseViewModel
{
public DashBoardViewModel()
{
IsLoading = true;
//Code to get data
}
}
As you can see in the above code i have setted the IsLoading = true, but at client end we don't get propertychanged event for this property.
Below is my one of client code to show progressdialog.
On Android we are using fragments, below is my MainView activity
public class MainView : MvxFragmentActivity
{
private ViewPager _viewPager;
private TabPageIndicator _pageIndicator;
private MvxViewPagerFragmentAdapter _adapter;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.MainView);
var fragments = new List<MvxViewPagerFragmentAdapter.FragmentInfo>
{
new MvxViewPagerFragmentAdapter.FragmentInfo
{
FragmentType = typeof(DashboardFragment),
Title = "",
ViewModel = ViewModel.DashboardVM
},
new MvxViewPagerFragmentAdapter.FragmentInfo
{
FragmentType = typeof(MyProfileFragment),
Title = "",
ViewModel = ViewModel.AccountVM
}
};
_viewPager = FindViewById<ViewPager>(Resource.Id.viewPager);
_adapter = new MvxViewPagerFragmentAdapter(this, SupportFragmentManager, fragments);
_viewPager.Adapter = _adapter;
_pageIndicator = FindViewById<TabPageIndicator>(Resource.Id.viewPagerIndicator);
_pageIndicator.SetViewPager(_viewPager);
_pageIndicator.CurrentItem = 0;
}
}
Below is my DashBoardFragment class
public class DashboardFragment : MvxFragment
{
public override View OnCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
var ignore = base.OnCreateView(inflater, container, savedInstanceState);
var viewToReturn = this.BindingInflate(Resource.Layout.Dashboard, null);
#region MVVMCROSS Binding
var set = this.CreateBindingSet<DashboardFragment, DashBoardViewModel>();
#endregion
#region Handle Progress bar
//initializing bindableprogress
var _bindableProgress = new BindableProgress(Activity);
//Binding with viewmodel property
set.Bind(_bindableProgress).For(m => m.IsLoading).To(vm => vm.IsLoading);
#endregion
#region Bind the set
set.Apply();
#endregion
return viewToReturn;
}
}
Note : BindableProgress is the class which has the property on which loading is been displayed and it is working fine on simple ViewModel.
Please help me to fix this.
Thanks
Aaman
I don't believe this is correct:
public class DashBoardViewModel : BaseViewModel
{
public DashBoardViewModel()
{
IsLoading = true;
//Code to get data
}
}
You probably don't want this happening in the constructor as you are creating the view models, not showing them yet. That is why you don't see an indicator because you set IsLoading to true but that is not the active screen.
Does something like this work?
public async Task Init()
{
IsLoading = true;
DashboardVM = new DashBoardViewModel();
AccountVM = new AccountViewModel();
await Task.Delay(0);
IsLoading = false;
}
The idea here being you should be showing the loading indicator for the MainView model during initialization of the child view models.
Related
Even though I am using ViewModel, whenever the device is rotated, the data in the Recyclerview disappears. I had to put the makeSearch() method inside the onClick() method because I need to get the text that the button grabs and use it as the search parameter. Is there a better way I can handle this to avoid this problem? My code is right here:
SearchActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// What happens when the search button is clicked
materialButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Objects.requireNonNull(textInputEditText.getText()).toString().isEmpty()) {
textInputEditText.setError("Type a search query");
} else {
mSearchInput = Objects.requireNonNull(textInputEditText.getText()).toString();
textInputEditText.setText("");
makeSearch();
}
}
});
}
// Gets the ViewModel, Observes the Question LiveData and delivers it to the Recyclerview
private void makeSearch() {
final SearchAdapter searchAdapter = new SearchAdapter();
SearchViewModel mSearchViewModel = new ViewModelProvider(this,
new CustomSearchViewModelFactory(new SearchRepository())).get(SearchViewModel.class);
mSearchViewModel.setQuery(mSearchInput);
mSearchViewModel.getQuestionLiveData().observe(this, new Observer<List<Question>>() {
#Override
public void onChanged(List<Question> questions) {
mQuestions = questions;
searchAdapter.setQuestions(questions);
}
});
mRecyclerView.setAdapter(searchAdapter);
searchAdapter.setOnClickListener(mOnClickListener);
}
SearchViewModel:
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<>();
private LiveData<List<Question>> mQuestionLiveData = Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
SearchViewModel(SearchRepository searchRepository) {
this.mSearchRepository = searchRepository;
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
}
SearchRepository:
public class SearchRepository {
//private String inTitle;
private MutableLiveData<List<Question>> mQuestions = new MutableLiveData<>();
public SearchRepository() {
//getQuestionsWithTextInTitle();
}
private void getQuestionsWithTextInTitle(String inTitle) {
ApiService apiService = RestApiClient.getApiService(ApiService.class);
Call<QuestionsResponse> call = apiService.getQuestionsWithTextInTitle(inTitle);
call.enqueue(new Callback<QuestionsResponse>() {
#Override
public void onResponse(Call<QuestionsResponse> call, Response<QuestionsResponse> response) {
QuestionsResponse questionsResponse = response.body();
if (questionsResponse != null) {
mQuestions.postValue(questionsResponse.getItems());
//shouldShowData = true;
} else {
Log.d("SearchRepository", "No matching question");
//shouldShowData = false;
}
}
#Override
public void onFailure(Call<QuestionsResponse> call, Throwable t) {
//shouldShowData = false;
t.printStackTrace();
}
});
}
public LiveData<List<Question>> getQuestions(String inTitle) {
getQuestionsWithTextInTitle(inTitle);
return mQuestions;
}
}
Your approach of passing the search input in through your CustomSearchViewModelFactory and into the constructor for the ViewModel and into the constructor for your SearchRepository isn't going to work in any case. While the first time you search your CustomSearchViewModelFactory creates the ViewModel, the second time you hit search, your SearchViewModel is already created and your factory is not invoked a second time, meaning you never get the second query.
Instead, you should file the ViewModel Overview documentation, and use Transformations.switchMap() to convert your input (the search string) into a new LiveData<List<Question>> for that given query.
This means that your ViewModel would look something like
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<String>();
private LiveData<List<Question>> mQuestionLiveData =
Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
public SearchViewModel() {
mSearchRepository = new SearchRepository();
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
}
You'd then update your Activity to:
Always observe the getQuestionLiveData() (note that you won't get a callback to your Observer until you actually set the first query)
Call setQuery() on your SearchViewModel in your makeSearch()
Remove your CustomSearchViewModelFactory entirely (it would no longer be needed).
I have done like docs here but Live data'a value is not changing. Please tell me what am i doing wrong.
MainActivity
public class MainActivity extends AppCompatActivity {
private NameViewModel mModel;
private ActivityMainBinding binding;
int index = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.button.setOnClickListener((v) -> {
mModel.getCurrentName().setValue("Test");
});
mModel = ViewModelProviders.of(this).get(NameViewModel.class);
final Observer<String> nameObserver = (text) -> {
binding.textInputLayout.getEditText().setText(text);
};
mModel.getCurrentName().observe(this, nameObserver);
}
}
NameViewModel.java
public class NameViewModel extends ViewModel {
private MutableLiveData<String> mCurrentName;
public MutableLiveData<String> getCurrentName() {
if (mCurrentName == null) {
return new MutableLiveData<>();
}
return mCurrentName;
}
}
This is because, your logic returns new instance of mCurrentName each time. Please use the following function.
public class NameViewModel extends ViewModel {
private MutableLiveData<String> mCurrentName;
public MutableLiveData<String> getCurrentName() {
// Ensure there is only 1 instance of mCurrentName
if (mCurrentName == null) {
mCurrentName = new MutableLiveData<>();
}
return mCurrentName;
}
}
A much better and safer way (reduce chance of making such mistake), is to initialize mCurrentName in constructor, and mark it as final.
public class NameViewModel extends ViewModel {
private final MutableLiveData<String> mCurrentName;
public NameViewModel() {
mCurrentName = new MutableLiveData<>();
}
public MutableLiveData<String> getCurrentName() {
return mCurrentName;
}
}
I am trying to change back arrow image in navigation page. For this in android app i created navigation page renderer and then using method toolbar.SetNavigationIcon and its not working, but when i use toolbar.SetLogo its working fine.
protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
{
base.OnElementChanged(e);
toolbar.SetNavigationIcon(Resource.Drawable.arrow);
toolbar.SetLogo(Resource.Drawable.arrow);
}
public override void OnViewAdded(Android.Views.View child)
{
base.OnViewAdded(child);
if (child.GetType() == typeof(Android.Support.V7.Widget.Toolbar))
{
toolbar = (Android.Support.V7.Widget.Toolbar)child;
toolbar.ChildViewAdded += Toolbar_ChildViewAdded;
}
}
I also tried add image to app:navigationIcon in toolbar.axml, and it shows great in designer
my arrow
But, when i starting my app i have the same standart arrow icon
enter image description here
If your MainActivity is FormsApplicationActivity, you could refer to this example :
https://github.com/jessejiang0214/ChangeBackIconInXF/tree/master/Droid
If your MainActivity type is FormsAppCompatActivity, you could custom a PageRenderer and change the Toolbar's NavigationIcon.
For example :
[assembly: ExportRenderer(typeof(ContentPage), typeof(NavigationPageRendererDroid))]
...
public class NavigationPageRendererDroid : PageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Page> e)
{
base.OnElementChanged(e);
var context = (Activity)Xamarin.Forms.Forms.Context;
var toolbar = context.FindViewById<Android.Support.V7.Widget.Toolbar>(Droid.Resource.Id.toolbar);
toolbar.NavigationIcon = Android.Support.V4.Content.ContextCompat.GetDrawable(context, Resource.Drawable.Back);
}
}
Usage :
MainPage = new NavigationPage(new MainPage());
...
//When click a button in MainPage, navigate to another page
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestPage());
}
Effect.
Update :
When you use Navigation.PushAsync() method navigate to another page, the system will automatically update the Toolbar's icon, you could find in the source code :
protected virtual Task<bool> OnPushAsync(Page view, bool animated)
{
return SwitchContentAsync(view, animated);
}
Task<bool> SwitchContentAsync(Page page, bool animated, bool removed = false, bool popToRoot = false)
{
...
UpdateToolbar();
...
}
void UpdateToolbar()
{
...
bool isNavigated = ((INavigationPageController)Element).StackDepth > 1;
if (isNavigated)
{
...
if (NavigationPage.GetHasBackButton(Element.CurrentPage))
{
//Set the navigation back icon < =================== !!! =-=
var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext);
icon.Progress = 1;
bar.NavigationIcon = icon;
}
}
...
}
Solution :
So we have no choice but to custom a NavigationPageRenderer, override the OnPushAsync method to set the Toolbar's icon.
using AToolbar = Android.Support.V7.Widget.Toolbar;
[assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(NavigationPageRendererDroid))] // APPCOMP
...
public class NavigationPageRendererDroid : Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer // APPCOMP
{
public AToolbar toolbar;
public Activity context;
protected override Task<bool> OnPushAsync(Page view, bool animated)
{
var retVal = base.OnPushAsync(view, animated);
context = (Activity)Xamarin.Forms.Forms.Context;
toolbar = context.FindViewById<Android.Support.V7.Widget.Toolbar>(Droid.Resource.Id.toolbar);
if (toolbar != null)
{
if (toolbar.NavigationIcon != null)
{
toolbar.NavigationIcon = Android.Support.V4.Content.ContextCompat.GetDrawable(context, Resource.Drawable.Back);
//toolbar.SetNavigationIcon(Resource.Drawable.Back);
}
}
return retVal;
}
}
The CustomNavigationPage are defined in PCL :
public class CustomNavigationPage : NavigationPage
{
public CustomNavigationPage(Page startupPage) : base(startupPage)
{
}
}
Usage :
public App()
{
InitializeComponent();
MainPage = new CustomNavigationPage(new MainPage());
}
...
// In MainPage
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestPage());
}
I solved this the next way:
In my MainActivity i am added static toolbar property and identified it in OnCreateOptionsMenu
public static Toolbar ToolBar { get; private set; }
public override bool OnCreateOptionsMenu(IMenu menu)
{
ToolBar = FindViewById<Toolbar>(Resource.Id.toolbar);
ToolBar.SetNavigationIcon(Resource.Drawable.arrow);
return base.OnCreateOptionsMenu(menu);
}
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
....
}
Then in PageRenderer:
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
MainActivity.ToolBar?.SetNavigationIcon(Resource.Drawable.arrow);
}
But!! From 2 ways i have bad effect with redrawing
Usage:
async void tapImage_Tapped(object sender, EventArgs e)
{
await Navigation.PushAsync(new ChooseGenrePage(_listGenres));
}
SignUpFragment uses SignUpPresenter and SignUpFragment inplements SignUpView. SingUpPresenter extends BasePresenter
where BasePresenter:
public abstract class BasePresenter<V> {
private WeakReference<V> mView;
public void bindView(#NonNull V view) {
mView = new WeakReference<>(view);
if (setupDone()) {
updateView();
}
}
public void unbindView() {
mView = null;
}
protected V view() {
if (mView == null) {
return null;
} else {
return mView.get();
}
}
protected abstract void updateView();
private boolean setupDone() {
return view() != null;
}
}
public interface SignUpView extends BaseView {
void showResult(UserInfo result);
}
SignUpPresenter connects with SignUpFragment via view() like:
view().showResult()
view().showError()
I want to know if in SignUpPresenter I want to add validation via RxAndroid:
Observable<CharSequence> loginObservable = RxTextView.textChanges(mEmail);
I mean I want to have access to mEmail of SignUpFragment in SignUpPresenter. Is it ok to add method in SignFramgnet method like:
public EditText getEditTextEmail(){return mEmail;}
Which I could use in SignUpPresenter like mEail = view().getEditTextEmail();
Or I need to add all this part in Activity/Fragment:
Observable<CharSequence> loginObservable = RxTextView.textChanges(mLogin);
loginObservable
.map(this::isValidLogin)
.subscribe(isValid -> mLogin.setCompoundDrawablesRelativeWithIntrinsicBounds(null,null, (isValid ? mValidField : mInvalidField), null));
Create loginObservable in your View and pass it to Presenter. Observable<CharSequence> is not a part of Android Framework so it can be easily unit-tested.
//View
Observable<CharSequence> loginObservable = RxTextView.textChanges(mEmail);
presenter.setLoginObservable(loginObservable);
//Presenter
void setLoginObservable(Observable<CharSequence> observable) {
observable
.map(this::isValidLogin)
.subscribe(isValid -> {
//call appropriate view methods
});
I have two Activities like
[Activity(Label = "View for FirstViewModel", MainLauncher = true)]
public class FirstView : MvxActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.FirstView);
}
}
[Activity(Label = "View for SecondViewModel", MainLauncher = false)]
public class SecondView : MvxActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.SecondView);
}
}
and two ViewModels
public class FirstViewModel
: MvxViewModel
{
private string _hello = "Hello MvvmCross 1";
public FirstViewModel()
{
}
public string Hello
{
get { return _hello; }
set { _hello = value; RaisePropertyChanged(() => Hello); }
}
public ICommand Next
{
get
{
return new MvxCommand(() => ShowViewModel<SecondViewModel>());
}
}
}
public class SecondViewModel : MvxViewModel
{
public SecondViewModel()
{
}
private string _hello = "Hello MvvmCross 2";
public string Hello
{
get { return _hello; }
set { _hello = value; RaisePropertyChanged(() => Hello); }
}
public ICommand Back
{
get
{
return new MvxCommand(() => ShowViewModel<FirstViewModel>());
}
}
}
I'm trying just two switch back and forth between them. the back step just does not work. Logging looks okay to me. Using the same VMs with an WPF project works just fine. So what the problem with androids activities?
Thanks in advance!
MH