ElixirDesktop AndroidでGPS Loggerを作ってみた

# はじめに

本記事はElixirDesktopを使用してGPS Loggingを行うAndroidアプリを作成した記事になります

- プロジェクトをつくる
- Androidでビルドする
- saasからTailwind,DaisyUIに差し替える
- DaisyUIでHeaderとBottomTabを実装する
- GPS ロギング画面を実装する
- AndroidのWebViewで位置情報を取得できるようにする
- JS HookでLiveView側に反映させる
- GPSログをDBに保存する

# ElixirDesktopとは
WxWidgetとPhoenixを組み合わせてwebサーバーを起動することなくスタンドアローンでネイティブアプリを起動・実装できるライブラリです

サンプルアプリはDBにはSqlite３を使っています

https://github.com/elixir-desktop/desktop

また、自分はまだ成功していませんが、iOSでも同一のコードでネィティブアプリを起動することができます
iOS: https://github.com/elixir-desktop/ios-example-app
Android: https://github.com/elixir-desktop/android-example-app

# 下準備
※アプリバージョンが上がっていて、README通りに進めれば問題なくできるようになりました 2023/04/21
install Android Studio + NDK
ADV Managerでデバイスを１つ作成

https://qiita.com/rkunihiro/items/433ec719396154f170f2

asdfだとうまく行かないので brew installします
また kerlというErlangのバージョン管理ライブラリも追加します
kerlでElixirDesktop用にカスタムされたotp24のErlangをインストールします
```
brew install elixir kerl
mkdir -p ~/projects/
kerl build git https://github.com/diodechain/otp.git diode/beta 24.beta
kerl install 24.beta ~/projects/24.beta
```

# プロジェクト作成
では早速作っていこうと思いますが、まだ`mix phx.new`に該当するコマンドは無いようなので
サンプルのREADMEに沿ってやってみましょう

```README.md
How to build & run
Install the beta OTP build *(see known issues)

Install Android Studio + NDK.

Go to "Files -> New -> Project from Version Control" and enter this URL: https://github.com/elixir-desktop/android-example-app/

Update the run_mix to activate the correct Erlang/Elixir version during build.

Connect your Phone to Android Studio

Start the App
```
githubからプロジェクトを作成するを選択し
![スクリーンショット 2022-07-26 20.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/7175a597-3468-d99f-672e-6b62e57977aa.png)
AndroidサンプルのリポジトリURLを入れてディレクトリ名をandroid-qiitaにしてcloneを実行します
![スクリーンショット 2022-07-26 20.40.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ac813d3c-976d-c6bf-70be-349a8da53ab2.png)

プロジェクトが作成されるとgradleの同期が走りますのでしばらく待ちましょう
左下に小さく進捗が表示されています

# Androidアプリとしてビルドする
完了したら下準備で作成したバーチャルデバイスでRunを押してビルドを実行します

残念ながら失敗しますので順次解決してきましょう

## Error1 Javaのバージョンがあっていない
![スクリーンショット 2022-07-26 20.48.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/78ef3566-5240-a9e5-0623-09099160bfc4.png)

```
An exception occurred applying plugin request [id: 'com.android.application']
> Failed to apply plugin 'com.android.internal.application'.
   > Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8.
     You can try some of the following options:
       - changing the IDE settings.
       - changing the JAVA_HOME environment variable.
       - changing `org.gradle.java.home` in `gradle.properties`.

```
こちらのエラーが出た場合はこちらを参考に解消しましょう

https://qiita.com/takahirom/items/5e8d7b69e873edb3dcaf

## Error2 run_mixが成功しない
次はこんなエラーが出るかと思います
```
Execution failed for task ':app:buildNum'.
> Process 'command '../run_mix'' finished with non-zero exit value 1
```
### 手動でやってみる
コンソールで開いて以下の箇所まで移動し `run_mix`を実行します

```
cd ~/StudioProjects/android-qiita/app
../run_mix
```

### Elixirが起動しない
asdfを使っている方は下のようなエラーが出るかと思われます

```
{"init terminating in do_boot",{undef,[{elixir,start_cli,[],[]},{init,start_em,1,[]},{init,do_boot,3,[]}]}}
init terminating in do_boot ({undef,[{elixir,start_cli,[],[]},{init,start_em,1,[]},{init,do_boot,3,[]}]})
```

これは run_mixでkerlでerlangを切り替えたのですが、asdfに入っているElixirが対応していない場合に出ます。 `brew install elixir`でインストールしたElixirだと大丈夫なので、asdfを無効化にしましょう

direnvを使用します

https://typememo.jp/tech/chromium-build-macos-should-be-disable-asdf/

```
brew install direnv
touch .envrc
```
プロジェクト内にファイルを作成したら以下の内容を追加します userは適宜自分のユーザー名に変えてください

```
PATH_rm /Users/user/.asdf/shims
PATH_rm /Users/user/.asdf/bin
```

完了したら以下のコマンドを実行します

```
direnv allow
```

### 再度実行
../run_mix を実行すると以下のようなエラーになるのでphoenix部分のフォルダに移動移動してdeps.getを実行します
`Unchecked dependencies for environment prod:`

```
cd elixir-app
mix deps.get
cd ..
```

### wxが無い！
elixir-desktopライブラリに含まれているはずですが無いので最新版を追加します

```elixir-app/mix.exs
  defp deps do
    [
      {:ecto_sqlite3, "~> 0.7"},
      # {:desktop, path: "../desktop"},
      {:desktop, github: "elixir-desktop/desktop", tag: "v1.4.0"},
      {:wx, "~> 1.0.10", hex: :bridge, targets: [:android, :ios]}, #　追加
      ...
    ]
  end
```

```
cd elixir-app
mix deps.get
cd ..
../run_mix
```

### SASSのファイルがダウンロードできない！
スタイリングで使っているsassのバイナリがリンク切れでDLできないそうなのでバージョン等を上げて対応します

```
** (RuntimeError) could not download dart_sass for architecture: arm-apple-darwin21.2.0
```

```app/elixir-app/mix.exs
defmodule Todo.MixProject do
  use Mix.Project
  ...
  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # Assets
      {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
      {:dart_sass, "~> 0.5", runtime: Mix.env() == :dev}, # 0.5に上げる

      # Credo
      {:credo, "~> 1.5", only: [:dev, :test], runtime: false}
    ]
  end
end
```

```elixir-app/config/config.exs
config :dart_sass,
  version: "1.49.11", # バージョン上げる
  default: [
    args: ~w(css/app.scss ../priv/static/assets/app.css),
    cd: Path.expand("../assets", __DIR__)
  ]
```

```
cd elixir-app
mix deps.get
cd ..
../run_mix
```
これでelixir側のビルドは完了しました
### ビルド失敗するのでrun_mixを何もしない別のものに変える
まだAndroid側のビルドは失敗するので Android側で実行するシェルスクリプトを空にしましょう

```
cp ../run_mix .
```

```bash:run_mix
#!/bin/bash
set -e
```
ビルド時に実行するファイルを変更します
```build.gradle
lugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    task buildNum(type: Exec, description: 'Update Elixir App') {
        // さっきの空シェルスクリプトを実行するようにする
        commandLine './run_mix', 'package.mobile'        
    }
    ...
}
```

gradleファイルを変更してrunボタンが無効化されている場合は、右上のsync project with gradle Filesボタンを押します

完了したら 緑のrunボタンを押します

## 起動できた！
![スクリーンショット 2022-07-26 21.53.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3f2f480c-f542-ba34-1797-333635da01be.png)

色々やりましたがこちらの記事の方が手順が少ないのでこちらのほうがいいかもしれません

https://qiita.com/torifukukaiou/items/5458458e2ec1bcee5152

# SASSからTailwind, DaisyUIに差し替える
せっかく起動するようにしましたが tailwindとdaisyuiでスタイリングするのでsass消していきます

最初にscssをcssにリネームします
scssのファイル読み込みを消して、scss形式のコメントアウトを解除してコメントインします

```elixir-app/assets/app.css
@use "../node_modules/nprogress/nprogress.css";
@import "./todo.scss"; /* 消す */

/* LiveView specific classes for your customizations */
.invalid-feedback {
  color: #a94442;
  display: block;
  margin: -1rem 0 2rem; /* コメントイン */
}
...
```
こちらを参考にtailwindを設定していきます

https://tailwindcss.com/docs/guides/phoenix


```diff_elixir:elixir-app/mix.exs
  defp aliases do
    [
      "assets.deploy": [
        "phx.digest.clean --all",
        "esbuild default --minify",
-        "sass default --no-source-map --style=compressed",
         "tailwind default --minify", # 追加
        "phx.digest"
      ]
    ]
  end

 defp deps do
    [
      ...
      # Phoenix
      {:phoenix, "~> 1.6"},
      {:phoenix_live_view, "~> 0.17.4"}, # 0.17に上げる
      ...
      # Assets
      {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
-      {:dart_sass, "~> 0.5", runtime: Mix.env() == :dev},
       {:tailwind, "~> 0.1", runtime: Mix.env() == :dev}, # 追加

      # Credo
      {:credo, "~> 1.5", only: [:dev, :test], runtime: false}
    ]
  end
```

```diff_elixir:elixir-app/config/config.exs
- config :dart_sass,
-  version: "1.49.11",
-  default: [
-    args: ~w(css/app.scss ../priv/static/assets/app.css),
-    cd: Path.expand("../assets", __DIR__)
-  ]
# 以下追加
config :tailwind, version: "3.1.6", default: [
  args: ~w(
    --config=tailwind.config.js
    --input=css/app.css
    --output=../priv/static/assets/app.css
  ),
  cd: Path.expand("../assets", __DIR__)
]
```

```diff_elixir:elixir-app/config/dev.exs
config :todo_app, TodoWeb.Endpoint,
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
-    sass:
-      {DartSass, :install_and_run,
-       [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]}
     tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} # 追加
  ],
  ...
```

```
cd elixir-app
mix deps.get
mix tailwind.install
```

これでtailwind.config.jsも作られるので、次のdaisyuiをインストールしていきます

```
cd assets
npm install daisyui
cd ..
```

```elixir-app/assets/tailwind.config.js
let plugin = require('tailwindcss/plugin')

module.exports = {
  content: [
    './js/**/*.js',
    '../lib/*_web.ex',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
    require("daisyui"),　 // 追加
    ...
  ]
}
```

Androidだといちいちビルドが必要なので
UIの開発はホットリロードしてくれるデスクトップで行っていきます

```
iex -S mix
```

![スクリーンショット 2022-07-26 22.51.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3b6e8921-4956-ac64-fa60-4537313be864.png)
sassが剥げたプレーンなtailwind状態になりました

# DaisyUIでHeaderとBottomTabを実装する
tailwindとdaisyuiが入ったのでスマホっぽいUIを組んでいきましょう

共通コンポーネントとして作るので
live/componentsフォルダを作ります

## Header
daisyuiにあるのでそれを使います

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

```elixir-app/lib/todo_web/live/components/header.ex
defmodule TodoWeb.Components.Header do
  @moduledoc """
  Application Header
  """

  use TodoWeb, :live_component

  @impl true
  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end
end
```
描画部分 assignにtitleを受け取り、真ん中に表示します
```leex:elixir-app/lib/todo_web/live/components/header.html.heex
<div class="navbar bg-primary text-primary-content">
  <div class="navbar-start">
  </div>
  <div class="navbar-center">
    <a class="btn btn-ghost normal-case text-4xl"><%= @title %></a>
  </div>
  <div class="navbar-end">
  </div>
</div>
```

## BottomTab
こちらも　ｄaisyuiにあるので使います

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

```elixir-app/lib/todo_web/live/components/bottom_tab.ex
defmodule TodoWeb.Components.BottomTab do
  @moduledoc """
  Bottom Tab Navigation
  """

  use TodoWeb, :live_component

  @impl true
  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end
end
```

まだ他のページがないので全てトップへのリンクにしておきます
titleが一致した場合はactive状態にして線を表示するようにします

```elixir:elixir-app/lib/todo_web/live/components/bottom_tab.html.heex
<div class="btm-nav">
  <button class={if @title == "Home", do: "active", else: ""}>
    <a href="/">      
      <span class="btm-nav-label">Home</span>
    </a>
  </button>
  <button class={if @title == "GPS", do: "active", else: ""}>
    <a href="/">
      <span class="btm-nav-label">GPS</span>
    </a>
  </button>
  
  <button class={if @title == "Log", do: "active", else: ""}>
    <a href="/">
      <span class="btm-nav-label">Log</span>
    </a>
  </button>
</div>
```

## todo_liveでの読み込み
コンポーネントができたので実際に使ってみましょう
コンポーネントで使うのでtitleをアサインします

```elixir-app/lib/todo_web/live/todo_live.ex
defmodule TodoWeb.TodoLive do
  @moduledoc """
    Main live view of our TodoApp. Just allows adding, removing and checking off
    todo items
  """
  use TodoWeb, :live_view
  alias TodoWeb.Components.{Header, BottomTab}
  
  @impl true
  def mount(_args, _session, socket) do
    todos = TodoApp.Todo.all_todos()
    TodoApp.Todo.subscribe()

    {
      :ok,
      socket
      |> assign(:title, "Home")
      |> assign(todos: todos)
    }
  end
  ...
end
```
サンプルのコードはleexなのでheexにリネームします
コンポーネントを上下に配置して、コンテンツはHome画面なので良さげなHeroUnitを貼り付けます

https://daisyui.com/components/hero/#centered-hero
```elixir:elixir-app/lib/todo_web/live/todo_live.html.heex
<.live_component module={Header} id="header" title={@title} />
<div class="hero min-h-screen bg-base-200">
  <div class="hero-content text-center">
    <div class="max-w-md">
      <h1 class="text-5xl font-bold">Hello there</h1>
      <p class="py-6">Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem quasi. In deleniti eaque aut repudiandae et a id nisi.</p>
      <button class="btn btn-primary">Get Started</button>
    </div>
  </div>
</div>
<.live_component module={BottomTab} id="bottom_tab" title={@title} />
```

![スクリーンショット 2022-07-26 23.40.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/217d3774-5bf9-b165-6b7a-17b5c6b5486f.png)

いい感じになりました！

# GPS ロギング画面を実装する

GPSロギング画面を作りましょう

```elixir-app/lib/todo_web/live/gps_live.ex
defmodule TodoWeb.GpsLive do
  use TodoWeb, :live_view
  alias TodoWeb.Components.{Header, BottomTab}

  @impl true
  def mount(_args, _session, socket) do
    {
      :ok,
      socket
      |> assign(:title, "GPS")
      |> assign(:lat, 0)
      |> assign(:lng, 0)
    }
  end
end
```

Statという変動する数値を表示するのに良さそうなコンポーネントを使用します

https://daisyui.com/components/stat/#vertical

```elixir:elixir-app/lib/todo_web/live/gps_live.html.heex
<div id="gps" class="w-full h-screen bg-base-200">
  <.live_component module={Header} id="header" title={@title} />
  <div class="hero bg-base-200 mt-20">
    <div class="hero-content text-center">
      <div class="max-w-md">
        <h1 class="text-5xl font-bold">GPS Logger</h1>
        <div class="mt-20">
          <button class="btn btn-primary">Start Logging</button>
          <button class="btn btn-secondary">Stop Logging</button>
        </div>
      </div>
    </div>
  </div>
  <div class="stats stats-vertical shadow mt-20">  
    <div class="stat w-screen">
      <div class="stat-title">Latitude</div>
      <div class="stat-value text-primary"><%= @lat %></div>
    </div>
    
    <div class="stat w-screen">
      <div class="stat-title">Longtitude</div>
      <div class="stat-value text-secondary"><%= @lng %></div>
    </div>  
  </div>

  <.live_component module={BottomTab} id="bottom_tab" title={@title} />
</div>
```

画面を作ったらルーティングとBottomTabに追加します


```elixir-app/lib/todo_web/router.ex
defmodule TodoWeb.Router do
  use TodoWeb, :router

  ...
  scope "/", TodoWeb do
    pipe_through :browser
    live "/", TodoLive
    live "/gps", GpsLive #追加
  end
end
```

２つめのリンクをGPSに変更します

```elixir:elixir-app/lib/todo_web/live/components/bottom_tab.html.heex
<div class="btm-nav">
  <button class={if @title == "Home", do: "active", else: ""}>
    <a href="/">      
      <span class="btm-nav-label">Home</span>
    </a>
  </button>
  <button class={if @title == "GPS", do: "active", else: ""}>
    <%= live_redirect to: Routes.live_path(@socket, TodoWeb.GpsLive) do %>    
      <span class="btm-nav-label">GPS</span>
    <% end %>
  </button>
  
  <button class={if @title == "Log", do: "active", else: ""}>
    <a href="/">
      <span class="btm-nav-label">Log</span>
    </a>
  </button>
</div>
```

GPSロギング部分の画面ができました

![スクリーンショット 2022-07-27 11.44.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ddce345f-f1bc-cc29-fc6b-1f9d7e6337cf.png)

# AndroidのWebViewで位置情報を取得できるようにする
このままだとAndroidのWebViewでは位置情報が取得できないので
Android側の設定を行います
220行目あたりにWebView周りの設定があるので seGeolocationEnabled(true)で位置情報取得を有効化します

権限要求を追加します
```src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.elixirdesktop.example"
    android:installLocation="internalOnly">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
...
```

```app/src/main/java/io/elixirdesktop/example/Bridge.kt
class Bridge(private val applicationContext : Context, private var webview : WebView) {

    ...
    fun setWebView(_webview: WebView) {
        webview = _webview
        val settings = webview.settings
        settings.setGeolocationEnabled(true) //　追加
        settings.javaScriptEnabled = true
        settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL
        settings.useWideViewPort = true
        // enable Web Storage: localStorage, sessionStorage
        settings.domStorageEnabled = true        

        if (lastURL.isNotBlank()) {
            webview.post { webview.loadUrl(lastURL) }
        }
    }
    ...
}
```

kotlin,Javaは詳しくないので以下からコードをコピペ、AndroidStudioだと貼り付け時にJavaコードをKotlinに変えてくれるみたいでそのままで良かったです
すごいな・・・

https://qiita.com/gksdyd88/items/feeaccdef401ce5644c7

```app/src/main/java/io/elixirdesktop/example/MainActivity.kt
package io.elixirdesktop.example

// 以下を追加
import android.Manifest
import android.content.pm.PackageManager
import android.webkit.GeolocationPermissions
import android.webkit.WebChromeClient
import androidx.core.app.ActivityCompat

class MainActivity : Activity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        checkPermission() // 位置情報の使用承諾画面を出す
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.browser.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView, url: String) {
                if (binding.browser.visibility != View.VISIBLE) {
                    binding.browser.visibility = View.VISIBLE
                    binding.splash.visibility = View.GONE
                }
            }
        }

        // パーミッション周りの記述
        binding.browser.webChromeClient = object : WebChromeClient() {
            override fun onGeolocationPermissionsShowPrompt(
                origin: String?,
                callback: GeolocationPermissions.Callback?
            ) {
                //tell the webview that permission has granted
                callback!!.invoke(origin, true, true)
            }
        }
        if (bridge != null) {
            // This happens on re-creation of the activity e.g. after rotating the screen
            bridge!!.setWebView(binding.browser)
        } else {
            // This happens only on the first time when starting the app
            bridge = Bridge(applicationContext, binding.browser)
        }
    }

    // パーミッション許可のモーダル部分の内容
    private fun checkPermission() {
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
            && ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // パーミッションの許可を取得する
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION
                ),
                1000
            )
        }
    }
    ...
}
```

同じ方法でWebViewの設定をすればジャイロセンサーやカメラなネイティブな部分も使えるかと思います


# JS HookでLiveView側に反映させる
WebViewで位置情報を取得できるようになったので、JSの GeolocationAPIで取得していきます

https://developer.mozilla.org/ja/docs/Web/API/Geolocation_API/Using_the_Geolocation_API#examples

PhoenixLiveViewには相互にデータと関数の送信を行える JS Hookというものがあるのでそちらを使用します

https://hexdocs.pm/phoenix_live_view/0.17.0/js-interop.html#client-hooks


マウント時に
startボタンを押したらロギングを開始し、更新があったらPhoenix側のupdateを実行する
stopボタンを押したら停止する
イベントを追加します

```elixir-app/assets/js/hooks.js
let Hooks = {};
Hooks.Gps = {
  mounted() {
    this.handleEvent("start_logging", () => {
      watchID = navigator.geolocation.watchPosition((position) => {
        this.pushEvent("update", { lat: position.coords.latitude, lng: position.coords.longitude });
      });
      window.watchID = watchID
    })

    this.handleEvent("stop_logging", () => {
      navigator.geolocation.clearWatch(window.watchID);
    })
  }
}

export default Hooks
```

hookを作成したらapp.jsで読み込みます

```elixir-app/assets/js/app.js
import Hooks from "./hooks"
let liveSocket = new LiveSocket("/live", Socket, { 
  hooks: Hooks, 
  params: { _csrf_token: csrfToken } 
})
```

hooksを作成したら id属性を付けたタグで読み込み
StartとStopボタンにクリックイベントを追加します

```elixir:elixir-app/lib/todo_web/live/gps_live.html.heex
<div id="gps" class="w-full h-screen bg-base-200" phx-hook="Gps">
  <.live_component module={Header} id="header" title={@title} />
  <div class="hero bg-base-200 mt-20">
    <div class="hero-content text-center">
      <div class="max-w-md">
        <h1 class="text-5xl font-bold">GPS Logger</h1>
        <div class="mt-20">
          <button phx-click="start" class="btn btn-primary">Start Logging</button>
          <button phx-click="stop" class="btn btn-secondary">Stop Logging</button>
        </div>
      </div>
    </div>
  </div>
...
</div>
```

クリックイベント時とGPSの値が送信された時の処理を追加します

JS側のイベントを発火させる際には push_eventを実行し、何も引数がなくてもMapを第３引数にいれます
JS側から発火されるupdateイベントもhandle_eventで実装します

```elixir-app/lib/todo_web/live/gps_live.ex
defmodule TodoWeb.GpsLive do
  use TodoWeb, :live_view
  alias TodoWeb.Components.{Header, BottomTab}

  @impl true
  def mount(_args, _session, socket) do
    {
      :ok,
      socket
      |> assign(:title, "GPS")
      |> assign(:lat, 0)
      |> assign(:lng, 0)
    }
  end

  # 以下追加
  @impl true
  def handle_event("start", _, socket) do
    {:noreply, push_event(socket, "start_logging", %{})}
  end

  @impl true
  def handle_event("stop", _, socket) do
    {:noreply, push_event(socket, "stop_logging", %{})}
  end

  @impl true
  def handle_event("update", %{"lat" => lat, "lng" => lng} = params, socket) do
    {
      :noreply,
      socket
      |> assign(:lat, lat)
      |> assign(:lng, lng)
    }
  end
end
```

# GPSログをDBに保存する
ログを取得できるようになったので、その値をDBに保存したいと思います
phx.gen.liveで行いたいですが、現在のファイル構成だと齟齬が出るので修正していきます
VSCode等のプロジェクト内コード検索で 
TodoWebとなっている箇所をTodoAppWebに置き換えます
todo_webとなっている箇所を todo_app_webに置き換えます

todo_web.exをtodo_app_web.exにリネーム
lib/todo_web を lib/todo_app_webにリネーム
準備が整ったら以下を実行します

```
mix phx.gen.live Loggers Position positions lat:float lng:float
```

routesが吐き出されるのでrouter.exに貼り付けます

```elixir-app/lib/todo_app_web/router.ex
defmodule TodoAppWeb.Router do
  use TodoAppWeb, :router
  ...
  scope "/", TodoAppWeb do
    pipe_through(:browser)
    live("/", TodoLive)
    live("/gps", GpsLive)
    # 以下追加
    live("/positions", PostionLive.Index, :index)
    live("/positions/new", PostionLive.Index, :new)
    live("/positions/:id/edit", PostionLive.Index, :edit)

    live("/positions/:id", PostionLive.Show, :show)
    live("/positions/:id/show/edit", PostionLive.Show, :edit)
  end
end
```

```app/elixir-app/lib/todo_app_web/live/gps_live.ex
defmodule TodoAppWeb.GpsLive do
  use TodoAppWeb, :live_view
  alias TodoAppWeb.Components.{Header, BottomTab}
  alias TodoApp.Loggers #追加

  @impl true
  def handle_event("update", %{"lat" => lat, "lng" => lng} = params, socket) do
    # create_positionを挟み込む
    case Loggers.create_postion(params) do
      {:ok, _postion} ->
        {
          :noreply,
          socket
          |> assign(:lat, lat)
          |> assign(:lng, lng)
        }

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

index.html.heexの上下にHeaderとBottomTabを付けます

```elixir:elixir-app/lib/todo_app_web/live/postion_live/index.html.heex
<.live_component module={Header} id="header" title={@title} />
<h1>Listing Positions</h1>
...
<.live_component module={BottomTab} id="bottom_tab" title={@title} />
```

titleをアサインします

```elixir-app/lib/todo_app_web/live/postion_live/index.ex
defmodule TodoAppWeb.PostionLive.Index do
  use TodoAppWeb, :live_view

  alias TodoApp.Loggers
  alias TodoApp.Loggers.Postion
  alias TodoAppWeb.Components.{Header, BottomTab}

  @impl true
  def mount(_params, _session, socket) do
    {
      :ok,
      socket
      |> assign(:title, "Log")
      |> assign(:positions, list_positions())
    }
  end
  ...
```

タブにリンクを追加します

```elixir:elixir-app/lib/todo_app_web/live/components/bottom_tab.html.heex
<div class="btm-nav">
  ...  
  <button class={if @title == "Log", do: "active", else: ""}>
    <%= live_redirect to: Routes.postion_index_path(@socket, :index) do %>    
      <span class="btm-nav-label">Log</span>
    <% end %>
  </button>
</div>
```

AndroidアプリだとAndroid側のsqliteを叩く必要があり、
マイグレーション周りがまだ整備されていない感じなので今回はSQLベタ書きにします
```elixir-app/lib/todo_app/repo.ex
defmodule TodoApp.Repo do
  use Ecto.Repo, otp_app: :todo_app, adapter: Ecto.Adapters.SQLite3

  def initialize() do
    Ecto.Adapters.SQL.query!(__MODULE__, """
        CREATE TABLE IF NOT EXISTS positions (
          id INTEGER PRIMARY KEY,
          lat REAL,
          lng REAL
        )
    """)
  end
end
```

timestampを削除

```elixir-app/lib/todo_app/loggers/postion.ex
defmodule TodoApp.Loggers.Postion do
  use Ecto.Schema
  import Ecto.Changeset

  schema "positions" do
    field :lat, :float
    field :lng, :float

    timestamps() # 削除
  end
  ..
end

```
elixir-app内でビルドしたコードだとandroidだと動かないので一度cleanしてからビルドし直します

```
mix deps.clean --all
mix deps.get
cd ..
../run_mix
```

エミュレーターがうまく動かないのでfakeGPSの値を取得するようにします

https://medium.com/@msaudi/android-test-location-services-and-gps-with-fake-gps-data-mock-locations-in-emulators-or-real-df211de4d891

https://developer.android.com/studio/debug/dev-options?hl=ja
developer optionsはビルド番号を７回タップして有効化してください
developer optionsでplaystoreからインストールしたfake gpsを設定してください


# demo

![27c198fb05194b750262ef8c8d276fec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/a6c8ec35-15e5-db5b-0d6f-6dc455d423c4.gif)

# 最後に

というわけでElixirDesktopでGPSロガーを作ってみました　
いかがでしょうか、まだExpo(react native)やFlutterほど簡単に開発できるほどこなれていませんが
整備されてきてこの部分が解消されたら、tailwind、phx.gen.live等を使って爆速でアプリを作れるようになる　
しかも Elixirでそんな楽しそうな未来が見えてワクワクしますね

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

# 参考サイト
https://github.com/elixir-desktop/desktop
https://github.com/elixir-desktop/ios-example-app
https://github.com/elixir-desktop/android-example-app
https://qiita.com/rkunihiro/items/433ec719396154f170f2
https://qiita.com/takahirom/items/5e8d7b69e873edb3dcaf
https://typememo.jp/tech/chromium-build-macos-should-be-disable-asdf/
https://tailwindcss.com/docs/guides/phoenix
https://daisyui.com/components/
https://qiita.com/the_haigo/items/22ca888ab2b19b558828
https://qiita.com/torifukukaiou/items/5458458e2ec1bcee5152
https://sakura-bird1.hatenablog.com/entry/20130610/1370864805
https://www.gesource.jp/weblog/?p=7303
https://qiita.com/gksdyd88/items/feeaccdef401ce5644c7
https://hexdocs.pm/phoenix_live_view/0.17.0/js-interop.html#client-hooks
https://developer.mozilla.org/ja/docs/Web/API/Geolocation_API/Using_the_Geolocation_API#examples