Ausnahme gefangen: SSL certificate problem: certificate is not yet valid 📌 WhatsApp UI in React Native (part 1)

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 WhatsApp UI in React Native (part 1)


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

Billion of us rely on WhatsApp every day.

WhatsApp's simple interface is a a huge part of their success.

But how hard is it to build WhatsApp's UI in React Native?

Let's find out!

Preview of the app

You can see the full code for this project here

In part two, I will complete more features of the UI.

Setup

I'm using Expo for this project.
I recommend checking out the Expo docs but you should follow something like the below:

$ npm install --global expo-cli && npx create-expo-$app my-app
$ cd my-app
$ npx expo install react-dom react-native-web @expo/webpack-config
$ npx expo start

Screens & components

Screens

We will have two main screens, and two placeholder screens.

  • screens/Chats/Chats.tsx - this is a list of Chat conversations. I put it in a folder along with its styling file, Chats.styles.ts
  • screens/Chat/Chat.tsx - this is the screen for a conversation. I put it in a folder along with its styling file, Chat.styles.ts
  • screens/Status.tsx - this is a placeholder for the status tab - we won't work on it today
  • screens/Calls.tsx - this is a placeholder for the status tab - we won't work on it today

Components

  • components/ChatMessages/ChatMessages.tsx - this is the list of all the chat messages in any given chat conversation
  • components/ConversationPreview/ConversationPreview.tsx - This is the preview of a message that you see in the list of all your chat conversations
  • components/MessageBubble/MessageBubble.tsx - this is an individual message within a chat conversation
  • components/SendButton/SendButton.tsx - This is the whole section where you send a message: including the text input and the actual button to send the message.

Navigation - Chats / Status / Calls

The central navigation feature for Whatsapp is navigating between Chats and Calls (+ Status but who uses that?!)

Image description

To get these cool tabs, we can use:

import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";

This gets us most of the way without any styling.

Image description

But before we start styling, we still need to think about navigation between screens.

For example, we need a list of all the chat conversations and then we need to be able to navigate to that specific chat conversation.

Image description

But we don't want each chat conversations to be in their own tab. They need to be within the chat conversations tab.

Image description

And we need to be able to see other chats with the back button.

Image description

So we need another StackNavigator.

The initial way I thought about structuring the navigation was having a navigator nested within each of the tabs like this:

TopTabsNavigator
-----ChatNavigator
------------ChatMessage
-----StatusNavigator
-----CallsNavigator

But then you end up seeing your Top Tabs at every level. And we only want them on that home screen.

So instead we can create a StackNavigator at the root and render our TopTabNavigator as the default Screen within it.

Then within our TopTabsNavigator, the default Screen is our Chats screen.

One potential point of confusion

One thing that could be confusing is that our Conversation Chat screen is within our Root Navigator.

That's because when someone clicks on a conversation preview in our Chats list, they are navigating from one screen in the Root stack (Chats in TopTabsNavigator), to another screen in the Root stack.

Finally, our RootNavigator is wrapped in a NavigationContainer and this is exported as a Navigation component so we can render it inside App.tsx.

// navigation/index.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as React from "react";

import TopTabNavigator from "./TopTabNavigator";
import Chat from "../screens/Chat/Chat";
import CallsScreen from "../screens/Calls";
import StatusScreen from "../screens/Status";
import ModalScreen from "../screens/ModalScreen";
import { RootStackParamList } from "../types";

export default function Navigation() {
  return (
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  );
}
const Stack = createNativeStackNavigator<RootStackParamList>();

function RootNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Root" component={TopTabNavigator} />
      <Stack.Screen name="Chat" component={Chat} />
      <Stack.Group screenOptions={{ presentation: "modal" }}>
        <Stack.Screen name="Modal" component={ModalScreen} />
      </Stack.Group>
    </Stack.Navigator>
  );
}

// navigation/TopTabNavigator.tsx
import * as React from "react";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";

import ChatsScreen from "../screens/Chats/Chats";
import StatusScreen from "../screens/Status";

import { RootTabParamList, RootTabScreenProps } from "../types";

const TopTab = createMaterialTopTabNavigator<RootTabParamList>();
export default function TopTabNavigator() {
  return (
    <TopTab.Navigator tabBarPosition="top" initialRouteName="Chats">
      <TopTab.Screen
        name="Chats"
        component={ChatsScreen}
        options={({ navigation }: RootTabScreenProps<"Chats">) => ({
          title: "Chats",
        })}
      />
      <TopTab.Screen
        name="Status"
        component={StatusScreen}
        options={{
          title: "Status",
        }}
      />
      <TopTab.Screen
        name="Calls"
        component={CallsScreen}
        options={{
          title: "Calls",
        }}
      />
    </TopTab.Navigator>
  );
}

In our App.tsx file, we want to return a Navigation component like this:

// App.tsx
import Navigation from "./navigation";

export default function App() {
  return (
    <SafeAreaProvider>
      <Navigation />
    </SafeAreaProvider>
  );
}

To style the navigation, you can play around with screenOptions to get the WhatsApp-y look.

I used the following options, to get it looking pretty close.

    <TopTab.Navigator
      tabBarPosition="top"
      initialRouteName="Chats"
      screenOptions={{
        tabBarIndicatorStyle: {
          backgroundColor: Colors.light.white,
        },
        tabBarInactiveTintColor: Colors.light.inactiveGreen,
        tabBarActiveTintColor: Colors.light.white,
        tabBarLabelStyle: { fontSize: 16, fontWeight: "bold" },
        tabBarStyle: {
          backgroundColor: Colors.light.darkGreen,
        },
      }}
    >

Image description

We can also add some options to the "Root" screen to make the header look a bit more WhatsApp-y too.

      <Stack.Screen
        name="Root"
        component={TopTabNavigator}
        options={{
          headerShown: true,
          headerTitle: "WhatsApp",
          headerStyle: {
            backgroundColor: Colors.light.darkGreen,
          },
          headerTitleStyle: {
            fontWeight: "bold",
            color: Colors.light.white,
            fontSize: 20,
          },
          headerTitleAlign: "left",
        }}
      />

Image description

Rough Data structure of Whatsapp

This is mostly a frontend exercise but we need some kind of simple data.

I laid mine out like this:

export type Conversation = {
  id: string; // a unique id of conversation
  messages: Message[]; // messages sent
  users: number[]; // ids of users in the conversation
  title: string; e.g. "Nick Cage" or "30th birthday"
};

export type Message = {
  text: string; // message contents
  time: Date; // timestamp of send time
  userID: number; // userID of sender
  id: string; // unique of of message
};

You can see an example of fake data I generated here, I saved it in data/messages

State management

I found it a lot easier to build this with some global state management.

To do this I used react's context to manage:

  • conversations - (also includes the messages)
  • sendMessage - this adds messages to the conversation and sorts them
  • setCurrentConversation - this keeps track of which conversation you pressed on
  • getCurrentConversation - this tells you which conversation to load

To build this I set up a ConversationsProvider that contains the values above that I need globally

export const ConversationsContext = createContext<ConversationsContextType>({
  conversations: startingConversations,
  sendMessage: () => {},
  getCurrentConversation: () => {
    return { id: "", messages: [], users: [], title: "" };
  },
  setCurrentConversation: (id) => {},
});

export const ConversationsProvider = ({ children }) => {
  const [conversations, setConversations] = useState<ConversationType[]>(
    sortConversations(startingConversations)
  );
  const [currConversation, setCurrConversation] = useState<ConversationType>();

  const getCurrentConversation = () => {
    if (currConversation) return currConversation;
    else return { id: "", messages: [], users: [], title: "" };
  };

  const setCurrentConversation = (id: string) => {
    const currentConvo = conversations.filter((conv) => conv.id === id)[0];
    setCurrConversation(currentConvo);
  };

  function sendMessage(
    newMsg: string,
    thisConversationID: string,
    userID: number,
    setNewMsg: (msg: string) => void,
    isTyping: boolean,
    setIsTyping: (isTyping: boolean) => void
  ) {
    if (isTyping) {
      setNewMsg("");
      setIsTyping(false);
      if (currConversation && currConversation.id === thisConversationID) {
        setCurrConversation((prevConvo: ConversationType) => {
          return {
            ...prevConvo,
            messages: [...prevConvo.messages, formatMessage(newMsg, userID)],
          };
        });
      }
      setConversations((previousConversations: ConversationType[]) => {
        const allConversations = previousConversations.map(
          (conversation: ConversationType) => {
            if (conversation.id === thisConversationID) {
              return {
                ...conversation,
                messages: [
                  ...conversation.messages,
                  formatMessage(newMsg, userID),
                ],
              };
            }
            return conversation;
          }
        );
        return sortConversations(allConversations);
      });
    }
  }
  return (
    <ConversationsContext.Provider
      value={{
        conversations,
        sendMessage,
        getCurrentConversation,
        setCurrentConversation,
      }}
    >
      {children}
    </ConversationsContext.Provider>
  );
};

Sending a message

When a message is sent, its appended on to the end of the messages array within the relevant conversation.

It also clears the state of the message box so it becomes empty again.

sorting conversations

I wrote a function to sort conversations and I run it every time a message is sent

const sortConversations = (conversations: ConversationType[]) => {
  return conversations.sort((a, b) => {
    const lastMessageA = a.messages[a.messages.length - 1];
    const lastMessageB = b.messages[b.messages.length - 1];
    return lastMessageB.time.getTime() - lastMessageA.time.getTime();
  });
};

Accessing the state

To access this global state, I wrap the rest of my app in the ConversationsProvider

export default function App() {
  return (
    <ConversationsProvider>
      <SafeAreaProvider>
        <Navigation />
      </SafeAreaProvider>
    </ConversationsProvider>
  );
}

And then I can access this state with:

const { conversations, sendMessage } = useContext(ConversationsContext);

List of chat conversations

Our chat conversations will work great as a FlatList. But instead of FlatList we're doing to use FlashList from Shopify, which is said to have better performance than FlatList (but I've not verified this).

// screens/Chats/Chats.tsx
import { View } from "react-native";
import { FlashList } from "@shopify/flash-list";

import ConversationPreview from "../../components/ConversationPreview/ConversationPreview";
import { Conversation, RootTabScreenProps } from "../../types";
import conversations from "../../data/messages";
import styles from "./Chats.styles";

interface ConversationItemProps {
  item: Conversation;
}
export default function ChatsScreen({}: RootTabScreenProps<"Chats">) {
  const renderConversationPreview = (props: ConversationItemProps) => {
    const { item } = props;
    return <ConversationPreview key={item.id} conversation={item} />;
  };
  return (
    <View style={styles.mainContainer}>
      <FlashList
        data={conversations}
        renderItem={renderConversationPreview}
        keyExtractor={(item) => item.id}
        estimatedItemSize={40}
      />
    </View>
  );
}

I estimated the size as being 40 items but feel free to adjust this. FlashList perform more effectively by knowing roughly how many items you will have.

I kept the styling fairly light.

// screens/Chats/Chats.styles.ts
import { StyleSheet } from "react-native";
import Colors from "../../constants/Colors";

export default StyleSheet.create({
  mainContainer: {
    backgroundColor: Colors.light.white,
    height: "100%",
    paddingTop: "3%",
  },
});

In our FlashList, we render out a ConversationPreview for each of our conversations. It looks something like this.

Image description

ConversationPreview

In ConversationPreview, I'm doing a few fun things.

Text previews

We can create the '...' eliptical effect by fetching the last message from the conversation and then setting the following options in our text component.

numberOfLines={1}
ellipsizeMode="tail"

Profile images

We are setting profile image by the conversation id - they're saved on an images object which we store the assets on.

const images: {
  [key: string]: any;
} = {
  "101": require("./images/101.jpeg"),
  "102": require("./images/102.jpeg"),
  "103": require("./images/103.jpeg"),
  "104": require("./images/104.jpeg"),
  "105": require("./images/105.jpeg"),
};

export default images;

Times & notifications

We use the dayjs library for getting the timestamp of our messages and we're hard coding a notification as default for now.

// components/ConversationPreview/ConversationPreview.tsx
import { Image, Text, View, TouchableOpacity } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { useContext } from "react";
import dayjs from "dayjs";

import { ConversationType } from "../../types";
import styles from "./ConversationPreview.styles";
import { ConversationsContext } from "../../context/conversationContext";
import images from "../../assets/index";

interface ConversationPreviewProps {
  conversation: ConversationType;
}

interface ChatRouteParams {
  conversation: ConversationType;
}

export default function ConversationPreview(props: ConversationPreviewProps) {
  const { conversation } = props;
  const { setCurrentConversation } = useContext(ConversationsContext);
  const navigation = useNavigation();
  const profileImg = images[conversation.id];

  const chatRouteParams: ChatRouteParams = {
    conversation,
  };

  const _onPress = () => {
    setCurrentConversation(conversation.id);
    navigation.navigate("Chat", chatRouteParams);
  };

  return (
    <TouchableOpacity onPress={_onPress} style={styles.messageContainer}>
      <View style={styles.imgAndMsgSubContainer}>
        <Image style={styles.profileImg} source={profileImg} />
        <View>
          <Text style={styles.msgTitle}>{conversation.title}</Text>
          <Text
            numberOfLines={1}
            ellipsizeMode="tail"
            style={styles.msgPreview}
          >
            {conversation.messages[conversation.messages.length - 1].text}
          </Text>
        </View>
      </View>
      <View style={styles.msgDataContainer}>
        <View style={styles.msgDataSubContainer}>
          <Text style={styles.timeText}>
            {dayjs(
              conversation.messages[conversation.messages.length - 1].time
            ).format("HH:mm")}
          </Text>
          <View style={styles.numberOfMsgsContainer}>
            <Text style={styles.numberOfMsgsText}>2</Text>
          </View>
        </View>
      </View>
    </TouchableOpacity>
  );
}

// components/ConversationPreview/ConversationPreview.styles.ts
import { StyleSheet } from "react-native";
import Colors from "../../constants/Colors";
import { Dimensions } from "react-native";
const windowHeight = Dimensions.get("window").height;
const windowWidth = Dimensions.get("window").width;

export default StyleSheet.create({
  messageContainer: {
    width: windowWidth * 0.97,
    height: windowHeight / 15,
    flexDirection: "row",
    justifyContent: "space-around",
  },
  imgAndMsgSubContainer: {
    flexDirection: "row",
    width: "90%",
    height: "100%",
    alignItems: "center",
  },
  profileImg: {
    width: 50,
    height: 50,
    marginRight: 10,
    marginLeft: 10,
    borderRadius: 50,
  },
  msgTitle: {
    fontWeight: "bold",
    color: Colors.light.offBlack,
  },
  msgPreview: {
    color: Colors.light.offBlack,
    width: "80%",
  },
  msgDataContainer: {
    flexDirection: "row",
    alignItems: "center",
  },
  msgDataSubContainer: {
    flexDirection: "column",
    alignItems: "center",
  },
  timeText: {
    color: Colors.light.brightGreen,
  },
  numberOfMsgsContainer: {
    backgroundColor: Colors.light.brightGreen,
    height: 20,
    width: 20,
    borderRadius: 50,
    padding: 0,
    margin: 0,
    alignItems: "center",
    justifyContent: "center",
  },
  numberOfMsgsText: {
    color: Colors.light.white,
    fontWeight: "bold",
    padding: 0,
    margin: 0,
  },
});

Pressing a message

When we press on a message preview, we navigate to the Chat Screen and we set the current conversation.

  const chatRouteParams: ChatRouteParams = {
    conversation,
  };

  const _onPress = () => {
    setCurrentConversation(conversation.id);
    navigation.navigate("Chat", chatRouteParams);
  };

Chat

Our messages are displayed on the Chat.tsx screen

Background image

We set the background image as an image I found online.

<ImageBackground
        style={styles.backgroundImg}
        source={require(whatsappBackgroundImg)}
        resizeMode="cover"
>

Image description

We used it with the component from react-native

ChatMessages

The ChatMessages component is where we display all the messages in this conversation.

Image description

This component is a FlashList that renders all the messages as MessageBubbles.

      <FlashList
        inverted
        data={[...messages].reverse()}
        renderItem={renderMessageBubble}
        estimatedItemSize={40}
      />

Position of ChatMessages

When someone opens the keyboard, our ChatMessages need to move up above the keyboard (same as our SendButton below).

Image description

But also, when the input box expands as message size increases, ChatMessages need to move up too.

Image description

I probably overcomplicated this (let me know in the comments), but I solved it like this:

// components/ChatMessages/ChatMessages.tsx
<View
      style={{
        height:
          windowHeight * 0.8 -
          keyBoardOffsetHeight * 0.95 -
          getMessageHeightOffset(heightOfMessageBox, windowHeight),
      }}
    >
      <FlashList
        inverted
        data={[...messages].reverse()}
        renderItem={renderMessageBubble}
        estimatedItemSize={40}
      />
    </View>

and with the getMessageBoxHeightOffset function

// helpers/getMessageBoxHeightOffset.ts
const getMessageHeightOffset = (
  heightOfMessageBox: number,
  windowHeight: number
): number => {
  const maxHeightOfMessageBox = windowHeight * 0.3;
  if (heightOfMessageBox > maxHeightOfMessageBox) {
    return maxHeightOfMessageBox - windowHeight * 0.05;
  }
  if (heightOfMessageBox > 24) {
    return heightOfMessageBox - windowHeight * 0.035;
  }
  return 0;
};

export default getMessageHeightOffset;

MessageBubble

Image description

There is quite a bit of styling in this component. And the styling depends on whether it is from a recipient or sender.

We just establish if I sent the message by checking if it matched my user id.

const isMyMessage = message.userID === 1;

Then we adjust the styling based on that

import { View, Text } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import dayjs from "dayjs";

import { MessageDataType } from "../../types";
import styles from "./MessageBubble.styles";

export default function MessageBubble(props: MessageDataType) {
  const { message } = props;
  const isMyMessage = message.userID === 1;
  const isMessageRead = false;
  return (
    <View
      style={{
        ...styles.messageContainer,
        alignSelf: isMyMessage ? "flex-start" : "flex-end",
        backgroundColor: isMyMessage ? "#fcfcfc" : "#dfffc7",
        borderTopLeftRadius: isMyMessage ? 0 : 5,
        borderTopRightRadius: isMyMessage ? 5 : 0,
      }}
    >
      <View
        style={{
          ...styles.leftMessageArrow,
          display: isMyMessage ? "flex" : "none",
        }}
      ></View>
      <Text
        style={{
          ...styles.messageText,
          left: isMyMessage ? 0 : 10,
        }}
      >
        {message.text}
      </Text>
      <View
        style={{
          ...styles.timeAndReadContainer,
          left: isMyMessage ? 0 : 10,
        }}
      >
        <Text style={styles.timeText}>
          {dayjs(message.time).format("HH:mm A")}
        </Text>
        <View>
          {isMessageRead ? (
            <MaterialCommunityIcons name="read" size={16} color="#5bb6c9" />
          ) : (
            <MaterialCommunityIcons name="check" size={16} color="grey" />
          )}
        </View>
        <View
          style={{
            ...styles.rightMsgArrow,
            display: isMyMessage ? "none" : "flex",
          }}
        ></View>
      </View>
    </View>
  );
}

SendButton

The sendButton is a fun one.

position of the sendButton

The Whatsapp sendButton starts at the bottom, but when someone opens the keyboard, it moves upwards.

Image description

So we need the style to be absolute and dependent on whether the keyboard is open.

bottom: Math.max(keyBoardOffsetHeight, windowHeight * 0.02),

To get this keyBoardOffsetHeight I wrote a hook

// components/SendButton/SendButton.tsx
const keyBoardOffsetHeight = useKeyboardOffsetHeight();
// helpers/useKeyboardOffsetHeight.tsx
import { useEffect, useState } from "react";
import { Keyboard } from "react-native";

export default function useKeyboardOffsetHeight(): number {
  const [keyBoardOffsetHeight, setKeyboardOffsetHeight] = useState(0);

  useEffect(() => {
    const keyboardWillShowListener = Keyboard.addListener(
      "keyboardWillShow",
      (e) => {
        setKeyboardOffsetHeight(e.endCoordinates.height);
      }
    );
    const keyboardWillHideListener = Keyboard.addListener(
      "keyboardWillHide",
      () => {
        setKeyboardOffsetHeight(0);
      }
    );

    return () => {
      keyboardWillHideListener.remove();
      keyboardWillShowListener.remove();
    };
  }, []);

  return keyBoardOffsetHeight;
}

Typing = icon change animation

When we start typing, WhatsApp changes the voice record icon to the send icon.

Image description

Switching the icons is easy - we just keep an isTyping flag. It's initialised in Chat.tsx because its shared across components.

But for it to look smooth, we want an animation between the icons so we can use react-native-reanimated library

<Transitioning.View ref={ref} transition={msgTypeTransition}>
              {isTyping ? (
                <Ionicons name="send" size={16} color={Colors.light.white} />
              ) : (
                <FontAwesome5
                  name="microphone"
                  size={16}
                  color={Colors.light.white}
                />
              )}
            </Transitioning.View>

const msgTypeTransition = (
  <Transition.Together>
    <Transition.Out type="scale" durationMs={100} />
    <Transition.Change interpolation="easeInOut" />
    <Transition.In type="scale" durationMs={100} />
  </Transition.Together>
);
...



📌 The State of React Native Tooling (React Native CLI - The Ultimate Guide)


📈 35.72 Punkte

📌 React Native Networking – How To Perform API Requests In React Native using the FetchAPI


📈 35.72 Punkte

📌 Building a Modern Document Website for React Native Library Like React Native ECharts


📈 35.72 Punkte

📌 How to Implement Face Detection in React Native Using React Native Vision Camera


📈 35.72 Punkte

📌 This Week In React #139: React.dev, Remix, Server Components, Error Boundary, Wakuwork, React-Native, Bottom Sheet...


📈 35.58 Punkte

📌 This Week In React #146: Concurrency, Server Components, Next.js, React-Query, Remix, Expo Router, Skia, React-Native...


📈 35.58 Punkte

📌 WhatsApp UI in React Native (part 1)


📈 29.5 Punkte

📌 Native Web Apps: React and WebAssembly to Rewrite Native Apps


📈 26.86 Punkte

📌 Native Vs React Native Development: Key Differences


📈 26.86 Punkte

📌 TOP 6 React Native libraries with native performance ⚡️


📈 26.86 Punkte

📌 This Week In React #126: Perf, Progressive Enhancement, Remix, Storybook, React-Native, FlashList, Nitro, TC39...


📈 26.72 Punkte

📌 Share code between React Native and React Web


📈 26.72 Punkte

📌 React vs React Native: How Different Are They, Really?


📈 26.72 Punkte

📌 How to Manage State in React and React Native with the PullState Library


📈 26.72 Punkte

📌 This Week In React #172: Next.js, PPR, Remotion, State of React Native, Parcel, Panda, Remix, Skia, Storybook, Tamagui...


📈 26.72 Punkte

📌 Is Transitioning from React.js to React Native as Easy as It Seems?


📈 26.72 Punkte

📌 React Native vs React: Key Differences and Use Cases


📈 26.72 Punkte

📌 React vs React Native: Pros, Cons, and Key Differences


📈 26.72 Punkte

📌 This Week In React #127: Nextra, React-Query, React Documentary, Storybook, Remix, Tamagui, Solito, TC39, Rome...


📈 26.58 Punkte

📌 This Week In React #131: useReducer, Controlled Inputs, Async React, DevTools, React-Query, Storybook, Remix, RN , Expo...


📈 26.58 Punkte

📌 How to build a Barcode Widget in React Native (part II: iOS)


📈 24.79 Punkte

📌 How to build a Barcode Widget in React Native (part I: Android)


📈 24.79 Punkte

📌 [React Native Guide 2024] Part 1: Builds and flavors.


📈 24.79 Punkte

📌 React Beyond the Boilerplate: Unleashing Creativity with Manual Mastery - Part 1: React as a CDN


📈 24.65 Punkte

📌 Oracle will ein Stück vom Cloud-Native-Kuchen mit dem Cloud Native Framework


📈 18 Punkte

📌 Cloud Native Show: What’s Cloud Native, Really?


📈 18 Punkte

📌 CVE-2022-23219 | Oracle Communications Cloud Native Core Network Function Cloud Native Environment CNE buffer overflow


📈 18 Punkte

📌 What is Cloud Native, Really? | Cloud Native


📈 18 Punkte

📌 Cloud Native Configuration and Setting in ASP.NET Core | The Cloud Native Show


📈 18 Punkte

📌 Where Does Serverless Fit in Cloud Native? | The Cloud Native Show


📈 18 Punkte

📌 CVE-2021-3177 | Oracle Communications Cloud Native Core Network Function Cloud Native Environment CNE buffer overflow


📈 18 Punkte

📌 Why .NET Core for building Cloud Native Apps? | Cloud Native


📈 18 Punkte

📌 InfiniteIO Hybrid Cloud Tiering: Providing native file access for traditional and cloud-native applications


📈 18 Punkte

📌 Understanding Cloud Native Application Bundles (CNAB) | The Cloud Native Show


📈 18 Punkte











matomo