Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
220 lines (163 sloc) 13.5 KB



  • Updated React-Native to 0.61.2: this has been a painless update 🙌
  • Added sound effects for guided audio exercises. I personally requested and bought the audio voice lines from voicebunny.
  • Automatically switch to dark/light mode theme based on the iOS 13 theme settings


2019-03-11 First Release 🎉

Project Structure

The directory structure of the project is the following:

 ├── assets
 │   └── techniques // JSON data of the breathing techniques
 │        ├── awake.json
 │        ├── deep-calm.json
 │        └── ...
 ├── components // The building blocks of the UI
 │   ├──  App
 │   │    ├── App.tsx
 │   │    ├── AppMain.tsx
 │   │    └── ... // Other "App" related components
 │   ├── ButtonAnimator
 │   │    ├── ButtonAnimator.tsx
 │   │    └── ... // Other "ButtonAnimator" related components
 │   ├── Exercise
 │   │    ├── Exercise.tsx
 │   │    ├── ExerciseCircle.tsx
 │   │    ├── ExerciseCircleDots.tsx
 │   │    ├── ExerciseCircleComplete.tsx
 │   │    └── ... // Other "Exercise" related components
 │   └── ...
 ├── config
 │   ├── constants.ts // Constants used across the app
 │   ├── fonts.ts // Fonts settings definitions
 │   ├── images.ts // Exports all the images/icons used in the app
 │   ├── techniques.ts // Exports all the available techniques
 │   ├── themes.ts // Dark and light themes settings
 │   └── timerLimits.ts // Exports all available timer limits settings
 ├── context
 │   └── AppContext.tsx // A Redux-like approach to state management
 ├── hooks
 │   ├── useInterval.ts
 │   ├── useOnMount.ts
 │   └── useOnUpdate.ts
 ├── types // TypeScript type definitions
 └── utils // Common utils used across the app

A few interesting notes:

Breathing techniques directory

I preferred to not hard-code the breathing techniques in the app, they can be defined using the following JSON format:

// ./src/assets/techniques/awake.json
  "id": "awake",
  "name": "Awake",
  "durations": [6, 0, 2, 0],
  "description": "Use this technique first thing in the morning for quick burst of energy and alertness.",
  "color": "#F1646C"

And are exported in src/config/techniques.ts.

Components directory

The React components used in the app are grouped by context/usage:

All the app components are exported using a named export (instead of the default export) because VSCode TypeScript auto-import works incredibly well with it.

React Hooks

This was not my first test drive for the React hooks, I already had the chance to use them in a few other side projects in the past, and one of the main reasons I wanted to add them to this project was to create an abstraction hook over the Animated API to make the code cleaner and more re-usable.
...and I failed.
I tried a few different approaches, but I wasn't able to strike a balance between making it generic and flexible enough, so I just kept working with the plain Animated API.

Libraries and app size

Breathly uses just a few external libraries to keep the bundle size to the minimum.
The libraries used are:

Might sound funny, but I had a some issues with all of these libraries.
Luckily enough, I already worked with them in the past so I already knew some workarounds for solving them:

  • react-native-haptic has a missing podspec and is shipping the example with NPM, so I'm using this branch instead.
  • react-native-splash-screen required manually linking the library on both iOS and Android and it also has a few issues that must be solved before running it in production (making sure the assets aren't too big, adding a specific color to the res)
  • For some reasons Gradle didn't like react-native-navigation-bar-color's build file when building a release APK, so I just extreacted the interested native methods and added them directly in the native Android source code (hence why you won't see it in the package.json).

Being careful with the number of libraries was helpful to reduce the bundle size, but to keep it as small as possible on Android I also enabled ProGuard and the separate CPU architecture build.
In this way the bundle size on Android is way smaller than a standard React-Native's app one (~8MB instead of ~20MB):


Protip: Enable ProGuard and the separate CPU architecture split build from the beginning of your development: they might be incompatible with some code/libraries so by enabling them from start you'll be able to catch these issues soon.

App performance

Even if Breathly is quite small I took care of the performance from its inception in a few different ways.

To ensure good performance on the animations side I'm using the Native Driver on every single animation of the app. Again, I have previous experience on animations with the Native Driver, but there are a few things that can be a time sink for a newcomer:

  • Try to not animate a text component (Animated.Text) directly: wrap it in an Animated.View instead. I'm not sure why but some animated values (opacity for example) won't work as you expect on an Animated.Text component on iOS.
  • Make sure to check the finished parameter of the animation start() callback if you're planning to run some code when an animation ends: if you don't do so then the code will try to run even if the animation has been interrupted by a component unmount.
  • Loops using the Native Driver can cause issues if they use Animated.Sequence: as a workaround you can implement your own version of the loop.
  • Do not give up: using the Native Driver puts a limit on what values you can animate but 99% of the time you can build your animation with them, or at least achieve a similar effect to what you want to do.

Just one rule of thumb here: don't load static assets from the JavaScript bundle.

Loading images from the JavaScript side (using import/require) from my experience is way slower than loading them from the native side.
In Breathly all the images (logo, icons, background) are bundled as native resources on both iOS and Android. Bundling them as native resources ensures they're loaded as fast as possible, but it was also time consuming:

  • I had to resize each asset multiple times for both iOS (@2, @3) and Android (hdpi, xhdpi, etc...)
  • I'm keeping track of them in a custom config file
  • For rendering the images I had to specify both width and height (you can't automatically render them based on a single width/height if they're imported from the bundle).

State Management

There's not much to say about the app state management.
I'm using Context + useContext + useReducer to keep track of the global state (see context/AppContext). Since I wanted to add side effects (storing to AsyncStorage) when dispatching some actions I ended up creating my own "action creators" as well.
Am I happy to have built my own state management instead of going with Redux/MobX? Yes.
Was it worth it? Honestly, I don't think so.
I basically created a bunch of boilerplate code when I could have "just" used Redux (notice the irony) and its middlewares or MobX.

UI and UX

I'm really interested in UI, UX and design patterns but I'm not a designer. That said, I still tried to make the experience pleasant by taking care of a few small details and I'm also quite that I finally had a chance to manually design icons, splash screens and logo using Photosthop (you can find the PSDs in the .assets dir).

App icon
The app icon was the result of multiple iterations but I'm finally happy with the result:

  • It's square and simmetrical so it can be easily re-used in other places (e.g.: the splash screen)
  • It's not heavy on details
  • The foreground "bubbles" can be used on different backgrounds (because the outer bubbles are using a lower opacity)

Since the icon is square, I was able to easily resize it to the iOS app standard and also to use it with the adaptive icon pattern on Android.

I would have preferred a more "meaningful" icon but I wasn't able to find a less abstract concept to work on.

Splash screen
Implementing the splash screen on iOS was quite easy using the XCode storyboard:

  1. I created the splash screen background (2732x2732, to cover the widest side of the iPad Pro).
  2. I created the splash screen foreground icon/image.
  3. I placed both the background and foreground in an XCode storyboard and made sure that the foreground image keeps its aspect ratio and scales accordingly with the device size.
  4. I made sure the Status Bar uses a light content during the splash screen.

On Android it was a different story:

  1. I created the splash screen background.
  2. I created the splash screen foreground icon/image.
  3. I placed both the background and foreground in the Android res folder after resizing them manually
  4. I orchestrated the background and foreground positioning using an XML layout
  5. I made sure the Status Bar color matched the color of the foreground top (which is a vertical gradient) and that the Navigation Bar color matched the color of the foreground bottom.

The splash screen is programmatically hidden from the JS code only once the app has loaded all the data from the Async Storage: in this way if the user is using the "dark mode" he won't see a white flash after the splash screen.

Status bar and navigation bar
The Status Bar and Navigation Bar (Android only) colors match the "screen color".

On iOS the Status Bar background is always transparent, I just had to change the Status Bar content color:

  • During the splash screen the Status Bar content is white
  • In the main menu the Status Bar content is black by default and white if the user is using the light mode
  • In the exercise screen the Status Bar content is always white

On Android the Status Bar and the Navigation Bar have a solid background color (I didn't like the effect of the translucent background) that matches the screen color and their content works exactly like on iOS.

App Store, Play Store, privacy and licensing

The app is available on both Apple's App Store and Google's Play Store.
I also created a simple landing page. You can find the landing page source code here.

The Breathly text font on iOS is San Francisco, while the text font on Android is Roboto. I prefer the San Francisco one but it cannot be used on apps distributed on the Play Store.

To accomodate the release on the stores Breathly is license under the Mozilla Public License.

Breathly doesn't use any third party software to track/log/debug the app usage.

You can’t perform that action at this time.