Sunday, January 8, 2017

Introducing EasyLayout.Droid For Simpler Xamarin Android Layouts

If you've done much Xamarin iOS work you're probably run into Frank Krueger's awesome framework, EasyLayout, that makes manually coded auto layout's considerably easier to read and maintain.

If you've ever wanted the same type of functionality for Xamarin Android either for consistency or ease of cross platform code sharing, now you can with EasyLayout.Droid.


What Is EasyLayout?


The original EasyLayout takes Xamarin iOS code like this:

_passwordField.AddConstraint(NSLayoutConstraint.Create(
    _passwordField, NSLayoutAttribute.Top, NSLayoutRelation.Equal,
    _usernameTextField, NSLayoutAttribute.Bottom, 1f, 20f));
_passwordField.AddConstraint(NSLayoutConstraint.Create(
    _passwordField, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal,
    View, NSLayoutAttribute.CenterX, 1f, 0f));

And turns it into this:

View.ConstrainLayout(() =>
    _passwordField.Frame.Top == _usernameTextField.Frame.Bottom + 20 &&
    _passwordField.Frame.GetCenterX() == View.Frame.GetCenterX()
    );

If you're on a team, or storyboards just aren't your thing it's a lifesaver!


What's Wrong with Android .axml?


Android's axml files are ok, but on large projects they take a long time to generate, and they make it hard to share layout information cross platform.  But if you try to code Android by hand, you quickly discover the same type of verbosity that Xamarin iOS had.  Enter EasyLayout.Droid.

Example 1 - Parent Align


If you want to align an image to the edges of the frame you used to do this:

var layoutParams = new RelativeLayout.LayoutParams(
    ViewGroup.LayoutParams.MatchParent,
    ViewGroup.LayoutParams.MatchParent);
layoutParams.AddRule(LayoutRules.AlignParentTop);
layoutParams.AddRule(LayoutRules.AlignParentBottom);
layoutParams.AddRule(LayoutRules.AlignParentRight);
layoutParams.AddRule(LayoutRules.AlignParentLeft);
_image.LayoutParams = layoutParams;

Now you can do this:

relativeLayout.ConstrainLayout(() =>
    _image.Top == relativeLayout.Top
    && _image.Right == relativeLayout.Right
    && _image.Left == relativeLayout.Left
    && _image.Bottom == relativeLayout.Bottom
    );

There's no need to set LayoutParams at all.  If they don't exist EasyLayout.Droid will add them, if they do EasyLayout.Droid will append to them.  And if you don't add them it will take care of choosing LayoutParams.MatchParent or WrapContent.

Example 2 - Relative Alignment and Constants


If you wanted to align an image 20 dp under another image and center align it to the parent you used to do this:

var layoutParams = new RelativeLayout.LayoutParams(
    ViewGroup.LayoutParams.WrapContent,
    ViewGroup.LayoutParams.WrapContent);
layoutParams.AddRule(LayoutRules.CenterHorizontal);
layoutParams.AddRule(LayoutRules.AlignBottom, image1.Id);
layoutParams.TopMargin = DpToPx(20);
_image2.LayoutParams = layoutParams;


There's a couple of gotchas.  

  1. If you set the TopMargin to 20, then Android assumes you mean pixels not device independent pixels.  To fix that you need to remember to call a function like DpToPx().  
  2. Your relative view (image1) needs to have an Id.  If you forget to set it there's no error, it just does strange layout things.

EasyLayout.Droid replaces the code above with:

relativeLayout.ConstrainLayout(() =>
    _image2.Top == _image1.Bottom + 20
    && _image2.GetCenterX() == relativeLayout.GetCenterX()
    );


That's less code, and it's easier to read, plus there's some other small benefits: 

  1. If you forget to add an Id to _image1, EasyLayout.Droid will throw a helpful runtime error.  
  2. EasyLayout.Droid always assumes every number is in Dp, so it automatically converts all literals for you.

Incidentally, GetCenterX() is one of a couple of new extension methods along with GetCenterY() and GetCenter().

Example 3 - Constants


Constants weren't difficult to work with previously, but for completeness they used to work like this:

var layoutParams = new RelativeLayout.LayoutParams(
    DpToPx(50),
    DpToPx(ViewModel.SomeHeight);
_image2.LayoutParams = layoutParams;


With EasyLayout.Droid you can do this:

relativeLayout.ConstrainLayout(() =>
    _image.Width == 50
    && _image.Height == ViewModel.SomeHeight.ToConst()
    );


As mentioned previously 50 will be assumed to be in dp and will be auto-converted to pixels.  Also arbitrary properties such as SomeHeight will need the .ToConst() extension method applied in order to tell EasyLayout.Droid that they should be treated as constants.

Limitations


Android relative layouts are far from a replacement for iOS's auto layout.  To that end you cannot do the following operations that EasyLayout could:

  • Constraining Heights or Widths to be equal to the Heights or Widths of other elements
  • Using >= or <= signs to indicate GreaterThanOrEqual or LessThanOrEqual type constraints
  • Multiplication of elements (e.g. _image2.Width == _image1.Width * .25f)

Installation


If you want to add this to your project you can either install via NuGet (safer):

Install-Package EasyLayout.Droid

or if you think it's perfect as-is (you don't want updates) you can copy EasyLayoutDroid.cs into your source.  Next using EasyLayout.Droid and you're good to go.

Conclusion


Hope this helps make someone's Xamarin Android day a little better.  The code is MIT licensed.  If you have any questions please contact me on twitter.


Benefits Of Git Rebase

In the first article in this series (Git: Rebase vs Merge) I covered the tactics of rebasing.  I discussed what merge commits are, and how to avoid them with rebasing.  In this post I'll cover the benefits of rebasing, including how its use speeds up finding hard to track down bugs via git blame and git bisect.

Is Rebase Really Worth It?


I worked on one large project that discouraged rebasing a while ago.  In short, this was the result:


The repository was insanely complicated to look at with gitk (or SourceTree in this case).  Trying to understand what was going on across the program was virtually impossible.
The recalcitrant developer (hey, someone forced me to learn these stupid SAT words, might as well inflict them on others), at this point, might simply respond: "So what?  As long as my stuff works."  And perhaps it's true, just like the old joke:
Patient: Doctor, it hurts whenever I do this.
Doctor: Don't do that.

Merge Commits: Ugly, Painful, or Both?


But even if someone never looked at the repo with gitk, or a fancy git GUI, in the project above roughly 50% of all commits were merge commits!
That meant 50% of the history of the project was useless noise distributed like a shotgun blast into the true history.  Furthermore, some of those merge commits hid sneaky little bugs introduced when merge conflicts were poorly resolved.  
Granted, 50 percent may be high for most projects.  The actual number depends on how many developers, how often they commit, and how often they merge.  For example if developers pull from develop daily, a given project will get one merge commit per developer per day.  If developers commit 3 times per day (seems kind of average from my observations), then 25% of commits will be merge commits.
The recalcitrant developer (stupid SAT) might again at this point respond: "25% of commits are merges, so what?  Just don't look at the commit history!  And if forced to, just ignore the merge commits!"
However, there are two specific cases where a messy history may yet affect a fast and loose, merge happy team.

Don't Blame Git!


Have you ever looked at a file and wondered who wrote a particular line of crap?  In my case 99% of the time that person is me.  But never mind that.  The tool for this job is clearly git blame.
The command git blame (or git annotate for the more more politically correct) will annotate every line of a particular file with its last commit.  Apart from finding who wrote something, it's also an essential tool for discovering why something was done the way it was when spelunking through larger, or especially older, codebases.
However, merge commits obfuscate git blame.  If an associated commit message is simply "merged develop into feature.awesome" and the developer is no longer around to ask, then we have to go through additional effort to track down history.

For instance, in the example above line 3 "c" was actually created by commit C (b49c7b1), but git blame incorrectly shows the merge commit (d9400d4) as the author.

Git Bisect For The Win!

The second scenario in which merge commits complicate history is in tracking a bug that someone introduced, typically, within the last few days.  One could manually checkout every commit between the good commit and the bad commit, or simply use git bisect.  
Git bisect is wonderful for automating the process of finding a commit.  It allows you to specify the last known good commit, and the last known bad commit, and then it performs a binary search of all the commits in between to discover the bad commit as quickly as possible.
Regardless of whether you search manually, or use git bisect, life gets hard as soon as you try to juggle many branches with lots of merge commits.  The automated approach makes navigating many merged branches easier, but either way if you have a fully merge-based project, you are now required to take an additional 25-50% steps.   If each step takes time to build or deploy, these extra steps can quickly add up (trust me, I've had to do this a lot).
For instance consider the following project with three feature branches, seven real commits, and five merge commits.
gitbisect2.png
Now pretend that you've come in on Monday morning to discover that after committing D on Friday, some developers over the weekend committed E and F, and suddenly there's a hard to track down bug.  Git bisect will solve it for you like this: 
Lee@lee-xps MINGW64 /c/Temp/deletemegit (feature.awesomeness)
$ git bisect start
Lee@lee-xps MINGW64 /c/Temp/deletemegit (feature.awesomeness|BISECTING)
$ git bisect good 880e84a
Lee@lee-xps MINGW64 /c/Temp/deletemegit (feature.awesomeness|BISECTING)
$ git bisect bad be49c0d
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[d9400d4c62807046f8ea235170e681b3e8952200] Merge branch 'develop' into feature.awesomeness
Lee@lee-xps MINGW64 /c/Temp/deletemegit ((d9400d4...)|BISECTING)
$ git bisect good
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[96cf2877d16183cccce1f822d72626d331b582ef] Merge branch 'develop' into feature.awesomeness
Lee@lee-xps MINGW64 /c/Temp/deletemegit ((96cf287...)|BISECTING)
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 1 step)
[190dadb1046822fc169193e86484a19e3543b783] Merge branch 'feature.2' into develop
Lee@lee-xps MINGW64 /c/Temp/deletemegit ((190dadb...)|BISECTING)
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[ef4a5532ace6075aead0850f088322f98e7afbf1] E
Lee@lee-xps MINGW64 /c/Temp/deletemegit ((ef4a553...)|BISECTING)
$ git bisect bad
ef4a5532ace6075aead0850f088322f98e7afbf1 is the first bad commit
commit ef4a5532ace6075aead0850f088322f98e7afbf1
Author: Lee Richardson
Date:   Mon Sep 5 21:56:07 2016 -0400
    E
:000000 100644 0000000000000000000000000000000000000000 d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 A      2.txt
Lee@lee-xps MINGW64 /c/Temp/deletemegit ((ef4a553...)|BISECTING)
$ git bisect reset
Previous HEAD position was ef4a553... E
Switched to branch 'feature.awesomeness'
Your branch is based on 'origin/feature.awesomeness', but the upstream is gone.
  (use "git branch --unset-upstream" to fixup)
Lee@lee-xps MINGW64 /c/Temp/deletemegit (feature.awesomeness)

Beautiful!  Commit E was the culprit.  The only problem: three of the four steps wouldn't have been needed if the team had been rebasing.

Summary


If your project is still small, and you haven't had to use git blame or git bisect yet, you may not find these arguments compelling.  However, if this is the case I suspect you may also  not find much value in unit testing either.  Unit testing and rebasing both require extra up-front work in order to build quality into your work and set your future self up for success.
More worried about immediate deadlines than your future self?  Consider the developer who will replace you when you leave.  Not worried about her?  Consider your customer at some future point in time when they're attempting to spelunk through your code.  Unless this is a throwaway project, the chances are good that a little extra effort learning a new technique today could save yourself, other developers, and possible a future employer considerable time and energy.  Not a bad investment.