LoginSignup
7
3

More than 1 year has passed since last update.

ElixirDesktop AndroidでGPS Loggerを作ってみた

Last updated at Posted at 2022-07-27

はじめに

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

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

ElixirDesktopとは

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

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

また、自分はまだ成功していませんが、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でデバイスを1つ作成

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
AndroidサンプルのリポジトリURLを入れてディレクトリ名をandroid-qiitaにしてcloneを実行します
スクリーンショット 2022-07-26 20.40.19.png

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

Androidアプリとしてビルドする

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

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

Error1 Javaのバージョンがあっていない

スクリーンショット 2022-07-26 20.48.08.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`.

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

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を使用します

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 .
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

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

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を設定していきます

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
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__)
]
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
sassが剥げたプレーンなtailwind状態になりました

DaisyUIでHeaderとBottomTabを実装する

tailwindとdaisyuiが入ったのでスマホっぽいUIを組んでいきましょう

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

Header

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

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を受け取り、真ん中に表示します

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

こちらも daisyuiにあるので使います

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-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を貼り付けます

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

いい感じになりました!

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という変動する数値を表示するのに良さそうなコンポーネントを使用します

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

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

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

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に変えてくれるみたいでそのままで良かったです
すごいな・・・

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で取得していきます

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

マウント時に
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-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を第3引数にいれます
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-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-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://developer.android.com/studio/debug/dev-options?hl=ja
developer optionsはビルド番号を7回タップして有効化してください
developer optionsでplaystoreからインストールしたfake gpsを設定してください

demo

27c198fb05194b750262ef8c8d276fec.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

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3