Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Architecture Components refactor: LiveData & ViewModel #41

Merged

Conversation

@lgvalle
Copy link
Member

commented May 23, 2018

LiveData & ViewModel

This PR is part of a series of PRs to explore Architecture Components.
The aim of this series is to showcase how to use Architecture Components and discuss tradeoffs, not to write a perfectly designed app.
With that in mind, changes are been kept to the minimum necessary to better show what are the needed changes.

This PR is meant to showcase how to replace RxJava with LiveData and ViewModel.

In this project, RxJava was used to perform asynchronous work in a separate thread, execute some transformations on the downloaded data and callback the main thread with the result.

All that can be done with LiveData + ViewModel:

  • Asynchrony is achieved through retrofit enqueue(retrofit2.Callback<T>).
  • LiveData is just a data holder class that can be observed. When you post a value into a LiveData instance the observer will be notified in the main thread.
  • A ViewModel stores the related UI data state and it's lifecycle aware: the stored data will survive configuration changes.

The sample app existing key components are:

  • MainActivity : represents the app UI, a list of movies fetched from IMDB. The list is paged: when the user reaches the bottom a new page is fetched.
  • MovieService : this class acts as repository and view state holder. It's composed with MoviesApi and contains a MoviesSate object to store the list of fetched movies and the current page. It provides asynchrony and api to domain data mapping with RxJava
  • MoviesApi : A Retrofit API definition interface, describes the API calls but has no logic

before

The proposed changes in this PR change that scenario:

  • A new key component, MoviesViewModel, is introduced. It's responsibilities are store the app's UI state and transform API data into domain data. Because MoviesViewModel is a ViewModel it's linked with MainActivity's lifecycle, so there's no need to manage subscription state.
  • LiveData replaces RxJava as delivery mechanism to observe data changes.
  • MovieService responsibilities are reduced to only make network calls using MovieApi

after

Considerations

This app is divided in two modules: app and core. Originally MovieService was inside core but I moved it into app in the destination branch before opening the PR.
The reason was to keep the diff easier to follow (which is the main goal of this PR). LiveData depends on Android so a modified version of MovieService would need to go into app. If the original MovieService were inside core the diff would consider them separate files and instead of highlight only the changes it would mark the whole files red and green

Follow up

Upcoming PRs will showcase how to use other Architecture Components:

  • ViewModel & LiveData (this PR)
  • DataBinding #42
  • Paging
  • Room
  • Navigation
  • WorkManager

In order to showcase some of those components, it might be necessary to extend the app and add a few more use cases and screens.

Progress will be tracked here: https://github.com/novoda/android-demos/projects/1

@@ -30,7 +31,8 @@ protected void onCreate(Bundle savedInstanceState) {
ButterKnife.bind(this);
setTitle("Top Rated Movies");

movieService = ((MoviesApplication) getApplication()).movieService();
MovieService movieService = ((MoviesApplication) getApplication()).movieService();
moviesViewModel = ViewModelProviders.of(this, new MoviesViewModelFactory(movieService)).get(MoviesViewModel.class);

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 23, 2018

Author Member

ViewModelProviders is an utility class provided by the lifecycle library to provision ViewModels. It ensures that ViewModels are reused when an Activity is recreated.
In this example, MoviesViewModel needs to be injected with MovieService in it's constructor. In order to do that, we need to provide a ViewModelFactory. This class knows how to create our MoviesViewModel and it's used by ViewModelProviders the first time it needs to create a MoviesViewModel

@Override
protected void onStop() {
super.onStop();
movieService.unsubscribe(callback);

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 23, 2018

Author Member

There's no need to manage subscription anymore since ViewModels lifecycle is tight to the activity they are linked with

protected void onStop() {
super.onStop();
movieService.unsubscribe(callback);
});

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 23, 2018

Author Member

private Callback callback;
private final MutableLiveData<List<Movie>> moviesLiveData = new MutableLiveData<>();
private final MutableLiveData<List<Video>> videosLiveData = new MutableLiveData<>();

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 23, 2018

Author Member

When someone calls loadMore or loadTrailerFor what happens is:

  • It enqueues an API call
  • It returns the corresponding liveData immediately so the caller can subscribe and observe it
  • When the API call finishes, it updates (postValue) the LiveData object, triggering a call to the onChange method on the observer.

This comment has been minimized.

Copy link
@takecare

takecare May 24, 2018

Contributor

why not hold these in the view model?

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 24, 2018

Author Member

They could be in the ViewModel, yes. In this example we can perfectly kill MovieService, inject MovieApi directly into the ViewModel and perform the network requests right there.

I kept the current architecture just for demonstration purposes. In a more complex app, you'll have something closer to this: a Repository performing requests to network or disk, catching, etc, and a ViewModel observing the repository's exposed liveData and mapping the received values into something the view can understand better.

}

public LiveData<MoviesSate> moviesLiveData() {
return Transformations.map(movieService.loadMore(moviesSate.pageNumber()),

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 23, 2018

Author Member

Transformations is another utility class that comes with the Lifecycle architecture components library.
From the documentation:

You can use transformation methods to carry information across the observer's lifecycle. The transformations aren't calculated unless an observer is observing the returned LiveData object.
Because the transformations are calculated lazily, lifecycle-related behavior is implicitly passed down without requiring additional explicit calls or dependencies.

Here we use Transformations.map to transform the raw API data into a domain data model. This is a very simple use case but it serves the purpose.

One interesting thing about Transformations.map is that it implicitly passes down the lifecycle owner. The LiveData object returned by movieService.loadMore will be linked to the same lifecycle owner that calls moviesLiveData()

This comment has been minimized.

Copy link
@ataulm

ataulm May 23, 2018

Contributor

So the benefit of the transformation API is to give it a chance to ... not do the transformation if there's nothing observing anymore?

I thought that onchanged would not be called if nothing was observing the livedata.

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 24, 2018

Author Member

Good question.
Transformations.map is just a map, the benefit of using it is to express intention, keep things cleaner and share a LifecycleOwner between observed liveDatas

For example, you could achieve the same result by observing the movieService LiveDataA from the viewModel and using the observed result to update a LiveDataB observed from MainActivity
The problem with this second approach is the lifecycleOwner you need to pass to observe LiveDataA. MainActivity is a lifecycleOwner so it can observe LiveDataB from the ViewModel with no problem. But MoviesViewModel is not a lifecycleOwner, so in order to observe LiveDataA from movieService you'll need to pass a reference of MainActivity to MoviesViewModel so it can use it to observe LiveDataB.

By using Transformations.map the first lifecycleOwner is shared among all chained LiveDatas

This comment has been minimized.

Copy link
@ataulm

ataulm May 24, 2018

Contributor

ah right, so in this case, it's just demonstrating how to use the API, but it's not specifically beneficial in our setup here?

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 24, 2018

Author Member

The mapping it's doing is trivial, so in that sense it's not adding much benefit but rather demonstrating how to use the API
But it also allows us to avoid passing a lifecycleOwner to the viewModel just to do a second observation. And this is a real benefit

This comment has been minimized.

Copy link
@ataulm

ataulm May 24, 2018

Contributor

ohhh seeen. because we're mapping from LiveData<X> -> LiveData<Y>, not just X -> Y which I initially thought, and wondered why we couldn't just do it in the method.

List<Movie> movies = moviesSate.movies();
movies.addAll(input);
moviesSate = new MoviesSate(movies, moviesSate.pageNumber() + 1);
return moviesSate;

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 23, 2018

Author Member

This API to domain mapping code is the same we had in the old MovieService class: https://github.com/novoda/android-demos/pull/41/files#diff-5643575e943cc4ee1d7bbb5b78d39395L49

@joetimmins
Copy link
Member

left a comment

Looks great! Good stuff @lgvalle

One question though, what is a Sate?

@ataulm
Copy link
Contributor

left a comment

Writing these comments on mobile, worst decision of my day 😭

movies.addAll(response.body().results);
moviesSate = new MoviesSate(movies, moviesSate.pageNumber() + 1);
callback.onNewData(moviesSate);
moviesLiveData.postValue(response.body().results);

This comment has been minimized.

Copy link
@ataulm

ataulm May 23, 2018

Contributor

Why post instead of set?

This comment has been minimized.

Copy link
@amlcurran

amlcurran May 24, 2018

Member

set should only be used on the main thread, post on other threads: https://developer.android.com/topic/libraries/architecture/livedata#update_livedata_objects

This comment has been minimized.

Copy link
@ataulm

ataulm May 24, 2018

Contributor

This is the main thread no?

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 24, 2018

Author Member

Yes, @ataulm is right: enqueue calls back into the main thread so in this case you can use moviesLiveData.setValue() too

This comment has been minimized.

Copy link
@ataulm

ataulm May 24, 2018

Contributor

are there any drawbacks to using post? e.g. for simplicity, could one ONLY use post?

*simplicity of remembering, not simplicity in technical whats-actually-happening

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 24, 2018

Author Member

AFAIK there's no difference for the observer. From the documentation:

  • setValue: Sets the value. If there are active observers, the value will be dispatched to them. This method must be called from the main thread
  • postValue: Posts a task to a main thread to set the given value. If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.

So both can be called from the main thread but only postValue can be called from a background thread. That "if" could be the only real use case difference.

Also from the docs:

If you have a following code executed in the main thread:

liveData.postValue("a");
liveData.setValue("b");

The value "b" would be set at first and later the main thread would override it with the value "a".

Something to have in mind if you use both setValue and postValue

This comment has been minimized.

Copy link
@ataulm

ataulm May 24, 2018

Contributor

in this case do you think it's worth using setValue since it's in the main thread?

If we use these as examples, I'd rather someone defaults to setValue and then is forced to read up on postValue because setValue is impossible to use on a background thread, rather than using postValue and getting caught out by the last example you gave.

}

public LiveData<MoviesSate> moviesLiveData() {
return Transformations.map(movieService.loadMore(moviesSate.pageNumber()),

This comment has been minimized.

Copy link
@ataulm

ataulm May 23, 2018

Contributor

So the benefit of the transformation API is to give it a chance to ... not do the transformation if there's nothing observing anymore?

I thought that onchanged would not be called if nothing was observing the livedata.

@@ -14,5 +13,5 @@
Call<MoviesResponse> topRated(@Query("page") int page);

@GET("movie/{id}/videos?api_key="+API_KEY)
Single<VideosResponse> videos(@Path("id") String movieId);
Call<VideosResponse> videos(@Path("id") String movieId);

This comment has been minimized.

Copy link
@ataulm

ataulm May 23, 2018

Contributor

Call handles moving stuff to a bg thread?

This comment has been minimized.

Copy link
@lgvalle

lgvalle May 24, 2018

Author Member

Yes, Call has two methods to run the query: execute (sync) and enqueue (async)

This comment has been minimized.

Copy link
@ataulm

ataulm May 24, 2018

Contributor

noice

@ataulm

This comment has been minimized.

Copy link
Contributor

commented May 23, 2018

I think would have been cool to do separate livedata and ViewModel.

In my understanding, swapping out of Rx would involve using a different bg threading mechanism (I guess this is the Call return type in retrofit interface) + livedata for observer pattern: viewmodel stuff isn't related.

@amlcurran
Copy link
Member

left a comment

nice overview 💯

@lgvalle

This comment has been minimized.

Copy link
Member Author

commented May 24, 2018

I've updated the PR description to add a "Considerations" section

@lgvalle

This comment has been minimized.

Copy link
Member Author

commented May 24, 2018

@joetimmins I believe MovieSate is a typo for MovieState but I didn't want to change it everywhere and make a mess of the PR

@ataulm

ataulm approved these changes May 24, 2018

@ataulm

This comment has been minimized.

Copy link
Contributor

commented May 24, 2018

Thanks Luis 💯

@lgvalle lgvalle changed the title Arch-Components refactor : LiveData & ViewModel Architecture Components refactor : LiveData & ViewModel May 30, 2018

@lgvalle lgvalle changed the title Architecture Components refactor : LiveData & ViewModel Architecture Components refactor: LiveData & ViewModel May 30, 2018

@xrigau xrigau merged commit ba69cdb into movies/arch-components-refactor Jun 7, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.