Thursday, July 8, 2021

Refactoring the Thunderstone Quest Randomizer: Generating dart code with source_gen and build_runner

This post is part of a series about the implementation of my Thunderstone Quest randomizer. For more background, see my original post from Summer 2020 or my earlier update in Summer 2021

I remember when I first learned to use SharedPreferences to implement user settings. It was exciting to see the feature work in the Web build. I remember thinking that the implementation was a little messy, but that I could clean it up as I go. Predictably, in the excitement of adding new features, I did a lot of copy-paste coding, which led to bloat in the SettingsModel class. Some days ago, I decided to dive back into that implementation and clean it up.

If you look at the previous implementation of SettingsModel, you will see that the logic for managing user preferences is spread throughout the file. Each preference needed to add a key constant, a declaration, an accessor and a mutator, and clauses to the _loadPrefs, clear, and _updatePrefs methods. Adding the second and third ones were easy, but the more I added, the more cumbersome it became. SettingsModel also serves as a facade, hiding the fact that some preferences are handled quite differently than others.

My initial attempts at refactoring this had me spinning my wheels. Clearly, I could extract the behavior of a preference into its own class; this would just be a matter of traditional OOP. It would make sense for each of these to be a ChangeNotifier. (Check the docs for more about this important Dart class.) The SettingsModel itself could still be a ChangeNotifier that simply echoes any notification from the preference objects it contains. To set this up, I needed a way to iterate through all of the preferences. A list of preferences would do the trick, but I also needed independent fields for each preference. To reduce duplication, then, I wanted an automated way to loop through all of the Preference fields in the SettingsModel.

I assumed there would be a way to do this with runtime reflection, but I had no luck with this. I found a few references to dart:mirrors, but this does not seem to be supported in Flutter. Every time I tried to get reflection going via reflectable, I ran into a rat's nest of dependencies that I didn't understand—notably build_runner. It seems to me that, for the high quality of Flutter documentation, there was almost nothing about solving the problem I was facing.

I found a comment on a defect discussion that piqued my interest: it was a claim that anything you would want to do with reflection would instead be more idiomatically done with code generation. After several failed attempts with reflection, I started trying to better understand how to use source_gen for code generation. This video by Creative Bracket helped clarify how the pieces fit together, especially the relationship with the aforementioned build_runner.

This helped me to build a prototype that, at build time, scanned SettingsModel and added to it a list of all the preferences declared within. This was the move that gave momentum to this approach. I was able to enhance this source_gen-based approach first by generating accessors and mutators for each preference based on its declaration, and then to generalize these along with a preference type variable. In the end, this approach required no changes to the client code, which was an added bonus. 

I extracted the preferences API into its own library. It contains an abstract Preference<T> class with several implementors, including common types such as BoolPreference and IntPreference as well as special-case cases such as BrightnessPreference. Each new preference type is very simple, only having to declare the methods that are used for reading and writing to the SharedSettings object.

I have debated whether SettingsModel should be a facade or whether it should return references to the preference objects, which would allow parts of the UI to consume only those preferences that it cares about. This would lead to an awkward proliferation of producers rather than the convenient wrapping currently in SettingsModel.

The implementation is up on GitHub and licensed under GPL v3.0, so feel free to take a look if you're interested in more details.

No comments:

Post a Comment