DEV Community

Rushikesh Pandit
Rushikesh Pandit

Posted on • Updated on

Mobile app development with LiveView Native and Elixir. Part - 2

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

application.ex changes

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Your router file should look something like this.

router.ex

Now, run the application using iex -S mix phx.server and hit http://localhost:4000/counter on the browser.

You will see the following.

Counter web image

Now, open native/swiftui/NativeDemo.xcodeproj using Xcode and try to run the application,

you should be able to see the following.

Counter page home

Tap the Counter Demo button to navigate to the next page as shown below.

Counter page

If you can see this, congratulations! Now, let's see the live-view native in action.

Live view 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!!!

#myelixirstatus , #liveviewnative , #dockyard , #elixir , #phoenixframework

Top comments (2)

Collapse
 
maratk profile image
Marat

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

Image description

Collapse
 
maratk profile image
Marat

Nice article! Will be interesting to try this out!