Blog

AndroidX, LiveData and the Observable Pattern

01.27.2020 | Frontend Mobile | Luke Mueller

hero

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: 1. While our ViewModel's private member currentGiftCard is of type MutableLiveData we only expose LiveData to observers. This is important because it isolates the responsibility of managing that state to our ViewModel. 1. The AsyncLoadingViewModel is just a parent class that extends Android's ViewModel class and manages a single piece of state for loading masks of type LiveData<Boolean> 1. 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!

Share

Read More

Related Posts

related_image

06.30.2021 | Culture | Katy Scott

At Focused Labs, collaboration is key to how we work together; it helps our teams learn from each other, brings us closer and helps us become more efficient...

related_image

06.23.2021 | Culture | Austyn

Late-night feedings and diaper changes, the 3-4 month sleep regression, teething, and a growth spurt all mean I'm getting less sleep than...

related_image

05.12.2021 | Culture Backend Frontend | Ryan Taylor

Temporarily disrupts "normal" business operations and allow self-organized teams to rapid prototype around their interest areas

related_image

04.27.2021 | Culture | Erin Hochstatter

Several years ago, I'd been trying to find an approach to software consulting that made sense for me [...]

related_image

01.28.2021 | Backend | Parker Drake

Recently I found myself needing to validate fields in a Spring Boot controller written in Kotlin...

related_image

01.22.2021 | Tutorial | Luke Mueller

⌘+⇧+g is the way to go

related_image

01.21.2021 | Devops | Katy G

Kube jobs running wild? To delete successful jobs...

additional accent
accent
FocusedLabs

171 N Aberdeen St
Suite 400
Chicago, IL 60607
(708) 303-8088

[email protected]

© 2021 FocusedLabs, All Rights Reserved.

  • facebook icon
  • twitter icon
  • linkedin icon
  • github icon