DEV Community

Rushikesh Pandit
Rushikesh Pandit

Posted on

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

Hi all,

This week, I have come up with the exciting new blog in which I am going to explain you how to handle state in mobile application which we are building with the help of LiveView Native by Dockyard.

In my earlier blog, I have explained how to setup the LiveView Native project, We are going to continue from the same step from where we have left it.

If you have not gone through that blog, please check it out here in the following link.

https://dev.to/rushikeshpandit/mobile-app-development-with-liveview-native-and-elixir-4f79
Enter fullscreen mode Exit fullscreen mode

Let's start writing the actual code for counter example.

Navigate to lib/native_demo_web/live/ directory and change the code of 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

Also, replace the render method of home_live.ex with below code.

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 above code, we have added link component in mobile app and button component in web app, which will navigate user to /counter route.

Now, navigate to lib/native_demo/ and create a new file with name counter.ex and paste the following content.

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 above file, we are starting a GenServer with the initial state as %{count: 0}. Also we are have written couple of methods such as increment_count, get_count which eventually gets handled by GenServer and changes will be notified to all the subscribers.

You can read more about GenServer in the following link.

https://dev.to/rushikeshpandit/demystifying-elixir-genservers-building-resilient-concurrency-in-elixir-9jm
Enter fullscreen mode Exit fullscreen mode

As we have written GenServer, next step is to start it. To do this, we have to navigate to application.ex which is inside same directory and over there we need to find def start(_type, _args) method and add NativeDemo.Counter to the children's array. Start method should look something like this.

application.ex changes

Now, navigate back to lib/native_demo_web/live/ and create 2 files with name 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 are adding our live view process to GenServer and getting the value of count. and in the render(assigns) method, we have a button with the phx-click which sends an event with name increment-count. In the handle_event method, we are incrementing the count by one. Our GenServer then notify the all subscriber with the event count_changed. Once our Live view gets this event, handle_info comes in actions and update the count which was incremented by the server. This happens in case of live view web.

Mobile app is pretty straightforward. In counter_live.swiftui.ex
mobile app is showing only value of count which is added into socket by webview and there is one button with the phx-click which sends an event with name increment-count. That's it. Rest all is taken by live view.

Once you done with all the changes mentioned above, head to the router.ex which is inside lib/native_demo_web directory and add 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 on Counter Demo button. It will navigate you to next page as shown below.

Counter page

If you are able to see this, then Congratulations!!!

Now, lets see the live view native in action.

Live view in action

Congratulations!!!

You have successfully created and setup GenServer utilize it into mobile app to manage the state.

You can find sample code on GitHub

In case if you face any issue, try deleting _build directory and compile code again using mix compile command.

If you have any suggestions/doubts or stuck somewhere in this process, feel free to reach to me via one of the following method.

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 be try to add some styles to the mobile app and also try to cover some more concepts.

Stay tuned!!!

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

Top comments (0)