News

Facebook Icon Twitter Icon Linkedin Icon

AnyMind Group

Facebook Icon Twitter Icon Linkedin Icon

[Tech Blog] The Cross-platform solution for the new TalentMind People

Hi, I’m Reacher, Senior Front-end Engineer at TalentMind People which is a transnational HR and payroll system. We’ve started working on our new structure. In this article, I’d like to share the ideas and tech stacks of the new structure.

■ Issues we were facing

We started TalentMind People with a web app in the MVP stage. Later on, we brought it into iOS and Android with React Native. As time goes by, it comes with a big problem that we have to do the same thing twice. You probably could imaging the situation when we try to create a new feature or modify some features, we have to develop it on the web, after that we have do it again on mobile apps and they might actually use the the same business logic or calling the same APIs. It’s really bad for productivity and maintenance. To solve the mentioned problems and in the faith of ‘Do not repeat yourself’, I started to think about the cross-platform solution and started the journey.

■ Considering Pros and Cons of Cross-platform solutions

Choosing a tech stack is always a tradeoff. It will be better that we think more before jumping into the pool. These are my ideas about considering such solution for the TalentMind People:

■ Pros

My plan is to build web app, iOS app, Android App and to support PWA with one codebase. In this case, we could reuse most of our logic business code and most of our UI code (with responsive design) and for the new features, we just need to build it once. Doing this could bring us the following advantages:

  • – More Productivity
  • – Highly reusable code
  • – Consistency between platforms since platforms are shared the same components mostly

■ Cons

Like I said, choosing a tech stack is a tradeoff, it comes with some cons for sure.

  • – More complicate code. You might need to make some specific code for the specific platform in some cases, so you might see some files end up with .native.ts, .android.ts and .ios.ts
  • – Need to wait for a while for supporting new features for a new version of OS

For the complexity, by well separating components and code could highly reduce the impact. And TalentMind People is not using lots of device feature, I don’t really think new versions of OS will be a big problem for us. I believe cross-platform solutions could fit our requirements after considering.

■ Modern Solutions

Then it’s time to think about how many choices of implementation that we could do. There are some solutions like Kotlin multi-platform, React Native, Flutter …etc in modern world.

React Native and Flutter are two I was considering more than others. Here are couple reasons.

  • – Web support
    TalentMind People still relies on the web app to do some complicate interactions such like monthly/yearly payroll handling. React Native and Flutter offer much better web support than the others.

  • – Performance
    Performance is one thing that I considered as well. React Native and Flutter has their own way to handle communications between UI and Native APIs. Both of them offer the native like performance.

Here are some differences between these two, both of them are good enough to let you starting your cross-platform projects.

■ React Native

  • – Big communities, lots of third party libraries and solutions
  • – Share same APIs with React.js
  • – More like functional programming, we all like mighty react-hook
  • – Use JavaScript or Typescript as the developing language
  • – It build a javascript engine as a bridge to connect native and our javascript code

■ Flutter

  • – Google strongly supports it and put lot of effort on it
  • – It’s a framework not a library. So most of things you might need like uni-testing are already inside the Flutter.
  • – Object oriented programing
  • – Use Dart as the developing language
  • – It has it’s own rendering engine to render the views
  • – Better developing experience

I pick up React Native eventually. Since it’s not an article to compare React Native and Flutter, I will just talk about my thought shortly and roughly. React Native is more suitable for the teams that come from the React eco-system and Flutter might be suitable for the teams that come from Angular, in our case, we really like syntaxes and functional programing parts of React.

Also, Flutter for web had a annoying problem that it will have a huge bundle size after compiling and it doesn’t offer the good code splitting solution from framework level, you have to do it by defer from Dart but we could very easily to do that in React.

I think Flutter still have lot of potential and I will always keep an eye on it.

■ Implementations

We developed our new TalentMind People on React Native with React Native for Web, it help us to let the React Native project could be built to a web.

To do it, we have to install react-script and it’s dependencies then create an src file in the React Native project and add a new index.ts inside it. We have to do it is due to react-script will use the index.ts as the entry file for web and we will use react-script to build our web app so that we don’t need to set up everything like webpack from scratch. So, in our project there will be two entry points, index.ts for web and index.native.ts for apps. That’s generally the basic settings.

There is an important thing we have to understand for developing cross-platform apps. Most of the things we were familiar with in the web world will no longer exist. So I’m gonna share some tips how we build the web-like features.

■ Styling, Theme and Responsive

In new TalentMind People, I hope it supports dark mode which is offered by the modern apps quite commonly. Another thing is to highly reuse our components, it will be better to let the components be responsive. And if you had experience of developing a React Native app, you will see handling styles in React Native is a little bit different since it doesn’t support css file. We will not be able to do these on media query or something like that. So here is how I handle it.

Let’s take a quick look at our Input component which will be reused around the whole project.

Input.tsx

const Input = (props: InputProps) => {
  // state
  const [inputStatus, setInputStatus] = useState(InputBehaviorType.DEFAULT)
  const { inputStyle, inputTitleStyle, errorMessageTextStyle, ... } = useStyles({
    getStyles: getInputStyles,
    getStylesProps: { status: inputStatus },
  })
  ...

I create a custom hook useStyles. It takes two args. getStyles is a function which will return the styles of the Input component base on the current breakpoints and the theme mode. getStylesProps is an object if we need some other args when rendering the styles.

useStyles.ts

const useStyles = (
  { getStyles, getStylesProps }: UseStylesHookProps
) => {
  const {
    globalState: { currentBreakpoint, mode }
  } = useContext(GlobalStateContext)
  const styles = getStyles
    ? getStyles({ mode, currentBreakpoint, getStylesProps })
    : {}
  return { styles, mode: parseInt(mode, 10), currentBreakpoint }
}
export default useStyles

As you could see, the hook useStyles is actually wrapping the global context. It will get the currentBreakpoint and mode then use it with the getStyles function and returns the styles eventually.

And this is how we handle the getInputStyles function

const getInputStyles: GetStylesFunc = ({
  mode,
  getStylesProps: { status },
}) => {
  const { spaces, borderRadius, colors, fontSizes } = theme
  return {
    inputStyle: {
      paddingTop: spaces[1],
      paddingBottom: spaces[1],
      ...
      color: [colors.black[6], colors.black[3]][mode],
    },
    ...,
    inputTitleStyle: {
      fontSize: fontSizes[3],
      marginBottom: spaces[1] + 'px',
      color: [colors.black[6], colors.black[3]][mode],
    },
    ...,
    errorMessageTextStyle: {
      fontSize: fontSizes[2],
      color: colors.red[4],
    },
  }
}
export default getInputStyles

You probably noticed some styles like this paddingTop: spaces[1], it was inspired from a popular styling solution styled-system. we’ve defined our spaces like this which is used for margin and padding

const spaces = [4, 6, 8, 12, 16, 18, 20, 24, 27, 30, 33, 36, 40]

and with typescript, we could use enum for the breakpoints and mode like followings and after compiling they will be transfer as indexes which is very useful for us.

export enum Breakpoints {
  xs,
  sm,
  md,
  lg,
  xl,
  xxl,
}
// xs = 0, sm = 1, md =2, lg = 3, xl = 4, xxl = 5
export enum ThemeModeType {
  LIGHT_MODE,
  DARK_MODE,
}
// LIGHT_MODE = 0, DARK_MODE = 1

With the indexes, we could easily handle our styles for modes and breakpoints

rightLabelStyle: {
  fontSize: fontSizes[2],
  fontWeight: 800,
  color: ['#fff', mainColors.black[3]][mode],
}
// mode = 0 (light mode), color: #fff
// mode = 1 (dark mode), color: mainColors.black[3]

we could even do this for responsive

logoStyle: {
  width: currentBreakpoint < Breakpoints.sm ? 125 : 223,
  height: currentBreakpoint < Breakpoints.sm ? 20 : 37,
  marginBottom: spaces[5],
}
// Breakpoints.sm = 1

This is the displaying example from our login page.

We actually put our current breakpoint in the global state and add the onLayout method to listen the changing of the layout. Once the layout was changed, it will dispatch an action to update our breakpoint and it will trigger the re-render process of React.

const App = () => {
  ...
  return (
    <ApolloProvider client={client}>
      <FullScreenFlex
        bg={bg}
        onLayout={({ nativeEvent }) =>
          globalStateDispatch({
            type: AppStateActionType.UPDATE_BREAKPOINT,
            payload: getCurrentBreakpoint({
              currentWindowWidth: nativeEvent.layout.width,
            }),
          })
        }
      >
        <AppRouter />
      </FullScreenFlex>
    </ApolloProvider>
  )
}
export default App

Here is a small tip for the reducer. Make sure return the new breakpoint only in necessary and return state directly when the breakpoint doesn’t change. It will help you avoiding useless re-rendering and saving performances.

function globalStateReducer(state: any, action: any) {
  switch (action.type) {
    case AppStateActionType.UPDATE_BREAKPOINT:
      return state.currentBreakpoint === action.payload
        ? state
        : { ...state, currentBreakpoint: action.payload }
    ...
  }
}

■ Reuse routers

We separate routes to index.tsx and index.native.tsx but these are just wrappers. We still reuse our core route settings.

Router/index.tsx

import { BrowserRouter as Router } from 'react-router-dom'
import Routes from './Routes'
const AppRouter = () => (
  <Router>
    <Routes />
  </Router>
)

Router/index.native.tsx

import { NativeRouter, Link } from 'react-router-native'
import Routes from './Routes'
const AppRouter = () => (
  <NativeRouter>
    <Routes />
  </NativeRouter>
)

Routes.tsx

// A special wrapper for <Route> that knows how to
// handle "sub"-routes by passing them in a `routes`
// prop to the component it renders.
function RouteWithSubRoutes(route: any) {
  return (
    <Route
      path={route.path}
      render={props => (
        // pass the sub-routes down to keep nesting
        <route.component {...props} routes={route.routes} />
      )}
    />
  )
}
const routes = [
  {
    path: '/',
    exact: true,
    component: () => <HomeLayout />,
  },
  {
    path: `/${Screens.PEOPLE}/${SubScreens.LIST}`,
    component: () => <HomeLayout />,
  },
  {
    path: `/${Screens.TIME_ATTENDANCE}`,
    component: () => <TimeAttendanceScreen />,
  },
  {
    path: '/login',
    component: LoginLayout,
  },
]
const Routes = () => (
  <Switch>
    {routes.map((route, i) => (
      <RouteWithSubRoutes key={i} {...route} />
    ))}
  </Switch>
)
export default Routes

■ Conclusions

From my perspective, cross-platform solutions are valuable for the projects that don’t really need to integrate with native device APIs deeply. With proper designing of components and structures, it will help you saving lots of cost and avoiding some mistakes of platforms differences. Ideally, if we build shared resources on cross-platform solutions, all our new projects base on the same solution will immediately get the abilities to be built to apps for different platforms and saving lot of cost to re-build those shared resources.

I hope this article could give you some inspirations or give you some helps if you are going to implement a cross-platform solutions. I’m Reacher, until next time!

Latest News