ห้องสมุดนี้อยู่ในอัลฟ่า API ส่วนใหญ่เสร็จสมบูรณ์และจะไม่เปลี่ยนแปลง แต่ก็ยังมีงานที่ต้องทำเพื่อให้การผลิตพร้อมในทุกแพลตฟอร์ม สำหรับ JVM และ Multiplatform ห้องสมุดพร้อมสำหรับการใช้งานการผลิต แต่สำหรับ iOS, JS และ Native มันยังไม่ได้ตั้งค่าอย่างสมบูรณ์ หากคุณต้องการความช่วยเหลืออย่าลังเลที่จะเปิดปัญหาหรือการประชาสัมพันธ์
Redukks เป็นชุดของ abstractions ที่ปลอดภัยแบบง่าย ๆ ที่จำเป็นในการใช้สถาปัตยกรรม uniflow และ redux-like ใน Kotlin Multiplatform มันทำให้การสร้างร้านค้าลดลงและการกระทำและเป็นวิธีที่ง่ายในการทดสอบ
ในขณะที่มีการใช้งาน uniflow หรือ redux จำนวนมากสำหรับ Kotlin/Kotlin Multiplatform ส่วนใหญ่มีความซับซ้อนเกินไปหรือง่ายเกินไป การตั้งชื่อนั้นเป็น "reduxy" เกินไปพวกเขาบังคับให้คุณทำสิ่งต่าง ๆ หรือสิ่งที่เป็นนามธรรมนั้นซับซ้อนเกินไปที่จะเขียนตามปกติ และในขณะที่มีสถาปัตยกรรมที่สอดคล้องกับสไตล์นี้การตั้งชื่อที่พวกเขาใช้นั้นค่อนข้างเฉพาะเจาะจงสำหรับ Android และสับสนกับนักพัฒนาที่ไม่ใช่แอนด์ดรอยด์ทำให้ยากที่จะให้เหตุผลหรือพูดคุยกับนักพัฒนาอื่น ๆ
เนื่องจากฉันพบว่าตัวเองใช้ชุดนามธรรมชุดเดียวกันในช่วงหลายปีที่ผ่านมาฉันจึงตัดสินใจที่จะทำให้พวกเขากลายเป็นห้องสมุดที่ฉันสามารถนำกลับมาใช้ใหม่และแบ่งปันได้
เป้าหมายคือการจัดหา abstractions ที่เรียบง่ายประเภทปลอดภัยและทดสอบได้สำหรับสถาปัตยกรรมที่มีลักษณะคล้าย Redux ในขณะที่ยังไม่ผูกคุณไว้กับรูปแบบเดียวอย่างสมบูรณ์ทำให้คุณมีนามธรรมที่สามารถช่วยคุณสร้างโซลูชันของคุณเอง ในขณะที่ห้องสมุดส่วนใหญ่พยายามที่จะผลักดันรูปแบบ Redux แบบเต็มรูปแบบตัวลดสามารถเป็นค่าใช้จ่ายสำหรับบางกรณีง่าย ๆ และคุณควรจะสามารถหลีกเลี่ยงได้หากคุณต้องการ - นั่นเป็นสาเหตุที่ Redukks ไม่ บังคับใช้ รูปแบบ Redux
สำหรับการให้เหตุผลเพิ่มเติมว่าทำไม Uniflow และทำไม Redux คุณสามารถตรวจสอบสถาปัตยกรรมที่สร้าง (DE)
การจัดการสถานะ : Redukks ให้วิธีง่ายๆในการจัดการสถานะของคุณอัปเดตด้วยวิธีที่คาดการณ์ได้และฟังการเปลี่ยนแปลง คุณสามารถใช้มันเพื่อจัดการสถานะของแอพทั้งหมดของคุณคุณสมบัติเดียวมีสถานะที่ใช้ร่วมกันระหว่างคุณสมบัติที่แตกต่างกัน (หน้าจอ) หรือมีหลายสถานะสำหรับหนึ่งหน้าจอ
การจัดการการกระทำ : Redukks ให้วิธีง่ายๆในการจัดการการกระทำและดำเนินการในวิธีที่คาดเดาได้ คุณสามารถใช้เพื่อจัดการกับการกระทำจาก UI การโทรเครือข่ายหรืองานอื่น ๆ ที่คุณอาจต้องการ ลักษณะที่ไม่ได้รับการผ่าตัดของห้องสมุดช่วยให้คุณสามารถใช้ตัวจัดการแอ็คชั่นของคุณเองเฉพาะกับ USECASE ของคุณ - แม้จะให้คุณทำลายรูปแบบ Uniflow โดยใช้การส่งสัญญาณย้อนหลัง
การทดสอบ : ด้วย redukks การทดสอบสถานะของคุณตัวลดและการกระทำนั้นง่ายอย่างไม่น่าเชื่อ - ทั้งเนื่องจากลักษณะของ API และความปลอดภัยในการรวบรวมพิเศษ คุณสามารถทดสอบตรรกะการจัดการสถานะของคุณได้อย่างง่ายดายหรือตรรกะการจัดการการกระทำของคุณโดยไม่จำเป็นต้องทดสอบ API ที่ซับซ้อน
สถาปัตยกรรม Uniflow ขึ้นอยู่กับแนวคิดง่ายๆหนึ่งแอพของคุณคือวัฏจักรของข้อมูลที่ไหลในทิศทางเดียว รัฐและการเปลี่ยนแปลงนั้นเกิดจาก UI และ UI นั้นถูกขับเคลื่อนโดยรัฐ สิ่งนี้ช่วยให้คุณมีแหล่งความจริงเพียงแหล่งเดียวและให้วิธีคิดและเหตุผลเกี่ยวกับแอปของคุณง่ายๆ
ในขณะเดียวกัน Redux เป็นรูปแบบที่ช่วยให้คุณจัดการสถานะของคุณ มันขึ้นอยู่กับความคิดที่ว่ารัฐของคุณเป็นแหล่งเดียวของความจริงและควรได้รับการปรับปรุงในลักษณะที่คาดการณ์ได้
Redukks เป็นการรวมกันของแนวคิดทั้งสองนี้ - มันให้คุณเป็นนามธรรมพื้นฐานในการใช้สถาปัตยกรรม Uniflow และ Redux แต่ไม่ได้บังคับให้คุณทำสิ่งต่าง ๆ

การไหลของตัวเองนั้นค่อนข้างง่าย - คุณมีสถานะ X และคุณมีวิธีเปลี่ยนแปลง วิธีการเปลี่ยนแปลงเหล่านั้นเรียกว่า "ตัวลด" และพวกเขาเป็นวิธีเดียวที่จะเปลี่ยนสถานะนั้น ดังนั้นแทนที่จะอัปเดตอย่างดุเดือดจากทุกที่ที่เราโปรดเรากำหนดชุดของ "การอัปเดต" ที่สามารถใช้เพื่ออัปเดตสถานะ
แต่เนื่องจากแอปพลิเคชันส่วนใหญ่มีความซับซ้อนมากกว่าเคาน์เตอร์ง่าย ๆ คุณจึงต้องใช้วิธีจัดการงานที่ซับซ้อนเช่นกัน นั่นคือสิ่งที่การกระทำเข้ามาคุณ "ส่ง" การกระทำและดำเนินการบางอย่างและในระหว่างการดำเนินการนั้นสามารถอัปเดตสถานะผ่านตัวลดหรือเรียกใช้การกระทำอื่น ๆ
สิ่งนี้ทำให้ง่ายต่อการกำหนดและทดสอบการเปลี่ยนรูปแบบที่เป็นไปได้ทั้งหมดของการเปลี่ยนแปลงของรัฐและทำให้ง่ายต่อการให้เหตุผลเกี่ยวกับสถานะของแอปของคุณและมั่นใจว่ามันทำงานตามที่คุณตั้งใจไว้
Gratle Groovy:
dependencies {
implementation ' com.ianrumac.redukks:redukks:0.1.4 '
}Gradle Kotlin:
implementation( " com.ianrumac.redukks:redukks:0.1.4 " ) data class CountState ( val total : Int )
sealed class Updates ( override val reduce : ( CountState ) -> CountState ) : Reducer<CountState> {
class Add ( val number : Int ) : Updates({
state.copy(state.total + number)
})
class Subtract ( val number : Int ) : Updates({
state.copy(state.total - number)
})
}FeatureContext ที่มีการพึ่งพาทั้งหมดสำหรับคุณสมบัติของคุณ (หรือสำหรับการกระทำของคุณ) interface CountContext {
val client : CountingAPI
val store : Store < CountState >
}Actions ที่สามารถดำเนินการในบริบท sealed class Actions ( override val execute : suspend CountContext .() -> Unit ) : TypedAction<CountContext> {
class AddToClient ( val number : Int ) : Actions({
// here, we have access to all the CountContext properties that are captured during execution
// For example, we can access the store and update it with the result of the network call.
val result = client.add(number)
state.update( Updates . Add (result))
})
class SubtractFromClient ( val number : Int ) : Actions({
val result = client.subtract(number)
state.update( Updates . Subtract (result))
})
} data class CountWithAnAPI ( val scope : CoroutineScope ) : CountContext {
override val client = CountingAPI ()
override val store = reducedStore( CountState ( 0 ))
override val handler =
actionHandlerFor(scope, this ) // this is a coroutine scope that will be used to execute the actions
}
val context = CountWithAnAPI (scope)
// In a class that implements Dispatcher<Actions>
// This is actually a call to context.handler.dispatch(Actions.Add(1))
dispatch( Actions . Add ( 1 )) interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : ActionDispatcher < Actions >
data class CountState ( val total : Int )
sealed class Updates ( override val reduce : ( CountState ) -> CountState ) : Reducer<CountState> {
class Add ( val number : Int ) : Updates({
state.copy(state.total + number)
})
class Subtract ( val number : Int ) : Updates({
state.copy(state.total - number)
})
}
sealed class Actions ( override val execute : suspend CountContext .() -> Unit ) : TypedAction<CountContext> {
class AddToClient ( val number : Int ) : Actions({
// here, we have access to all the CountContext properties that are captured during execution
// For example, we can access the store and update it with the result of the network call.
val result = client.add(number)
state.update( Updates . Add (result))
})
class SubtractFromClient ( val number : Int ) : Actions({
val result = client.subtract(number)
state.update( Updates . Subtract (result))
})
}
} val context = CountWithAnAPI (scope)
val handler = context.handler
handler.dispatch( Actions . AddToClient ( 1 ))
context.store.listen().collectLatest { state ->
// do something with the state
} คุณสามารถใช้ Redukks กับ ViewModel ของ Android ได้อย่างง่ายดายและเหมาะกับการแต่งเพลง Jetpack (และกรอบมุมมองด้วย!) เพียงแค่รวบรวมร้านค้าเป็น stateflow หรือใช้กับ Livedata ViewModel ของคุณยังสามารถใช้ Dispatcher<Actions> เพื่อให้คุณสามารถส่งการกระทำต้นน้ำ (มอบหมายให้กับตัวจัดการการกระทำ) นอกจากนี้คุณยังสามารถสร้าง abstractions สำหรับสิ่งนี้เช่น ReduxViewModel ที่จัดการกับสิ่งเหล่านี้ให้คุณ
การใช้กับ ViewModel นั้นง่าย:
@HiltViewModel
class CountViewModel @Inject constructor(
val actionHandler : Dispatcher < CountContext . Actions >,
val store : Store < CountContext . State >
) : ViewModel(), Dispatcher<Actions> by actionHandler {
// Note: Not a real implementation, be careful where you collect and map your state
val state = store.listen().map( this ::mapToViewModel)
.stateIn(viewModelScope, SharingStarted . WhileSubscribed (), initialValue = CountVM ( 0 ))
fun mapToViewModel ( it : CountContext . State ): CountVM {
.. .
}
}และตอนนี้ใน UI ของคุณคุณสามารถทำได้:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))นอกจากนี้คุณยังสามารถควบคุมวงจรชีวิตของสถานะของคุณได้โดยยกขึ้นหรือลงในแผนผังวงจรชีวิต ตัวอย่างเช่นหากคุณต้องการแชร์ร้านค้าของรัฐคุณสามารถเลื่อนขึ้นจาก IE ขอบเขตของส่วนไปยังขอบเขตกิจกรรมและส่งผ่านไปยังชิ้นส่วน ด้วยวิธีนี้คุณสามารถแบ่งปันสถานะระหว่างชิ้นส่วน แต่ยังมีร้านค้าของรัฐเพียงแห่งเดียว คุณสามารถจัดหาร้านค้าหลายแห่งสำหรับการดำเนินการหรือจัดหาร้านค้าหลายแห่งให้กับ UI ของคุณเพื่อรวม
typealias CountStore = Store < CountState >
typealias UserStore = Store < UserState >
interface CountContext {
val countStore : CountStore
val userStore : UserStore
}
class AuthenticatedCounterContext ( userStore : UserStore ) {
override val userStore = userStore
override val countStore = CountStore ( CountState ( 0 ))
}
// ...
class AddForUser : Actions ({
val count = countStore.state.total
countStore.update( Updates . Add (1))
userStore.update( UpdateStateForUser (countStore.total))
})
สิ่งนี้ใช้งานได้อย่างสวยงามด้วย Jetpack Compose เนื่องจากคุณสามารถหา UI จากสถานะได้อย่างง่ายดาย (โปรดแมปกับคลาส ViewModel ก่อนที่จะทำเช่นนั้นอย่าผสมโมเดลหลักและรุ่น UI ของคุณ)

เนื่องจาก Redukks เป็นชุดของ abstractions แบบแยกส่วนคุณสามารถใช้เฉพาะชิ้นส่วนที่คุณต้องการสำหรับระดับความซับซ้อนที่คุณต้องการ Redukks ให้การใช้งานเริ่มต้นเล็กน้อยของ abstractions แต่คุณสามารถสร้างของคุณเองได้อย่างง่ายดาย
เริ่มต้นด้วยเคสที่ง่ายที่สุดร้านค้าที่เรียบง่าย เราจะเริ่มต้นด้วยการใช้งานเคาน์เตอร์อย่างง่ายโดยมีเพียงร้านค้าที่ไม่มีตัวลดหรือการกระทำ โดยทั่วไปหากคุณไม่ต้องการเขียนตัวลดคุณสามารถใช้คลาส BasicStore ซึ่งเป็นร้านค้าที่เรียบง่ายที่เปิดเผยฟังก์ชั่น update ด้วยการปิดที่ใช้ในสถานะปัจจุบันและส่งคืนสถานะใหม่
data class CountState ( val total : Int )
val store = BasicStore ( CountState ( 0 ))
store.update {
it.copy(it.total + 1 )
}
println (store.state.total) // prints 1
store.listen().collectLatest { state ->
// do something with the state
} ตอนนี้เรายังสามารถใช้ตัวลดเพื่อกำหนดการอัปเดตสถานะในแบบที่ต้องการ ดังนั้นเรามาสร้างตัวลด ตัวลดเป็นเพียงวัตถุที่ใช้อินเตอร์เฟส Reducer<StateType> ซึ่งหมายความว่าพวกเขามีการ reduce การปิดที่ใช้ในสถานะปัจจุบันและส่งคืนสถานะใหม่ มันเหมือนกับการเขียน
fun addNumber ( state : CountState , number : Int ) : CountState {
return state.copy(state.total + number)
}แต่ด้วยความปลอดภัยที่เพิ่มขึ้นเล็กน้อยเพื่อให้คุณมั่นใจได้ว่าการเปลี่ยนแปลงสถานะที่เป็นไปได้ทั้งหมดจะถูกกำหนดไว้ล่วงหน้าและทดสอบ
sealed class Updates ( override val reduce : ( CountState ) -> CountState ) : Reducer<CountState> {
class Add ( val number : Int ) : Updates({
state.copy(state.total + number)
})
class Subtract ( val number : Int ) : Updates({
state.copy(state.total - number)
})
} ตอนนี้ในการใช้ตัวลดนี้เราจำเป็นต้องสร้าง ReducedStore<StateType, ReducerType> นี่คือร้านค้าที่สามารถอัปเดตผ่านประเภทตัวลดที่กำหนดไว้เท่านั้น การใช้งานเริ่มต้นนั้นมีให้เป็นพื้นฐานที่ได้รับการปรับปรุง แต่คุณสามารถสร้างของคุณเองได้อย่างง่ายดาย
val basicStore = BasicStore ( CountState ( 0 ))
// you can either provide it with a store or the initial state to use a BasicStore
val store = BasicReducedStore < CountState , Updates >(basicStore)
// or use the helper function
val store = createReducedStore< CountState , Updates >( CountState ( 0 ))
store.update( Updates . Add ( 1 ))
store.listen().collectLatest { state ->
// do something with the state
}การกระทำเป็นส่วนหนึ่งของรหัสแบบอะซิงโครนัสที่สามารถดำเนินการได้ โดยปกติแล้วพวกเขาจะใช้เพื่อทำผลข้างเคียงเช่นการโทรเครือข่ายหรือเพื่อส่งการกระทำและการอัปเดตอื่น ๆ ตัวอย่างเช่นการดึงรายการของรายการจากเซิร์ฟเวอร์จากนั้นส่งการดำเนินการเพื่ออัปเดตสถานะด้วยผลลัพธ์
การกระทำมีความซับซ้อนมากขึ้นเล็กน้อยเนื่องจากพวกเขามักจะต้องสามารถเข้าถึงการพึ่งพาได้ ในการทำเช่นนี้เราได้จัดเตรียม <Context> ที่มีการพึ่งพาทั้งหมดจากนั้นสร้างการกระทำที่ดำเนินการกับบริบทในฐานะผู้รับซึ่งหมายความว่าพวกเขาสามารถเข้าถึงได้เช่น this ด้วยสิ่งนี้เราสามารถส่งพวกเขาไปยัง Dispatcher<ActionType> ซึ่งควรจัดการกับการกระทำ
ตัวจัดการการกระทำที่ใช้ coroutine พื้นฐานถูกนำมาใช้โดยค่าเริ่มต้นภายใต้ TypedActionHandler และตัวเลือกที่กำหนดเองสามารถนำไปใช้งานได้ง่ายโดยการจัดหา Dispatcher<ActionType> หรือ AsyncDispatcher<ActionType> สำหรับการควบคุมการดำเนินการเพิ่มเติม (ผ่านการเลื่อนออก) นอกจากนี้ยังมีคลาส AsyncAction ที่สามารถใช้เพื่อผ่านการปิดผ่านตัวสร้าง
ทีนี้มาดูกันว่าจะกำหนดการกระทำของ Async ได้อย่างไร
อันดับแรกเราต้องมีบริบทที่สามารถดำเนินการการกระทำเหล่านี้ได้
ในขณะที่สำหรับกรณีง่ายๆ (เช่นเพียงแค่อัปเดตร้านค้า) คุณสามารถใช้ Store<StateType> ได้ดีกว่าที่จะสร้างบริบทที่กำหนดเอง สิ่งนี้ช่วยให้คุณมีวิธีที่ง่ายในการเข้าถึงการพึ่งพาเยาะเย้ยพวกเขาแทนที่พวกเขา ฯลฯ นอกจากนี้ยังเป็นวิธีปฏิบัติที่ดีในการทำให้การกระทำเข้ากันอย่างแน่นหนากับพวกเขาเพื่อให้คุณสามารถเห็นการกระทำที่ต้องการสิ่งที่การพึ่งพา
ด้วยการรับบริบทของ Kotlin ระหว่างทางรหัสสามารถทำความสะอาดได้ในอนาคตลบความต้องการการกระทำโดยสิ้นเชิงและเพียงแค่ใช้ฟังก์ชั่นง่าย ๆ บนผู้รับ แต่เนื่องจากรหัสฐานส่วนใหญ่ยังไม่มีตัวรับบริบทเราจะมุ่งเน้นไปที่การใช้งานนี้ในตอนนี้เท่านั้น
ก่อนอื่นเรากำหนดอินเทอร์เฟซ CountContext ที่มีการอ้างอิงที่เราต้องการ:
interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : Dispatcher < Actions >
} จากนั้นเราสามารถกำหนดการกระทำผ่านคลาสที่ปิดผนึกซึ่งใช้ TypedAction<CountContext> :
sealed class Actions ( override val execute : suspend CountContext .() -> Unit ) : TypedAction<CountContext> {
class Add ( val number : Int ) : Actions({
// here, we have access to all the CountContext properties that are captured during execution
// For example, we can access the store and update it with the result of the network call.
val result = client.add(number)
state.update( Updates . UpdateState (result))
})
class Subtract ( val number : Int ) : Actions({
val result = client.subtract(number)
state.update( Updates . UpdateState (result))
})
}ตอนนี้เราสามารถใช้บริบทและใช้ dispatcher เพื่อดำเนินการ:
data class CountWithAnAPI ( val scope : CoroutineScope ) : CountContext {
override val client = CountingAPI ()
override val store = BasicReducedStore < CountState , Updates >( CountState ( 0 ))
override val handler = createActionHandler(scope, this )
}
// In a class that implements Dispatcher<Actions>, like a viewmodel that delegates it to the handler
// This is actually a call to context.handler.dispatch(Actions.Add(1))
dispatch( Actions . Add ( 1 ))
dispatch( Actions . Subtract ( 1 ))
การทดสอบค่อนข้างง่ายสำหรับทั้งตัวลดและการกระทำ สำหรับตัวลดคุณสามารถสร้างสถานะและตัวลดเช่น val state = CountState(0) และ val reducer = Updates.Add(1) จากนั้นโทร reducer.reduce(state) ตอนนี้คุณสามารถยืนยันได้ว่าผลลัพธ์คือสิ่งที่คาดหวัง
@Test
fun `update the total count by adding 1` () {
val state = CountState ( 0 )
val reducer = Updates . Add ( 1 )
val result = reducer.reduce(state)
assertEquals( 1 , result.total)
} สำหรับการกระทำคุณสามารถสร้างบริบทและตัวแจกจ่ายเช่น val context = CountWithAnAPI(TestCoroutineScope()) และ val action = Action.Add(1) จากนั้นเรียกใช้ action.execute(context) สิ่งนี้ให้วิธีการทดสอบการกระทำที่ง่ายเนื่องจากคุณสามารถเยาะเย้ยบริบทและยืนยันได้ว่าการกระทำนั้นถูกดำเนินการตามที่คาดไว้หรือใช้ไลบรารีเยาะเย้ยเช่น Mockk เพื่อตรวจสอบการจำลองที่ดำเนินการอย่างถูกต้อง
@Test
fun `update the total count and add from client` () = runTest {
val context = CountWithAnAPI ( this )
val action = Actions . Add ( 1 )
action.execute(context)
assertEquals( 1 , context.store.state.total)
}แต่ละองค์ประกอบจะถูกเขียนในลักษณะที่ช่วยให้สามารถปรับแต่งได้ง่ายเพื่อให้คุณสามารถสร้างของคุณเองได้อย่างง่ายดาย ความหมายโดยการใช้ร้านค้าหรือ dispatcher คุณสามารถมีสิ่งต่าง ๆ เช่น API/ซ็อกเก็ต/ร้านค้าที่ได้รับการสนับสนุนหรือการกระทำที่ส่งสัญญาณเมื่อเสร็จแล้วหรือกระแสอื่น ๆ ในร้านของคุณ อย่ากลัวที่จะปรับแต่งรูปแบบตามความต้องการของคุณ - เราไม่ได้เขียนโค้ดเพื่อตอบสนองรูปแบบเรากำลังเขียนเพื่อตัวเราเองเพื่อนร่วมทีมของเราและผู้ใช้ของเรา
อย่าลังเลที่จะเปิดปัญหาหรือการประชาสัมพันธ์หากคุณมีคำแนะนำหรือการปรับปรุงใด ๆ