Skip to content

Commit b93ef12

Browse files
authored
feat: brownie (#172)
* feat: brownie store feat: move to separate package feat: improve cli feat: improve plan and remaining todos feat: handle multiple stores feat: add tests for the cli feat: make DX of defining your store better feat: make hook work like useState feat: wip rewrite to shared cxx layer feat: improve swift api fix: use built-in folly methods feat: update the plan with re-render optimizations fix: sync yarn.lock fix: temporairly disable Android feat: check-in generated code feat: move store plan to ArchitecturePlan feat: move installed to separate class * fix: sync * fix: review comments
1 parent 73e0556 commit b93ef12

56 files changed

Lines changed: 3414 additions & 106 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/TesterIntegrated/App.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import React, { useEffect } from 'react';
2-
import { StyleSheet, Text, View, Button } from 'react-native';
1+
import { useEffect } from 'react';
2+
import { StyleSheet, Text, View, Button, TextInput } from 'react-native';
3+
import { useBrownieStore } from '@callstack/brownie';
34
import {
45
createNativeStackNavigator,
56
type NativeStackScreenProps,
67
} from '@react-navigation/native-stack';
78
import ReactNativeBrownfield from '@callstack/react-native-brownfield';
89
import { NavigationContainer } from '@react-navigation/native';
10+
import './BrownfieldStore.brownie';
911

1012
const getRandomValue = () => Math.round(Math.random() * 255);
1113
const getRandomTheme = () => {
@@ -22,10 +24,17 @@ const getRandomTheme = () => {
2224
};
2325
};
2426

25-
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
27+
type RootStackParamList = {
28+
Home: { theme: { primary: string; secondary: string } };
29+
};
2630

27-
function HomeScreen({ navigation, route }: Props) {
28-
const colors = route.params?.theme || getRandomTheme();
31+
type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
32+
33+
const theme = getRandomTheme();
34+
35+
function HomeScreen({ navigation, route }: HomeScreenProps) {
36+
const colors = route.params?.theme || theme;
37+
const [state, setState] = useBrownieStore('BrownfieldStore');
2938

3039
useEffect(() => {
3140
const unsubscribe = navigation.addListener('focus', () => {
@@ -37,10 +46,29 @@ function HomeScreen({ navigation, route }: Props) {
3746

3847
return (
3948
<View style={[styles.container, { backgroundColor: colors.primary }]}>
40-
<Text style={[styles.text, { color: colors.secondary }]}>
49+
<Text style={[styles.title, { color: colors.secondary }]}>
4150
React Native Screen
4251
</Text>
4352

53+
<Text style={[styles.text, { color: colors.secondary }]}>
54+
Count: {state.counter}
55+
</Text>
56+
57+
<TextInput
58+
style={styles.input}
59+
value={state.user.name}
60+
onChangeText={(text) =>
61+
setState((prev) => ({ user: { ...prev.user, name: text } }))
62+
}
63+
placeholder="User name"
64+
/>
65+
66+
<Button
67+
onPress={() => setState((prev) => ({ counter: prev.counter + 1 }))}
68+
color={colors.secondary}
69+
title="Increment"
70+
/>
71+
4472
<Button
4573
onPress={() => {
4674
navigation.push('Home', {
@@ -65,9 +93,6 @@ function HomeScreen({ navigation, route }: Props) {
6593
</View>
6694
);
6795
}
68-
type RootStackParamList = {
69-
Home: { theme: { primary: string; secondary: string } };
70-
};
7196

7297
const Stack = createNativeStackNavigator<RootStackParamList>();
7398

@@ -87,9 +112,22 @@ const styles = StyleSheet.create({
87112
justifyContent: 'center',
88113
alignItems: 'center',
89114
},
90-
text: {
115+
title: {
91116
fontSize: 30,
92117
fontWeight: 'bold',
93118
margin: 10,
94119
},
120+
text: {
121+
fontSize: 16,
122+
margin: 5,
123+
},
124+
input: {
125+
borderWidth: 1,
126+
borderColor: '#ccc',
127+
borderRadius: 8,
128+
padding: 10,
129+
width: 200,
130+
marginVertical: 10,
131+
backgroundColor: '#fff',
132+
},
95133
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { BrownieStore } from '@callstack/brownie';
2+
3+
interface BrownfieldStore extends BrownieStore {
4+
counter: number;
5+
user: {
6+
name: string;
7+
};
8+
}
9+
10+
interface SettingsStore extends BrownieStore {
11+
theme: 'light' | 'dark';
12+
notificationsEnabled: boolean;
13+
privacyMode: boolean;
14+
}
15+
16+
declare module '@callstack/brownie' {
17+
interface BrownieStores {
18+
BrownfieldStore: BrownfieldStore;
19+
SettingsStore: SettingsStore;
20+
}
21+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.callstack.kotlinexample
2+
3+
data class BrownfieldStore (
4+
val counter: Double,
5+
val user: User
6+
) {
7+
companion object {
8+
const val STORE_NAME = "BrownfieldStore"
9+
}
10+
}
11+
12+
data class User (
13+
val name: String
14+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.callstack.kotlinexample
2+
3+
data class SettingsStore (
4+
val notificationsEnabled: Boolean,
5+
val privacyMode: Boolean,
6+
val theme: Theme
7+
) {
8+
companion object {
9+
const val STORE_NAME = "SettingsStore"
10+
}
11+
}
12+
13+
enum class Theme {
14+
Dark,
15+
Light
16+
}
Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
21
const path = require('path');
2+
const { getDefaultConfig } = require('@react-native/metro-config'); // Import from `@expo/metro-config` if using Expo CLI
3+
const { withMetroConfig } = require('react-native-monorepo-config');
34

4-
const root = path.resolve(__dirname, '../..');
5-
6-
/**
7-
* Metro configuration
8-
* https://reactnative.dev/docs/metro
9-
*
10-
* @type {import('@react-native/metro-config').MetroConfig}
11-
*/
12-
const config = {
13-
watchFolders: [root],
14-
};
15-
16-
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
5+
module.exports = withMetroConfig(
6+
getDefaultConfig(__dirname), // Metro config to extend
7+
{
8+
root: path.resolve(__dirname, '../..'), // Path to the monorepo root
9+
dirname: __dirname, // Path to the current directory
10+
}
11+
);

apps/TesterIntegrated/package.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
"scripts": {
66
"start": "react-native start",
77
"build:tester-integrated:android": "cd kotlin && ./gradlew assembleDebug",
8-
"build:tester-integrated:ios": "cd swift && xcodebuild -workspace SwiftExample.xcworkspace -scheme SwiftExample -configuration Release -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO -derivedDataPath ./build"
8+
"build:tester-integrated:ios": "cd swift && xcodebuild -workspace SwiftExample.xcworkspace -scheme SwiftExample -configuration Release -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO -derivedDataPath ./build",
9+
"codegen": "brownie codegen"
910
},
1011
"dependencies": {
12+
"@callstack/brownie": "*",
1113
"@callstack/react-native-brownfield": "*",
14+
"@react-navigation/native": "^7.0.15",
15+
"@react-navigation/native-stack": "^7.2.1",
1216
"react": "19.1.1",
13-
"react-native": "0.82.1"
17+
"react-native": "0.82.1",
18+
"react-native-safe-area-context": "^5.3.0",
19+
"react-native-screens": "^4.15.2"
1420
},
1521
"devDependencies": {
1622
"@babel/core": "^7.25.2",
@@ -22,12 +28,15 @@
2228
"@react-native/babel-preset": "0.82.1",
2329
"@react-native/metro-config": "0.82.1",
2430
"@react-native/typescript-config": "0.82.1",
25-
"@react-navigation/native": "^7.0.15",
26-
"@react-navigation/native-stack": "^7.2.1",
27-
"react-native-safe-area-context": "^5.3.0",
28-
"react-native-screens": "^4.15.2"
31+
"@types/react": "^19.1.1",
32+
"react-native-monorepo-config": "^0.3.1"
2933
},
3034
"engines": {
3135
"node": ">=20"
36+
},
37+
"brownie": {
38+
"swift": "./swift/Generated",
39+
"kotlin": "./kotlin/app/src/main/java/com/callstack/kotlinexample/Generated",
40+
"kotlinPackageName": "com.callstack.kotlinexample"
3241
}
3342
}

apps/TesterIntegrated/react-native.config.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const path = require('path');
2-
const pkg = require('../../packages/react-native-brownfield/package.json');
2+
const brownfieldPkg = require('../../packages/react-native-brownfield/package.json');
3+
const browniePkg = require('../../packages/brownie/package.json');
34

45
module.exports = {
56
project: {
@@ -11,12 +12,19 @@ module.exports = {
1112
},
1213
},
1314
dependencies: {
14-
[pkg.name]: {
15+
[brownfieldPkg.name]: {
1516
root: path.join(__dirname, '../../packages/react-native-brownfield'),
1617
platforms: {
1718
ios: {},
1819
android: {},
1920
},
2021
},
22+
[browniePkg.name]: {
23+
root: path.join(__dirname, '../../packages/brownie'),
24+
platforms: {
25+
ios: {},
26+
android: null,
27+
},
28+
},
2129
},
2230
};
Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,120 @@
1+
import Brownie
12
import ReactBrownfield
23
import SwiftUI
34

5+
let initialState = BrownfieldStore(
6+
counter: 0,
7+
user: User(name: "Username")
8+
)
9+
10+
/*
11+
Toggles testing playground for side by side brownie mode.
12+
Default: false
13+
*/
14+
let isSideBySideMode = false
15+
416
@main
517
struct MyApp: App {
6-
init() {
7-
ReactNativeBrownfield.shared.startReactNative {
8-
print("loaded")
9-
}
18+
init() {
19+
ReactNativeBrownfield.shared.startReactNative {
20+
print("loaded")
21+
}
22+
23+
_ = Store(initialState, key: BrownfieldStore.storeName)
24+
}
25+
26+
var body: some Scene {
27+
WindowGroup {
28+
ContentView()
29+
}
30+
}
31+
32+
struct ContentView: View {
33+
var body: some View {
34+
if isSideBySideMode {
35+
SideBySideView()
36+
} else {
37+
FullScreenView()
38+
}
39+
}
40+
}
41+
42+
struct SideBySideView: View {
43+
var body: some View {
44+
VStack(spacing: 0) {
45+
NativeView()
46+
.frame(maxHeight: .infinity)
47+
48+
Divider()
49+
50+
ReactNativeView(moduleName: "ReactNative")
51+
.frame(maxHeight: .infinity)
52+
}
1053
}
54+
}
1155

12-
var body: some Scene {
13-
WindowGroup {
14-
ContentView()
56+
struct FullScreenView: View {
57+
@UseStore<BrownfieldStore> var store
58+
59+
var body: some View {
60+
NavigationView {
61+
VStack {
62+
Text("React Native Brownfield App")
63+
.font(.title)
64+
.bold()
65+
.padding()
66+
.multilineTextAlignment(.center)
67+
68+
Text("Count: \(Int(store.state.counter))")
69+
70+
TextField("Name", text: Binding(get: { store.state.user.name }, set: { data in
71+
store.set { $0.user.name = data }
72+
}))
73+
.textFieldStyle(.roundedBorder)
74+
.padding(.horizontal)
75+
76+
Button("Increment") {
77+
store.set { $0.counter += 1 }
78+
}
79+
.buttonStyle(.borderedProminent)
80+
.padding(.bottom)
81+
82+
NavigationLink("Push React Native Screen") {
83+
ReactNativeView(moduleName: "ReactNative")
84+
.navigationBarHidden(true)
85+
}
1586
}
87+
}.navigationViewStyle(StackNavigationViewStyle())
1688
}
17-
}
89+
}
90+
91+
struct NativeView: View {
92+
@UseStore<BrownfieldStore> var store
1893

19-
struct ContentView: View {
2094
var body: some View {
21-
NavigationView {
22-
VStack {
23-
Text("React Native Brownfield App")
24-
.font(.title)
25-
.bold()
26-
.padding()
27-
28-
NavigationLink("Push React Native Screen") {
29-
ReactNativeView(moduleName: "ReactNative")
30-
.navigationBarHidden(true)
31-
}
32-
}
33-
}.navigationViewStyle(StackNavigationViewStyle())
95+
VStack {
96+
Text("Native Side")
97+
.font(.headline)
98+
.padding(.top)
99+
100+
Text("User: \(store.state.user.name)")
101+
Text("Count: \(Int(store.state.counter))")
102+
103+
TextField("Name", text: Binding(get: { store.state.user.name }, set: { data in
104+
store.set { $0.user.name = data }
105+
}))
106+
.textFieldStyle(.roundedBorder)
107+
.padding(.horizontal)
108+
109+
Button("Increment") {
110+
store.set { $0.counter += 1 }
111+
}
112+
.buttonStyle(.borderedProminent)
113+
114+
Spacer()
115+
}
116+
.frame(maxWidth: .infinity)
117+
.background(Color(.systemBackground))
34118
}
119+
}
35120
}

0 commit comments

Comments
 (0)