

Expo is a great delivery system for getting React Native mobile apps off the ground and into the hands of users. If I were to do it all over again, here is what I would have loved to know ahead of time!
Standalone App = A native app file built by Expo for iOS (ipa) or Android (apk) that is ready to upload to the respective app stores.
App Store = Either Apple’s App Store or Google Play for Android
Publish = Ah this does not mean publishing to the app stores. This means an over-the-air update. This term is accurate for Expo because your app becomes immediately available to use for users in the Expo App Client itself.
OTA = Over The Air, meaning a live update of your app content and functionality via the internets. Like serving a webpage.
Build = Means a process by which Expo uploads your code to its servers (publish) and then runs a build process in the Cloud that gives you a link to download the final product.
An Expo Account is *the* account that stores and serves your app. It is not a user account! Publishing your apps under this account essentially locks it to the username, and trying to switch to a different account later can cause problems. For example I identified that switching your account and republishing the same app to the same release channel will cause all the previously saved AsyncStorage user data (iOS UserDefaults) to be inaccessible.
Also if you do get into a bind of transitioning off to a new Expo account, you will end up having to annoyingly maintain apps on an old Expo login and duplicate efforts to publish your app. This is because Accounts essentially own the ecosystem which is explained later.
The solution to the above problem is to use SecureStore to persist values in the user’s phone keychain. This even persists data after the user has deleted their app. SecureStore does have character limits though and I wouldn’t trust it to deal with large amounts of object data, so it should only be important key-value pairs. AsyncStorage is still important for persisting app session state, but essentially it should be treated as if the data could be wiped tomorrow.
Do it now or feel the pain later. The hierarchy of Expo is this:
As noted above, if you ever have to switch accounts, you will end up maintaining this entire ecosystem TWICE. Your “production” release channel is different between each account!
I recommend creating new release channels for each standalone app version update too. This is because Expo often comes out with new and awesome SDK version upgrades which require a new build. The new SDK version might have code changes or features that either are not compatible or do not exist in the previous versions. A new build is also required when adding new app permissions, loading screen stuff, or tablet support.
And I won’t get into it, but you’ll see how even more complicated things get if you decide to splinter iOS and Android into separate release channels.
Expo provides an option to disable Over-the-Air updates (OTA) and provides a nice API to administer Updates yourself. This seems easy at first but can get you in trouble later.
Don’t worry about it! Are updates really that big? If you’re pushing minor updates now and then the download time is minimal. And if you’re doing massive code changes, you’re better off just re-building the app for store publishing. Plus your app is basically an Expo wrapper to begin with, so you’re not really saving that much time for the convenience.
Firstly, app store updates are also meaningless. Assuming you didn’t generally tweak the app.json, you could push out app store versions 1.1, 1.2, and 1.3 and they could all read a single “production” release channel. A single publish would push to all users regardless of version. In Expo, the app.json “version” is your arbitrary versioning scheme, only “sdkVersion” is critical. Meaning it’s marketing fluff for users, much like how Google Play uses versionName vs versionCode.
I already noted some use cases by which an app store update is required. However, the magic really comes when we talk about OTA updates that fail. If an OTA update fails, meaning Expo can’t get the newest hottest thing you published, it will automatically fallback to its “bundled” state. This means Expo will serve from cache the app at the time you built it.
When disabling OTA updates and having bundled assets, you basically mimic typical native app development where each version update is a snapshot build. With OTA updates enabled, you still want an emergency cushion if Expo can’t serve the newest stuff. Under the hood, Expo will continue to try and update the user and they should see new stuff on the next app open.
Expo and React Native have definitely solved the problem of fast development and relative ease of delivery. However long-term maintenance and deployment of live code is still an issue. There is for example no UI by which one can easily track release channels or which users are seeing them, you really have to memorize them and provide good analytics reporting. The Expo website is a pretty basic listing of your apps and recent builds.
Overall if your team is investing in React Native over native, this is the platform to go with but do maintain robust internal tracking of your releases.
When working with different SDKs of Expo, switching the existing repo will cause watchman and various caches to be out of sync. Running the various clearing commands doesn’t seem to resolve it.
The build process is best done when you run a cache cleared instance with expo start -c
and then in a separate window, run the build command you need.
In the case of sending form data in Angular, $http POST data information is serialized as “application/json” which is not interpretable by PHP.
You have to transform the request (the form data object) into a format that it understands. The easiest way is to configure the $httpProvider.
/* Because PHP sucks */ myApp.config(['$httpProvider', '$httpParamSerializerProvider', function($http, $httpParamSerializerProvider) { var paramSerializer = $httpParamSerializerProvider.$get(); $http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; $http.defaults.transformRequest = function(data) { return paramSerializer(data); }; }]);
Back in the 2000s, creative agency websites really liked the grid effect where you would hover over a block in a grid and that block would expand into some content. Of course then, people would use Flash or some complicated jQuery contraption. I created my own code example of this for a client once to demonstrate a CSS3 based design working with typical HTML element flow instead of overriding everything with position absolute and calculating block sizes on the fly. This example also serves to teach how powerful CSS inheritance can be to create series of movements that look like fancy Javascript.
It’s not a perfect example because if a user mouseovers too quickly to the next block, the widths may exceed the row and break to the next line. A solution could be to give some extra spatial buffer to the .main-content wrapper. jQuery is still used here to trigger the next child block to be “hidden.” Since the transitions are set the same, one smoothly replaces the other. At the end of the row, it triggers the previous child.
I felt this TechCrunch article “Ride Sharing Will Give Us Back Our Cities” jumped the gun for me on issues of land use equity. Planners have to be skeptical about how technology will change the landscape. Our own foray into technology, the freeway, created an unforeseen sprawl landscape and car-centric culture. Currently, the decoupling of home, work, and play is making it difficult to predict successful fixed-route transit. Ride sharing as a permanent altering of transportation habits and infrastructure? Let’s think about it.
The article is a bit confusing, because it advocates that residents give up their cars and then states those residents must adopt car-share. This quandary reflects the assumptions made:
This millennial is planning his awesome experience at the Northern Spark art party, but he secretly wishes he had things too.
The article does get it right in declaring the problem at hand:
Considering the inefficient use of the personal automobile, its exorbitant cost, the sheer volume of urban land devoted to serving that inefficient use and the material efficiencies achieved through ride-sharing and ride-hailing services, we just might have a chance to radically redesign our cities. If the 20th century was devoted to building the infrastructure to service the personal automobile, then perhaps the 21st century will be devoted to undoing most of it.
Public Works officials have already begun road diets for its “inclusive” mode design (see Fresno, CA, Louisville, KY). It’s design is intentionally to slow vehicle traffic. With AirBnb having defeated a major city’s ordinance, how will ride-share startups respond to future infrastructure policies (see Los Angeles’ Highland Park).
The article suggest road diets could be an outcome of ridesharing. Arguably road diets have actually emerged from Vision Zero to eliminate pedestrian-vehicle deaths. How will rideshare companies respond when parking/stopping lanes are removed along money-making corridors, or when travel times are slowed across important thoroughfares which could affect their algorithms. Local startup Split caters to this point of friction and curates required pick-up and drop-off locations near addresses.
Glen Park’s BART streetscape redesign eliminated a coveted stopping lane in favor of an extended pedestrian waiting area.
So the article speaks for a car-less future with cars. It would be like suggest e-cigarettes (vaping) will herald the end of smoking. In actuality the future urban fabric may be fully to eliminate the car itself. Currently, it would be more important to see rideshare’s effect upon transit, if it’s complementary or in actuality duplicative. In 2012 TCRP wrote a 72 page report on ridesharing only to conclude:
Evidence that ridesharing complements public transit is limited, according to this examination of the state of the practice. Even though ridesharing has been around for decades as a travel mode and despite the benefits that a number of agencies have experienced a good deal of skepticism about combining ridesharing and public transit still exists.
It took me some Googling to nail down the question “Where does MAMP’s phpMyAdmin (PMA) store mysql databases in OSX?” so I thought I’d summarize my findings. The reason mysqldump
isn’t as convenient is that MAMP is purposely GUI-focused, so switching brains and trying to navigate Terminal to MAMP’s mysql core is annoying and not intuitive (MAMP stores things all over the place). Also mysqldump doesn’t easily like dumping all databases. If we’re GUI, then let’s see if there’s a GUI solution and voila there is.
.frm
) inside HD/Library/Application Support/appsolute/MAMP PRO/db/mysql
Similarly now you know how to cherry pick from old Time Machine backups instead of needing to setup the MAMP environment all over again. Of course different versions of PMA may have changed that db folder structure, so be wary of that.
As a followup to my strangely popular KML Toggle example on Google Maps, someone has prompted me to do a Marker Toggle code example. I’m pretty late to the game to notice that Google updated some of the Maps library syntax, you can now for example pass an object literal for latitude longitude. As a warning, the code is a bit clumped together to briefly give an example into working with markers. I wouldn’t recommend master functions that do everything under the sun which is why Angular is a great library to separate your concerns and dependencies.
This example organizes code as follows:
initializeMap()
procedural function
createMarkers()
function that instantiates markers with infowindows and stores it back in the json.createControls()
dynamically generates the controls based on information in the json. This would be akin to using Angular’s ng-repeat.toggleControl()
is a state changing function that sets your checkbox state, class style, and marker visibility. It uses inverted logic, if the checkbox is checked, then do the opposite.Click here to the Google Maps Marker Toggle code example.
// Our data source object in json format var markerJson = { "coffee1": { "name": "Urban Bean Coffee", "coordinates": { "lat": 44.958813, "lng": -93.287918 } }, "coffee2": { "name": "Spyhouse Coffee", "coordinates": { "lat": 44.998846, "lng": -93.246241 } }, "coffee3": { "name": "Blue Moon", "coordinates": { "lat": 44.948480, "lng": -93.216707 } } }; // Set a global variable for map var map; // Setup a listener to load the map via Google google.maps.event.addDomListener(window, 'load', initializeMap); /* Google Maps Related Functions */ // Initialize our goo function initializeMap() { var options = { center: { lat: 44.9812, lng: -93.2687 }, zoom: 13, mapTypeId: google.maps.MapTypeId.ROADMAP } map = new google.maps.Map(document.getElementById("map_canvas"), options); // Create markers into DOM createMarkers(markerJson); // Create controls dynamically after parsing json createControls(markerJson); }; // Instantiate markers in the background and pass it back to the json object function createMarkers(markerJson) { for (var id in markerJson) { var shop = markerJson[id]; var marker = new google.maps.Marker({ map: map, position: shop.coordinates, title: shop.name, animation: google.maps.Animation.DROP }); // This attaches unique infowindows to each marker // You could otherwise do a global infowindow var and have it overwrite itself marker.infowindow = new google.maps.InfoWindow({ content: "This coffeeshop is called " + shop.name }); marker.addListener('click', function() { this.infowindow.open(map, this); }); shop.marker = marker; } }; // In this example create the controls dynamically with all checked, obj is each "coffee" listing function createControls(markerJson) { var html = ""; for (var id in markerJson) { var shop = markerJson[id]; html += '<li><a class="selected" href="#" id="' + id + '" onclick="toggleControl(this); return false"><input onclick="inputClick(this)" type="checkbox" checked id="' + id + '" />' + shop.name + '</a></li>'; } document.getElementById("controls").innerHTML = html; }; // Toggle class, checkbox state, and marker visibility function toggleControl(control) { var checkbox = control.getElementsByTagName("input")[0]; var shop = markerJson[control.id]; if (checkbox.checked == true) { checkbox.checked = false; control.className = "normal"; shop.marker.setVisible(false); // If you have hundreds of markers use setMap(map) } else { checkbox.checked = true; control.className = "selected"; shop.marker.setVisible(true); // Similarly use setMap(null) } }; // Cleanup function, resets controls, hides all markers, does not destroy function removeAll() { for (var id in markerJson) { var shop = markerJson[id]; shop.marker.setVisible(false); document.getElementById(id).className = "normal"; document.getElementById(id).getElementsByTagName("input")[0].checked = false; } }; // In this case we are keeping the input box for accessibility purposes, so we bubble up the click event to the parent control function inputClick(input) { input.parentElement.click(); };