5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

mix phx.newしたプロジェクトをElixirDesktop Android app化する方法(最小の変更)

Posted at

Elixir Desktop化の困った状態

Elixir Desktop化する方法が本家のドキュメントに含まれていました(以下、your_first_app.mdと呼びます)。

このドキュメント通りにすればよいとおもったんですが、これだけでは、うまくいきませんでした。

  • サンプルのtodo app(以下、todo appと呼びます)は、動作はするが、your_first_app.mdとは異なる点が多い(Applicationがtodo_app.exに含まれている等、Phx.newで作成したものとDesktop化に関係のない違いがある。

  • your_first_app.mdは、変更の要が書かれているが、実際には、この変更だけでは、動作しない。SQLite3の起動などは含まれてない。

という困った状況でした。

こんな中、ElixirDesktop Androidでphx.newで作成したプロジェクトを動かすがブレークスルーとなり、your_first_app.mdに不足している点を明らかにできました。
勉強も兼ねて、phx.newの素の状態にできるだけ近い状態でDesktop化してみます。

この調査は、Androidアプリ化を前提にしてます。
DBはSQLite3を使用して、起動する設定を含めてみます。

変更点

変更点の詳細は、最後のdiff結果を参照してください。

ファイル your_first_app.md記載箇所 追加した内容、コメント
mix.exs (1) desktop追加 wx、ecto_sqlite3も追加した
application.ex (2) Desktop.Window追加
(5)identify_default_locale追加
SQLite3の初期化の処理追加が必要だった
endpint.ex (3) Desktop.Endpointに変更 ドキュメント通り
config.exs (4) http: [ip: {127, 0, 0, 1}, port: 0]とserver: true secret_key_base: :crypto.strong_rand_bytes(64)を追加
prod.exs       host: "example.com"行削除。Desktop化とは関係なくProd環境設定として必要な変更
runtime.exs ファイル削除。Androidで、App起動時に環境変数を読まない設定にするため必要
repo.ex adapterをSQLite3、DB初期化のSQLを記載
build.sh         ビルドしてapp.zip.xz作成するスクリプト(おまけ)

application.ex,repo.exについて、補足説明しておきます。

application.ex

SQLite3の起動

SQLite3の起動前の準備、起動、起動後のDB初期化の処理を追加します。

まず参考にした、todo appの処理をみてみます。
todo appでは、application.exは存在しておらず、todo_app.exで同様の処理をおこなっています。

todo_app.ex
  def config_dir() do
    Path.join([Desktop.OS.home(), ".config", "todo"])
  end

  @app Mix.Project.config()[:app]

  def start(:normal, []) do
    Desktop.identify_default_locale(TodoWeb.Gettext)
    File.mkdir_p!(config_dir()) # SQLite3の初期ファイル用ディレクトリ作成

    Application.put_env(:todo_app, TodoApp.Repo,
      database: Path.join(config_dir(), "/database.sq3")
    ) # SQLite3の初期ファイル作成

    {:ok, sup} = Supervisor.start_link([TodoApp.Repo], name: __MODULE__, strategy: :one_for_one)# ←-Repo起動定
    TodoApp.Repo.initialize()# DB初期化

  # 以下、そのほかのモジュールの起動

    {:ok, _} = Supervisor.start_child(sup, TodoWeb.Sup)

    {:ok, _} =
      Supervisor.start_child(sup, {
        Desktop.Window,
        [
          app: @app,
          id: TodoWindow,
          title: "TodoApp",
          size: {600, 500},
          icon: "icon.png",
          menubar: TodoApp.MenuBar,
          icon_menu: TodoApp.Menu,
          url: &TodoWeb.Endpoint.url/0
        ]
      })
  end

Supervisor.start_link()では、ElixirDesktop.Repoだけ先に起動して、ElixirDesktop.Repo.initialize()でDBを初期化し、その後、Supervisor.start_child()で、他のモジュールを起動しています。

この仕組みを、application.exに移植します。同様の記述をしてみたんですが、なぜか、Supervisor.start_childでエラーが発生しうまくいきませんでした。
DynamicSupervisorのstart_childを使ったを参考に、DynamicSupervisorを使って、同様の処理を行っています。

etsについて

todo appでは、sessionにetsを使う設定をしています。これは行った好ましいものなのか、必要ないものなのか判断つきませんでした。今回は、phx.newからの最小限の変更ということで含めていません。

application.ex
  def config_dir() do
    Path.join([Desktop.OS.home(), ".config", "elixir_desktop"])
  end

  def setup_sqlite3_env() do
    File.mkdir_p!(config_dir())
    Application.put_env(:elixir_desktop, ElixirDesktop.Repo,
      database: Path.join(config_dir(), "/database.sq3")
    )
  end

  @impl true
  def start(_type, _args) do
    Desktop.identify_default_locale(ElixirDesktopWeb.Gettext)
    opts = [strategy: :one_for_one, name: ElixirDesktop.Supervisor]
    Supervisor.start_link([{DynamicSupervisor, strategy: :one_for_one, name: ElixirDesktop.DynamicSupervisor}], opts)

    setup_sqlite3_env()
    DynamicSupervisor.start_child(ElixirDesktop.DynamicSupervisor, ElixirDesktop.Repo)
    ElixirDesktop.Repo.initialize()

    children = [
      # Start the Telemetry supervisor
      ElixirDesktopWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: ElixirDesktop.PubSub},
      # Start the Endpoint (http/https)
      ElixirDesktopWeb.Endpoint,
      # Start a worker by calling: ElixirDesktop.Worker.start_link(arg)
      # {ElixirDesktop.Worker, arg}
      {
        # After your other children
        # Starting Desktop.Windows
        Desktop.Window,
        [
          app: :elixir_desktop,
          id: ElixirDesktopWindow,
          url: &ElixirDesktopWeb.Endpoint.url/0
        ]
      }
    ]
    children
     |> Enum.map(&DynamicSupervisor.start_child(ElixirDesktop.DynamicSupervisor, &1))
     |> Enum.at(-1)
  end

参考 Phx.new直後のapplication.ex

application.ex
def start(_type, _args) do
    children = [
      # Start the Ecto repository
      ElixirDesktop.Repo,
      # Start the Telemetry supervisor
      ElixirDesktopWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: ElixirDesktop.PubSub},
      # Start the Endpoint (http/https)
      ElixirDesktopWeb.Endpoint
      # Start a worker by calling: ElixirDesktop.Worker.start_link(arg)
      # {ElixirDesktop.Worker, arg}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: ElixirDesktop.Supervisor]
    Supervisor.start_link(children, opts)
  end

repo.ex

アプリを起動した時、SQLiteはまっさらな状態で、mix ecto.createにあたる処理を行う必要があります。
CREATE TABLEのSQL文を実行するようにしています。
この例では、todo appと同じものを記載しています。実際のアプリに合わせて変更する必要があります。

  • Desktop化しない状態で、mix ecto.createを実行
  • sqlite3 database.db .dumpを実行してDBの内容をSQLで表示。Create TABLEの行を、repo.exに転記する。

このような方法で作成できます。

repo.ex
defmodule ElixirDesktop.Repo do
  use Ecto.Repo, otp_app: :elixir_desktop, adapter: Ecto.Adapters.SQLite3

  def initialize() do
    Ecto.Adapters.SQL.query!(__MODULE__, """
        CREATE TABLE IF NOT EXISTS todos (
          id INTEGER PRIMARY KEY,
          text TEXT,
          status TEXT
        )
    """)
  end
end

build.sh

Desktop化の変更点とは関係ありませんが、おまけです。

appのビルドは、run_mixスクリプトを切り出してきて以下のbuild.shで行ってます。

set -e
. ~/projects/24.beta/activate
export MIX_ENV=prod
export MIX_TARGET=android
mix assets.deploy
mix release --overwrite
cd _build/android_prod/rel/elixir_desktop/
zip -0r ../../../../app.zip lib/ releases/ --exclude "*.so"
xz -9ef ../../../../app.zip

変更点のdiff

変更点の全体のdiffです

diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..d3acc05
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,9 @@
+set -e
+. ~/projects/24.beta/activate
+export MIX_ENV=prod
:...skipping...
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..d3acc05
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,9 @@
+set -e
+. ~/projects/24.beta/activate
+export MIX_ENV=prod
+export MIX_TARGET=android
+mix assets.deploy
+mix release --overwrite
+cd _build/android_prod/rel/elixir_desktop/
+zip -0r ../../../../app.zip lib/ releases/ --exclude "*.so"
+xz -9ef ../../../../app.zip
\ No newline at end of file
diff --git a/config/config.exs b/config/config.exs
index 69fc678..b7faf31 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -12,7 +12,9 @@ config :elixir_desktop,
 
 # Configures the endpoint
 config :elixir_desktop, ElixirDesktopWeb.Endpoint,
-  url: [host: "localhost"],
+  http: [ip: {127, 0, 0, 1}, port: 0],
+  secret_key_base: :crypto.strong_rand_bytes(64),
+  server: true,
   render_errors: [view: ElixirDesktopWeb.ErrorView, accepts: ~w(html json), layout: false],
   pubsub_server: ElixirDesktop.PubSub,
   live_view: [signing_salt: "WznVGqh4"]
diff --git a/config/prod.exs b/config/prod.exs
index 33f2ea5..6216540 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -10,7 +10,6 @@ import Config
 # which you should run after static files are built and
 # before starting your production server.
 config :elixir_desktop, ElixirDesktopWeb.Endpoint,
-  url: [host: "example.com", port: 80],
   cache_static_manifest: "priv/static/cache_manifest.json"
 
 # Do not print debug messages in production
diff --git a/config/runtime.exs b/config/runtime.exs
deleted file mode 100644
index 14c36d5..0000000
--- a/config/runtime.exs
+++ /dev/null
@@ -1,73 +0,0 @@
-import Config
-
-# config/runtime.exs is executed for all environments, including
-# during releases. It is executed after compilation and before the
-# system starts, so it is typically used to load production configuration
-# and secrets from environment variables or elsewhere. Do not define
-# any compile-time configuration in here, as it won't be applied.
-# The block below contains prod specific runtime configuration.
-if config_env() == :prod do
-  database_url =
-    System.get_env("DATABASE_URL") ||
-      raise """
-      environment variable DATABASE_URL is missing.
-      For example: ecto://USER:PASS@HOST/DATABASE
-      """
-
-  config :elixir_desktop, ElixirDesktop.Repo,
-    # ssl: true,
-    # socket_options: [:inet6],
-    url: database_url,
-    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
-
-  # The secret key base is used to sign/encrypt cookies and other secrets.
-  # A default value is used in config/dev.exs and config/test.exs but you
-  # want to use a different value for prod and you most likely don't want
-  # to check this value into version control, so we use an environment
-  # variable instead.
-  secret_key_base =
-    System.get_env("SECRET_KEY_BASE") ||
-      raise """
-      environment variable SECRET_KEY_BASE is missing.
-      You can generate one by calling: mix phx.gen.secret
-      """
-
-  config :elixir_desktop, ElixirDesktopWeb.Endpoint,
-    http: [
-      # Enable IPv6 and bind on all interfaces.
-      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
-      # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
-      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
-      ip: {0, 0, 0, 0, 0, 0, 0, 0},
-      port: String.to_integer(System.get_env("PORT") || "4000")
-    ],
-    secret_key_base: secret_key_base
-
-  # ## Using releases
-  #
-  # If you are doing OTP releases, you need to instruct Phoenix
-  # to start each relevant endpoint:
-  #
-  #     config :elixir_desktop, ElixirDesktopWeb.Endpoint, server: true
-  #
-  # Then you can assemble a release by calling `mix release`.
-  # See `mix help release` for more information.
-
-  # ## Configuring the mailer
-  #
-  # In production you need to configure the mailer to use a different adapter.
-  # Also, you may need to configure the Swoosh API client of your choice if you
-  # are not using SMTP. Here is an example of the configuration:
-  #
-  #     config :elixir_desktop, ElixirDesktop.Mailer,
-  #       adapter: Swoosh.Adapters.Mailgun,
-  #       api_key: System.get_env("MAILGUN_API_KEY"),
-  #       domain: System.get_env("MAILGUN_DOMAIN")
-  #
-  # For this example you need include a HTTP client required by Swoosh API client.
-  # Swoosh supports Hackney and Finch out of the box:
-  #
-  #     config :swoosh, :api_client, Swoosh.ApiClient.Hackney
-  #
-  # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
-end
diff --git a/lib/elixir_desktop/application.ex b/lib/elixir_desktop/application.ex
index 00e8209..dc5bf5a 100644
--- a/lib/elixir_desktop/application.ex
+++ b/lib/elixir_desktop/application.ex
@@ -5,25 +5,50 @@ defmodule ElixirDesktop.Application do
 
   use Application
 
+  def config_dir() do
+    Path.join([Desktop.OS.home(), ".config", "elixir_desktop"])
+  end
+
+  def setup_sqlite3_env() do
+    File.mkdir_p!(config_dir())
+    Application.put_env(:elixir_desktop, ElixirDesktop.Repo,
+      database: Path.join(config_dir(), "/database.sq3")
+    )
+  end
+
   @impl true
   def start(_type, _args) do
+    Desktop.identify_default_locale(ElixirDesktopWeb.Gettext)
+    opts = [strategy: :one_for_one, name: ElixirDesktop.Supervisor]
+    Supervisor.start_link([{DynamicSupervisor, strategy: :one_for_one, name: ElixirDesktop.DynamicSupervisor}], opts)
+
+    setup_sqlite3_env()
+    DynamicSupervisor.start_child(ElixirDesktop.DynamicSupervisor, ElixirDesktop.Repo)
+    ElixirDesktop.Repo.initialize()
+
     children = [
-      # Start the Ecto repository
-      ElixirDesktop.Repo,
       # Start the Telemetry supervisor
       ElixirDesktopWeb.Telemetry,
       # Start the PubSub system
       {Phoenix.PubSub, name: ElixirDesktop.PubSub},
       # Start the Endpoint (http/https)
-      ElixirDesktopWeb.Endpoint
+      ElixirDesktopWeb.Endpoint,
       # Start a worker by calling: ElixirDesktop.Worker.start_link(arg)
       # {ElixirDesktop.Worker, arg}
+      {
+        # After your other children
+        # Starting Desktop.Windows
+        Desktop.Window,
+        [
+          app: :elixir_desktop,
+          id: ElixirDesktopWindow,
+          url: &ElixirDesktopWeb.Endpoint.url/0
+        ]
+      }
     ]
-
-    # See https://hexdocs.pm/elixir/Supervisor.html
-    # for other strategies and supported options
-    opts = [strategy: :one_for_one, name: ElixirDesktop.Supervisor]
-    Supervisor.start_link(children, opts)
+    children
+     |> Enum.map(&DynamicSupervisor.start_child(ElixirDesktop.DynamicSupervisor, &1))
+     |> Enum.at(-1)
   end
 
   # Tell Phoenix to update the endpoint configuration
diff --git a/lib/elixir_desktop/repo.ex b/lib/elixir_desktop/repo.ex
index 4134b77..cb80bc6 100644
--- a/lib/elixir_desktop/repo.ex
+++ b/lib/elixir_desktop/repo.ex
@@ -1,5 +1,13 @@
 defmodule ElixirDesktop.Repo do
-  use Ecto.Repo,
-    otp_app: :elixir_desktop,
-    adapter: Ecto.Adapters.Postgres
+  use Ecto.Repo, otp_app: :elixir_desktop, adapter: Ecto.Adapters.SQLite3
+
+  def initialize() do
+    Ecto.Adapters.SQL.query!(__MODULE__, """
+        CREATE TABLE IF NOT EXISTS todos (
+          id INTEGER PRIMARY KEY,
+          text TEXT,
+          status TEXT
+        )
+    """)
+  end
 end
diff --git a/lib/elixir_desktop_web/endpoint.ex b/lib/elixir_desktop_web/endpoint.ex
index b1d4315..df9cbdc 100644
--- a/lib/elixir_desktop_web/endpoint.ex
+++ b/lib/elixir_desktop_web/endpoint.ex
@@ -1,5 +1,5 @@
 defmodule ElixirDesktopWeb.Endpoint do
-  use Phoenix.Endpoint, otp_app: :elixir_desktop
+  use Desktop.Endpoint, otp_app: :elixir_desktop
 
   # The session will be stored in the cookie and signed,
   # this means its contents can be read but not tampered with.
diff --git a/mix.exs b/mix.exs
index 2ebfa13..5b7f1be 100644
--- a/mix.exs
+++ b/mix.exs
@@ -33,10 +33,9 @@ defmodule ElixirDesktop.MixProject do
   # Type `mix help deps` for examples and options.
   defp deps do
     [
+      {:ecto_sqlite3, "~> 0.7"},
       {:phoenix, "~> 1.6.2"},
       {:phoenix_ecto, "~> 4.4"},
-      {:ecto_sql, "~> 3.6"},
-      {:postgrex, ">= 0.0.0"},
       {:phoenix_html, "~> 3.0"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},
       {:phoenix_live_view, "~> 0.16.0"},
@@ -48,7 +47,9 @@ defmodule ElixirDesktop.MixProject do
       {:telemetry_poller, "~> 1.0"},
       {:gettext, "~> 0.18"},
       {:jason, "~> 1.2"},
-      {:plug_cowboy, "~> 2.5"}
+      {:plug_cowboy, "~> 2.5"},
+      {:desktop, "~> 1.4"},
+      {:wx, "~> 1.0.10", hex: :bridge, targets: [:android, :ios]}
     ]
   end
 

参考サイト
https://qiita.com/the_haigo/items/0fd907d24fdde1905aa2
https://github.com/elixir-desktop/desktop/blob/main/guides/your_first_app.md
https://elixirforum.com/t/how-to-add-a-children-to-supervisor-start-link-dynamically/24329/6

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?