Prior to authenticating I take the opportunity to display my “fancy” “brand” imaging.
The login happens on an Auth0 web page hosted in an in-app browser - it’s a bit jarring transitioning into this from
the otherwise dark mode-ish app, but whatever.
The main page gives a quick run-down of what every thermostat is up to,
including a description of what setpoints they’re chasing and what actions they’re taking.
Unlike the webapp, settings for each thermostat can be changed by clicking on any thermostat from the home page.
This felt like a more touch-friendly UX than porting the same concepts (and code, perhaps) from the web version.
Touching any one setting opens up a dedicated screen for editing a given setting.
At the bottom of the screen for editing a setting is a Remove and Save button.
When the setting is unchanged, the Remove button is enabled and when clicked will remove the setting without any further confirmation.
Once the setting has been changed, the Save button is enabled and will similarly save the setting without any further confirmation;
the Remove button becomes disabled when the setting is changed to avoid letting a user accidentally delete a setting when they meant to save it.
Navigating back before saving just discards the edits.
Tapping the person-shaped icon at the top right of the home screen gets to the Account screen,
effectively a mix of account information, account preferences, and various debugging information.
How it’s built
The web app is built using React and uses react-native-paper as its foundational UX element library.
The data and state management is the same as for the web app and the level of code sharing is basically just glorious.
I use react-navigation for managing in-app navigation.
It’s … a bit of a trip.
It all makes good sense until you want to configure the navigation options per screen and then things go a bit weird.
After much back-and-forth, the best approach seems to be to articulate a screen as implementing NavigationStackScreenComponent<>
rather than React.FunctionComponent<>, and then providing a static navigationOptions function like so:
This means you can’t access state or computed values of a screen in its header
and my brain melted a bit thinking about how implementing something like a Save button in the screen header
could be implemented. Ah well.
A nicer model might be to have the rendered elements include an element that defines non-static navigation options,
but that may just violate the top-down flow paradigm in React.
I don’t know what the right thing to do here is but it feels like dogma is taking precedence over practicality a little.
Also don’t get me started on passing parameters to screens (note also that parameters and options are different things).
I can type this well enough by passing my desired parameters as a generic parameter to NavigationStackScreenComponent:
…but why must they show up in navigation.state.params (awkward) and then also as optional (wat)?
type things and it some ways I appreciate that the optional params acknowledge that reality,
but it’s also just irritating.
Uploading to the Google Play store
Note: since our family is entirely Android-based, I only bothered standing up the Android version of this React Native app.
Given how extra it is to get any one platform and store working, I’m not particularly interested in making this work on iOS
just for the fun of it.
Boy howdy is it hard to distribute an app to four people (the current target audience for WarmAndFuzzy).
Automating that is even more … fun.
Step 1: modify android/app/build.gradle to load keystore properties from a local file:
I especially appreciate that eslint makes sure there are no unused styles declared - insta-gardening!
React Native is a pretty magical piece of kit. The ~1 second latency from my hitting CTRL-S to the emulator running updated code
is pretty magical. +++ would buy again.
That said, it does a lot at runtime that anyone who’s used to compiled languages would have expected to be a compile-time activity.
This means that major oopsies can go entirely unnoticed.
The most irritating episode was when I lit up flatbuffers support in the shared-client library.
does some weird module loading goop that I can’t quite comprehend at the end:
// Exports for Node.js and RequireJSthis.flatbuffers = flatbuffers;
the approach taken above worked fine in my shared-client package and in my webapp package, but not in mobile.
Hilariously (not), the mobile package built fine and deployed fine all the way through Google Play,
except opening the app just got me this:
And of course I can’t be bothered to have any actual UI tests (preferring to invest my time in compile-time type safety instead),
so I managed to break an actual deploy to the Play Store without noticing that the app was completely dead in the water.
It still boggles the mind that I can run an entire app through build, CI/CD, and deploy, and it’s fucking dead in the water.
That’s really something.
For what it’s worth,
I ended up “rewriting” Flatbuffers in TypeScript
and fixing up assorted TypeScript problems. I don’t quite understand why Google doesn’t do this but the project may just be too dead at this time.
And while I’m complaining,
the Android toolchain is a pain in the ass to babysit (let alone update);
making an Android emulator work on modern Windows with Hyper-V is super-extra and requires clowning around with Visual Studio (come on, Google, get it together);
and any environment changes (e.g. going from npm run start-mobile:local:dev to npm run start-mobile:remote:prod)
require a deep clean (lerna run clean-mobile, i.e. ./gradlew clean) for whatever reasons.
For all the effort it takes to make gradle do its thing (and boy howdy is that thing extra), it feels like it should get that sort of dependency tracking right.
It does not seem to.
With all those complaints, the mobile app is probably my least favorite part of the project to work on;
on the other hand, it’s pretty awesome to get to write a bit of code and then hold it in my hand five minutes later.
Painful but worth it.