0

Is SharedPreferences a safe way of sharing settings between a service and the UI?

I am calling commit() in a service and when I read the prefs in the UI 300ms later, the data is not there. This is on API 27.

I already use the Messenger for communicating between the service and the UI.

This is how I'm storing data on the services side:

  local e = context:getSharedPreferences("characters", context.MODE_PRIVATE):edit()
  e:putString("list", table.concat(names, "|"))
  log("saved these names in characters: %s", tstring(names))
  e:commit()
  local stuff = context:getSharedPreferences("characters", context.MODE_PRIVATE)

Retrieving it in the UI:

  charlist_settings = activity:getSharedPreferences("characters", activity.MODE_PRIVATE)
  log("refreshlist hit")
  if charlist_settings:contains("list") and charlist_settings:getString("list", "") ~= "" then
    [...]
  else
    log("no list in char_settings :(")
    log("all: %s", charlist_settings:getAll():toString())
  end

Service log, notice timestamp:

12-22 18:56:30.246 4142-4142/org.vadisystems.irehelper:Service I/IRE Helper: modified is: added
12-22 18:56:30.253 4142-4142/org.vadisystems.irehelper:Service I/IRE Helper: saved these names in characters: {"Vadimuses_Achaea"}

UI log, this is after a message is received from the service:

12-22 18:56:30.700 4120-4120/org.vadisystems.irehelper I/IRE Helper: refreshlist hit
12-22 18:56:30.701 4120-4120/org.vadisystems.irehelper I/IRE Helper: no list in char_settings :(
12-22 18:56:30.702 4120-4120/org.vadisystems.irehelper I/IRE Helper: all: {}
Vadim Peretokin
  • 2,221
  • 3
  • 29
  • 40
  • 3
    Use Intents or Local broadcasts for that! – Xenolion Dec 22 '17 at 16:43
  • How are you getting the `SharedPreferences` object in your service and in your UI? Reading the docs for `Context.getSharedPreferences()` it seems like what you're doing _ought_ to work, but perhaps I'm making an assumption I shouldn't. – Ben P. Dec 22 '17 at 17:47
  • That's right, I am. Added in the code. – Vadim Peretokin Dec 22 '17 at 17:59
  • I don't think sharedpreferences is good way the best way to communicate betweeb activity and service is BroadcastReceivers https://stackoverflow.com/a/3599696/5492047 highly recomended. – Reyansh Mishra Dec 22 '17 at 18:19
  • I already use the Messenger for communicating between the service and the UI. – Vadim Peretokin Dec 22 '17 at 18:20

3 Answers3

0

This should work, using either commit() or apply() based on the documentation. The change should be visible immediately between all in-process threads and components. However I have noted bugs in Android trying to do this between a Service and Activity in the past. Having you tried using getDefaultSharedPreferences() instead of a custom file. I have had no problems using the default file.

Steve M
  • 9,296
  • 11
  • 49
  • 98
  • Also, see https://stackoverflow.com/questions/22183755/preferenceactivity-saving-preferences-when-app-is-destroyed/22185291#22185291 . You can't use MULTI_PROCESS anymore, though. – Steve M Dec 22 '17 at 20:47
  • Recoded it to use `getDefaultSharedPreferences()` - no change. – Vadim Peretokin Dec 25 '17 at 10:36
0

Not even restarting the activity worked to get the data after it has been commited. The only thing that did was restarting the application entirely. This was on API 27.

Vadim Peretokin
  • 2,221
  • 3
  • 29
  • 40
-1

Use a local broadcast receiver, when the action is done, broadcast. On the listen side you have a class that takes care of persisting to SharedPreferences and also the UI can have a separate listener and update itself.

Sample Kotlin code

[EDIT] After suggesting using apply vs commit as a quick workaround relying on a in-memory cache layer that turned out not to be provided by SharedPreferences we are adjusting this answer to use a Local Broadcast receiver instead.

jugutier
  • 179
  • 1
  • 13
  • I already use the Messenger for communicating between the service and the UI - that's how the UI knows it can check for data. The problem is that I apply/commit the data in the service, ping the UI through Messenger, UI checks SharedPreferences - data isn't there. I _could_ pass the contents through the message, but then state tracking could get really complicated... – Vadim Peretokin Dec 22 '17 at 16:59
  • Commit vs apply wouldn't help him. It would actually make it worse, as it would take longer to write to disk. – Gabe Sechan Dec 22 '17 at 17:06
  • Hi @GabeSechan according to the docs apply would write to memory and then write to disk asynchronously. Have you looked at the excerpt above? – jugutier Dec 22 '17 at 17:48
  • @Vadi based on what you mention I'd recommend testing by adding a thread sleep when you retrieve the data and play around with different times to see if the problem is that the transaction is not completed by then. Also I would test with apply, because it writes to memory the data should be available immediately in the UI side. Let me know how it goes. – jugutier Dec 22 '17 at 17:51
  • @jugutier You're not wrong about the exerpt. It just wouldn't help him at all. His problem is he writes it via commit in the service, then 300ms later it isn't readable in the Activity. Using apply, which would make it even more asynchronous, won't help him get the value in time. The right answer is to use a broadcast, event bus, or other means of passing the data (using the file system to pass data is a little hacky anyway). – Gabe Sechan Dec 22 '17 at 18:16
  • @GabeSechan we agree that it's not a good mechanism to pass data. What I was suggesting is using the intermediate memory layer (described in the excerpt) that apply() provides as a communication layer. With that, the data would be accessible instantly and SharedPreferences takes care of persisting it later. – jugutier Dec 22 '17 at 18:22
  • @jugutier You misunderstand what it means. There is no global caching layer in there. All that means is that apply will write to disk later. Not that a separate instance of SharedPreference will necessarily get that same data before its written to disk – Gabe Sechan Dec 22 '17 at 18:23
  • Do you have any references that say there isn't an in-memory representation of SharedPreferences throughout the runtime of the app? All I can find on the internet seems to back that up. Maybe @Vadi has already tried the change? if not I'll set up a test project. but then again broadcast is the way to go. – jugutier Dec 22 '17 at 18:31
  • Do you have any reference that there is? I don't read that in there at all- its just saying it saves the parameters and calls commit on a new thread. I can assure you that there is not a global shared preference object, because each one is associated with a file and you can/should have multiple preference files for different parts of your app. – Gabe Sechan Dec 22 '17 at 18:39
  • I'm already using the messenger to notify the UI of the state change. I was just hoping to read it off the preferences instead of having to pass state information around. The UI should also be able to know the state without an event telling it. – Vadim Peretokin Dec 22 '17 at 18:40
  • Also, think about it logically- if it was doing that for apply, why wouldn't it do it for commit as well? Why wouldn't it write to its own internal memory before doing the disk write. Of course it would. There's no sane implementation that would behave like that. – Gabe Sechan Dec 22 '17 at 18:40
  • @GabeSechan. Sure, my reference was provided in the excerpt above "apply() commits its changes to the in-memory SharedPreferences" here's another one from a google search: https://stackoverflow.com/questions/29634847/are-shared-preferences-stored-in-memory-during-runtime Logically, if you're doing the operation synchronously you would have it available in the next line so it would make sense to avoid an intermediate step that nothing wouldn't benefit anything. Maybe you could provide an example that solves his issue? that may be simpler than dismissing others ;) – jugutier Dec 22 '17 at 18:49
  • I tried both apply and commit together, no luck. Logs show getting the shared prefs 400ms later (in this run) the data is not there. – Vadim Peretokin Dec 22 '17 at 18:57
  • @jugutier No, it would make no sense at all to do it that way. If you have an asynchronous and synchronous method of doing the same thing, the asynchronous method always works exactly the same way but on a thread it starts. Anything else would be useless code duplication and buggy. Especially since its the asynchronous part which is slow, so avoiding the sync part it does would be minimal perf improvement. If someone coded it any other way, fire them. – Gabe Sechan Dec 22 '17 at 19:03
  • And solving the issue is: don't use files on disk as a way of transferring synchronous data. Send it via an intent, broadcast, event bus, or other method made for it. Even if commit is finished that doesn't mean the OS has flushed it all the way to disk (you know OS caches file data and doesn't write to data immediately, right?) – Gabe Sechan Dec 22 '17 at 19:04
  • Thanks for testing this @Vadi! As a next step we could try a local broadcast receiver, when the action is done, broadcast. On the listen side you have a class that takes care of persisting to SharedPreferences and also the UI can have a separate listener and update itself. This Kotlin code may help : https://stackoverflow.com/a/45473649/1904232 – jugutier Dec 22 '17 at 19:05
  • I'm already using Messenger to communicate when the data has been saved though, so the correct notification mechanism is in place. It's just getting this data out is the problem. – Vadim Peretokin Dec 22 '17 at 19:24
  • 1
    commit() and apply() have no functional difference because apply() operates on the in-memory object immediately according to the documentation. The only reason to use commit() is if you need the return value, which you generally don't. – Steve M Dec 22 '17 at 20:42