Slides of Tetiana Dushenkivska, Chief Inspector at CleverBunny, Creator of Elixircards, at Elixir Club Evening 4 Online, 19.11.2020
Video of the talk you can watch here https://www.youtube.com/watch?v=EkC7GB89pXI
Next conference - http://www.elixirkyiv.club/
Description of the presentation:
Agile.. Agile.. Agile.. It’s almost become a buzz word. Many companies claim to do Agile, but in reality far from it. In this talk, I would like to share how I used Agile principles to build a flashcards quiz with Phoenix LiveView. It’s not a walkthrough tutorial on how to build something with LiveView, it’s a set of the most interesting decisions around writing code I’ve encountered on my way
Follow us on social networks @ElixirClubUA and #ElixirClubUA
Announce and materials from conf - https://www.fb.me/ElixirClubUA
News - https://twitter.com/ElixirClubUA
Photo and free atmosphere - https://www.instagram.com/ElixirClubUA
*Organizer’s channel - https://t.me/incredevly
26. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/router.ex
scope "/", QuizWeb do
...
live "/:conf_name", ConfLive
end
27. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
def mount(%{"conf_name" => conf_name}, _, socket) do
state = init_state(conf_name)
{:ok, assign(socket, state)}
end
defp init_state(conf_name) do
conf = Quiz.get_by_url(conf_name)
...
end
36. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
defp is_correct(answer, correct_answer) do
answer == correct_answer
end
37. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
defp is_correct(answer, correct_answer) do
answer == correct_answer
end
iex> "hello" == "hello "
false
38. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
defp is_correct(answer, correct_answer) do
answer == correct_answer
end
39. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
defp is_correct(answer, correct_answer) do
answer == correct_answer
end
defp strip(answer) do
answer
|> String.downcase()
|> String.trim
end
59. @tetiana12345678 Kiev Elixir Club 2020
Readability issue
2 long files conf_live.ex game.ex
state
new state
PUREIMPURE
60. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
def handle_event("next_card", _, socket) do
updated_game = Game.update_state(socket.assigns.game)
{:noreply, assign(socket, game: updated_game)}
end
61. @tetiana12345678 Kiev Elixir Club 2020
lib/game.ex
def init_state(conf) do
%Game{
card_index: 1,
number_of_cards: Enum.count(conf.cards),
card: hd(conf.cards),
cards: tl(conf.cards)
}
end
Before
62. @tetiana12345678 Kiev Elixir Club 2020
lib/game.ex
After
def init_state(conf) do
cards = setup_cards(conf.cards)
%Game{
card_index: 1,
number_of_cards: Enum.count(conf.cards),
card: hd(conf.cards),
cards: tl(conf.cards)
}
end
63. @tetiana12345678 Kiev Elixir Club 2020
lib/game.ex
After
defp setup_cards(cards) do
add_card_index = fn card, i ->
{Card.update_card(card, %{index: i}), i + 1}
end
{cards, index} = Enum.map_reduce(cards, 1, add_card_index)
cards
end
64. @tetiana12345678 Kiev Elixir Club 2020
lib/card.ex
After defmodule Quiz.Card do
use Ecto.Schema
import Ecto.Changeset
schema "cards" do
...
field :index, :integer, virtual: true
end
def update_card(card, params) do
card
|> cast(params, [:index])
|> apply_changes()
end
end
66. @tetiana12345678 Kiev Elixir Club 2020
After
lib/game.ex
def update_state(game) do
%Game{
card_index: game.number_of_cards - Enum.count(game.cards),
card: hd(conf.cards),
cards: tl(conf.cards)
}
end
67. @tetiana12345678 Kiev Elixir Club 2020
Readability issue
2 long files
index.html.leex
state1.html.leex
state2.html.leex
state3.html.leex
…
68. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
def render(assigns) do
Phoenix.View.render(QuizWeb.QuizView, "index.html", assigns)
end
95. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
def mount(%{"conf_name" => conf_name}, socket) do
...
topic = conf_name
if connected?(socket) do
Phoenix.PubSub.subscribe(QuizWeb.PubSub, topic)
end
{:ok, socket}
end
96. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
def handle_event("email", params, socket) do
add_new_score(socket, params)
{:noreply, socket}
end
97. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
defp add_new_score(socket, params) do
case Store.Quiz.add_new_score(params) do
{:ok, score} ->
message = {:score_added, score}
topic = socket.assigns.game.conf_name
Phoenix.PubSub.broadcast(QuizWeb.PubSub, topic, message)
_ -> :noop
end
end
98. @tetiana12345678 Kiev Elixir Club 2020
quiz_web/live/quiz_live.ex
def handle_info({:score_added, score}, socket) do
high_scores =
[score | socket.assigns.high_scores]
|> Enum.sort_by(& &1.score, :asc)
{:noreply, assign(socket, high_scores: high_scores)}
end
117. @tetiana12345678 Kiev Elixir Club 2020
editor/priv/repo/migrations/*add_meta_to_card.exs
defmodule Editor.Repo.Migrations.AddMetaToCard do
use Ecto.Migration
def change do
alter table(:cards) do
add(:meta, :map)
end
end
end
118. @tetiana12345678 Kiev Elixir Club 2020
editor/lib/card.ex
defmodule Editor.Card do
...
@derive {Jason.Encoder, only: [:meta]}
schema "cards" do
...
embeds_one(:meta, Editor.CardMeta, on_replace: :delete)
end
def changeset(card, params) do
params_with_meta = Map.update!(params, "meta", &Jason.decode!/1)
card
|> cast(params_with_meta, @fields)
|> cast_embed(:meta)
...
end
end
119. @tetiana12345678 Kiev Elixir Club 2020
editor/lib/card_meta.ex
defmodule Editor.CardMeta do
use Ecto.Schema
import Ecto.Changeset
@fields ~w(style hints)a
@derive {Jason.Encoder, only: @fields}
@primary_key false
embedded_schema do
field :style, :string
field :hints, {:array, :string}, default: []
end
def changeset(schema, params) do
cast(schema, params, @fields)
end
end
120. @tetiana12345678 Kiev Elixir Club 2020
editor/lib/card_meta.ex
defmodule Editor.CardMeta do
use Ecto.Schema
import Ecto.Changeset
@fields ~w(style class hints)a
@derive {Jason.Encoder, only: @fields}
@primary_key false
embedded_schema do
field :style :class, :string, default: ""
field :hints, {:array, :string}, default: []
end
def changeset(schema, params) do
cast(schema, params, @fields)
end
end
132. @tetiana12345678 Kiev Elixir Club 2020
Summary
Who is
your
customer?
What problem
are you
solving?
Earliest
Testable
Product
133. @tetiana12345678 Kiev Elixir Club 2020
Summary
Who is
your
customer?
What problem
are you
solving?
Earliest
Testable
Product
Feedback
134. @tetiana12345678 Kiev Elixir Club 2020
Summary
Who is
your
customer?
What problem
are you
solving?
Earliest
Testable
Product
Small releasable
iterations of
complete features
Feedback