Hey everyone,
In my latest blog post, I'll be explaining how to manage state in a mobile application using LiveView Native by Dockyard.
If you haven't read my previous blog, you can check it out using the following link.
https://dev.to/rushikeshpandit/mobile-app-development-with-liveview-native-and-elixir-4f79
Let's write the code for the counter example. Go to the lib/native_demo_web/live/
directory and modify the code in home_live.swiftui.ex
as shown below.
home_live.swiftui.ex
defmodule NativeDemoWeb.HomeLive.SwiftUI do
use NativeDemoNative, [:render_component, format: :swiftui]
def render(assigns, _interface) do
~LVN"""
<VStack id="hello-ios">
<HStack>
<Text class={["bold(true)"]} >Hello iOS!</Text>
</HStack>
<HStack>
<.link navigate={"/counter"} >
<Text>Counter Demo</Text>
</.link>
</HStack>
</VStack>
"""
end
end
Replace the render
method of home_live.ex
with the code provided below.
def render(assigns) do
~H"""
<div>
Hello from Web <br />
<br />
<button
phx-click="navigate"
class="text-stone-100 bg-indigo-600 font-semibold rounded py-2.5 px-3 border border-indigo-600 transition hover:bg-indigo-700"
>
<.link href={~p"/counter"}>Go to counter example</.link>
</button>
</div>
"""
end
In the code above, we added the link
component to the mobile app and the button
component to the web app. These components will navigate the user to the /counter
route. Next, go to lib/native_demo/
and create a new file named counter.ex
. Then, paste the following content into the file.
counter.ex
defmodule NativeDemo.Counter do
use GenServer
alias __MODULE__, as: Counter
@initial_state %{count: 0, subscribers: []}
# Client
def start_link(_initial_state) do
GenServer.start_link(Counter, @initial_state, name: Counter)
end
def increment_count do
GenServer.call(Counter, :increment_count)
end
def get_count do
GenServer.call(Counter, :get_count)
end
def join(pid) do
GenServer.call(Counter, {:join, pid})
end
def leave(pid) do
GenServer.call(Counter, {:leave, pid})
end
# Server (callbacks)
def init(initial_state) do
{:ok, initial_state}
end
def handle_call(:increment_count, _from, %{subscribers: subscribers} = state) do
new_count = state.count + 1
new_state = %{state | count: new_count}
notify_subscribers(subscribers, new_count)
{:reply, :ok, new_state}
end
def handle_call(:get_count, _from, state) do
{:reply, state.count, state}
end
def handle_call({:join, pid}, _from, state) do
Process.monitor(pid)
{:reply, :ok, %{state | subscribers: [pid | state.subscribers]}}
end
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
{:noreply, %{state | subscribers: Enum.reject(state.subscribers, &(&1 == pid))}}
end
# Private functions
defp notify_subscribers(subscribers, count) do
Enum.each(subscribers, fn pid -> send(pid, {:count_changed, count}) end)
end
end
In the above file, we start a GenServer with the initial state %{count: 0}
and have written methods like increment_count
and get_count
, which are handled by the GenServer
and notify all subscribers of any changes. You can learn more about GenServer at the following link.
https://dev.to/rushikeshpandit/demystifying-elixir-genservers-building-resilient-concurrency-in-elixir-9jm
After writing the GenServer, navigate to the application.ex
file in the same directory. Locate the def start(_type, _args)
method, add NativeDemo.Counter
to the children's array, and ensure that the start method looks like this.
Please remember to go back to lib/native_demo_web/live/
and create two files named counter_live.ex
and counter_live.swiftui.ex
.
counter_live.ex
defmodule NativeDemoWeb.CounterLive do
use NativeDemoWeb, :live_view
use LiveViewNative.LiveView,
formats: [:swiftui],
layouts: [
swiftui: {NativeDemoWeb.Layouts.SwiftUI, :app}
]
alias NativeDemo.Counter
@impl true
def render(assigns) do
~H"""
<div>
<div class="text-slate-800 bg-slate-50 content-center items-center text-center">
<.back navigate={~p"/home"}>Back to Home</.back>
<div class="mb-2.5">This button has been clicked <%= @count %> times.</div>
<div>
<button
phx-click="increment-count"
class="text-stone-100 bg-indigo-600 font-semibold rounded py-2.5 px-3 border border-indigo-600 transition hover:bg-indigo-700"
>
<span>Click me</span>
</button>
</div>
</div>
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
Counter.join(self())
{:ok, assign(socket, :count, Counter.get_count())}
end
@impl true
def handle_info({:count_changed, count}, socket) do
{:noreply, assign(socket, :count, count)}
end
@impl true
def handle_event("increment-count", _params, socket) do
NativeDemo.Counter.increment_count()
{:noreply, socket}
end
end
counter_live.swiftui.ex
defmodule NativeDemoWeb.CounterLive.SwiftUI do
use NativeDemoNative, [:render_component, format: :swiftui]
def render(assigns, _interface) do
~LVN"""
<.header>
Counter
</.header>
<HStack>
<Text class={["bold(true)"]}>This button has been pressed <%= @count %> times.</Text>
</HStack>
<HStack>
<Button phx-click="increment-count">
<Text >Press me</Text>
</Button>
</HStack>
"""
end
end
Explanation for above code.
In the mount
method of counter_live.ex
, we add our live view process to GenServer
and retrieve the current count value. In the render(assigns)
method, we have a button with the phx-click
attribute, which triggers an event named increment-count
. In the handle_event
method, we increment the count by one. Our GenServer then notifies all subscribers with the event count_changed
. When our live view receives this event, the handle_info
method updates the count, which was incremented by the server. This process occurs in the case of the live view web.
The mobile app in counter_live.swiftui.ex
displays the count value sent by the web view to the socket. A button with the phx-click
attribute triggers an event named increment-count
, while the live view handles everything else.
Once you've made the changes, go to router.ex
in the lib/native_demo_web
directory and add the following line.
live "/counter", CounterLive
Your router file should look something like this.
Now, run the application using iex -S mix phx.server
and hit http://localhost:4000/counter
on the browser.
You will see the following.
Now, open native/swiftui/NativeDemo.xcodeproj
using Xcode and try to run the application,
you should be able to see the following.
Tap the Counter Demo
button to navigate to the next page as shown below.
If you can see this, congratulations! Now, let's see the live-view native in action.
Congratulations!!!
You have successfully created and set up a GenServer. Utilise it in the mobile app to manage the state.
You can find sample code on GitHub
If you encounter any issues, try deleting the _build
directory and then recompile the code using the mix compile
command.
If you have any suggestions or doubts, or if you're stuck at any point during this process, feel free to contact me using one of the following methods:
LinkedIn: https://www.linkedin.com/in/rushikesh-pandit-646834100/
GitHub: https://github.com/rushikeshpandit
Portfolio: https://www.rushikeshpandit.in
In my next blog, I will try to add some styles to the mobile app and cover some more concepts.
Stay tuned!!!
Top comments (3)
I could successfully run the demo app on the iPhone 13 and iPhone 15 simulators.
However, when building for iPhone 13 I was getting the following warning:
Traditional headermap style is no longer supported; please migrate to using separate headermaps and set 'ALWAYS_SEARCH_USER_PATHS' to NO
The LiveView Native with Elixir is a powerful way to build real-time, dynamic apps with minimal client-side code. It uses Elixir's concurrency and LiveView's live interactions to build highly responsive, scalable mobile experiences. This reduces complexity and boosts productivity, making it a great choice for modern app development. It is an exciting combination for building seamless, performant mobile solutions.
Nice article! Will be interesting to try this out!