One of the things I love about being an Android developer is the amount of resources available at my fingertips on the Internet. There’s a strong community of developers sharing open source libraries, promoting design patterns, and just helping out their peers. For the last several years I’ve primarily been a consumer of these resources, but it’s time to change that!
My first blog post describes the process of fixing a relatively obscure bug. I’m following the advice of @chiuki: “When something takes longer than expected, write it down.”
The Android team at EG had been working on an internal project to sharpen our skills. We were trying out the Data Binding library, among other things. The main screen consisted of a RecyclerView to show time tracking entries. We were binding some data to the RecyclerView’s Adapter, and it seemed to be working fine.
The relevant portions of our Adapter looked like this:
@Override public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) { ItemTimeEntryBinding timeEntryBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_time_entry, parent, false); return new BindingHolder(timeEntryBinding); } @Override public void onBindViewHolder(BindingHolder holder, int position) { ItemTimeEntryBinding timeEntryBinding = holder.binding; timeEntryBinding.setViewModel(new TimeEntryItemViewModel(context, timeEntries.get(position))); } public static class BindingHolder extends RecyclerView.ViewHolder { private ItemTimeEntryBinding binding; public BindingHolder(ItemTimeEntryBinding binding) { super(binding.cardView); this.binding = binding; } }
As a separate learning exercise, one of our developers converted a relatively complex RelativeLayout to a PercentRelativeLayout from the Percent Support Library. The ability to specify dimensions and margins as percentages can be quite handy. However, this is where the subtle bug was introduced.
On most devices, it worked fine. However, on some devices the PercentRelativeLayout rendered without the margins we had defined. Then, after scrolling around in the RecyclerView for a bit, the view holders would get reused and the issue would correct itself. Puzzling!
What was going on? This is the part of the story that all developers are familiar with: I searched Google and Stackoverflow over and over, scratched my head repeatedly, took lots of snack breaks, etc. Eventually I found the following nugget in the official documentation:
Immediate Binding
When a variable or observable changes, the binding will be scheduled to change before the next frame. There are times, however, when binding must be executed immediately. To force execution, use the executePendingBindings() method.
Sure enough, adding this line to my adapter fixed the problem!
@Override public void onBindViewHolder(BindingHolder holder, int position) { ItemTimeEntryBinding timeEntryBinding = holder.binding; timeEntryBinding.setViewModel(new TimeEntryItemViewModel(context, timeEntries.get(position))); timeEntryBinding.executePendingBindings(); }
In hindsight, it makes sense that the RecyclerView needs the data to be bound immediately. However, why did it sometimes work without this line? This is still a mystery to me. I’m guessing the combination of using PercentRelativeLayout with a somewhat complex adapter layout caused some atypical timing issues. Since timing problems like this are non-deterministic, I count this as a strike against Data Binding.
Would I still use Data Binding in a production project? Maybe for certain situations where it could remove a lot of boilerplate code. My other complaint was the lack of two-way bindings, but this has been addressed since we had investigated the library here at E-gineering. The official documentation hasn’t been updated to reflect this, which is a bit concerning.
I’ve created a small sample application that demonstrates the issue in this post. The source is posted on Github. Once you launch the app, the button will toggle whether or not executePendingBindings() is called. Remember that not all devices will exhibit the problem so you may not see a change after pressing the button.
If anyone can explain the root cause of the issue in more detail, I’d love to know. Reach out to me on Twitter at @nate_ridderman and I’ll be sure to edit this post with anything I learn and proper attribution.