📜 ⬆️ ⬇️

Xamarin and Xamarin.Forms - cactus in chocolate. Part 2

Most recently, we published an article about the features and problems of the popular Xamarin mobile framework. Today we will continue the story and focus on the nuances of the Xamarin.Forms library. Under the cut, you will find a story about what rake awaits the person who decided to make a cross-platform UI.

Basic problems


For a start, the layout can be prepared both in code and in XAML format. Unfortunately, you can’t see the real-time preview of the interface, although this feature is available for native development tools. Therefore, we have chosen to develop an interface from code. It will look a bit cumbersome, but in general it is convenient:

public class LoginViewController: ContentPage { public LoginViewController() { Content = new StackLayout { Orientation = StackOrientation.Vertical, Children = { new Entry { Placeholder = ". ", Keyboard = Keyboard.Email, }, new Entry { Placeholder = "", IsPassword = true, }, new Button { Text = "" }, new ActivityIndicator { IsRunning = true, IsVisible = false, } } }; } } 

Further, the set of components in Xamarin.Forms is not very large. There are not enough such seemingly banal things, such as “carousels” for custom content. There is a full-screen carousel controller, but we needed a similar component that occupies only part of the screen. I had to bend a bit of a third-party bike .

The components that are available often lack the properties or events available on iOS or Android. It may not be possible to change the placeholder font or the color of the cursor, set the maximum length for the text field, and so on, such things have to be added independently. In the Xamarin.Forms 2.0 version released in mid-November 2015, some of these properties have been added, but up to 100% coverage of all the capabilities of native platforms is still far away.
')
Not happy with the inability to set all components indentation (padding and margin) - they are only in containers. Want a button or input field to indent? Wrap it in a container:

 new ContentView { Padding = new Thickness { Top = Sizes.StandartTopPadding Left = Sizes.StandartLeftPadding }, Content = new Label { Text ="  " } } 

But too deep a hierarchy slows down the rendering process, which Forms is in principle somewhat slower than the native components. Especially slowdown is noticeable on the lists, but simply quite complex forms for some applications can become a serious problem .

Not less happy that some of the features are implemented incorrectly and this is a feature. For example, when using standard navigation in Android, controllers will not be called up part of the life cycle events when switching to a new screen, since Navigation does not occur across real screens or fragments, but as a banal change of view within a single physical screen.

Bugs


Components often have bugs. For example, ScrollView had a problem - when the keyboard appeared, it was possible to scroll further than necessary to the area without content.



The source of the problem is the contents of the ScrollView are smaller in height than the container. The size of the area for scrolling content is determined by the following code:

 protected override void LayoutChildren(double x, double y, double width, double height) { //[...] ContentSize=new Size(width, Math.Max(height, Content.Bounds.Bottom + Padding.Bottom)); } 

As a result, an idea emerged of how quickly (and dirty) you can solve the problem - create a ScrollView descendant with overlapping the desired method:

 protected override void LayoutChildren(double x, double y, double width, double height) { //[...] // Max,       ContentSize = new Size(width, Content.Bounds.Bottom + Padding.Bottom); } 

Simply? In any case, the ContentSize property has a private setter and in its successor it doesn’t change its value. But since we went along the curved path - you can always call for help reflection and change the value of the property.

 public class ScrollViewCopycat : ScrollView { private readonly Action<Size> setContentSize; public ScrollViewCopycat() { var methodInfo = typeof(ScrollViewCopycat) .GetProperty("ContentSize", BindingFlags.Instance | BindingFlags.Public) .GetSetMethod(true); setContentSize = value => methodInfo.Invoke(this, new object[] { value }); } protected override void LayoutChildren(double x, double y, double width, double height) { //[...] setContentSize(new Size(width, Content.Bounds.Bottom + Padding.Bottom)); } } 

At some point, we were finally finished off by the following bug: if the visibility property value changes for a bundle of controls (the IsVisible property was set for several fields on the screen, one in False and the other in True ), the element could not appear on the screen! At the same time, he took his place in the hierarchy (a hole appeared on the screen), but in reality he was hidden. The problem arose not only with us, you can find several discussions on the Xamarin forum - here are examples once or twice .

The bug turned out to be floating, and it appeared in Xamarin.Forms 1.3.3.6323 and later, the problem arose because of the race condition inside the Forms themselves. Therefore, we for some time remained at an older, but not available version of this bug - 1.3.1.6296 . Unfortunately, this version also had its own bugs fixed in later ones.

So in the end we came to this decision:


Detailed code
 public class Batch { private readonly ILayoutController visualElement; public Batch(ILayoutController visualElement) { this.visualElement = visualElement; } public IDisposable Begin() { var animatables = GatherAnimatables(visualElement).ToArray(); foreach (var animatable in animatables) animatable.BatchBegin(); return new ActionDisposable(() => { foreach (var animatable in animatables) animatable.BatchCommit(); }); } private static IEnumerable<IAnimatable> GatherAnimatables(ILayoutController root) { return root.Children.OfType<IAnimatable>() .Concat(root.Children.OfType<ILayoutController>().SelectMany(GatherAnimatables)); } } 

This code not only solves the mentioned problem, but is recommended when changing several component properties at once. Let's say if the code is written like this:

 if (alert) { errorlabel.IsVibislbe = true; errorlabel.TextColor = Colors.Red; errorlabel.Text = AlertText; } 

That component will be redrawn three times, after each property change. But if you wrap it in BatchBegin / BatchCommit, the redrawing (and recalculation of the size) will occur only once, which will have a positive effect on speed.

There are other bugs, for example, TextView can affect the size of its container, although in addition the option “stretch to full width” is set:



This occurs if the vertical container lies in another container with a horizontal orientation.

The code leading to the problem.
 Content=new StackLayout { Orientation = Orientation.Horizontal, BackgroundColor = Color.Green, Children = { new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.FillAndExpand, HorizontalOptions = LayoutOptions.FillAndExpand, Children = { new Label { BackgroundColor = Color.Red, HorizontalOptions = LayoutOptions.FillAndExpand, } } } } } 


Communication models and UI-components (binding)


The built-in support for two-way binding between the model and the view also did not please us. Here is the first way to specify the connection:

 public class Model1 { public string Text { get; private set; } public Model1 (string text) { Text = text; } } var label1 = new Label { BindingContext = new Model1("Hello, problems!") } label1.SetBinding(Label.TextProperty, "Text"); 

If you make a mistake, and instead of “Text” you write another name - then nothing will explode either at the compilation stage or in runtime. Just Label appears without text.

Of course there is a little better way to establish a connection:

 label1.SetBinding<Model1>(Label.TextProperty, source => source.Text); 

But he does not save us from the situation when another object is placed in the Label:

 var label1 = new Label { BindingContext = new Model2(), }; 

In this case, again, nothing in the performance will not fall.

But that's not all. If you need interrelated fields in the model (when another changes and the other changes) - for the UI to work you will have to add some rather boring code - to implement the INotifyPropertyChanged interface and independently report the list of changed fields:

 public class Model : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName]string propertyName = null) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } private int value1; public int Value1 { get { return value1; } set { value1 = value; OnPropertyChanged(); OnPropertyChanged("Value2"); } } public int Value2 { get { return Value1*2; } } } 

For these reasons, the binding between the model and the controllers we wrote our own - checking the compliance of field types, automatically updating the associated fields, etc.

Lists


Well, a separate headache - lists. Let's start with the little things: the list has a header and a footer ( footer and header ), sort of unique cells that scroll along with the usual lines. It's good. But when replacing the content of the title that does not recalculate its height, if the new title is more or less than its predecessor, and the height of the rows of the table is fixed. You have to do it manually.

 public interface IHeader { Layout GetView(); double GetHeight(); } public void SetHeaderForm(IHeader value) { value.GetView().Layout(new Rectangle(value.GetView().X, value.GetView().Y, Width, value.GetHeight())); list.Header = value; } 

If you write on native iOS components - this problem does not arise, the size is recalculated by itself.

Another unpleasant moment is “ contextual actions ”. This menu is usually called on Android with a long tap, and on iOS, with a cell swipe. The trouble is that for these contextual actions in Xamarin.Forms, a MenuItem object is used, which has, among other things, an Icon property. But in these menus, no icons are displayed. And this is a feature .

So to display the icons, we used the Object-C library MGSwipeTableCell , around which we wrote our own wrapper. However, as a result, we lost the ability to automatically resize the cells in the list - they all now have to be strictly the same height, because writing the correct complex custom cell render is not as easy as it seems.

And finally, although the list as a data source is IEnumerable , there is no “load as it scrolls” by default - at the moment of determining the source, the component reads the data to the end. Not that we strongly expected such a behavior, because “out of the box” of infinite lists is neither in iOS nor in Android, but there was a light hope. Alas, the Xamarin.Forms components implement only the subsistence minimum of possibilities - everything else will have to be added by ourselves.

findings


Whether or not to use Xamarin.Forms - the next stage will show us, the transfer of the Java project already written for Android to Forms. But now we can say that Xamarin.Forms should be used only for the most simple UI. If there are plans to use every single chip of a specific platform or tricky design solutions, Xamarin.Forms will be more of a hindrance than help. In this option, it is better to use Xamarin exclusively for business logic, and make layout for each of the platforms native.

If you have any questions or comments, we will be happy to answer them in the comments.

Source: https://habr.com/ru/post/273161/


All Articles