Computer >> คอมพิวเตอร์ >  >> ระบบ >> Android

สร้างแอป Android ด้วย Kotlin และ Jetpack Compose

Jetpack Compose เป็นชุดเครื่องมือที่ทันสมัยของ Android สำหรับการสร้าง UI ดั้งเดิม ช่วยลดความยุ่งยากและเร่งการพัฒนา UI บน Android

เราเพิ่งเปิดตัวหลักสูตรในช่อง YouTube freeCodeCamp.org ซึ่งจะสอนวิธีสร้างแอป Sudoku สำหรับ Android ด้วย Kotlin และ Jetpack Compose

ระหว่างทาง คุณจะได้เรียนรู้เกี่ยวกับโครงสร้างข้อมูลกราฟและอัลกอริธึม

Ryan M. Kay พัฒนาหลักสูตรนี้ Ryan เป็นนักพัฒนาและครูมากประสบการณ์

ต่อไปนี้คือหัวข้อที่ครอบคลุมในหลักสูตรนี้:

  • แนวทางการออกแบบแอป:ห้องสมุดบุคคลที่สาม Minimalism &MV- ไม่ว่าสถาปัตยกรรมใด
  • แพ็คเกจโดเมน:Repository Pattern, Enum, Data Class, Sealed Class, Hash Code, Interfaces
  • แพ็คเกจทั่วไป:ฟังก์ชันส่วนขยายและตัวแปร, หลักการเปิด-ปิด (OCP), คลาสนามธรรม, ซิงเกิลตัน
  • แพ็กเกจ Persistence (Storage):Clean Architecture Back End w/ Java File System Storage, Jetpack Proto Datastore
  • แพ็คเกจ UI:Jetpack Compose พื้นฐาน UI สไตล์ การพิมพ์ ธีมสีอ่อนและสีเข้ม
  • แพ็คเกจส่วนประกอบ UI:ตัวดัดแปลง แถบเครื่องมือที่ใช้ซ้ำได้ &หน้าจอโหลด
  • แพ็คเกจฟีเจอร์เกม UI Active:Presentation Logic &ViewModel w/ Coroutines, Kotlin Function Types
  • แพ็คเกจฟีเจอร์เกม UI Active:เกม Sudoku พร้อม Jetpack Compose UI &คอนเทนเนอร์กิจกรรม
  • แพ็คเกจการคำนวณเชิงตรรกะ:ภาพรวม การออกแบบ และการทดสอบกราฟ DS &Algos สำหรับซูโดกุ *square* ขนาด n

ดูหลักสูตรเต็มด้านล่างหรือในช่อง YouTube freeCodeCamp.org (รับชม 3.5 ชั่วโมง)

Transcript

(สร้างอัตโนมัติ)

ในหลักสูตรนี้ คุณจะได้เรียนรู้การสร้างแอป Android โดยใช้ไลบรารี UI ของการเขียน Jetpack

ระหว่างทาง คุณจะได้เรียนรู้เกี่ยวกับอัลกอริธึมกราฟและโครงสร้างข้อมูล

Ryan M. Kay สอนหลักสูตรนี้

เขาเป็นนักพัฒนาและผู้สอนมากประสบการณ์

ว่าไงนะทุกคน? นี่คือไรอันที่นี่ และฉันขอต้อนรับคุณสู่ซีรีส์บทแนะนำเกี่ยวกับแอปพลิเคชันกราฟ Sudoku

นี่คือแอปพลิเคชันที่ฉันเขียนขึ้นเป็นหลักเพื่อปรับความเข้าใจของฉันเกี่ยวกับโครงสร้างข้อมูลกราฟและอัลกอริธึม และไลบรารี UI ใหม่บน Android jetpack ที่เขียนขึ้นมา

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

ฉันจะพยายามทำให้เวอร์ชันสาธารณะของซอร์สโค้ดของโครงการทันสมัยอยู่เสมอ

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

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

สำหรับนักพัฒนาขั้นสูง แหล่งข้อมูลเต็มรูปแบบมีให้สำหรับการเรียนรู้โดยตรง แต่คุณสามารถดูวิดีโอเพื่อล้างช่องโหว่ในความรู้ของคุณ

ฉันใช้คุณสมบัติขั้นสูงบางอย่างของภาษา kotlin และหลักการที่ไร้กาลเวลาของสถาปัตยกรรมซอฟต์แวร์ ดังนั้นคุณอาจเรียนรู้สิ่งหนึ่งหรือสองสิ่ง

สำหรับผู้เริ่มต้น เป็นสิ่งสำคัญมากที่คุณจะต้องทำตามโค้ดกับฉันตามจังหวะของคุณเอง

คุณอาจไม่รู้สึกว่ากำลังก้าวหน้าในตอนแรก แต่เข้าใจว่าคุณจะได้ฝึกทักษะการเขียนโค้ด แม้ว่าคุณจะยังไม่เข้าใจว่าคุณกำลังเขียนอะไรอยู่

ฉันจะแอบสอนวิธีเขียนโค้ดให้คุณซึ่งง่ายต่อการเขียน อ่าน แก้ไข ปรับปรุง และทดสอบ

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

มีสี่หัวข้อทั่วไปที่บทช่วยสอนนี้จะแสดงโครงสร้างข้อมูลกราฟและอัลกอริธึม jetpack ที่เขียนสถาปัตยกรรม UI ที่สะอาดตา และคุณลักษณะภาษา kotlin

เราจะสำรวจหัวข้อของกราฟสีกำกับและการทดลองของฉันในการเขียนอัลกอริธึมเพื่อสร้างปริศนาซูโดกุขนาด n ที่แก้ได้และยังไม่ได้แก้

เราจะสร้างอินเทอร์เฟซผู้ใช้ทั้งหมดโดยใช้การเขียนแบบ Jetpack ซึ่งช่วยให้เราสร้าง UI ของเราใน kotlin ได้ทั้งหมด

ตรงข้ามกับมุมมองและรูปแบบ XML

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

อย่างไรก็ตาม ฉันสอนหัวข้อเหล่านี้ด้วยวิธีของฉันเอง ดังนั้นอย่าคาดหวังศัพท์แสงหรือวิศวกรรมศาสตร์มากนัก

ฉันยังจะสาธิตวิธีการและเวลาที่ต้องใช้คุณสมบัติพื้นฐานและขั้นสูงของภาษาการเขียนโปรแกรมที่สวยงามนี้

วิดีโอส่วนนี้จัดทำขึ้นสำหรับนักพัฒนาระดับกลางและระดับสูง

ไม่จำเป็นต้องเข้าใจการตัดสินใจออกแบบนี้เพื่อจบบทแนะนำ

ตลอดบทช่วยสอนนี้ คุณจะสังเกตเห็นว่านอกจากการเรียบเรียงและที่เก็บข้อมูลโปรโตแล้ว ฉันแทบไม่ใช้ไลบรารี่ใด ๆ จาก Android jetpack เลย อันที่จริง ไลบรารีของบุคคลที่สามโดยทั่วไปน้อยมาก โดยอาศัยไลบรารีมาตรฐาน kotlin และ Java และ Android SDK รหัสของฉัน ทนทานต่อการเลิกใช้งานและการเปลี่ยนแปลงในไลบรารีมากขึ้น

นี่เป็นเพราะว่า Android SDK และไลบรารีมาตรฐานมีแนวโน้มที่จะเปลี่ยนแปลงน้อยกว่าไลบรารีของบุคคลที่สาม เช่น ที่คุณเห็นใน Android Jetpack

นอกจากนี้ยังหมายความว่าบางสิ่งที่ไลบรารีเช่น jetpack viewmodel, jetpack, การนำทางหรือความช่วยเหลืออาจจัดการต้องเขียนด้วยมือเรา

ฉันชอบแบบนั้นจริงๆ แต่คุณอาจมีระบบค่าที่ต่างออกไป

และเป้าหมายของฉันที่นี่คือไม่กีดกันคุณจากการเรียนรู้เครื่องมือเหล่านี้หากคุณสนใจเครื่องมือเหล่านี้

จากที่กล่าวมา คุณอาจแปลกใจว่ามันง่ายเพียงใดในการเขียนการนำทาง viewmodel ของคุณเองหรือโค้ดการพึ่งพาอาศัยกันโดยไม่มีพวกเขาในแอปเล็กๆ แบบนี้

แอปพลิเคชันนี้ใช้ Model View ไม่ว่าจะเป็นสถาปัตยกรรมใดก็ตาม ซึ่งเป็นวิธีที่ฉันบอกว่าฉันไม่ทำตามสไตล์ของใครๆ

หลังจากศึกษาหัวข้อนี้มาหลายปีแล้ว ฉันปล่อยให้ข้อกำหนดของโครงการในการทำความเข้าใจหลักการออกแบบซอฟต์แวร์ที่ดีเป็นแนวทางในสถาปัตยกรรมของฉัน

ในกรณีนี้ ฉันพบว่าการเรียบเรียงถูกสร้างขึ้นสำหรับวิธีการที่ใช้ MVVM แต่ฉันได้เพิ่มคลาสตรรกะของการนำเสนอด้วยเหตุผลเฉพาะ

เหตุผลนี้เรียกว่ามุมมองแบบพาสซีฟหรือวัตถุที่ต่ำต้อย

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

ชั้นเรียนนี้เขียนและทดสอบได้ง่ายสุด ๆ เนื่องจากไม่มีการพึ่งพาบุคคลที่สาม

และป้องกันไม่ให้โมเดลการดูของฉันกลายเป็นวัตถุพระเจ้าที่น่าเกลียด

ต้องลองสักครั้ง

ฉันออกแบบสถาปัตยกรรมนี้ง่ายๆ โดยใช้หลักการที่สำคัญที่สุดเพียงข้อเดียวของสถาปัตยกรรมซอฟต์แวร์ แยกข้อกังวลต่างๆ ออก

เพียงเท่านี้ในซีรีส์นี้

ตอนนี้เรามาเริ่มเขียนโค้ดกัน

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

นอกจากนี้ สิ่งทั่วไปส่วนใหญ่ที่โปรแกรมนี้ต้องทำ ซึ่งโปรแกรมแสดงโดยใช้ฟังก์ชันและอินเทอร์เฟซ

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

สำหรับการแนะนำกระบวนการนั้นที่ชัดเจนและเรียบง่าย

ดูวิดีโอนี้ในช่องของฉัน วิธีออกแบบระบบข้อมูลและแอปพลิเคชัน

วิดีโอนั้นเป็นบันทึกการสนทนาที่ฉันพูดคุยกับวิศวกรซอฟต์แวร์ในอียิปต์เกี่ยวกับหัวข้อนั้นๆ

อย่างไรก็ตาม โค้ดส่วนใหญ่ในแพ็คเกจนี้เรียบง่าย แต่มีรูปแบบการออกแบบ ซึ่งฉันจะแนะนำในตอนนี้

แพ็คเกจนี้ประกอบด้วยอินเทอร์เฟซหลายแบบ ซึ่งใช้เพื่อสร้างรูปแบบการออกแบบที่เก็บ

รูปแบบนี้เรียกอีกอย่างว่ารูปแบบซุ้ม

และเป้าหมายทั่วไปของรูปแบบนั้นง่ายกว่าคำจำกัดความทางเทคนิค

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

มาดูตัวอย่างการใช้งานจริงกัน

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

สิ่งนี้ให้ประโยชน์หลายประการกับคลาสตรรกะการนำเสนอของเรา

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

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

ประโยชน์เหล่านี้เป็นผลมาจากการสร้างระบบซอฟต์แวร์ซึ่งเชื่อมโยงกันอย่างหลวมๆ

และรูปแบบที่เก็บหรือซุ้มก็เป็นวิธีที่ง่ายในการส่งเสริมการมีเพศสัมพันธ์แบบหลวม

อันที่จริง อินเทอร์เฟซโดยทั่วไปมีแนวโน้มที่จะส่งเสริมการมีเพศสัมพันธ์แบบหลวม

ตอนนี้ไม่จำเป็นต้องใช้รูปแบบนี้ทุกที่

กฎทั่วไปที่ดีคือการใช้สิ่งเหล่านี้ในขอบเขตทางสถาปัตยกรรมที่สำคัญ

ในกรณีนี้ ฉันใช้เป็นขอบเขตระหว่างส่วนหน้าและส่วนหลังของแอปพลิเคชันนี้

ในการเริ่มต้นสิ่งต่าง ๆ ให้คลิกขวาที่แพ็คเกจโดเมน ไปที่ไฟล์ kotlin ใหม่ และสร้างไฟล์ที่เรียกว่าความยาก

และนั่นจะเป็นคลาส enum

คลาส enum ใน kotlin และ Java และภาษาอื่นๆ มีประโยชน์สำหรับการสร้างชุดค่าที่จำกัด

ตามที่เราจะเห็นในภายหลัง คุณสามารถใช้คลาสที่ปิดสนิทใน kotlin เพื่อสร้างชุดประเภทที่จำกัดได้

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

อย่างที่เราเห็นในอีกสักครู่ เรามาเพิ่มรายการ enum ของเรากัน

ค่อนข้างชัดเจน enum นี้จะแสดงถึงความยากของปริศนา Sudoku ที่กำหนด

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

ดังนั้นในการเพิ่มค่าให้กับ enum ใน kotlin เราจำเป็นต้องให้คุณสมบัติหรือคุณสมบัติบางอย่างแก่มัน

อย่างที่คุณเห็น ผลงานของเราได้รับการอ่านแล้ว เห็นได้ชัดว่าเราต้องเพิ่มเป็นสองเท่า

และนั่นคือทุกสิ่งที่เราต้องทำที่นี่

คลิกขวาที่แพ็คเกจหลัก ไปที่ New kotlin file for class

และเราจะสร้างคลาสข้อมูลที่เรียกว่าการตั้งค่า

การตั้งค่าเป็นโมเดลข้อมูลแรกของเรา เพราะฉันชอบเรียกมันหรือวัตถุ kotlin แบบเก่าทั่วไป

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

ดังนั้นปริศนาซูโดกุแบบสี่คูณสี่จะมีขอบเขตเป็นสี่ ส่วนเก้าต่อเก้าจะมีขอบเขตเป็นเก้า

คีย์เวิร์ดข้อมูลเมื่อนำหน้าคีย์เวิร์ดของคลาสจะเพิ่มหรือสร้างวิธีการช่วยเหลือสองสามวิธี เช่น เท่ากับแฮชโค้ดหรือคัดลอก

เราจะใช้สำเนาในภายหลังอย่างแน่นอน อาจไม่ใช่ในชั้นเรียนนี้ แต่ในชั้นเรียนนี้และชั้นเรียนอื่นๆ

ในบางจุด เราจะใช้ฟังก์ชันแฮชโค้ดที่สร้างขึ้นด้วย

อย่างไรก็ตาม นี่เป็นคลาสที่เรียบง่ายมาก เราแค่จะเพิ่มคุณสมบัติสองอย่างเข้าไป

แค่นั้นเอง

คลิกขวาที่แพ็คเกจโดเมน ไปที่ New kotlin class or file.

และครั้งนี้ เราจะสร้างคลาสที่เรียกว่าสถิติผู้ใช้

และนั่นก็จะเป็นคลาสข้อมูล

ตอนนี้ จุดประสงค์ของคลาสนี้คือการแสดงเวลาที่ดีที่สุดของผู้ใช้ในการฟันฝ่าอุปสรรคหรือขนาดของปริศนาซูโดกุ

โดยพื้นฐานแล้วเราจะเพิ่มคุณสมบัติทั้งหมดที่ค่อนข้างเหมือนกัน

ตอนนี้ สิ่งหนึ่งที่คุณสามารถทำได้ใน IntelliJ IDEA หรือ Android Studio ก็คือ คุณสามารถคลิกที่นั่นแล้วกด Ctrl D กี่ครั้งก็ได้ที่คุณต้องการ และนั่นจะคัดลอกไปยังบรรทัดใหม่

นั่นเป็นพื้นฐานสำหรับชั้นเรียนนี้

ตอนนี้ คุณอาจสงสัยว่าทำไมเราถึงใช้เวลานานในที่นี้ จริงๆ แล้วเรากำลังจัดเก็บเวลาที่ผู้ใช้ใช้ในการจบเกมหนึ่งๆ ในหน่วยมิลลิวินาที

นั่นคือเหตุผลที่เราต้องการค่าจำนวนเต็มแบบยาว

อีกครั้ง ให้คลิกขวาที่แพ็คเกจโดเมน ไปที่ New kotlin filer class และนี่จะเป็นคลาสข้อมูลชื่อ Sudoku note

นี่คือสิ่งที่เริ่มซับซ้อนและน่าสนใจขึ้นเล็กน้อยด้วยโมเดลข้อมูลของเรา

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

แต่เพื่อให้คุณมีไอเดีย จริงๆ แล้วเรากำลังจะสร้างกราฟสีกำกับ

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

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

เป็นชุดของโหนดและขอบ ซึ่งโดยพื้นฐานแล้วจะเหมือนกับเส้นระหว่างความสัมพันธ์ของโหนดระหว่างโหนด

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

หมายเหตุเหล่านี้จะรวมพิกัด X และ Y ด้วย ดังนั้นด้านบนซ้ายจะเป็น x ศูนย์ y ศูนย์ ด้านล่างขวาจะเป็น x แปด y แปด และเราจะใช้การจัดทำดัชนีแบบอิงศูนย์

ดังนั้นแทนที่จะเริ่มจาก x หนึ่งถึง x เก้า เราก็แค่ลบมันด้วยหนึ่งเท่านั้น

สำหรับคำนำนั้น เรามาเริ่มเขียนโค้ดกันเลย

เราจะเริ่มด้วยค่า x และ y

ต่อไปเราจะเพิ่มสีที่จะเป็นตัวแปรเพราะมันสามารถเปลี่ยนแปลงได้ตลอดการดำเนินการ

จากนั้นเราจะเพิ่มบูลีนที่เรียกว่าอ่านอย่างเดียว และฉันจะอธิบายว่ามันคืออะไรหลังจากที่เราเขียนมัน

จุดประสงค์ของบูลีนแบบอ่านอย่างเดียวที่นี่ค่อนข้างง่าย

เมื่อเราสร้างและแก้ปริศนาซูโดกุโดยพื้นฐานแล้ว ซึ่งเป็นอีกวิธีหนึ่งที่จะบอกว่าเราสร้างปริศนาซูโดกุใหม่ จากนั้นจึงลบเบาะแสจำนวนหนึ่งเพื่อให้เกมเล่นได้และสนุกจริง ๆ

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

อย่างที่เราจะได้เห็นกันในภายหลัง

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

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

เอาล่ะ โค้ดแฮชจะมีการใช้งานเริ่มต้น ซึ่งอิงตามค่าที่ให้ไว้ที่นี่ในคุณสมบัติต่างๆ กัน จริงๆ แล้วเราจะทำอย่างอื่นต่างหาก

เราจะพิมพ์ get hash แบบนั้น

และนี่คือฟังก์ชันที่เราจะสร้างด้วย เราจะเพิ่มพารามิเตอร์สองตัวที่นี่สำหรับ x และ y

ตกลง ตอนนี้เรากำลังจะเพิ่มฟังก์ชัน get hash และมันจะอยู่ในระดับสูงสุด ซึ่งหมายความว่ามันอยู่นอกวงเล็บของคลาสโหนด Sudoku ของเรา

เอาล่ะ เรามาใช้งานฟังก์ชันนี้กัน จากนั้นฉันจะอธิบายสิ่งที่เราทำที่นี่

โอเค ให้ฉันเริ่มด้วยการอธิบายว่าแฮชโค้ดหรือค่าแฮชคืออะไร

ดังนั้นจึงเป็นคีย์ที่สร้างขึ้นหรือตัวระบุเฉพาะบางประเภทตามอัลกอริทึมบางประเภท

ในกรณีนี้ ผมมีอัลกอริธึมที่ง่ายมาก ทั้งหมดที่ผมทำคือคูณค่า x ด้วย 100

และฉันปล่อยให้ค่า y อยู่คนเดียว

โดยพื้นฐานแล้วฉันก็แค่รวมค่าทั้งสองเข้าด้วยกันเป็นจำนวนเต็ม

เหตุผลที่ฉันคูณ x ด้วย 100 คือถ้าฉันไม่ทำอย่างนั้นในปริศนาซูโดกุ 9 ต่อ 9 จะมีบางกรณีที่ถึงแม้ค่า X และ Y จะแตกต่างกันในทางเทคนิค แต่ผลลัพธ์ที่ได้คือรหัสแฮช จะไม่ซ้ำกันสำหรับโหนดต่างๆ หลายโหนด

พูดง่ายๆ ก็คือ ฉันกำลังคูณด้วย 100 เพื่อทำให้แฮชโค้ดไม่ซ้ำกันสำหรับไทล์แต่ละรายการในปริศนาซูโดกุ

สำหรับเหตุผลที่เราใช้แฮชโค้ด โดยทั่วไปแล้ว ฉันจะพยายามทำให้มันตรงไปตรงมา

โดยพื้นฐานแล้ว เราจะจัดเก็บแต่ละโหนดในกราฟในแผนที่แฮชที่เชื่อมโยง

ดังนั้นค่าแฮชจะแสดงคีย์สำหรับแมปแฮชนั้น

แมปแฮชมีคู่ของค่าคีย์ ในกรณีที่คุณไม่ทราบ เราจะเห็นสิ่งนั้นในอีกสักครู่

แต่สิ่งนี้กลับกลายเป็นว่ามีประโยชน์มากเพราะอินเทอร์เฟซผู้ใช้ของเราจะแสดงสิ่งต่าง ๆ ในรูปแบบพิกัด X และ Y นั้นด้วย

ดังนั้นเพียงแค่ใช้คำของฉันสำหรับมัน

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

แต่เราสามารถดึงข้อมูลอ้างอิงโดยรับรหัสแฮชแทน

อ้อ ก่อนที่เราจะไป เราจำเป็นต้องเพิ่มอีกสิ่งหนึ่งที่นี่ เราจะทำให้สิ่งนี้ใช้งานซีเรียลไลซ์ได้

โดยพื้นฐานแล้ว การทำเช่นนี้ทำให้เราสามารถอ่านและเขียนโหนด Sudoku และไขปริศนาทั้งหมดลงในไฟล์ได้

เนื่องจากเราจะเก็บปริศนาไว้ครั้งละหนึ่งตัวเท่านั้น ฉันจึงไม่ต้องการใช้บางอย่างเช่นฐานข้อมูล มันจึงสมเหตุสมผลกว่าที่จะใช้ระบบไฟล์ และทำให้เป็นอนุกรมได้และทำให้ทำได้ง่ายขึ้น เป็นหลัก

โอเค เรามีโมเดลข้อมูลสุดท้ายที่จะสร้างในแพ็คเกจนี้อีกครั้ง คลิกขวา New kotlin file class ของเรา อันนี้จะเรียกว่า Sudoku puzzle

และอีกครั้ง มันจะเป็นคลาสข้อมูล

วิธีที่ดีในการคิดเกี่ยวกับโมเดลข้อมูล อย่างที่ฉันชอบเรียกมันว่าพวกมันเป็นตัวแทนของวัตถุในโลกแห่งความเป็นจริง ในกรณีนี้คือปริศนาซูโดกุ

วิธีที่ฉันออกแบบคลาสนี้ในตอนแรกคือการถามคำถามที่สำคัญเกี่ยวกับสิ่งที่ประกอบเป็นปริศนาซูโดกุ

สิ่งต่างๆ เช่น ขอบเขต มีไพ่สี่ใบต่อแถวหรือคอลัมน์ หรือมีเก้าแผ่น เช่น เรามีปัญหา

และที่สำคัญที่สุด เรามีโครงสร้างข้อมูลแบบกราฟเอง

นอกจากนี้ยังมีเวลาที่ผู้ใช้ใช้เพื่อไขปริศนาตัวต่อ

ไปข้างหน้าและเพิ่มคุณสมบัติเหล่านั้นเข้าไปแล้วฉันจะอธิบายคุณสมบัติบางอย่างที่ต้องอธิบายในภายหลัง

ก่อนที่ฉันจะลืม มาเพิ่มในการใช้งานซีเรียลไลซ์ได้ที่นี่

โอเค คุณคงสงสัยว่าซูโดกุใหม่คืออะไร

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

นอกจากนี้ หากคุณสงสัยว่าเกิดอะไรขึ้นกับแผนที่แฮชที่เชื่อมโยง ซึ่งเต็มไปด้วยรายการที่เชื่อมโยง นั่นเป็นวิธีหนึ่งในการแสดงรายการที่อยู่ติดกัน

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

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

และเราจะใช้ไวยากรณ์นิพจน์เดียว

ผมจะพิมพ์กราฟเท่ากับ

มาสร้างอินเทอร์เฟซใหม่ที่เรียกว่า I game repository กันเถอะ

ฉันชอบใช้หลักการตั้งชื่อนี้โดยใส่ตัวพิมพ์ใหญ่ I ไว้ข้างหน้าอินเทอร์เฟซ

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

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

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

ฉันก็เลยอยากจะใส่มันลงไป

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

แต่ในกรณีนี้ ในแอปพลิเคชันที่ง่ายกว่า โดยทั่วไปแล้ว กรณีใช้งานเป็นคลาสเอง มักจะเป็นชั้นนามธรรมพิเศษที่ไม่จำเป็น

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

และนั่นก็เป็นนามธรรมที่เพียงพอสำหรับการใช้งานขนาดนี้

วิธีการทำงานของอินเทอร์เฟซก็คือมันคล้ายกับคลาสมาก ยกเว้นอย่างที่เราจะได้เห็น ในเวลานี้ เราไม่ได้รับอนุญาตให้ใช้งานจริงหรือให้เนื้อความกับฟังก์ชันที่เราจะเขียน เราแค่เขียน ที่เรียกกันทั่วไปว่า abstract functions หรือ stubs ของฟังก์ชัน

ตอนนี้มีสองประเด็นสำคัญที่นี่

ประการแรก มีการใช้คีย์เวิร์ด suspend เนื่องจากฟังก์ชันเหล่านี้จะถูกเรียกจากขอบเขตรูทีนร่วม ซึ่งมีอยู่ในคลาสลอจิกหรือพรีเซนเตอร์ที่จะอ้างอิงอินเทอร์เฟซเฉพาะนี้

นั่นคือทั้งหมดที่เราต้องทำเพื่อสร้างการทำงานพร้อมกันในตอนนี้

ในกรณีที่คุณไม่คุ้นเคย สิ่งที่ฉันใช้ที่นี่คือสิ่งที่เรียกว่าประเภทฟังก์ชัน

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

จากนั้นในการใช้งานที่เก็บจะใช้งานสิ่งต่าง ๆ ที่ใช้อินเทอร์เฟซเฉพาะ นั่นคือวิธีที่พวกเขาจะโทรกลับพร้อมผลลัพธ์บางประเภท ไม่ว่าจะเป็นผลลัพธ์ที่สำเร็จหรือล้มเหลว

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

ดังนั้น นี่จึงเทียบเท่ากับการส่งต่อเป็นโมฆะหรือโดยพื้นฐานแล้วไม่มีอะไรในฟังก์ชันนี้โดยเฉพาะ แล้วไม่ส่งคืนสิ่งใดจากฟังก์ชันนั้น

แต่สิ่งที่จะทำคือมันจะส่งสัญญาณบางอย่างเช่น oncomplete หากคุณมีประสบการณ์กับ RX Java โดยพื้นฐานแล้วเราต้องการให้แอปพลิเคชันทำงานต่อได้สำเร็จเมื่อเรียกใช้ฟังก์ชันนี้

แต่ฟังก์ชันพิเศษนี้ไม่จำเป็นต้องส่งคืนอะไรเลย

ต่อไป เราจะเห็นตัวอย่างเมื่อเราต้องการคืนค่าจริงผ่านประเภทฟังก์ชัน on Success

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

สิ่งที่ฉันทำอยู่นี้คือ ฉันกำลังสร้างฟังก์ชันพิเศษขึ้นมา โดยขึ้นอยู่กับสิ่งที่เราต้องการบรรลุจากมุมมองของผู้ใช้

ตอนนี้ผมจะสาธิตให้เห็นเมื่อเราต้องการคืนค่าจากประเภทฟังก์ชันเฉพาะเหล่านี้จริง ๆ

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

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

และในกรณีนั้น สมมติว่าปริศนาถูกต้อง และผู้ใช้ได้ป้อน กระเบื้องสุดท้าย ที่จะส่งสัญญาณว่ากำไรเสร็จสมบูรณ์

ตรงนี้อาจจะดูงงๆ หน่อย

แต่การประสบความสำเร็จไม่ได้แปลว่าตัวเกมได้เสร็จสิ้นไปแล้วเสมอไป

นั่นคือเหตุผลที่ฉันสร้างความแตกต่างที่นั่น

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

และจริงๆ แล้วมีกรณีที่ผู้ใช้เล่นเกมจนจบโดยออกจากแอปพลิเคชัน แล้วรีสตาร์ทแอปพลิเคชัน

นั่นคือเหตุผลที่เรายังคงส่งผ่านธงที่สมบูรณ์

ที่นี่เราจะกลับมาอย่างชัดเจน เป็นวัตถุการตั้งค่า

มาสร้างอินเทอร์เฟซอื่นกันเถอะ

และเรียกอีกอย่างว่า ฉันได้รับ พื้นที่เก็บข้อมูล

และนั่นเป็นอินเทอร์เฟซอย่างชัดเจน

ก่อนที่เราจะเขียนอินเทอร์เฟซ เราจะทำบางอย่างที่แตกต่างออกไปเล็กน้อย

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

คลาส Seal เป็นหนึ่งในคุณสมบัติง่ายๆ ที่ฉันโปรดปรานของภาษาการเขียนโปรแกรม kotlin

ทำให้เราสามารถสร้างได้ในชุดประเภทที่จำกัด และประเภทเหล่านั้นสามารถมีค่าเฉพาะได้

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

และวัตถุเฉพาะนี้สามารถแสดงสถานะต่างๆ ได้หลายสถานะ

อย่างที่ฉันได้แสดงให้คุณเห็นก่อนหน้านี้ในที่เก็บเกม AI ในที่นี้ เรากำลังแสดงสถานะข้อผิดพลาดและสถานะความสำเร็จเป็นการอ้างอิงฟังก์ชันสองส่วนแยกกัน

ในตัวอย่างนี้ เราจะแสดงสถานะทั้งสองผ่านวัตถุเดียว

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

แต่เราไม่ได้ทำอย่างนั้นในแอปพลิเคชันนี้

ดังนั้นคุณไม่จำเป็นต้องเพิ่มเข้าไป

มาปิดอินเทอร์เฟซให้เสร็จกันเถอะ

สร้างอินเทอร์เฟซอื่นที่เรียกว่า I การตั้งค่าที่เก็บข้อมูล

ตอนนี้ เราจะใช้ตัวตัดผลลัพธ์ที่นี่ด้วย

ที่จริงผมจะแค่คัดลอกและวางสิ่งนี้ลงไป

และเรากำลังจะเปลี่ยนชื่อบางอย่าง

เอาล่ะ และตอนนี้เราก็สามารถเขียนอินเทอร์เฟซได้แล้ว

อีกหนึ่งอินเทอร์เฟซสำหรับแพ็คเกจนี้

อันนี้จะเรียกว่าสถิติ pi ถ้าฉันสามารถสะกดที่เก็บได้ และแน่นอนว่ามันจะเป็นอินเทอร์เฟซ

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

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

ในส่วนนี้ของบทช่วยสอน เราจะเรียนรู้เกี่ยวกับฟีเจอร์ภาษา kotlin ต่างๆ มากมาย ซึ่งออกแบบมาเพื่อแชร์โค้ดอย่างชาญฉลาดและมีประสิทธิภาพ

หัวข้อที่ครอบคลุมรวมถึงฟังก์ชันส่วนขยายและปัญหา คลาสนามธรรมเหล่านี้ หลักการปิดแบบเปิด ออบเจกต์ Singleton และตัวมอบหมายงานประจำร่วม

ก่อนที่เราจะเขียนโค้ดเรามาพูดถึงหลักการ open Closed กันก่อน

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

เอนทิตีซอฟต์แวร์ที่ใช้ซ้ำทั่วไปซึ่งคาดว่าจะเปลี่ยนแปลงควรมีอินเทอร์เฟซสาธารณะคงที่และวิธีเปลี่ยนการใช้งาน

เพื่อให้คำจำกัดความนั้นสมเหตุสมผล มีสองสิ่งที่ฉันจำเป็นต้องแกะ

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

ลองมาตรวจสอบกันว่าฉันหมายถึงอะไรโดยอินเทอร์เฟซสาธารณะ และเหตุใดฉันจึงโต้แย้งว่าควรแก้ไขหากใช้ซ้ำโดยทั่วไป

โดยอินเทอร์เฟซสาธารณะ ฉันไม่ได้พูดถึงอินเทอร์เฟซ Java หรือ kotlin โดยเฉพาะ แต่ฉันหมายถึงแง่มุมที่เปิดเผยต่อสาธารณะของคลาสหรือฟังก์ชัน

เนื่องจากนี่คือบทช่วยสอนของ Android เรามาดูตัวอย่างคลาสกิจกรรมกันเถอะ

กิจกรรมเหมาะกับความต้องการของฉันในการนำกลับมาใช้ใหม่โดยทั่วไปและคาดว่าจะมีการเปลี่ยนแปลงเมื่อเวลาผ่านไป

ดังนั้นจึงเป็นกรณีที่สมบูรณ์แบบที่จะนึกถึง OCP

ทุกคลาสย่อยของกิจกรรมต้องมีฟังก์ชัน onCreate ซึ่งเป็นส่วนหนึ่งของส่วนต่อประสานสาธารณะ ซึ่งทำให้คลาสที่อ้างอิงสามารถใช้งานได้

เหตุผลที่เราไม่ต้องการให้อินเทอร์เฟซสาธารณะนี้เปลี่ยนแปลงนั้นง่ายมาก

สมมติว่านักพัฒนาแพลตฟอร์ม Android ตัดสินใจเลิกใช้และนำอินสแตนซ์ที่บันทึกไว้ออกโดยกะทันหัน ระบุบันเดิลจากฟังก์ชันวงจรชีวิตทั้งหมด

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

สิ่งที่ฉันหมายถึงโดยเฉพาะคือคลาสย่อยของกิจกรรมทั้งหมดในฐานโค้ดทั้งหมด ซึ่งไม่ได้ลบพารามิเตอร์นี้ จะไม่สามารถคอมไพล์ได้

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

เนื่องจากเราได้พิสูจน์แล้วว่าเหตุใดอินเทอร์เฟซสาธารณะแบบคงที่จึงมีความสำคัญมาก

คำถามต่อไปเป็นเรื่องง่าย

แล้วเราจะให้กลไกหรือวิธีการสำหรับการนำส่วนต่อประสานสาธารณะไปใช้งานเปลี่ยนแปลงได้อย่างไร? ปรากฎว่า kotlin มีตัวเลือกมากมายให้คุณแก้ปัญหานี้

แทนที่จะอธิบายทั้งหมดด้วยวาจา ฉันจะสอนวิธีใช้พวกเขาในโค้ด

ขณะที่เราสร้างแอปพลิเคชันนี้ ให้คลิกขวาที่แพ็คเกจทั่วไป แล้วไปที่ New kotlin file or class

และนี่จะเป็นคลาสนามธรรม ซึ่งพวกเขาไม่มีตัวเลือกสำหรับที่นี่

สิ่งที่เราจะทำคือพิมพ์ตรรกะพื้นฐาน เราจะเพิ่มคำสำคัญที่เป็นนามธรรมลงไป

น่าเสียดายที่ฉันไม่มีเวลาอธิบายความแตกต่างระหว่างคลาสนามธรรมและอินเทอร์เฟซในการสืบทอดแบบเก่า

ในหลักสูตรเฉพาะนี้ นี่คือสิ่งที่ฉันจะอธิบายและอธิบายอย่างชัดเจนในหลักสูตรวิดีโออื่นๆ ของฉัน

แต่สิ่งที่ฉันจะทำคืออธิบายว่าเหตุใดเราจึงใช้คลาสนามธรรมที่นี่แทนอินเทอร์เฟซ

เหตุผลที่เราต้องการใช้คลาสนามธรรมก็เนื่องมาจากสถานการณ์เมื่อเราต้องการแบ่งปันพฤติกรรม

ตัวอย่างเช่น เราจะเขียนฟังก์ชัน stub หรือ abstract function ซึ่งฉันต้องการแชร์ในคลาสใดๆ ซึ่งสืบทอดมาจากตรรกะพื้นฐาน

และฉันต้องการแชร์ตัวแปรด้วย แต่ตัวแปรนี้จะต้องได้รับการปกป้องแทนที่จะเปิดเผยต่อสาธารณะ

และถ้าเราจะลองทำสิ่งนี้โดยใช้อินเทอร์เฟซ ค่านั้นจำเป็นต้องเปิดเผยต่อสาธารณะ เราจะใช้ประเภททั่วไปด้วย

ฉันจะแสดงให้คุณเห็นว่าต้องทำอย่างไร

ไวยากรณ์สำหรับประเภททั่วไปคือการใช้วงเล็บเหลี่ยม

And then you could take quite literally whatever you wanted between those angle brackets.

But my suggestion to you is to not use something which is already used, hence why I'm using this all capitals event.

Now, if it doesn't make sense what we're doing here, it will make more sense when we write the classes which inherit from base logic.

Let's go ahead and finish this off.

To briefly explain the intent of this abstract class.

Basically, I'm saying that I want a set of classes, the ones which will inherit from base logic, all of which will have this function on event.

In other words, these classes will handle events from the user interface.

And then as we'll see, we're going to use this job object which comes from the coroutines API as a way to Cancel child co routines.

And also to make each of these logic classes as its own co routine scope.

I'll explain that when we get to that particular part of the tutorial, right click on the common package and create a new kotlin file, which is just going to be a plain old file, and it's going to be called extensions.

gotlands extension functions and extension properties are among my favorite features of the language as a whole.

Without getting too technical here, extensions allow you to employ the open closed principle, which states that software entities should be open for extension, but closed for modification.

If that doesn't make sense, don't worry about it is kind of a confusing definition.

But it allows us to add new functionality to existing source code without having to modify the original source code.

Now, this particular file extensions.kt is kind of like a replacement for static utilities that we might have used in Java or something like that.

It's really just a place where you stick utility code which is used across the application.

Let's write our first extension function to see how this works.

The purpose of this particular extension function, obviously it will be used within activities is really just syntactic sugar, its way to make it so that I don't have to type out toast dot make text and supply this message toast dot length long and dot show.

Instead, in the activity where we'll be using activities, I should say where we'll be using this particular extension function, we can just type make toast, give it whatever string we want to display, and it's handled like that.

By making it an extension function of the activity class, I can use it seamlessly in any activity.

Let's write another much uglier utility extension function.

The purpose of this ugly little function here is to take the elapsed time of the given puzzle which the user is currently working on, and to attempt to convert it into a value based on minutes and seconds or a string to display based on minutes and seconds.

Now, if it takes the user longer than an hour, then we end up just displaying like a generic more than 5959.

Now if you think this code is ugly, in kotlin, I challenge you to write it in Java.

Now for beginners, this might not make sense intuitively, but it's important to understand what this is referring to.

This is actually referring to the long object, which we will be calling dot two time on.

That might make a little bit more sense when we actually get to using this particular extension function.

There's only one more extension, we need to add, and it's actually going to be an extension property this time.

So what I'm doing here is I'm hitting alt, enter on this particular red thing, and then I'm going to hit Add remaining branches.

going to hit alt enter, again, to import our, these are obviously string resources.

That's one thing, we're not going to be writing by hand.

So hopefully, what you've done is you've gone and grabbed the source code for the starting point, which includes things like string resources, right click on the common package again, and we're going to create a new kotlin interface, which is going to be called dispatcher provider.

This interface is very small, what we'll do is we'll write the code and then I'll briefly explain what it does.

Now, unfortunately, I can't briefly explain what a co routine context is.

But I can't explain the purpose of this particular class and how we're going to be using these co routine contexts.

So in most situations, most of the work that we're going to be doing within co routines land is going to take place on the main thread or the UI thread.

Now, with that being said, there are a few operations like writing to a file, which we don't actually want to occur on the main thread.

And that would be a situation where we're going to provide the IO context.

Now, the actual purpose of this particular interface is really key here.

What we're going to be doing is that if we wanted to hypothetically test any class, which needs to use these co routine contexts, in a JVM environment, so not an actual running application, then what we could do is we could return a particular kind of CO routine context, which allows us to test in that particular environment.

I know that's a lot of technical detail, but I can't really make it a whole lot simpler than that.

However, by using this interface here, when we want to use our co routines in the production environment, we can provide the real UI main thread context for the front end, and then we can provide a real dispatcher for the IO thread.

To make that even simpler, we're really just making the code easier to test.

Right click on the common package, go to New kotlin file or class, this time, it's going to be an object.

And hopefully I can spell this right production dispatcher provider.

Again, what we'll do is we'll write the code here and then I'll explain how it works afterwards.

I'm going to hit alt Enter again.

And this is where we will return the actual dispatchers that we'll be using in production as per the name of this particular object.

Now there's a number of reasons why I'm using the object keyword here.

So basically objects in kotlin are in this particular case Singleton's.

So that basically means that we will only ever have one of these production dispatcher, a provider software thingies floating around in memory space at one particular time.

They're also thread safe, which is important because although co routine is not necessarily a thread, our dispatchers dot main and dispatchers.io has something to do with threading.

And the other thing that an object can do is it can actually inherit from an interface.

Now we're not actually going to be writing any unit tests in this particular application, which require the dispatchers but just to show you What you would do if you wanted to unit test some class which needs to use these co routine context, what you can do is you can just instead return dispatchers dot unconfined, and then you would return that for both the IO context and the UI context.

And then that is what you would use in like a JVM j unit test environment.

The persistence package contains classes and functions, which have the role of persisting or storing data beyond the lifecycle of an Android process.

If you don't know what a process is, it simply means a program which is running on a device.

Practically speaking, we will store the progress which the user has made in the current Sudoku game, as well as the settings for that game, and the user's personal records or statistics, as I call them.

Here's a quick look at the architecture of the persistence package.

The game repository in this situation functions as a back end decision maker for the two data sources, which coordinates the data sources themselves.

Just try to carry out CRUD operations, create, read, update, delete, and either report with a success or a failure if an exception is thrown.

The general principle here is to keep things together, which makes sense to be kept together to separate what doesn't need to be kept together, and to also use an abstraction or an interface.

In any place where the implementation might change, I might decide to stop using the local file storage or proto data store.

So hiding these details from the repository is not over engineering, but rather a calculated decision.

Speaking of data sources or storage mechanisms, we will use two different mechanisms for storing our data.

Firstly, we will store the user's preferred Game Settings and their personal statistics in protro data store data store provides a lightweight and efficient way to store this kind of data using protocol buffers.

Protocol Buffers is a serialization language similar to JSON.

However, I find it easier to read than JSON.

And fortunately, the library we will use also comes with its own protobuf compiler that will generate some of the boilerplate code which we would otherwise need to write ourselves.

We also use the device's file storage to store the progress of the user in the currently active game.

Every Android app is given some memory space to store files, which is what we will use.

This is done by making all of the domain models implement serializable.

And using Java as input and output streams to read and write objects from kotlin language.

So in case you aren't following along with the tutorial, and you haven't downloaded the starting point repository, what you're going to want to do is you're going to want to add a directory called pro tau in the main source set, the starting point repository should already have that directory.

So just go ahead and right click on it, and go to new file.

And this file is going to be called gain underscore settings, dot proto, and make sure it's all lowercase.

Go ahead and type this in the top of the file.

So protocol buffers are essentially like a serialization language.

It's very similar to JSON.

If you want to look more into it, you can about what the benefits and the pros and cons of using something like JSON.

But personally, this being the only project that I've used Protocol Buffers in so far, I'm quite happy with it.

Okay, so let's just add two more lines.

And I'll explain some more from there.

Okay, so we'll talk a little bit more about this in a moment.

But basically, what's going to happen here is, we're going to define this protocol buffer message, as it's called, which is kind of like a data type for lack of a better term.

And what we can do is, so this file will be consumed by something called the protocol buffer compiler.

And in this case, what we're basically telling it is that we're going to be generating Java files.

Now in the generated class files.

The protocol buffer compiler is going to basically add whatever we put in the Java package as the package for the generated Java class file.

It's just useful to not mix up your namespaces and stuff like that.

And as for the second option, here, Java, multiple files.

If you don't have that turned on, then what can happen is that basically, the generated Java files will all be in one single file.

We don't really want that, although I'm not sure if it's absolutely integral to getting this application to work.

Like I say, we're going to go through this pretty practically and I'm not an expert in protocol buffers.

Okay, now, we're going to do Line a message which is kind of like one of the main data types for lack of a better term in this particular language.

Okay, so let's talk about what we just did here.

So we've defined a message, which in Protocol Buffers is kind of like a data type or a collection of fields.

And we've done two things.

So within the game settings message, we have a 32 bit integers, like a kind of a smaller integer to represent the boundary of a Sudoku puzzle.

So when I say boundary, I mean like a four by four Sudoku puzzle will have a boundary of four, a nine by nine Sudoku puzzle will have a boundary of nine, obviously.

And the other thing we did here is we defined an enum in protocol buffers.

Now when you're creating these enums, you'll need like a default value unknown.

And then you've got the other values that the enum can potentially be.

Also notice how in boundary and difficulty the fields above the enum I'm giving it default values, naturally, those will be like the values that the protocol buffer gets pre loaded with, like the first time you access it.

Now, the important thing to understand here is that assuming you've added the support for Protocol Buffers into your build Gradle configuration, the proto buffer compiler is going to actually generate some Java files or classes out of this particular message.

Okay, so what I'm doing here is I've opened up the completed project, and I'm just having a look at the file which was generated by the protocol buffer compiler.

And all I really want you to notice here is that when you're using proto data store, what's going to happen is it's actually going to generate a Java class for you.

Obviously, you can see we have our gain settings in camel case, which is what we defined as our message.

And then we also have that enum defined below.

So what does this actually do for us, basically, this is going to allow us to serialize or basically translate from Java into the protocol buffer language and vice versa.

And it also means that we don't actually have to create our own plain old Java object in order to do that.

The library is going to generate that for us.

But we can still use it in our code, which we'll do in a moment, we're going to add one more proto file.

So go ahead and open up the protobuf directory, right click again, go to file.

And this one's going to be called user statistics dot Proto.

Alright, so I've just copied and pasted the first three lines from the other protocol because we'll be reusing them.

And we are going to create another message here.

Now when I say statistics, this is kind of like my way of talking about the user's personal records.

So what are the shortest times to completion that a user has made in solving a particular size and difficulty in a particular Sudoku puzzle? It's pretty straightforward.

So let's just write it out.

And there you have it.

Now, you might be wondering why I'm using 64 bit integers here.

So these actual values are going to be stored in milliseconds, which is why I do want the 64 bit integer storage there instead of the 32 bit integer.

I'm not actually 100% sure if that's necessary, but I did that just to be safe, and realistically, it's not really going to eat up that much extra memory.

Okay, so that's it for our protocol buffer files.

Now, we're going to have to create some protocol buffer data stores, which is how we're actually going to create and access our protocol buffers.

Go ahead and right click on the persistence package, go to New kotlin file or class and this is just going to be a file called data stores.

Okay, so before proceeding, you're going to want to go to build and make project.

Now the build will probably fail, but all we really wanted to do is to generate the appropriate Java class out of the protocol buffer.

But if for some reason that doesn't work for you, just follow along, and eventually it will work.

Okay, so for each protocol buffer based data source, we're going to need to provide a way to get ahold of it or create it from context, then the other thing we'll need is a serializer.

Go ahead and import everything.

And there's two things we need to add into the delegate here.

Okay, so don't worry that it's showing up red will actually write this serializer.

Next, so I just wanted to explain what's going on here.

So we're creating a data store object, and it takes the protocol buffer generated Java class, which is called Game Settings.

And essentially, what this does is it creates a reference which which we can use to either store or retrieve our protocol buffer.

Now, you might be wondering what game underscore setting.pb is, and why it has a different file extension than our proto files, to the best of my understanding game underscore settings.

PB is something that's generated after the fact by the compiler, whereas the profile is something we write for the compiler to consume.

But in case I'm wrong on that, then feel free to flame me on Twitter.

The other thing we'll need is a serializer, which takes care of serialization quite obviously.

After that, you can just click here, hit alt insert, override methods, and we only need the methods from the serializer interface.

So again, let's read the code and then I'll explain what I need to explain after the fact.

Okay, so I'm going to keep the details here pretty light.

So obviously, when we create our data store, it's given the game setting serializer here.

And what the serializer does is it helps us to read and write from input streams.

So in other words, we're going to be obviously reading from a protocol buffer file, and then that's going to be serialized, or rather D serialized into Java, and vice versa.

So basically, what the Android team has done for us here is they've made it a lot easier to handle things like error handling and dealing with input streams.

Because if you've ever worked with input streams in Java, then you can tell there's, you know, you're probably familiar with a lot of boilerplate code to do with that.

So basically, we do a little bit of boilerplate work here.

And it translates to a very simple API, when we actually want to read and write with this particular tool in the back end, which we'll be doing in a moment.

Okay, now, obviously, we need to write another data store and also serializer for the other data type.

So this is going to be one of those rare scenarios where I do actually just copy and paste because there's absolutely nothing new, we're just going to change a couple of the words.

So this would be one of the points where I encourage you to have the complete source code open on the side and then that way, you can do a little bit of copy paste action, like I'm going to do now.

And that is our data stores file complete.

Now obviously, if you had a whole bunch of these, you'd probably want to use separate files, but since I only have Have the two I just decided to stick them in the same file, right click on the persistence package and go to New kotlin class.

This one's going to be called local game storage ample.

So firstly, we're going to make a constant which will represent the name of the text file that we will be reading and writing the game data to.

Next, we'll create the constructor.

So you might be wondering where a file storage directory comes from.

When we create the build logic of this application, which is kind of like my inversion of control dependency injection type stuff, what's going to happen is we're going to call this one function to the Android system, which will return us the specific directory from the system where we can read and write things like files.

Let's go ahead and implement the interface.

Now, I'm going to try to get through this relatively quickly.

But one thing I want to explain is that you'll notice I'm making fairly extensive usage of helper functions.

The reason for that is just to avoid writing redundant code.

Also, as with the other implementations, we're going to be using the width context co routine builder to do this kind of IO work off of the main thread.

So what we'll do is we'll call a helper function called update game data, and we'll pass it in the game data.

And if that operation happens to be successful, then we'll actually just return the same game object that was passed in because it should be consistent.

Okay, now we can create the helper.

So here, we're going to throw the exception so that it'll actually get picked up by the catch block in the functions that we'll be calling this helper.

Now, we're going to be using input and output streams, which are part of the Java standard library in order to rate our data to and from the file.

If you're wondering kind of what this word stream means, ultimately, what we're actually doing kind of at the low level, is we're going to take our game or Sudoku puzzle object, and we're going to serialize it into basically a stream or a very long sequence of textual characters.

And that's what we'll actually be reading and writing from the file.

Okay, so two points, you always want to close your streams.

Also, you might be wondering, how is it that we can say dot write object and pass in our Sudoku puzzle, but let's just check the parameters here.

So I'm going to hit Ctrl p within the parameter brackets, and as you can see, it accepts any type.

Now the important thing is that if our different classes like Sudoku puzzle and Sudoku node did not extend serializable than we wouldn't be able to do this without errors.

So for update node, it's a little bit different, we're just updating one individual node.

So how this is going to work is we're going to get the old data and then we're just going to update that individual node.

And then we will rewrite the result back to storage.

So get game will be another helper, we write, and what I'm going to do is I'm actually going to write that one right away.

Otherwise, the autocomplete and error handling stuff will be all over the place.

Okay, that's what we need to do there.

Now, just a quick reminder here, when we say color, and really, whenever anyone talks about a color in a graph data structure, they're really just talking about a number.

So in this case, the number represents the actual value placed in a particular Sudoku square.

So it'll be like something from one through nine, or one through four, depending on the boundary of the Sudoku will also update the elapsed time.

After it's updated, we will write that result to storage hopefully.

And just to keep the front end synchronized with everything else, then we will return that same game object.

Now it has just come to my attention that I have forgotten to add a particular integer called color to this particular function when I wrote it, so let's just go ahead and fix that now.

There we go.

And I managed to save the easiest for last.

And that's it for this file.

Right click on the persistence package, go to New kotlin class, this one's going to be called game repository info.

So in case you jumped ahead, and you aren't actually familiar with the repository pattern, I actually already explained that in part two of this series where I built the domain package.

In any case, let me just reiterate, reiterate what the purpose of this particular classes, it's basically like a bridge and decision maker for the backend.

Now sometimes you'll have multiple different repositories or datasets.

In the back end, and it might be a good idea to keep them separate.

The reason why I didn't in this particular case is because the game storage and the settings storage are actually inextricably linked.

They are by nature closely related.

So based on that, and the fact that this isn't actually a very large application, I chose to put them together within this repository.

And then how it will work is that the repository will coordinate these two different data sources.

Let's start with the constructor and the repository interface.

Okay, so as you can see, we have our work cut out for us.

So what I'm going to do is I'm going to try to write the code relatively quickly.

And after it's written, I'll explain what it does.

So there shouldn't be anything new in this particular function, except for the fact that we're making an assignment statement within a control statement, Val current game result equals etc.

We're allowed to do that because kotlin is a beautiful and idiomatic language.

This one's actually pretty simple.

You know, for the life of me, I don't understand why it keeps putting on air on top.

I'll explain this function in a moment.

So puzzle is complete is actually a function which exists in the computation logic package, which we'll be writing later on, of course, and all it does is exactly what it says.

But it will return either a true or a false based on whether the puzzle is complete or not.

Hence is complete.

Okay, so what I've done here is I've copied and pasted in the plain language use case which describes this particular function.

Now, as you can see, it's pretty complicated to give a basic explanation of what's going on.

And why did this when we request the current game, ie when the application starts up, there's a number of different things that could happen.

So for starters, the user could have a currently active game and they just want to retrieve it.

It could be the first run of the application, so no game currently exists in storage.

And then there are different situations where errors could occur along the way.

This is something that happens when you're coordinating multiple different data sources.

Now I have my own system of tracking these different event streams, I use basically letters and numbers to denote steps and different potential event streams.

But whatever you do, my suggestion to you is to write this down in plain language first and then go ahead writing the code.

That's what I did this comment above you see here, I wrote that before I wrote the code.

Anyways, let's get started.

Okay, so for our first Event Stream, we attempt to retrieve the current game, and that returned successfully.

And then we also want to know whether the current game is complete or not.

We can just get rid of oncomplete.

And here we go again.

So this is obviously the case where the user has first loaded the application and we want to create a brand new game.

And Looks like I'll have to do this manually this time.

The autocomplete is not helping me here.

But in fairness, we haven't written that function yet.

Okay, I'm just gonna double check that I wrote that correctly.

Now, before I want to move on, I want to explain one thing about my perspective on software architecture.

While sometimes in a simpler application, we can do something like have the presenter coordinate different repositories or back end data sources.

In this particular case, there was enough complicated back end logic that I wanted to have also a decision maker class, which happened to be this game repository imple on the back end, and part of the purpose of this class is to take care of the logic of coordinating these different back end data sources, so that I can keep the presentation logic class doing what it's supposed to do, managing presentation logic, and then I have this class dealing with this messy kind of almost business logic type stuff here.

Anyways, we're not done yet.

Okay, so it just occurred to me that I have missed a function in the interface of a game repository.

So let's just go ahead and add that in.

So what I'm going to do is I'm just going to copy update game, paste it down below.

And what we're going to call this is create new game.

And it's going to take in a settings object and that's it.

So that's actually a helper function that I created mostly for legibility, let's just go ahead and add that in right now.

Just another quick note here, you'll notice that I like incredibly long and descriptive names of everything that's going on.

This is largely because I don't have a great memory for fine details.

So by making these things super long and descriptive, I don't actually have to remember them, I can just read my code and pretty much understand what it does.

Even in these complicated situations where we have all these different event streams and interactions occurring, okay, only two more short functions to go.

And that's it for our back end.

In the top level of the UI package, we have four small files, which we will use to create and apply styles, colors, fonts, and so on.

One of those files is the global theme for our application.

And I will show you how to create both a light and dark theme for the app in only a few extra lines of code.

Stay tuned for the end of this section, as I will do a live demo of the different themes.

Right click on the UI package and create a new kotlin file, which is going to be called color dot Katie.

This file will essentially be a replacement for colors dot XML, if you're used to working with the old resources system, which was based in XML, let's create a color object.

Make sure you import the Compose color class.

Okay, so before we proceed, the most important thing to understand here is how to read these particular values.

So the first two characters here 0x.

This basically tells the compiler, which is the program that will be reading this code that this is in fact a hexadecimal number.

The second two digits here indicates the alpha value as a percentage.

Alpha is another way of saying transparency or how opaque something is.

The remaining three pairs are the red, blue, and green or RGB values, again in a hexadecimal percentage, and that's pretty much all there is to know about these different color values.

I've copied and pasted over the rest of the values because there's absolutely no point in either was typing all this out.

But also keep in mind that they have some predefined values such as flack, for example, which you can also make use of right click on the UI package, and we're going to create another new kotlin file.

And this one's going to be called shape.

So in the old view system, when you wanted to do something like creating a background with rounded corners, or a button or widget or something like that, you had to create usually something inside of the drawable folder, which was XML based.

Again, since this is compose, we can just go ahead and do that in kotlin.

Instead, hey, I will just use some default parameters.

Now, this might be your first time seeing the.dp extension, let's just take a quick look at the source code.

So as you can see, you can basically just append it to an integer double and various kinds of numbers.

The important thing to understand here is that this basically tells the Compose framework that we want to use density independent pixels.

If you want a more profound explanation of what exactly those are, I strongly suggest you look into it because it's a little bit complicated.

Suffice it to say that the idea here is to allow the framework to create values for heights and widths and things like that would work across a variety of different screen sizes and form factors.

Right click on the UI package, and we're going to create another kotlin file, this one is going to be called type.

Now in case you're wondering, when we say type, we're not really talking about a type system, or anything to do with type theory, it has to do with type Pog, Rafi or text and how this text is styled or presented.

So again, this is very much the kind of thing that we used to do and styles dot XML, we're basically going to create a bunch of different text styles, which will use throughout the application.

And then we'll kind of see how to wrap those up in a typography object.

And then we'll see how to add that typography object to our sort of global compose theme.

First, let's create a text style.

Sometimes we have a situation where we want to keep a bunch of default values, but we might want one or two values, which are actually passed in as a parameter to create the text style object.

So I'll show you another way to create these text styles using function.

Just gonna do some quick copy paste here.

And then we can override the color.

So again, what I'm going to do for the rest of these textiles now that we've seen everything there is to see here is I'm going to copy and paste them over.

But there is one more thing that's new that we need to create in this particular file.

Okay, as you can see, we've got a couple different textiles here.

So the last thing we need to do is create a typography object.

So basically, what that's going to mean is that we're going to assign some of the text styles that we've created below, which are used in common things like the body text of a particular feature of the application, buttons, titles, that kind of thing.

If that doesn't make sense.

Let's just write the code.

Make sure you select compose dot material, not kotlin dot txt.

Okay, we're just gonna do To more.

All right, and the only other thing we need to do is set up our graph Sudoku theme.

Right click on the UI package, and we've got, you guessed it another kotlin file.

And it's going to be called graph Sudoku themed.

So one of the handy little features of jetpack compose is that it is incredibly easy to create a theme for light and dark modes.

As someone who uses generally speaking, dark mode almost always actually really appreciate this particular feature of compose.

The first step in that process is to create two different color palettes.

Let's start with the light color palette.

So some of these properties should probably be familiar to most Android developers like having a color primary.

That's how we used to do it also in the old XML system with colors, or at least that was a common naming convention.

Now, one thing I want to point out here is that there's a degree to which some of these more obscure ones like primary variant surface on primary and so forth, I'm really just using those because it's convenient, they don't necessarily have to mean anything in particular.

But the important thing to understand here is that if there's any different color between a light theme and a dark theme, we do want to define it somewhere in here, and then use it appropriate in the composable, which we'll be learning to do later on.

Okay, that was actually supposed to be uppercase there by convention.

And also notice that I've copy pasted over the dark color palette, because again, there's nothing new going on there.

The next step, however, is very important, we're going to create our theme, and it's actually going to be really, really easy.

Here's a little shortcut I learned from a friend, if you want to create a composable function really quickly start typing comp, and then hit enter, just saves you a little bit of time.

Now this theme is going to have two parameters here.

So before we write the body of this function, I just wanted to discuss these two parameters.

So as you can see, we're actually making a function call is system in dark theme, what's going to happen is this system call will return a Boolean, which will tell us whether the user has specified if the app is supposed to be in dark mode or light mode.

And then the content represents everything that will be wrapped inside of this theme.

What's important to understand here is that everything that we put inside of this composable, ie the content will have access to all these different colors, styles and typography information from within the theme itself.

The actual utility of this will make a lot more sense when we actually write the composable.

Just to finish things off, we're going to create a material theme composable.

And we won't need the lambda expression.

So there you have it, it only took a few minutes to create like the color resources and styles and typography information necessary to render both a dark color palette and a light color palette for different modes.

What I'm going to do is show you a quick demo of what this actually looks like in an application.

Here I'm going to be starting the application in the light theme.

Then I'm going to navigate to the operating system settings and set it to a preferred dark mode.

And upon returning we see immediately that the application now is using the dark theme.

We're now ready to start building our user interface.

The UI components package contains reusable elements of the user interface.

Since this is a very small app, the only two such elements are a toolbar, and a loading screen.

One of the great features of compose is that we can make our components reusable in different ways.

Firstly, if a component needs to be positioned according to where it fits in different parent composable, or parent screens, we can pass in a modifier instead of creating a modifier within the child composable.

This is worth experimenting with in case you haven't already.

Secondly, it is possible to pass in composable as arguments, which also allows reuse and extension of functionality.

In this app, we want different toolbar icons for the two different UI screens.

And we can achieve this by passing in the icon compostables.

From those parent UI screens, you'll see later on how we can specify and handle different icons and different click events.

Using the same toolbar will also create this reusable loading screen and later, I will show you how to animate it, right click on the UI package and go to new package.

And this one's going to be called components.

Just a brief explanation here, I've adopted this particular convention from the composed samples repository.

So what will go into this particular folder are composable, which will end up being reusable across a variety of different UI elements and different screens.

In this case, we're going to be creating a reusable toolbar, and also a reusable loading screen, right click on the components folder, and go to New kotlin file, and this one's going to be called app toolbar.

Let's create our functions stub, what I'm going to do is I'm going to type co MP and then the autocomplete will create a composable function.

This one's going to be called app toolbar.

First, let's write the parameter list and I'll explain it a little bit.

Make sure you select the compose.ui modifier.

Let's start by talking a little bit about modifiers.

So modifiers are basically how you can create most of these styles size and position kind of data for a particular composable.

Now there's kind of two different main ways to do this.

We could of course, create this modifier and use it within this widget.

But that would be for a situation when the widget itself is going to be deciding that kind of information.

Since we're using a reusable component here, an app toolbar, which we plan to use in multiple different places.

In this particular situation, we're going to pass the modifier into this function, which is a way of basically saying that the parent composable will actually decide where to position and how to size this particular UI element.

The title is pretty self explanatory, but what is a little more complicated is the icon.

And again, that will be dictated by something in the parent composable.

That's how I actually make this thing reusable and allow it to handle different icons or different actions when it's clicked.

After we finish off this particular composable, I'll show you a quick preview of the actual icon that we'll be using.

So hopefully that will make a little bit more sense.

The first thing we want to do is override the top app bar composable.

Let's just pause a moment and talk about different colors.

So one way to solve this problem would be to hard code some kind of color in here.

But in the previous section of this tutorial, we went through the trouble of setting up both a light and dark theme.

So what we're doing here is we're actually going to be using a color which is based on the theme.

Remember in the graph Sudoku theme composable, there was a call to a function which was his system and dark theme or something like that.

And that's actually going to dictate which color palette we select.

So by using material theme colors dot primary, it will automatically inherit the appropriate color based on whether we're in light and dark mode.

And that would be one reason to avoid hard coding something in here.

In this case, we have a color which will be the same regardless of whether it's light or dark mode.

So we're just going to add in a text composable which is effectively eight Extra view.

But if you wanted to add something like a logo for the application in front or after the title text, and what you could do is you could add in a row here and then just add in both the icon and then the text composable.

And then you'd be ready to go.

Go ahead and import that.

This is probably pretty self explanatory.

But when we want to inherit style data for particular fonts and stuff like that, then this is how we can do it.

Again, this is something super handy.

And you only see this in kotlin, certainly not Java.

So what we're doing here is we're explicitly asking is the application currently in light mode, and then we're picking a text color based on that.

This is really just an alternative way of handing this conditional UI logic without having to assign something to a theme specifically.

Next, we'll deal with alignment.

And that's it for the texts composable in our toolbar.

So action bar is probably something that will be more familiar to the older Android developers.

But basically think of this is like the icons within the toolbar.

Generally, they're used for very important actions in the user interface, like navigating to a new feature, indicating that you're done doing something.

And note importantly, that this particular lambda function is of type row scope.

So basically, what that means is, if you have several action buttons, you can place them within these two brackets here, and they will automatically be lined up like a row.

Now all we need to do is just type icon and then add in the parentheses here.

And this is because we're actually going to be passing this icon in from the parent composable.

As I said, moments ago, I just wanted to give you a sneak preview of the icon itself.

We're not going to be writing it yet, but we will do so later on.

The important thing to understand here is that we're deciding about how to handle on click and what this thing actually looks like in the parent composable, we're not actually doing it within the toolbar.

And by pulling that responsibility out of the toolbar, that's how we get the reusability that we want.

Right click on the components package, go to New kotlin file, and this one's going to be called loading screen.

Let's create our loading screen composable.

The first thing we'll need is a surface.

So you might be wondering, why are we using a surface here in particular, in this case, I really just want like a space surface of the UI, which has a particular color and specific dimensions.

Here I've set Phil max height to a fraction of point eight F, which is basically saying I want it to take up most of the width, or sorry, most of the height of the user interface.

But I might want some space for something like a an ad banner or something of that nature.

Anyways, I basically want an icon or an image which is stacked on top of a progress bar which will be stacked on top of some kind of like text.

So for that kind of situation, obviously we're going to want to use a column.

Obviously, we'll be centering things.

Go ahead and import that.

Now I'm noticing it's not improperly importing our I think there's something within the Compose libraries, which basically mimics our so let me just fix those imports before we proceed.

As you can see here, I've just copy and pasted it in the our import.

And now we're good to go.

That's our logo.

Here's your progress bar.

Okay, so you might be wondering about this painter thing.

So basically, in the alpha version of compose, we had to specify whether it was a vector asset or a bitmap asset and stuff like that.

So we can just use this generic painter resource thing and point it to basically anything in our drawable.

And it will actually figure out whether it's a bitmap or a vector asset.

Also, I wanted to point out the copy function here.

Suppose you have a color and you want to slightly change the alpha value or you have one of these textile objects and you want to make some kind of change to it.

The copy function is super handy for doing that.

In this part of the tutorial, we will create the event sealed class view model and presenter for the act of game feature.

Before we do that, let us look at a few design decisions involved in this architecture.

The purpose of our presentation logic class, which I call logic, for short, is exactly as the name implies, it handles the work of coordinating the container view model and backend repositories.

If notified of a non stop event, it will also cancel all co routines, it does not possess any Android platform code, which makes it loosely coupled and very easy to test.

I might also consider reusing it for a desktop version of this app.

But we'll see.

The purpose of the view model is also to do exactly what the name implies, it is a virtual representation of the user interface, which the view observes.

In simpler terms, it is a model of the view, it exposes function types, which is a very simple and easy standing for the observer pattern.

In situations where we don't require multiple observers.

Each time our logic class updates the view model, the view model will automatically publish the new data to the view.

Another design decision with this view model is that it does not extend jetpack view model.

There are several reasons for this decision, some of them simple, and some of them quite technical.

The simple reason is that using jetpack view model creates tight coupling with the Android platform.

And it has its own set of boilerplate code and dependencies, which I'm not a huge fan of.

In short, it doesn't solve more problems than it creates in this particular application.

And I wanted to practice creating view models which might be usable for kotlin desktop or kotlin.

j s.

The technical reason why is that in this application, we simply don't need to persist the data across activity instances or process death in order to have a good user experience.

Instead, we just make a fairly cheap call to the Android file system and reload the data from there if such events occur.

Now, before you apply that reasoning in every situation, understand that reloading data from a file system works fine in this application, but should not be considered a suitable replacement for unsaved instance state in every application you write.

If you like the models in save state handle, go right ahead and use it.

We also employ the strategy pattern to clean up the interface which our logic class exposes to the container in the view.

Each subclass of the sealed class represents an action that can occur in the view or container.

Rather than having a function for every UI event.

We have one function that accepts a single object that can represent multiple different paths of execution.

That's the strategy pattern.

Right click On the UI package, and go to new package called active game, right click on this new package, go to New kotlin file or class, and we're going to create an interface, and it's going to be called active game container.

This word container is a technical term.

The way I'm using it here is to kind of signify something which contains a large portion of an application or an entire application.

In my opinion, a container doesn't usually deal much with the business kind of logic of the application.

It basically just wires things together and builds things and kind of serves as an entry point.

In the next part of this tutorial, I'll explain what we'll actually be using as a container.

But by using an interface here, I'm basically stating quite explicitly that I might change my mind about what we use as a container.

Anyways, it only contains two abstract functions.

Right click on the active game package again, and we're going to create a sealed class this time.

And it's going to be called active game event.

So as I explained in the above comment, the active game event sealed class represents every kind of user interaction of a given feature, in this case, the active game feature.

This is a very common pattern that I use and we'll see how it works with our base logic abstract class which we created in the common package.

Okay, we are now going to create our view model.

Firstly, let's create a small little class here which will be like a virtual representation of a single tile in a Sudoku puzzle.

So obviously, x&y represent the x&y coordinates of the particular Sudoku tile value will represent what we talked about in graph data structures as the color again, it's literally just a number, I don't know why we need to call it a color.

Now it has focused indicates that the user has clicked on a particular tile, after which they can click on one of the input buttons to change that particular number.

And finally, a read only tile you can think of as a tile, which is like a given clue in the puzzle.

So therefore, the user is not allowed to actually change any read only tiles.

So before we start reading this view model, I just wanted to mention a couple of things here.

As discussed in the introduction for this particular section, I didn't actually want to use any of the jetpack libraries to achieve a publisher subscriber relationship between the view model and the view.

Now, it turns out that that publisher, subscriber relationship or pattern is actually quite easy to implement.

But in this case, I actually found it simpler to just use kotlin function types to achieve what I would call a poor person's publisher, subscriber pattern or observer pattern.

So this really means something simple in practice, although it might look kind of complicated for those who aren't really familiar with working with function types.

Our view model will possess these nullable function type references.

As we'll see in a moment, what we can do is each time we update the view model from the presentation logic class, we can then update the view by extension by invoking these function types from within the view model.

Now, the reason why we're using notables here is from within the view model.

I can never be 100% certain if there is actually anything listening.

But with that being said, I feel like if I played around with this particular class for a couple of hours, I could probably streamline a little bit and maybe make some of these internal variables private or something like that.

So really what I'm saying here is Feel free to take this general idea of having a view model, which isn't tightly coupled to Android jetpack, but also feel free to experiment with it and see if you can optimize it.

So with that out of the way, let's create some function types.

So all of these function types will be prefixed with sub to ensure good legibility.

Active game screen state is actually something we will create in the next part of this tutorial.

So just go ahead and leave it glowing red here.

Okay, let me just briefly explain these different function types.

So the board state is basically a virtual representation of the Sudoku board.

Obviously, the content state basically just means three different states.

So either we're loading the data, the user has a currently active game that they're solving, or the user has completed a particular game, we will use this to animate between different states in the user interface.

Now, timer state has to do with the count up timer, which basically records how long it takes for the user to complete a given Sudoku game.

So just to hopefully clear up any confusion here, timer state will be the actual long value in milliseconds representing the time and then sub timer state is the way that we actually update the user interface after we update the new timer state.

Let's finish off the rest of these variables.

These are quite obviously default values.

Next, we'll write a function to initialize this view model.

Okay, so let's just pause for a moment.

What we're doing here is we're taking the state of the data as it existed in storage, we're giving it to the view model, and then what we're doing is we're building the view models own virtual representation of that state.

Now, the view models internal representations will have things like has focus, which are concerned specifically of the user interface and not necessarily something that I would include in the original domain model.

Also, in case you're wondering, the key value is basically created from hashing the x value and the y value.

This is something that we covered very early on in this tutorial, in case you've jumped ahead.

Again, active gain screen state is something that we will create in the Compose part of the tutorial.

Here, we're binding that data to the view model.

And then we will invoke our function types to update the view assuming it's listening.

And that's it for our init function.

Now, we just have a few more functions, which will be called by our presenter to do various things with the state of the view model.

So here, we're just updating an individual tile.

So what we're doing here is when the user hits a particular tile, that's going to send a message into the presenter, which will have a particular x and y coordinate, and then the presenter will call this particular function.

And so what it will do is it will look for the tile which the user clicked on based on that X and Y value, and set that one to has focus equals true.

And then for every other tile, we want to set it to false.

Otherwise, we could have a situation where the user has selected multiple different tiles, which is not something our application is supposed to be allowed to Do and this would be the situation where our back end has determined that the current puzzle is complete.

Right click on the active game package.

And let's create a new kotlin class, which is going to be called active game logic.

Okay, so before we proceed, this is definitely one of those situations where I strongly suggest having the complete source code open on the side while you follow along here.

Obviously, I'm going to do my best not to make any mistakes, but it's possible that I will make a mistake.

Active game logic represents the presentation logic of this particular feature of the application.

As we'll see, it coordinates between the container the view model, and then by extension, the view, as well as the back end of the application.

Let's start with the constructor.

Okay, so just a bit of review before we move on, for the time being the container will actually be an activity.

But there's a possibility in the future, I might move to using fragments as containers instead, at this point, I don't really want to, but we'll just see if that makes sense in the future.

But this is the entire reason why I have included an interface here so that I can change what's behind the interface very easily.

The view model is pretty clear, we just wrote it.

Game repo is where we store the game data.

So that includes the game settings as well as the current progress of the users game stats.

repple is where we store the records for the shortest times to completion of each different difficulty and size of puzzle.

And if you're wondering what the dispatcher is go back and watch the common package when we created that we created this dispatcher provider and I basically explained what the purpose of it is there.

Base logic is also something that we created in the common package.

And we'll see the function that we inherit from that class in a moment.

Okay, let's start for a moment about co routines.

So one way to think about scopes, whether we're talking about co routine scope, or dagger or whatever, is to really just consider that it's about a life cycle.

Now, you're probably wondering, why are we not making something like a view model or a fragment or an activity, our life cycle class? Well, in case you haven't noticed by now, I don't like any kind of tight coupling to the Android platform, if I can avoid it.

There's a number of other reasons.

But one of the main ones is that because this class contains all of the presentation logic, in a sense, it's the head decision maker for this feature of the application, then, in my opinion, it makes sense to make it responsible for cancellation of any co routines, which happened to be currently running as far as this on event function, which we inherit from base logic.

Well, basically, this is an implementation of the strategy pattern.

I won't give you a long and technical explanation here.

It's actually a very simple pattern.

But basically, it provides sort of like a singular entry point into this particular class.

So instead of having like a single function, for every event, we have one function, which takes in an argument, our active game event, which is capable of representing all the different events, and I just find that really cleans up the interfaces between different classes, interfaces is used in the general sense in that statement.

Okay, first, let's implement our co routine context.

Remember, jobtracker exists in base logic, but we also need to initialize it Okay, now before we proceed, there's something really important we need to implement which is a ko routine timer.

As I mentioned before, the active game Screen does have a count up timer.

Now some of you are probably going to be wondering, why didn't I use the Java timer class or the androids count up timer or whatever it's called.

Basically, I did try using those things.

And they presented different application breaking problems.

And it turned out to be easiest just to create this kind of CO routine timer.

Okay, so this requires a little bit of explanation, obviously.

So we'll notice two different keywords here, which might be unfamiliar to some of you, we have the inline and cross inline keywords.

So whenever you see the inline keyword, the easiest way to understand that is to understand that it just means copy, paste.

And if you want to know what that means in code, then I suggest you decompile some of your kotlin code, which uses the inline modifier, and you'll see how the inline function is actually copy and pasted into the call site.

Now we have something else going on here, which is a cross inline function type.

So before I explain what the cross inline action is, let's talk about what this function actually does.

So here we have a pretty standard spin lock, while true.

So it's a loop that's going to just endlessly execute, it's going to invoke that function type, and then it's going to delay for 1000 milliseconds.

Now, there's a couple different things going on here.

Number one, you have to understand that we will be delaying this ko routine, but it's not actually going to block the thread that it's on, which is of course a big win.

Now the other thing that's going on here is action is going to be a lambda expression that we will pass into this particular function.

Really the only thing crossing line does is it basically makes it so that in the lambda function, which we will pass into this function here, we're not allowed to write a return statement in that function.

So in the most general sense here, what we're doing is we're taking a preventative step, to avoid a situation where we might accidentally return from within the lamda that we pass in here causing unexpected behavior.

Now, you're probably wondering, since we have this endless loop going on, how do we actually stop this particular core routine? Well, what we're going to do is we're going to create a job.

Let's just do that now.

And what we'll do soon is we will actually assign this job variable to our start co routine timer, and that will allow us to cancel it.

Let's just write another quick extension function to do with this timer business and then I'll explain what it does.

In experimenting with the user interface, how to make the timer the least janky or most accurate that it could be it turned out that subtracting one from the value each time we write it to the back end created a more consistent timer.

But one particular edge case is if the value equals zero, then obviously we don't want to subtract one from it.

Otherwise the timer will say negative one at first and that just doesn't look very good.

Okay, so with all that done, we can get to implementing the rest of the presentation logic Okay, so when the user hits an input button, we can have two different situations that could occur.

In one situation, the user has already selected a tile, which would become the focus tile.

Or it might be that they just hit an input button without actually focusing a tile, in which case, we don't really want to do anything.

Okay, so if you're wondering about the details of game repo, you can go back to the part of the tutorial where we actually build it.

Basically, we're going to be creating a lambda to represent the success case, and then another lambda to represent like an error exception case, to make that a little bit more legible.

I'll just add in a comment here.

Okay, so if you're again, wondering how we actually cancel the timer, this is exactly how we do it, we cancel the job object.

Now we'll write this other function in a moment.

Basically, if it's a new record, then we want to render the user interface slightly differently than if it wasn't a record.

But before we do that, let's finish off the error case.

So in order to actually know if it's record, we actually need to pass the value back into the stats repo just to check on that.

So I'm going to be honest, the error handling in this application is not the best, neither is it really the worst show error Well, for the time being just actually show a toast message explaining that some error occur.

Okay, just a quick fix.

This is actually supposed to be elapsed time not timer state.

Next, we have on new game clicked.

You'll notice a recurring theme here, which is at any time we want to perform concurrent operations.

So anytime we're working with the back end, we're going to wrap that into a launch co routine.

There's a lot of different ways to work with CO routines.

This is just one of the most simple straightforward ways to do it.

In my opinion.

Okay, so what we're doing here is first we're asking the view model has the user completed the current game, if they haven't, we actually want to store the progress the user has made in their current game, when they hit on new game clicked, because maybe they hit it by accident, or they want to be able to go back and finish the game or some reason like that.

That's right The update with time function.

Again, we have success and error cases.

Hopefully, that's pretty clear at this point.

Next, we'll implement that function.

Next, we'll write the cancel stuff function.

So basically, the cancel stuff function essentially cancels every ko routine.

Next, we'll write on start.

I forgot to mention earlier, the reason why we had an underscore in one of the functions for is complete.

It's just kind of a convention for a lambda argument or parameter, which doesn't actually end up getting used.

In this case, we're going to use it.

Okay, so obviously, this is where we start the coroutine timer, and we only want to do that when on start is called.

Now again, I feel like I could have handled this a little bit better, it kind of goes against my rules to consider standard flow the application as an exception.

But basically, what we're going to do here is in the event that we ask the storage for a current game, and it doesn't actually retrieve anything, generally speaking, this situation is going to occur when the user has run the application for the first time, and hence, there wouldn't actually be any data stored.

So in that particular case, we would want to do this.

Now, we could also end up here because of some kind of legitimate exception, but in that particular case, I still think navigating to the new game feature is still actually a good way to handle that good but maybe not the best.

Next we have on stop.

Okay, so onstop is actually tied to the lifecycle of the Android activity or fragment that it's bound to.

So when this function is called, that basically means that we want to save the user's current progress and then kind of shut everything down.

Finally, we have on tile focus.

So this would be when a user actually selects a particular Sudoku tile.

Now this function is incredibly complicated, so brace yourself.

Okay, I lied.

That's actually everything we need to do.

In this part of the tutorial, we will create the user interface for the active game feature.

Before proceeding I strongly suggest you watch my video entitled How to Understand jetpack compose a beginner's guide to composable and read composition.

We will be writing many composable and setting up re composition as well.

But that video is made for people who are just getting started with compose.

It explains what a composable is, what re composition is, and how to avoid doing read composition the wrong way.

And believe me, it is fairly easy to screw up read composition if you aren't aware of how it works.

I will link to this video in the pinned comment below.

This video is full of a ton of information, so please take a look at the timestamps in the description box below.

Topics include basic widgets, such as text, text, button, image, icon, spacer, and divider layouts, such as box column row box with constraints and my favorite constraint layout.

Simple transition animations to animate between a loading screen, active game and a complete game.

I also show you how to communicate with our presentation logic and our view model using function types and lambda expressions.

Before we write the composed code, though, I show you how to set up an activity as a container for composable.

The process for doing this is almost identical for fragments if you prefer them, I also show you how to write a very simple dependency injection extension function, which hides the backend details from the front end.

Right click on the active game package and go to new activity and choose an empty activity.

And make sure you uncheck generate a layout file.

And this activity will be called active game activity.

It will be the launcher activity.

Now in case you're wondering why we're using this wizard instead of just creating a class file.

The reason is simply that by using the wizard it will add an entry into the manifest so we don't have to do that.

Just to briefly recap the purpose of this activity here is as a feature specific container.

Let's start by creating a reference to our active game logic class.

Next, let's implement the active game container interface.

Click on the red and hit alt enter.

For show error, we will use the extension function that we created way earlier on in this tutorial.

Go ahead and import.

Also in case you're wondering, this is single expression syntax here it basically just replaces the brackets and return statement with just an equal sign.

Next one implement on new game click.

Now obviously we haven't created new game activity yet so that will show up as red until we do.

We also need to override two more lifecycle methods.

Here will tell the logic class that everything is ready to go And then we'll also override on stop.

And that will obviously signal the logic class that it's time to cancel things and tear stuff down.

Finally, we just need to add a few lines to on create.

First of all, create the view model.

Now, this is a really important part, what we're going to do next is we're basically going to anchor our composable that we'll be creating in the next part of this tutorial, to the activity here.

This is also something you can call inside of a fragment.

Go ahead and import that.

Naturally, we're gonna wrap everything in our compose theme.

Now, this is a very critically important thing to understand and a very important pattern.

So when we create active game screen, we're going to pass in a function type, which will serve as our event handler, which is basically my way of saying that is the way in which we will forward the events, the onClick events and stuff like that, that occur in the composable to our presentation logic class.

So make sure you pay attention to what I'm saying here, because this is a really important part, even if you don't use presenters wood, or whatever.

Function types are an excellent way to handle on click events, especially if you combine them with the strategy pattern, which we discussed in the previous section.

Okay, now, if you've never seen a function reference, I believe it's called basically what we're doing here is we are pointing to the on event function of the logic class.

This really is a function reference.

So hopefully, you can understand what I'm talking about here.

We'll also pass in the view model.

Now lastly, we actually need to build our logic class.

So what we'll do is we will write that code in an extension function, but what we can do first is just read it here.

And that's everything we need to do in our activity.

Right click on active game, go to new package, and this package will be called build logic.

Right click on that package and go to New kotlin file or classes is going to be a file.

And it's going to be called build active game logic.

If you've been watching my channel, or live streams for some time, you'll know that I talk a lot about dependency injection service locators.

And one of the things I say all the time is that in a small application, you really don't need to use di container, like hilt, dagger, whatever, you can use it.

But what I always advise for beginners is to write the code that these things generate for you, yourself first, so that you understand what these frameworks are doing for you.

So that's exactly what we're going to do.

We're going to write the kind of thing that these frameworks generate for you.

And in a small application, it's actually very simple code to write.

And, of course, it's going to return active game logic.

Okay, let's pause for just one moment here.

So in case you're wondering how we get the path to the storage directory that we can use for this application, you can call context dot files dir dot path.

Finally, our dispatcher.

And that's all we need to do.

Right click on the active game feature, and create new kotlin file called active game screen.

First, let's create an enum.

This enum represents different states, which this feature of the user interface can possess.

The actual state is held in the view model, but we will see how we can update our composable UI by binding to the view models function types we created in the previous part of this tutorial.

Active game screen represents the root composable.

In this hierarchy of composable, it has the responsibility of setting up the core elements of the UI, and also animating between them.

event handler function type reference is how we call back to the presentation logic.

When the user interacts with the application, it must be passed down to any composable, which has such interactions, we also pass in the view model, which is how we actually give the data to our UI.

In very simple language, whenever we have some kind of data or state, which may change at runtime, we want to wrap that data in a remember delegate.

This tells the Compose library under the hood, to watch for changes and to redraw the UI if a change occurs.

Now mutable transition state is used specifically for animations here, so don't use this everywhere.

We will see a more general purpose example of a remembered state later on.

Remember, delegate prepares compose for updates, but we also need a way to actually update the value.

We do this by binding a lambda expression to one of the function types which our view model possesses.

When one of those functions is invoked in the view model, the program automatically jumps to and executes this code within our composable.

This is what actually triggers the re composition.

We have a remembered transition state and a way to update that state from the view model.

Now we need to set up the transition animations themselves.

This is where you can get as creative as you like.

In this app.

Each content state has its own composable associated with it.

We animate between them simply by changing the alpha value or transparency.

Now it was truly up as red a moment ago, the way I fixed that was to manually import the Compose runtime.

So the transition spec tells compose details about what the animation should look like.

Essentially, this means we don't have to write our own mathematical instructions, which is great for someone like me who sucks at arithmetic.

One option for compose is to use the scaffold composable as a skeleton for your UI.

I personally prefer to do this myself, since it's not really that difficult, and it doesn't hide anything from me.

First, we have our app toolbar.

Let's go ahead and create that new game icon.

These icons come from the Compose material library, I highly recommend you use it.

This is how we actually trigger an on click event.

As explained in a previous part of the tutorial, by creating our toolbar icon here and passing it into the app toolbar composable, we make the app toolbar reusable.

Below the toolbar we have the main content of this screen, which can have three different states.

Each time a re composition occurs, this one statement will be executed again.

The act of alpha value will change when the transition animation occurs, thus fading out the previous content state and fading in the new one.

Obviously, we will create these in a moment.

And that's it for our route composable the most complex part of our UI comes from an active Sudoku game.

A nine by nine puzzle has 81 different texts composable, which is a large number of widgets.

The way I went about writing this composable was to think of each part of the Sudoku game as a particular layer or element.

Be sure to avoid writing God composable by making usage of helper functions, which break down the UI into the smallest reasonable parts.

box with constraints is kind of like a composable wrapper, which gives us information about the height, width and other measurements, we can use that information within its lambda expression.

We need to know the screen width in order to determine how wide and tall the Sudoku board should be.

Here we asked for the max width of this constraint layout.

Here we ask for the max width of this layout composable.

But we need that value to be in density independent pixels, and it needs to be relative to the density of the current screen as well.

That's where the two dp extension function comes in.

And it uses the local density to determine that value.

The margin of the board also needs to change based on the screen density.

I arrived at these values simply by testing the app on various densities using the emulator.

Next, we will write a constraint layout, which is a totally awesome way to manage dynamic layouts.

Now in order to constrain composable to each other, we need a way for them to reference each other.

This is equivalent to setting IDs for XML views.

First, we create these references and you will see how to bind them later on.

Let's create a layout container for the puzzle board.

Okay, so this is really important see how we're passing in that reference in the constrain as parameter there.

This is how we actually associate a particular composable with a particular reference.

This box composable will be associated with the name board.

Next we'll create the Sudoku board itself.

Again, the boundary is like the size of the puzzle.

So it's either a four by four puzzle or a nine by nine puzzle.

So boundary would either be four or nine.

This is supposed to say size.

So the offset here is used to evenly distribute the screen real estate for each Sudoku, tile and grid line.

Here's a way to make a mutable state which is not associated with some kind of transition animation.

So this is the more general purpose approach.

So the first argument here view model dot board state can be thought of as the initial value, never equal policy ensures that even minor changes in the state like has focus actually triggers a re composition.

Again, this is how we actually update the value once the view model is updated.

As you can see, here, again, I'm making usage of lots of helper functions to break things down.

Here we render the text fields which represent tiles in the puzzle, they can either be read only or mutable, thus, meaning that we need to render them slightly differently.

So here, we're saying if the user sets a particular tile to a value of zero, we actually just want to render it as an empty tile.

The main idea here is that we're using the x and y values of each individual tile along with the offset in order to evenly position each tile So when the user selects a tile, it's going to become focused and we want to render that tile obviously a little bit different than an unfocused tile.

Now we'll render the read only squares.

Next we'll create the board grid.

So Sq RT is an extension, which is actually defined in the computation logic.

In retrospect, I probably should have defined that in the common package, but it's pretty obvious what it does.

So this function here, we'll draw the grid lines that separate the Sudoku puzzles.

To make it more obvious to the user which sub grids are which we draw different borders to separate the four by four or nine by nine sub grids.

This is why we're using modulo here.

So this will draw both the vertical and the horizontal lines.

Okay, so we're jumping back into the game content composable to finish it off.

below our Sudoku board, we have some different icons to indicate the difficulty of the puzzle.

Next we need a layout container for the count up timer that's great, the timer texts composable.

Now the default value, it's just empty.

Okay, we're back in the game content composable.

The last thing we need to do is just add a layout container for the input buttons.

Now we're about to hard code some values in here and that is kind of bad practice.

But the reason is that the Compose team deprecated flow row, which I'm still upset about, and it worked perfectly for this situation, and I've been too lazy to implement flow myself.

Hey, at least I'm being honest.

In case you're wondering, 0.4 and five dot dot nine will emit a range inclusive of those values.

Let's create that composable a spacer is pretty self explanatory, it just takes up some space in the layout.

Next we have the buttons themselves.

This text button wrapper allows us to style a nice looking button instead of just adding on click on a text composable.

Alright, that's it for gain content.

Now we need to do the game complete content screen, which is obviously when a user finishes a game.

So this is basically just two images stacked on top of each other, but we're only going to render one of them if it is actually a new record that the user made.

So since we don't actually create the emoji events icon, we can change the color of it using this color filter thing.

Pretty handy.

Next, we have two texts composable.

And that's it.

ยินดีด้วย.