cover-image

Dindr

About

Dindr is a social food discovery application designed to identify commonly favoured restaurants among friends. It assists users in discovering the finest cuisines within a specified location, enabling them to express their interest in restaurants by swiping left or right. Users are then presented with shared restaurant preferences, facilitating groups in refining their selection of dining options.

Availability

Dindr is currently in closed beta testing. It is available through Apple's TestFlight program and Google Play's Internal testing program. If you want to try the app, please email hello@andrewvo.co for an invite.

Dindr is seeking funding to publish the app to Apple's App Store and Google Play Store for general availability. Please email hello@andrewvo.co to register interest in the project.

Technologies

  • React Native
  • Gluestack UI
  • Amazon Web Services
  • AWS CDK
  • Firebase Cloud Messaging
  • Google Maps API

Features

Real-time updates

Dindr supports real-time client updates using web sockets and DynamoDB streams. Once a client comes online, they register a web socket to the server that is unique to the client's device. Whenever a resource is created, updated or deleted, the corresponding DynamoDB stream will inform each client about the resource change. This creates a delightful user experience where the client has an accurate representation of the server data without having to perform manual refreshes.

Cross-platform

With over 99% of all mobile devices being on iOS or Android, it was a no-brainer to support both operating systems. Compiling down to each respective OS's native components, Dindr can render a native user experience. With the help of React Native, a single code base with shared logic means that both operating systems will share the same consistent experience.

In-app chat

Real-time chat was implemented in the context of groups. A group can communicate with each other to discuss events and food-related discussions. The chat experience is similar to any instant messaging platform where messages are displayed live, push notifications to inform group members and users can upload images and videos.

Push notifications

Push notifications are great to inform the user of important events that are triggered inside the app. It allows the users to be notified, even without having the app open. Important updates about events, groups and chat messages are broadcasted to necessary recipients immediately.

Geolocation search

With access to the user's device location through GPS, Dindr can search for restaurants within a certain area of the user. This allows users to get access to the most relevant search results based on their location.

Deep linking

The Dindr URL scheme is registered on the user's device upon installation of the app. This allows the device to open links beginning with `dindr://` directly inside the app. The app will automatically read the URL, parse it and point the user to the correct page depending on the contents of the URL. The best use case for deep linking in Dindr is being able to open invite links to public groups and automatically join the group.

Challenges

Web sockets

In the past, I typically used a backend as a service platform such as Firebase to handle real-time updates. For this project, I decided to implement a web socket API with native web sockets on the client side. This involved planning how a user would register and deregister web sockets, storing multiple web sockets (many devices) for each user, a permission structure for each resource on the server to determine which users would receive web socket updates and automatically expiring web sockets based on their ID token.

Client caching

To speed up the user experience and minimize network calls, a caching mechanism had to be implemented on the client. This meant creating a store where resources and lists of resources be cached and also updated upon receiving server updates. Luckily syncing server state with client state is handled very well using Tanstack Query (formerly React Query). Any queries matching a certain query key would be cached in the store with subsequent queries of the same key could automatically be read from the store. Replies from mutations or web socket updates would trigger an update in the cache and automatically produce re-renders on the components that depended on them.

Optimizing images

Images uploaded from the client can be uncompressed and large in file size. This results in slow performance for clients trying to read the images. Although some compression and resizing can be done on the client, the server can't rely on what it is receiving from the client's device. Server-side image optimization is one of the most common use cases of S3 triggers. The implementation, however, can be tricky as the time between uploading and resizing may take some time. Optimistic updates help the client side display the local image until the image is finished resizing. Some states need to be declared against the image to determine whether it has finished processing or not. Most implementations used the sharp library, however, I had issues with the different binaries it provides based on the server's architecture. Long story short, the lambdas were bundled on my M1 Mac (which is arm64 architecture), and the lambdas in the cloud were running x86_64 Linux architecture. Moving to jimp (which had no external dependencies) did the trick to mitigate this issue.

Optimistic updates

Optimistic updates give a smooth experience for the user of the app as it makes it feel like network calls are seamless and happen in real time. It is the process of displaying the intended result of a mutation to the user before receiving a reply from the server. In the case that the mutation fails, the client will roll back the change. Optimistic updates were implemented anywhere in the app where there would be mutations. The most challenging optimistic updates were managing the different states of a chat message. Certain UI elements had to be displayed when a user sends a message, when the server acknowledges it has been delivered and when a message fails to send. In the case that the message fails to send, a retry mechanism had to be implemented to re-send the failed message.

Resource cleanup

One of the challenges of managing resources is removing them when they are no longer required. To do that, a dependency tree had to be established to determine what resources are related to each other. For example, if an event was deleted, its event picture, all its places and liked places (from users) had to be cleaned up from the file store and database. This could not be done on the same API call as when the user deletes the event as these cleanup functions could take a lot of time depending on how many there are. Using DynamoDB streams, I was able to take an event-driven approach to resource cleanup. This meant that cleanup functions could be triggered automatically in the background and the client did not have to wait for the result of the cleanups. By using DynamoDB streams to watch for resource deletion, I was able to set off a chain of cleanup functions based on the dependency graph. For example, a de-activated user triggers the removal of the user from all groups, which deletes all their liked places from each event in the group.

Improvements

Dark mode

Support for a user to set their colour scheme to light, dark or system setting. This can easily be achieved using React Native's Appearance API. Combined with Gluestack Color Mode API, we can style components based on the user's preference.

External identity providers

The application currently only supports email & password sign-in. Support for identity providers such as Facebook and Google will ease the friction of user creation.

Automated testing

An increase in code coverage for unit tests as well as integration and end-to-end tests will give confidence for the application to perform as expected. There is currently little testing, as it is currently in the prototype stage.

Automated deployment pipelines

The application currently uses manual deployments to the Apple App Store and Google Play Store. It also uses manual deployments through AWS CDK for the server-side code. The implementation of a pipeline will automatically test and deploy the code on code pushes.

UI / UX

Android native components

image-764e3f58722c3c96696b144416851fdfda30b0e0-1080x2280-png
image-18440438d2538d9cf9c793a974d4de37a16a86b3-1080x2280-png
image-feaf5bfd470f01f3fb3373572fd6256c3cc12de9-284x600-gif

iOS native components

image-34429bdc7e6312478679f307410724a39d7e1c62-1179x2556-png
image-399bfde242aaa91d736f9aeda6a6398b4bb47772-1179x2556-png
image-52ed68f482e289f5cb54cab8fcc18b79ba8de551-277x600-gif

Swipe & match interactions

image-068aaf2bb51f69919be1878aa3f8f131e62e7ff2-276x600-gif
image-bbb540d25846f6f83cc88b431b8c42f80a4db8c7-276x600-gif

Chat

image-e50ae635830cfd4b45167ad003802cc9af037971-1080x2280-png
image-6bae14ea02db3bb41e166170e8d096357f8c491e-1179x2556-png