Android has come a loooong way
I was really pleased when returning to Android development after being away for a while at how much better it has gotten with AndroidX. Now this isn’t breaking news to most, but Android Jetpack and the androidx.*
package libraries not only solved one of the most confusing aspects of Android development ( those pesky versioned Android Support Libraries ) but also brought the observable pattern front and center with LiveData
and ViewModel
. While LiveData is not quite as robust or generic as RxJava, it is effectively a basic implementation of the Observable pattern with the added benefit of being Lifecycle Aware.
That last bit there is huge. Being lifecycle aware means that you don’t have to worry about memory leaks or hard-to-debug NPEs because your observables live and die with their observers. Activities and their layouts go in and out of memory as user's navigate around. Ensuring that our asynchronous data loads don't try and update screens long gone is especially important.
It’s all about the observe
method.
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer);
takes two parameters: an owner with a Lifecycle and an Observer for trigger when the underlying value changes. Pretty straightforward.
Coupling this with Android’s Data Binding Library
means screens that handling swiping full screen content layouts is a really clean and pleasant exercise. One of our latest client projects involved adding the capability for their customers to take the physical gift cards purchased in their stores and add them to a digital wallet. We wanted to build a way for customers to be able to view all the details of an individual card (card number, barcode, terms and conditions, transaction history, etc..) We decided that a full screen view of each card was required for all this information but wanted users to be able to swipe left and right to horizontally page between each of the cards in their wallet.
We decided to effectively use a ViewPager
and PagerAdapter
to accomplish the horizontal paging between gift cards. That, combined with a ViewModel
to track the current page meant that the our observe callback just had to update the data binding backing the view after loading from our API.
Here is a simplified example of the Activity:
public class GiftCardDetailsActivity extends AppCompatActivity implements GiftCardDetailsPagerListener {
private GiftCardsDetailViewModel giftCardsViewModel;
private GiftCardDetailsActivityBinding binding;
private GiftCardTransactionAdapter giftCardTransactionAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int initialPosition = (int) getIntent().getSerializableExtra(GIFT_CARD_INITIAL_POSITION_EXTRA);
giftCardsViewModel = ViewModelProviders.of(this).get(GiftCardsDetailViewModel.class);
giftCardTransactionAdapter = new GiftCardTransactionAdapter();
binding = DataBindingUtil.setContentView(this, R.layout.gift_card_details_activity);
binding.giftCardViewPager.setAdapter(new GiftCardDetailsPagerAdapter(this, initialPosition);
giftCardsViewModel.getCurrentGiftCard().observe(this, currentGiftCard -> {
binding.setGiftCard(currentGiftCard);
binding.barcodeImage.setImageBitmap(BarcodeGenerator.generateBarcodeImage(getResources(), currentGiftCard.getNumber()));
giftCardTransactionAdapter.setGiftCardTransactions(currentGiftCard.getTransactions());
giftCardTransactionAdapter.notifyDataSetChanged();
});
giftCardsViewModel.setCurrentGiftCardPosition(initialPosition);
}
@Override
public void onPageSelected(int position) {
giftCardsViewModel.setCurrentGiftCardPosition(position);
}
}
And the ViewModel:
public class GiftCardsDetailViewModel extends AsyncLoadingViewModel implements GiftCardsApiResponseListener {
private GiftCardApiService giftCardApiService;
private MutableLiveData<GiftCard> currentGiftCard = new MutableLiveData<>(new GiftCard());
private List<GiftCard> giftCards;
private int initialPosition;
LiveData<GiftCard> getCurrentGiftCard() {
if (giftCards == null) {
setIsLoading(true);
giftCardsApiService.fetch(this);
}
return currentGiftCard;
}
void setCurrentGiftCardPosition(int position) {
if (giftCards == null || position > giftCards.size()-1) {
initialPosition = position;
return;
}
currentGiftCard.setValue(giftCards.get(position));
}
@Override
public void onGiftCardsApiResponseSuccess(List<GiftCard> giftCards) {
setIsLoading(false);
this.giftCards = giftCards;
this.currentGiftCard.setValue(giftCards.get(initialPosition));
}
@Override
public void onGiftCardsApiResponseFailure(String errorMessage) {
setIsLoading(false);
// classic "error handling here" blog post comment...
}
}
On of the nice things about this implementation is that there is only one way, and exactly one way, in which data is set on the view binding. Our Activity’s onCreate()
is responsible for instantiating the ViewModel
that tracks the current page, and holds onto the full list of gift cards that are asynchronously loaded from our server’s API. Anytime the instance of the ViewModel
’s underlying LiveData
member's change, our observable callback is triggered and we simply update the bindings gift card. Notice that the first argument to the observe()
call is an implementation of LifecycleAware
. In this case, our Activity itself.
A few other noteworthy details:
- While our ViewModel's private member
currentGiftCard
is of typeMutableLiveData
we only exposeLiveData
to observers. This is important because it isolates the responsibility of managing that state to ourViewModel
. - The
AsyncLoadingViewModel
is just a parent class that extends Android'sViewModel
class and manages a single piece of state for loading masks of typeLiveData<Boolean>
- The
ViewModel
holds onto the initial position until the initial data load is completed.
Followup
I should note here that there are a few other ways to accomplish similar behavior by leveraging more of the AndroidX library, including Two-way data binding
, @BindingAdapters
, and BaseObservables
. Given we are not taking any user input on these largely view-only screens, this felt like overkill. This simple pattern seemed to work well for the initial version of these screens.
Perhaps a follow-up post to compare / contrast is in order!
Top comments (1)
In the dynamic business landscape of Singapore, corporate gifts play a pivotal role in strengthening relationships and fostering goodwill. From exquisite local delicacies to bespoke merchandise, the art of corporate gift Singapore reflects the nation's diverse culture and values. These thoughtful gestures serve as bridges, connecting businesses with clients, partners, and employees on a deeper level.