Erstellen einer realen React Native-App von Grund auf mit Expo und Metricalp (2024)
Wir wissen, dass Sie nicht gerne Dokumentationen und Boilerplates lesen. Daher haben wir ein echtes Beispiel für Sie vorbereitet. In diesem Beitrag erstellen wir eine React Native-App von Grund auf mit Expo und Metricalp.
Einführung
Wir werden eine ToDo-Anwendung in React Native entwickeln. Der Benutzer kann ToDos hinzufügen, aktualisieren und löschen. Aber während wir dies als eine echte Weltanwendung betrachten, werden wir einige nervige Anzeigen auf dem Bildschirm haben 😬. Der Benutzer kann auf ein PRO-Konto upgraden, um die Anzeigen loszuwerden. Daher wird unsere App einen zweiten Bildschirm /upgrade-pro Bildschirm haben. Auf diesem Bildschirm wird es einen Button zum Upgrade auf PRO geben. Wir werden Metricalp verwenden, um das Benutzerverhalten zu verfolgen und unsere App zu analysieren. Glaub mir, es wird Spaß machen.
Um es wie eine echte Weltanwendung zu machen, werden wir in unserer App einige großartige Bibliotheken verwenden, wie zum Beispiel den Zustand Store (zur Verwaltung des Zustands) und MMKV Store (um Daten auf sehr schnelle Weise im lokalen Speicher zu speichern). Hier einige Screenshots aus unserer App:
Okay, genug geredet, lasst uns loslegen und es bauen 🚀
Projekt Setup
Zuerst müssen wir ein neues React Native Projekt mit Expo erstellen:
npx create-expo-app@latest
In diesem Beispiel werden wir hauptsächlich den iOS-Simulator verwenden. Daher müssen Sie Xcode auf Ihrem Computer installiert haben. Wenn Sie Xcode nicht haben, können Sie es im App Store installieren. Außerdem werden wir Watchman installieren, um im Simulator richtig zu arbeiten.
brew update
brew install watchman
Dann installieren wir den Expo Dev Client, um mit unserer App im Simulator zu arbeiten:
npx expo install expo-dev-client
Stellen Sie sicher, dass Sie die neueste Version des Simulators installiert haben. Sie können dies unter XCode -> Preferences -> Components überprüfen.
Starten wir unsere App zum ersten Mal, innerhalb des neuen Anwendungsordners ausführen:
npx expo run:ios
Bibliotheken installieren
Jetzt werden wir einige Bibliotheken installieren, um unser Leben zu erleichtern. Zuerst installieren wir die Bibliothek react-native-mmkv Store von mrousavy. Sie basiert auf der neuen Architektur von React Native, sodass Sie synchron und schnell auf den lokalen Speicher zugreifen können:
npx expo install react-native-mmkv
An dieser Stelle müssen wir auch Expo prebuild ausführen
npx expo prebuild
Um unsere Benutzer und ihre ToDos zu verfolgen, verwenden wir zufällig generierte UUIDs. Wir werden die Crypto-Bibliothek von Expo installieren, um diese UUIDs zu generieren:
npx expo install expo-crypto
Wir werden die ToDos im Zustand Store (einer Zustandsverwaltungslibrary) speichern, also müssen wir sie installieren:
yarn add zustand
Schließlich werden wir Metricalp installieren, um das Benutzerverhalten in unserer App zu verfolgen (während wir diesen Artikel schreiben, ist die neueste Version 1.0.12, aber immer die neueste Version installieren):
yarn add @metricalp/react-native
App Struktur
Hier ist die finale App-Struktur. Wir werden alle verwendeten Dateien unten erklären:
Beginnen wir mit einigen Grundlagen. Erstellen Sie zuerst einen Ordner mit dem Namen 'utils' im Projektstamm.
Erstellen Sie dann eine Datei namens zustand-store.ts im utils Ordner. Diese Datei wird verwendet, um unseren Zustand in der App zu verwalten:
import { unstable_batchedUpdates } from 'react-native'; // or 'react-native'
import { create } from 'zustand';
import * as Crypto from 'expo-crypto';
import { storage } from './mmkv-store';
interface Todo {
id: string;
text: string;
}
interface ZustandStore {
todos: Todo[];
}
export const useZustandStore = create<ZustandStore>((set) => ({
todos: [],
}));
export const setTodos = (todos: Todo[]) => {
unstable_batchedUpdates(() => {
useZustandStore.setState({ todos });
});
storage.set('USER_TODOS_KEY', JSON.stringify(todos));
};
export const updateTodo = (id: string, text: string) => {
const todos = useZustandStore.getState().todos;
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, text };
}
return todo;
});
setTodos(updatedTodos);
};
export const addTodo = (text: string) => {
const todos = useZustandStore.getState().todos;
const newTodo = { id: Crypto.randomUUID(), text };
setTodos([...todos, newTodo]);
};
export const deleteTodo = (id: string) => {
const todos = useZustandStore.getState().todos;
const updatedTodos = todos.filter((todo) => todo.id !== id);
setTodos(updatedTodos);
};
export const resetTodos = () => {
setTodos([]);
};
Grundsätzlich haben wir einen Zustand Store mit ToDos definiert. Wir haben einige Funktionen zum Hinzufügen, Aktualisieren, Löschen und Zurücksetzen von ToDos. Wir verwenden mmkv-store, um ToDos im lokalen Speicher zu speichern. Wir aktualisieren auch diese lokale Speicher Kopie bei jeder Aktualisierung des Zustands. Wir generieren zufällige UUIDs für jedes neue ToDo.
Jetzt erstellen wir eine Datei namens mmkv-store.ts im utils Ordner:
import { MMKV } from 'react-native-mmkv';
export const storage = new MMKV();
Jetzt erstellen wir eine Datei namens constants.ts im utils Ordner. Wir speichern hier Konstanten:
export const METR_UUID_KEY = 'metr.uuid';
export const USER_TODOS_KEY = 'user.todos';
export const METRICALP_TID = 'mam48'; // Ersetze durch dein Metricalp TID
export const SCREENS = {
INDEX: 'index',
UPGRADE_PRO: 'upgrade-pro',
};
Zum Schluss erstellen wir eine Datei namens metricalp-events.ts im utils Ordner. Wir speichern unsere benutzerdefinierten Ereignisse für Metricalp in dieser Datei:
export const MetricalpCustomEvents = {
UPGRADE_PRO_NAVIGATOR: 'upgrade_pro_navigator',
TODO_DELETE_CLICK: 'todo_delte_click',
TODO_EDIT_CLICK: 'todo_edit_click',
ADD_TODO_CLICK: 'add_todo_click',
UPDATE_TODO_CLICK: 'update_todo_click',
UPGRADE_PRO_CLICK: 'upgrade_pro_click',
};
Wir werden die Edit- und Delete-Klicks der ToDos, den Klick auf den Button zum Hinzufügen von ToDos, den Klick zum Aktualisieren von ToDos und den Klick zum Upgrade auf PRO in unserer App verfolgen. Außerdem möchten wir verfolgen, wie der Benutzer zum Upgrade-Bildschirm navigiert hat, daher haben wir ein weiteres Ereignis upgrade_pro_navigator, das jedes Mal ausgelöst wird, wenn ein Benutzer zum Upgrade-Bildschirm navigiert. Wir fügen diesem Ereignis eine Eigenschaft hinzu, um festzuhalten, wie der Benutzer zum Upgrade gelangt ist. Außerdem speichern wir, wie viele ToDos der Benutzer hatte, als er sich für das Upgrade entschieden hat. Denn dies kann eine wichtige Metrik sein (das Marketingteam hat das so gesagt). Außerdem möchten wir die Tageszeit (Morgen, Nachmittag, Abend oder Nacht) für alle diese Ereignisse speichern. Wir wollen das Benutzerverhalten verstehen, vielleicht neigen sie dazu, die App nachts zu verwenden, haben aber mehr Neigung zum Upgrade am Morgen? Auch hier hat das Marketingteam nach diesen Daten gefragt. In Metricalp können wir alles behalten, wir können unbegrenzte Szenarien haben 🤝
Nun, lassen Sie uns die Datei app/_layout.tsx überprüfen, die unsere Hauptlayout-Datei ist, hier initialisieren wir Metricalp und unsere App:
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack, usePathname } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useRef, useState } from 'react';
import 'react-native-reanimated';
import * as Crypto from 'expo-crypto';
import { useColorScheme } from '@/hooks/useColorScheme';
import { storage } from '@/utils/mmkv-store';
import {
METRICALP_TID,
METR_UUID_KEY,
USER_TODOS_KEY,
} from '@/utils/constants';
import { Metricalp } from '@metricalp/react-native';
import { setTodos } from '@/utils/zustand-store';
import { AppState, NativeEventSubscription, Platform } from 'react-native';
// Verhindert, dass der Splash Screen automatisch ausgeblendet wird, bevor das Laden der Assets abgeschlossen ist.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
const [initializationCompleted, setInitializationCompleted] = useState(false);
const pathname = usePathname();
const lastPathnameRef = useRef<string>('/');
lastPathnameRef.current = pathname;
useEffect(() => {
let subscription: NativeEventSubscription;
let userUUID = storage.getString(METR_UUID_KEY);
if (!userUUID) {
userUUID = Crypto.randomUUID();
storage.set(METR_UUID_KEY, userUUID);
}
let todos = [];
const userTODOsFromMMKVStore = storage.getString(USER_TODOS_KEY);
if (userTODOsFromMMKVStore) {
todos = JSON.parse(userTODOsFromMMKVStore);
}
setTodos(todos);
const osWithVersion =
(Platform.OS === 'ios' ? 'iOS' : 'Android') + ' ' + Platform.Version;
Metricalp.init({
platform: Platform.OS,
app: '[email protected]',
language: 'German-DE',
os: osWithVersion,
uuid: userUUID,
tid: METRICALP_TID,
});
setInitializationCompleted(true);
subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
Metricalp.appLeaveEvent();
} else if (nextAppState === 'active') {
Metricalp.screenViewEvent(lastPathnameRef.current);
}
});
return () => {
subscription?.remove();
};
}, []);
useEffect(() => {
if (!initializationCompleted) {
return;
}
Metricalp.screenViewEvent(pathname);
}, [pathname, initializationCompleted]);
useEffect(() => {
if (loaded && initializationCompleted) {
SplashScreen.hideAsync();
}
}, [loaded, initializationCompleted]);
if (!loaded || !initializationCompleted) {
return null;
}
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</ThemeProvider>
);
}
Wir initialisieren unsere ToDos zuerst aus dem lokalen Speicher (mmkv store). Dann initialisieren wir Metricalp mit einigen Basisinformationen wie Plattform, App-Name, Sprache, Betriebssystem, UUID und TID. Wir verfolgen auch App-Zustandsänderungen, um zu verstehen, wann die App in den Hintergrund oder den Vordergrund geht. Wir verfolgen auch Bildschirmansichten in unserer App und verwenden den Hook usePathname, um den aktuellen Pfadnamen unserer App zu erhalten. Wir blenden den Splash Screen aus, wenn alle diese Initialisierungen abgeschlossen sind.
Wir generieren eine Benutzer-UUID pro Benutzer und speichern diese im lokalen Speicher. Metricalp verwendet diese UUID, um den Benutzer zu identifizieren und eindeutige Ansichten/Ereignisse zu zählen. Sie können auch user_id usw. verwenden, wenn Sie möchten.
Wenn die App in den Hintergrund oder in den Inaktivitätsmodus wechselt, generieren wir appLeaveEvent-Ereignisse aus der Metricalp-Bibliothek. Es hilft uns, Bildschirmansichtsdauern usw. zu erfassen.
Wir haben einen benutzerdefinierten Hook useDayTime im hooks Ordner erstellt (hooks/useDayTime.ts), um die Tageszeit des Benutzers zu erhalten:
export function useDayTime() {
const now = new Date();
const hours = now.getHours();
if (hours >= 6 && hours < 12) {
return 'Morgen';
}
if (hours >= 12 && hours < 18) {
return 'Nachmittag';
}
if (hours >= 18 && hours < 24) {
return 'Abend';
}
return 'Nacht';
}
Bearbeiten wir nun die Datei (tabs)/_layout.tsx, um ein Upgrade-Tab-Menü zu haben und das upgrade_pro_navigator-Ereignis auszulösen, wenn auf dieses Tab-Menü geklickt wird:
import { Tabs } from 'expo-router';
import React from 'react';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { SCREENS } from '@/utils/constants';
import { Metricalp } from '@metricalp/react-native';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useZustandStore } from '@/utils/zustand-store';
import { useDayTime } from '@/hooks/useDayTime';
export default function TabLayout() {
const colorScheme = useColorScheme();
const dayTime = useDayTime();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
}}
screenListeners={{
tabPress: (e) => {
if (e.target?.startsWith(SCREENS.UPGRADE_PRO)) {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_NAVIGATOR, {
day_time: dayTime,
placement: 'bottom-tab',
current_todos_count: useZustandStore.getState().todos.length,
});
}
},
}}
>
<Tabs.Screen
name={SCREENS.INDEX}
options={{
title: 'Startseite',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? 'home' : 'home-outline'}
color={color}
/>
),
}}
/>
<Tabs.Screen
name={SCREENS.UPGRADE_PRO}
options={{
title: 'Upgrade Pro',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? 'arrow-up-circle' : 'arrow-up-circle-outline'}
color={color}
/>
),
}}
/>
</Tabs>
);
}
Beachten Sie, dass wir dem benutzerdefinierten Ereignis auch die Eigenschaften placement, current_todos_count und day_time anfügen. Wie einfach und großartig, oder?
Bearbeiten Sie nun die Datei (tabs)/index.tsx, um unseren Hauptbildschirm der App zu erstellen:
import { SCREENS } from '@/utils/constants';
import {
addTodo,
deleteTodo,
updateTodo,
useZustandStore,
} from '@/utils/zustand-store';
import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
import { Metricalp } from '@metricalp/react-native';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useDayTime } from '@/hooks/useDayTime';
export default function HomeScreen() {
const todos = useZustandStore((state) => state.todos);
const [todoInput, setTodoInput] = useState('');
const [editId, setEditId] = useState('');
const dayTime = useDayTime();
const router = useRouter();
const handleAddTask = () => {
if (editId) {
Metricalp.customEvent(MetricalpCustomEvents.UPDATE_TODO_CLICK, {
day_time: dayTime,
});
updateTodo(editId, todoInput);
} else {
Metricalp.customEvent(MetricalpCustomEvents.ADD_TODO_CLICK, {
day_time: dayTime,
});
addTodo(todoInput);
}
setEditId('');
setTodoInput('');
};
const handleEditTask = (id: string) => {
const taskToEdit = todos.find((todo) => todo.id === id);
Metricalp.customEvent(MetricalpCustomEvents.TODO_EDIT_CLICK, {
day_time: dayTime,
});
setTodoInput(taskToEdit?.text || '');
setEditId(id);
};
const handleDeleteTask = (id: string) => {
Metricalp.customEvent(MetricalpCustomEvents.TODO_DELETE_CLICK, {
day_time: dayTime,
});
deleteTodo(id);
};
const renderItem = ({ item }: any) => (
<View style={styles.task}>
<Text style={styles.itemList}>{item.text}</Text>
<View style={styles.taskButtons}>
<TouchableOpacity onPress={() => handleEditTask(item.id)}>
<Text style={styles.editButton}>Bearbeiten</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleDeleteTask(item.id)}>
<Text style={styles.deleteButton}>Löschen</Text>
</TouchableOpacity>
</View>
</View>
);
const renderAds = (adsPlace: 'top' | 'bottom') => {
return (
<>
<View style={styles.adsContainer}>
<Text style={styles.adsText}>Hier sind einige Anzeigen</Text>
</View>
<TouchableOpacity
style={styles.upgradeButton}
onPress={() => {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_NAVIGATOR, {
placement: adsPlace,
day_time: dayTime,
current_todos_count: todos.length,
});
router.push(SCREENS.UPGRADE_PRO);
}}
>
<Text style={styles.upgradeButtonText}>
Upgrade auf Pro für eine werbefreie Erfahrung
</Text>
</TouchableOpacity>
</>
);
};
return (
<View style={styles.container}>
<Text style={styles.title}>ToDo Listen App</Text>
{renderAds('top')}
<TextInput
style={styles.input}
placeholder="Geben Sie ein Todo ein"
value={todoInput}
onChangeText={(text) => setTodoInput(text)}
/>
<TouchableOpacity style={styles.addButton} onPress={handleAddTask}>
<Text style={styles.addButtonText}>
{editId ? 'Todo aktualisieren' : 'Todo hinzufügen'}
</Text>
</TouchableOpacity>
<FlatList
data={todos}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
{renderAds('bottom')}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40,
marginTop: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
heading: {
fontSize: 30,
fontWeight: 'bold',
marginBottom: 7,
color: 'green',
},
adsContainer: {
borderWidth: 2,
borderColor: 'purple',
marginTop: 10,
padding: 5,
borderRadius: 10,
},
adsText: {
fontSize: 14,
fontWeight: 'bold',
color: 'purple',
},
input: {
borderWidth: 3,
borderColor: '#ccc',
padding: 10,
marginBottom: 10,
marginTop: 20,
borderRadius: 10,
fontSize: 18,
},
addButton: {
backgroundColor: 'green',
padding: 10,
borderRadius: 5,
marginBottom: 10,
},
addButtonText: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
fontSize: 18,
},
task: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
fontSize: 18,
},
itemList: {
fontSize: 19,
},
taskButtons: {
flexDirection: 'row',
},
editButton: {
marginRight: 10,
color: 'green',
fontWeight: 'bold',
fontSize: 18,
},
deleteButton: {
color: 'red',
fontWeight: 'bold',
fontSize: 18,
},
upgradeButton: {
backgroundColor: 'purple',
padding: 10,
marginVertical: 10,
borderRadius: 5,
marginBottom: 10,
},
upgradeButtonText: {
color: 'white',
},
});
Wir haben also eine Standard-ToDo-App. Wir können Todos hinzufügen/bearbeiten/löschen. Wir haben nervige Anzeigen und Upgrade-Schaltflächen neben den Anzeigen.
Wir verfolgen jeden Klick zum Hinzufügen/Bearbeiten/Löschen über Metricalp und fügen jedem dieser Ereignisse die day_time-Eigenschaft hinzu.
Aber wir fügen dem upgrade_pro_navigator-Ereignis auch die Eigenschaften placement und current_todos_count hinzu. Denn sie sind aus marketingtechnischer Sicht für uns wichtig.
Erstellen Sie abschließend die Datei (tabs)/upgrade-pro.tsx, um unseren Upgrade-Pro-Bildschirm zu erstellen:
import { useDayTime } from '@/hooks/useDayTime';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useZustandStore } from '@/utils/zustand-store';
import { Metricalp } from '@metricalp/react-native';
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
export default function UpgradeProScreen() {
const dayTime = useDayTime();
return (
<View style={styles.container}>
<Text style={styles.title}>ToDo Listen App</Text>
<Text>
Sie werden keine Werbung mehr in der Anwendung sehen, wenn Sie auf PRO upgraden.
</Text>
<TouchableOpacity
style={styles.upgradeButton}
onPress={() => {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_CLICK, {
day_time: dayTime,
current_todos_count: useZustandStore.getState().todos.length,
});
alert('Jetzt auf Pro aktualisiert');
}}
>
<Text style={styles.upgradeButtonText}>Jetzt auf Pro upgraden</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40,
marginTop: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
upgradeButton: {
backgroundColor: 'purple',
padding: 10,
marginVertical: 20,
borderRadius: 5,
marginBottom: 10,
},
upgradeButtonText: {
color: 'white',
},
});
Hier erzeugen wir ein benutzerdefiniertes Ereignis upgrade_pro_click, wenn der Benutzer auf die Upgrade-Schaltfläche klickt. Wir fügen diesem Ereignis auch die Eigenschaften day_time und current_todos_count hinzu. Wir zeigen dem Benutzer auch eine Benachrichtigung darüber an, dass er aufgerüstet ist.
current_todos_count kann wichtig sein, weil wir messen können, wann ein Benutzer entscheidet, zu upgraden, je nachdem, wie viele Todos er hat. Vielleicht können wir Benutzern, die mehr als 10 Todos haben, einen Rabatt anzeigen? 🤔 Sieh, wie Metricalp hilft, Marketingaktionen zu treffen.
Okay, wir sind fast fertig, aber wir müssen diese benutzerdefinierten Eigenschaften zu benutzerdefinierten Ereignissen im Metricalp-Dashboard definieren. Gehen wir zu den Tracker-Einstellungen im Metricalp-Dashboard, um dies zu tun:
Hier sind alle Ereignisse:
Lassen Sie uns benutzerdefinierte Eigenschaften an diese Ereignisse anhängen:
Nun, wir haben alle unsere benutzerdefinierten Ereignisse hinzugefügt. Wir haben 'day_time' als custom_prop1-Alias zu allen Ereignissen hinzugefügt. Wir haben auch placement als custom_prop2 und current_todos_count als custom_prop3 zum upgrade_pro_navigator-Ereignis hinzugefügt. Wir haben auch current_todos_count zum upgrade_pro_click-Ereignis als custom_prop2 hinzugefügt. Wir sind bereit 🚀
Schauen wir uns unser Dashboard an:
Nun, wir haben screen_view-Ereignisse mit Informationen zu Standort, Pfad, Benutzersprache, Betriebssystemversion und App-Version, aber zusätzlich haben wir benutzerdefinierte Ereignisse für Todo-Hinzufügen/Update/Löschen und Upgrade-Aktionen. Wir haben Tageszeitinformationen, Informationen zur aktuellen Todo-Anzahl und Informationen zur Platzierung des Upgrade-Navigators. Das ist großartig. Wir sammeln sehr wertvolle Daten wie bei einem echten A/B-Test. Wir können entscheiden, den Upgrade-Button beispielsweise Nutzern anzuzeigen, die mehr als 10 Todos haben, und wir können entscheiden, Rabatte in der Nachtzeit anzuzeigen. Wir können die untere Upgrade-Navigationsschaltfläche entfernen und nur die obere behalten. Ich meine, Daten werden unsere Marketingrichtung bestimmen, wie in einem echten Geschäft
Nun, alles ist einfach mit diesen leistungsstarken Technologien wie React Native, Expo und natürlich Metricalp 💜. Danke, wenn du bis hier gelesen hast, zögere nicht, uns zu kontaktieren, wenn du welche hast 🤝