Kotlin is an awesome language. It is functional and offers extension functions, null safety, smart casting, type-safe builders, default and named arguments, data classes and much more. It is arguably the language of choice for Android developers and very familiar to iOS developers.
React Native enables us to write only once for multiple platforms, has a simpler API than native development and a smooth learning curve. Steve Yegg, a former Google engineer, conjectures in his blog post that hybrid frameworks are likely to become even more popular. And the framework from Facebook is the major player at the moment.
So why not mixing both?
Standing on the shoulder of giants (JetBrains/kotlin-wrappers and ScottPierce/kotlin-react-native), we can.
An example repository can be found here.
React in a nutshell
You can learn the basics of React Native very quickly reading the official tutorial, but three important takeaways are:
- view = f(state)
- React has a virtual representation of the view, and it calculates the smallest diff between renders
- development should be component-driven
Hello world in Kotlin
After cloning the example repository and following the README, you can simply edit the render() function to immediately see the result on the device.
For example, writing the following code…
override function render() = view(style = Style(flex = 1, flexDirection: "column", justifyContent: "space-between", alignItems: "center")) { text("hello") button("press me") { alert("pressed!") } touchableHighlight(onPress = { alert("also pressed!") }) { text("world") } image(style = Style(width = 640, height = 360), uri = "https://fossbytes.com/wp-content/uploads/2017/05/kotlin-android-learn-640x360.png") }
…would render the following output:
Our first application with Kotlin and React Native
Thanks to the Kotlin language features, we can write the view using a builder DSL similar to JSX, instead of manually calling React.createElement(…, React.createElement(…)).
Creating our first component
Let’s say you have the following view architecture and you want to make the code for headers reusable:
view(style = Style(backgroundColor = "skyblue", padding = 20, marginBottom = 10)) { text(style = TextStyle(color = "white", fontSize = 26, fontWeight = "bold"), text = "Kotlin React Native") }
For this task, you could create a Header component:
class Header : React.PureComponent<Any, Any>() { override fun render() = view(style = Style(backgroundColor = "skyblue", padding = 20, marginBottom = 10)) { text(style = TextStyle(color = "white", fontSize = 26, fontWeight = "bold"), text = "Kotlin React Native") } }
And call it from the render() function as such:
override fun render() = view { component(Header()) }
Alternatively, if you want the extend the DSL so that you can call this component directly, just create an extension function:
fun ViewBuilder.header() = component(Header()) override fun render() = view { header() }
Managing state
React doesn’t dictate how state management should be. For the sake of this example, let’s write our own simplified, naive implementation of Redux.
In this pattern, we have three main elements:
- store: holds the state
- actions: describe the changes
- reducers: return an updated state based on the requested action
Here is the full implementation:
typealias Dispatcher<A> = (A) -> Unit typealias Reducer<S, A> = (S, A) -> S typealias Callback<S, A> = (S, Dispatcher<A>) -> Unit class Store<S, A>( initialState: S, private val reducer: Reducer<S, A>) { var callback: Callback<S, A>? = null var state: S = initialState private fun update() { callback?.invoke(state, dispatch) } fun subscribe(onUpdate: Callback<S, A>) { callback = onUpdate update() } fun unsubscribe() { callback = null } val dispatch: Dispatcher<A> = { action: A -> state.let { state = reducer(it, action) } update() } }
Evidently, on a real world application, you should prevent race conditions and implement the subscriptions in an atomic fashion, possibly using RXJS. But that’s good for now.
A less contrived example
Now let’s write a simple application which consists of a header, and two rating widgets, for giving scores to two different programming languages.
First we create the models:
data class Rating(val language: String, val score: Int) class RatingProps(val formattedText: String, val onIncrementClick: () -> Unit, val onDecrementClick: () -> Unit)
Then the actions that can be dispatched:
sealed class CounterAction { object INCREMENT: CounterAction() object DECREMENT: CounterAction() }
Later on, we define the reducer:
fun createReducer() = { state: Rating, action: CounterAction -> when (action) { is INCREMENT -> state.copy(score = state.score + 1) is DECREMENT -> state.copy(score = state.score - 1) } }
Then we define a view model, mapping state to view abstractions:
fun viewModel(state: Rating, dispatch: Dispatcher<CounterAction>) = RatingProps( formattedText = "${state.language} scores ${state.score}!", onIncrementClick = { dispatch(INCREMENT) }, onDecrementClick = { dispatch(DECREMENT) })
We connect everything with a store:
typealias RatingStore = Store<Rating, CounterAction> fun createStore(initialState: Rating) = Store(initialState, createReducer())
Finally, we create a stateless, dumb component:
val buttonStyle = Style(height = 30, flex = 1, margin = 10) class Rating : React.PureComponent<RatingProps, Any>() { override fun render() = view { text(style = TextStyle(margin = 10, fontSize = 18), text = props.formattedText) view(style = Style(flex = 1, flexDirection = "row", marginBottom = 60)) { view(buttonStyle) { button(title = "-1", onPress = props.onDecrementClick) } view(buttonStyle) { button(title = "+1", onPress = props.onIncrementClick ) } } } }
And another component that keeps track of the state:
class RatingContainer(private val store: RatingStore) : React.Component<Any, RatingProps>() { override fun componentWillMount() { store.subscribe { s, d -> setState(viewModel(s, d)) } } override fun componentWillUnmount() { store.unsubscribe() } override fun render(): dynamic { return view { component(Rating(), state) } } } fun ViewBuilder.rating(store: RatingStore) = component(RatingContainer(store))
And the final result is:
Our final application
So… Is Kotlin with React Native production-ready?
Possibly, but there are still some improvements to be made to the whole ecosystem. The interoperability with JavaScript isn’t effortless and has some pitfalls. For example, passing the store via props to the component won’t work because the state will become immutable. Also, using data classes for the props directly will return an error from React because apparently the serialization contains some metadata. In addition, you’re supposed to write your own type definitions, which is a huge productivity killer.
I think JetBrains is to blame for the unpopularity of KotlinJS, by not giving the language the love it deserves. Kotlin/ts2kt, for instance, could solve the type definition problem. However, most of the attempts for converting types fail, and the issues remain open. Actually, we don’t see many examples targeting JS at all, despite the fact that this a working platform for more than a year. Maybe they are focused on Kotlin/Native…
In the meantime, another alternative to JavaScript is Clojurescript, which is awesome with re-frame and in my opinion offers the best development experience out there. However, it’s not for the faint-hearted.
I hope that with this article and the example repository, the community can reconsider Kotlin as a language for React Native. Feel free to fork the code!