เกี่ยวกับ Proto DataStore

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

Proto DataStore ใช้อ็อบเจ็กต์ที่พิมพ์ซึ่งสนับสนุนโดยProtocol Buffersเพื่อจัดเก็บชุดข้อมูลที่มีขนาดเล็กลงในขณะที่ให้ความปลอดภัยประเภท โดยไม่จำเป็นต้องใช้คู่คีย์-ค่า ซึ่งทำให้โครงสร้างแตกต่างจาก รุ่นก่อนและการใช้งาน แบบSharedPreferencesพี่น้องPreferences DataStore อย่างไรก็ตาม นั่นไม่ใช่ทั้งหมด — DataStore นำการปรับปรุงอื่นๆมาSharedPreferencesสู่ . รู้สึกอิสระที่จะข้ามกลับไปที่โพสต์แรกของเราในซีรีส์นี้ อย่างรวดเร็ว และดูการเปรียบเทียบโดยละเอียดที่เราได้ทำไว้ที่นั่น ต่อจากนี้ไป เราจะเรียกProto DataStoreว่า Just Protoเว้นแต่จะระบุไว้เป็นอย่างอื่น

สรุป:

  • จัดเตรียมAPI แบบอะซิงโครนัสอย่างสมบูรณ์สำหรับการดึงและบันทึกข้อมูล โดยใช้พลังของ Kotlin coroutines
  • ไม่มีการสนับสนุนซิงโครนัสที่พร้อมใช้งาน — หลีกเลี่ยงการทำงานใดๆ ที่บล็อกเธรด UI . โดยตรง
  • อาศัยกลไกการส่งสัญญาณข้อผิดพลาดภายในของ Flow ช่วยให้คุณตรวจจับและจัดการข้อยกเว้น ได้อย่างปลอดภัย เมื่ออ่านหรือเขียนข้อมูล
  • จัดการการอัปเดตข้อมูลอย่างปลอดภัยในการดำเนินการอ่าน-แก้ไข-เขียนอะตอม มิก ให้การ รับประกันกรดที่แข็งแกร่ง
  • ช่วยให้ย้ายข้อมูลได้ง่ายและง่ายดาย
  • ต้องการความปลอดภัยเต็มรูปแบบและข้อมูลของคุณต้องทำงานกับคลาสที่ซับซ้อน มากขึ้น เช่น enums หรือ list? ไม่สามารถทำได้ด้วยPreferencesดังนั้นให้เลือกProtoแทน

ในการใช้ Proto DataStore คุณต้องทำความคุ้นเคยกับProtocol Buffers ซึ่งเป็นกลไกที่เป็นกลางทางภาษาและเป็นกลางของแพลตฟอร์มสำหรับการ จัดลำดับข้อมูล ที่มีโครงสร้าง มันเร็วกว่า เล็กกว่า ง่ายกว่าและคลุมเครือน้อยกว่า XML และอ่านง่ายกว่ารูปแบบข้อมูลอื่นที่คล้ายคลึงกัน

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

ตัวอย่างสคีมาของข้อมูลบางส่วนใน.protoไฟล์:

วิธีใช้โค้ด Kotlin ที่สร้างขึ้นเพื่อสร้างโมเดลข้อมูลของคุณ:

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

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

การตั้งค่าการพึ่งพา Proto DataStore

ตอนนี้ มาดูโค้ดบางส่วนและเรียนรู้วิธีการทำงานของProto

เราจะใช้ตัวอย่าง codelab ของ Proto DataStore หากคุณสนใจวิธีการลงมือปฏิบัติจริงมากขึ้นในการติดตั้งใช้งาน เราขอแนะนำให้คุณลองใช้โค้ดแล็บ Working with Proto DataStoreด้วยตัวคุณเอง

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

ขั้นแรกเราจะเพิ่มการพึ่งพา Proto และการตั้งค่า protobuf พื้นฐานบางอย่างให้กับโมดูลของbuild.gradleคุณ หากคุณสนใจการปรับแต่งขั้นสูงของการรวบรวม protobufs ให้ตรวจสอบProtobuf Plugin for Gradle notes :

💡 เคล็ดลับด่วน — หากคุณต้องการลดขนาดบิลด์ของคุณ อย่าลืมเพิ่มกฎเพิ่มเติมในproguard-rules.proไฟล์ของคุณเพื่อป้องกันไม่ให้ฟิลด์ของคุณถูกลบ:

การตั้งค่า Protobuf สำหรับ Proto DataStore

การเดินทางของเรากับ Proto เริ่มต้นด้วยการกำหนดโครงสร้างของข้อมูลที่คงอยู่ ของคุณ ใน.protoไฟล์ คิดว่ามันเป็นสคีมาที่อ่านได้สำหรับคุณและเป็นพิมพ์เขียวสำหรับคอมไพเลอร์ เราจะตั้งชื่อของเราuser_prefs.protoและเพิ่มลงในapp/src/main/protoไดเร็กทอรี

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

UserPreferences— คลาสข้อมูล Kotlin:

UserPreferences— .proto สคีมา:

หากคุณไม่เคยใช้ protobufs มาก่อน คุณอาจสงสัยเกี่ยวกับสองสามบรรทัดแรกในสคีมา มาทำลายพวกเขากันเถอะ:

  • syntax— ระบุว่าคุณกำลังใช้proto3ไวยากรณ์
  • java_package— ตัวเลือกไฟล์ที่ระบุการประกาศแพ็คเกจสำหรับคลาสที่คุณสร้างขึ้นซึ่งช่วยป้องกันความขัดแย้งในการตั้งชื่อระหว่างโปรเจ็กต์ต่างๆ
  • java_multiple_files— ตัวเลือกไฟล์ที่ระบุว่าจะสร้างไฟล์เดียวที่มีคลาสย่อยซ้อนกันสำหรับสิ่งนี้.proto(เมื่อตั้งค่าเป็นเท็จ) หรือหากไฟล์แยกต่างหากจะถูกสร้างขึ้นสำหรับข้อความระดับบนสุดแต่ละประเภท (เมื่อตั้งค่าเป็นจริง) เป็นเท็จโดยปริยาย

เครื่องหมาย= 1, = 2ในแต่ละองค์ประกอบจะระบุ "แท็ก" เฉพาะที่ฟิลด์ใช้ในการเข้ารหัสแบบไบนารี เช่นเดียวกับ ID ของการเรียงลำดับ เมื่อใช้ประเภทข้อความของคุณแล้ว ไม่ควรเปลี่ยนหมายเลขเหล่านี้

เมื่อคุณเรียกใช้คอมไพเลอร์บัฟเฟอร์โปรโตคอลบน.proto,คอมไพเลอร์ คอมไพเลอร์จะสร้างรหัสในภาษาที่คุณเลือก ในกรณีเฉพาะของเรา เมื่อรันคอมไพเลอร์ สิ่งนี้นำไปสู่การสร้าง คลาส ซึ่งพบใน ไดเร็กทอรี ... UserPreferencesของแอปของคุณ :build/generated/source/proto

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

ตอนนี้เรามีUserPreferencesแล้ว เราต้องระบุแนวทางสำหรับวิธีที่ Proto ควรอ่านและเขียน เราทำสิ่งนี้ผ่าน DataStore Serializerซึ่งจะกำหนดรูปแบบสุดท้ายของข้อมูลของคุณเมื่อจัดเก็บและวิธีการเข้าถึงอย่างถูกต้อง สิ่งนี้ต้องมีการแทนที่:

  • defaultValue— สิ่งที่จะส่งคืนหากไม่พบข้อมูล
  • writeTo — วิธีแปลงการแสดงหน่วยความจำของวัตถุข้อมูลของเราให้อยู่ในรูปแบบที่เหมาะสมสำหรับการจัดเก็บ
  • readFrom— ผกผันจากด้านบน วิธีการแปลงจากรูปแบบการจัดเก็บเป็นการแสดงหน่วยความจำที่สอดคล้องกัน

💡 เคล็ดลับด่วน — หาก AS ของคุณไม่พบสิ่งUserPreferencesที่เกี่ยวข้อง ใดๆ ให้ ทำความสะอาดและสร้างโครงการของคุณใหม่เพื่อเริ่มต้นการสร้างคลาส protobuf

การสร้าง Proto DataStore

คุณโต้ตอบกับ Proto ผ่านอินสแตนซ์ของDataStore<UserPreferences>. DataStoreเป็นอินเทอร์เฟซที่ให้การเข้าถึงข้อมูลที่UserPreferences มีอยู่ ในกรณีของเราในรูปแบบ ของการสร้าง

ในการสร้างอินสแตนซ์นี้ ขอแนะนำให้ใช้ผู้รับมอบสิทธิ์dataStoreและส่งต่อfileNameอาร์กิวเมนต์serializerบังคับ:

fileNameใช้สำหรับสร้างข้อมูลที่Fileใช้ในการเก็บข้อมูล นี่คือสาเหตุที่dataStoreผู้รับมอบสิทธิ์เป็นคุณสมบัติส่วนขยาย Kotlinซึ่งประเภทผู้รับต้องเป็นอินสแตนซ์Contextเนื่องจากจำเป็นสำหรับการสร้างไฟล์applicationContext.filesDirผ่าน หลีกเลี่ยงการใช้ไฟล์นี้นอก Proto ในทางใดทางหนึ่ง เนื่องจากจะทำให้ข้อมูลของคุณไม่สอดคล้องกัน

ในdataStoreผู้รับมอบสิทธิ์ คุณสามารถส่งอาร์กิวเมนต์ที่เป็นทางเลือกอีกหนึ่งข้อcorruptionHandler— ตัวจัดการนี้ถูกเรียกใช้ถ้า a CorruptionExceptionถูกส่งออกไปโดย serializer เมื่อข้อมูลไม่สามารถยกเลิกการซีเรียลไลซ์ได้ corruptionHandlerจากนั้นจะสั่ง Proto วิธีแทนที่ข้อมูลที่เสียหาย:

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

การอ่านข้อมูล

หากต้องการอ่านข้อมูลที่เก็บไว้UserPreferencesRepositoryเราจะเปิดเผย a Flow<UserPreferences> จากuserPreferencesStore.data. วิธีนี้ช่วยให้เข้าถึงสถานะที่บันทึกไว้ล่าสุดได้ อย่างมีประสิทธิภาพ และปล่อยทุกครั้งที่มีการเปลี่ยนแปลง นี่เป็นหนึ่งในจุดแข็งที่ใหญ่ที่สุดของ Proto — ค่านิยมของคุณFlowมาในรูปของค่าที่สร้างขึ้นUserPreferencesแล้ว ซึ่งหมายความว่าคุณไม่จำเป็นต้องทำการแปลงเพิ่มเติมจากข้อมูลที่บันทึกไว้เป็นโมเดลคลาสข้อมูล Kotlin เช่นเดียวกับที่คุณทำกับSharedPreferencesหรือPreferences DataStore:

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

🚨 อย่าสร้างที่เก็บแคชเพื่อมิเรอร์สถานะปัจจุบันของข้อมูล Proto ของคุณ การทำเช่นนั้นจะทำให้การรับประกันความสอดคล้องของข้อมูลของ DataStore เป็นโมฆะ หากคุณต้องการสแนปชอตข้อมูลของคุณเพียงรายการเดียวโดยไม่ต้องสมัครรับการปล่อยมลพิษของ Flow เพิ่มเติม โปรดใช้:userPreferencesStore.data.first()

กำลังเขียนข้อมูล

ในการเขียนข้อมูล เราจะใช้DataStore<UserPreferences>.updateData(transform: suspend (t: T) -> T)ฟังก์ชัน ระงับ

มาทำลายมันกันเถอะ:

  • DataStore<UserPreferences>อินเทอร์เฟซ — เรากำลังใช้userPreferencesStoreเป็นการนำ Proto ที่เป็นรูปธรรมไปใช้
  • transform: suspend (t: T) -> T) — บล็อกระงับที่ใช้เพื่อใช้การเปลี่ยนแปลงที่ระบุกับข้อมูลประเภท T . ที่คงอยู่ของเรา

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

มีไม่กี่ขั้นตอนในการวิเคราะห์:

  • toBuilder() — รับBuilderเวอร์ชันของเราcurrentPreferencesซึ่ง "ปลดล็อก" เพื่อการเปลี่ยนแปลง
  • .setShowCompleted(completed)— ตั้งค่าใหม่
  • .build()— เสร็จสิ้นกระบวนการอัพเดตโดยแปลงกลับเป็นUserPreferences

🚨 โปรดทราบว่านี่เป็นวิธีเดียวในการเปลี่ยนแปลงสถานะ DataStore การเก็บรักษาUserPreferencesข้อมูลอ้างอิงและการกลายพันธุ์ด้วยตนเองหลังจากtransformเสร็จสิ้นจะไม่เปลี่ยนข้อมูลที่คงอยู่ใน Proto ดังนั้นคุณจึงไม่ควรพยายามแก้ไขUserPreferencesนอกtransformบล็อก

หากการดำเนินการเขียนล้มเหลวด้วยเหตุผลใดก็ตาม ธุรกรรมจะถูกยกเลิกและมีข้อยกเว้นเกิดขึ้น

ย้ายจาก SharedPreferences

หากคุณเคยใช้SharedPreferencesในแอปของคุณและต้องการถ่ายโอนข้อมูลไปยัง Proto อย่างปลอดภัย คุณสามารถใช้SharedPreferencesMigration. ต้องมีบริบทSharedPreferencesชื่อ และคำแนะนำเกี่ยวกับวิธีแปลงSharedPreferencesคู่คีย์-ค่าของคุณให้UserPreferencesอยู่ภายในmigrateพารามิเตอร์ ส่งผ่านproduceMigrationsพารามิเตอร์ของ dataStoreผู้รับมอบสิทธิ์ เพื่อโยกย้ายอย่างง่ายดาย:

ในตัวอย่างนี้ เราเข้าสู่กระบวนการสร้างUserPreferencesและตั้งค่าsortOrderให้เป็นค่าที่เคยจัดเก็บไว้ในSharedPreferencesคู่คีย์-ค่าที่เกี่ยวข้องกัน หรือเพียงแค่ตั้งค่าเริ่มต้นเป็น NONE

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

produceMigrationsยอมรับรายชื่อของDataMigration. เราจะเห็นในตอนต่อๆ ไปว่าเราจะใช้สิ่งนี้กับการย้ายข้อมูลประเภทอื่นๆ ได้อย่างไร หากคุณไม่จำเป็นต้องย้ายข้อมูล คุณสามารถข้ามสิ่งนี้ได้เนื่องจากมีค่าเริ่มต้นlistOf()ให้ไว้แล้ว

การจัดการข้อยกเว้น

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

เราสามารถจัดการสิ่งนี้ได้อย่างปลอดภัยโดยใช้ตัวcatch()ดำเนินการ Flow และปล่อยgetDefaultInstance():

try-catch หรือด้วยการ เขียนบล็อกง่ายๆ :

หากมีการยกเว้นประเภทอื่น ให้เลือกส่งใหม่อีกครั้ง

ยังมีต่อ

เราได้กล่าวถึงProtocol Buffers และ การนำ Proto ของ DataStoreไปใช้งานแล้ว — เมื่อใดและอย่างไรที่จะใช้สำหรับการอ่านและเขียนข้อมูล วิธีจัดการกับข้อผิดพลาด และวิธีย้ายจากSharedPreferences. ในโพสต์ถัดไปและสุดท้าย เราจะก้าวไปอีกขั้นและดูว่า DataStore เหมาะสมกับสถาปัตยกรรมของแอปของคุณอย่างไร วิธีฉีดมันด้วย Hiltและแน่นอนวิธีทดสอบ แล้วพบกันใหม่!

คุณสามารถค้นหาโพสต์ทั้งหมดจากชุดข้อมูล Jetpack DataStore ได้ที่นี่: ข้อมูล
เบื้องต้นเกี่ยวกับ Jetpack DataStore
ทั้งหมดเกี่ยวกับการตั้งค่า DataStore
ทั้งหมดเกี่ยวกับ Proto DataStore
DataStore และการฉีดพึ่งพา
DataStore และ Kotlin serialization
DataStore และงานซิงโครนั
ส DataStore และการย้ายข้อมูล
DataStore และการทดสอบ

Suggested posts

การพังทลายอย่างช้าๆ: เปิดเผยวิธีที่ผู้คนสูญเสียไปในอาชีพของพวกเขา

การพังทลายอย่างช้าๆ: เปิดเผยวิธีที่ผู้คนสูญเสียไปในอาชีพของพวกเขา

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

บทสนทนาอันตราย

บทสนทนาอันตราย

หากเราต้องการจะเติบโตบนโลกใบนี้ต่อไป เราจะต้องสนทนาที่อันตราย คนที่ท้าทายสิ่งที่เราทำ สิ่งที่เรากำลังทำ และสิ่งที่เรากำลังจะทำต่อไป (หรือกำลังวางแผนที่จะทำ)

Related posts

ไม่มีรหัสผ่านอีกต่อไป — รหัสผ่านจะมาแทนที่รหัสผ่านของคุณได้อย่างไร

ไม่มีรหัสผ่านอีกต่อไป — รหัสผ่านจะมาแทนที่รหัสผ่านของคุณได้อย่างไร

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

การรวบรวม OSINT เพื่อการล่าภัยคุกคาม

การรวบรวม OSINT เพื่อการล่าภัยคุกคาม

สวัสดีชาวไซเบอร์! ยินดีต้อนรับกลับสู่ OSINT สำหรับซีรี่ส์การล่าภัยคุกคาม ในบทความแรก ฉันได้ให้ภาพรวมของ OSINT และความสำคัญในการตามล่าภัยคุกคาม

เครือข่ายทีวีทวิตเตอร์

เครือข่ายทีวีทวิตเตอร์

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

การฆาตกรรมของจอร์แดน นีลี และปัญหาการเหยียดเชื้อชาติ "ผู้รักชาติ"

สหรัฐฯ สามารถดำเนินขั้นตอนสำคัญในการไม่ถือเอาพฤติกรรมเหยียดเชื้อชาติกับการเป็นชาวอเมริกันที่ดีได้หรือไม่?

การฆาตกรรมของจอร์แดน นีลี และปัญหาการเหยียดเชื้อชาติ "ผู้รักชาติ"

[ทริกเกอร์การแจ้งเตือน: บทความนี้รวมถึงภาพหน้าจอของผู้คนที่ยกย่องฆาตกรจอร์แดน นีลี] ในที่สุดเจ้าหน้าที่บังคับใช้กฎหมายก็ได้ตั้งข้อหาแดเนียล เพนนี ชายผู้บีบคอจอร์แดน นีลีจนเสียชีวิตในข้อหาฆาตกรรมครั้งนั้น และอาจไม่เต็มใจนัก