3

I'm learning Compose by the article.

A stateless composable is a composable that doesn't hold any state. An easy way to achieve stateless is by using state hoisting, so I replace Code B with Code A, it's great!

The article tell me:

By hoisting the state out of HelloContent, it's easier to reason about the composable, reuse it in different situations, and test. HelloContent is decoupled from how its state is stored. Decoupling means that if you modify or replace HelloScreen, you don't have to change how HelloContent is implemented.

So I write Code C, it stores the value of name in a SharedPreferences, I think that Code C is just like Code A, but in fact, I can't input any letter with Code C, what wrong with the Code C ?

Code A

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }
    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

Code B

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       var name by remember { mutableStateOf("") }
       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

Code C

@Composable
fun HelloScreen() {
    var name: String by PreferenceTool( LocalContext.current ,"zipCode", "World")
    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

class PreferenceTool<T>(
    private val context: Context,
    private val name: String,
    private val default: T
) {

    private val prefs: SharedPreferences by lazy {
        PreferenceManager.getDefaultSharedPreferences(context)
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(name, value)
    }

    @Suppress("UNCHECKED_CAST")
    private fun findPreference(name: String, default: T): T = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default) ?: default
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }
        res as T
    }

    @SuppressLint("CommitPrefEdits")
    private fun putPreference(name: String, value: T) = with(prefs.edit()) {
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type can't be saved into Preferences")
        }.apply()
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
HelloCW
  • 843
  • 22
  • 125
  • 310

3 Answers3

12

Om is completely right about the reasons why your code doesn't work, and his answer will work.

To understand why you need a MutableState in compose I suggest you start with documentation, including this youtube video which explains the basic principles.

But PreferenceManager is deprecated and now you can use DataStore instead.

With compose in can be used like this:

@Composable
fun <T> rememberPreference(
    key: Preferences.Key<T>,
    defaultValue: T,
): MutableState<T> {
    val coroutineScope = rememberCoroutineScope()
    val context = LocalContext.current
    val state = remember {
        context.dataStore.data
            .map {
                it[key] ?: defaultValue
            }
    }.collectAsState(initial = defaultValue)

    return remember {
        object : MutableState<T> {
            override var value: T
                get() = state.value
                set(value) {
                    coroutineScope.launch {
                        context.dataStore.edit {
                            it[key] = value
                        }
                    }
                }

            override fun component1() = value
            override fun component2(): (T) -> Unit = { value = it }
        }
    }
}

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")

Usage:

var name by rememberPreference(stringPreferencesKey("zipCode"), "World")
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
4

One key piece is missing in Code C, which is MutableState.

In Compose the only way to modify content UI is by mutation of its corresponding state.


Your code doesn't have a mutable state object backing up PreferenceTool. So use of setValue by property delegation only modifies the SharedPreference (by calling putPreference(name, value)) but change is not propagated to UI.

operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    putPreference(name, value)
}

In order to correct the behavior, add a MutableState object within PreferenceTool.

This way the updates are detected by Compose and UI is updated accordingly.

class PreferenceTool<T>(
    private val context: Context,
    private val name: String,
    private val default: T
) {
    private val prefs: SharedPreferences by lazy {
        PreferenceManager.getDefaultSharedPreferences(context)
    }

    private val state = mutableStateOf(findPreference(name, default))

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        state.value = value
        putPreference(name, value)
    }

    @Suppress("UNCHECKED_CAST")
    private fun findPreference(name: String, default: T): T = { ... }

    @SuppressLint("CommitPrefEdits")
    private fun putPreference(name: String, value: T) = { ... }
}
Om Kumar
  • 1,404
  • 13
  • 19
0

Let's Suppose you inserted integer value 16 corresponding to key "FONT_SIZE".

const val FONT_SIZE = "FONT_SIZE"

Step 1: Define MutableState for value and initialize it with SharedPreferences value

val context = LocalContext.current
    val sharedPreferences = remember {
        context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE)
    }
val valueState = remember { mutableStateOf(sharedPreferences.getInt(FONT_SIZE, 16)) }

Step 2: Register a listener for SharedPreferences changes

DisposableEffect(Unit) {
    val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
        if (key == FONT_SIZE) {
            valueState.value = sharedPreferences.getInt(FONT_SIZE, 16)
        }
    }
    sharedPreferences.registerOnSharedPreferenceChangeListener(listener)

    onDispose {
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
    }
}

Now Any composable using the valueState.value will be recomposed on its value change