I have a an Android fragment which essentially displays a custom ListView. My Fragment looks like this:
public class ResultSummaryFragment : Android.Support.V4.App.Fragment
{
private List<ResultSummary> data;
public override async void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
ISharedPreferences preferences = Application.Context.GetSharedPreferences("UserInfo", FileCreationMode.Private);
string id = preferences.GetString("ID", string.Empty);
if (id != null)
{
RunnerData RunnerData = new RunnerData(id);
// This is not happening before OnCreateView?
data = await RunnerData.GetAllResults();
}
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
Context context = Application.Context;
View view = inflater.Inflate(Resource.Layout.results_summary_view, container, false);
ListView listview = view.FindViewById<ListView>(Resource.Id.AllResultsListView);
//data is being passed in as null...
ResultsSummaryListAdapter adapter = new ResultsSummaryListAdapter(context, data);
listview.Adapter = adapter;
return view;
}
}
The problem is that data being passed into ResultsSummaryListAdapter is null because the OnCreateView() code seems to get executed before data = await RunnerData.GetAllResults(); returns. I'm absolutely sure that this does return data (it can take time due to download time).
What am I doing wrong here?
I feel like a better way to handle this would be to instantiate the fragment, start getting the data and then trigger the below code:
ResultsSummaryListAdapter adapter = new ResultsSummaryListAdapter(context, data);
listview.Adapter = adapter;
I'm not sure how to go about that in Android.
Please refer to the below links on how to use Fragments to achieve your requirements.
https://learn.microsoft.com/en-us/xamarin/android/platform/fragments/implementing-with-fragments/
The below link gives you an overview of fragments and is well explained.
https://learn.microsoft.com/en-us/xamarin/android/platform/fragments/
Also, the attached image will explain to you the life cycle of fragments.
Related
I have inherited some code that requires a change in how it works. The original way didn't have the flexibility now required.
The application is a form generator, and hence has to create the UI on demand. This is Xamarin native, not Xamarin forms.
A FrameLayout for each form question is being created programmatically, added to the view, then a fragment is being added to this FrameLayout. All this is happening AFTER OnCreateView once the UI has been loaded to show a progress circle.
After working through a bunch of exceptions, I have become stuck with the exception
Java.Lang.IllegalArgumentException: No view found for id 0x50 (unknown) for fragment UploadFragment{a31e878 #7 id=0x50 upload_80}
My guess is that the FrameLayout doesn't exist when the fragment is trying to be displayed.
The exception occurs after the OnCreate() method runs after OnCreateView() completes.
I have not been able to find any code precedent for adding FrameLayouts programmatically with Fragments.
CODE Snippet
frame = new FrameLayout(this.Context);
frame.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
upload = new Widgets.UploadFragment(control, binding, Inflater, a, xFormInstance);
MainFormLayout.AddView(frame);
frame.Id = control.id;
fragmentTx.Add(frame.Id, upload, $"upload_{control.id}");
fragmentTx.Commit();
Any advice would be greatly appreciated. Thanks.
Extended Explanation
It may be a bit much to put in everything it does, but will try and put in as much as I can.
The Hierarchy of the page is
Activity -> FormFragment -> UploadFragment
So the parent of the UploadFragment is also a fragment, not the Activity.
Upload Fragment
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout>
<LinearLayout>
<TextView/>
<ImageButton/>
</LinearLayout>
<ImageView/>
</RelativeLayout>
</LinearLayout>
CODE
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
_inflater = inflater;
v = _inflater.Inflate(Resource.Layout.BindImageInput, container, false);
SetUpload();
return v;
//return base.OnCreateView(inflater, container, savedInstanceState);
}
SetUpload() Sets the values of the label, the events for the buttons, and the image (if exists) to the imageview. It also deals with a few extra events to do with form event handling. Stopping SetUpload() from running still has the exception occur.
FormFragment
<RelativeLayout>
<TextView />
<View />
<ScrollView>
<LinearLayout />
</ScrollView>
</RelativeLayout>
CODE
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
ShowLoading();
View v = inflater.Inflate(Resource.Layout.Form2, container, false);
MainFormLayout = v.FindViewById<LinearLayout>(Resource.Id.mainFormView);
MainScrollView = v.FindViewById<ScrollView>(Resource.Id.mainScrollView);
formBuilderWorker = new BackgroundWorker();
return v;
}
OnResume() Calls the method where formBuilderWorker.DoWork() exists
formBuilderWorker.DoWork += delegate
{
Form.LoadForm(null, this, FormInstance);
}
LoadForm() uses a Interface to tell the FormFragment to display a control. One of which is the UploadFragment.
public void AddControl(Controls control, int? sectionID)
{
///CODE REMOVED FOR OTHER CONTROL TYPES (they still use old codebase)
Bindings binding = XForm.GetBindingForControl(control, FormInstance);
try
{
// Create a new fragment and a transaction.
FragmentTransaction fragmentTx = this.FragmentManager.BeginTransaction();
FrameLayout frame = null;
Widgets.UploadFragment upload = null;
frame = new FrameLayout(this.Context);
frame.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
frame.Id = control.id;
upload = new Widgets.UploadFragment(control, binding, Inflater, a, xFormInstance);
MainFormLayout.AddView(frame);
ControlViews.Add(frame);
fragmentTx.Replace(frame.Id, upload, $"upload_{control.id}");
//fragmentTx.Show(upload);
fragmentTx.Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
This is cleaned code to remove as much irrelevant code as possible. The code shown is the path the code in question moves through.
I found the issue. Part of what I took out of the code above, was the Activity.RunOnUiThread() calls that add the frame to the main view. The issue was caused by Thread Timing. The UI thread was taking so long to add the frame to the view, that when the FragmentTransaction was trying to commit the changes, the frame still did not exist.
In Android, when should (and shouldn't) a view (activity/fragment) be updated with content?
TextView's shouldn't be set with text from the same thread OnCreate() is running on, correct, but instead set on the UI thread (which is done by posting statements to the UI threads MessageQueue)?
95% of my view's set data (in TextViews, TabHosts, Checkboxes, etc... ) directly in OnCreate() (or a function called from it) and works fine, but I realize now that just b/c I've been getting away with it doesn't mean it's right (yes i'm getting bit now).
However, running on the UI thread isn't enough, I'm finding I need to post from OnResume() for 100% guarantee that the RecordView will updated as expected.
The code below show's the two scenarios for calling UpdateView(). Is it enough to call UpdateView() in OnResume() to ensure the RecordView will display with populated data as expected, or is there a better, more correct and/or preferred way of doing this?
Also, is _thisView redundant to _container?
Is the RecordView that is having it's OnCreate() being called the same RecordView that is displayed? Why do I need to inflate the RecordView in OnCreate(), and then return it out? Shouldn't this happen automatically by the runtime, before OnCreate() is called (it's almost like a Factory pattern or something)?
Example Code:
public class RecordView : Fragment
{
private Bundle _bundle;
private ViewGroup _container;
private LayoutInflater _inflater;
ViewGroup _thisView;
TextView _tvTitle, _tvField1, _tvField2;
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle)
{
base.OnCreateView(inflater, container, bundle);
_inflater = inflater;
_container = container;
_bundle = bundle;
Render();
return _thisView;
}
public override void Render()
{
_thisView = (ViewGroup) _inflater.Inflate(Resource.Layout.Record, _container, false);
_tvTitle = _thisView.FindViewById<TextView>(Resource.Id.tv_title);
_tvField1 = _thisView.FindViewById<TextView>(Resource.Id.tv_field_1);
_tvField2 = _thisView.FindViewById<TextView>(Resource.Id.tv_field_2);
// *A* SOMETIMES WORKS - Title & fields sometimes blank
UpdateView();
// *B* SOMETIMES WORKS - Title & fields sometimes blank
_thisView.Post(() => { UpdateView(); });
}
public override void OnResume()
{
base.OnResume();
// *C* SOMETIMES WORKS - Title & fields sometimes blank
UpdateView();
// *D* ALWAYS WORKS - Title & fields always display data as expected
_thisView.Post(() => { UpdateView(); });
}
private void UpdateView()
{
_tvTitle.Text = "Todo";
_tvField1.Text = "RTFM";
_tvField2.Text = "ASAP";
}
}
You should show initial data or some loading animation in OnCreateView(..), load data in background and then post actual content on the main thread once it is available, i.E.:
new Handler(Looper.GetMainLooper()).post(() => {
//update views here
});
Also, setting a View's content in OnCreateView(..) ALWAYS works if you use one of the View.set(..) methods, in your case TextView.setText(..).
That is because setters call invalidate() on the view which in turn redraws the view.
It is by design not possible that setting a TextView's text in OnCreateView(..) is not updated on screen.
I FORGOT TO MENTION
No, _thisView is not the same as _container.
_thisView is a direct child of _container.
Consider this:
Your main layout.xml is a FrameLayout.
you add a Fragment to it that has a layout called frag.xml, beeing a TextView.
now the _container is the FrameLayout, but _thisView is the TextView.
first I have to mention that I'm new to Android app development. I'm using Xamarin (C#) for developing apps. Here's the problem. I want to write simple ToDoList application to learn how to store/share data between activities.
Button "Add" is placed in Main layout. When user clicks it, another screen shows up where user can name(EditText) task and add its description(EditText). When user clicks button "Complete" in that screen, app redirects user to Main screen and add user's task to a listview.
I did some research and found out that I could do that with SharedPreferences, saving data to file on device, or sql.
I decided to go with SharedPreferences. Problem is, everything I found about SharedPrefences (and I searched a lot, believe me) is written in Java which wouldn't be such a big problem, but nobody ever mentioned sharing data between different activites using shared preferences. My code is below, I'm really struggling with this over a day, so please help me.
My MainActivity (where button "Add" is):
public class MainActivity : Activity
{
Button add;
ListView list;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
///set layout
SetContentView(Resource.Layout.Main);
///define variables, buttons, lists
add = FindViewById<Button>(Resource.Id.add);
list = FindViewById<ListView>(Resource.Id.list);
///get string from secondActivity
var prefs = Application.Context.GetSharedPreferences("todolist", FileCreationMode.Private);
var preference = prefs.GetString("task", null);
///add string (preference) to list
List<string> lista = new List<string>();
lista.Add(preference);
///"convert" list to listview
ArrayAdapter<string> adapter = new ArrayAdapter<string>(this, Resource.Layout.Main, lista);
list.Adapter = adapter;
add.Click += delegate
{
StartActivity(new Intent(this, typeof(addActivity)));
};
}
}
My second screen (second activity):
public class addActivity : Activity
{
/// define variables and create list
Button complete;
EditText name, description;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.add);
///variables
complete = FindViewById<Button>(Resource.Id.complete);
name = FindViewById<EditText>(Resource.Id.name);
description = FindViewById<EditText>(Resource.Id.description);
///converting to string
string title = name.Text.ToString();
string content = description.Text.ToString();
///pass string to MainActivity
complete.Click += delegate
{
if (String.IsNullOrEmpty(title))
{
Toast.MakeText(this, "Please enter task name", ToastLength.Short).Show();
}
else
{
var prefs = Application.Context.GetSharedPreferences("todolist", FileCreationMode.Private);
var prefEditor = prefs.Edit();
prefEditor.PutString("task", title);
prefEditor.Commit();
Toast.MakeText(this, "Task added", ToastLength.Short).Show();
StartActivity(new Intent(this, typeof(MainActivity)));
}
};
}
}
You could use an SQLite database here. I would recommend doing so if you want to build a ToDo list. It will make it easier for you as well.
Xamarin has plenty of documentation on how to set this up here:
https://developer.xamarin.com/guides/cross-platform/application_fundamentals/data/
They also have an example app that does pretty much what you want to do here:
https://github.com/xamarin/mobile-samples/tree/master/Tasky
You can pass the string to the next activity through an Intent
Instead of defining that intent in the arg of your Start activivty you should define it before that and then add the string using the putExtra method
examples: How to use putExtra() and getExtra() for string data
I'm new to Xamarin and new to Android development (this is my first week coming from a windows background). I have an adapter that I'm attempting to add to a listview but I'm not sure where to place my code. I have multiple fragments being inflated from a flyout menu. I attempted placing the code in the fragment itself but this caused compilation errors, I also attempted placing the code in the main activity. Neither seemed to work correctly.
This is the code segment
var items = new String[] { "test", "test2" };
ArrayAdapter<string> adapter = new ArrayAdapter<string> (this, Android.Resource.Layout.SimpleListItem1, items);
var listViewMeds = FindViewById<ListView> (Resource.Id.medicationListView);
listViewMeds.Adapter = adapter;
You will do this in the OnCreateView inside the fragment.
eg
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = inflater.Inflate(Resource.Layout.MyLayoutId, container, false);
ListAdapter = new MyAdapter(Activity, Items);
return view;
}
Note that your fragment will need to be a ListFragment to be able to access the ListAdapter property.
Alternatively, if you don't have a ListFragment you can find your list view in the OnCreateView, eg.
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = inflater.Inflate(Resource.Layout.MyLayoutId, container, false);
var myListView = view.FindViewById<ListView>(Resource.Id.MyListViewId);
myListView.ListAdapter = new MyAdapter(Activity, Items);
}
Just started with Monodroid and I'm currently working on a ListView.
I got a List added to the ArrayAdapter and i can see my first two items correctly. However when i add a third element to the list, the listview doesnt update. Even though i call notifyDataSetChanged().
private ArrayAdapter<string> la;
private ListView list;
private List<String> dayData = new List<String>();
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.TestLayout);
dayData.Add(" Test");
dayData.Add(" Test2"); // Theese two elements shows up fine
list = this.FindViewById<ListView>(Resource.Id.menuList);
la = new ArrayAdapter<string>(this, Resource.Layout.list_item, dayData);
list.Adapter = la;
list.TextFilterEnabled = true;
dayData.Add(" Test3"); // This one is not shown
la.NotifyDataSetChanged();
} // OnCreate
Any clues on what i missed?
Found a solution myself at another forum. Somehow the ArrayAdapter does'nt take notice of list changes when using a List. Instead use a Android.Runtime.JavaList.
You can find the discussion here: http://mono-for-android.1047100.n5.nabble.com/Update-ListView-when-dataset-changes-td4757874.html
Works like a charm! :)
I am not entirely sure, but I think that the items are copied into the ArrayAdapter, thus what you need to do is:
la.Add(" Test3");
if you want to keep the list the same you will have to add it to list as well.
try this Add la.notifyDataSetInvalidated(); after la.NotifyDataSetChanged();
I'm not sure how much this will help anyone, but it seems to work fine in my instance.
I have a ViewModel class that keeps all the data I update within the App and trigger a "Collection Updated" action when a collection changes.
// All within ViewModel.cs
private Action SearchResultsUpdated;
private List<SearchResult> m_oSearchResults;
Public List<SearchResult> SearchResults
{
get
{
if (m_oSearchResults == null)
m_oSearchResults = new List<SearchResult> ();
return m_oSearchResults;
}
set
{
if (value != m_oSearchResults)
{
m_oSearchResults = value;
//
// Fire update event
if (SearchResultsUpdated != null)
SearchResultsUpdated ();
}
}
}
I then add a handler for this event within the adapter class.
// All within SearchResultsAdapter.cs
public class SearchResultsAdapter : BaseAdapter<SearchResult>
{
.
.
// Constructor
public SearchResultsAdapter (Activity oContext)
: base ()
{
// Add handler for list refresh
ViewModel.SearchResultsUpdated += NotifyDataSetChanged;
//
m_oContext = oContext;
}
}
Within the adapter I use the collection ViewModel.SearchResults as the data context for the list view. Hope that helps and is thorough enough for everyone to understand.
To update the ListView
private ListView lvAnuncios = null;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.lvAnuncios = this.FindViewById<ListView>(Resource.Id.MisAnuncios_lvAnuncios);
}
private void ReloadListView()
{
if (this.lvAnuncios.Adapter != null)
{
this.lvAnuncios.Adapter.Dispose();
this.lvAnuncios.Adapter = null;
}
//Class that inherits de ArrayAdapter
this.lvAnuncios.Adapter = new adAnuncio(this, Resource.Layout.FilaListViewAnuncio, csVariable.objUsr.lstAnuncios);
}