Skip to main content

Mobile

Minds app uses React Native to power both the Android and iOS applications. You can check out the code here

Setup dev environment

Please refer to React Native CLI Quickstart in the React Native Docs to setup your local dev environment.

We use react-native-cli, not expo, because we rely on many packages with native code.

Install

Clone the repository.

git clone git@gitlab.com:minds/mobile-native.git

Install the dependencies

yarn

Install pods (iOS only)

cd ios
pod install

Run the application

yarn ios

or

yarn android

Technologies

The app uses many open source packages, but there are a few technologies that would be good to know for anyone who wants to contribute or check the code.

  • App state: mobx & mobx-react\ We are migrating the app to react hooks and planning to move to mobx-react-lite when it is finished. So any new code should be following that guideline.

  • Navigation: @react-navigation

Structure

mobile
└───android
└───ios
└───locales

└───src
│ └───assets
│ └───config
│ └───common
│ │ └───components
│ │ └───services
│ │ └───stores
│ │ └───helpers
│ │ └───...
│ └───module
│ │ └───complexchild
│ │ │ └───Header.tsx
│ │ │ └───Body.tsx
│ │ │ └───ComplexChild.tsx
│ │ │ └───...
│ │ │
│ │ └───ModuleScreen.tsx
│ │ └───SomeInnerComponent.ts
│ │ └───SomeModel.ts
│ │ └───createModuleStore.ts
│ │ └───ModuleStore.ts
│ │ └───ModuleService.ts

└─ App.tsx

ios & android

These are the project folders for each platform, usually you will not need to touch anything there.

locales

The locales are stored in json format, the main locale files is en.json and all the others are downloaded from Poeditor. Check Mobile Internationalization

src/assets

All the assets go here, images, videos, and fonts.

src/config

Config.ts stores all the configuration constants for the application (like the api uri)

src/common

All the reusable code should be inside common:

  • Generic components
  • Reusable stores
  • Services used across many modules or the whole application
  • Helpers or utility functions.

src/module

Every module has his own folder inside src/{modulename}, there we have the screens or components of the module along with stores, services, and models.

Naming conventions

Components

Components should use Camel Case names and the .tsx extension\ Eg: FeedList.tsx

Screens

Screens are components too, but we add the Screen suffix to be able to easily identify them\ Eg: NewsfeedScreen.tsx

Stores

Stores should use Camel Case names, the Store suffix, and the .ts extension\ Eg: NewsfeedStore.ts

Module Services

Services should use Camel Case names, the Service suffix, and the .ts extension\ Eg: NewsfeedService.ts

Global Services

Services should use Kebab Case names, the .service suffix, and the .ts extension\ Eg: api.service.ts

Models

Models should use Camel Case names, the Model suffix, and the .ts extension\ Eg: ActivityModel.ts

Stores

The stores are implemented using MobX to keep the app state, currently we are using the store injection mechanism but it should be considered legacy.

The targeted architecture is to have globals stores provided using a react-context and local stores for the components.

Local and Global stores

Always prefer local stores for the components using the useLocalStore hook. This in most cases simplifies the logic of the stores, because it creates a new instance for each component's instance, avoiding the need of clean up data on each run.\ Global stores are available for the whole app, they are instantiated when the app starts and they live until it is closed. A store should be global only if it needs to be accessed from many components of the app tree Eg: UserStore which stores the current user.

Creating local stores

For simple stores it is better to just have it inline, it improves the readability of the code\

export default observer(function (props) {
const store = useLocalStore(() => {
votes: 0;
voteUp() {
store.votes++;
},
voteDown() {
store.votes--;
}
});

return (
<View>
<Text>{store.votes}</Text>
<Text onPress={store.voteUp}>Vote Up</Text>
<Text onPress={store.voteDown}>Vote Down</Text>
</View>
);
});

But, if your store is complex or generic, you should move the creation outside the component

// createVotesStore.ts
export default () => {
votes: 0;
voteUp() {
this.votes++;
},
voteDown() {
this.votes--;
}
...
}

// VotesCounter.tsx
import createVotesStore from './createVotesStore';
export default observer(function (props) {
const store = useLocalStore(createVotesStore);

return (
<View>
<Text>{store.votes}</Text>
<Text onPress={store.voteUp}>Vote Up</Text>
<Text onPress={store.voteDown}>Vote Down</Text>
</View>
);
});

Passing local stores to child components

There are two ways of passing the store to the child components the most simple is just passing the store as a property

...
return (
<View>
<Header store={store}/>
...
</View>
);

//Header.tsx
...
<Text onPress={props.store.voteUp}>Vote up</Text>

The second is to create a context and a hook to access the store, this should be used only if you need to access the store in a deep child in a very complex tree.\ Eg:

Parent (Store)
└───Body
│ └───Modal
│ └───Header
│ └───List
│ │ └───Item
│ │ │ └───Header
│ │ │ │ └───Title (Store)
│ │ │ └───Detail
│ │ └───Item
│ │ └───Header
│ │ │ └───Title (Store)
│ │ └───Detail
│ └───Footer
...

Using global stores

To use a global store you should use the useStores hook.

import { useStores } from "../common/hooks/use-stores";
export default observer(function (props) {
const { user } = useStores();
return (
<View>
<Text>{user.me.name}</Text>
</View>
);
});

Models

Models are similar to stores, they have normal and observable properties, and actions that modify those properties. The difference is that a model represents an Entity into the application.\ Eg: ActivityModel BlogModel

Creating a model

All the models in the application must extend from BaseModel

class ContactModel extends BaseModel {
@observable name = '';
@observable phone = '';
count = 1; // no observable

@action
setName(name) {
this.name = name;
}

...
}

Instantiating a model

The base model provides you handy static methods to create instances

Create

// create an instance of contact model and initializes the properties
const contact: ContactModel = ContactModel.create({
name: 'martin', phone: '555-12344321'
});

Create Many

// create many instances of the contact model
const rawContacts = [
{ name: 'martin', phone: '555-12344321' },
{ name: 'emma', phone: '555-43211234' },
...
]
const contacts: Array<ContactModel> = ContactModel.createMany(rawContacts);

Model composition

You can easily add composition to your models by implementing the childModels method and return an object with the properties and the models they represents


class LocationModel extends BaseModel {
address = '';
}

class ContactModel extends BaseModel {
@observable name = '';
@observable phone = '';
count = 1; // no observable

/**
* Child models
*/
childModels() {
return {
location: LocationModel,
referrer: ContactModel,
};
}

@action
setName(name) {
this.name = name;
}

...
}

Now, you can instantiate a model with all its composed model in a single call to create

const contact: ContactModel = ContactModel.create({
name: 'martin',
phone: '555-12344321'
location: {
address: 'Somewhere 123'
},
referrer: {
name: 'emma',
phone: '555-43211234'
location: {
address: 'Planet earth 123'
},
}
});

This will instantiate the main contact and all his children models\

  • contact.location will be a LocationModel
  • contact.referrer will be a ContactModel
  • contact.referrer.location will be a LocationModel
  • You can call contact.referrer.setName('newname')

The same works for createMany.

Check or create

Check if the passed parameter is an instance of the model\ if this is the case, it will return the same instance or it will create a new one otherwise

function(contact: ContactModel | object) {
const contactModel = ContactModel.checkOrCreate(contact);
contactModel.setName('new name');
...
}

Styling and themes

Please read the walk through of styling and themes

End-to-end testing

We use Detox. You will find these tests on the e2e folder under the project root.

First things first, you may want to add detox-cli to your environment.

Since we use yarn, you can use: yarn global add detox-cli

Another thing that you should check before building and running it's the detox config in the file .detoxrc.json. There you will find:

  • testRunner used under the hood (we use jest)
  • the runner config e2e/config.json
  • apps to define builds divided between build command and binaryPath. It's important to know that if this fails, it's a problem on react-native side, not detox
  • devices used to run the apps
  • configurations to connect the apps with the devices

Note: This configuration may change on future Detox releases

Running the tests

Build detox build -c configName

Run detox test -c configName path\to\test.e2e.js

Useful while making tests

  • We have a set of helpers methods under e2e/helpers/waitFor.js
  • Sometimes you may want to mock components. Check Detox Mocking Guide
  • Use detox test --reuse to re-run the tests whitout re-installing the app

Troubleshooting

  • Build fails: this it's related to react-native. Did something change? react-native version maybe?
  • Running fails with time out. Most of the time it's because detox waits for requests to finish. Consider using detox url black list

Feature flags

To hide something behind a feature flag you can do it using the features service

import featuresService from '../common/services/features.service.ts'

function (props) {

return (
<View>
{featuresService.has('voteUp') && (
<Text onPress={props.store.voteUp}>Vote Up</Text>
)}
<Text onPress={props.store.voteDown}>Vote Down</Text>
</View>
);
}

Keep in mind that this service first loads the cached feature flags, and then it updates them from the server. This is an async function and it takes some time to be ready. Because of this, a feature flag is not a good idea for a core app initialization functionality or to change the initial screen structure.