0

A few months back, I was working on designing a client API (FooManager) for adding/removing/fetching a list of objects (Bar).

The requirements were simple-
1. Fetch operation is CPU-intensive and should be asynchronous.
2. API should be clear about when what data is available.

The options I thought of were-

Option #1 (using LiveData)-

class FooManager {
  private LiveData<List<Bar>> barList;

  public void initialize() {
    barList = new MutableLiveData();
    // do something
  }

  public LiveData<List<Bar>> fetchBars() {
    return barList;    
  }

  public boolean add(Bar bar) {
    // barList isn't guaranteed to be initialized by this point
    barList.getValue().add(bar);
    return true;  
  }

  public boolean remove(Bar bar) {
    // barList isn't guaranteed to be initialized by this point
    barList.getValue().remove(bar);
    return true;
  }
}

Pros: UI can directly observe on these elements, fairly less verbose, lifecycle aware.
Cons: No distinction between when what data is available for the client. Default state is confusing, when the Bar list hasn't been initialized yet.

Option #2 (using Futures and chaining Futures)-

class FooManager {
  private ListenableFuture<List<Bar>> barFuture;

  public initialize() {
    // do something
    barFuture = SettableFuture.create();
  }

  public ListenableFuture<List<Bar>> fetchBars() {
    // Doesn't seem right to return a chained future when add/remove operation is also in progress with this transformed future?
    return barFuture;    
  }

  public ListenableFuture<Boolean> add(Bar bar) {
    barFuture = Futures.transform(barFuture, list -> { list.add(bar); //... });
  }

  public ListenableFuture<Boolean> remove(Bar bar) {
    barFuture = Futures.transform(barFuture, list -> { list.remove(bar); //... });
  }
}

Pros: Less verbose, no global initialized state.
Cons: Chaining Futures seems unnecessary/overhead for add/remove operations.
Not sure if this is the right use case for a Future. Any ways to improve this option?

Option #3 (Using global initialized state with a listener)-

class FooManager {
  interface InitializationCompleteListener {
    void onInitializationComplete();
  }

  private InitializationCompleteListener listener;
  private boolean initialized;
  private List<Bar> barList;

  public initialize() {
    barList = new ArrayList<>();
    // do something, barList.add(bar); ...
    initialized = true;
    if (listener != null) {
      listener.onInitializationComplete();
    }
  }

  public boolean isInitialized() {
    return initialized;
  }

  public registerInitializationCompleteListener(InitializationCompleteListener listener) {
    this.listener = listener;
  }

  // Only allowed to be called after initialization.
  public List<Bar> fetchBars() {
    return barList;    
  }

  public boolean add(Bar bar) {
    barList.add(bar)
    return true;
  }

  public boolean remove(Bar bar) {
    barList.remove(bar)
    return true;
  }
}

Pros: Very clear for clients as to when the data will be available.
Cons: Too verbose, clients need to hold-off until the global initialization state is set.

This is probably a basic question but I'm looking for the best practices in the industry. Given the pros and cons, which of these approaches make the most sense, or if there's a separate pattern we could have followed here?

Thanks!

  • None of these will work in android because in android gui update is only allowed in the main gui thread which requires special synchronsation – k3b Feb 13 '19 at 15:45
  • Not really, all of these are perfectly fine to use in Android. LiveData was introduced for the exact same purpose. ListenableFuture on the other hand can be scheduled on a main thread executor to update UI. – VishalHemnani Feb 21 '19 at 18:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.