I am trying to implement tabs through MvvmCross in Xamarin. I came across MvxTabActivity in Android and MvxTabBarViewController in IOS. Both are working well. The problem is MvxTabActivity is obselete. Are there any alternatives for MvxTabActivity?
I found another way to implement this, which uses TabLayout and a ViewPager.
The solution asks to use fragments within a fragment. I have pasted the code for this approach. The problem here is on swiping the tabs, all the data in previous tabs is lost.
I tried using RetainInstance = true, that gave following exception : "Can't retain fragements that are nested in other fragments."
Product Detail Activity :
[Activity(Label = "ProductDetailView")]
public class ProductDetailView : MvxAppCompatActivity<ProductDetailViewModel>
{
private FrameLayout _mainFrame;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.product_detail_view);
if (FindViewById<FrameLayout>(Resource.Id.frame_Detail) != null)
{
var frag = new NutritionCategoryView();
frag.ViewModel = ViewModel.NutritionCategoryModel;
var trans = SupportFragmentManager.BeginTransaction();
trans.Replace(Resource.Id.frame_Detail, frag);
trans.AddToBackStack(null);
trans.Commit();
}
}
}
Nutrition Category View Fragment :
public class NutritionCategoryView : MvxFragment
{
public NutritionCategoryViewModel vm
{
get { return (NutritionCategoryViewModel) ViewModel; }
}
private TabLayout _tablayout;
private ViewPager _viewPager;
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
this.EnsureBindingContextIsSet(inflater);
var view = this.BindingInflate(Resource.Layout.nutrition_category_view, container, false);
SetViewPager(view);
return view;
}
private void SetViewPager(View view)
{
_viewPager = view.FindViewById<Android.Support.V4.View.ViewPager>(Resource.Id.viewpager);
if (_viewPager != null)
{
var fragments = new List<CategoryTabsAdapter.FragmentInfo>
{
new CategoryTabsAdapter.FragmentInfo
{
FragmentType = typeof(CategoryView),
Title = "Proximates",
ViewModel = vm.Category1
},
new CategoryTabsAdapter.FragmentInfo
{
FragmentType = typeof(CategoryView),
Title = "Minerals",
ViewModel = vm.Category2
},
new CategoryTabsAdapter.FragmentInfo
{
FragmentType = typeof(CategoryView),
Title = "Fats",
ViewModel = vm.Category3
},
new CategoryTabsAdapter.FragmentInfo
{
FragmentType = typeof(CategoryView),
Title = "Vitamins",
ViewModel = vm.Category4
}
};
_viewPager.Adapter = new CategoryTabsAdapter(Activity, ChildFragmentManager, fragments);
}
_tablayout = view.FindViewById<TabLayout>(Resource.Id.sliding_tabs);
_tablayout.SetBackgroundColor(Android.Graphics.Color.Black);
_tablayout.SetupWithViewPager(_viewPager);
}
}
Category Tabs Adapter :
public class CategoryTabsAdapter : FragmentStatePagerAdapter
{
private readonly Context _context;
public IEnumerable<FragmentInfo> Fragments { get; private set; }
public CategoryTabsAdapter(Context context, FragmentManager fragmentManager, IEnumerable<FragmentInfo> fragments) : base(fragmentManager)
{
_context = context;
Fragments = fragments;
}
public override int Count
{
get { return Fragments.Count(); }
}
public override Fragment GetItem(int position)
{
var fragmentInfo = Fragments.ElementAt(position);
var fragment = Fragment.Instantiate(_context, Java.Lang.Class.FromType(fragmentInfo.FragmentType).Name);
((MvxFragment)fragment).ViewModel = fragmentInfo.ViewModel;
return fragment;
}
public override ICharSequence GetPageTitleFormatted(int position)
{
return new Java.Lang.String(Fragments.ElementAt(position).Title);
}
public class FragmentInfo
{
public string Title { get; set; }
public Type FragmentType { get; set; }
public IMvxViewModel ViewModel { get; set; }
}
}
Category View Fragment
public class CategoryView : MvxFragment<CategoryViewModel>
{
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
this.EnsureBindingContextIsSet(inflater);
var view = this.BindingInflate(Resource.Layout.category_view, container, false);
//Exception caused here :
//RetainInstance = true;
return view;
}
}
I am new to Xamarin and MvvmCross, so could come up with this much research only. Any solution for either approaches would be of great help.
P.S. This is my first question on Stackoverflow.
I think you need to use the MvxCachingFragmentStatePagerAdapter which is in the MvvmCross.Droid.Support.V4 Nuget package. Then hook it up to your TabLayout with SetupWithViewPager().
var viewPager = view.FindViewById<ViewPager>(Resource.Id.viewpager);
if (viewPager != null)
{
var fragments = new List<MvxCachingFragmentStatePagerAdapter.FragmentInfo>
{
new MvxCachingFragmentStatePagerAdapter.FragmentInfo(
"TitleA",
typeof (YourFragmentA),
typeof (YourViewModelA)),
new MvxCachingFragmentStatePagerAdapter.FragmentInfo(
"TitleB",
typeof (YourFragmentB),
typeof (YourViewModelB)),
new MvxCachingFragmentStatePagerAdapter.FragmentInfo(
"TitleC",
typeof (YourFragmentC),
typeof (YourViewModelC))
};
viewPager.Adapter = new MvxCachingFragmentStatePagerAdapter(Activity, ChildFragmentManager, fragments);
viewPager.OffscreenPageLimit = fragments.Count;
var tabLayout = view.FindViewById<TabLayout>(Resource.Id.tabs);
tabLayout.SetupWithViewPager(viewPager);
}
Your tabs are being recreated each time because you are using FragmentStatePagerAdapter instead of FragmentPagerAdapter. From the docs, FragmentPagerAdapter:
This version of the pager is best for use when there are a handful of
typically more static fragments to be paged through, such as a set of
tabs. The fragment of each page the user visits will be kept in
memory, though its view hierarchy may be destroyed when not visible.
This can result in using a significant amount of memory since fragment
instances can hold on to an arbitrary amount of state. For larger sets
of pages, consider FragmentStatePagerAdapter.
FragmentStatePagerAdapter:
This version of the pager is more useful when there are a large number
of pages, working more like a list view. When pages are not visible to
the user, their entire fragment may be destroyed, only keeping the
saved state of that fragment. This allows the pager to hold on to much
less memory associated with each visited page as compared to
FragmentPagerAdapter at the cost of potentially more overhead when
switching between pages.
So use FragmentPagerAdapter, but I think it will have to be at the Activity level, and not a fragment within a fragment.
Related
In my app I am using a fragment and inside the fragment I am trying to use
view.FindViewById<TextView>(Resource.Id.tv1).Typeface = Typeface.CreateFromAsset(Assets, "fonts/Montserrat_Light.ttf");
to change the typeface of a textview. But I am getting an error that 'The name 'assets' does not exist in the current context'
This is the fragment code
public class xFragment : Fragment
{
const string ARG_PAGE = "ARG_PAGE";
private int mPage;
public static xFragment newInstance(int page)
{
var args = new Bundle();
args.PutInt(ARG_PAGE, page);
var fragment = new xFragment ();
fragment.Arguments = args;
return fragment;
}
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
mPage = Arguments.GetInt(ARG_PAGE);
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = inflater.Inflate(Resource.Layout.AgeLayout, container, false);
view.FindViewById<TextView>(Resource.Id.tv1).Typeface = Typeface.CreateFromAsset(Assets, "fonts/Montserrat_Light.ttf");
if (mPage == 2)
{
view = inflater.Inflate(Resource.Layout.AgeERLayout, container, false);
}
return view;
}
}
I found the solution and was so easy. I just had to use Activity.Assets
i'm facing a small issue figuring out on the following -
I have a RecyclerView in my MainActivity, the RecyclerView has CardView within it.
The data displays on the MinActivity page, cuz the RecyclerView is in it.
Now, I want a new activity to pop-up once a CardView is clicked.
The new activity will use the same data as the CardView had.
I'm using Firebase storage if it matters.
Example -
My CardView 1 has a name, age and country - Jake, 19, UK.
The other activity will get the collection data from Firebase which is the exact same as CardView 1 and implement it in to the new activity.
I hope I explained it well..
(I'm using getters and setters)
My code so far (Only the needed parts)-
MainActivity
list_post = new ArrayList<>();
list_header = new ArrayList<>();
postsAdapter = new PostsAdapter(list_post);
headerAdapter = new HeaderAdapter(list_header);
headerRecycler = (RecyclerView) findViewById(R.id.headerRecycler);
headerRecycler.setAdapter(headerAdapter);
headerRecycler.setLayoutManager(new LinearLayoutManager(this));
firebaseFirestore = FirebaseFirestore.getInstance();
firebaseFirestore.collection("FeaturedPosts").limit(1).addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(QuerySnapshot queryDocumentSnapshotss, FirebaseFirestoreException ee) {
for (DocumentChange doc2: queryDocumentSnapshotss.getDocumentChanges()) {
if (doc2.getType() == DocumentChange.Type.ADDED) {
ListForHeader listForHeader = doc2.getDocument().toObject(ListForHeader.class);
list_header.add(listForHeader);
headerAdapter.notifyDataSetChanged();
}
}
}
});
RecyclerAdapter
public class HeaderAdapter extends RecyclerView.Adapter<HeaderAdapter.ViewHolder2> {
public List<ListForHeader> list_header;
public Context contextt;
public HeaderAdapter (List<ListForHeader> list_header) {
this.list_header = list_header;
}
#NonNull
#Override
public HeaderAdapter.ViewHolder2 onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.featured_activity, parent, false);
contextt = parent.getContext();
return new HeaderAdapter.ViewHolder2(view);
}
#Override
public void onBindViewHolder(#NonNull HeaderAdapter.ViewHolder2 holder, int position) {
String header_data2 = list_header.get(position).getHeader2();
holder.setHeaderText2(header_data2);
String date_data2 = list_header.get(position).getDate2();
holder.setDateText2(date_data2);
String image_data2 = list_header.get(position).getImage_url2();
holder.setIntroIMG2(image_data2);
}
#Override
public int getItemCount() {
return list_header.size();
}
public class ViewHolder2 extends RecyclerView.ViewHolder {
private View mView;
private ImageView introIMG2;
private TextView headerText2;
private TextView dateText2;
public ViewHolder2(View itemView) {
super(itemView);
mView = itemView;
}
public void setHeaderText2(String headText2) {
headerText2 = mView.findViewById(R.id.introHeader2);
headerText2.setText(headText2);
}
public void setDateText2(String tarihText2) {
dateText2 = mView.findViewById(R.id.introDate2);
dateText2.setText(tarihText2);
}
public void setIntroIMG2 (String downloadUri) {
introIMG2 = (ImageView) mView.findViewById(R.id.introImage2);
Glide.with(contextt).load(downloadUri).into(introIMG2);
}
}
}
As far as I see its simple issue. You can add cardview Id to viewHolder and set onlclicklister to it in bindview and in on click Start activity and pass the current data in Intent. Let me know if its something else stopping you from sending data or getting click on cardview.
Extra: you may have to set cardview clickable true.
here is the example.
class DemoViewHolder extends RecyclerView.ViewHolder implements
View.OnClickListener {
#BindView(R.id.card)
CardView cardView;
public DemoViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
cardView.setOnClickListener(this);
}
#Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.card:
doSomethingOn(mList.get(getAdapterPosition()))
break;
}
}
public class BookingAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
Context mContext;
ArrayList<Booking> mList;
public BookingAdapter(Context context, ArrayList<Booking> list) {
mContext = context;
mList = list;
this.cancelBookingListener = cancelBookingListener;
}
I'm using Firebase3 in Ionic2 App. The requirement is to load data of approx 2000 objects and every time when page switched(using navigation) the data needs to be loaded. But loading every time data does not looks well.
So, is there any solution to fetch data once - like contacts list.
Then the loaded data will be displayed from local.
Thanks!
Try this code :-
private List<Contact> contactList;
private SharedPreferences shared ;
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
shared = getSharedPreferences("AppPref", MODE_PRIVATE);
//**Get the data from local storage**
List<Contacts> contactList = new Gson().fromJson((shared.getString("contacts", "")),new TypeToke<List<Contact>>(){}.getType());
if(contactList ==null) { // if no data found then call firebase service
contactList=new ArrayList();
//Use addListenerForSingleValueEvent instead of ValueEventListener.So this will listen only once.
mDataBaseRef.child("Your_Node").addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for(DataSnapshot postSnapshot : dataSnapshot.getChildren()){
contactList.add(postSnapshot.getValue(Contact.class);
}
//Save the data in Shared Preferences
SharedPreferences.Editor editor = shared.edit();
editor.putString("contacts", new Gson().toJson(contactList));
editor.commit();
}
#Override
public void onCancelled(FirebaseError firebaseError) {
contactList = null ;
}
});
}
return view ;
}
I have two fragments which are VolleyFragment and AnotherFragment, setting VolleyFragment as the initial fragment. In this fragment, the bundle data is displaying ok. However, when I go to AnotherFragment, the app crashes. It says this error:
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String[] android.os.Bundle.getStringArray(java.lang.String)' on a null object reference
at com.example.rendell.volleyfragment.AnotherFragment.onCreateView(AnotherFragment.java:28)
MainActivity
public class MainActivity extends AppCompatActivity {
public static final String JSON_URL = "http://192.168.0.102/musicmania/music/getMF";
TextView id,name,email;
String[] ids, names, emails;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
id = (TextView) findViewById(R.id.id);
name = (TextView) findViewById(R.id.name);
email = (TextView) findViewById(R.id.email);
sendRequest();
}
private void sendRequest(){
StringRequest stringRequest = new StringRequest(JSON_URL,
new Response.Listener<String>() {
#Override
public void onResponse(String response) {
showJSON(response);
}
},
new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_LONG).show();
}
});
RequestQueue requestQueue = Volley.newRequestQueue(this);
requestQueue.add(stringRequest);
}
private void showJSON(String json){
ParseJSON pj = new ParseJSON(json);
pj.parseJSON();
id.setText(ParseJSON.ids[0]);
name.setText(ParseJSON.titles[0]);
email.setText(ParseJSON.artists[0]);
FragmentManager fragmentTransaction = getFragmentManager();
android.app.FragmentTransaction transaction = fragmentTransaction.beginTransaction();
VolleyFragment squadFragment = new VolleyFragment();
AnotherFragment anotherFragment = new AnotherFragment();
Bundle bundle = new Bundle();
bundle.putStringArray("id", ParseJSON.ids);
bundle.putStringArray("name", ParseJSON.titles);
bundle.putStringArray("email", ParseJSON.artists);
squadFragment.setArguments(bundle);
anotherFragment.setArguments(bundle);
transaction.replace(R.id.containerView, squadFragment);
transaction.commit();
}
public void changeFragment(View view) {
FragmentManager fragmentTransaction = getFragmentManager();
android.app.FragmentTransaction transaction = fragmentTransaction.beginTransaction();
AnotherFragment anotherFragment = new AnotherFragment();
transaction.replace(R.id.containerView, anotherFragment);
transaction.commit();
}
}
VolleyFragment
public class VolleyFragment extends Fragment {
TextView id,name,email;
String[] ids,names,emails;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.volley_fragment, container, false);
id = (TextView) view.findViewById(R.id.id);
name = (TextView) view.findViewById(R.id.name);
email = (TextView) view.findViewById(R.id.email);
ids = getArguments().getStringArray("id");
names = getArguments().getStringArray("name");
emails = getArguments().getStringArray("email");
//jsonArray.setIds(id);
id.setText(/*jsonArray.getIds()*/ids[0]);
name.setText(names[0]);
email.setText(emails[0]);
return view;
}
}
AnotherFragment
public class AnotherFragment extends Fragment {
TextView id,name,email;
String[] ids,names,emails;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.another_fragment, container, false);
id = (TextView) view.findViewById(R.id.id);
name = (TextView) view.findViewById(R.id.name);
email = (TextView) view.findViewById(R.id.email);
ids = getArguments().getStringArray("id");
names = getArguments().getStringArray("name");
emails = getArguments().getStringArray("email");
//jsonArray.setIds(id);
id.setText(/*jsonArray.getIds()*/ids[0]);
name.setText(names[0]);
email.setText(emails[0]);
return view;
}
}
I'm having trouble following exactly what you're doing here, but I see a number of problems:
In showJSON, you new up a ParseJSON object and assign it to pj, but never make use of pj. Instead you reach statically into ParseJSON for the values to populate a Bundle. That seems really odd. I'd expect the pj instance to contain the data.
Typically one does not "new" up a fragment. The usual pattern is to use a static builder in the class of the fragment itself.
You're passing the same Bundle to two different fragments. Unless you know for a fact that's OK, try instead using a different Bundle for each one.
in showJSON, your AnotherFragment instance doesn't seem to be used at all
I don't see any place where changeFragment is called
You're passing string arrays in the Bundle, but you only ever use the first element in the array in the fragment. Consider just passing a single string instead?
My Xamarin Android app uses fragments for each view rather than separate activities, and one of the fragments needs to contain a ViewPager for a photo slider.
From everything I've seen, the ViewPager has to bind to an adapter that uses fragments. This is problematic because this means I have a fragment within a fragment, which has to use the ChildFragmentManager, which isn't supported in versions prior to Android 4.2. My app needs to work on Android 4.0+, so I need guidance on how to work around this issue.
Some lines of thought I had...
Is there a way to bind some other kind of adapter to a ViewPager that doesn't use fragments? In my case all I need is to show an ImageView as each item, so a fragment seems overkill and obviously has the 4.2+ issue.
Is there some other control that I could use besides ViewPager that would give me the desired UX (list of photos sliding left/right) without relying on child fragments?
Is there a way to make ChildFragmentManager work with Android 4.0 and 4.1?
Current code to setup the ViewPager with only Android 4.2+ support:
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="200dip"
android:id="#+id/property_details_image_gallery" />
private ViewPager gallery;
gallery = view.FindViewById<ViewPager> (Resource.Id.property_details_image_gallery);
gallery.Adapter = new ImagePagerAdapter (ChildFragmentManager, 0, facts.PhotoUris);
gallery.SetOnPageChangeListener (this);
public class ImagePagerAdapter : FragmentPagerAdapter
{
private int resourceId;
private List<string> photos;
public ImagePagerAdapter (FragmentManager fragmentManager, int resourceId, List<string> photos) : base(fragmentManager)
{
this.resourceId = resourceId;
this.photos = photos;
if (photos.Count == 0)
photos.Add (string.Empty); // If there are no photos, we add a single photo that will result in a 'noimage' photo
}
public override Fragment GetItem (int position)
{
// Instantiate a new fragment
ImageGalleryFragment fragment = new ImageGalleryFragment (photos[position], position);
return fragment;
}
public override long GetItemId (int position)
{
return position;
}
public override int Count {
get {
return photos.Count;
}
}
}
I just started using Xamarin 3 days ago, so please be gentle. :)
I was able to get this working by having ImagePagerAdapter inherit directly from PagerAdapter.
public class ImagePagerAdapter : PagerAdapter {
private List<string> data;
private Context context;
public ImagePagerAdapter(Context context, List<string> data) {
this.context = context;
this.data = data;
}
public override Java.Lang.Object InstantiateItem(View collection, int position) {
// Create new image view
var uri = data [position];
var imageView = new ImageView(context);
var defaultImage = context.Resources.GetDrawable (Resource.Drawable.noimage);
imageView.SetUrlDrawable (uri, defaultImage);
// Add image view to pager
var viewPager = collection as ViewPager;
viewPager.AddView(imageView);
return imageView;
}
public override void DestroyItem(View collection, int position, Java.Lang.Object view) {
var viewPager = collection as ViewPager;
viewPager.RemoveView(view as View);
}
public override bool IsViewFromObject(View view, Java.Lang.Object obj) {
return view == obj;
}
public override IParcelable SaveState() {
return null;
}
public override void StartUpdate(View arg0) {
}
public override void FinishUpdate(View arg0) {
}
public override int Count {
get {
return data != null ? data.Count : 0;
}
}
}