هذه المكتبة في ألفا. API مكتمل في الغالب ولن يتغير ، ولكن لا يزال هناك عمل يتعين القيام به لجعل الإنتاج جاهزًا على جميع المنصات. بالنسبة إلى JVM و Multiplatform ، تكون المكتبة جاهزة للاستخدام في الإنتاج ، ولكن بالنسبة إلى iOS و JS و Native لا تزال غير مصممة تمامًا. إذا كنت ترغب في المساعدة ، فلا تتردد في فتح مشكلة أو علاقات عامة.
Redukks هي مجموعة من التجريدات البسيطة التي آمنت الأنواع اللازمة لتنفيذ الهندسة المعمارية التي تشبه uniflow و Redux على kotlin multiplatform. إنه يبسط إنشاء المتاجر والمخفضات والإجراءات ، ويوفر طريقة بسيطة لاختبارها.
في حين أن هناك العديد من تطبيقات Uniflow أو Redux لـ Kotlin/Kotlin Multiplatform ، فإن معظمها إما معقدة للغاية أو بسيطة للغاية. إما أن تسمية "reduxy" للغاية ، فإنها تجبرك على طريقة واحدة لفعل الأشياء أو أن التجريدات معقدة للغاية بحيث لا يمكن كتابتها بشكل منتظم. وعلى الرغم من أن هناك بنيات تتوافق مع هذا النمط ، إلا أن التسمية التي يستخدمونها محددة تمامًا لنظام Android ومربكة للمطورين غير الأندرويديين ، مما يجعل من الصعب التفكير في المطورين الآخرين أو مناقشتهم.
نظرًا لأنني وجدت نفسي أعيد استخدام نفس مجموعة التجريد على مر السنين ، فقد قررت أن أجعلها في مكتبة يمكنني إعادة استخدامها ومشاركتها.
الهدف من ذلك هو توفير التجريدات البسيطة ، الآمنة ، والاختبار للهندسة المعمارية التي تشبه Redux ، في حين لا تزال لا تربطك تمامًا بنمط واحد ، مما يوفر لك التجريدات التي يمكن أن تساعدك في بناء الحل الخاص بك. على الرغم من أن معظم المكتبات تحاول الضغط من أجل نمط Redux بالكامل ، إلا أن المخفضات يمكن أن تكون نفقات عامة لبعض الحالات البسيطة ، ويجب أن تكون قادرًا على تجنبها إذا كنت ترغب في ذلك - ولهذا السبب لا تنفذ Redukks نمط Redux نفسه.
لمزيد من التفكير حول سبب Uniflow ولماذا Redux ، يمكنك الاطلاع على الهندسة المعمارية (DE).
إدارة الدولة : توفر Redukks طريقة بسيطة لإدارة حالتك وتحديثها بطريقة يمكن التنبؤ بها والاستماع إلى التغييرات. يمكنك استخدامه لإدارة حالة التطبيق بالكامل ، أو ميزة واحدة ، أو لديك حالة مشتركة بين ميزات مختلفة (شاشات) أو لديك حالات متعددة لشاشة واحدة.
معالجة الإجراءات : توفر Redukks طريقة بسيطة للتعامل مع الإجراءات وتنفيذها بطريقة يمكن التنبؤ بها. يمكنك استخدامه للتعامل مع الإجراءات من واجهة المستخدم أو مكالمات الشبكة أو أي عمل آخر قد تحتاجه. تتيح لك الطبيعة غير المألوفة للمكتبة تنفيذ معالجات الإجراءات الخاصة بك الخاصة بـ USECASE - حتى تتيح لك كسر نمط Uniflow من خلال تنفيذ الإشارة إلى الوراء.
الاختبار : مع Redukks ، واختبار حالتك ، فإن المخفضات والإجراءات بسيطة بشكل لا يصدق - سواء بسبب طبيعة واجهة برمجة التطبيقات وسلامة التجميع الإضافية. يمكنك بسهولة اختبار منطق إدارة الولاية الخاص بك ، أو منطق التعامل مع الإجراء دون الحاجة إلى اختبار API المعقد.
تعتمد بنية Uniflow على فكرة واحدة بسيطة - تطبيقك هو دورة مستمرة من البيانات التي تتدفق في اتجاه واحد. الدولة والتغييرات مدفوعة من قبل واجهة المستخدم ، ووزارة واجهة المستخدم مدفوعة بالدولة. يتيح لك ذلك الحصول على مصدر واحد للحقيقة ويوفر لك طريقة بسيطة للتفكير والعقل تجاه تطبيقك.
وفي الوقت نفسه ، Redux هو نمط يساعدك على إدارة حالتك. إنها تستند إلى فكرة أن ولايتك مصدر واحد للحقيقة ، وأنه يجب تحديثه بطريقة يمكن التنبؤ بها.
Redukks عبارة عن مزيج من هاتين الفكرتين - فهو يوفر لك التجريدات الأساسية لتنفيذ بنيات UNIFLOW و REDUX ولكنها لا تجبرك على طريقة واحدة لفعل الأشياء.

التدفق نفسه بسيط للغاية - لديك حالة X ولديك طرق لتغييره. وتسمى تلك الطرق لتغييرها "المخفضات" وهي الطريقة الوحيدة لتغيير تلك الحالة. لذا ، بدلاً من تحديثه بعنف من أي مكان من أين نرغب ، نحدد مجموعة من "التحديثات" التي يمكن استخدامها لتحديث الحالة.
ولكن نظرًا لأن معظم التطبيقات أكثر تعقيدًا من عداد بسيط ، فأنت بحاجة إلى طريقة للتعامل مع العمل المعقد أيضًا. هذا هو المكان الذي تأتي فيه الإجراءات. يمكنك "إرسال" إجراء وتنفيذ بعض الأعمال ، وخلال هذا التنفيذ ، يمكنها تحديث الحالة عبر المخفضات أو استدعاء إجراءات أخرى.
هذا يجعل من السهل تحديد واختبار جميع التباديل الممكنة لتغييرات الدولة ، ويجعل من السهل التفكير في حالة تطبيقك وأن تكون واثقًا من أنه يعمل على كيفية تقصدك.
Gradle 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 مع Android's ViewModel وهو يتناسب مع Jetpack Compose (وإطار العرض أيضًا!). ما عليك سوى جمع المتجر باعتباره تدفقًا في حالة أو استخدامه مع Livedata. يمكن لـ ViewModel أيضًا تنفيذ Dispatcher<Actions> حتى تتمكن من إرسال الإجراءات المنبع (تفويضها إلى معالج الإجراء). يمكنك أيضًا إنشاء تجريدات لهذا مثل 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 {
.. .
}
}والآن في واجهة المستخدم الخاصة بك يمكنك ببساطة القيام به:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))يمكنك أيضًا التحكم في دورة حياة ولايتك من خلال رفعها لأعلى أو لأسفل في شجرة نطاق دورة الحياة. على سبيل المثال ، إذا كنت بحاجة إلى مشاركة متجر حكومي ، فيمكنك فقط تحريكه من نطاق جزء من نطاق الشظية إلى نطاق النشاط ونقله إلى الشظايا. وبهذه الطريقة ، يمكنك مشاركة الحالة بين الشظايا ، ولكن لا يزال لديك متجر واحد فقط. يمكنك حتى توفير متاجر متعددة لإجراء ما ، أو توفير متاجر متعددة لواجهة واجهة المستخدم الخاصة بك للجمع.
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 ، حيث يمكنك بسهولة استخلاص واجهة المستخدم من الحالة (من فضلك ، قم بتخطيطها إلى فئة ViewModel قبل القيام بذلك ، لا تخلط بين النماذج الأساسية ونماذج واجهة المستخدم الخاصة بك).

نظرًا لأن Redukks عبارة عن مجموعة معيارية من التجريدات ، يمكنك استخدام الأجزاء التي تحتاجها فقط لمستوى التعقيد الذي تريده. يوفر Redukks بعض التطبيقات الافتراضية للتجريدات ، ولكن يمكنك بسهولة إنشاء خاص بك.
لنبدأ بأبسط الحالة ، وهو متجر بسيط. سنبدأ بتنفيذ مضاد بسيط ، مع متجر فقط بدون مخفضات أو إجراءات. بشكل عام ، إذا كنت تفضل عدم كتابة مخفضات ، فيمكنك استخدام فئة 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
} الآن ، يمكننا أيضًا استخدام مخفض لتحديد تحديثات الحالة بطريقة typesafe. لذلك دعونا ننشئ مخفضًا. المخفضات هي مجرد كائنات تنفذ واجهة 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> . هذا متجر لا يمكن تحديثه إلا عبر نوع المخفض المحدد. يتم توفير التنفيذ الافتراضي باعتباره BasicreDucedStore ، ولكن يمكنك بسهولة إنشاء خاص بك.
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))
})
}الآن ، يمكننا تنفيذ السياق واستخدام المرسل لتنفيذ الإجراءات:
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)
}تتم كتابة كل مكون بطريقة تسمح بتخصيص سهل ، بحيث يمكنك بسهولة إنشاء خاص بك. بمعنى تنفيذ متجر أو مرسل ، يمكنك بسهولة الحصول على أشياء مثل متجر API/Socket/Room ، أو الإجراءات التي تشير عند الانتهاء منها ، أو تدفقات أخرى في متجرك. لا تخف من تخصيص النمط لتلبية احتياجاتك - نحن لا نكتب رمزًا لتلبية الأنماط ، فنحن نكتبه لأنفسنا وزملائنا في الفريق ومستخدمينا.
لا تتردد في فتح مشكلة أو العلاقات العامة إذا كان لديك أي اقتراحات أو تحسينات.