This started as a means to create a Flutter app without the baggage of an ugly outdated codebase; it's since evolved into an experiment of how feature-rich I can make an app about spinners, without AI intervention and given a reasonable time-frame.
Try it out on the web - dunno.liamdoka.dev - thanks to Flutter's inherent cross-compilation. You may have to change the aspect ratio of your browser. I didn't create it with the intention of it being web-based. Just a happy little accident.
Here's what it's supposed to look like:
Spinner Screen | Home Screen | Stats Screen |
---|---|---|
![]() | ![]() | ![]() |
Pay close attention to the variety of color palettes, and curated statistics that are provided for peak user-enjoyment. This was made possible due to Material Colors, a once-hated design and color system. For the most part, that has not changed.
Material Design
I've never really gone all-in on material design, so after using it exclusively on this project, I've come away with some thoughts.
TLDR: I'll use the Material Theme to save me time when prototyping. If I'm not doing that, I'll invest the time into creating a more robust solution.
To access the material colors, you need to use a widget's context. This means that changes upstream in parent widget can be reflected in the children and children's children.
Material sets the theme colors at the top-most widget with the ThemeData()
class.
To ensure that the latest configuration of the context is used when referencing the colors,
you need to use the prefix
1Theme.of(context).colorScheme.yourColorHere;
I'm sure you can imagine how this will get cumbersome over time. What you can do is write a fun little extension in Dart:
1extension ThemeOfContext on BuildContext { 2 ColorScheme get colors => Theme.of(this).colorScheme; 3} 4 5// Which can then be referenced by... 6final color = context.colors.primaryContainer;
The gist of it is that colors are named after what they're used for, as opposed to what they are. This meant that if the color that it provided for me wasn't perfect for my use-case, it took quite a bit of searching through the other options before finding something that worked.
This works perfectly if you want a really variable theme, with different accent colors, light mode, and dark mode.
It falls short if you care about const
declarations and having a color-scheme that isn't overtly google-flavoured.
Code Generation
The tech stack I used leaned heavily on the code generation pattern.
TLDR: Beware of magic, read the docs,
$ dart run build_runner watch -d
Package | Why |
---|---|
AutoRoute | Used to generate "named" routes, allows for flexible routing, navigation, and deep links. |
Riverpod | A fantastic state-management and data caching library. Basically every state ever is global but lazily-loaded by leveraging the observable pattern. |
Freezed | Creates immutable classes with built-in helper functions, enforces functional programming, and ties in nicely with Riverpod to create data models. |
Hive | Local persistent data storage. Hive is more flexible than SharedPreferences and provides code-generation to adapt any class to a storable format. |
There's an obvious trade-off here; in exchange for more robust, complete, and battle-tested code,
I'll have to run the build_runner
every time I make a change to a file leveraging these packages.
Fortunately it's easy enough to have a terminal running in the background with $ dart run build_runner watch
,
but it does tangibly increase the latency of the editor when making these structural changes.
Another thing to consider is how obscuring the "inner workings" of these classes and functions, you're making the whole ordeal more "magical" and more difficult to understand for the uninitiated.
Another thing I'd like to mention is that Riverpod's code generator automatically adds the word Provider
to the end of your classes... which means you have to be very deliberate about the naming conventions that you are using,
let me dump some (simplified & hopefully self-explanatory) code.
1 2sealed class SpinnerModel extends _$SpinnerModel { 3 const factory SpinnerModel({ 4 ("") String title, 5 ([]) List<String> segments, 6 // ... 7 }) = _SpinnerModel; 8} 9 10 11 12List<SpinnerModel> spinnerList(Ref ref) => [SpinnerModel(), SpinnerModel()]; 13 14 15class SpinnerListScreen extends ConsumerStatefulWidget { 16 const SpinnerListScreen({super.key}); 17 18 19 ConsumerState<SpinnerListScreen> createState() => _SpinnerListScreenState(); 20} 21 22 23class _SpinnerListScreenState extends ConsumerState<SpinnerListScreen> { 24 25 Widget build(BuildContext context) { 26 final spinners = ref.watch(spinnerListProvider); 27 28 return Center( 29 child: Text("Number of spinners: ${spinners.length}") 30 ); 31 } 32}
Now make no mistake, there's a lot going on... but here are the rules I made up.
- Objects are called
models
- An instance of a model is called a
state
- Not to be confused with widget state.
- Providers are just called what they provide.
- The
spinnerList
is going to be provided by thespinnerListProvider
- Avoid using
state
orprovider
orcontroller
ormodel
and other such keywords in the provider definition
- The
- If the UI fills the screen, it's called a screen.
- The router pushes a screen to the navigation stack.
So as you can imagine, it's very possible that I may turn around and want a Widget
called SpinnerList
.
This is not allowed, as it would overlap with the spinnerList
provider definition and become increasingly confusing
as the project went on. Instead, I'd call it the SpinnerListWidget
or a SpinnerListScreen
or a SpinnerListSection
or something of the sort.
Future Considerations
These are some things that I didn't get around to / couldn't be bothered implementing. Not necessarily because it's hard or complicated, but because it would be very time-consuming. Honestly a very likely candidate for an AI Agent to take over and implement the following things
Thing | Why |
---|---|
Firebase | From analytics to crashlytics to firestore to automatic deployment, this honestly just makes a lot of sense for the project. Haven't done a tonne of integrations like this so would be a good exercise in finding out the limitations. |
Track wins per Spinner segment | Have you ever wondered how unfair the "Spin" algorithm is, well now you can with the all new analytics feature where each spinner tracks how many wins each segment has achieved in its lifetime. |
Share Spinners and Color Palettes | If i hypothetically added firebase, I could get users to log in with google and share a bunch of information between each other! |
Ads | God I wish i could somehow make a million dollars for no good reason |
Show Banner with winning segment title | The spinner finishing its slowdown feels a little anticlimactic, even with the confetti. I think this could be coupled with a dialog box that shows the name of the winner and maybe some stats about the spinner |
Finish Color Palette Implementation | Yeah I didn't really finish the "Create Your Own Color Palette" screen, I probably should have but just hit a wall and started writing up this website post... |
Onboarding and default spinners | Maybe a cute little onboarding flow and some prebuilt spinners to ease new users into the app. |
Evil spinners that always land on the same segment | Could be funny, don't tell the user. |
Okay that's all for my scope-creep simulator, toodles.