-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement and use internal API for reading and writing preferences #12161
base: main
Are you sure you want to change the base?
Conversation
I planned to do something to solve the same problem, but wanted more testing infrastructure/key-safety first, although not part of my GSoC project (which is basically finished). I was going for using simple POJOs though, like wikipedia app does + adapt the current settings that already use enums (e.g. themes) + add enums for whatever needs an Enum but doesn't have one. I'm not fixed on any approach, so I'm +1 for the job as long as this is well tested. Left some considerations about your points below
The benefit is being able to use the same key used on the XMLs (Preferences, Deck Options and Filtered deck options) and avoid problems caused by typos
var language by stringSetting(R.string.pref_language_key, "")
.apply {
whenChanged { functionThatSetsLanguage(value) }
} For the screens that use preferences (the UI things), I believe that more simple to use the default
try {
settings.validate(key, newValue /* Any? is accepted */)
} except (e: Exception) {
// show a toast with error
} For the Preference preferences, probably only useful for EditTextPreference and inheritors, but I don't remember any case where it's actually useful to extract the validation. For the non-Preference preferences, just validate on the setter |
I am myself a bit conflicted, on one hand it feels that this is a bit too much sugar, on the other the API for the enums especially are rather neat if I do say so myself.
I understand, I mean, I am not sure if this will actually prevent any mistakes. Never had a problem with this. It seems that even if you do make a mistake, it will be found right away. Besides, if you change stuff, you still have to check that you use the correct default value, or if you have a choice preference, you have to check that the stored values in code are the same as the ones in XML. (I wonder if it's somehow possible to say
I simply do not think that this stuff semantically belongs to UI. A preference may change without any UI involvement, e.g. you can make text size change by pressing volume buttons, or perhaps you can decided to duplicate a setting by using a context menu somewhere.
The point here is to remove all validation from UI code entirely; it doesn't semantically belongs there either: validation does most of what reading a preference and making an useful object out of it does. Preferences can simply call |
For these cases it may be useful. But for most of the simple preferences that are changed only on their screens, I think that it's easier to just use the default API provided by the library, which will still need to be used even with these new changes, especially if the user wants to change any property of the preference (isEnabled, isVisible, summary) or affecting other preferences (UI).
Not a pattern I've seen on other applications, and I think it's the same point. May be useful for some cases, so it's better to have actual examples on AnkiDroid's codebase where this will help, and validating inside the preference code seems simpler IMO |
I would say separating the UI and business logic is a commendable endeavor on its own. ¯\_(ツ)_/¯ |
Probably why the preferences are on XMLs and why Android has a configurable PreferenceFragmentCompat and a whole API to configuring individual preferences, which is very sufficient IMO. If the validation is tied/extends the default androidx.Preference API instead of adding another layer of complexity, I think it may be good. |
Updated to sliiigthly change a method. To clarify some things as discussed on Discord,
|
If the changes are on the UI, the block will need to be on the UI, like the example you give of changing language with whenChanged{}, which needs to call requireActivity().recreate()
In the example you give, someone may need to set a change listener later to change other things besides validating it. I am away from my PC now, but IIRC overriding callChangeListener may do the job here. I understand the motivation of trying to divorce the androidx Prefererences and the rest. As dealing with the XMLs, data validation and storage in just one place difficults one's life (testing, code duplication, etc). But probably only death may do them part. Maybe extending the Preferences or creating a whole set of settings views instead of using Android's library may be ideally the best thing, as some apps do out there. Do we have a problem that needs to go such lengths? I don't think so. Especially because only custom sync server preferences may benefit with the divorce, as I've discussed on Discord (while we have more than 100 other preferences that may get more complex without needing it) random thought: maybe create a HttpUrlPreference to avoid repetition on custom sync url and media sync url I still like the getters and setters for accessing SharedPreferences on other places. With tests, of course. If whenChanged has a global scope, probably will be difficult to test and lead to bugs, as it may be changed on different places of the code. The validation may be inside the setter to simplify things (i.e. have one less method on the interface) or outside if we have repeated uses of it |
Hello 👋, this PR has been opened for more than 2 months with no activity on it. If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing! You have 7 days until this gets closed automatically |
Oh, I forgot to reply! Sorry
It would be more proper to perform UI changes when a setting change, rather than when user taps on a thing in preferences. In case of app language, it would be more proper to call
If you want to, you can always exclude some preferences from auto-opting into validation. I'm not if there's going to be many cases where you'd want that
I'm not sure I see the need. Preference UI and the storage is sufficiently separated I think, and any settings interface you may come up with is going to be built on top of the two, not instead of.
Actual settings ( The stuff in |
Forgot about this too. Overall, I'm fine with the proposed API, but not sure if we would actually use it enough to compensate the churn. For the getters and setters I can see the benefit immediately, but for the other methods I'd like some real examples on the current codebase that have a actual benefit on maintainability or testability so we aren't just spending time writing things on a different way |
YAGNI is a fine principle, but I prefer to slightly overengineer in cases like these. Validation of raw values as the idea seems to fit the design, as well as change listeners of sorts; while these require some additional code, it's rather straightforward. Of course, this is no more than a proof of concept. It remains to be seen what should be added or removed for this to work with all of the existing preferences. There's quite a few of them eh |
Hello 👋, this PR has been opened for more than 2 months with no activity on it. If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing! You have 7 days until this gets closed automatically |
At this point this is not a real PR but more like a RFC or something like that.
Currently, preferences are retrieved using
SharedPreference
APIs directly, e.g.There's a few problems with this:
HttpUrl
s for HTTP URLs;answerButtonPosition
is not a string;I propose to introduce a new internal API for retrieving and storing preferences, with which the above example will look like this:
The single runnable commit here is a proof-of-concept example of how this could look like. Only a few settings are fetched via the API.
I named things “settings” simply to distinguish them from preferences. As in, preferences are the UI things, settings are internal things.
I used context receivers as IMO these make the code a bit cleaner, but I am not sure if everyone will agree on this. As someone said on Reddit, Kotlin is going to develop diabetes with all the syntactic sugar it is building up. 😅
Most of the setting keys are already conveniently in
R.string.
already, so we can use those; hopefully, this will be the only place where the string resource is used in code. E.g.Alternatively, it's possible to de-extract strings and revert to hardcoding them. As long as the string are not scattered all over the code, I am not sure if keeping strings in resources is of any substantial benefit. Especially if you consider that the default values are also hardcoded, as are the values for choice preferences.
Most of the choice pereferences can be represented with enums. In XML, choice values (
"top"
,"bottom"
) are often instring-array
s. We can hardcode these values, e.g.Or we can extract strings and use those, for instance:
Writing settings is straightforward:
However, this will create a new editor under the hood. It looks like it isn't possible to mandate the above statement to have an editor context, as in
settings.edit { centerCardContentsVertically = false }
, without also mandating it for retrieval. I wonder if this is an issue.SharedPreferences.Editor
is useful especially if you want to have atomic changes; I'm not sure if there are places in the app where this matters.The settings are retrieved on the first read as well as on change. It is also possible to read all settings in advance, perhaps in a worker thread; also, it's possible to only invalidate settings on change instead of re-reading them.
Also, as long as the the settings are retrieved in sync, I guess it's possible to swap the underlying setting storage layer for something else, while keeping the API (phrases like
settings.answerButtonPosition
), so that's neat.I suggest using the
Settings
class to also attach all the change listeners, e.g.The API can also validate preferences, which can be used by UI preferences to prevent people from entering bad values, e.g. invalid URLs:
This is currently done in the preferences code; I think this belongs here.
I did not look too far into this, so there's probably a few edge cases that I'm missing.