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.
Related
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.
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.
I am creating an application for Xamarin.android in c# and ran into an odd problem.
I use a wrapper class which inflates a view. After that I add the view to a linearlayout with AddView. Sometimes the gui doesn’t update for no apparent reason. The Elements aren’t added. When I call the task button (I don’t know if that’s the right name – the button left of the home button) and then navigate back to my app the elements will appear. I read somewhere that you could use Invalidate() to update the gui but that does not seem to work.
Any suggestions are highly appreciated.
This is my constructor of the wrapper class:
public BoxDisplay(MainActivity activity, Product product, ViewGroup root)
{
View = activity.LayoutInflater.Inflate(Resource.Layout.BoxItem, root, false);
shelf = View.FindViewById<TextView>(Resource.Id.Shelf);
name = View.FindViewById<TextView>(Resource.Id.Name);
barcode = View.FindViewById<TextView>(Resource.Id.Barcode);
Stock = View.FindViewById<EditText>(Resource.Id.Stock);
typ = View.FindViewById<TextView>(Resource.Id.type);
this.activity = activity;
SetProduct(product);
Stock.ClearFocus();
Stock.Click += (o, arg) =>
{
Stock.SelectAll();
};
Stock.EditorAction += EditorAction;
}
and here I add the view long after the activity was created:
private void GenerateView(List<Product> products)
{
var boxlist = FindViewById<LinearLayout>(Resource.Id.BoxList);
foreach (var product in products)
{
var box = new BoxDisplay(this, product, boxlist);
boxDisplays.Add(box);
box.EnableEditText(false);
boxlist.AddView(box.View);
box.View.Click += (sender, e) => { box.UpdateBox(); };
}
}
A couple of ideas more than an answer, and maybe could be bad practice, but I do not run into your issue :
I usually add programmatically created Views in OnViewCreated (Fragment) / OnCreate(Activity)
Make sure your LinearLayout has a parent. It must be added to a parent view (root Layout, another Layout, a scrollview, ...).
Make sure you do not have a blocking, manually launched thread involving UI update (even through RunOnUiThread), which could be unlocked by activity recreation.
Make sure your view isn't "flattened" by another existing one in your layout (for example having a view matching parent size in both dimensions)
Beside the fact that it won't display your layout, is your application running properly (no permanent freeze) ?
Ok... So my problem is to prevent from activity to reload after orientation is changed.
Basically, what I did is this:
[Activity(Label = "migs", ConfigurationChanges = Android.Content.PM.ConfigChanges.Orientation)]
This is worked fine, until I changed "Target API" to 14. If I'm changing it back to 12, then everything is working, but on 14, activity is being restarted (OnCreate method is fires after rotation).
So... You'll ask why do I need "Target API" 14? - Easy! Because in my app, I'm playing video, and for that I need "true full screen". All API's below 14 adding "Settings" (three dots) button. In case of HTC, it's big and ugly button, that I was unable to get rid of.
If you know how to do one of the two (Get rid of the "settings" button in API 12, or prevent activity from reload after orientation changed in API 14), I'll be very thank full for your help.
Ok... At last I solved it! :)
Saving activity state instead of preventing activity from reload, from first sight can seem to be a little tricky, but in fact is really easy and it's the best solution for situations like this.
In my case, I had a ListView, that populates from the internet with items, that stored in custom list adapter. If device orientation was changed, the activity was reloaded, so does the ListView, and I was loosing all the data.
All I needed to do is to override the OnRetainNonConfigurationInstance method.
Here's a quick sample of how to do it.
First of all, we need a class, that can handle all of our stuff.
Here is a wrapper for all the things we need to save:
public class MainListAdapterWrapper : Java.Lang.Object
{
public Android.Widget.IListAdapter Adapter { get; set; }
public int Position { get; set; }
public List<YourObject> Items { get; set; }
}
In our activity, we need to hold variables, to store all the data:
ListView _listView; //Our ListView
List<YourObject> _yourObjectList; //Our items collection
MainListAdapterWrapper _listBackup; //The instance of the saving state wrapper
MainListAdapter _mListAdapter; //Adapter itself
Then, we overriding the OnRetainNonConfigurationInstance method in the activity:
public override Java.Lang.Object OnRetainNonConfigurationInstance()
{
base.OnRetainNonConfigurationInstance();
var adapterWrapper = new MainListAdapterWrapper();
adapterWrapper.Position = this._mListAdapter.CurrentPosition; //I'll explain later from where this came from
adapterWrapper.Adapter = this._listView.Adapter;
adapterWrapper.Items = this._yourObjectList;
return adapterWrapper;
}
And the final stage is to load saved state in OnCreate method:
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.list);
this._listView = FindViewById<ListView>(Resource.Id.listView);
if (LastNonConfigurationInstance != null)
{
this._listBackup = LastNonConfigurationInstance as MainListAdapterWrapper;
this._yourObjectList = this._listBackup.Items;
this._mListAdapter = this._listBackup.Adapter as MainListAdapter;
this._listView.Adapter = this._mListAdapter;
//Scrolling to the last position
if(this._listBackup.Position > 0)
this._listView.SetSelection(this._listBackup.Position);
}
else
{
this._listBackup = new MainListAdapterWrapper();
//Here is the regular loading routine
}
}
And about the this._mListAdapter.CurrentPosition... In my MainListAdapter, I added this property:
public int CurrentPosition { get; set; }
And the, in the `GetView' method, I did that:
this.CurrentPosition = position - 2;
P.S.
You don't have to implement exactly as I showed here. In this code, I'm holding a lot of variables, and making all the routine inside the OnCreate method - that is wrong. I did that, just to show how it can be implemented.
what happen when orientation change (consider you enable rotation in your phone) ?
Android restart activity onDestroy() is called, followed by onCreate() , you can distinguish between onDestroy() call to kill activity or restart app throw old answer.
Prevent Activity restart
just set ConfigurationChanges to both Orientation , ScreenSize
[Activity (Label = "CodeLayoutActivity", ConfigurationChanges=Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize)]
why this may be not working ?
I dont think this will not working but set RetaintInstance to true read more about RetainInstance
class myFragment: Fragment
{
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.RetainInstance = true;
// this to change screen orientation
Activity.RequestedOrientation = ScreenOrientation.Landscape;
}
.....
}
hope this help
Above API 13, you need to include screensize in your ConfigChanges.
As denoted here.
Maybe adding that tag to your activity for API13+ will help?
I've figured out how to set the tint for the title bar and have set a background image for all my views, but for the life of me I cannot figure out how to set the default UILabel color for section headers and such. I don't want to riddle my code setting all my colors to UIColor.Black by hand. Is there any way to get a list of different UIElements and the way to set defaults (colors, fonts, sizes) for each? I'm specifically interested in the color of labels, but any other information would be extremely helpful for the future.
Starting with iOS 5 there's an UIAppearance class to handle global appearance of many UI elements.
The way it works with MonoTouch is that an inner type, called Appearance, exists on those types. You can directly set the properties from it (easy with MonoDevelop's code completion). E.g.
UILabel.Appearance.BackgroundColor = UIColor.Blue;
Sadly it does not cover everything you might want for every control (nor will it work before iOS 5).
This is kind of hacky, but this does the trick, and it works with all kinds of other UI controls that subclass UIView. This assumes you have your own subclassed DialogViewController (mine has lots of handy time-saving things in it, and all my views subclass my DVC to do work).
protected void ForAllSubviewUIControlsOfType<TUIType>(Action<TUIType, int> actionToPerform) where TUIType : UIView
{
processSubviewUIControlsOfType<TUIType>(this.View.Subviews, actionToPerform, 0);
}
private void processSubviewUIControlsOfType<TUIType>(UIView[] views, Action<TUIType, int> actionToPerform, int depth) where TUIType : UIView
{
if (views == null)
return;
if (actionToPerform == null)
return;
foreach (UIView view in views)
{
if (view.GetType () == typeof(TUIType))
{
actionToPerform((TUIType)view, depth);
}
if (view.Subviews != null)
{
foreach (UIView subview in view.Subviews)
{
processSubviewUIControlsOfType<TUIType>(view.Subviews, actionToPerform, depth + 1);
}
}
}
}
To solve the original problem and change the text colors of all labels that are in sections, this is what you need to do:
public override void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
ForAllSubviewUIControlsOfType<UILabel>((label, depth) => {
if ( /*label.Superview is UIView && label.Superview.Superview is UITableView*/ depth == 1 )
{
label.TextColor = UIColor.Black;
}
});
}
The commented out part is what was determining if the label existed inside a section, before I decided to return an int depth count to see if it was on the view.
EDIT: Seems as if this isn't the perfect solution... the depth count varies for these elements. I'll have to figure out something better in the next day or two.