Kotlin is all about letting us do more work with less code, and SharedPreferences are no exception. In this post we’re going to focus on making SharedPreferences as easy to use as possible, and by the end we’ll be able to use and modify our preferences from anywhere inside the app!
Since Kotlin is still a fairly new language, rather than jumping right to the conclusion, we’re going to start with a simple app where we can change the background color. Then we’ll add a preference for the background color, and start exploring the ways that Kotlin can help us write less code.
And here’s the app! It’s just a list of all the color constants from the Color class (e.g. Color.GREEN), and when a color is selected the background will update to that color.
Here’s the code to create the app:
(Don’t worry if the layout part doesn’t totally make sense; it’s in Anko and there will be a course on that shortly. ;))
class MainActivity : AppCompatActivity() {
val colors = mapOf("Black" to Color.BLACK, "Blue" to Color.BLUE, "Cyan" to Color.CYAN,
"Dark Gray" to Color.DKGRAY, "Gray" to Color.GRAY, "Green" to Color.GREEN,
"Light Gray" to Color.LTGRAY, "Magenta" to Color.MAGENTA, "Red" to Color.RED,
"Transparent" to Color.TRANSPARENT, "White" to Color.WHITE, "Yellow" to Color.YELLOW)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bgColor = Color.BLACK
// using Anko for the layout
verticalLayout {
backgroundColor = bgColor
radioGroup {
colors.forEach {
radioButton {
val colorText = it.key
val colorInt = it.value
text = colorText
textColor = Color.WHITE
buttonTintList = ColorStateList.valueOf(Color.WHITE) // API 21+
onClick { selectColor(this@verticalLayout, colorInt) }
}
}
}
}
}
private fun selectColor(view: View, color: Int) {
view.backgroundColor = color
}
}
At this point, we’ve got a very familiar problem. Since we aren’t persisting anything about the background color, if we rotate the app, the color will always go back to black. So let’s introduce a SharedPreferences object to help us remember the background color.
To create a SharedPreferences object we’re going to use the ‘getSharedPreferences’ method from the Context class. It takes in a String for the name of the preferences file and an int which in almost every case is Context.MODE_PRIVATE (aka 0).
...
val PREFS_FILENAME = "com.teamtreehouse.colorsarefun.prefs"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefs = this.getSharedPreferences(PREFS_FILENAME, 0)
...
Then we need to come up with a name for our color preference, ‘background_color’. And next, we’ll need to get a ‘SharedPreferences.Editor’ from our SharedPreferences object and use that Editor to store and retrieve our color.
Unfortunately, this means we’ll need to pull our ‘prefs’ object out of ‘onCreate’ and scope it to the Activity. And since Activities aren’t initialized with a Context, we’ll need to set our ‘prefs’ object equal to ‘null’, and then update it to be a SharedPreferences object in the ‘onCreate’ method.
...
val PREFS_FILENAME = "com.teamtreehouse.colorsarefun.prefs"
val BACKGROUND_COLOR = "background_color"
var prefs: SharedPreferences? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = this.getSharedPreferences(PREFS_FILENAME, 0)
val bgColor = prefs!!.getInt(BACKGROUND_COLOR, Color.BLACK)
verticalLayout {...}
}
private fun selectColor(view: View, color: Int) {
view.backgroundColor = color
val editor = prefs!!.edit()
editor.putInt(BACKGROUND_COLOR, color)
editor.apply()
}
}
Now when we rotate the app, the background color will be saved and reused. At this point our SharedPreferences are starting to take up a pretty sizable portion of our Activity. And for something as simple as saving a user’s favorite color, it’s sure adding a lot of visual noise to our code.
To fix this, let’s create a new ‘Prefs’ class to store all of our SharedPreferences. And let’s make it take a Context in its constructor; this way we’ll have a Context right at initialization and won’t have to deal with setting anything to ‘null’ in our new class.
Inside our Prefs class, we’re going to copy in our PREFS_FILENAME and BACKGROUND_COLOR constants, and then create a new SharedPreferences property.
Then we’ll create a new ‘Int’ property named ‘bgColor’, and we’ll set the ‘get’ and ‘set’ methods of this property to get and set our background color preference.
class Prefs (context: Context) {
val PREFS_FILENAME = "com.teamtreehouse.colorsarefun.prefs"
val BACKGROUND_COLOR = "background_color"
val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, 0);
var bgColor: Int
get() = prefs.getInt(BACKGROUND_COLOR, Color.BLACK)
set(value) = prefs.edit().putInt(BACKGROUND_COLOR, value).apply()
}
And now, using and saving the user’s selected background color is as easy as creating an instance of the Prefs class and then just using ‘prefs!!.bgColor’ like it’s a regular variable.
class MainActivity : AppCompatActivity() {
val colors = ...
var prefs: Prefs? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = Prefs(this)
val bgColor = prefs!!.bgColor
verticalLayout {...}
}
private fun selectColor(view: View, color: Int) {
view.backgroundColor = color
prefs!!.bgColor = color
}
}
Pretty cool, right? Definitely, but there’s still more work to be done! What would happen if this was a much larger app with several different Activities? We’d end up needing to create a new Prefs object for each Activity, and while there’s nothing wrong with that, there is a better way.
Instead of creating the Prefs object in the Activity we can create it in the Application. If you’re not familiar with the Application class here’s the three things you need to know about it.
- It’s the base class for maintaining global application state.
- It has an ‘onCreate’ method which is called before any Activities or Services have been created.
- We have access to a Context inside the ‘onCreate’ method.
Now that we know all that, let’s create a new class named, “App”, which extends Application. Then let’s create a companion object, and inside let’s add the ‘prefs’ declaration from our Activity. This way we can access our ‘prefs’ object by just typing, ‘App!!.prefs’, from anywhere within our code.
Next, we need to populate our ‘prefs’ object in the Application’s ‘onCreate’ method, which we can do by using the ‘applicationContext’ property. And now, our Activity is down to just two lines of preference code: one line for getting the background color, and one line for setting it.
class App : Application() { companion object { var prefs: Prefs? = null } override fun onCreate() { prefs = Prefs(applicationContext) super.onCreate() } }
class MainActivity : AppCompatActivity() { val colors = ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val bgColor = App.prefs!!.bgColor verticalLayout {...} } private fun selectColor(view: View, color: Int) { view.backgroundColor = color App.prefs!!.bgColor = color } }
But it’s still a little distracting. Wouldn’t it be nice if it was just ‘prefs!!.bgColor’? Or even better just ‘prefs.bgColor’? Well, getting it to ‘prefs!!.bgColor’ is pretty simple; we could just move our ‘prefs’ declaration out of the companion object and into package scope.
And actually, getting to ‘prefs.bgColor’ is pretty simple as well. But first you’ll need to know about delegated properties in Kotlin. Specifically, you need to know about the ‘lazy’ function.
In Kotlin, we’re allowed to define a property like this: val x: Int by lazy { 42 }. And what that means, is that the value of ‘x’ isn’t set until the first time we use it. So the first time we use the ‘x’ variable it’s going to run that function ({ 42 }), and then store that result in ‘x’. Then, any other time we use the ‘x’ variable, it’s just going to use the value stored in ‘x’ (42).
What this means for us, is that we can provide a non-null ‘prefs’ object to our entire app just by doing the following:
val prefs: Prefs by lazy { App.prefs!! } class App : Application() { companion object { var prefs: Prefs? = null } override fun onCreate() { prefs = Prefs(applicationContext) super.onCreate() } }
So instead of initializing our ‘prefs’ object right away, we won’t initialize it until it gets used for the first time. At that point, we’re going to set it equal to the ‘prefs’ object in our App which we assert is not null. And since we’re populating our App’s ‘prefs’ object as the very first thing in our Application’s ‘onCreate’ method, we can feel safe about asserting that it isn’t null.
This way, all we need to do to add a new preference to our app, is to create that preference in our Prefs class. Once we’ve done that, we can access it from anywhere within the app just by using our package-level ‘prefs’ property!
class MainActivity : AppCompatActivity() { val colors = ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val bgColor = prefs.bgColor verticalLayout { backgroundColor = bgColor radioGroup {...} } private fun selectColor(view: View, color: Int) { view.backgroundColor = color prefs.bgColor = color } }
I hope you liked this post! And if you’re looking to learn more about Kotlin, then I encourage you to check out our Kotlin course. It covers everything you need to know about Kotlin, and it does so by building a Solitaire app. It’s super exciting, and super efficient (< 3 hours). Check it out!
Thank you! Quick question, if I want to save a custom ArrayList, not an Int, how would I do that?
you have a static reference to the context here. therefore the class never gets garbage collected?
Great work, Excellent post!, very helpful
Use non-null `lateinit var´ and save yourself countless `!!`.