使用 Kamiflex 快速生成
好維護的 LINE Flex Message
卡米哥
卡米哥
今天的內容
LINE Flex Message 是什麼
Flex Message
{
"type": "flex",
"altText": "hello world!",
"contents": {
"type": "bubble",
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "hello world!"
}
]
}
}
}
Flex Message
Flex Message
Flex Message
LINE Flex Message 的應用
展示資訊 (Kamigo Demo)
資料列表 / 互動引導 (YouBike Today 小幫手)
資料列表 / 分頁 (Line貍端機)
各式圖表 (泰國開發者)
車牌
收據
登機證
各式圖表 (泰國開發者)
油價
餐廳排隊卡
包裹運送狀態
圖表
分享訊息至群組
Flex Message
Flex Message Bubble 寬度尺寸
Flex Message Bubble 寬度尺寸
Flex Message Bubble 寬度尺寸
Flex Message Bubble 寬度尺寸
尺寸 | iOS | Android | Mac OS | Windows |
nano | 120 | 120 | 120 | 120 |
micro | 160 | 160 | 160 | 160 |
kilo | 260 | 260 | 260 | 260 |
mega | 280~300 | 280~300 | 300 | 300 |
giga | 280~500 | 280~500 | 500 | 500 |
單位:px
kilo 以下的 bubble size 用 pixel 排版在所有裝置上是正確的
如何製作 LINE Flex Message
Flex Message 的開發需求
資料套版 - 目標版型
資料套版 - 資料模型
class Product
attr_accessor :name
attr_accessor :price
attr_accessor :image
attr_accessor :inventory
def initialize(name, price, image, inventory)
self.name = name
self.price = price
self.image = image
self.inventory = inventory
end
end
資料套版 - 資料模型
class Product
attr_accessor :name
attr_accessor :price
attr_accessor :image
attr_accessor :inventory
def initialize(name, price, image, inventory)
self.name = name
self.price = price
self.image = image
self.inventory = inventory
end
end
資料套版 - 資料模型
products = [
Product.new(
name: "Arm Chair, White",
price: 49.99,
inventory: true,
image: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_5_carousel.png"
),
Product.new(
name: "Metal Desk Lamp",
price: 11.99,
inventory: false,
image: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_6_carousel.png"
),
Product.new(
name: "3",
price: 3.3,
inventory: false,
image: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_6_carousel.png"
)
]
資料套版 - 分頁
def to_carousel(products)
{
"type": "carousel",
"contents": [
products.first(2).map do |product|
...
end,
({
"type": "bubble",
...
} if products.length > 2)
].flatten.compact
}
end
資料套版 - 圖片
products.first(2).map do |product|
{
"type": "bubble",
"hero": {
"type": "image",
"size": "full",
"aspectRatio": "20:13",
"aspectMode": "cover",
"url": product.image
},
"body": { ... },
"footer": { ... }
}
end,
資料套版 - 名稱
"body": {
"type": "box",
"layout": "vertical",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": product.name,
"wrap": true,
"weight": "bold",
"size": "xl"
},
...
]
資料套版 - 價格
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "text",
"text": "$#{product.price.to_s.split(".")[0]}",
...
},
{
"type": "text",
"text": ".#{product.price.to_s.split(".")[1]}",
...
}
]
}
資料套版 - 缺貨
({
"type": "text",
"text": "Temporarily out of stock",
"wrap": true,
"size": "xxs",
"margin": "md",
"color": "#ff5551",
"flex": 0
} unless product.inventory)
].compact
資料套版 - 缺貨
"footer": {
...
"contents": [
{
"type": "button",
"style": "primary",
"color": product.inventory ? "#00c300" : "#aaaaaa",
"action": {
"type": "uri",
"label": "Add to Cart",
"uri": "https://linecorp.com"
}
},
]
}
資料套版 - Flex
# 資料套版 line flex simulator
def to_carousel(products)
...
end
# flex message
def to_flex(contents)
{
"type": "flex",
"altText": "this is a flex message",
"contents": contents
}
end
# 實際呼叫
puts to_flex(to_carousel(products)).to_json
模組化前 - 價格
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "text",
"text": "$#{product.price.to_s.split(".")[0]}",
...
},
{
"type": "text",
"text": ".#{product.price.to_s.split(".")[1]}",
...
}
]
}
模組化後 - 價格
def render_price(price)
price_parts = price.to_s.split(".")
{
"type": "box",
"layout": "baseline",
"contents": [
{
"text": "$#{price_parts[0]}",
...
},
{
"text": ".#{price_parts[1]}",
...
}
]
}
end
JSON 太大導致可維護性差
JSON 太大導致可維護性差
def to_carousel(products)
{
"type": "carousel",
"contents": [
products.first(2).map do |product|
{
"type": "bubble",
"hero": {
"type": "image",
"size": "full",
"aspectRatio": "20:13",
"aspectMode": "cover",
"url": product.image
},
"body": {
"type": "box",
"layout": "vertical",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": product.name,
"wrap": true,
"weight": "bold",
"size": "xl"
},
render_price(product.price),
({
"type": "text",
"text": "Temporarily out of stock",
"wrap": true,
"size": "xxs",
"margin": "md",
"color": "#ff5551",
"flex": 0
} unless product.inventory)
].compact
},
"footer": {
"type": "box",
"layout": "vertical",
"spacing": "sm",
"contents": [
{
"type": "button",
"style": "primary",
"color": product.inventory ? "#00c300" : "#aaaaaa",
"action": {
"type": "uri",
"label": "Add to Cart",
"uri": "https://linecorp.com"
}
},
{
"type": "button",
"action": {
"type": "uri",
"label": "Add to wishlist",
"uri": "https://linecorp.com"
}
}
]
}
}
end,
({
"type": "bubble",
"body": {
"type": "box",
"layout": "vertical",
"spacing": "sm",
"contents": [
{
"type": "button",
"flex": 1,
"gravity": "center",
"action": {
"type": "uri",
"label": "See more",
"uri": "https://linecorp.com"
}
}
]
}
} if products.length > 2)
].flatten.compact
}
end
Kamiflex 提供的解決方案
Kamiflex
資料套版 - 分頁(結合迴圈/判斷式)
# flex message
def to_flex(products)
Kamiflex.hash(self) do
carousel do
bubbles products.first(2) do |product|
...
end
bubble do
body do
url_button "See more", "https://linecorp.com", flex: 1, gravity: :center
end
end if products.length > 2
end
end
end
資料套版 - 圖片
hero product.image, size: :full, aspectRatio: "20:13", aspectMode: :cover
資料套版 - 名稱
body spacing: :sm do
text product.name, wrap: true, weight: :bold, size: :xl
...
end
資料套版 - 價格(計算式)
baseline_box do
price_parts = product.price.to_s.split(".")
text "$#{price_parts[0]}", wrap: true, weight: :bold, size: :xl, flex: 0
text ".#{price_parts[1]}", wrap: true, weight: :bold, size: :sm, flex: 0
end
資料套版 - 缺貨(判斷式)
unless product.inventory
text "Temporarily out of stock", wrap: true, size: :xxs, margin: :md, color: "#ff5551", flex: 0
end
資料套版 - 缺貨(計算式)
footer do
color = product.inventory ? "#00c300" : "#aaaaaa"
url_button "Add to Cart", "https://linecorp.com", style: :primary, color: color
url_button "Add to wishlist", "https://linecorp.com"
end
資料套版 - Flex
# flex message
def to_flex(products)
Kamiflex.hash(self) do
carousel do
...
end
end
end
# 實際呼叫
puts to_flex(products).to_json
模組化前 - 價格
baseline_box do
price_parts = product.price.to_s.split(".")
text "$#{price_parts[0]}", wrap: true, weight: :bold, size: :xl, flex: 0
text ".#{price_parts[1]}", wrap: true, weight: :bold, size: :sm, flex: 0
end
模組化後 - 價格
# 模組化的價格
def render_price(price)
baseline_box do
price_parts = price.to_s.split(".")
text "$#{price_parts[0]}", wrap: true, weight: :bold, size: :xl, flex: 0
text ".#{price_parts[1]}", wrap: true, weight: :bold, size: :sm, flex: 0
end
end
簡潔的 DSL導致可維護性佳
# flex message
def to_flex(products)
Kamiflex.hash(self) do
carousel do
bubbles products.first(2) do |product|
hero product.image, size: :full, aspectRatio: "20:13", aspectMode: :cover
body spacing: :sm do
text product.name, wrap: true, weight: :bold, size: :xl
render_price product.price
unless product.inventory
text "Temporarily out of stock", wrap: true, size: :xxs, margin: :md, color: "#ff5551", flex: 0
end
end
footer do
color = product.inventory ? "#00c300" : "#aaaaaa"
url_button "Add to Cart", "https://linecorp.com", style: :primary, color: color
url_button "Add to wishlist", "https://linecorp.com"
end
end
bubble do
body do
url_button "See more", "https://linecorp.com", flex: 1, gravity: :center
end
end if products.length > 2
end
end
end
參考資料
kamigo demo
結論:
Kamiflex 超讚
kamigo demo