บทความทั้งหมด

หากเปรียบเทียบการออกแบบ UX UI เป็นประตู ดังนั้น Designer คือผู้ที่ต้องแก้ไขปัญหา เพื่อให้ตอบโจทย์ผู้ใช้ ไม่ใช่คำนึงแค่ความสวยงามเท่านั้น โดยใน Sec.0 นี้ เราจะมองภาพง่ายๆแบบใกล้ตัว เช่น การออกแบบประตู ซึ่งการออกแบบ เราจะมีพื้นฐานด้วยกัน 4 อย่าง ดังนี้


พื้นฐานการออกแบบมี 4 อย่าง คือ

  • การออกแบบหน้าตา (User Interface Design) :: หน้าตาของประตู ส่วนใหญ่จะเป็นรูปสี่เหลี่ยม เพื่อง่ายต่อการใช้งาน ง่ายต่อการติดตั้ง แต่บางกรณีก็จะมีรูปทรงต่างๆ เช่น ทรงกลม แต่ก็ต้องยอมรับว่า รูปทรงง่ายๆอย่างสี่เหลี่ยม เป็นรูปทรงที่นิยมใช้กันมากที่สุด

ดังนั้นการออกแบบหน้าตาก็คือ องค์ประกอบ, ขนาด, รูปแบบ และ สิ่งที่ผู้ใช้สัมผัส

  • การออกแบบการสื่อสาร (Information Design) :: สีของประตู ถ้าอยู่ในบ้านจะให้ความรู้สึกสบาย โดยส่วนใหญ่มักใช้สีอ่อนๆ หรือสีธรรมชาติ และหากเป็นประตูที่มีความหมายเฉพาะเจาะจง ก็จะใช้สีสัน ข้อความ หรือรูปภาพติดที่ประตู เพื่อให้เห็นได้อย่างง่ายดาย เช่น ประตูหนีไฟที่มีสีแดง

ดังนั้นการออกแบบการสื่อสารก็คือ สี, ข้อความ และ สิ่งที่ต้องการสื่อกับผู้ใช้

  • การออกแบบการตอบสนอง (Interaction Design) :: การออกแบบว่าการหมุนลูกบิดเพื่อเปิดประตู หรือการผลักประตูเพื่อให้เปิดออก และดันกลับเข้ามาเพื่อให้ประตูปิด

ดังนั้นการออกแบบการตอบสนองก็คือ ผู้ใช้งานต้องสามารถใช้งานตามความเข้าใจได้อย่างถูกต้อง

  • การออกแบบประสบการณ์ (User Experience Design) :: เป็นสิ่งที่ไม่มีรูปร่าง เป็นเรื่องของความรู้สึก ความพอใจและการเข้าใจ

โดยทั้งหมดนี้จะต้องสัมพันกันทั้งหมด เมื่อต้องการออกแบบหน้าตา เราก็จำเป็นจะต้องรู้และเข้าใจ การออกแบบการสื่อสาร และเมื่อเรา ต้องการออกแบบการสื่อสาร เราต้องคำนึงถึง การออกแบบการตอบสนอง และสุดท้ายทั้งหมดจะเข้าไปเกี่ยวพันกับ การออกแบบประสบการณ์


การออกแบบ UX UI สำหรับเครื่องคอมพิวเตอร์

ออกแบบมาจากเทคโนโลยีเก่าๆ เช่น หน้าจอคอมพิวเตอร์ ถูกออกแบบดัดแปลงมาจากหน้าจอทีวี และแป้นพิมพ์ ก็ถูกออกแบบมาจากแป้นพิมพ์ดีด ซึ่งที่กล่าวมานี้คือการลดการเรียนรู้ของผู้ใช้งาน ไม่ต้องทำความเข้าใจใหม่ ทำให้รู้สึกง่าย และรับรู้การใช้งานได้อย่างรวดเร็ว และคอมพิวเตอร์ จะประกอบไปด้วยเทคโนโลยีด้วยกัน 2 แบบ ดังนี้

  • เทคโนโลยี AI :: ตัวอย่างเช่น Siri หรือ Google Home เป็นต้น ซึ่งเทคโนโลยีนี้ จะใช้เสียงเป็นตัวประสาน สามารถสั่งงานได้ด้วยเสียง หรือเรียกว่า Voice User Interface

  • เทคโนโลยี VR/AR :: โดย VR จะเน้นภาพเสมือนจริง แต่ไม่ใช่ของจริง สื่อสารด้วยภาพเสมือนจริงแบบ 3มิติ โดยมีอุปกรณ์เสริม สำหรับสร้างประสบการณ์เพิ่ม และ AR คือการเอาสิ่งที่ถูกสร้างขึ้นในโลกของดิจิตอลมาซ้อนทับกับโลกแห่งความจริง เพื่อให้ภาพปรากฎขึ้น เสมือนว่าสิ่งๆนั้นเกิดขึ้นจริง


การออกแบบ UX UI สำหรับSmart Phone

Smart Phone หรืออีกมุมหนึ่งก็คือ คอมพิวเตอร์ส่วนตัวอย่างแท้จริง ถูกออกแบบให้พกพาได้สะดวก เรียนรู้ได้ง่าย เปลี่ยนแปลงต่อสถานการณ์ได้ง่าย ตอบสนองรวดเร็ว และสร้างประสบการณ์ใหม่ๆให้กับผู้ใช้ โดยการออกแบบของSmart Phoneนั้น มีหลักการดังนี้

  • การออกแบบหน้าตา (User Interface Design) :: รูปร่างและขนาดที่จับถนัดมือ รองรับการใช้งานแบบมือเดียว

  • การออกแบบการสื่อสาร (Information Design) :: มีปุ่มน้อย เน้นการใช้ระบบสัมผัส

  • การออกแบบการตอบสนอง (Interaction Design) :: ออกแบบอย่างหลากหลาย มีการตอบสนองหลากหลาย เช่น การสั่นหรือใช้เสียงเพื่อแจ้งเตือน และการปลดล็อกด้วยการสแกนลายนิ้วมือ หรือใบหน้า เป็นต้น

  • การออกแบบประสบการณ์ (User Experience Design) :: Smart Phoneถือเป็นประสบการณ์ใหม่ และแตกต่างจากคอมพิวเตอร์ได้เป็นอย่างดี สามารถพกพาไปไหนก็ได้ ง่าย และ สะดวกสบาย


การออกแบบ UX UI สำหรับโปรแกรมคอมพิวเตอร์

หลักการออกแบบของMobile App / Websiteนั้น มีหลักการดังนี้

  • การออกแบบหน้าตา (User Interface Design) :: ต้องคำนึงถึงขนาดของหน้าจอแสดงผล ซึ่งมีความหลากหลาย ต้องให้ความสำคัญของการจัดวาง รูปทรง โครงสร้าง สี ซึ่งจะส่งผลต่อการใช้งาน เช่น ส่วนใดไว้ดู ส่วนใดไว้พิมพ์ หรือส่วนใดที่กดได้

  • การออกแบบการสื่อสาร (Information Design) :: ต้องคำนึงถึงการใช้ภาพ การใช้ข้อความ และการใช้คำเพื่อการสื่อสาร ต้องให้ผู้ใช้เข้าใจว่าต้องทำอะไร

  • การออกแบบการตอบสนอง (Interaction Design) :: ต้องคำนึงการตอบสนองที่ถูกต้อง ให้ผู้ใช้เข้าใจว่าปุ่มนี้ใช้สำหรับทำอะไร และผลลัพธ์ที่เกิดขึ้นก็ต้องถูกต้อง

  • การออกแบบประสบการณ์ (User Experience Design) :: ต้องคำนึงถึงประสบการณ์ทั้งความรู้สึก ความคิด การตัดสินใจ การลงมือทำ เช่น พอเข้ามาแล้ว ผู้ใช้สามารถรู้หรือไม่ว่าแอพนี้ใช้ทำอะไร เพื่ออะไร ระหว่างการใช้งานยุ่งยากหรือไม่ ไม่ใช่แค่สวยงามหรือเข้าใจง่าย แต่ต้องได้คุณค่า และผลลัพธ์ที่ดี


ประโยชน์ของการออกแบบ UX UI ที่ดี

ประโยชน์ของการออกแบบ UX UI ที่ดี จะแบ่งออกเป็น 2 อย่างคือ

  • ถูกต้อง :: โดยจะดูที่การใช้งาน ว่าง่ายหรือไม่ สะดวกหรือไม่ ประสบความสำเร็จหรือไม่ ถึงขั้นตอนไหนแล้ว หรือถ้ากรณีที่มีปัญหา จะแก้ไขอย่างไร เมื่อใช้งานเสร็จผู้ใช้ได้ผลลัพธ์ตามที่ต้องการหรือไม่

  • ถูกใจ :: โดยจะดูความพึงพอใจ ความสุขของผู้ใช้ ประสบกาณ์ที่ได้ เมื่อใช้แล้วอยากกลับมาใช้ซ้ำหรือไม่ สร้างความกังวลหรือเกิดความกังวลกับผู้ใช้มากน้อยแค่ไหน

สรุปสั้นๆคือ ประโยชน์ของ UX UI ที่ดีคือ

  • สร้างกระบวนการการทำงานที่ถูกต้อง เพื่อให้เกิดผลลัพธ์ที่ดี
  • สร้างความถูกใจเพื่อให้เกิดความจดจำและอยากกลับไปใช้งานซ้ำ

โทษของการออกแบบ UX UI ที่ไม่ดี

  • เสียเวลา :: ใช้เวลามากในการเรียนรู้ หรือเข้าใจกับสิ่งที่ออกแบบ

  • เสียแรง / เสียกำลังสมอง :: ถ้าการออกแบบไม่ดีจะทำให้ ผู้ใช้งานต้องคิดซับซ้อน ต้องคาดเดา วิเคราะห์มากขึ้น และสูญเสียพลังสมอง ซึ่งจะส่งผลต่ออารมณ์ และความรู้สึก ทำให้รู้สึกไม่ดี และไม่อยากกลับมาใช้ซ้ำ

  • เสียโอกาสและทรัพย์สิน :: การสื่อสารที่ผิดหรือไม่ชัดเจน จะทำให้เกิดความผิดพลาด รู้สึกไม่ปลอดภัย และผู้ใช้จะไม่กลับมาใช้ซ้ำอีก

  • เสียความรู้สึก :: เมื่อผู้ใช้รู้สึกเสียความรู้สึกอย่างต่อเนื่อง จะทำให้ผู้ใช้ตัดสินใจไม่ใช้งานแอพหรือเว็บไซต์ของเรา


ทำไมต้องเรียนรู้การออกแบบ UX UI Design

เพื่อการออกแบบที่ดี และประสบความสำเร็จ สร้างประสบการณ์ให้กับผู้ใช้ได้อย่างดี ทันสมัย และยกระดับงานดีไซน์เพื่อให้ผู้ใช้ประทับใจ และกลับมาใช้ซ้ำในแอพหรือเว็บไซต์ของเรา


และตอนนี้เราก็ได้รู้เรื่องราวคร่าวๆของ UX UI ไปบ้างแล้ว และในSectionต่อไป เราจะมาเริ่มเรียนรู้ UX/UI Design แบบจริงจังกันค่ะ!

วิธีสร้างปุ่ม Login Facebook บน WordPress ด้วยปลั๊กอิน Nextend Social Login Plugin

WordPress คือ เว็บไซต์สำเร็จรูป ที่สามารถสร้างและจัดการเนื้อหาได้ง่ายๆ เพียงแค่คุณล็อกอินเข้าสู่ระบบ ก็สามารถสร้างเว็บไซต์ข่าวสาร หรือร้านค้าขายของออนไลน์ของตัวเองได้เลย เพราะมีระบบการจัดการข้อมูล และยังมีปลั๊กอินมากมาย ที่ช่วยให้คุณสร้างเว็บไซต์ของตัวเองได้ง่ายยิ่งขึ้น

ต้องยอมรับว่าการ Login ด้วย Social นั้นเป็นสิ่งจำเป็นขั้นพื้นฐานไปแล้วสำหรับเว็บไซต์ปัจจุบันนี้ เพราะไม่ต้องเสียเวลากรอกแบบฟอร์มการสมัครสมาชิกของเว็บไซต์ ที่บางเว็บก็ต้องกรอกข้อมูลเยอะมาก จะดีกว่ามัย ถ้าเเว็บของเราสามารถ Login ผ่าน Social account ได้เลย เพราะฉะนั้นบทความนี้จะมาจะมาสอนวิธีการ Login Facebook ด้วยปลั๊กอิน Nextend Social Login ที่เป็นปลั๊กอินฟรี ติดตั้งง่าย และใช้งานได้จริง

  1. โหลด Plugin Nextend Social Login โหลดที่ https://th.wordpress.org/plugins/nextend-facebook-connect/
    และปิดใช้งาน

  2. หลังจากกดใช้งานเรียบร้อยแล้ว ไปที่เมนู settings > Nextend Social Login > กด Getting Started ที่ใต้รูป Facebook

  3. สร้าง Facebook App ไปที่ https://developers.facebook.com/apps/ คลิกที่ "เพิ่มแอพใหม่"

เลือก “จัดการผสานธุรกิจ”

  1. ใส่รายละเอียด ตั้งชื่อ App และใส่ email คลิก สร้าง ID ของแอพ

  1. คลิกที่ ตั้งค่า ใต้ “การเข้าสู้ระบบ Facebook”

  1. เลือก www สำหรับทำ login บน เว็บไซต์

  1. ใส่ link เว็บไซต์ กด save และคลิก ดำเนินการต่อ

  2. ไปที่เมนู การตั้งค่า > ข้อมูลพื้นฐาน

  3. ใส่รายละเอียดข้อมูลของเว็บไซต์

  4. เลื่อนลงมาข้างล่างส่วน เว็บไซต์ ให้ใส่ link เว็บไซต์ และ คลิกที่ บันทึกการเปลี่ยนแปลง

  1. กลับไปที่หน้าตั้งค่าปลั๊กอิน Nextend Social บนเว็บของเรา ไปที่หัวข้อ Getting Started ดูหัวข้อที่ 13 ให้คัดลอก URL นั้น

  2. คลิกที่

  3. กลับไปที่ App Facebook ไปที่เมนู การเข้าสู่ระบบ Facebook > การตั้งค่า

  1. ตั้งค่า OAuth ตามนี้พร้อมใส่ link ที่ copy มาจากข้อ 11 มาใส่ และ กด บันทึกการเปลี่ยนแปลง

  2. กลับไปที่เมนู “ข้อมูลพื้นฐาน”

  1. คลิกเปิดใช้งาน App

  2. Copy ในส่วนของ ID ของแอพ และ ข้อมูลลับของแอพ

  3. ใส่ที่เว็บของเรา

  4. คลิกที่ Verify Settings

  1. จะได้แบบนี้ แสดงว่าเสร็จสมบูรณ์เเล้ว !!

  1. ทำการบันทึก

ภาษา JavaScript นั้นเป็นภาษาที่เริ่มเขียนได้ไม่ยาก ยิ่งถ้าใครเขียนภาษาสาย Structure/OOP เช่น C/C++, C#, Java มาก่อน อาจจะชอบในความเป็น Dynamic Type เราสร้างตัวแปรแล้วเก็บค่าอะไรก็ได้ ทำให้มีความคล่องตัวในการเขียนมากๆ ก่อนที่พอเขียนไปเรื่อยๆ เราจะพบความแปลกของมัน ยิ่งเขียนไปนานๆ เราก็พบว่ามันเป็นภาษาที่ทำให้เกิดบั๊กได้ง่ายมาก และเป็นหนึ่งในภาษาที่น่าปวดหัวที่สุดที่คนโหวตๆ กันเลย ด้วยเหตุผลแรกคือการที่ตัวแปรไม่มี Type นั่นเอง (อีกเหตุผลหนึ่งคือส่วนใหญ่ของคนที่เปลี่ยนมาเขียน JavaScript เคยชินกับภาษาแนว OOP มาก่อน พอมาเขียนภาษาแนว Functional เลยงงหน่อยๆ)

หลักการคือคอมไพล์ออกมาเป็น JavaScript

เอาจริงๆ ตัวภาษา TypeScript เอาไปรันอะไรไม่ได้เลย

แต่เราใช้วิธีการคอมไพล์ไฟล์ .ts ให้ออกมาเป็นไฟล์ .js อีกที หน้าที่ของมันมีแค่นั้นเอง

หรือบางกรณีถ้าเราเขียน React เราอาจจะรู้จักกับ .jsx สำหรับ TypeScript ก็มีไฟล์นามสกุล .tsx ให้ใช้เช่นกัน

Installation มาติดตั้ง TypeScript กัน

สำหรับ TypeScript Compiler นั้นสามารถติดตั้งได้ทั้งผ่าน npm หรือ yarn ด้วยคำสั่งประมาณนี้

npm install -g typescript

พอติดตั้งแล้วเราสามารถใช้คำสั่ง tsc สำหรับคอมไพล์ไฟล์ TypeScript ได้เลย

วิธีนี้เป็นการติดตั้งแบบ global คือติดตั้งครั้งเดียว ใช้ได้ทุกที่เลย แต่ถ้าอยากติดตั้งแต่ในโปรเจคๆ ไปก็สามารถทำได้ด้วย

npm install typescript --save-dev

หรือจะใช้งานผ่าน npx ก็ได้เช่นกัน

npx tsc

flag --save-dev หรือตัวย่อ -D ใส้ไว้เพื่อระบุว่าเราใช้แพ็กเกจนี้เฉพาะตอน develop เท่านั้น เมื่อไหร่ที่จะ build โปรเจคเอาไปใช้จริง ไม่ต้องเอาติดไปด้วยนะ เพราะอย่างที่บอกคือ TypeScript นั้นตัวมันไม่ได้เอาไปรัน แต่คอมไพล์เป็น JavaScript เมื่อไหร่ที่เขียนเสร็จแล้ว ก็ไม่ต้องใช้ต่อแล้ว

คอมไพล์ไฟล์ TypeScript

เริ่มแรกลองมาสร้างไฟล์ TypeScript กันดีกว่า

//app.ts

function sum(n: number): number {
    let sum: number = 0
    for(let i = 0; i < n; i++){
        sum += i
    }
    return sum
}

const total: number = sum(100)

console.log(total)

เขียนโค้ดนี้แล้วเซฟเป็นไฟล์ชื่อ app.ts ไว้ จะเห็นว่า syntax ทั้งหมดนั้นเหมือนกัน JavaScript ทุกอย่าง ที่เพิ่มเติมมาก็แค่หลังชื่อตัวแปร ตอนที่ประกาศตัวแปรและ parameter เราจะต้องกำหนดชนิดตัวแปรลงไปด้วย โดยใช้ตัว : ตามแพทเทิร์นนี้

variable-name: type

ไทป์ที่ใช้ได้ก็เช่น

let decimal: number = 6
let hex: number = 0xf00d
let binary: number = 0b1010
let octal: number = 0o744

let color: string = "blue"

let isDone: boolean = false

สำหรับตัวแปรประเภท Array นั้นประกาศได้ 2 แบบคือ

let list: number[] = [1, 2, 3]
let list: Array<number> = [1, 2, 3]

ส่วนตัวแปรแบบ Object นั้นจะต้องสร้าง interface เพื่อกำหนดไทป์ขึ้นมาก่อน

interface Point {
    x: number
    y: number
}

let myPoint: Point = { x: 10, y: 20 }

สำหรับ TypeScript นั้น เราไม่สามารถสร้างตัวแปรแบบไม่กำหนดไทป์ได้ แต่ถ้าเราไม่รู้ไทป์จริงๆ เราสามารถประกาศไทป์ any ได้ (แต่ก็ไม่แนะนำให้ทำนะ)


จากนั้นเราใช้คำสั่ง tsc ในการคอมไพล์ไฟล์ TypeScript ของเรา

tsc app.ts

หลังจากคอมไพล์เสร็จแล้ว เราจะได้อีกไฟล์เพิ่มมาในชื่อเดียวกันแต่เป็นนามสกุล .js แทน

เมื่อเราเอามาเปิดดูก็จะพบว่ามันมีหน้าตาคล้ายๆ กับโค้ด TypeScript ที่เราเขียนไป แต่ชนิดตัวแปรที่เคยประกาศจะหายไป และอาจจะมีอะไรอีกนิดหน่อยด้วย (เช่นมี ; เพิ่มเข้ามาให้ จากที่เคยใช้ let, const ก็เปลี่ยนเป็น var) ซึ่งโดยร่วมไม่ได้ทำให้โลจิคเปลี่ยนไปนะ

// app.js

function sum(n) {
    var sum = 0;
    for (var i = 0; i < n; i++) {
        sum += i;
    }
    return sum;
}
var total = sum(100);
console.log(total);

ซึ่งจุดนี้อาจจะทำให้คนที่เพิ่งเริ่มใช้ TypeScript สงสัยว่าถ้ามันแค่ตัด Type ทิ้งไป แล้วทำไมเราไม่เขียน JavaScript ตรงๆ ไปเลยล่ะ?

เช่นผลคือ TypeScript สามารถช่วยเราในส่วนของการเช็ก Compiler-Time Error ได้

เช่นโค้ดนี้

เราประกาศฟังก์ชันที่ต้องรับ number แต่เวลาเรียกใช้งาน ดันใส่ string เข้าไป ถ้าเป็น JavaScript จะสามารถนำไปรันได้ (แต่ผลออกมาจะเกิด Runtime-Error หรือมีโลจิคผิดพลาดตรงไหนจะไม่สามารถรู้ได้เลย)

แต่สำหรับ TypeScript นั้นจะแจ้งออกมาตั้งแต่ตอนสั่งคอมไพล์เลยว่าเรามีการเขียนผิดพลาดเกิดขึ้นนะ และ Editor ส่วนใหญ่ก็สามารถแสดงผลข้อผิดพลาดพวกนี้แบบ real time ได้อยู่แล้วด้วย ทำให้ไม่ต้องเสียเวลาพิมพ์ไปซะนาน แต่พอเอามารันเพิ่งจะรู้ว่าพิมพ์ผิดไป

เริ่มสร้างโปรเจคกันเถอะ

ในโปรเจคขนาดใหญ่ แน่นอนว่าเราไม่ได้มีแค่ไฟล์ sourcecode แค่ไฟล์สองไฟล์ แต่อาจจะมีเป็นร้อยๆ ไฟล์ การจะมานั่งคอมไพล์ TypeScript ทีละไฟล์นั้นทำไม่ได้แน่นอน (จริงๆ ก็ทำได้ แต่ใครจะทำนะ !)

การสร้างโปรเจคเลยเป็นทางเลือกที่ดีกว่า เช่นเราอาจจะสร้างโฟลเดอร์ /src สำหรับเอาไว้เก็บไฟล์ .ts ทั้งหมด และสร้างโฟลเดอร์ /dist หรือ distribute เอาไว้เก็บไฟล์ JavaScript

.
├── src
│   ├── app1.ts
│   ├── app2.ts
│   ├── app3.ts
│   └── ...
└── dist
    ├── app1.js
    ├── app2.js
    ├── app3.js
    └── ...

ซึ่งสำหรับ TypeScript เราสามารถใช้คำสั่งนี้

tsc --init

เพื่อกำหนดว่าโฟลเดอร์นั้นจะเป็นโปรเจค TypeScript ได้ หลังจากสั่งคำสั่งนั้นแล้ว เราจะได้ไฟล์ tsconfig.json มา ภายในไฟล์จะมี config สำหรับโปรเจคอยู่ หน้าตาประมาณนี้

รายละเอียดการเซ็ตค่า tsconfig สามารถดูได้ที่เว็บไซต์ของ TypeScript

ปกติการตั้งค่าหลักๆ ก็จะใช้ของที่ TypeScript สร้างมาให้ ยกเว้นบางตัวที่เราต้องเซ็ตเอง เช่น

{
  "compilerOptions": {

    // สั่งให้คอมไพล์ออกมาเป็น JavaScript ES6
    "target": "es6",

    // ชื่อโหลเดอร์ที่ output ไฟล์ JavaScript ที่คอมไพล์แล้ว
    "outDir": "./dist",

    // ชื่อโฟลเดอร์ sourcecode ไฟล์ TypeScript
    "rootDir": "./src",

    // หากใช้งานกับ React คือมีไฟล์ .tsx ให้คอมไพล์เป็น .jsx ด้วย
    "jsx": "react",

    // หากใช้กับ node
    "moduleResolution": "node",
  },

  // กำหนดขอบเขตของไฟล์ที่จะให้คอมไพล์
  // เช่น ทุกไฟล์ที่นามสกุล .ts ในโฟลเดอร์ไหนก็ได้ใต้ /src
  "include": [
      "src/**/*.ts"
  ],

  // กำหนดไฟล์และโฟลเดอร์ที่ไม่ต้องคอมไพล์ 
  // เช่นโฟลเดอร์ node_modules หรือไฟล์ spec
  "exclude": [
      "node_modules",
      "**/*.spec.ts"
  ]
}

จากนั้นเวลาเราจะคอมไพล์ ก็สั่งแค่

tsc

มันก็จะเป็นการคอมไพล์ทุกไฟล์ในทั้งโฟลเดอร์ /src/ ที่เรากำหนดไว้

แต่นี่ก็เป็นการคอมไพล์แค่ครั้งเดียว ถ้าเรามีการแก้ไขไฟล์ TypeScript เพิ่ม เราก็ต้องสั่งคอมไพล์อีกรอบ ถ้าไม่อยากจะมาสั่งคอมไพล์ทุกรอบที่แก้ไขโค้ด เราสามารถสั่งคอมไพล์แบบ watch ได้

tsc --watch

คำสั่งนี้จะเป็นการสั่งให้คอมไพล์พร้อมกับ watch หรือคอยดูไฟล์ทุกไฟล์เอาไว้ด้วย ถ้ามีการเปลี่ยนแปลงให้คอมไพล์ใหม่ทันทีโดยเราไม่ต้องสั่งอีกรอบ

ในบทความนี้จะเล่าถึงที่มาที่ไปของ TypeScript อย่างเดียวนะ ส่วนเรื่องสอนว่าใช้งานยังไงได้บ้าง จะเขียนอีกทีในบล็อกต่อๆ ไป

สำหรับภาษาโปรแกรมทุกวันนี้ ถ้าแบ่งคร่าวๆ ด้วยชนิดของตัวแปร เราจะแบ่งได้ 2 อย่าง คือ Static Type: ต้องกำหนดชนิดตัวแปร เช่น int, string ตั้งแต่สร้างตัวแปร และ Dynamic Type ตัวแปรประเภทนี้ไม่ต้องบอกว่าเก็บค่าชนิดไหน เปลี่ยนไปได้เรื่อยๆ

สำหรับภาษาแบบ Dynamic Type ที่ไม่ต้องกำหนดชนิดตัวแปรให้แน่นอน จะเซ็ตค่าเป็นอะไรก็ได้นั้นอาจจะทำให้เขียนง่าย แถมบางภาษาเช่น Python ไม่ต้องประกาศตัวแปรก่อนใช้ด้วยซ้ำ อยากใช้ก็ใช้เลย

Static Type

int x;
x = 10;
x = "str"; //Type Error!

Dynamic Type

x = 10
x = "str" #Ok!

แต่สำหรับระยะยาวและโปรเจคขนาดใหญ่ที่มีความซับซ้อนสูงมากๆ การที่ไม่ต้องประกาศ/กำหนดชนิดตัวแปรทำให้เกิดปัญหาตามมามากมาย ยิ่งถ้าต้องทำงานเป็นทีมร่วมกันแล้ว ถ้าเราไม่รู้ว่าตัวแปรตัวนี้เป็นชนิดอะไร จะทำให้เสียเวลาไล่โค้ดนานมาก

ลองดูตัวอย่างต่อไปนี้

def sum_product_price(data):
  total = 0
  for product in data['products']:
    total += product.price
  return total

จะเห็นว่าการที่ตัวแปรสร้างเป็นไทป์อะไรก็ได้ทำให้เวลาอ่าน/แก้ไขโค้ดทำได้ยากมาก เพราะไม่รู้ structure ของตัวแปร เช่น data ที่เราต้องมาจำเองว่ามันมีฟิลด์ชื่อ products และเป็นอ็อบเจคที่มี .price

โค้ดที่เขียนเองอาจจะไม่เท่าไหร่ แต่ถ้าเป็นโค้ดที่เขียนต่อจากคนอื่น อันนี้มีงงแน่นอน

ช่วงหลังๆ มีหลายภาษาที่เริ่มต้นจากการเป็นภาษา Dynamic Type แต่ก็ต้องเพิ่มฟีเจอร์ให้กำหนดตัวแปรได้มาภายหลัง เช่น PHP (กำหนดไทป์ได้ตั้งแต่เวอร์ชัน 7) และ Python (กำหนดไทป์ได้ตั้งแต่เวอร์ชัน 3.6)

JavaScript ก็เป็นหนึ่งในนั้น แต่สำหรับ JavaScript นั้นมีความแปลกกว่าภาษาอื่นนิดหน่อย เพราะถ้าเราต้องการใช้ไทป์ในภาษานี้ เราจะต้องเขียนมันด้วย TypeScript ซึ่งเป็นภาษาอีกตัวหนึ่งจากทาง Microsoft ที่พอเขียนเสร็จแล้วมันจะคอมไพล์กลับมาเป็น JavaScript อีกทีหนึ่ง

ดังนั้นคำแนะนำในการเขียน JavaScript ข้อแรกคือ ..

อย่าเขียน JavaScript แต่ให้เขียน TypeScript แทน !!

กำเนิด TypeScript

TypeScript นั้นถูกสร้างโดย Microsoft หลังจากที่ VBScript ของตัวเองแพ้ให้กับ JavaScript

Microsoft ใช้หลักการว่า "ถ้าต้านไม่อยู่ ก็เข้าร่วมซะ" โดยมอบหมายงานนี้ให้กับ นักออกแบบภาษาโปรแกรมที่เคยออกแบบ C# และ .NET Framework มาแล้วอย่าง Anders Hejlsberg ซึ่งคอนเซ็ปที่เขาใช้ก็คือเพิ่ม Type ให้กับ JS ซะ

TypeScript = JavaScript + Type

แนวคิดนี้เหมือนกับตอนที่ C++ ถูกสร้างขึ้นมาคือเป็นการเพิ่มความสามารถให้ภาษา C อีกที (ในเวอร์ชันแรก C++ นั้นคอมไพล์ออกมาเป็นโค้ดภาษา C)

ครั้งนี้ก็เช่นกัน ภาษาใหม่ที่ออกแบบจะไม่ฉีกแนวไปจาก JavaScript เลย แต่เพิ่ม syntax ที่บวกความสามารถเรื่อง Type และอื่นๆ เข้าไปแทน โดยตัว TypeScript นี้ไม่ใช่ภาษาที่เอาไปรันได้ แต่ตัวมันคอมไพล์แล้ว output ออกมาเป็น JavaScript นั้นเอง

จริงๆ ต้องบอกว่า Alternative Language หรือภาษาที่เอามาเขียนแทน JavaScript ได้นั้นมีเยอะมาก เช่น CoffeeScript หรือภาษา Functional อย่าง Clojure

แต่ทำไมอยู่ๆ TypeScript ถึงดังขึ้นมาล่ะ? ทั้งที่ตอนนั้นกระแสโปรแกรมเมอร์ไม่ชอบ Microsoft เยอะมาก (ย้อนกลับไปช่วงต้นปี 2010 นะ) อะไรที่ Microsoft ทำในช่วงนั้น คนจะหนีกันหมด

จริงๆ แล้วตอนแรก TypeScript ก็ไม่ได้รับความนิยมอะไรมากหรอก

จน Google มีโปรเจคสร้างเฟรมเวิร์คสำหรับเขียน Frontend ตัวใหม่ที่ชื่อว่า AngularJS ออกมานั่นแหละ

ตอนนั้น AngularJS กำลังอยู่ในเวอร์ชัน 1 (เขียนด้วย JavaScript เพียวๆ เลย) และทาง Google ก็พบปัญหามากมาย จนอยากจะรื้อเขียนใหม่ให้เป็นเวอร์ชัน 2 ซะ แล้วไหนๆ ก็จะเขียนใหม่แล้ว เขียนมันด้วย ES6 เลยดีกว่า! (ในปีนั้นมาตราฐาน ES6 ยังไม่ออก) จะรอให้มาตราฐานออก ก็ไม่รู้ว่าจะมาเมื่อไหร่ งั้นก็ลองไปค้นๆ ในตลอดดูซิว่าตอนนี้มีภาษาอะไรเอามาใช้แทนก่อนได้มั้ย?

และหวยก็ไปออกที่ TypeScript นั่นเอง !!

คือตอนนั้นทีม Angular สร้างภาษาของตัวเองชื่อ ATScript ขึ้นมาเพื่อครอบ TypeScript อีกที แต่ก็เปลี่ยนใจยกเลิกไป แล้วหันมาใช้ TypeScript เพียวๆ แทน

และเมื่อ Angular ดังขึ้น --> TypeScript ก็เลยดังขึ้นมาพร้อมๆ กัน

สรุป

TypeScript เป็นแค่ภาษาที่เพิ่ม Type เข้ามาให้ภาษา JavaScript ซึ่งก็อาจจะถือว่าเป็นภาษาดวงดี ออกมาถูกจังหวะ จนทุกวันนี้ได้รับความนิยมสูงมาก โปรเจคใหญ่ๆ ไม่ว่าจะเป็น Node.js, React, Vue.js ล้วนแต่เอา TypeScript ไปผสมได้ทั้งนั้น (ถ้าเป็น Deno, Angular นั้นซัพพอร์ท TypeScript ตั้งแต่แรกอยู่แล้ว)

บล็อกต่อไปเราจะสอนวิธีการเซ็ตโปรเจค TypeScript ในโปรเจค Node.js กัน

This entry is part 5 of 5 in the series Async-in-Dart-Series

เนื้อหาในบทนี้ เป็นคลาสเฉพาะที่มากับ Flutter Framework เท่านั้น .. ถ้าเขียน Dart ธรรมดาจะไม่มีให้ใช้นะ!!

เอาจริงๆ 99% ของคนที่ศึกษาภาษา Dart ก็เพื่อเอาไปเขียนแอพ (หรือเว็บ/เด็กส์ท็อป) แบบ cross-platform ด้วยเฟรมเวิร์ก Flutter นั่นแหละ

ตามความคิดของเรา จริงๆ Flutter น่าจะเลือกภาษา Kotlin มาใช้แทนมากกว่า แต่ก็มีเหตุผลหลายๆ อย่างนั่นแหละที่ทำให้ทำไม่ได้

สำหรับการเขียน Flutter นั้น ส่วนประกอบในแต่ละส่วนของแอพนั้นจะพูดออกแบบมาเป็น Component เอามาประกอบเข้าด้วยกันเป็นชั้นๆ เรียกว่า "Widget"

มี Widget อยู่ 2 ประเภทหลักๆ คือ

  • StatelessWidget: เป็น Widget ที่ไม่สามารถเปลี่ยนแปลงได้หลังจาก render ไปแล้ว
  • StatefulWidget: เป็น Widget ที่สามารถ render หน้าตา UI ของแอพใหม่ได้ถ้ามีการเปลี่ยนแปลงข้อมูล

ในบทความนี้เราจะโฟกัสกันที่ StatefulWidget เป็นหลักนะ หน้าตาของ StatefulWidget ก็จะเป็นประมาณนี้ (StatefulWidget จะมาพร้อมกับคลาส State ของมันเสมอ)

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('This is my App.'),
    );
  }
}

เมธอดหลักที่ทำหน้าที่ในการกำหนดส่วนแสดงผลหรือ UI คือเมธอด build()

แน่นอน เวลาเราเขียนแอพจริงๆ โครงสร้างมันจะเป็นการเอา Widget มาซ้อนกันหลายๆๆๆ ชั้นมากๆ เช่น

Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Column(
        children: [
          Container(
            child: Row(
              children: [
                Text('This is my App.'),
              ]
            ),
          ),          
        ],
      ),
    ),
  );
}

ทีนี้ ปัญหามันก็อยู่ตรงนี้แหละ เพรายิ่งโครงสร้างซ้อนกันหลายชั้นมากๆ ถ้ามีการเปลี่ยนแปลงข้อมูล ก็ต้อง render หน้านี้ใหม่ (เรียกคำสั่ง build() ใหม่) แม้ว่าเราจะเปลี่ยนแปลงข้อมูลแค่ไม่กี่จุดเล็กๆ ก็ตาม

ลองมาดูตัวอย่างแอพมาตราฐานคือ Counter กัน (แอพตัวอย่างนี้เป็นแอพที่เวลาเราสั่ง new Flutter project มันจะสร้างให้เป็น default เลย)

แต่เราจะขอตัดมาแค่ส่วนที่จะใช้อธิบายนะ และขอแยก Widget ออกเป็นชิ้นๆ ด้วย

class MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('render - Scaffold');
    return Scaffold(
      body: columnWidget(),
    );
  }

  Widget columnWidget(){
    print('render - Column Widget');
    return Column(
      children: [
        incrementButtonWidget(),
        counterWidget(),
      ],
    );
  }

  Widget incrementButtonWidget(){
    print('render - Increment Button Widget');
    return RaisedButton(
      child: incrementButtonTextWidget(),
      onPressed: _incrementCounter,
    );
  }

  Widget incrementButtonTextWidget(){
    print('render - Increment Button Text Widget');
    return Text('Increment');
  }

  Widget counterWidget(){
    print('render - Counter Widget');
    return Text('count is $_counter');
  }
}

เมื่อเราเปิดหน้าแอพขึ้นมาครั้งแรก เราจะพบว่า build() เริ่มทำงาน โดยมันจะเรนเดอร์ Widget ทุกชิ้นขึ้นมาก่อน

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget

จากนั้น เมื่อเรากด increment จะทำให้เมธอด _incrementCounter() ทำงาน ไปทำการ setState() เพื่อเปลี่ยนค่า _counter

ผลที่ได้คือเมื่อ state เปลี่ยนไป ทั้งหน้าจะต้องทำการเรนเดอร์ใหม่อีกครั้ง เมธอด build() ก็ต้องเริ่มทำงานใหม่ตั้งแต่ต้นอีกครั้ง ทำให้เราได้ผลแบบนี้ออกมาอีกที

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget

แต่จริงๆ แล้วส่วนที่มีการเปลี่ยนแปลงจริงๆ มีแค่วิดเจ็ต Text('count is $_counter') เท่านั้น

ทางทีม Flutter ของ Google เลยแนะนำมาว่าการเขียนแอพแบบนี้จะทำให้ประสิทธิภาพไม่ดี ถ้ามีข้อมูลแปลี่ยนเป็นบางตัว เราควรจะเรนเดอร์ใหม่เฉพาะวิดเจ็ตที่จำเป็นน่าจะดีกว่า

นั่นเลยเป็นที่มาของวิดเจ็ตพิเศษที่ชื่อว่า FutureBuilder และ StreamBuilder

FutureBuilder / StreamBuilder

มีวิธีการสร้างแบบนี้

FutureBuilder(
    future: _future,
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        return ...
    },
)

StreamBuilder(
    stream: _stream,
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        return ...
    },
)

ส่วนวิธีการใช้งานนั้นง่ายมาก คือวิดเจ็ตตัวไหนที่สามารถเรนเดอร์ใหม่ได้เฉพาะส่วน ให้เอา FutureBuilder ไม่ก็ StreamBuilder ครบมันลงไป แบบนี้

แล้วเมื่อไหร่จะใช้ Future / Stream

ถ้าข้อมูลของเราเปลี่ยนแปลงได้แค่ครั้งเดียว เราจะใช้ FutureBuilder เช่นต้องการโหลดข้อมูลจาก API แค่ครั้งเดียว

แต่ถ้าข้อมูลของเราเปลี่ยนได้เรื่อยๆ มากกว่า 1 ครั้ง เราจะใช้ StreamBuilder เช่นการกดปุ่มที่กดได้มากกว่า 1 ครั้ง

แน่นอนว่าเราสามารถใช้ StreamBuilder แทน FutureBuilder ได้แทบจะทุกกรณีเลย

ทีนี้ลองเอา StreamBuilder มาใช้กับโค้ด Counter ดูบ้าง

ในเคสนี้ เราเปลี่ยน Text ตรงที่แสดงตัวนับให้กลายเป็น StreamBuilder จากนั้นสร้าง StreamController ขึ้นมาหนึ่งตัว ซึ่งจะอัพเดทค่า counter แทนการสั่ง setState()

class MyHomePageState extends State<MyHomePage> {

  int _counter = 0;
  StreamController<int> controller = new StreamController<int>();

  @override
  void initState(){
    super.initState();
    controller.add(_counter);
  }

  @override
  void dispose(){
    super.dispose();
    controller.close();
  }

  void _incrementCounter() {
    //แทนที่จะใช้ setState ก็เซ็ตค่าผ่าน StreamController แทน
    controller.add(++_counter);
  }

  @override
  Widget build(BuildContext context) {
    print('render - Scaffold');
    return Scaffold(
      body: columnWidget(),
    );
  }

  Widget columnWidget(){
    print('render - Column Widget');
    return Column(
      children: [
        incrementButtonWidget(),
        counterWidget(),
      ],
    );
  }

  Widget incrementButtonWidget(){
    print('render - Increment Button Widget');
    return RaisedButton(
      child: incrementButtonTextWidget(),
      onPressed: _incrementCounter,
    );
  }

  Widget incrementButtonTextWidget(){
    print('render - Increment Button Text Widget');
    return Text('Increment');
  }

  Widget counterWidget(){
    return StreamBuilder(
      stream: controller.stream,
      builder: (context, snapshot){
        print('render - Counter Widget');
        return Text('count is ${snapshot.data}');
      },
    );
  }
}

ในกรณีนี้วิดเจ็ตทั้งหมดจะมีการเรนเดอร์แค่ครั้งแรกครั้งเดียว หลังจากนั้นถ้ามีการกดปุ่ม วิดเจ็ตตัวอื่นที่ไม่เกี่ยวข้องด้วย จะไม่มีการเรนเดอร์ใหม่ เรนเดอร์เฉพาะตัวของ StreamBuilder เท่านั้น

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget
render - Counter Widget
render - Counter Widget
render - Counter Widget

จะเห็นว่าวิธีนี้ทำให้ประสิทธิภาพของแอพเพิ่มขึ้นเยอะมาก เพราะไม่จำเป็นต้องเรนเดอร์ทั้งหน้าใหม่ทุกครั้งที่มีข้อมูลเปลี่ยนแปลงเพียงเล็กน้อย

ซึ่งก็แลกมากับการที่เราต้องเขียนโค้ดเยอะขึ้นล่ะนะ (ฮา)

AsyncSnapshot

ในการใช้ทั้ง FutureBuilder และ StreamBuilder จะมีสิ่งที่เรียกว่า AsyncSnapshot ส่งมาให้ตัว เจ้าตัวนี้เป็นเหมือนกับตัวที่บอกว่าตอนนี้ข้อมูลของเรามีสถานะเป็นยังไงบ้างแล้ว

// สถานะของ future/stream ในตอนนั้น
snapshort.connectionState

// มี error เกิดขึ้นไหม
snapshop.hasError
// error คืออะไร
snapshop.error

// ได้รับ data มาแล้วรึยัง
snapshop.hasData
// data คืออะไร
snapshop.data

ConnectionState

สำหรับ Future

  • waiting: ขณะกำลังรอข้อมูล
  • done: เมื่อได้รับข้อมูลมาแล้ว

สำหรับ Stream

  • waiting: ขณะกำลังรอข้อมูล
  • active: เมื่อได้รับข้อมูลมาแล้ว แต่ stream ยังไม่ close
  • done: เมื่อสั่งให้ stream close
(BuildContext context, AsyncSnapshot snapshot) {

  // เช็กก่อนว่ามี error มั้ย
  if(snapshot.hasError){
    return Text('เกิดข้อผิดพลาดในการโหลดข้อมูล ${snapshot.error}');
  }

  // ถ้าไม่มี ตอนนี้โหลดข้อมูลเป็นยังไงบ้างแล้ว
  switch(snapshort.connectionState){
      // ข้อมูลยังไม่มา กำลังโหลดอยู่
      case ConnectionState.waiting:
          return Text('กำลังโหลดข้อมูลอยู่');

      // ข้อมูลมาเรียบร้อยแล้ว แสดงผลได้
      case ConnectionState.done:
      case ConnectionState.active:
          return Text('ข้อมูลคือ ${snapshot.data}');
  }
}

ทิ้งท้าย...

การใช้ FutureBuilder / StreamBuilder ใน Flutter เป็นสิ่งที่ช่วยเพิ่มประสิทธิภาพให้ตัวแอพไม่ต้องเรนเดอร์วิดเจ็ตที่ไม่ได้เปลี่ยนแปลงซ้ำๆ

ซึ่งคอนเซ็ปนี้จะถูกเอาไปใช้ต่อในแพทเทิร์นที่ทาง Google แนะนำมาให้ใช้กับการวางโครงสร้าง UI ใน Flutter ที่ชื่อว่า BLoC หรือ Business Logic Component ซึ่งเดี๋ยวเราจะเอามาสอนกันต่อในบทความซีรีส์ Flutter ต่อไป

ส่วนบทความซีรีส์ Async in Dart ก็ขอจบลงแค่ตอนนี้ก่อนล่ะ

Series Navigation<< Async in Dart (4) ควบคุมข้อมูลในstreamอย่างเหนือชั้นด้วย StreamController

ในโลกของ OOP เรามักจะสร้างคลาสต่างๆ ที่มีการเรียกใช้กันต่อเป็นทอดๆ มากมาย

เช่นโค้ดข้างล่างนี่

class Car {

    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

}

Car car = new Car();

เราสร้างคลาส Car และ Engine คือรถและเครื่องยนต์ขึ้นมา โดยการจะสร้างรถได้ เราจะต้องสร้างเครื่องยนต์ขึ้นมาด้วย

ในกรณีแบบนี้เราเรียกว่าคลาส Car depend on Engine (Car ต้องการ Engine) หรือแปลว่า เราไม่สามารถสร้างคลาส Car ได้ถ้าไม่มีคลาส Engine หรือ Engine นั้นเป็น "Dependency" ของคลาส Car นั่นเอง

แต่ตามหลักการออกแบบ OOP แล้วเราไม่ควรเขียนแบบโค้ดด้านบน คือไม่ควร new อ็อบเจคตรงๆ ในคลาสอื่น เพราะจะทำให้เราเปลี่ยนแปลงพฤติกรรมต่างๆ ของโค้ดทำได้ยาก

เช่นถ้าเราต้องการจะเปลี่ยน Engine ไปเป็น EcoEngine เครื่องยนต์แบบอีโค่เราจะต้องแก้โค้ดในคลาส Car เท่านั้น

วิธีการแก้แบบมาตราฐานก็คือถ้าเราต้องการใช้อ็อบเจคจากคลาสอื่น อย่าnewเอง แต่ให้รับอ็อบเจคเข้ามาแทน อาจจะทาง constructure แบบนี้ก็ได้

class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

}

แต่สำหรับมือใหม่หัดเขียน OOP นั้นมักจะไม่เขียนโค้ดในแพทเทิร์นแบบนี้ ด้วยเหตุผลว่าเวลาเราจะสร้าง Car ขึ้นมาใช้นั้น จะต้องไปสร้าง Dependencies ทั้งหมดของ Car ขึ้นมาซะก่อน (คือ new ตรงๆ ทันทีไม่ได้นั่นแหละ)

Engine engine = new Engine();
Car car = new Car();

ถ้ามี Dependencies ไม่เยอะก็ยังไม่เท่าไหร่ แต่ถ้าเยอะๆ นี่ก็มีความปวดหัวเหมือนกัน

เช่น ถ้าเราต้องการสร้างบ้านขึ้นมา แต่บ้านก็ต้องมี ประตู หน้าต่าง กำแพง บลาๆๆ

Door door = new Door();
Window window = new Window();
Wall wall = new Wall();
Ceil ceil = new Ceil();
Floor floor = new Floor();
House house = new House(door, window, wall, ceil, floor);

ซึ่งถ้าเราย้ายโค้ดพวกนี้เข้าไปไว้ในคลาส การจะสร้าง House ขึ้นมาก็จะเหลือแค่นี้

House house = new House();

DI Service แบบคร่าวๆ

หลักการ DI (หรือ Dependency Injection) แบบคร่าวๆ คือเราจะไม่ new อ็อบเจคในคลาสแบบที่เล่าไปข้างต้น แต่เพราะวิธีการนี้มันยุ่งยากกว่า คนส่วนใหญ่เลยชอบสร้างคลาสกันแบบ new ตรงๆ ในคลาสเลย ไม่ทำการรับอ็อบเจคเข้ามาทาง Constructure

ดังนั้นเลยมีคนเห็นปัญหานี้ ในเมื่อ DI เป็นคอนเซ็ปที่ดี แต่เขียนยุ่งยากมาก ... จะดีกว่าถ้าเราสามารถสร้างอ็อบเจคที่มี dependencies เต็มไปหมดด้วยการสั่ง new ง่ายๆ

DI Service คือตัวช่วยที่ว่า ซึ่งเป็นเหมือนตัวที่จะคอยจัดการและสร้างอ็อบเจคให้เราแทน

ซึ่งการใช้งานส่วนใหญ่จะแบ่งออกเป็น 2 เฟส นั่นคือการกำหนดค่าว่า Dependencies แต่ละตัวมีวิธีสร้างยังไง หลังจากนั้นคือการสั่งให้มันสร้างอ็อบเจคกลับมาให้เรา

// Setup
DIService di = ...
di.set<Engine>(new Engine());

// Create Object
Car car = di.newObject<Car>();

แต่เอาจริงๆ บางครั้ง ในบางภาษา การจะใช้งาน DI Service พวกนี้ก็ยุ่งยากกว่าการสร้างเองตรงๆ ซะดี (ฮา) ตัวอย่างเช่น Dagger2 สำหรับภาษา Java

แต่..เอ๊ะ! นี่มัน Service Locator ตั้งหาก

ในบางภาษา หากเราเสิร์จหา library เพื่อมาช่วยเราทำ DI Service เราอาจจะเจอตัวที่ใช้แพทเทิร์นแบบ Container

แบบนี้

class Car {

    private Engine engine;

    public Car() {
        this.engine = Container.get<Engine>();
    }

}

นั่นคือ แทนที่จะรับอ็อบเจคเข้ามาผ่าน Constructure ตรงๆ แต่จะใช้วิธีการขอจากสิ่งที่เรียกว่า Container แทน (ไม่ได้ new เองนะ แต่ขออ็อบเจคมาจากคอนเทนเนอร์)

วิธีใช้งานก็คล้ายๆ กับ DI Service นั่นแหละ คือต้องทำการกำหนดค่า dependencies ซะก่อน แล้วถึงจะเรียกใช้งานได้

// Setup
Container.set<Engine>(new Engine());

// Create Object
Car car = new Car();

เอาล่ะ! ถึงผลลัพธ์จะออกมาเหมือนกันเป๊ะ! แถมคนส่วนใหญ่ยังเรียกวิธีการทั้ง 2 ตัวนี้ว่า DI ทั้งคู่อีกตั้งหาก

แต่เนื่องจากโค้ดมันเขียนไม่เหมือนกัน ดังนั้นเขาเลยตั้งชื่อแยกมันออกเป็น 2 แพทเทิร์น นั่นคือ "Dependency Injection" และ "Service Locator"

ข้อแตกต่างล่ะ?

Service Locator

  • คอนเซ็ปคือคลาสเป้าหมาย ขออ็อบเจคจาก Service Locator ซึ่งจะไปตามหามาให้
  • คลาสเป้าหมายเรียกใช้ Service Locator
  • มีสิ่งที่เรียกว่า Container สำหรับเก็บอ็อบเจค
  • คลาสจะขออ็อบเจคจาก Container แทนการ new ขึ้นมาเอง
  • ใช้งานและเข้าใจได้ง่าย เพราะอยากได้อ็อบเจคอะไรก็ขอเอาจาก Container ตรงๆ ได้เลย (ส่วนใหญ่ Container จะเป็นแบบ static คือเรียกจากตรงไหนในโปรแกรมเราก็ได้)
  • ปัญหาคือมันจะทำให้มี dependency เพิ่มขึ้นมาในระบบเราอีก 1 ตัวคือ Container นั่นแหละ แถมคลาสแทบทุกคลาสในระบบเรายัง depend on เจ้าคอนเทนเนอร์นี้ซะอีก
  • ถ้าเกิดอยากเปลี่ยน library สำหรับทำ Service Locator จะต้องแก้โค้ดแทบจะทั้งโปรแกรม!

Dependency Injection

  • คอนเซ็ปคือ DI Service จะเช็กดูว่าคลาสเป้าหมาย เวลาจะสร้างต้องการ dependencies อะไรไป ก็จะไปหามาให้ แล้วเอาไป new อ็อบเจคเป้าหมายขึ้นมา
  • คลาสเป้าหมายไม่ได้เรียกใช้ DI Service, แต่ DI Service เป็นฝ่ายที่จะ new อ็อบเจคขึ้นมาเอง
  • ใช้วิธี inject อ็อบเจคเข้าไปตรงๆ
  • สำหรับคลาสเป้าหมายจะไม่ต้องเปลี่ยนอะไรมากนัก เพราะ dependencies ทั้งหมดถูกส่งเข้ามาทาง Constructure อยู่แล้ว
  • เขียนเทสได้ง่ายโดยการสร้าง mock dependencies ขึ้นมาเองไม่ต้องผ่าน DI Service เลยก็ยังได้

เขียนเทสยังไง?

จริงๆ เป้าหมายของการทำ DI ทั้งหมดคือเพื่อเอามาเขียนเทสหรือการทดสอบโปรแกรมได้ง่ายๆ นะ

class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

}

Car testCar = new Car(new TestEngine());

ซึ่งในเคสนี้ Service Locator จะ mock อ็อบเจคต่างๆ ได้ยากมากเพราะเป็นการเรียกใช้งานแบบ static คือเรียกตรงๆ ที่ Container ในคลาสเลย

ดังนั้นคำแนะนำสำหรับคนใช้แพทเทิร์น Service Locator คืออย่าเรียก Container ตรงๆ แต่ให้ inject Container นี้เข้าไปแทน

แบบนี้

class Car {

    private Engine engine;

    public Car(Container container) {
        this.engine = container.get<Engine>();
    }

}

เพราะจะทำให้เราสร้าง container สำหรับเทสอ็อบเจคขึ้นมาได้

// Develop
Container container = new Container();
container.set<Engine>(new Engine());

Car car = new Car(container);

// Testing
Container testContainer = new Container();
testContainer.set<Engine>(new TestEngine());

Car testCar = new Car(testContainer);
This entry is part 4 of 5 in the series Async-in-Dart-Series

Async in Dart (4) ควบคุมข้อมูลในstreamอย่างเหนือชั้นด้วย StreamController

ในบทที่แล้วเราสอนการสร้าง Stream แบบง่ายๆ ไปแล้ว แต่ในบางครั้งเราต้องการควบคุมข้อมูลใน Stream แบบคัสตอมมากๆ ซึ่งข้อมูลอาจจะเข้ามาจากหลายทางมากๆ การที่เรามีแค่ yield จากฟังก์ชันเดียวอาจจะไม่เพียงพอ

ดังนั้นเลยเป็นที่มาของคลาสที่ชื่อว่า StreamController ซึ่งเป็นคลาสที่เอาไว้ควบคุม Stream อีกที

StreamController

การสร้าง Stream แบบธรรมดาก็จะเป็นประมาณนี้

Stream<int> getNumberStream() async* {
    ...
}

Stream<int> numberStream = getNumberStream();

ส่วนใหญ่เราจะสร้าง Stream จากฟังก์ชันประเภท async* ซึ่งภายในฟังก์ชันก็จะมีการ yield ข้อมูลกลับมา

ทีนี้ ถ้าเราจะเปลี่ยนไปใช้ StreamController แทน ก็จะเป็นแบบนี้

StreamController<int> controller = StreamController<int>();

นั่นคือ จากปกติที่เราจะสร้างข้อมูลในฟังก์ชันเท่านั้น การสร้างด้วย StreamController คือเราไม่ต้องกำหนดว่า Stream ตัวนี้จะสร้างอะไรเลย (แค่ new ตัวคอนโทรเลอร์ขึ้นมาเฉยๆ ก็พอแล้ว)

StreamController ไม่ใช่ Stream นะ ไม่สามารถเอาไปใช้แทน Stream ได้ แต่สามารถดึง Stream ออกมาจากตัวมันได้

ให้คิดง่ายๆ ว่ามันคือ Controller ที่ถือ Stream เอาไว้ข้างในอีกทีก็ได้

สำหรับอ็อบเจค StreamController ที่เราสร้างขึ้นมา จะมี Stream อยู่ข้างใน เราสามารถดึงออกมาใช้งานได้ด้วยการสั่ง .stream ตรงๆ เลย

Stream stream = controller.stream;
stream.listen((value) {
    print('value: $value');
});

//หรือ

controller.stream.listen((value) {
    print('value: $value');
});

แล้วข้อมูลจะมาจากไหน? ใช้คำสั่ง add ยังไงล่ะ

StreamController นั้นไม่ได้เป็นฟังก์ชัน ดังนั้นมันจะไม่มีโลจิคการสร้างข้อมูลอะไรอยู่กับตัวมันทั้งนั้น!

StreamController เป็นแค่ตัวกลางสำหรับใส่ข้อมูลเข้า Stream เท่านั้น

import 'dart:async';

var controller = StreamController<int>();

void main() {
    controller.stream.listen((x) {
        print(x);
    });

    ...
}

void addNumberToStream(int x) {
    controller.add(x);
}

จากโค้ดด้านบน เรามีการสร้าง StreamController ขึ้นมา 1 ตัว แล้วจัดการ listen() มันใน main (listen ไว้ก่อน ยังไม่มีข้อมูลอะไรเข้ามาทั้งนั้น)

ทีนี้ เราก็ไปสร้างฟังก์ชันอีกตัวหนึ่งชื่อ addNumberToStream(int x) ซึ่งจะมีการเพิ่มค่าเข้า Stream ผ่านคำสั่ง add()

เมื่อฟังก์ชัน addNumberToStream(int x) ถูกเรียก StreamController ก็จะส่งค่านั้นไปให้ listener ที่รอค่าอยู่นั่นเอง

เราสามารถส่งข้อมูลกี่ครั้งก็ได้ผ่านคำสั่ง add()

ส่วนใหญ่ use case ที่เราต้องใช้ StreamController แทนการสร้างฟังก์ชัน async* แบบธรรมดาจะเป็นการเขียนโปรแกรมแบบ Event-Driving เพราะเราไม่รู้ว่าข้อมูลจะเข้ามาเมื่อไหร่ ตอนไหนที่ผู้ใช้จะกดปุ่ม อะไรแบบนั้น เราเลยสร้างฟังก์ชันเตรียมไว้ก่อนไม่ได้ ทำได้แค่ให้ controller คอยส่งข้อมูลมาให้เมื่อเกิด Event เท่านั้น

คำเตือน! StreamController มักจะไม่รู้จุดสิ้นสุด!

การใช้งาน Stream ธรรมดา เราสามารถเปลี่ยนจากการใช้ callback ด้วย listen() ไปใช้ await เพื่อเปลี่ยนโค้ดให้อยู่ในรูปแบบ sync แทนได้

Stream<int> numberStream = getNumberStream();

await for(var number in numberStream) {
    print('Receive: $number');
}

ซึ่งมีข้อควรระวังในการใช้ await คือ Stream ตัวนั้นจะต้องเป็น "Finite Stream" หรือสตรีมที่รู้ว่ามีจุดสิ้นสุดแน่ๆ !

ไม่งั้นโค้ดของเราจะถูก await บล็อกการทำงานอยู่ที่ลูปนั้น ไปต่อไม่ได้ เช่น

import 'dart:async';

var controller = StreamController<void>();

void main() {

    print('start');

    await for(var _ in controller.stream) {
        print('clicked!');
    }

    print('end');

    ...
}

void onClick() {
    controller.add(null);
}

ในกรณีแบบนี้ โค้ดจะถูกบล็อกอยู่ที่ลูป (โปรแกรมจะไม่หลุดไปยังคำสั่งปริ้น end เลย) เพราะไม่มีการปิดสตรีม

ดังนั้นถ้าฟังก์ชันเรามีคำสั่งให้ทำอะไรบ้างอย่างต่อไป เราไม่ควรเขียนมันในรูป await นะ ใช้ listen() จะดีกว่า

ยังมี addError และ close อีกนะ

นอกจากการ add ข้อมูลเข้า Stream แล้วเรายังสามารถสั่งอีก 2 คำสั่งกับคอรโทรเลอร์ได้ คือการโยน Exception หรือสั่งปิด Stream นั่นเอง

import 'dart:async';

var controller = StreamController<int>();

void main() {
    controller.stream.listen(
        (x) {
            print(x);
        },
        onDone: () {
            print('จบแล้วจ้า!');
        },
        onError: (e){
            print('error is $e');
        },
        cancelOnError: false,
    );

    print('start');
    controller.add(1);
    controller.add(2);
    controller.add(3);
    controller.addError(Exception('ตู้มมม'));
    controller.add(4);
    controller.add(5);
    controller.close();
    print('end');
}

output:

start
end
1
2
3
error is Exception: ตู้มมม
4
5
จบแล้วจ้า!

การสั่ง addError ไม่ได้ทำให้ Stream หยุดทำงานนะ ไม่งั้นต้องเพิ่มอ็อบชัน cancelOnError: true ลงไปด้วย (ถ้าไม่เซ็ต ดีฟอลต์จะเป็น false คือถึงเจอ Exception ฉันก็ไม่หยุดนะ)

ข้อสังเกตอีกอย่างคือ listen() นั้นเป็น Isolates นะ มันจะปริ้น start และ end (อยู่ใน Isolates ของ ฟังก์ชัน main) ก่อน

ใครงงว่า Isolates คืออะไร ย้อนกลับไปอ่านบทความแรกของซีรีส์นี้อีกทีนะ

สุดท้าย การใช้งาน StreamController นั้นมีอีกเรื่องที่ต้องระวัง คือ...

Stream อาจจะไม่ได้ถูก subscription ในทันที

ลองดูตัวอย่างโค้ดนี้

Stream<int> counter1(int maxCount) {
    var controller = StreamController<int>();
    int counter = 0;

    Timer.periodic(Duration(seconds: 1), (Timer timer) {
        counter++;
        controller.add(counter);
        if (counter >= maxCount) {
            timer.cancel();
            controller.close();
        }
    });

    return controller.stream;
}

เราตั้งให้ฟังก์ชัน counter1 ทำการ add ตัวเลขเข้าไปเรื่อยๆ ทุกๆ 1 วินาที (ฟังก์ชัน Timer.periodic() เอาไว้สำหรับสั่งงานที่ทำงานซ้ๆ กันทุกๆ ช่วงเวลาหนึ่ง คล้ายๆ กับ setInterval ใน JavaScript)

เวลาใช้งาน ก็ใช้ listen() ตามปกติหรือใช้ for loop แล้วเติม await ลงไปก็ได้

void main() {
    //start!
    var counterStream = counter1(5);

    //listen
    counterStream.listen((n){
        print(n);
    });
}

แบบนี้ถ้าเอาไปรัน ก็จะได้ output เป็นเลข 1, 2, 3, 4, 5 ตัวใน 5 วินาที ... อันนี้มาตราฐาน ตามคอมมอนเซ้นส์ ไม่มีอะไรแปลก

แต่ถ้าเรามีการสั่ง delay ก่อนที่เราจะ listen() แบบนี้

void main() {
    //start!
    var counterStream = counter1(5);

    //waiting 2 sec.
    await Future.delayed(const Duration(seconds: 2));

    //listen
    counterStream.listen((n){
        print(n);
    });
}

คำถามคือถ้าเขียนโค้ดแบบนี้ เราคาดหวังว่า output จะเป็นยังไง?

  • โปรแกรมก็น่าจะหยุดรอ 2 วินาที
  • แล้วก็ค่อยๆ ปริ้นเลข 1, 2, 3, 4, 5 ทีละวิ

แต่มันไม่ใช่แบบนั้นน่ะสิ!

การทำงานของโปรแกรมคือหลังจาก delay 2 วินาทีแล้ว มันจะทำการปริ้นเลข 1, 2 ออกมาในทันที (ดูรูปข้างล่างประกอบนะ)

สำหรับการใช้งาน Stream นั้นมันจะทำงานทันที แม้ว่าจะยังไม่มีใครมารอ listen มันเลยก็ตาม

แบบในเคสนี้ เราเรียกฟังก์ชัน counter1() ให้ทำงาน แล้วทำการ delay มันซะ 2 วินาทีแล้วค่อย listen แต่ตัว counter นั้นดันทำงานก่อนไปแล้ว ไม่ได้เริ่มทำงานตอนที่เรา listen

แต่ถ้าเราต้องการให้มันทำงานเมื่อสั่ง listen จังหวะนั้นเลย (แบบ counter2 ในรูปข้างบน) เราจะต้องเซ็ตค่าตอนสร้าง StreamController

ในการสร้าง StreamController เราสามารถกำหนดอ็อบชันเพิ่มได้คือ

  • onListen
  • onPause
  • onResume
  • onCancel

วิธีการก็คือเราจะต้องสร้าง Stream ที่ตอนแรกยังไม่เริ่มนับ counter แล้วสั่งให้มันเริ่มนับตอน onListen นั่นเอง

Stream<int> counter2(int maxCount) {
    StreamController<int> controller;
    Timer timer;
    int counter = 0;

    //ฟังก์ชันสำหรับเริ่มรับเลข
    void startTimer() {
        timer = Timer.periodic(
            Duration(seconds: 1), 
            (_) {
                counter++;
                controller.add(counter);
                if (counter == maxCount) {
                    timer.cancel();
                    controller.close();
                }
            },
        );
    }

    //ฟังก์ชันสำหรับหยุดนับเลข
    void stopTimer() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    controller = StreamController<int>(

        //เริ่มทำงาน startTimer() เมื่อ listen นะ
        onListen: startTimer,

        onPause: stopTimer,
        onResume: startTimer,
        onCancel: stopTimer,
    );

    return controller.stream;
}
Series Navigation<< Async in Dart (3) ตอบค่าเป็นกระแสข้อมูลด้วย StreamAsync in Dart (5) รู้จักกับ FutureBuilder/StreamBuilder, ตัวช่วยสร้าง Async-Widget ใน Futter >>

Deno เป็นเอ็นจิน runtime ที่เอาไว้รันภาษา JavaScript (และ TypeScript) คล้ายๆ กับ V8-Engine ที่ใช้ใน Node.js

สร้างโดย Ryan Dahl ที่เป็นผู้เริ่มโครงการ Node.js ตั้งแต่ปี 2009
ตอนแรกพัฒนาด้วยภาษา Go แต่เปลี่ยนไปเขียนด้วยภาษา Rust แทนในภายหลัง (V8 ใช้ C++)

ฟีเจอร์เด่นๆ ของ Deno

  • Support TypeScript: นี่น่าจะเป็นข้อดีอันดับต้นๆ เลยล่ะ เพราะตามกฎการเขียน JavaScript ที่กล่าวไว้ว่า "คุณไม่ควรเขียน JavaScript!...ให้เขียน TypeScript แทน" (ฮา)
  • Security by default: แอพพลิเคชันที่รัน จะไม่สามารถเข้าถึงระบบ File System, Network, หรือพวก Environment ได้เลยถ้ายังไม่มีการกำหนดสิทธิตอนรันให้ เช่นต้องใส่ flag --allow-net เพิ่ม
  • มี build-in utilities ให้ใช้ เช่น dependency inspector, code formatter
  • สำหรับ standard modules นั้นมีกลุ่ม reviewer คอยเช็กความถูกต้องและเทสเพื่อการันตีว่ามันทำงานได้เวิร์คจริงๆ นะ

มี Node.js อยู่แล้ว จะสร้าง Deno มาทำไม?

Ryan Dahl ผู้สร้าง Deno บอกเอาไว้ว่า
JavaScript นั้นเปลี่ยนไปมากหลังจาก Node.js ถูกสร้างขึ้นตั้งแต่ปี 2009
อารมณ์ก็คล้ายๆ กับภาษา PHP ที่ไม่ได้ออกแบบโครงสร้างที่ดีมาตั้งแต่ต้น พอจะต่อเติมอะไรเข้าไปเพิ่มก็ทำได้ยาก

เช่น Module System นั้นออกแบบมาไม่ดีตั้งแต่แรก, มี API เก่าที่ควรเลิกใช้ได้แล้วแต่ก็ยังต้องทำงานอยู่, ความปลอดภัย

Ryan Dahl บอกว่าถึงแม้ Deno มันจะหน้าตาดูคล้ายๆ Node.js แต่มันถือว่าเป็นคนละตัวกันเลยนะ ตอนนี้ Deno ยังใหม่มาก อาจจะยังไม่เหมาะกับการใช้งานในบางโปรเจค โดยเฉพาะโปรเจคที่ยังต้องพึ่ง library จาก npm อยู่

installation

เข้าไปดูจาก https://deno.land/ ได้เลย

Hello World

ตัวอย่างการสร้าง server แบบง่ายๆ จะเห็นว่ามันเป็น JavaScript นั่นแหละ แต่มันไม่ใช่ Node.js

import { serve } from "https://deno.land/[email protected]/http/server.ts";

const s = serve({ port: 8000 });
console.log("http://localhost:8000/");

for await (const req of s) {
  req.respond({ 
    body: "Hello World\n" 
  });
}

Deno เพิ่งออกเวอร์ชัน 1.0 ตอนนี้ก็ต้องรอดูกันต่อไป ว่าไดโนเสาร์ตัวนี้จะมาแทน Node.js ได้ในอนาคตหรือไม่?

This entry is part 3 of 5 in the series Async-in-Dart-Series

ในการเขียนโปรแกรมทั่วๆ ไปเรามักจะส่งข้อมูลกันในรูปแบบของ sync เช่น

int getNumber() {
    return 1;
}

การเรียกใช้งานก็ง่ายๆ แบบนี้

print(getNumber());

แต่ในบทก่อนๆ เราเคยสอนเรื่องของ Generator ไปแล้ว นั่นคือการที่ส่งข้อมูลแบบหลายๆ ตัวกลับมาในรูปแบบของ sync*

ทบทวนแบบเร็วๆ! คือแทนที่จะตอบข้อมูลกลับแค่ครั้งเดียวด้วย return ก็เปลี่ยนเป็น yield แทน (yield ตอบได้หลายครั้งมากกว่า return)

Iterable<int> getNumbers() sync* {
    yield 1;
    yield 2;
    yield 3;
}

เวลารับข้อมูลจากฟังก์ชันพวกนี้ไปใช้ ก็เลยต้องใช้การวนลูปนั่นเอง

for(var num in getNumbers()) {
    print(num);
}

ถ้าสรุปเป็นรูป ก็จะได้แบบข้างล่างนี่

แต่ทั้ง 2 วิธีนี้มันเป็นการส่งข้อมูลแบบ Synchronous คือโค้ดยังทำงานเรียงๆ กันตามลำดับ และโค้ดทั้งหมดจะไม่ถูกแตกเป็นส่วนๆ เข้า Event Loop แบบ Asynchronous

ดังนั้นต่อไป... เรามาดู 2 กรณีนี้ แต่อยู่ในรูปของ Asynchronous กันบ้างดีกว่า!


ในบทที่แล้วเราพูดถึง Future กันไปแล้ว ซึ่งมันคือการเปลี่ยนจาก sync ให้กลายเป็น async นั่นเอง

แปลว่าเรายังขาดอยู่หนึ่งตัว คือ async* นั่นเอง

Single Value Zero or more Values
Sync int Iterable
Async Future Stream

สรุปง่ายๆ คือ Stream ก็คือ Iterable ที่อยู่ในรูปของ Asynchronous นั่นเอง

วิธีการดึงค่าออกมาจาก Stream

การใช้งาน Stream ก็คล้ายๆ กับ Future นั่นแหละ แต่มีรายละเอียดเยอะกว่า เพราะมันตอบได้หลายค่ามากกว่า Future ยังไงล่ะ

ขอสมมุติว่าเรามีฟังก์ชันหนึ่ง ที่ตอบค่าเป็น Stream ชื่อ getNumberStream() นะ

รอฟังค่าเรื่อยๆ ด้วย listen

สำหรับ Future เราจะใช้คำสั่ง then() ในการรอค่า แต่ Stream จะใช้ listen() แทน

Stream<int> numberStream = getNumberStream();

var subscription = numberStream.listen((num){
    print('Receive: $num');
});

listen() จะตอบกลับเป็น subscription ซึ่งเดี๋ยวจะพูดถึงอีกที
แน่นอนว่าเวลาเอาไปรัน ผลที่ได้อาจจะเป็นแบบนี้

output:
Receive: 1
Receive: 2
Receive: 3
Receive: ...

ก็คือเราได้รับตัวเลขหลายๆ ตัวนั่นเอง แต่อาจจะได้ตอนไหนก็ไม่รู้ แบบนี้

time            │     │    │
0|             1sec.  │    │
1| Receive: 1  ─┘    2sec. │
2| Receive: 2  ───────┘    │
3|                        4sec.
4| Receive: 3  ────────────┘
5|

สำหรับตัว listen() นั้นมี options เสริมให้เราใช้งานได้หลายตัว คือ

  • onError: เมื่อเกิด error ให้ทำอะไร
  • onDone: เมื่อได้รับข้อมูลครบทุกตัวแล้วให้ทำอะไร (ทำงานเมื่อ Stream ตอบข้อมูลครบหมดทุกตัวแล้ว มันจะเรียก onDone ให้ทำงาน)
  • cancelOnError: เป็นตัวกำหนดว่าถ้าเกิด error ขึ้นแล้ว (ในกรณีที่ข้อมูลยังส่งกลับมาไม่ครบทุกตัว) จะเลือกที่จะหยุดการทำงานของ Stream ไปเลย หรือจะยังให้ทำงานต่อก็ได้
var subscription = numberStream.listen(
    (num){
        print('ได้รับข้อมูล $num');
    }, 
    onError: (err){
        print('มีข้อผิดพลาดเกิดขึ้นล่ะ $err');
    },
    onDone: (){
        print('ได้รับข้อมูลครบแล้วจ้า');
    },
    cancelOnError: false,
);

ลดรูป Stream ด้วย await

เช่นเดียวกับตอน Future นะ คือเราสามารถเปลี่ยน listen() ให้ไปอยู่ในรูปแบบการเขียนแบบ Synchronous ได้ แต่ก็จะไม่ได้ตรงๆ แบบ Future นะเพราะเราต้องรับข้อมูลด้วยลูป

Stream<int> numberStream = getNumberStream();

await for(var number in numberStream) {
    print('Receive: $number');
}

Broadcast Stream

ตามปกติแล้วการใช้งาน Stream จะมีการ listen() ได้แค่ครั้งเดียวเท่านั้น

เมื่อเรา subscription ไปครั้งหนึ่งแล้ว ถ้าจะ subscription ซึ่งกับ Stream ตัวเดิมมันจะเกิด Exception ขึ้นนะ!!

Stream<int> numberStream = getNumberStream();

var subscription1 = numberStream.listen((num){
    print('Receive: $num');
});

//second subscribe -> Exception!!
var subscription2 = numberStream.listen((num){
    print('Receive: $num');
});

วิธีการแก้ (ถ้าอยาก subscription หลายครั้งจริงๆ) ให้เปลี่ยน Stream ตัวนั้นให้เป็นสิ่งที่เรียกว่า "Broadcast Stream" แทน

วิธีเปลี่ยนก็ง่ายๆ คือใช้ get property ที่ชื่อว่า asBroadcastStream แบบนี้

Stream<int> numberStream = getNumberStream().asBroadcastStream;

var subscription1 = numberStream.listen((num){
    print('First receive:  $num');
});

var subscription2 = numberStream.listen((num){
    print('Second receive: $num');
});

ทีนี้ ถ้า Stream ของเรามีการส่งตัวเลข [1 เมื่อผ่านไป1วินาที] และ [3 เมื่อผ่านไป3วินาที] ก็จะได้ผลลัพธ์แบบนี้ (สังเกตดูว่าเราได้ข้อมูลตัวละ 2 ครั้ง เพราะมี subscription 2 ตัวนั่นเอง)

time                   │     │
0|                   1sec.   │
1| First receive:  1  ─┤     │
 | First receive:  1  ─┘    3sec.
2|                           │
3| Second receive: 3  ───────┤
 | Second receive: 3  ───────┘
4|

Subscription

กลับมาที่สิ่งที่เราพูดค้างไว้ นั่นคือตอบที่เราสั่ง listen() เราจะได้สิ่งที่เรียกว่า subscription กลับมา

เพราะ Stream คือ "กระแสข้อมูล" ที่ไหลมาเรื่อยๆ มาตอนไหนก็ไม่รู้ เลยมี subscription เอาไว้ควบคุมกระแสนั้นอีกที

//หยุดการรับข้อมูล
subscription.pause();

//กลับรับข้อมูลใหม่
subscription.resume();

//แคนเซิล Stream ตัวนั้น
subscription.cancel();

แต่มีข้อควรระวังอย่างนึง คือการ pause() ไม่ใช้การไม่รับข้อมูลในจังหวะนั้น แต่เป็นการหยุดรับข้อมูลชั่วคราวเท่านั้น แปลว่า "ข้อมูลไม่ได้หายไปนะ มันแค่เข้าคิวรอเรา resume() อีกทีนั่นเอง!"

ลองดูตัวอย่างข้างล่างประกอบ ขอสมมุติว่าเรามี stream อยู่หนึ่งตัวที่จะส่งตัวเลขกลับมาเรื่อยๆ ทุกๆ 1 วินาที [1, 2, 3, 4, 5, ...] แบบนี้นะ

var subscription = stream.listen((x){
    print('Receive: $x');
});
Future.delayed(Duration(seconds: 3), (){
    subscription.pause();
});
Future.delayed(Duration(seconds: 6), (){
    subscription.resume();
});
Future.delayed(Duration(seconds: 8), (){
    subscription.cancel();
});

แล้วเราก็ตั้งค่า (โดยใช้ Future.delayed) ให้มัน..

  • pause: เมื่อผ่านไป 3 วินาที
  • resume: เมื่อผ่านไป 6 วินาที (จากตอนเริ่มโปรแกรม)
  • cancel: เมื่อผ่านไป 8 วินาที (จากตอนเริ่มโปรแกรม)

เราอาจจะคิดว่าระหว่างวินาทีที่ 3-6 เราจะไม่ได้รับข้อมูลช่วงนั้น (คือข้อมูล [4,5] หายไปเลย)

แต่ไม่ใช่นะ! เพราะเลข [4,5] นั้นยังอยู่นะ แค่มันเข้าคิวรอที่จะออกมาอยู่

เมื่อเรา resume ในวินาทีที่ 6 มันก็จะออกมาทีเดียวหมดเลย [4,5,6]

ดูอธิบายด้วย timeline ข้างล่างนี่น่าจะเข้าใจมากกว่า

time  
1| Receive: 1
2| Receive: 2
3| Receive: 3
4|            ├─ waiting until (6)
5|            │
6| Receive: 4 ┐
 | Receive: 5 ├─ 3 values in 1 sec.
 | Receive: 6 ┘
7| Receive: 7
8| Receive: 8
9|

มาลองสร้าง Stream กันบ้าง

การสร้าง Stream ทำได้หลายวิธีมากๆ แต่แบบง่ายที่สุดคือใช้วิธีแบบ Generator

แปล Iterable เป็น Stream

เราเคยสอนการสร้าง Generator ไปแล้วในบท Dart 105: มันวนซ้ำได้นะ! มาสร้าง Generator และใช้งาน Iterable กันเถอะ แต่ตอนนั้นเราเขียนมันในรูปแบบของ sync

อย่างที่พูดไปตอนต้นว่า Stream ก็คือ Iterable ที่อยู่ในรูปของ Asynchronous เราจะมาแสดงให้เห็นกัน

ขอเปิดด้วยโค้ดแบบ Iterable ในรูปของ Synchronous

โจทย์คือ... เราต้องการสร้างฟังก์ชันสำหรับดึง Data ออกมาจำนวนหนึ่งตั้งแต่ from ถึง to

Data fetchOneData(int id){
    ...
}

Iterable<int> getData(int from, int to) sync* {
    var dataList = [];
    for(var i=from; i<=to; i++){
        dataList.add(fetchOneData(i))
    }
    return dataList;
}

(ฟังก์ชัน fetchOneData() ทำอะไรไม่ต้องสนใจนะ ไม่ใช่ประเด็นของเรื่อนี้)

แต่เพื่อ performance ที่ดี เลยเราไม่โหลดข้อมูลในครั้งเดียว แต่เลือกที่จะเขียนมันในรูปแบบ Generator แทน ก็จะได้โค้ดแบบนี้..

Data fetchOneData(int id){
    ...
}

Iterable<int> getData(int from, int to) sync* {
    for(var i=from; i<=to; i++){
        yield fetchOneData(i);
    }
}

เวลาเราจะใช้งาน ก็เอาไปวนลูปได้ธรรมดาๆ แบบนี้

var dataList = getData(1, 10);

for(var data in dataList) {
    print(data);
}

ขึ้นต่อไปคือ แล้วถ้าข้อมูลของเราไม่ได้มาแบบ sync ล่ะ?

ถ้าฟังก์ชัน fetchOneData() ใช้เวลาโหลดข้อมูลนานขึ้น เราคงต้องเปลี่ยนมันเป็น Future แบบนี้

Future<Data> fetchData(int id) {
    ...
}

เมื่อเป็นแบบนี้ เราก็มีปัญหาซะแล้ว!

เพราะฟังก์ชัน getData() ที่เป็น sync ไม่สามารถเรียก fetchOneData() ที่เป็น async ได้ล่ะ!!

วิธีการแก้ก็คือเปลี่ยนฟังก์ชัน fetchOneData() ให้หลายเป็น async ยังไงล่ะ

"ด้วยการเปลี่ยน Iterable ให้กลายเป็น Stream"

สิ่งที่เราต้องทำ มีอยู่ 3 อย่าง

  1. จากเดิมที่รีเทิร์นค่าเป็น Iterable เราต้องเปลี่ยนมันเป็น Stream แทน
  2. จากเดิมที่ฟังก์ชันเป็นชนิด sync* เราต้องเปลี่ยนมันเป็น async*
  3. จากเดิมที่เรา yield ค่าทันทีได้เลย แต่เมื่อมันเป็น Future แล้วเราก็ต้องสั่งให้มันรอด้วย await
Stream<Data> getData(int from, int to) async* {
    for(var id=from; id<=to; id++) {
        yield await fetchOneData(id);
    }
}

void main() {
    fetchAllData(1, 10).listen(print);
}

จะเห็นว่า Iterable นั้นแปลงเป็น Stream ได้ง่ายๆ เลย

ลองมาดูกันอีกตัวอย่างกัน

int sumIt(List<int> items) {
    int sum = 0;
    for(var item in items) {
        sum += item;
    }
    return sum;
}

เรามีฟังก์ชัน sumIt() สำหรับหาผลรวมทั้งหมดใน List แต่ถ้าลิสต์ตัวนี้ไม่ได้ค่ามาแบบ sync ล่ะ? จะทำยังไง?

คราวนี้ให้สังเกตว่า parameter นั้นรับ List (หรือก็คือ Iterable นั่นแหละนะ) เข้ามา แล้วมันรีเทิร์นค่าแบบสเกล่าธรรมดา (คือ int ธรรมดาๆ นี่แหละ)

สำหรับเคสนี้ สิ่งที่เราต้องทำก็คือ

  1. เปลี่ยนค่าแบบสเกล่าธรรมดาให้กลายเป็น Future
  2. เปลี่ยนให้ฟังก์ชันเป็น async (ระวัง! ไม่ได้เปลี่ยนเป็น async* นะ เพราะมันไม่ได้รีเทิร์น Stream)
  3. จุดที่เป็นปัญหาคือ for loop ที่วนค่าตัว Stream .. ให้เราเติม await ลงไปข้างหน้าเป็นอันจบ
Future<int> sumIt(Stream<int> items) async {
    int sum = 0;

    await for(var item in items) {
        sum += item;
    }

    return sum;
}

เรื่องสุดท้ายคือในบางกรณี เราอาจจะมีฟังก์ชัน Stream ที่เรียกใช้งาน Stream อีกตัวก็เป็นได้นะ

Stream<Data> getFirstStream() async* {
    ...
}

Stream<Data> getSecondStream() async* {
    yield getFirstStream(); //Not Work!!
}

วิธีการนี้โค้ดอาจคอมไพล์ได้ แต่เราจะไม่ได้ข้อมูลอะไรเลย (ถ้าใครยังไม่เข้าใจว่าทำไมไม่เวิร์ค ลองกลับไปอ่านบทที่เราพูดถึง Generator อีกทีนะ)

วิธีการแก้ก็เหมือนกับฟังก์ชัน sync* นั่นแหละ คือเราจะต้องวนลูปทีละตัว

Stream<Data> getFirstStream() async* {
    ...
}

Stream<Data> getSecondStream() async* {
    await for(var s in getFirstStream()){
        yield s;
    }
}

หรือแบบง่ายกว่าคือการใช้ yield* ก็ได้

Stream<Data> getFirstStream() async* {
    ...
}

Stream<Data> getSecondStream() async* {
   yield* getFirstStream(); //Ok!!
}

นอกจากวิธีสร้างแบบ Generator ที่เราใช้แนวคิดเดียวกับ Generator ใน sync แล้วยังมีอีกวิธีหนึ่งที่ให้เราคุมการไหลของข้อมูลใน Stream ได้คล่องตัวขึ้น นั่นคือใช้ StreamController

ซึ่งเราจะพูดถึงกันในบทต่อไปนะ (พร้อมกับเก็บตกเนื้อหาเกี่ยวกับ Stream กันอีกนิดหน่อยด้วย)

Series Navigation<< Async in Dart (2) เดี๋ยวจะตอบให้ในอนาคต กับการใช้ FutureAsync in Dart (4) ควบคุมข้อมูลในstreamอย่างเหนือชั้นด้วย StreamController >>