Networking Across Configuration Changes

Now that you have your app deserializing JSON into model objects, take a closer look at how your implementation behaves when a configuration change occurs. Run your app, make sure auto-rotate is turned on for your device or emulator, and then rotate the device quickly five or so times in a row. Inspect the Logcat output, filtering by PhotoGalleryFragment and turning soft wraps off (Figure 24.8).

Figure 24.8  Logcat output across multiple rotations

Logcat output across multiple rotations

What is going on here? A new network request is made every time you rotate the device. This is because you kick off the request in onCreate(…). Since the fragment is destroyed and re-created every time you rotate, a new request is issued to (unnecessarily) re-download the data. This is problematic because you are doing duplicate work – you should instead issue a download request when the fragment is first created. That same request (and the resulting data) should persist across rotation to ensure a speedy user experience (and to avoid unnecessarily using up the user’s data if they are not on WiFi).

Instead of launching a new web request every time a configuration changes occurs, you need to fetch the photo data once, when the fragment is initially created and displayed onscreen. Then you can allow the web request to continue to execute when a configuration change occurs by caching the results in memory for the perceived life of the fragment, across any and all configuration changes (such as rotation). Finally, you can use these cached results when available rather than making a new request.

ViewModel is the right tool to help you with this job. (If you need a refresher on ViewModel, refer back to Chapter 4.)

First, add the lifecycle-extensions dependency to app/build.gradle.

Listing 24.29  Adding the lifecycle-extensions dependency (app/build.gradle)

dependencies {
    ...
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
    ...
}

Next, create a ViewModel class named PhotoGalleryViewModel. Add a property to store a live data object holding a list of gallery items. Kick off a web request to fetch photo data when the ViewModel is first initialized, and stash the resulting live data in the property you created. When you are done, your code should match Listing 24.30.

Listing 24.30  Shiny new ViewModel (PhotoGalleryViewModel.kt)

class PhotoGalleryViewModel : ViewModel() {

    val galleryItemLiveData: LiveData<List<GalleryItem>>

    init {
        galleryItemLiveData = FlickrFetchr().fetchPhotos()
    }
}

You call FlickrFetchr().fetchPhotos() in PhotoGalleryViewModel’s init{} block. This kicks off the request for photo data when the ViewModel is first created. Since the ViewModel is only created once in the span of lifecycle owner’s lifetime (when queried from the ViewModelProviders class for the first time), the request will only be made once (when the user launches PhotoGalleryFragment). When the user rotates the device or some other configuration change happens, the ViewModel will remain in memory, and the re-created version of the fragment will be able to access the results of the original request through the ViewModel.

With this design, the FlickrFetchr repository will continue to execute the request even if the user backs out of the fragment’s hosting activity early. In your app, the result of the request will just be ignored. But in a production app, you might cache the results in a database or some other local storage, so it would make sense to let the fetch continue to completion.

If you instead wanted to stop an in-flight FlickrFetchr request when the user exits the fragment, you could update FlickrFetchr to stash the Call object representing the web request and cancel the request when the ViewModel is removed from memory. See the section called For the More Curious: Canceling Requests later in this chapter for more details.

Update PhotoGalleryFragment.onCreate(…) to get access to the ViewModel. Stash a reference to the ViewModel in a property. Remove the existing code that interacts with FlickrFetchr, since PhotoGalleryViewModel handles that now.

Listing 24.31  Getting a ViewModel instance from the provider (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoGalleryViewModel: PhotoGalleryViewModel
    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val flickrLiveData: LiveData<List<GalleryItem>> = FlickrFetchr().fetchPhotos()
        flickrLiveData.observe(
            this,
            Observer { galleryItems ->
                Log.d(TAG, "Response received: $galleryItems")
            })

        photoGalleryViewModel =
                ViewModelProviders.of(this).get(PhotoGalleryViewModel::class.java)
    }

    ...
}

Recall that the first time a ViewModel is requested for a given lifecycle owner, a new instance of the ViewModel is created. When PhotoGalleryFragment is destroyed and re-created due to a configuration change like rotation, the existing ViewModel persists. Successive requests for the ViewModel return the same instance that was originally created.

Now, update PhotoGalleryFragment to observe PhotoGalleryViewModel’s live data once the fragment’s view is created. For now, log a statement indicating the data was received. Eventually you will use these results to update your recycler view contents.

Listing 24.32  Observing the ViewModel’s live data (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreateView(
        ...
    ): View {
        ...
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        photoGalleryViewModel.galleryItemLiveData.observe(
            viewLifecycleOwner,
            Observer { galleryItems ->
                Log.d(TAG, "Have gallery items from ViewModel $galleryItems")
                // Eventually, update data backing the recycler view
            })
    }
    ...
}

Eventually you will update UI-related things (such as the recycler view adapter) in response to data changes. Starting the observation in onViewCreated(…) ensures that the UI widgets and other related objects will be ready. It also ensures that you properly handle the situation where the fragment becomes detached and its view gets destroyed. In this scenario, the view will be re-created when the fragment is reattached, and the live data subscription will be added to the new view once it is created.

(Out in the wild, you might also see the observation kicked off in onCreateView(…) or onActivityCreated(…), which should work fine but is less explicit about the relationship between the live data being observed and the life of the view.)

Passing viewLifecycleOwner as the LifecycleOwner parameter to LiveData.observe(LifecycleOwner, Observer) ensures that the LiveData object will remove your observer when the fragment’s view is destroyed.

Run your app. Filter Logcat by FlickrFetchr. Rotate the emulator multiple times. You should only see FlickrFetchr: Response received printed to the Logcat window one time, no matter how many times you rotate.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.70.157