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 aLocationModel
contact.referrer
will be aContactModel
contact.referrer.location
will be aLocationModel
- 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
Currently, there are two common styles in the app, the src/styles/Common.ts
and the src/styles/ThemedStyles.ts
service, please use ThemedStyles, Common should be considered legacy.
Themed style
Themed styles add support for light and dark themes (for the color definitions you can check src/styles/Colors.ts
).
This service provides some frequently used styles, for common layouts, colors, font size, and spacing (padding and margin)
Colors
It provides you of different colors for text, icons and backgrounds. Eg. colorPrimaryText
backgroundPrimary
Font Sizing
Normalized font sizes are fontS
fontXS
fontM
fontL
fontXL
fontXXL
fontXXXL
Spacing
For spacing you can use the margin and padding properties of the style service, they are auto-generated by multiples of 5.
Eg:\
Style | |
---|---|
padding | padding of 5 |
margin | margin of 5 |
padding2x | padding of 10 |
margin4x | margin of 20 |
marginTop4x | margin of 20 to top the view |
paddingVertical2x | padding of 10 into the top and bottom |
paddingHorizontal | padding of 5 into left and right |
function (props) {
const theme = ThemedStyles.style;
const { user } = useStores();
return (
<View style={[theme.paddingTop2x, theme.marginHorizontal]}>
<Text style={theme.marginTop}>{user.me.name}</Text>
<Text style={theme.marginTop}>{user.me.username}</Text>
</View>
);
}
Layout styles
Usually, you will create a stylesheet in your component to define the main layout, but there are some commonly used layout styles that are available for you in the themed styles service. Eg flexContainer
= {flex: 1}
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.