Providing User Control over Polling

Some users may not want your app to run in the background. An important control to provide users is the ability to enable and disable background polling.

For PhotoGallery, you will add a menu item to the app bar that will toggle your worker when selected. You will also update your work request to run your worker periodically instead of just once.

To toggle your worker, you first need to determine whether the worker is currently running. To do this, supplement your PreferencesRepository to store a flag indicating whether the worker is enabled.

Listing 22.12  Saving Worker state (PreferencesRepository.kt)

class PreferencesRepository private constructor(
    private val dataStore: DataStore<Preferences>
) {
    ...
    suspend fun setLastResultId(lastResultId: String) {
        dataStore.edit {
            it[PREF_LAST_RESULT_ID] = lastResultId
        }
    }

    val isPolling: Flow<Boolean> = dataStore.data.map {
        it[PREF_IS_POLLING] ?: false
    }.distinctUntilChanged()

    suspend fun setPolling(isPolling: Boolean) {
        dataStore.edit {
            it[PREF_IS_POLLING] = isPolling
        }
    }

    companion object {
        private val SEARCH_QUERY_KEY = stringPreferencesKey("search_query")
        private val PREF_LAST_RESULT_ID = stringPreferencesKey("lastResultId")
        private val PREF_IS_POLLING = booleanPreferencesKey("isPolling")
        private var INSTANCE: PreferencesRepository? = null
        ...
    }
}

Next, add the string resources your options menu item needs. You will need two strings, one to prompt the user to enable polling and one to prompt them to disable it.

Listing 22.13  Adding poll-toggling resources (res/values/strings.xml)

<resources>
    ...
    <string name="new_pictures_text">You have new pictures in PhotoGallery.</string>
    <string name="start_polling">Start polling</string>
    <string name="stop_polling">Stop polling</string>
</resources>

With your strings in place, open up your res/menu/fragment_photo_gallery.xml menu file and add a new item for your polling toggle.

Listing 22.14  Adding a poll-toggling item (res/menu/fragment_photo_gallery.xml)

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
    ...
    <item android:id="@+id/menu_item_clear"
          android:title="@string/clear_search"
          app:showAsAction="never" />

    <item android:id="@+id/menu_item_toggle_polling"
          android:title="@string/start_polling"
          app:showAsAction="ifRoom|withText"/>
</menu>

The default text for this item is the start_polling string. You will need to update this text if the worker is already running. Start by getting a reference to your new menu item in PhotoGalleryFragment.

Listing 22.15  Accessing the menu item (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    private var searchView: SearchView? = null
    private var pollingMenuItem: MenuItem? = null
    ...
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.fragment_photo_gallery, menu)

        val searchItem: MenuItem = menu.findItem(R.id.menu_item_search)
        searchView = searchItem.actionView as? SearchView
        pollingMenuItem = menu.findItem(R.id.menu_item_toggle_polling)

        searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            ...
        })
    }
    ...
    override fun onDestroyOptionsMenu() {
        super.onDestroyOptionsMenu()
        searchView = null
        pollingMenuItem = null
    }
    ...
}

Next, include whether the worker is running in your PhotoGalleryUiState by collecting the latest value from the isPolling property on the PreferencesRepository class. Also, add a function to toggle the property.

Listing 22.16  Adding more data to PhotoGalleryUiState (PhotoGalleryViewModel.kt)

class PhotoGalleryViewModel : ViewModel() {
    ...
    init {
        viewModelScope.launch {
            preferencesRepository.storedQuery.collectLatest { storedQuery ->
                ...
            }
        }

        viewModelScope.launch {
            preferencesRepository.isPolling.collect { isPolling ->
                _uiState.update { it.copy(isPolling = isPolling) }
            }
        }
    }

    fun setQuery(query: String) {
        viewModelScope.launch { preferencesRepository.setStoredQuery(query) }
    }

    fun toggleIsPolling() {
        viewModelScope.launch {
            preferencesRepository.setPolling(!uiState.value.isPolling)
        }
    }
    ...
}

data class PhotoGalleryUiState(
    val images: List<GalleryItem> = listOf(),
    val query: String = "",
    val isPolling: Boolean = false,
)

Open PhotoGalleryFragment.kt and update your menu item text whenever you receive a new PhotoGalleryUiState value. Do that work in a separate private function named updatePollingState(). You will add some more code to that function in just a second.

Listing 22.17  Setting correct menu item text (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                photoGalleryViewModel.uiState.collect { state ->
                    binding.photoGrid.adapter = PhotoListAdapter(state.images)
                    searchView?.setQuery(state.query, false)
                    updatePollingState(state.isPolling)
                }
            }
        }
    }
    ...
    override fun onDestroyOptionsMenu() {
        ...
    }

    private fun updatePollingState(isPolling: Boolean) {
        val toggleItemTitle = if (isPolling) {
            R.string.stop_polling
        } else {
            R.string.start_polling
        }
        pollingMenuItem?.setTitle(toggleItemTitle)
    }
}

Now, call the newly created toggleIsPolling() on your PhotoGalleryViewModel whenever your menu item is pressed.

Listing 22.18  Handling menu item presses (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.menu_item_clear -> {
                photoGalleryViewModel.setQuery("")
                true
            }
            R.id.menu_item_toggle_polling -> {
                photoGalleryViewModel.toggleIsPolling()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
    ...
}

Finally, delete the OneTimeWorkRequest logic from the onCreate(…) function, since it is no longer needed. Instead, add code to the new updatePollingState() to update the background work. If the worker is not running, create a new PeriodicWorkRequest and schedule it with the WorkManager. If the worker is running, stop it.

Listing 22.19  Handling poll-toggling item clicks (PhotoGalleryFragment.kt)

private const val TAG = "PhotoGalleryFragment"
private const val POLL_WORK = "POLL_WORK"

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)

        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.UNMETERED)
            .build()
        val workRequest = OneTimeWorkRequest
            .Builder(PollWorker::class.java)
            .setConstraints(constraints)
            .build()
        WorkManager.getInstance(requireContext())
            .enqueue(workRequest)
    }
    ...
    private fun updatePollingState(isPolling: Boolean) {
        val toggleItemTitle = if (isPolling) {
            R.string.stop_polling
        } else {
            R.string.start_polling
        }
        pollingMenuItem?.setTitle(toggleItemTitle)

        if (isPolling) {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.UNMETERED)
                .build()
            val periodicRequest =
                PeriodicWorkRequestBuilder<PollWorker>(15, TimeUnit.MINUTES)
                    .setConstraints(constraints)
                    .build()
            WorkManager.getInstance(requireContext()).enqueueUniquePeriodicWork(
                POLL_WORK,
                ExistingPeriodicWorkPolicy.KEEP,
                periodicRequest
            )
        } else {
            WorkManager.getInstance(requireContext()).cancelUniqueWork(POLL_WORK)
        }
    }
}

If you are given a choice when importing TimeUnit, select java.util.concurrent.TimeUnit.

Focus first on the else block you added here. If the worker is currently not running, then you schedule a new work request with the WorkManager. In this case, you are using the PeriodicWorkRequest class to make your worker reschedule itself on an interval. The work request uses a builder, like the OneTimeWorkRequest you used previously. The builder needs the worker class to run as well as the interval it should use to execute the worker.

If you are thinking that 15 minutes is a long time for an interval, you are right. However, if you tried to enter a smaller interval value, you would find that your worker still executes on a 15-minute interval. This is the minimum interval allowed for a PeriodicWorkRequest so that the system is not tied up running the same work request all the time. This saves system resources – and the user’s battery life.

The PeriodicWorkRequest builder accepts constraints, just like the one-time request, so you add the unmetered network requirement. To schedule the work request, you use the WorkManager class, but this time you use the enqueueUniquePeriodicWork(…) function. This function takes in a String name, a policy, and your work request. The name allows you to uniquely identify the request, which is useful when you want to cancel it.

The existing work policy tells the work manager what to do if you have already scheduled a work request with a particular name. In this case you use the KEEP option, which discards your new request in favor of the one that already exists. The other option is REPLACE, which, as the name implies, will replace the existing work request with the new one.

If the worker is already running, then you need to tell the WorkManager to cancel the work request. In this case, you call the cancelUniqueWork(…) function with the "POLL_WORK" name to remove the periodic work request.

Run the application. You should see your new menu item to toggle polling. If you do not want to wait for the 15-minute interval, you can disable the polling, wait a few seconds, then enable polling to rerun the work request.

PhotoGallery can now keep the user up to date with the latest images automatically, even when the app is not running (Figure 22.5).

Figure 22.5  The end result

The end result

In the next chapter, you will finish your work on PhotoGallery by allowing users to open a photo’s page on Flickr.

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

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