1 of 57

使用 Kamiflex 快速生成

好維護的 LINE Flex Message

卡米哥

2 of 57

卡米哥

3 of 57

今天的內容

  • 認識 LINE Flex Message
  • Flex Message 的開發需求
  • Kamiflex 的解決方案

4 of 57

LINE Flex Message 是什麼

5 of 57

Flex Message

  • 是一種訊息格式
  • 使用 JSON 描述

{

"type": "flex",

"altText": "hello world!",

"contents": {

"type": "bubble",

"body": {

"type": "box",

"layout": "vertical",

"contents": [

{

"type": "text",

"text": "hello world!"

}

]

}

}

}

6 of 57

Flex Message

  • 是一種訊息格式
  • 使用 JSON 描述

7 of 57

Flex Message

  • 是一種訊息格式
  • 使用 JSON 描述
  • 分為四個部分

8 of 57

Flex Message

  • 是一種訊息格式
  • 使用 JSON 描述
  • 分為四個部分
  • 可以放文字、圖片、按鈕
  • 可指定可點擊區域
  • 可以排版

9 of 57

LINE Flex Message 的應用

10 of 57

展示資訊 (Kamigo Demo)

11 of 57

資料列表 / 互動引導 (YouBike Today 小幫手)

12 of 57

資料列表 / 分頁 (Line貍端機)

13 of 57

各式圖表 (泰國開發者)

車牌

收據

登機證

14 of 57

各式圖表 (泰國開發者)

油價

餐廳排隊卡

包裹運送狀態

圖表

15 of 57

分享訊息至群組

  • 透過 Share Target Picker
  • 以真人身分發送 Flex 訊息

16 of 57

Flex Message

  • LINE Bot 前端工程之一
  • 卡片可放最多 12 張
  • 可放 APNG 動圖 (300KB)
  • 100 個以上的可點擊區域
  • 可取代 Imagemap
  • 超讚

17 of 57

Flex Message Bubble 寬度尺寸

18 of 57

Flex Message Bubble 寬度尺寸

19 of 57

Flex Message Bubble 寬度尺寸

20 of 57

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 排版在所有裝置上是正確的

21 of 57

如何製作 LINE Flex Message

22 of 57

23 of 57

24 of 57

Flex Message 的開發需求

25 of 57

資料套版

26 of 57

資料套版 - 目標版型

27 of 57

資料套版 - 資料模型

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

28 of 57

資料套版 - 資料模型

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

29 of 57

資料套版 - 資料模型

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"

)

]

30 of 57

資料套版 - 分頁

def to_carousel(products)

{

"type": "carousel",

"contents": [

products.first(2).map do |product|

...

end,

({

"type": "bubble",

...

} if products.length > 2)

].flatten.compact

}

end

31 of 57

資料套版 - 圖片

products.first(2).map do |product|

{

"type": "bubble",

"hero": {

"type": "image",

"size": "full",

"aspectRatio": "20:13",

"aspectMode": "cover",

"url": product.image

},

"body": { ... },

"footer": { ... }

}

end,

32 of 57

資料套版 - 名稱

"body": {

"type": "box",

"layout": "vertical",

"spacing": "sm",

"contents": [

{

"type": "text",

"text": product.name,

"wrap": true,

"weight": "bold",

"size": "xl"

},

...

]

33 of 57

資料套版 - 價格

{

"type": "box",

"layout": "baseline",

"contents": [

{

"type": "text",

"text": "$#{product.price.to_s.split(".")[0]}",

...

},

{

"type": "text",

"text": ".#{product.price.to_s.split(".")[1]}",

...

}

]

}

34 of 57

資料套版 - 缺貨

({

"type": "text",

"text": "Temporarily out of stock",

"wrap": true,

"size": "xxs",

"margin": "md",

"color": "#ff5551",

"flex": 0

} unless product.inventory)

].compact

35 of 57

資料套版 - 缺貨

"footer": {

...

"contents": [

{

"type": "button",

"style": "primary",

"color": product.inventory ? "#00c300" : "#aaaaaa",

"action": {

"type": "uri",

"label": "Add to Cart",

"uri": "https://linecorp.com"

}

},

]

}

36 of 57

資料套版 - 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

37 of 57

模組化

38 of 57

模組化前 - 價格

{

"type": "box",

"layout": "baseline",

"contents": [

{

"type": "text",

"text": "$#{product.price.to_s.split(".")[0]}",

...

},

{

"type": "text",

"text": ".#{product.price.to_s.split(".")[1]}",

...

}

]

}

39 of 57

模組化後 - 價格

def render_price(price)

price_parts = price.to_s.split(".")

{

"type": "box",

"layout": "baseline",

"contents": [

{

"text": "$#{price_parts[0]}",

...

},

{

"text": ".#{price_parts[1]}",

...

}

]

}

end

40 of 57

JSON 太大導致可維護性差

41 of 57

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

42 of 57

Kamiflex 提供的解決方案

43 of 57

Kamiflex

44 of 57

資料套版 - 分頁(結合迴圈/判斷式)

# 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

45 of 57

資料套版 - 圖片

hero product.image, size: :full, aspectRatio: "20:13", aspectMode: :cover

46 of 57

資料套版 - 名稱

body spacing: :sm do

text product.name, wrap: true, weight: :bold, size: :xl

...

end

47 of 57

資料套版 - 價格(計算式)

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

48 of 57

資料套版 - 缺貨(判斷式)

unless product.inventory

text "Temporarily out of stock", wrap: true, size: :xxs, margin: :md, color: "#ff5551", flex: 0

end

49 of 57

資料套版 - 缺貨(計算式)

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

50 of 57

資料套版 - Flex

# flex message

def to_flex(products)

Kamiflex.hash(self) do

carousel do

...

end

end

end

# 實際呼叫

puts to_flex(products).to_json

51 of 57

模組化

52 of 57

模組化前 - 價格

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

53 of 57

模組化後 - 價格

# 模組化的價格

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

54 of 57

簡潔的 DSL導致可維護性佳

55 of 57

# 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

56 of 57

參考資料

kamigo demo

57 of 57

結論:

Kamiflex 超讚

kamigo demo