Replace fragments in viewpager Mvvmcross - xamarin.android

tldr: How can I replace fragments in viewpager using MvvmCross?
I have tried to replace a fragment in a viewpager using the approach described at How to replace fragments within a Viewpager. The idea is that within the view pager a "RootFragment" is shown. However, it is replaced by a first fragment. If needed, it can also be replaced by a second fragment, third fragment and so on. As more fragments are added, they are added to the backstack as well.
So this would be a snippet of the code for the "RootFragment"
public class RootView : MvxFragment
{
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = inflater.Inflate(Resource.Layout.root_fragment, container, false);
var transaction = FragmentManager.BeginTransaction();
var fragment = FirstView.NewInstance();
transaction.Replace(Resource.Id.root_frame, fragment);
transaction.Commit();
return view;
}
}
Then, a snippet for the "First fragment" would be
[MvxFragmentPresentation(typeof(MainViewModel),
Resource.Id.root_frame, true)]
public class ScoringView : MvxFragment<FirstViewModel>
{
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Create your fragment here
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.OnCreateView(inflater, container, savedInstanceState);
return this.BindingInflate(Resource.Layout.first_fragment, null);
}
}
The layout for "RootFragment" is:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="#000"
android:layout_height="match_parent"
android:id="#+id/root_frame" />
The code for "FirstViewModel" would be:
public class FirstViewModel: MvxViewModel
{
private IMvxAsyncCommand _openPreguntasCommand;
private IMvxNavigationService _navigationService;
public IMvxAsyncCommand OpenSecondCommand { get => _openSecondCommand?? (new MvxAsyncCommand(async () =>
{
await _navigationService
.Navigate<SecondViewModel>();
})); }
public FirstViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
}
}
I have tried to accomplish this task using a PresentationHint. I modified the code used in How to clear entire Backstack using viewmodel(navigation service) to suit my needs to no avail. So perhaps a custom presenter could be an option. I could override one "OnBeforeFragmentChanging" or some other related method. What I ended up doing was that used the MvxFragmentPresentation attribute and I set the fragmentContentId to the Id of the placeholder in the "RootFragment" layout. However, it seems to me that there has to be a better way.
[MvxFragmentPresentation(typeof(MainViewModel), Resource.Id.root_frame, true)]

Did you try the Playground example that comes with the MvvmCross source code? There is a good implementation of fragments

Related

ViewPager not Loading Fragments when using TabLayout

I got a simple layout with TabLayout and PageViewer but can't get them to work together
<com.google.android.material.tabs.TabLayout
android:id="#+id/sliding_tabs_emoji"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:tabMode="scrollable"
app:tabGravity="fill"
app:tabIndicatorColor="#color/gray"
app:tabIndicatorHeight="1dp"
android:paddingBottom="2dp"/>
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewpager_emoji"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/red"/>
In my Activity OnCreate initializing the following
//Fragment array
var fragments = new AndroidX.Fragment.App.Fragment[]
{
Library.Fun.Emoji.Fragments.Recent.NewInstance(),
Library.Fun.Emoji.Fragments.People.NewInstance(),
};
//Tab title array
var titles = Android.Runtime.CharSequence.ArrayFromStringArray(new[] {
"Recent" ,
"People"
});
var viewPager = FindViewById<ViewPager>(Resource.Id.viewpager_emoji);
//viewpager holding fragment array and tab title text
viewPager.Adapter = new EmojiTabsPagerAdapter(SupportFragmentManager, fragments, titles);
// Give the TabLayout the ViewPager
TAB_Layout.SetupWithViewPager(viewPager);
Toast.MakeText(this, "SET", ToastLength.Short);
TAB_Layout.GetTabAt(0).SetIcon(Resource.Drawable.ic_camera);
TAB_Layout.GetTabAt(1).SetIcon(Resource.Drawable.ic_camera);
Where the Adapter is simple as it could be as following
public class EmojiTabsPagerAdapter : FragmentPagerAdapter
{
private readonly AndroidX.Fragment.App.Fragment[] fragments;
private readonly ICharSequence[] titles;
public EmojiTabsPagerAdapter(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment[] fragments, ICharSequence[] titles) : base(fm)
{
this.fragments = fragments;
this.titles = titles;
}
public override int Count
{
get
{
return fragments.Length;
}
}
public override AndroidX.Fragment.App.Fragment GetItem(int position)
{
return fragments[position];
}
public override ICharSequence GetPageTitleFormatted(int position)
{
//return titles[position];
return null;
}
}
Tabs are appearing but Fragments are not loading, The results are the following
Where it should load RECENT and PEOPLE fragments on each tab, my Fragment XML is the following
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="#color/green">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="RECENTS"/>
</RelativeLayout>
And fragment's code is the following (for RECENT)
public class Recent : AndroidX.Fragment.App.Fragment
{
public static Recent NewInstance()
{
var frag = new Recent { Arguments = new Bundle() };
return frag;
}
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Create your fragment here
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.Inflate(Resource.Layout.fragment_emoji_recent, container, false);
return view;
}
}
Any Idea what am I doing wrong?
I use two Fragments: TabFragment1, TabFragment2 to test. In your code, you did provide the code about SetupWithViewPager.
The code below works well for me.
void setupViewPager(Android.Support.V4.View.ViewPager viewPager)
{
var adapter = new Adapter(SupportFragmentManager);
adapter.AddFragment(new TabFragment1(), "First Fragment");
adapter.AddFragment(new TabFragment2(), "Second Fragment");
viewPager.Adapter = adapter;
viewpager.Adapter.NotifyDataSetChanged();
//viewpager.OffscreenPageLimit(4);
}
In MainActivity, you could set like below.
var tabLayout = FindViewById<TabLayout>(Resource.Id.tabs);
tabLayout.SetupWithViewPager(viewpager);
You could download the source file from the link: https://www.c-sharpcorner.com/article/xamarin-android-create-viewpager-tablayout-floatingactionbutton-supportacti/
Did this work before moving to AndroidX?
I think you may need to pass ChildFragmentManager instead of FragmentManager to your PagerAdapter.

Avoid refreshing of content in SplitLayout

I have a splitlayout in my vaadin application in which in the first column I should show different pages added through the addToPrimary method, while in the second column I should have a page which contains an IFrame with a videoconference. Now the problem is that when I change route in the first column, even the second is updated and this refreshes the IFrame. The implementation of the showRouterLayoyutContent is the following:
#Override
public void showRouterLayoutContent(HasElement content) {
if (this.accessControl.isAccessGranted(UI.getCurrent(), ((ContentView) content).getName()) && ((ContentView) content).getName().equals("contattaView") ) {
setLayoutCall((com.vaadin.flow.component.Component) content);
}
else if (this.accessControl.isAccessGranted(UI.getCurrent(), ((ContentView) content).getName())) {
setLayoutContent((com.vaadin.flow.component.Component) content);
}
}
And the two methods setLayoutCall and setLayoutContent are the following:
private void setLayoutContent(com.vaadin.flow.component.Component content) {
split.addToPrimary(content);
}
private void setLayoutCall(com.vaadin.flow.component.Component content) {
split.addToSecondary(content);
split.setThemeName("visible-split");
}
How can I avoid to refresh the entire content when I update the first column of the split layout through navigation?
UPDATE: I'm showing also a very simple code on which I'm testing. The following class is the main layout:
private SplitLayout split = new SplitLayout();
private HorizontalLayout hl = new HorizontalLayout();
private Div firstDiv = new Div();
private Div secondDiv = new Div();
public MainView() {
Button button = new Button("Click me",
event -> Notification.show("Clicked!"));
final VerticalLayout menuBar = new VerticalLayout();
menuBar.add(new RouterLink("first view", FirstView.class));
menuBar.add(new RouterLink("second view", SecondView.class));
menuBar.setAlignItems(Alignment.CENTER);
add(menuBar);
//split.addToPrimary(firstDiv);
//split.addToSecondary(secondDiv);
//firstDiv.setId("first");
//secondDiv.setId("second");
//hl.add(firstDiv,secondDiv);
add(split);
//add(hl);
}
#Override
public void showRouterLayoutContent(HasElement element) {
if(element!=null && element.getClass().getName().contains("FirstView")) {
split.addToPrimary((Component) element);
//firstDiv.removeAll();
//firstDiv.add((Component) element);
//firstDiv.removeAll();
//firstDiv.getElement().appendChild(new Element[]{element.getElement()});
}
else if(element!=null && element.getClass().getName().contains("SecondView") ) {
secondDiv.removeAll();
secondDiv.add((Component) element);
split.addToSecondary((Component) element);
//split.addToSecondary(element.getElement().getComponent().get());
}
}
While these are the two views added to the split:
#Route(value="v1",layout=MainView.class)
public class FirstView extends VerticalLayout implements RouterLayout {
public FirstView() {
add(new Label("First View"));
}
}
#Route(value = "v2",layout=MainView.class)
public class SecondView extends VerticalLayout implements RouterLayout {
public SecondView() {
IFrame frame = new IFrame();
frame.setSrc("https://www.youtube.com/watch?v=LoigVtPCYPk&list=RDLoigVtPCYPk&start_radio=1");
add(frame);
}
}
Your comment does indeed seem to be the issue.
I recommend creating a Div wrapper for the primary content, and instead changing the content of that.
private final Div wrapper;
public MyLayout() {
wrapper = new Div();
wrapper.setSizeFull();
split.addToPrimary(wrapper);
}
private void setLayoutContent(com.vaadin.flow.component.Component content) {
wrapper.removeAll();
wrapper.add(content);
}
You might also want to do the same for the secondary. In addition, to prevent any components from being automatically removed when navigating, you can override removeRouterLayoutContent as well (available in Vaadin 14)
#Override
public void removeRouterLayoutContent(HasElement oldContent) {
// Do nothing, we remove manually in showRouterLayoutContent
}
Edit
If you can't override removeRouterLayoutContent, you can try creating your own instance of the HasElement to add. This is a bit of a hack, but might be the simplest solution.
public void showRouterLayoutContent(HasElement content) {
if (content.getClass().getSimpleName().contains("TestView")) {
// Creating a new instance should stop it from being auto removed
content = new TestView();
firstDiv.add((Component) content);
}
...
}

Could not get or set edittext value from separate OnClickListener class using android studio 3

Using eclipse i was able to get edittext value from separate onclicklistener class, but after i transfer the same style of code to android studio 3 the edittext returns empty here is my code
public class MainActivity extends AppCompatActivity {
Button btnGenerate;
private static final String TAG = MainActivity.class.getSimpleName();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//getting the generate_btn actually works
btnGenerate = findViewById(R.id.generate_btn);
btnGenerate.setOnClickListener(new OnClickListenerGenerate());
}
}
And here is the OnClickListener class
public class OnClickListenerGenerate implements View.OnClickListener {
EditText second_digit;
private static final String TAG = OnClickListenerGenerate.class.getSimpleName();
#Override
public void onClick(View view) {
final Context context = view.getRootView().getContext();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View formElementsView = inflater.inflate(R.layout.activity_main, null, false);
second_digit = formElementsView.findViewById(R.id.txtSecondDigit);
//returns empty
Log.d(TAG, ">>" + second_digit.getText().toString());
}
}
I found the answer from this link Android::findViewByID - how can I get View of a TextView through a Listener of another UI element? and now it's working.
class ButtonListener implements android.view.View.OnClickListener {
public void onClick(View v) {
View parent = (View)v.getParent();
if (parent != null) {
TextView txtView = parent.findViewById(R.id.mytextview);
txtView.setText(...);
}
}
}

populate images with firebase recycler adapter through databinding

I am populating a firebase data with firebase recycler adapter through databinding and want to populate images with the data either from Picasso or Glide, but unable to do. So can any one help me what to write and where to write the code of either Picasso or Glide.
public class ShopByCategoryFragment extends Fragment {
FirebaseRecyclerAdapter adapter;
Firebase mFirebaseRef = new Firebase("https://abc.firebaseio.com/").child("subCategories");
public ShopByCategoryFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.recycler_view, container, false);
final RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
adapter = new FirebaseRecyclerAdapter<SubCategories, ViewHolder>(SubCategories.class, R.layout.fragment_shop_by_category,
ViewHolder.class, mFirebaseRef) {
#Override
protected void populateViewHolder(ViewHolder viewHolder, SubCategories subCategories, int i) {
FragmentShopByCategoryBinding binding = viewHolder.getBinding();
binding.setSubCategories(subCategories);
}
};
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
return rootView;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public FragmentShopByCategoryBinding binding;
public ViewHolder(View itemView) {
super(itemView);
binding = DataBindingUtil.bind(itemView);
}
public FragmentShopByCategoryBinding getBinding() {
return binding;
}
}
#Override
public void onDestroy() {
super.onDestroy();
adapter.cleanup();
}
}
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="SubCategories"
type="com.abc.www.shopping.model.SubCategories"/>
</data>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="#dimen/tile_height"
android:id="#+id/sub_category_image"
android:scaleType="centerCrop"
android:src="#{SubCategories.image}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="#dimen/tile_height"
android:text="#{SubCategories.title}"
android:id="#+id/sub_category_title"
android:padding="16dp"
android:textColor="#color/colorSubCategoryTitle"
android:layout_gravity="center"
android:textAppearance="#android:style/TextAppearance.Material.Title"
tools:targetApi="lollipop" />
</FrameLayout>
</android.support.v7.widget.CardView>
public class SubCategories extends BaseObservable {
private String title;
private String image;
public SubCategories() {
// empty constructor
}
public SubCategories(String title, String image) {
this.title = title;
this.image = image;
}
public String getTitle() {
return title;
}
public String getImage() {
return image;
}
}
I have made something similar to what you need. I'll share my code and do a little explanation.
I have a class that returns the FirebaseRecyclerAdapter like this:
public RecyclerView.Adapter postAdapter(String userKey, final Context context, MyClickListener clickListener) {
Query userPostRef = postRef.child(userKey);
myClickListener = clickListener;
return new FirebaseRecyclerAdapter<PostModel, DataObjectHolder>(PostModel.class, R.layout.stream_layout, DataObjectHolder.class, userPostRef) {
#Override
public void populateViewHolder(final DataObjectHolder dataviewHolder, final PostModel postModel, int position) {
StorageReference mStorageRef = FirebaseStorage.getInstance().getReference();
mStorageRef.child("uploaded_captures/" + postModel.getImageFilename() + ".jpg").getDownloadUrl()
.addOnSuccessListener(new OnSuccessListener<Uri>() {
#Override
public void onSuccess(Uri uri) {
dataviewHolder.setImage(uri.toString(), context);
dataviewHolder.setComment(postModel.getPostComment());
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
Log.e("FirebaseRecyclerAdapter", exception.getMessage());
}
});
}
};
}
and a ViewHolder that loads the image into the ImageView used by the layout:
private static class DataObjectHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
View mView;
public DataObjectHolder(View itemView) {
super(itemView);
mView = itemView;
}
void setImage(final String imageURL, final Context context) {
final ImageView postImage = (ImageView) mView.findViewById(R.id.imageView);
if (BuildConfig.DEBUG) {
Picasso.with(context).setIndicatorsEnabled(true);
}
Picasso.with(context)
.load(imageURL)
.networkPolicy(NetworkPolicy.OFFLINE)
.placeholder(R.drawable.ic_menu_camera)
.fit().centerCrop().into(postImage, new Callback() {
#Override
public void onSuccess() {
}
#Override
public void onError() {
Picasso.with(context)
.load(imageURL)
.placeholder(R.drawable.ic_menu_camera)
.fit().centerCrop().into(postImage);
}
});
}
void setComment(String text) {
TextView comment = (TextView) mView.findViewById(R.id.comment);
comment.setText(text);
}
#Override
public void onClick(View v) {
myClickListener.onItemClick(getAdapterPosition(), v);
}
}
On Picasso I'm using the NerworkPolicy.OFFLINE to check if the image is on the disk cache before trying to get it from the network. It's not perfect and I'm still having some issues with the delay of the images once the ViewHolder it's been recycled but I think it's a start for you to go on.
In my ViewHolder I do this by:
Glide.with(context)
.load(chat.getImageUrl())
.into(imageView);
I haven't tried using data binding for this yet, but the code is simple enough.

Injecting fragment and configuration change handling

public class HostActivity extends Activity {
#Inject HostedFragment fragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_host);
ObjectGraph.create(new HostActivityModule()).inject(this);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();
}
}
public static class HostedFragment extends Fragment{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_hosted, container, false);
}
}
#Module(injects = HostActivity.class)
public static class HostActivityModule {
#Provides #Singleton
HostedFragment provideHostedFragment() {
return new HostedFragment();
}
}
}
In a new project, I start to use dagger and nested fragment and come got a 2 questions in my mind.
1. Should fragment be injected into activity or another fragment?
2. What is the correct way to inject fragment which handle recreation after configuration change?
The problem I encounter is in the above code, another HostedFragment will be created and injected into HostActivity.
if (savedInstanceState == null) {
ObjectGraph.create(new HostActivityModule()).inject(this);
getFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();
}
Modifying to the above version might avoid duplicate HostedFragment being created but if we need injections other then fragment, they are not injected during recreation.
Can anyone help me?
I come up with an approach, am I correct?
The only thing bother me is if HostedFragment is not specified in the #Module inject, I cannot get it from the graph.
public class HostActivity extends Activity {
private static final String FRAGMENT_TAG = "fragment tag";
private ObjectGraph objectGraph;
private HostedFragment fragment;
#Inject LocationManager locationManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_host);
// Injecting fields that are required
objectGraph = ObjectGraph.create(new HostActivityModule(this));
objectGraph.inject(this);
// Injecting fragment
fragment = (HostedFragment) getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
if (fragment == null) {
fragment = objectGraph.get(HostedFragment.class);
getFragmentManager().beginTransaction().add(R.id.fragment_container, fragment, FRAGMENT_TAG).commit();
}
}
public static class HostedFragment extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_hosted, container, false);
}
}
#Module(
injects = {
HostActivity.class,
HostedFragment.class
},
library = true
)
public static class HostActivityModule {
private Context mContext;
public HostActivityModule(Context mContext) {
this.mContext = mContext;
}
#Provides #Singleton HostedFragment provideHostedFragment() {
return new HostedFragment();
}
#Provides #Singleton LocationManager provideLocationManager() {
return (LocationManager) mContext.getSystemService(LOCATION_SERVICE);
}
}
}

Resources