Phoenix1.7とElixirDesktopでスマホアプリ開発 CRUD編

# はじめに

本記事は [Qiita AdventCalendar2022 Elixir](https://qiita.com/advent-calendar/2022/elixir) vol4 18日目の記事です

セットアップ編でPhoenixプロジェクトを新たに作成し、それをスマホアプリ化してトップページを表示できるところまでやりました
今回はphx.gen.liveでCRUD画面とヘッダーとボトムタブのナビゲーションコンポーネントを作っていきます

ElixirDesktopでスマホアプリ作成シリーズ
1. [Phoenix1.7とElixirDesktopでスマホアプリ開発 セットアップ編](https://qiita.com/the_haigo/items/fa3906e45cf805ca38fa)
2. [Phoenix1.7とElixirDesktopでスマホアプリ開発 認証機能編](https://qiita.com/items/c831ff92b06b207d0dbf)
3. [Phoenix1.7とElixirDesktopでスマホアプリ開発 CRUD編](https://qiita.com/the_haigo/items/7472274168939bccbe91)
4. [Phoenix1.7とElixirDesktopでスマホアプリ開発 GPSと地図アプリ編](https://qiita.com/the_haigo/items/7d0e3e24ef215fba13c1)

# DaisyUIのインストール
デフォルトでCSSフレームワークがTailwindなのでTailwindベースのUIライブラリのdaisyuiを使っていきます

https://daisyui.com/

installガイドにそってtailwind.config.jsのpluginに追加して完了です

```
cd assets
npm i daisyui
```

```diff_javascript:phoenix/assets/tailwind.config.js

const plugin = require("tailwindcss/plugin")

module.exports = {
  ...
  plugins: [
    require("@tailwindcss/forms"),
+    require("daisyui"),
    plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
    plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
    plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
    plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
  ]
}
```

# CRUD
CRUD画面を作っていきます
~~なんのアプリか特に決めてなかったのですが、書籍管理にでもしましょうか~~
~~ユーザー > 棚 > 本　な感じで本とファイルアップロードまでだとでかくなるので棚を登録するところまでをやっていきましょう~~
GPS Loggerにしようと思います！
ユーザー >　ルート > GPSログな感じで保存していきます、今回は最初のルートだけをやっていきます

## phx.gen.live
phx.gen.liveでCRUD画面を作ります user has_many routeにしたいので user_idを外部キーにしてスキーマを作ります

```
mix phx.gen.live Loggers Route routes name:string user_id:refrences:users
```
routerの認証が必要なsessionの配下に追加します

```diff_elixir:phoenix/lib/spotties_web/router.ex
defmodule SpottiesWeb.Router do
  use SpottiesWeb, :router
  ...
  scope "/", SpottiesWeb do
    pipe_through([:browser, :require_authenticated_user])

    live_session :require_authenticated_user,
      on_mount: [{SpottiesWeb.UserAuth, :ensure_authenticated}] do
      live("/users/settings", UserSettingsLive, :edit)
      live("/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email)
+      live("/routes", RouteLive.Index, :index)
+      live("/routes/new", RouteLive.Index, :new)
+      live("/routes/:id/edit", RouteLive.Index, :edit)

+      live("/routes/:id", RouteLive.Show, :show)
+      live("/routes/:id/show/edit", RouteLive.Show, :edit)
    end
  end

  ...
end
```

終わったらDBに反映させます
```
mix ecto.migrate
```

## リレーション周り修正
そのままだとユーザーに関連付けて保存できないので手を加えます

Userにhas_manyリレーションを設定します
```diff_elixir:phoenix/lib/spotties/accounts/user.ex
defmodule Spotties.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field(:email, :string)
    field(:password, :string, virtual: true, redact: true)
    field(:hashed_password, :string, redact: true)
    field(:confirmed_at, :naive_datetime)

+    has_many(:routes, Spotties.Loggers.Route)

    timestamps()
  end
  ...
end
```
belongs_to リレーションと
castでchangesetの保存時にuser_idが入るようにします

```diff_elixir:phoenix/lib/spotties/loggers/route.ex
defmodule Spotties.Loggers.Route do
  use Ecto.Schema
  import Ecto.Changeset

  schema "routes" do
    field(:name, :string)
+    belongs_to(:user, Spotties.Accounts.User)

    timestamps()
  end

  @doc false
  def changeset(route, attrs) do
    shelf
-    |> cast(attrs, [:name])
-    |> validate_required([:name)
+    |> cast(attrs, [:name, :user_id])
+    |> validate_required([:name, :user_id])
  end
end
```

## list_routes/1を実装
user_idでwhereするlist_routes/1を実装します

```phoenix/lib/spotties/routes.ex
defmodule Spotties.Loggers do
  ...
  def list_routes(user_id) do
    from(
      route in Route,
      where: route.user_id == ^user_id
    )
    |> Repo.all()
  end
  ...
end
```

## index.exでユーザーに関連付けられたrouteを取得する
list_routesにuser_idを渡せるようにします

以下は変更した関数だけ表示しています
```diff_elixir:phoenix/lib/spotties_web/live/route_live/index.ex
defmodule SpottiesWeb.RouteLive.Index do
  use SpottiesWeb, :live_view

  alias Spotties.Loggers
  alias Spotties.Loggers.Route

  @impl true
  def mount(_params, _session, socket) do
+    user = socket.assigns.current_user
+    {:ok, assign(socket, :routes, list_routes(user.id))}
-    {:ok, assign(socket, :routes, list_routes())}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
+    user = socket.assigns.current_user
    shelf = Loggers.get_route!(id)
    {:ok, _} = Loggers.delete_route(route)

-    {:noreply, assign(socket, :routes, list_routes())}
+    {:noreply, assign(socket, :routes, list_routes(user.id))}
  end

-  defp list_routes() do
-    Loggers.list_routes()
-  end
+  defp list_routes(user_id) do
+    Loggers.list_routes(user_id)
+  end
end
```

## formデータにuser_idを追加する
index.exで初期値に入れて、formのhidden_inputにuser_idを追加してもいいのですが
新規作成のときだけ元のパラメータにuser_idを追加するほうが個人的に好みなので、この方法でやっていきます

RouteLiveのFormComponentにassignsを渡していきます
assignsにcurrent_userがあるのでコンポーネントでuser_idで参照できるようにします
```diff_elixir:phoenix/lib/spotties_web/live/route_live/index.html.heex
<.modal
  :if={@live_action in [:new, :edit]}
  id="shelf-modal"
  show
  on_cancel={JS.navigate(~p"/routes")}
>
  <.live_component
    module={SpottiesWeb.RouteLive.FormComponent}
    id={@route.id || :new}
    title={@page_title}
    action={@live_action}
    route={@route}
+    user_id={@current_user.id}
    navigate={~p"/rotues"}
  />
</.modal>
```

route_paramsにMap.putするときの注意点なのですが
route_paramsのキーは全てStringなので
`Map.put(params, :user_id, 1)`とStringのキーの中にAtomを入れるとエラーになります
キーの型は統一するようにしましょう

```diff_elixir:phoenix/lib/spotties_web/live/route_live/form_component.ex
defmodule SpottiesWeb.RouteLive.FormComponent do
  use SpottiesWeb, :live_component
  ...
  defp save_route(socket, :new, route_params) do
+    route_params = Map.put(route_params, "user_id", socket.assigns.user_id)

    case Loggers.create_route(route_params) do
      {:ok, _route} ->
        {:noreply,
         socket
         |> put_flash(:info, "Route created successfully")
         |> push_navigate(to: socket.assigns.navigate)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
end
```

## tableのwidth調整
tableコンポーネントですがデフォルトだとスマホサイズでedit,deleteが見えないので修正します

```diff_elixir:phoenix/lib/spotties_web/components/core_components.ex:L443
defmodule SpottiesWeb.CoreComponents do
  ...
  def table(assigns) do
    ~H"""
    <div id={@id} class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
-      <table class="mt-11 w-[40rem] sm:w-full">
+      <table class="mt-11 w-full">
      ...
      </table>
    </div>
    """
  end
  ...
end
```

## 削除モーダルの作成
スマホだとalertやconfirmダイアログ表示がネイティブ側に手を加える必要があって面倒なので、confirmもモーダルにします

core_component.exにconfirm modalの例があったのでそれを参考にします

```elixir:phoenix/lib/spotties_web/components/core_components.ex:L17-36
  @doc """
  Renders a modal.

  ## Examples

      <.modal id="confirm-modal">
        Are you sure?
        <:confirm>OK</:confirm>
        <:cancel>Cancel</:cancel>
      </.modal>

  JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
  for the caller to react to each button press, for example:

      <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
        Are you sure you?
        <:confirm>OK</:confirm>
        <:cancel>Cancel</:cancel>
      </.modal>
  """
```
最初はindex.ex
handle_paramsのapply_actionの最後にdeleteを追加して、
削除実行後リダイレクト処理とフラッシュメッセージを出すようにします

```diff_elixir:phoenix/lib/spotties_web/live/route_live/index.ex
defmodule SpottiesWeb.RouteLive.Index do
  use SpottiesWeb, :live_view

  ...
  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Routes")
    |> assign(:route, nil)
  end

+  defp apply_action(socket, :delete, %{"id" => id}) do
+    socket
+    |> assign(:page_title, "Delete Route")
+    |> assign(:route, Loggers.get_route!(id))
+  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    user = socket.assigns.current_user
    route = Loggers.get_route!(id)
    {:ok, _} = Loggers.delete_route(route)

-   {:noreply, assign(socket, :routes, list_routes(user.id))}
+   socket
+   |> assign(:routes, list_routes(user.id))
+   |> put_flash(:info, "Route deleted successfully")
+   |> push_navigate(to: ~p"/routes")
+   |> then(&{:noreply, &1})
  end
  ...
end
```

index.html.heexの末尾に :deleteの時に表示されるconfirm modalを追加します
on_cancelにcancelボタンを押した時の挙動（モーダルを閉じる）
on_confirmにOKボタン（delete）を押した時の挙動(deleteイベントを発火)
をそれぞれ渡しています
```diff_elixir:phoenix/lib/spotties_web/live/route_live/index.html.heex
<.header>
  Listing Routes
  <:actions>
    <.link patch={~p"/routes/new"}>
      <.button>New Route</.button>
    </.link>
  </:actions>
</.header>

<.table id="routes" rows={@routes} row_click={&JS.navigate(~p"/routes/#{&1}")}>
  <:col :let={route} label="Name"><%= route.name %></:col>
  <:action :let={route}>
    <div class="sr-only">
      <.link navigate={~p"/routes/#{route}"}>Show</.link>
    </div>
    <.link patch={~p"/routes/#{route}/edit"}>Edit</.link>
  </:action>
  <:action :let={route}>
-    <.link phx-click={JS.push("delete", value: %{id: route.id})} data-confirm="Are you sure?">
-      Delete
-    </.link>
+    <.link patch={~p"/routes/#{route}/delete"}>Delete</.link>    
  </:action>
</.table>

<.modal>
...
</.modal>

+ <.modal
+   :if={@live_action in [:delete]}
+   id="route-delete-modal"
+   show
+   on_cancel={JS.navigate(~p"/routes")}
+   on_confirm={JS.push("delete", value: %{id: @route.id})}
+ >
+   <p class="m-8">Are you sure?</p>
+   <:confirm><span class="mx-8 text-neutral-200">Delete</span></:confirm>
+   <:cancel><span class="mx-8">Cancel</span></:cancel>  
+ </.modal>
```

deleteが黒なのは気に食わないので赤にします
該当のclassは96行目にあります

```diff_elixir:phoenix/lib/spotties_web/components/core_components.ex:L48-115
defmodule SpottiesWeb.CoreComponents do
  ...
  def modal(assigns) do
    ~H"""
    <div id={@id} phx-mounted={@show && show_modal(@id)} class="relative z-50 hidden">
      <div id={"#{@id}-bg"} class="fixed inset-0 bg-zinc-50/90 transition-opacity" aria-hidden="true" />
      <div ...>
        <div class="flex min-h-full items-center justify-center">
          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
            <.focus_wrap ...>
              <div class="absolute top-6 right-5">
                <button...>
                  <Heroicons.x_mark solid class="h-5 w-5 stroke-current" />
                </button>
              </div>
              <div id={"#{@id}-content"}>
                <header :if={@title != []}>...</header>
                <%= render_slot(@inner_block) %>
                <div :if={@confirm != [] or @cancel != []} class="ml-6 mb-4 flex items-center gap-5">
                  <.button
                    :for={confirm <- @confirm}
                    id={"#{@id}-confirm"}
                    phx-click={@on_confirm}
                    phx-disable-with
-                    class="py-2 px-3"
+                    class="py-2 px-3 bg-red-400 hover:bg-red-600"
                  >
                    <%= render_slot(confirm) %>
                  </.button>
                  <.link
                    :for={cancel <- @cancel}
                    phx-click={hide_modal(@on_cancel, @id)}
                    class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
                  >
                    <%= render_slot(cancel) %>
                  </.link>
                </div>
              </div>
            </.focus_wrap>
          </div>
        </div>
      </div>
    </div>
    """
  end
  ...
end
```

最後にroutingにdeleteを追加します

```diff_elixir:phoenix/lib/spotties_web/router.ex
defmodule SpottiesWeb.Router do
  use SpottiesWeb, :router
  ...
  scope "/", SpottiesWeb do
    pipe_through([:browser, :require_authenticated_user])

    live_session :require_authenticated_user,
      on_mount: [{SpottiesWeb.UserAuth, :ensure_authenticated}] do
      live("/users/settings", UserSettingsLive, :edit)
      live("/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email)
      live("/routes", RouteLive.Index, :index)
      live("/routes/new", RouteLive.Index, :new)
      live("/routes/:id/edit", ShelfLive.Index, :edit)
+      live("/routes/:id/delete", RouteLive.Index, :delete)

      live("/routes/:id", RouteLive.Show, :show)
      live("/routes/:id/show/edit", RouteLive.Show, :edit)
    end
  end
  ...
end
```
これでconfirm modalができました

## トップページの変更
ログイン後のトップページを `/routes`に変更します

```diff_elixir:phoenix/lib/spotties_web/controllers/page_controller.ex
defmodule SpottiesWeb.PageController do
  use SpottiesWeb, :controller
  alias Spotties.Accounts.User

  def home(conn, _params) do
    # The home page is often custom made,
    # so skip the default app layout.
    render(conn, :home, layout: false)
  end

  def index(conn, _params) do
    redirect_to(conn, conn.assigns.current_user)
  end

  def redirect_to(conn, %User{}) do
-    redirect(conn, to: ~p"/users/settings")
+    redirect(conn, to: ~p"/routes")
  end

  def redirect_to(conn, nil) do
    redirect(conn, to: ~p"/users/log_in")
  end
end
```

# CRUDデモ
ここで一回動作確認をしてみましょうか

![d9f305408308713da6bc2f40ca95ec05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/89c7f832-c706-77c0-722d-929941b1eab6.gif)

ちゃんとうごいますね

# 共通コンポーネントの作成
phx.gen.authで作られたグローバルメニューを消したので、ログイン後はsettingに移動できないので、以下のナビゲーション共通コンポーネントを作ります

- gheader
- bottom_tab

# default headerを削除
まず元のヘッダーを消して、pyを20から24にします

```diff_elixir:phoenix/lib/spotties_web/components/layouts/app.html.heex
- <header class="px-4 sm:px-6 lg:px-8">
-  <div class="flex items-center justify-between border-b border-zinc-100 py-3">
-  ...
-  </div>
- </header>
- <main class="px-4 py-20 sm:px-6 lg:px-8">
+ <main class="px-4 py-24 sm:px-6 lg:px-8">
  <div class="mx-auto max-w-2xl">
    ...
    <%= @inner_content %>
  </div>
</main>
```

# CoreComponentの解説
core_component.exのheaderを参考にします

```elixir:phoenix/lib/spotties_web/components/core_components.ex:L398-421
...
  @doc """
  Renders a header with title.
  """
  attr(:class, :string, default: nil)

  slot(:inner_block, required: true)
  slot(:subtitle)
  slot(:actions)

  def header(assigns) do
    ~H"""
    <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
      <div>
        <h1 class="text-lg font-semibold leading-8 text-zinc-800">
          <%= render_slot(@inner_block) %>
        </h1>
        <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
          <%= render_slot(@subtitle) %>
        </p>
      </div>
      <div class="flex-none"><%= render_slot(@actions) %></div>
    </header>
    """
  end
...
```
使用例
```elixir
<.header>
  Listing Routes
  <:actions>
    <.link patch={~p"/routes/new"}>
      <.button>New Route</.button>
    </.link>
  </:actions>
</.header>
```

attrは以下のように指定したものが `@class`で展開される
```elixir
<.header class={"bg-black"}>
```
slotはslot名のタグ(今回は:actions)で囲った内容が`render_solot`で表示してくれるというイメージ
```elixir
  <:actions>
    <.link patch={~p"/routes/new"}>
      <.button>New Route</.button>
    </.link>
  </:actions>
```
slotで指定していない要素は `@inder_block` に渡される
この場合は `Listing Routes`が該当する
```elixir
<.header>
  Listing Routes
  <:actions>
    <.link patch={~p"/routes/new"}>
      <.button>New Route</.button>
    </.link>
  </:actions>
</.header>
```

表示フラグは`:if`で制御できる slotが指定されていない場合は `[]`が渡されるようだ

```
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
  <%= render_slot(@subtitle) %>
</p>
```
これを踏まえてコンポーネントを作成してみよう

## gheader
ベースはこちらになります

https://daisyui.com/components/navbar/#navbar-with-dropdown-center-logo-and-icon

attrはタイトル
slot backは戻るボタンの戻る先をつけた`<.link>`を渡す
slot actionsはEditやAddのリンクを指定した`<.link>`を渡す
attrとslotはdocオプションで注釈等をつけることができます

```elixir:phoenix/lib/spotties_web/components/navigation.ex
defmodule SpottiesWeb.Components.Navigation do
  use Phoenix.Component

  attr(:title, :string, default: "")

  slot(:back, doc: "add back navigation within .link component")
  slot(:actions, doc: "add action navigation such as add, edit and etc... within .link component")

  def gheader(assigns) do
    ~H"""
    <div class="fixed navbar bg-primary text-primary-content w-full z-10 top-0 left-0">
      <div class="navbar-start">
        <button :if={@back != []} type="button" class="btn btn-ghost normal-case text-xl">
          <%= render_slot(@back) %>
        </button>
      </div>
      <div class="navbar-center">
        <a class="btn btn-ghost normal-case text-4xl">
          <%= @title %>
        </a>
      </div>
      <div class="navbar-end">
        <button :if={@actions != []} type="button" class="btn btn-ghost normal-case text-xl">
          <%= render_slot(@actions) %>
        </button>
      </div>
    </div>
    """
  end
  ...
end
```

## bottom_tab
ベースはこちらになります

https://daisyui.com/components/bottom-navigation/#bottom-navigation


attr titleがlinksのタイトルと一致すると borderdクラスを付与するようにしています

```phoenix/lib/spotties_web/components/navigation.ex
defmodule SpottiesWeb.Components.Navigation do
  use Phoenix.Component

  ...
  attr(:title, :string, default: "")

  def bottom_tab(assigns) do
    ~H"""
    <div class="btm-nav">
      <%= for {title, path} <- links() do %>
        <a href={path} class={if @title == title, do: "active", else: ""}>
          <button>
            <span class="btm-nav-label"><%= title %></span>
          </button>
        </a>
      <% end %>
    </div>
    """
  end

  defp links() do
    [
      {"Route", "/routes"},
      {"Setting", "/users/settings"}
    ]
  end

  def active?(false), do: ""
  def active?(true), do: "bordered"
end
```
## PJ全体で読み込む
CoreComponentですが lib/xx_webに追加することでPJ全体で使えるようにしてあるので
共通コンポーネントであるNavigationも追加しましょう
```diff_elixir:phoenix/lib/spotties_web.ex:L83-98
defmodule SpottiesWeb do
  ...
  defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import SpottiesWeb.CoreComponents
+      import SpottiesWeb.Components.Navigation
      import SpottiesWeb.Gettext

      # Shortcut for generating JS commands
      alias Phoenix.LiveView.JS

      # Routes generation with the ~p sigil
      unquote(verified_routes())
    end
  end
  ...
end
```

## routes, routes/:id, users/setting に追加
随所に追加していきます

```diff_elixir:phoenix/lib/spotties_web/live/route_live/index.html.heex
+ <.gheader title="Routes">
+  <:actions>
+    <.link patch={~p"/routes/new"} class="btn btn-ghost normal-case text-4xl">+</.link>
+  </:actions>
+ </.gheader>

<.header>
  Listing Routes
-  <:actions>
-    <.link patch={~p"/routes/new"}>
-      <.button>New Route</.button>
-    </.link>
-  </:actions>
</.header>

<.table id="routes" rows={@routes} row_click={&JS.navigate(~p"/routes/#{&1}")}>
  <:col :let={route} label="Name"><%= route.name %></:col>
  <:action :let={route}>
    <div class="sr-only">
      <.link navigate={~p"/routes/#{route}"}>Show</.link>
    </div>
    <.link patch={~p"/routes/#{route}/edit"}>Edit</.link>
  </:action>
  <:action :let={route}>
    <.link patch={~p"/routes/#{route}/delete"}>Delete</.link>
  </:action>
</.table>

+ <.bottom_tab title="Route" />

<.modal>
...
</.modal>
```

```diff_elixir:phoenix/lib/spotties_web/live/route_live/show.html.heex
+ <.gheader title="Route">
+  <:back>
+    <.link navigate={~p"/routes"}>Back</.link>
+  </:back>
+  <:actions>
+    <.link patch={~p"/routes/#{@routes}/show/edit"} phx-click={JS.push_focus()}>
+      Edit
+    </.link>
+  </:actions>
+ </.gheader>

<.header>
  Route <%= @route.id %>
  <:subtitle>This is a shelf record from your database.</:subtitle>
-  <:actions>
-    <.link patch={~p"/routes/#{@route}/show/edit"} phx-click={JS.push_focus()}>
-      <.button>Edit</.button>
-    </.link>
-  </:actions>
</.header>

<.list>
  <:item title="Name"><%= @route.name %></:item>
</.list>

- <.back navigate={~p"/routes"}>Back to Routes</.back>
+ <.bottom_tab title="route" />

<.modal>
...
</.modal>
```

```diff_elixir:phoenix/lib/spotties_web/live/user_settings_live.ex
defmodule SpottiesWeb.UserSettingsLive do
  use SpottiesWeb, :live_view

  alias Spotties.Accounts

  def render(assigns) do
    ~H"""
+    <.gheader title="Setting" />
    <.header>Change Email</.header>

    <.simple_form>
    ...
    </.simple_form>

    <.header>Change Password</.header>

    <.simple_form>
    ...
    </.simple_form>

    <.button class="mt-4"><.link href={~p"/users/log_out"} method="delete">Log out</.link></.button>
+    <.bottom_tab title="Setting" />
    """
  end
  ...
end
```

# 完成！

![117046a83b1c47ac6bbb24a6080c0476.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/62ee0cad-b8cd-afe4-db0b-b6797fdfafe5.gif)

タブもナビゲーションもちゃんと動いてますね

# 最後に
だいぶ長くなってしましましたが無事CRUDとナビゲーションコンポーネントを作成してスマホっぽいUIができました

これを元に誰かアプリを作って申請を通してみた記事とか書いてくれないかな（チラ

本記事は以上になりますありがとうございました