【Android】ExpandableListViewの使い方
Kotlinが公式になったというので5, 6年ぶりにAndroid再入門。
想定するデータセット
親子関係のデータ。ここではこんなんを想定しています。
folders
id | name |
---|---|
1 | fodler1 |
2 | fodler2 |
3 | fodler3 |
categories
id | folder_id | name |
---|---|---|
1 | 1 | category1 |
2 | 1 | category2 |
3 | 1 | category3 |
4 | 2 | category1 |
5 | 2 | category2 |
6 | 2 | category3 |
実装概要
データセットのマップリストとかマップのマップリストとか用意してアダプタに差し込みます。
この辺りを書くのがJavaだと冗長でつらたんですがKotlinはval
で宣言できるしnew
もいらんのですね。
// java List<Map<String, String>> folders = new ArrayList<Map<String, String>>(); List<List<Map<String, String>>> categoriesOfFolder = new ArrayList<List<Map<String, String>>>();
// kotlin val folders = mutableListOf<Map<String, String>>() val categoriesOfFolder = mutableListOf<MutableList<Map<String, String>>>()
このリストをアダプタに設定します。引数はリファレンス見てください。
SimpleExpandableListAdapter | Android Developers
val adapter = SimpleExpandableListAdapter( this, folders, android.R.layout.simple_expandable_list_item_1, arrayOf(KEY_PARENT), intArrayOf(android.R.id.text1, android.R.id.text2), categoriesOfFolder, android.R.layout.simple_expandable_list_item_1, arrayOf(KEY_CHILD), intArrayOf(android.R.id.text1, android.R.id.text2) )
リストのイベントは
- OnGroupClickListener
- OnChildClickListener
- OnGroupExpandListener
- OnGroupCollapseListener
などで拾ってください。
画面イメージ
このままだとリストが開いているのか閉じているのか分かりにくいので、開閉しているステートを取ってビューの色変えたりしたいですよね。ListenerでもいけそうだけどAdapterカスタマイズして書くのがスマートだった気がします。そこはまたの機会に。
ソースコード
package com.example import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.widget.ExpandableListView import android.widget.SimpleExpandableListAdapter class MainActivity : AppCompatActivity() { companion object { const val KEY_PARENT = "FOLDER" const val KEY_CHILD = "CATEGORY" } private lateinit var mAdapter: SimpleExpandableListAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) loadFolders() } private fun loadFolders() { val folders = mutableListOf<Map<String, String>>() val categoriesOfFolder = mutableListOf<MutableList<Map<String, String>>>() for (i in 1..5) { val category = mapOf(KEY_PARENT to KEY_PARENT + i.toString()) folders.add(category) val categories = mutableListOf<Map<String, String>>() for (j in 1..3) { val todo = mapOf(KEY_CHILD to KEY_CHILD + j.toString()) categories.add(todo) } categoriesOfFolder.add(categories) } mAdapter = SimpleExpandableListAdapter( this, folders, android.R.layout.simple_expandable_list_item_1, arrayOf(KEY_PARENT), intArrayOf(android.R.id.text1, android.R.id.text2), categoriesOfFolder, android.R.layout.simple_expandable_list_item_1, arrayOf(KEY_CHILD), intArrayOf(android.R.id.text1, android.R.id.text2) ) val listView = (findViewById(R.id.todoListView) as ExpandableListView) listView.setAdapter(mAdapter) } }
Kotlinスタートブック -新しいAndroidプログラミング
- 作者: 長澤太郎
- 出版社/メーカー: リックテレコム
- 発売日: 2016/07/13
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
PostgreSQLにアクセスする
標準ライブラリであるdatabase/sql
を利用してPostgreSQLにアクセスしてみます。
データベースドライバのインストール
SQLDrivers にドライバの一覧があります。
PostgreSQLのドライバがいくつかありますが今回はlib/pq
を利用します。
$ go get github.com/lib/pq
データベースへの接続
sql.Open()
でデータベースオブジェクトを生成します。この関数は*sql.DB
を返します。
db, err := sql.Open("postgres", "host=localhost port=32768 user=postgres dbname=golang_dev sslmode=disable")
sql.Open()
はドライバや接続文字列のバリデーションをしているだけで実際は接続しません。
db.Ping()
したりクエリを発行した時点でコネクションが確立します。
クエリの発行
参照系は複数件取得するdb.Query()
、1件取得するdb.QueryRow()
を利用します。
rows, err := db.Query("select id, name from users where id = $1", 1) for rows.Next() { err := rows.Scan(&id, &name) }
rows.Scan()
は渡した変数の型に応じてタイプキャストしてくれます。
例えばidはデータベース上bigint
で定義していますが変数id string
を渡せば文字列型で取得できます。(あまりよろしくないと思いますが)
キャストできない型は当然エラーになります。
sql: Scan error on column index 1: converting driver.Value type string ("foo") to a int: invalid syntax
更新系はdb.Exec()
を利用します。
db.Exec("insert into users (name) values ($1)", "foo")
プリペアドステートメントを使えるdb.Prepare()
とかもあります。
関数化とかしてませんがまとめるとこんな感じ。
package main import ( "database/sql" _ "github.com/lib/pq" "log" ) func main() { // DBオブジェクト生成 db, err := sql.Open("postgres", "host=localhost port=32768 user=postgres dbname=golang_dev sslmode=disable") if err != nil { log.Fatal(err) } defer db.Close() // 疎通確認 err = db.Ping() if err != nil { log.Fatal(err) } // 登録 stmt, err := db.Prepare("insert into users (name) values ($1)") if err != nil { log.Fatal(err) } res, err := stmt.Exec("Foo") if err != nil { log.Fatal(err) } rowCnt, err := res.RowsAffected() if err != nil { log.Fatal(err) } log.Printf("affected = %d\n", rowCnt) // 取得 var ( id int name string ) rows, err := db.Query("select id, name from users") if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { err := rows.Scan(&id, &name) if err != nil { log.Fatal(err) } log.Println(id, name) } err = rows.Err() if err != nil { log.Fatal(err) } }
Amazon Route53でドメインを購入する
はじめに
Macbook Pro 2017(タッチバーなし)
スペースグレイいけてますね。バタフライキーボードも思ったより快適です。
新しいもの手に入れるとテンション上がりますよね!
本題
突然ですがR53でドメインを購入してみます。
購入手順
ドメイン管理コーンソールを開いて「Register Domain」を選択します。
取得したいドメイン名を入力すると利用可否の一覧が表示されます。
購入するドメインを「Add to card」すると料金が年額で表示されるのでOKなら次へ進みます。
ドメインの管理者情報を入力します。
Whoisで個人情報が表示されないように「Privacy Protection」で「Hide contact information if the TLD registry, and the registrar, allow it」が選択されていることを必ず確認しましょう。
TLDによっては表示されてしまうこともあるので注意
入力した情報を確認し購入手続きを完了します。
手続きが完了すると「In Progress」状態になります。
数十分で「Registered domains」に表示されます。
確認用のメールが飛んできているはずなので、そこからリンクを踏んで登録完了です。
Whoisで確認
ドメインの更新
ドメイン詳細をみると「Auto Renew: Enabled」となっているので、1年ごとに自動更新されるようです。
自動更新したくない場合は「Disabled」に切り替えましょう。
VRゴーグルを作ってみた
大晦日
何してましたか?僕は連日飲みすぎて気分悪いので、引きこもって工作してました。
VR!VR!
VR流行ってますよね、たぶん。気になってます。
Oculus RiftとかPSVRとかいろいろあると思うんですが、少々お高い!
合わない人は合わないらしいんで、買って後悔するのもあれだなーとか思いながら、都内でVR体験できるとこあるけど行くのめんどいなーとか。
そしたらこんなんあるんですね。
Linkcool Google Cardboard(グーグル・カードボード)3Dメガネ 3d Vrメガネ 組み立て式 NFCタグとベルト付き
- 出版社/メーカー: linkcool
- メディア: Wireless Phone Accessory
- この商品を含むブログを見る
ダンボール製やん → 自作できるよね?ってことで調べてみたら簡単に作れるんですねー。
というわけで数十年ぶりに工作してみました。ハマったところだけ強調しときます。
いざ100均
ダイソーでミニルーペをゲッツします。
売り場
老眼鏡とかサングラスの横 (10分くらいハマりました)
取り外し方
ひねれば取れるとかあったんですが、たぶん最近のやつは接着剤でがっちりくっついてるっぽいんですね。
まずマイナスドライバーでテコの原理を利用して分解します。
更にマイナスドライバーでテコの原理を利用して1枚目のレンズを分離します
そして2枚目のレンズは周りをニッパーなどでバッキバキにし、マイナスドライバーでテコの原理を利用してとり外します。
箱作り
お菓子の箱でもなんでもいいんですが、懐かしい工作用紙が売ってたのでそれで作ることにしました。
で、レンズ間の距離が重要ですのでこれだけはしっかりと調整します。
VR対応のアプリとか表示しながら、レンズどうしを近づけたり目やスマホとレンズの距離を調整してベストな間隔を決めます。
ちなみに僕の場合はレンズ間が3.5cm、スマホとレンズの距離が5cmくらいでいい感じに見えました。
組み立て
だいぶ雑いですが完成しました!
いざVR!
VRの元動画とか見たときは何やただの立体視だかマジカル・アイみたいなもんか、思ってたんですが...
こんなダンボールのくせに!溢れ出す臨場感!!
次は天空でVR楽しんでこようかと思います。
良いお年を!!
Oculus Rift cv1 製品版 2016 オキュラス リフト (Oculus Rift cv1 製品版) [並行輸入品]
- 出版社/メーカー: Oculus
- メディア: エレクトロニクス
- この商品を含むブログを見る
Elixir - Agent
Agent
Agent
は状態を保持します。
# 空のリストを初期状態として生成 {:ok, agent} = Agent.start_link(fn -> [] end) # 現在の状態(list)を引数として新しい状態を生成する Agent.update(agent, fn list -> ["eggs" | list] end) # 現在の状態を取得する Agent.get(agent, fn list -> list end) # プロセスを終了する Agent.stop(agent) Process.alive?(agent) # > false
Agentを利用してキーバリューストアを実装してみます。
# kv.exs defmodule KV do def start_link do Agent.start_link(fn -> %{} end) end def get(kv, key) do Agent.get(kv, &Map.get(&1, key)) end def put(kv, key, value) do Agent.put(kv, &Map.put(&1, key, value)) end def delete(kv, key) do Agent.get_and_update(kv, &Map.pop(&1, key)) end end
iexから実行してみます。
$ iex kv.exs iex> {:ok, kv} = KV.start_link {:ok, #PID<0.64.0>} iex> KV.put(kv, :hello, "world") :ok iex> KV.get(kv, :hello) "world" iex> KV.delete(kv, :hello) "world" iex> KV.get(kv, :hello) nil
Process間でAgentを共有してみる
一つのカウンターをマルチプロセスでカウントアップしてみます。
# counter.exs defmodule Counter do def start_link do Agent.start_link(fn -> 0 end) end def increment(counter) do Agent.update(counter, fn x -> x + 1 end) end def count(counter) do Agent.get(counter, fn x -> x end) end end defmodule CountProcess do def start_link(counter, parent) do {:ok, spawn_link(fn -> loop(counter, parent) end)} end defp loop(counter, parent) do count = Counter.count(counter) IO.puts "#{inspect(self)} #{count}" cond do count < 10_000 -> Counter.increment(counter) loop(counter, parent) true -> IO.puts "#{inspect(self)} #{count} Completed!!" end end end defmodule ConcurrentCounter do def run(n) do {:ok, counter} = Counter.start_link Enum.each(1..n, fn x -> CountProcess.start_link(counter, self) end) end end
$ iex counter.exs iex> ConcurrentCounter.run(4) ... #PID<0.71.0> 9999 #PID<0.70.0> 9999 #PID<0.72.0> 10003 #PID<0.69.0> 10003 #PID<0.71.0> 10003 #PID<0.70.0> 10003 #PID<0.72.0> 10003 Completed!! #PID<0.69.0> 10003 Completed!! #PID<0.71.0> 10003 Completed!! #PID<0.70.0> 10003 Completed!!
複数のプロセスが起動してカウントアップしているのが確認できました。
が、同期を取っていないので10,000を超えてカウントされてしまっています。
Javaだとスレッドでsynchronizedとかして同期させた記憶があるのですが、Elixirだとsend-receive? Task? GenServer? この辺りを利用するのかな?
Elixir - Process
Getting StartedrのProcessについてサクッとまとめました。
- spawn ... 子プロセスを生成する
- spawn_link ... 子プロセスを生成する(例外発生時に共倒れ)
- send, receive ... プロセス間のメッセージ送受信
- Task ... spawnのラップ、結果も返してくれる
- State ... send, receiveの再帰呼び出しで実現
Processes
すべてのコードはプロセス中で実行されます。プロセスはお互いに隔離されており、並列で実行されメッセージパッシングを通じてやり取りします。並列処理のベースであるだけではなく、これにより分散型のフォールトトレラントプログラムを構築する役割があります。
ElixirのプロセスはOSのそれと混同すべきではありません。他のプログラミング言語のスレッドとは違い、メモリとCPUの観点からすると非常に軽量です。そのため、数十万のプロセスを同時に実行することも珍しくありません。
spawn
spawn/1
は新しいプロセスを生成しPIDを返します。
生成されたプロセスは関数を実行し終了します。
iex> pid = spawn fn -> 1 + 2 end #PID<0.58.0> iex> Process.alive?(pid) false
プロセス自身のIDはself/0
で取得できます。
iex> self() #PID<0.56.0> iex> Process.alive?(self()) true
send と receive
プロセス間のメッセージのやり取りにはsend/2
とreceive/1
を利用できます。
以下は自分自身にメッセージを送信するサンプルです。
defmodule ProcessSample do def send_message(msg) do # 自分自身のメールボックスに保存 send self(), msg end def receive_message do # 送信されたメッセージを受け取る receive do {:msg, msg} -> msg after # タイムアウトを設定 # ※設定しない場合はパターンマッチするメッセージを受信するまで待ち続ける 1_000 -> 'timeout' end end end # 送信したメッセージがパターンマッチする場合 ProcessSample.send_message({:msg, 'hello, world'}) IO.puts ProcessSample.receive_message # マッチしない場合 ProcessSample.send_message('hello, world') IO.puts ProcessSample.receive_message
$ elixir process.exs hello, world timeout
これをspawn/1
を利用してプロセス間でやり取りするように書き換えてみます。
defmodule InterProcessSample do def run do parent = self() # PIDを送信する子プロセスを生成 spawn fn -> send(parent, {:pid, inspect(self())}) end end def receive_message do receive do {:pid, pid} -> "I'm #{pid}." after 1_000 -> 'timeout' end end end InterProcessSample.run IO.puts InterProcessSample.receive_message
$ elixir process.exs I'm #PID<0.52.0>.
Links
プロセスの生成で実際によく利用されるのはspawn_link/1
です。
spawn/1
は子プロセスが例外を起こしても親プロセスは実行され続けますが、
spawn_link/1
は親プロセスも共に終了します。
# spawn.exs # spawn/1は子プロセスで例外が起きてもプログラムは終了しない # spawn fn -> raise "oops" end # spawn_link/1はプログラムが終了する spawn_link fn -> raise "oops" end receive do :hello -> '子プロセスが例外を起こすまで待つ' end
この仕組みはフォールトトレラントシステムを構築するのに重要な役割を果たします。 Elixirではプロセスの死活監視、生成を行うSupervisorにリンクすることが多いです。
他のプログラミング言語ではtry/catch
等で例外処理をしますが、
このような仕組みがあるため、例外は放っておくのが実際のところです。
Tasks
Task
はspawnより詳細なエラーレポートを提供します。
Task.start/1
, Task.start_link/1
はPIDと併せて結果も返してくれるためSupervisorで利用されます。
iex> spawn fn -> raise "oops" end #PID<0.58.0> iex> 10:40:03.817 [error] Process #PID<0.58.0> raised an exception ** (RuntimeError) oops :erlang.apply/2 iex> Task.start fn -> raise "oops" end {:ok, #PID<0.60.0>} iex> 10:40:11.695 [error] Task #PID<0.60.0> started from #PID<0.56.0> terminating ** (RuntimeError) oops (elixir) lib/task/supervised.ex:94: Task.Supervised.do_apply/2 (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3 Function: #Function<20.54118792/0 in :erl_eval.expr/5> Args: []
State
アプリケーションの設定や読み込んだファイルの内容等の状態を保持する場合にもプロセスを利用します。
以下はキーバリューストアのサンプルです。
# kv.exs defmodule KV do def start_link do # 空のMapとともに子プロセスを生成 Task.start_link(fn -> loop(%{}) end) end defp loop(map) do receive do {:get, key, caller} -> # caller(呼び出し元)にメッセージを送信 send caller, Map.get(map, key) loop(map) {:put, key, value} -> loop(Map.put(map, key, value)) end end end
これをiexで実行してみます。
flush/0
はメールボックス内のメッセージを出力します。
$ iex kv.exs iex> {:ok, pid} = KV.start_link {:ok, #PID<0.62.0>} iex> send pid, {:put, :hello, :world} {:put, :hello, :world} iex> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.60.0>} iex> flush :world :ok
また、pidに名称を与えて実行することも可能です。
iex> {:ok, pid} = KV.start_link {:ok, #PID<0.63.0>} iex> Process.register(pid, :kv) true iex> send :kv, {:put, :hello, :world} {:put, :hello, :world}
大抵の場合これらのパターンを自前で実装することはなく、
例えばAgent
等の抽象化されたものを利用します。
iex> {:ok, pid} = Agent.start_link(fn -> %{} end) {:ok, #PID<0.72.0>} iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end) :ok iex> Agent.get(pid, fn map -> Map.get(map, :hello) end) :world
Elixirのパターンマッチ
JSONを取得するサンプル
APIサーバー
http://localhost:3000/capitals.jsonにアクセスすると首都一覧を返します。
const express = require('express') const app = express() app.get('/capitals.json', (request, response) => { response.contentType('application/json') const capitals = [ { country: 'China', city: 'Beijing' }, { country: 'Japan', city: 'Tokyo' }, { country: 'Mongolia', city: 'Ulan Bator' }, { country: 'North Korea', city: 'Pyhongyang' }, { country: 'South Korea', city: 'Seoul' }, ] response.send(JSON.stringify(capitals)) }) app.listen(3000)
クライアント
HTTPoisonのサンプル通りに記述してJSONを取得します。
defmodule JsonHandler do def main(args) do fetch_capitals end def fetch_capitals do url = "http://localhost:3000/capitals.json" case HTTPoison.get(url) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> IO.puts body {:ok, %HTTPoison.Response{status_code: 404}} -> IO.puts "Not found :(" {:error, %HTTPoison.Error{reason: reason}} -> IO.puts "Unknown error" end end end
実行
$ ./json_handler [{"country":"China","city":"Beijing"},{"country":"Japan","city":"Tokyo"},{"country":"Mongolia","city":"Ulan Bator"},{"country":"North Korea","city":"Pyhongyang"},{"country":"South Korea","city":"Seoul"}]
最初のcaseにマッチするのでレスポンスが出力されました。
次にサーバーを起動しない状態で実行すると...
$ ./json_handler Unknown error
最後のcaseにマッチするのでメッセージが出力されます。
ではマッチしないケースはどうなるでしょうか。
503を返すようにサーバーを書き換えて実行してみます。
response.status(503)
response.send()
$ ./json_handler ** (CaseClauseError) no case clause matching: {:ok, %HTTPoison.Response{body: "", headers: [{"X-Powered-By", "Express"}, {"Content-Type", "application/json"}, {"Date", "Thu, 18 Aug 2016 04:39:06 GMT"}, {"Connection", "keep-alive"}, {"Content-Length", "0"}], status_code: 503}} (error_handling_sample) lib/error_handling_sample.ex:9: ErrorHandlingSample.fetch_capitals/0 (elixir) lib/kernel/cli.ex:76: anonymous fn/3 in Kernel.CLI.exec_fun/2
マッチするケースがないよ、ってことでクラッシュしてしまいます。
というわけで200, 404以外にマッチするパターンを追加します。
case HTTPoison.get(url) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> IO.puts body {:ok, %HTTPoison.Response{status_code: 404}} -> IO.puts "Not found :(" {:ok, %HTTPoison.Response{status_code: status_code}} -> IO.puts "Status code #{status_code}" {:error, %HTTPoison.Error{reason: reason}} -> IO.puts "Unknown error" end
これで503も拾えました。
$ ./json_handler
Status code 503
case文を関数化する
もう少しElixirらしく?、各パターンを関数に書き換えます。
要注意なのがcase文と同じく先にある関数からマッチがされていくという点です。
つまり関数の記述順序も気にしないといけません。(まぁこの場合はパターンが悪いと思いますが)
defmodule JsonHandler do def main(args) do fetch_capitals end def fetch_capitals do url = "http://localhost:3000/capitals.json" HTTPoison.get(url) |> process_response end defp process_response({:ok, %HTTPoison.Response{status_code: 200, body: body}}) do IO.puts body end defp process_response({:ok, %HTTPoison.Response{status_code: 404}}) do IO.puts "Not found :(" end defp process_response({:ok, %HTTPoison.Response{status_code: status_code}}) do IO.puts "Status code #{status_code}" end defp process_response({:error, %HTTPoison.Error{reason: reason}}) do IO.puts "Unknown error" end end
クラッシュ怖い
マッチするパターンがないとクラッシュするって言われると、あらゆるパターン(特にエラー処理)を想定しなければならず、正常系よりも異常系の処理を書くことがメインのお仕事になってきます。
そんなの辛い。
"Let it crash."
Elixir(とそのベースになっているErlang)のプロセスは生成のためのコストが小さいため「下手にエラー処理するコードを書いてプロセスを維持するよりはさっさとクラッシュさせて、それに続く処理の中で対策して再起動したほうがよい」という思想があります。
クラッシュ上等というわけですね。
というわけで今度はクラッシュしまくるスタイルに書き換えてみる予定です。