When one teaches, two learn. — Robert Heinlein
As of this writing, I’m in term 2 of Udacity’s Android Developer Nanodegree program. When I enrolled, I already had years of Android experience. Nevertheless, I expected to benefit from some formalized training. Plus, I really want that fancy certificate of completion. 😀
What I hadn’t anticipated, however, was the opportunity to help other students. After all, the best way to learn is to teach! What follows are a sampling of answers I’ve authored during interactions in their Q&A Knowledge Board and Student Hub. Hopefully others will find it helpful.
Thinking about enrolling in a Nanodegree program? Use my referral link for $50 off.
How should I design this app to support both sorted and favorite movie lists?
Let’s say MainActivity observes a LiveData list of Movies in your ViewModel; anytime the list changes, it updates the recyclerview. With this, MainActivity doesn’t need to care about the sort order or if it’s a list of favorites or even how those lists are created. When the order (or favorites choice) changes, MainActivity simply tells the ViewModel… and then keeps on watchin’ for the list to change. Easy-peasy.
The ViewModel now needs to “transform” its public LiveData list of movies accordingly and conditionally “switch” it to some other list. You can do this kind of thing using Transformations.switchMap or MediatorLiveData (the former uses the latter). Using switchMap, you can basically say: anytime this LiveData changes (say, one that represents the type of Movie list to return), I want to run this function and return the appropriate LiveData in response. The ViewModel then calls methods of a so-called Repository class where you would put all calls to your dao, services, etc.
The switchMap method describes this, although admittedly this javadoc made no sense to me until I actually decided to try to use it. Here’s some pseudo-code of the ViewModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private LiveData movieListTypeLiveData; private LiveData favoriteLiveData = moviesDao.getFavorites(); private LiveData sortedByRatingLiveData = moviesDao.getSortedByRating(); private LiveData movieListLiveData = // transform the movie list based on the movie list type Transformations.switchMap(movieListTypeLiveData, Function { public LiveData apply(movieListType) { return movieListType == favorite ? favoriteLiveData : sortedByRatingLiveData; } }); public LiveData getMovieListLiveData() { // activity observes this return movieListLiveData; } public void changeMovieListType(movieListType) { // activity calls this movieListTypeLiveData.setValue(movieListType); } |
Why do my movie trailers load only after leaving and returning to the details screen?
I think I see the issue. The call to mReviewDao.getReviews is going to quickly return a LiveData. This does not mean that a value on it has been set, so you can’t immediately call getValue on the next line (this is why we usually observe LiveData to know when it’s “ready”).
Here’s one approach I’ve seen to this problem. You can create your own LiveData. Then, off the main thread, you check the db and call the service if necessary—both synchronously. Once you have a result, you post the value to your LiveData.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public LiveData<List<Review>> getReviews(int id) { final MutableLiveData<List<Review>> reviews = new MutableLiveData<>(); AppExecutors.getInstance().diskIO().execute(() -> { List<Review> reviewsFromDb = mReviewDao.getReviews(id); if (reviewsFromDb == null || reviewsFromDb.size() == 0) { AppExecutors.getInstance().networkIO().execute(() -> { reviews.postValue(loadReviews(String.valueOf(id))); }); } else { reviews.postValue(reviewsFromDb); } }); return reviews; } |
Are there other types of configuration changes besides device rotation? Can I lock my app like Uber and skip a lot of code?
Honestly, I think what you’re feeling is common among everyone when they first start developing in Android. It seems unnecessary and a burden… but I promise eventually most people come to appreciate the design and flexibility it provides.
First, note that some apps that appear to prevent rotation on a phone may behave differently on larger screens. They might unlock it for mid-size devices in portrait mode and then switch to a two-pane (master/detail) layout in landscape.
Consider all of the “resources” your app might need such as images, layouts, integer values such as margin sizes, colors, text strings, fonts, animation definitions, etc, etc! The design allows these values to change based on, not only the screen (size, density, orientation), but also things like api level, day and night, existence of a d-pad, and localization for right-to-left languages. The chosen resources don’t need to remain static through the life of your app but can change to provide the best experience possible for the current “conditions”.
Yes, there are other events that trigger configuration changes such as plugging in an external keyboard, going into multi-window mode or a change to uiMode (device docked or night mode engaged). It’s rare that you’ll have a legitimate reason to disable the normal processing for these (I’ve only seen it with game apps that completely manage their own statefulness).
But, saving state is not only about configuration changes. It’s also the case that the system may decide to kill your app anytime the user is off interacting with another (less awesome) app. And it’s your responsibility to ensure the state is restored when they return.
Here’s a nice discussion and chart comparing ViewModel, onSaveInstanceState and persistent storage:
https://developer.android.com/topic/libraries/architecture/saving-states
Here’s a list of other pages on the subject:
https://developer.android.com/guide/topics/resources/runtime-changes#HandlingTheChange
https://developer.android.com/guide/topics/manifest/activity-element#config
https://developer.android.com/reference/android/app/Activity.html#configuration-changes
https://developer.android.com/guide/topics/resources/runtime-changes
https://developer.android.com/guide/components/activities/activity-lifecycle#saras
https://developer.android.com/guide/topics/ui/multi-window.html#lifecycle
https://developer.android.com/reference/android/content/res/Configuration.html
What is the attachToRoot parameter when calling LayoutInflator.inflate()?
First, it’s important to know exactly why the second parameter is necessary.
View inflate(int resource, ViewGroup root, boolean attachToRoot)
Remember that ViewGroups are a special kind of View that can contain other child Views. So, for example, we can add a Button to a LinearLayout. When we do this, we set LayoutParams such as android:layout_width on the Button to tell its parent, the LinearLayout, how it wants to be laid out. Sometimes we use params that are specific to the ViewGroup into which our View will be placed, like android:layout_weight which is a LinearLayout.LayoutParam.
So when we add the Button to a LinearLayout, the inflator needs to know that it’s going into a LinearLayout so it can make sense of the layout params being set on the View. Or, from the javadoc: “root is only used to create the correct subclass of LayoutParams for the root view in the XML.”
Next the inflator says, “now that I know how to interpret the parameters, do you want me to add it to the ViewGroup for you now… or will you (or someone else) do it later?” You can see the source in LayoutInflator.java (line 651). It generates instances of your view hierarchy, generates layout parameters based on the root, and then conditionally adds the view: if (attachToRoot) root.addView(child). The return value is also affected by this (line 657). Per the docs: if root was supplied and attachToRoot is true, the return is “root”; otherwise it is the root of the inflated XML file.
RecyclerView ViewHolders require you to send false for this parameter because they do their own management of the pool of Views that get recycled in and out.
Note that the “parent” isn’t in the layout you’re inflating. It’s the container into which you’re adding the layout you’re inflating. The inflate method is often used inside methods where the appropriate root ViewGroup is provided for us. For example, the onCreateViewHolder method in an adapter and the onCreateView method for Fragments both have an incoming ViewGroup parameter that we should use as the parent when calling inflate.
How can I store a model class that has multiple List object fields in a room database?
You can do one-to-many (and many-to-many) relationships in SQLite using Room. You can find several posts on the subject. Also take a look at the @Relation annotation and StackOverflow. And here’s a quick example I made for you.
Could I get an example of using JsonReader for the Baking app assignment?
Now days, we have libraries like Moshi that make it easy to parse JSON. Look into how they can be plugged into Retrofit, and you’ll enjoy coding a lot more. I wrote some ugly sample code to show how JsonReader could be used… but really, why would you want to do this?
Are there any articles that explain how to test AsyncTask?
It depends on what type of testing you’re going to do. Here are several approaches regarding testing of AsyncTasks.
Isolate the work being done in doInBackground and just test it directly outside of the AsyncTask:
https://stackoverflow.com/a/49529277
Write a local junit test that extends your AsyncTask and uses either a CountDownLatch or wait/notify to block until it is complete:
https://stackoverflow.com/a/44222992
https://medium.com/@v.danylo/simple-way-to-test-asynchronous-actions-in-android-service-asynctask-thread-rxjava-etc-d43b0402e005
https://marksunghunpark.blogspot.com/2015/05/how-to-test-asynctask-in-android.html
Make a factory and mock the AsyncTask task so it doesn’t do the actual work:
https://github.com/marcouberti/mockandroidasynctask
Note that Espresso tests should already recognize busy AsyncTasks:
https://developer.android.com/reference/android/support/test/espresso/IdlingResource.html