Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

19. RxPreferences
Written by Alex Sullivan

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Every good Android developer is intimately familiar with SharedPreferences. You use it to store one-off values that you want to persist across the lifetime of the app.

Many developers will also be familiar with the tools you use to listen to changes in these preferences. The RxPreferences library provides a reactive wrapper around these preference notification listeners.

In this chapter, you’ll learn how the library works and how you can use it to effectively stream preference changes.

Getting started

In this chapter, you’re going to put the final touches on the HexColor app that you started in the Chapter 15, “Testing RxJava Code,” code and expanded upon in Chapter 17, “RxBindings.”

Open the starter project in Android Studio and run the app. You should see a familiar screen:

Try tapping out a hex color. You’ll see the screen change to that color, and a color name will appear in the top right below the actual hex value. If you tap on that color name, you should see a new pop-up appear at the bottom of the screen with it’s own edit text where you can enter a hex code.

At the bottom of that pop-up, there will be a small heart that should be the color of whatever hex code you input in the edit text at the top of the pop-up.

The goal for this last update to the HexColor app is to allow the user to tap that heart icon and have the main app’s background update to that new color.

Right now, the app is using a BottomSheetDialogFragment to show the bottom dialog and an Activity to show the main keyboard and color view.

As you already know, communicating between fragments and activities is a painful process. It usually means defining an interface for the Activity to implement and then using getActivity from the BottomSheetDialogFragment to hopefully communicate any changes back up to the Activity. However, you need to be careful to make sure that the Activity you get back from getActivity isn’t null, since that’s always a possibility!

If only there was a better way to communicate this information…

Using SharedPreferences

There is! The Android SDK provides SharedPreferences as a means to save small amounts of information that the user may be interested in across app restarts. The Android SDK also provides a way to observe preference changes for individual preference keys using the OnSharedPreferenceChangedListener interface, allowing you to build up an app that reacts to preference changes.

class ColorBottomSheetViewModel(
  startingColor: String,
  colorCoordinator: ColorCoordinator,
  sharedPreferences: SharedPreferences
) : ViewModel()
private val favoriteClicksObservable =
  PublishSubject.create<Unit>()
fun onFavoriteClick() = favoriteClicksObservable.onNext(Unit)
return ColorBottomSheetViewModel(colorString, ColorCoordinator(),
  PreferenceManager
    .getDefaultSharedPreferences(requireContext())) as T
favorite.clicks().subscribe {
  viewModel.onFavoriteClick()
}.addTo(disposables)
favoriteClicksObservable
  .subscribe {
    sharedPreferences.edit()
      .putString("favoriteColor", closestColorLiveData.value)
      .apply()
  }
  .addTo(disposables)

Listening for preference updates

Just like before, you’ll need to pass in an instance of SharedPreferences into the view model corresponding to the ColorActivity. Update the ColorViewModel class to accept an instance of SharedPreferences:

class ColorViewModel(
  backgroundScheduler: Scheduler,
  mainScheduler: Scheduler,
  colorApi: ColorApi,
  colorCoordinator: ColorCoordinator,
  sharedPreferences: SharedPreferences
) : ViewModel()
return ColorViewModel(Schedulers.io(),
  AndroidSchedulers.mainThread(),
  ColorApi, ColorCoordinator(),
  PreferenceManager
    .getDefaultSharedPreferences(this@ColorActivity)) as T
sharedPreferences.registerOnSharedPreferenceChangeListener {
  sharedPreferences, key ->
  if (key == "favoriteColor") {
    hexStringSubject.onNext(
        sharedPreferences.getString(key, ""))
  }
}
Registers a callback to be invoked when a change happens to a preference.

Caution: The preference manager does not currently store a strong reference to the listener. You must store a strong reference to the listener, or it will be susceptible to garbage collection. We recommend you keep a reference to the listener in the instance data of an object that will exist as long as you need the listener.
private val listener =
  SharedPreferences.OnSharedPreferenceChangeListener {
    sharedPreferences, key ->
    hexStringSubject.onNext(
      sharedPreferences.getString(key, ""))
}
sharedPreferences
  .registerOnSharedPreferenceChangeListener(listener)

Using RxPreferences

Now that you’ve seen how to write reactive code using SharedPreferences on your own, it’s time to take a look at the RxPreferences library to see an easier and more efficient way to use SharedPreferences reactively.

implementation 'com.f2prateek.rx.preferences2:rx-preferences:2.0.0'
sharedPreferences: SharedPreferences
sharedPreferences: RxSharedPreferences
val preference = sharedPreferences.getString("favoriteColor")
favoriteClicksObservable
  .map { closestColorLiveData.value!! }
  .subscribe(preference::set)
  .addTo(disposables)
return ColorBottomSheetViewModel(colorString, ColorCoordinator(),
  RxSharedPreferences.create(
    PreferenceManager
      .getDefaultSharedPreferences(requireContext()))) as T

Subscribing to preference changes

You’re properly saving the favoriteColor preference, so now it’s time to start observing it using the RxSharedPreferences library. Just like before, you’ll need to swap out the class arguments for ColorViewModel.

private val sharedPreferences: RxSharedPreferences
return ColorViewModel(Schedulers.io(), AndroidSchedulers.mainThread(),
  ColorApi, ColorCoordinator(),
  RxSharedPreferences.create(
    PreferenceManager
      .getDefaultSharedPreferences(this@ColorActivity))) as T
sharedPreferences.getString("favoriteColor")
  .asObservable()
  .filter { !it.isBlank() }
  .subscribe { hexStringSubject.onNext(it) }

Dealing with old versions of RxJava

There’s one thing missing with the Rx chain that you just wrote:

sharedPreferences.getString("favoriteColor")
  .asObservable()
  .filter { !it.isBlank() }
  .subscribe { hexStringSubject.onNext(it) }
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public fun Disposable.addTo(compositeDisposable: CompositeDisposable): Disposable defined in io.reactivex.rxjava3.kotlin
implementation "com.github.akarnokd:rxjava3-bridge:3.0.0"
fun <T> io.reactivex.Observable<T>.toV3Observable():  
    io.reactivex.rxjava3.core.Observable<T> {
  return RxJavaBridge.toV3Observable(this)
}
sharedPreferences.getString("favoriteColor")
  .asObservable()
  .toV3Observable()
  .filter { !it.isBlank() }
  .subscribe { hexStringSubject.onNext(it) }
  .addTo(disposables)

Saving custom objects

You’re now sending color data from the ColorBottomSheet to the ColorActivity seamlessly.

class ColorResponseConverter:
    Preference.Converter<ColorResponse> {
  override fun deserialize(serialized: String): ColorResponse {
    TODO()
  }

  override fun serialize(value: ColorResponse): String {
    TODO()
  }
}
val gson = Gson()
return gson.toJson(value)
val gson = Gson()
return gson.fromJson(serialized, ColorResponse::class.java)
val defaultColorResponse = ColorResponse(ColorName("#", "#"))
private var previouslyFetchedColor: ColorResponse =
    defaultColorResponse
// 1
val preference = sharedPreferences.getObject(
    "favoriteColor",
    defaultColorResponse,
    ColorResponseConverter()
)
favoriteClicksObservable
  // 2
  .map { previouslyFetchedColor }
  // 3
  .subscribe(preference::set)
  .addTo(disposables)
.doOnNext { previouslyFetchedColor = it }

Observing a custom object

Open the ColorViewModel class. Delete the code observing the "favoriteColor" preference string and replace it with the following:

sharedPreferences.getObject(
      "favoriteColor",
      defaultColorResponse,
      ColorResponseConverter()
).asObservable()
  .toV3Observable()
  .map { it.name }
  .subscribe {
    colorNameLiveData.postValue(it.value)
    hexStringSubject.onNext(it.closest_named_hex)
  }
  .addTo(disposables)
hexStringSubject
  .filter { it.length == 7 }
  .observeOn(mainScheduler)
  .subscribe {
    colorNameDisposable?.dispose()
    colorNameDisposable = colorApi.getClosestColor(it)
      .subscribeOn(backgroundScheduler)
      .subscribe { response ->       
          colorNameLiveData.postValue(response.name.value)
      }
  }
  .addTo(disposables)
hexStringSubject.onNext(currentHexValue() + it)
if (currentHexValue().length == 7) {
  colorNameDisposable?.dispose()
  colorNameDisposable =
    colorApi.getClosestColor(currentHexValue())
      .subscribeOn(backgroundScheduler)
      .subscribe { colorResponse ->
          colorNameLiveData.postValue(colorResponse.name.value)
      }
}

Key points

  • You can use RxPreferences to create reactive streams out of individual preferences.
  • RxPreferences provides type safe ways to access data stored in shared preferences.
  • Make sure to keep a strong reference to the RxSharedPreferences class to avoid listeners being garbage collected prematurely!
  • If you want to store and retrieve custom objects, use the Converter interface to convert between strings and your object type.
  • You can use the rxjava-bridge library to bridge between RxJava2 and RxJava3 types

Where to go from here?

Now that you know all about making SharedPreferences reactive, you can move even farther Rx-ifying your apps! Hopefully you’re starting to notice that for every core component needed to write an Android app, an existing Rx-ified library exists to keep your code base reactive.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now