Networking Across Configuration Changes

Now that you have your app deserializing JSON into model objects, take a closer look at how your implementation behaves across a configuration change. 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.

    15:49:07.304 D/PhotoGalleryFragment: Response received: [GalleryItem(...
    15:49:16.794 D/PhotoGalleryFragment: Response received: [GalleryItem(...
    15:49:20.098 D/PhotoGalleryFragment: Response received: [GalleryItem(...
    15:49:23.565 D/PhotoGalleryFragment: Response received: [GalleryItem(...
    15:49:27.043 D/PhotoGalleryFragment: Response received: [GalleryItem(...
    15:49:30.099 D/PhotoGalleryFragment: Response received: [GalleryItem(...
    ...

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 onViewCreated(…). 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 change 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.

You already added the ViewModel dependency to the project, so go ahead and create a ViewModel class named PhotoGalleryViewModel. This ViewModel will look very similar to the ViewModels in CriminalIntent. Use a StateFlow to expose a list of gallery items to the fragment. Kick off a web request to fetch photo data when the ViewModel is first initialized, and stash the resulting data in the property you created. Use a try/catch block to handle any errors.

When you are done, your code should match Listing 20.28.

Listing 20.28  Shiny new ViewModel (PhotoGalleryViewModel.kt)

private const val TAG = "PhotoGalleryViewModel"

class PhotoGalleryViewModel : ViewModel() {
    private val photoRepository = PhotoRepository()

    private val _galleryItems: MutableStateFlow<List<GalleryItem>> =
        MutableStateFlow(emptyList())
    val galleryItems: StateFlow<List<GalleryItem>>
        get() = _galleryItems.asStateFlow()

    init {
        viewModelScope.launch {
            try {
                val items = photoRepository.fetchPhotos()
                Log.d(TAG, "Items received: $items")
                _galleryItems.value = items
            } catch (ex: Exception) {
                Log.e(TAG, "Failed to fetch gallery items", ex)
            }
        }
    }
}

Recall that the first time a ViewModel is requested for a given lifecycle owner, a new instance of the ViewModel is created. Successive requests for the ViewModel return the same instance that was originally created.

You call PhotoRepository().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 lifecycle owner’s lifetime (when queried from the ViewModelProvider 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 occurs, 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.

Thanks to coroutines, when the viewModelScope is canceled, your network request will also be canceled. 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.

Update PhotoGalleryFragment to get access to the PhotoGalleryViewModel. Remove the existing code that interacts with PhotoRepository, since PhotoGalleryViewModel handles that now.

Also, update PhotoGalleryFragment to observe PhotoGalleryViewModel’s StateFlow 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 20.29  Getting a ViewModel instance from the provider (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    private var _binding: FragmentPhotoGalleryBinding? = null
    private val binding
        get() = checkNotNull(_binding) {
            "Cannot access binding because it is null. Is the view visible?"
        }

    private val photoGalleryViewModel: PhotoGalleryViewModel by viewModels()
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            try {
                val response = PhotoRepository().fetchPhotos()
                Log.d(TAG, "Response received: $response")
            } catch (ex: Exception) {
                Log.e(TAG, "Failed to fetch gallery items", ex)
            }
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                photoGalleryViewModel.galleryItems.collect { items ->
                  Log.d(TAG, "Response received: $items")
                }
            }
        }
    }
    ...
}

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 views and other related objects will be ready. It also ensures that you properly handle the situation where the fragment becomes detached and its view is destroyed. In this scenario, the view will be re-created when the fragment is reattached, and the subscription will be added to the new view once it is created.

Run your app. Filter Logcat by PhotoGalleryViewModel. Rotate the emulator multiple times. You should only see PhotoGalleryViewModel: Items 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
18.118.253.223