Test-driving LiveView Native: Native Apps Driven by Elixir

Test-driving LiveView Native:  Native Apps Driven by Elixir
Photo by Adeolu Eletu / Unsplash

A new project called LiveView Native lets you build fully native iOS or Android apps which are fully driven by a backend server running the Phoenix Framework with the Elixir language.  Based on the LiveView library for interactive server-rendered HTML apps, this new project enables a new paradigm of frontend app development: frontend changes are driven by the server pushing out updated views in response to changes in application state.  In effect, this eliminates the need for frontend code and logic.

In this post, we'll take a look at what LiveView Native is, how it compares to LiveView, and take it for a quick test-drive.  In a future post, we'll do a hands-on tutorial on building a real-time, native chat app using LiveView Native, backed by the power of Elixir and the Phoenix Framework.

What is LiveView

LiveView is a powerful library for the Phoenix framework tht lets you build rich, highly interactive server-driven web apps without a line of JavaScript.  It uses server-rendered HTML which updates automatically on the client when server state changes.  Instead of dealing with frontend JavaScript logic to fetch new data and update the DOM, the server pushes out updated HTML fragments to the client automatically.

Imagine we have a simple template on our backend which renders a list of messages:

<%= for message <- @messages do %>
  <p><%= message %></p>
<% end %>

In LiveView, as soon as the @messages content changes on the server, the new list is reflected on the client automatically, without a line of JavaScript!  This also works for events initiated on the client: a button or form in the browser triggers a server-side event, which in turn changes server state and triggers a re-render.  Once again, all of this without a line of JavaScript.

On the web, LiveView lets you build rich, highly interactive apps while taking JavaScript entirely out of the equation.

LiveView does its magic with a persistent websocket connection between the client and the backend server.  This websocket performs two-way communication to take events from the client, and to push out optimized HTML diffs back from the server when changes to the markup occur.

For a full rundown of LiveView and its benefits, I highly recommend you take a look at this article on Why Your Next Web App Frontend Might be the Backend.

Why Your Next Web App Frontend Might be The Backend
How LiveView and other real-time server-side rendered HTML technologies are changing how the frontend is written If you’re developing a rich, highly interactive web app in 2022, chances are you’re using some sort of frontend JavaScript framework such as React, Angular or Vue, which talks to a backe…

And if you're interested in a more hands-on tutorial on LiveView, check out building a real-time chat app with LiveView:

Build Real-time Chat With Phoenix and LiveView in Fewer Than 50 Lines of Code
If you haven’t experienced the awesome power of LiveView in the Phoenix framework, strap in: we’re going to build a real-time, high-performance chat system with fewer than 50 lines of code. That includes all the code for both the frontend and the backend (spoiler alert: they’re kind

What is LiveView Native and How Does it Work?

LiveView Native is a new project being built by DockYard which extends the LiveView concept to native apps.  It lets your LiveView backend drive a native iOS or Android app via dynamic server-generated markup.  (Note: though Android support is in the works, the iOS frontend seems to be more mature at this point, and is what I used for testing).

LiveView Native icon
LiveView Native (via GitHub)

LiveView Native works in pretty much the same way as its web-based counterpart, only instead of sending HTML, it sends an alternative markup format which specifies how the native view is constructed.  In the case of iOS, this means sending a SwiftUI-like markup:

<vstack>
  <hstack>
    <text>1</text>
    <text>2</text>
    <text>3</text>
  </hstack>
</vstack>

Note that instead of HTML elements like div and p, we're referencing SwiftUI components like VStack, HStack and Text.

Adding LiveView Native to a SwiftUI application is as simple as creating a LiveViewCoordinator which connects to a LiveView socket, then adding a view in the hierarchy for the coordinator:

struct ContentView: View {
    @State var coordinator = LiveViewCoordinator(URL(string: "http://my-phoenix-app")!)

    var body: some View {
        VStack {
            LiveView(coordinator: coordinator)
        }
    }
}

That's it!  The view is now fully driven by the Phoenix LiveView backend markup.

One nice aspect of LiveView Native is that it simply runs within a SwiftUI View, and that View can be embedded anywhere in the View hierarchy.

VStack {
  Text("My LiveView Native App")
  LiveView(coordinator: coordinator)
  SomeOtherView()
}
.padding()

This means that LiveView Native can play nicely with existing SwiftUI apps, and that using LiveView Native is not an all-or-nothing proposition, the way some frameworks tend to approach this kind of thing.  

If you feel so inclined, you can even have multiple LiveView Native views within the same app.  I tried this with my chat example, making a second copy of the view which then connected like an independent client.  This probably is not a common use case, but it does open the door to using LiveView Native selectively for specific interactive server-driven components rather than letting it drive the entire app.

Tradeoffs of LiveView Native

There are several benefits that LiveView and LiveView Native can bring to app development, and of course, some downsides.  Let's explore the tradeoffs here.

Benefits of LiveView Native

First, let's take a look at the benefits of the LiveView Native approach.

  • Fully dynamic frontends: with the markup returned by the server, it can be changed dynamically just like a webpage.  This can be as simple as minor content updates, or entire app redesigns and restructures.
  • Unified state manage — no client/server synchronization issues: unlike a typical architecture in which the client and server might communicate with API calls to pass data back and forth and synchronize states, LiveView Native features a fully unified state management.  There is no separate client state.  It's hard to overstate the benefits of this approach, as it removes or reduces the need for a great deal of client code and logic around state and API management.
  • Not just a single language — a single application: the most profound benefit of the LiveView approach is that, in effect, you're no longer dealing with a "server-side" app and a "client-side" app: it's all a single logical application.  This is also true of LiveView Native, though to a slightly lesser extent, as we still need to build and install a separate client app to host the LiveView Native experience

Potential Drawbacks of LiveView Native

There are of course some drawbacks to the LiveView Native approach.

  • No offline functionality: one of the biggest downsides of the LiveView and LiveView native approach is that it cannot function with there's no network connectivity.  In practice, this means that LiveView Native is not appropriate for all apps or for all views within an app.  So it's best to restrict the usage to views that do inherently need a network connection, and provide fallback UX.
  • Open questions about native app lifecycle and API interoperability: the current implementation of LiveView Native is all about how views are rendered and how state is managed.  There are some unanswered quesitons for me about how to best interact with the rest of the app lifecycle and with native APIs on the device.  LiveView on the web has clear patterns around solving these issues with JavaScript "hooks", but I was unable to find good guidance on the right patterns to solve these problems in LiveView Native.  I suspect this will improve as the project matures.
  • Server load & scalability concerns: because computation and rendering in LiveView Native is server-side, you introduce a higher potential for scalability concerns.  Though the Elixir language is highly performant and great for highly concurrent applications, we still need to acknowledge the potential for increased load.

So in the end, LiveView and LiveView Native are amazing for some applications and components, but not necessarily the best choice for others. Consider these tradeoffs when picking your architecture, and do take advantage of the fact that applications can mix-and-match technologies, using LiveView Native for specific components that can take advantage of it.  The chat use-case we explore below is a good example of a component LiveView Native is perfect for: it's highly dynamic, driven by server-state changes, and inherently requires network connectivity to function properly.

Taking LiveView Native for a Test Drive

I tried out LiveView Native starting with a simple chat app that I had previously written for LiveView on the web.  My goal was to build a simple native iOS version of the same app reusing the existing chat infrastructure.

As it turns out, with all of the LiveView chat infrastructure in place, very few changes were needed on the server-side.  In fact, the only thing I really needed to do was to render a different view for the iOS frontend.  I didn't even need to install any new libraries on the server.  This simplicity and compatibility with existing LiveView apps is a major advantage that I think will help the adoptation of the tech.

I got started by diving into the iOS LiveView Native tutorial, and adapting it for my use case.

GitHub - liveviewnative/ios-tutorial: Sample code for the liveview-client-swiftui tutorial app.
Sample code for the liveview-client-swiftui tutorial app. - GitHub - liveviewnative/ios-tutorial: Sample code for the liveview-client-swiftui tutorial app.

Instead of replacing my HTML frontend with an iOS frontend, I wanted to support both at the same time.  This was as simple as picking up a "platform" flag from the connection and choosing the right template to render for each client, web or iOS.  One minor inconvenience I encountered though, was in the code to get LiveView to render the desired template.  It was simple enough to work around the issue manually, but it felt slightly clumsy:

  def render(assigns) do
    EEx.eval_file(
      template(assigns),
      [assigns: assigns],
      engine: Phoenix.LiveView.HTMLEngine
    )
  end

  def template(%{platform: "ios"}) do
    "lib/liveview_chat_web/live/chat_live/index.ios.heex"
  end

  def template(_) do
    "lib/liveview_chat_web/live/chat_live/index.html.heex"
  end
Rendering separate templates for iOS and web

I have no doubt this sort of thing will improve as the project matures, but illustrates some of the rough edges in building a LiveView Native app at the moment.  (It's also perfectly possible I missed a solution to this in the documentation!).

Once I had the code in place to choose the right template, it was as simple as writing the view for my frontend.  For iOS, this means basically a SwiftUI-like markup that resembles HTML, but with a mix of SwiftUI and custom components:

<vstack>
  <text>ChatLive</text>
  <list>
    <%= for {message, i} <- Enum.with_index(@messages) do %>
    <text id={"msg-#{i}"}><%= message.name %>: <%= message.text %></text>
    <% end %>
  </list>
  <phx-form id="chat" phx-submit="send">
    <hstack>
      <textfield name="text" value={@text} />
    </hstack>
    <phx-submit-button><text>Send</text></phx-submit-button>
  </phx-form>
</vstack>

This is almost identical in structure to its HTML counterpart, but using different tags, like vstack for SwiftUI's verticle stack component instead of a div.

With the new markup in place, I launched the app on the iPhone and–success!–I had real-time chat working between the iPhone and the web.

A photo of an iPhone running the demo app, showing communication between a user on phone and web
My simple LiveView chat adapted for LiveView Native on iOS

All-in-all, this was about 30 lines of code to build an iOS client for my existing (extremely simple) LiveView chat app. While this is truly simple sample, it clearly demonstrates the power and simplicity of using LiveView Native.

Recap

In this post we explored LiveView Native and how it can be used to build native iOS and Android apps.  Similar to LiveView on the web, LiveView Native represents an entirely different paradigm for building client apps and components.  

The project is very early in its development, but mark my words, it has the potential to be a game-changer in the way mobile app development is done.  Just like its counterpart on the web.

In an upcoming post, we'll dive into a hands-on tutorial on building the chat app I discussed in this post, for both web and iOS using LiveView and LiveView Native.

In the meantime, check out the LiveView native iOS documentation and iOS tutorial!