Mobile Elixir Development: The Making of the ElixirConf Chat App with LiveView Native

A hand holding an iPhone showing the ElixirConf app
Cynthia  Gandarilla

Marketing Manager

Cynthia Gandarilla

Elixir saves your team time and money, and now you can get those benefits for your mobile app. Get in touch to learn how LiveView Native can work for your native development.

The Challenge Begins: July 2023

In just a few weeks, our CEO Brian Cardella would deliver a keynote about the state of LiveView Native and its future on the final day of ElixirConf 2023. We knew part of his talk would dive into the technical changes to the latest version of LiveView Native, but we also knew he would touch on a new and bold value proposition: It’s at least 40% cheaper to build a non-trivial app with LiveView Native than any other popular framework, without sacrificing quality.

To showcase how simply LiveView Native reduces cost and accelerates time to market thanks to cross-platform compilation and an integrated front-end and back-end developer experience, we decided to build a chat app. Aside from being a good technical showcase of LiveView Native, it would be a valuable experience for ElixirConf 2023 attendees.

We created the elixirconf_chat repository on GitHub and pushed our first commit: Create new LiveView Native project.

The Solution Goes Live: September 2023

As ElixirConf kicked off, our MVP was ready. We launched ElixirConf 2023 Chat on multiple platforms, including the iOS App Store, for attendees to message each other from multiple devices and platforms, join rooms based on the talks they attended, and generally connect over their shared experiences at ElixirConf.

They experienced the concurrency and real-time benefits of building LiveView Native on Phoenix/Elixir, and then hopped directly into the public repository to see practical examples of our code. We gave them all the practical knowledge they needed to build their next startup’s MVP—particularly relevant for the technical founders and early-stage CTOs looking for a framework that helps them work, quickly, even when constrained on developer hours.

We reached this goal even with plenty of constraints. Like plenty of startups and small digital product teams, we faced the challenges of a small team on a tight schedule. Just three developers contributed more than 98% of the project’s code, and in the six weeks we had to develop the app none of them could work on it full time. Instead, they contributed what they could during DockYard Days.

Let’s talk about how LiveView Native got us from idea to real-time, cross-platform MVP in just six weeks.

Product Requirements: Authentication Flows to Cross-Platform Styling

We wanted our chat app to feel like a startup’s first real product, not a barebones demo put together via a hackathon. We put some ambition behind our product requirements both from a technical and end-user perspective.

  • Authentication flow: Attendees would log in using the same email they registered for ElixirConf with. That required a database for valid emails and various authentication flows for registering a new account, creating a password, and so on.
  • Simple—but beautiful—UI: We wanted a real-time chat experience similar to Slack or Discord, with a roster of scheduled talks, a general hallway channel, and additional channels specific to each talk as the conference went on. The UI needed to feel native to each platform and harmonious across multiple platforms.
  • Concurrency: Every user, channel, and additional discrete component of the chat experience should operate as a separate process that could send and receive/queue messages for a real-time, fault-tolerant experience.
  • Multi-platform: The chat app needed to compile cross-platform, notably web and iOS, and allow users to chat on multiple devices simultaneously without authentication or concurrency issues.
  • Single codebase: We didn’t want to deal with the barrier between front end and back end that other frameworks create, or worry about building an API layer with WebSockets and deploying infrastructure. Phoenix with LiveView Native allowed us to write composable functions and reuse code to speed up our development lifecycle.

The Roadblocks, and How We Overcame Them

With these requirements in hand we started building the chat app, diligently maximizing every available moment of time on the project. And like most startup crunches toward MVP, the process was not without hiccups and solutions to them, many of which are now foundational to the future of LiveView Native.

Owning the state on iOS

In SwiftUI, modifiers operate like CSS, but with a much wider scope. You can use them for not just visual styling, like colors and dimensions and relative/absolute position, but also to define complex user experience (UX) decisions like swipe actions and modals. LiveView Native now supports more than 300 SwiftUI modifiers, which gives us a powerful foundation for creating a consistent cross-platform chat experience.

With Phoenix LiveView, regardless of the platform you’re targeting, the back end service handles the initial state and every subsequent change, such as new messages and users logging in/out. When a state change occurs on the back end, the smallest possible diff of changed data is pushed to the client which then automatically refreshes the client-side representation of the view template to reflect the updated state.

On the web, LiveView relies on the browser’s HTML and CSS engines to refresh the visual state of the page when a diff is pushed. To support real-time UI updates for non-web platforms, LiveView Native provides its own reimplementations of these internal systems in platform-specific languages like SwiftUI. This is where the performance bottleneck was ultimately discovered.

When an iOS client was receiving a state update, it was re-applying every SwiftUI modifier that had been applied for the current page, regardless of whether it had changed or not. With no reliable way to diff only the modifiers that were changed, our iOS app was hitting a performance bottleneck that would happen consistently and grow as the page increased in size (for example, when new messages are posted to a channel).

To solve this issue, we completely changed how LiveView native handles modifiers, with a new stylesheet system that’s coming in v0.2. Here’s what a basic LiveView Native template looks like:

<VStack>
  <Text class="color-red bg:child-rect">
    Top
    <Rectangle template="child-rect" class="fill-gray bg:child-circle">
      <Circle template="child-circle" class="fill-blue frame:300:300" />
    </Rectangle>
  </Text>
</VStack>

During compilation, LiveView Native parses the template, extracts the class names, and compiles an abstract syntax tree (AST) of only what SwiftUI needs to render the app.

Now, upon every state change, LiveView Native offers near-identical rendering performance to conventional SwiftUI apps, with the added benefit of having a comfortable and accessible developer experience, much like the Tailwind framework for CSS.

Composability for Multiple OSs

In most cross-platform app development lifecycles, you have a back end service that handles API requests from multiple clients, all of which are written in different languages, like React for the web, SwiftUI for iOS, Jetpack for Android, and so on.

LiveView Native let us create multiple platform-specific templates, which are rendered based on pattern matching of the target platform (:swiftui for iOS/iPadOS clients and falling back to HTML for web clients) to the same event handler function.

For example, these two templates—simplified and minimized versions of their real app counterparts—have platform-specific UI but call the same post_message function via phx-submit=“post_message”.

SwiftUI:

def chat_input(%{format: :swiftui} = assigns) do
  ~SWIFTUI"""
  <LiveForm id="chat" phx-submit="post_message">
    <HStack>
      <TextField name="body">
        Enter Message...
      </TextField>
      <LiveSubmitButton after-submit="clear">
        <Image system-name="paperplane.fill" />
      </LiveSubmitButton>
    </HStack>
  </LiveForm>
  """
end

Web:

def chat_input(assigns) do
  ~H"""
  <form id="chat" phx-change="typing" phx-submit="post_message">
    <div>
      <label class="sr-only" for="chat-input"></label>
      <input
          type="text"
          name="body"
          placeholder="Enter Message..."
          id="chat-input"
          value={@body}
          maxlength={max_body_length()}
          phx-debounce="80"
          required
        />
      <button
          type="submit"
        >
          <span class="sr-only">Submit</span>
        </button>
    </div>
  </form>
  """
end

We gained two major efficiencies from this LiveView Native functionality:

We could render a <.chat_input /> from any other template with no additional development work, such as the different UIs for the general channel vs. a talk-specific channel, allowing us to reuse code with built-in cross-platform compatibility. We were freed from having to write separate event handlers for multiple OSs and platforms—we wrote the back end logic once and reused it many times over.

Concurrency

Building a chat app seems easy, but any real-time operation comes with issues around concurrency—your system’s ability to juggle multiple tasks at once without getting its wires crossed, so to speak. Every sent message spawns many additional processes to handle it, store it, update client state, and so on.

Concurrency is hard for everyone. Even harder when you scale.

LiveView Native is built on top of Elixir and Phoenix, which means every cross-platform app built on the framework gets a concurrent processing model for free thanks to Erlang and the BEAM virtual machine. Instead of running on expensive OS threads, the Erlang VM is designed to run millions of lightweight internal processes, each lightweight and isolated, to coordinate work by quickly passing along messages.

Erlang’s processes don’t block I/O, like writing a message to a database, don’t block compute power to tackle an expensive task, and isolate their errors without negatively affecting the processes around them.

What did that mean for the ElixirConf chat app? Every user and channel, for example, becomes a discrete process that can send and receive messages, creating a responsive and real-time experience for every user and platform. For us, it meant very little work or worrying about how we’d scale a back end service to meet ElixirConf 2023 demand—Elixir is already proven to scale to 1.5 billion pageviews/month with just five servers.

That is exactly the kind of resilience you want when you’re pushing toward your MVP and big launch day—and want to be sure you can handle whatever happens when you go live.

Wrapping Up Our Cross-Platform App

With just three developers putting in less-than-part-time hours over six weeks, we launched the ElixirConf 2023 Chat app without a hitch—and without a dedicated operations team nervously monitoring its health and performance as folks signed up and started chatting.

You can now see exactly how we broke down the barriers between front-end and back-end development and seamlessly compiled to multiple platforms in the ElixirConf 2023 Chat repository.

LiveView Native lived up to the 40% cheaper claim and then some—without it, we could not have achieved that most critical goal of pre-launch startup survival: We launched. Quickly.

When you’re ready to launch something amazing (and cross-platform, concurrent, scalable, resilient, and so on…), you can find the first steps to building your first LiveView Native in our documentation.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box