Perpustakaan ini ada di Alpha. API sebagian besar lengkap dan tidak akan berubah, tetapi masih ada pekerjaan yang harus dilakukan untuk membuat produksi siap di semua platform. Untuk JVM dan multiplatform, perpustakaan siap untuk digunakan produksi, tetapi untuk iOS, JS dan asli masih belum sepenuhnya diatur. Jika Anda ingin membantu, jangan ragu untuk membuka masalah atau PR.
Redukks adalah serangkaian abstraksi jenis-aman sederhana yang diperlukan untuk mengimplementasikan arsitektur uniflow dan redux seperti pada multiplatform Kotlin. Ini menyederhanakan pembuatan toko, pereduksi dan tindakan, dan memberikan cara sederhana untuk mengujinya.
Meskipun ada banyak implementasi uniflow atau redux untuk multiplatform Kotlin/Kotlin, kebanyakan dari mereka terlalu kompleks, atau terlalu sederhana. Entah penamaan itu terlalu "reduxy", mereka memaksa Anda ke dalam satu cara melakukan sesuatu atau abstraksi terlalu rumit untuk ditulis secara teratur. Dan sementara ada arsitektur yang sesuai dengan gaya ini, penamaan yang mereka gunakan cukup spesifik untuk Android dan membingungkan bagi pengembang non-android, membuatnya lebih sulit untuk bernalar atau berdiskusi dengan pengembang lain.
Karena saya mendapati diri saya menggunakan kembali serangkaian abstraksi yang sama selama bertahun-tahun, saya telah memutuskan untuk membuatnya menjadi perpustakaan yang dapat saya gunakan kembali dan bagikan.
Tujuannya adalah untuk memberikan abstraksi yang sederhana, aman, dan dapat diuji untuk arsitektur seperti redux, sementara masih belum mengikat Anda sepenuhnya ke satu pola tunggal, memberikan Anda abstraksi yang dapat membantu Anda membangun solusi Anda sendiri. Sementara sebagian besar perpustakaan mencoba mendorong pola redux penuh, peredam dapat menjadi overhead untuk beberapa kasus sederhana, dan Anda harus dapat menghindarinya jika Anda mau - itulah sebabnya redukks tidak menegakkan pola redux itu sendiri.
Untuk lebih banyak alasan mengapa Uniflow dan mengapa Redux, Anda dapat memeriksa arsitektur pembuatan Talk (DE).
Manajemen Negara : Redukks menyediakan cara sederhana untuk mengelola negara Anda, memperbarui dengan cara yang dapat diprediksi dan mendengarkan perubahan. Anda dapat menggunakannya untuk mengelola keadaan seluruh aplikasi Anda, satu fitur, memiliki keadaan bersama antara berbagai fitur (layar) atau memiliki banyak status untuk satu layar.
Penanganan Tindakan : Redukks menyediakan cara sederhana untuk menangani tindakan, dan melaksanakannya dengan cara yang dapat diprediksi. Anda dapat menggunakannya untuk menangani tindakan dari UI, panggilan jaringan, atau pekerjaan lain yang mungkin Anda butuhkan. Sifat perpustakaan yang tidak terikat memungkinkan Anda untuk mengimplementasikan penangan tindakan Anda sendiri khusus untuk usecase Anda - bahkan memungkinkan Anda memecahkan pola uniflow dengan menerapkan pensinyalan mundur.
Pengujian : Dengan reduks, menguji keadaan Anda, pereduksi dan tindakan sangat sederhana - baik karena sifat API dan keamanan kompilasi tambahan. Anda dapat dengan mudah menguji logika manajemen negara Anda, atau logika penanganan tindakan Anda tanpa perlu pengujian API yang kompleks.
Arsitektur uniflow didasarkan pada satu ide sederhana - aplikasi Anda adalah siklus data yang konstan yang mengalir dalam satu arah. Negara dan perubahannya didorong oleh UI, dan UI didorong oleh negara. Ini memungkinkan Anda untuk memiliki satu sumber kebenaran dan memberi Anda cara sederhana untuk berpikir dan beralasan tentang aplikasi Anda.
Sementara itu, Redux adalah pola yang membantu Anda mengelola keadaan Anda. Ini didasarkan pada gagasan bahwa negara Anda adalah satu sumber kebenaran, dan itu harus diperbarui dengan cara yang dapat diprediksi.
Redukks adalah kombinasi dari dua ide ini - ini memberi Anda abstraksi dasar untuk mengimplementasikan arsitektur berbasis uniflow dan redux tetapi tidak memaksa Anda ke dalam satu cara dalam melakukan sesuatu.

Aliran itu sendiri cukup sederhana - Anda memiliki status X dan Anda memiliki N cara untuk mengubahnya. Cara -cara untuk mengubahnya disebut "reduksi" dan mereka adalah satu -satunya cara untuk mengubah keadaan itu. Jadi, alih -alih memperbarui dengan liar dari mana pun kami tolong, kami menentukan satu set "pembaruan" yang dapat digunakan untuk memperbarui negara.
Tetapi, karena sebagian besar aplikasi lebih kompleks daripada penghitung sederhana, Anda membutuhkan cara untuk menangani pekerjaan yang kompleks juga. Di situlah tindakan masuk. Anda "mengirim" suatu tindakan dan itu mengeksekusi beberapa pekerjaan, dan selama eksekusi itu, itu dapat memperbarui negara melalui peredam atau memohon tindakan lain.
Ini membuatnya mudah untuk mendefinisikan dan menguji semua permutasi perubahan negara yang mungkin, dan memudahkan untuk beralasan tentang keadaan aplikasi Anda dan yakin itu berhasil seperti yang Anda maksudkan.
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 yang berisi semua dependensi untuk fitur Anda (atau untuk tindakan Anda). interface CountContext {
val client : CountingAPI
val store : Store < CountState >
}Actions yang dapat dieksekusi pada konteks 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
} Anda dapat dengan mudah menggunakan redukks dengan ViewModel Android dan sangat cocok dengan komposisi Jetpack (dan kerangka tampilan juga!). Cukup kumpulkan toko sebagai aliran negara atau gunakan dengan livedata. ViewModel Anda juga dapat mengimplementasikan Dispatcher<Actions> sehingga Anda dapat mengirim tindakan ke hulu (mendelegasikannya ke penangan tindakan). Anda juga dapat membuat abstraksi untuk ini seperti ReduxViewModel yang menangani hal -hal ini untuk Anda.
Menggunakannya dengan viewmodel sederhana:
@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 {
.. .
}
}Dan sekarang di UI Anda, Anda bisa melakukannya:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))Anda juga dapat mengontrol siklus hidup negara Anda dengan mengangkatnya ke atas atau ke bawah di pohon lingkup siklus hidup. Misalnya, jika Anda perlu berbagi toko negara, Anda bisa memindahkannya dari IE lingkup fragmen ke ruang lingkup aktivitas dan meneruskannya ke bawah ke fragmen. Dengan cara ini, Anda dapat berbagi keadaan di antara fragmen, tetapi masih hanya memiliki satu toko negara. Anda bahkan dapat menyediakan beberapa toko untuk suatu tindakan, atau menyediakan beberapa toko untuk digabungkan UI Anda.
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))
})
Ini bekerja dengan indah dengan komposisi Jetpack, karena Anda dapat dengan mudah mendapatkan UI dari negara (tolong, peta ke kelas ViewModel sebelum melakukannya, jangan mencampur model inti dan model UI Anda).

Karena Redukks adalah serangkaian abstraksi modular, Anda hanya dapat menggunakan bagian yang Anda butuhkan untuk tingkat kompleksitas yang Anda inginkan. Redukks menyediakan beberapa implementasi default dari abstraksi, tetapi Anda dapat dengan mudah membuat sendiri.
Mari kita mulai dengan kasing paling sederhana, toko sederhana. Kami akan mulai dengan implementasi penghitung sederhana, dengan hanya toko tanpa peredam atau tindakan. Secara umum, jika Anda lebih suka tidak menulis peredam, Anda dapat menggunakan kelas BasicStore , yang merupakan toko sederhana yang memperlihatkan fungsi update dengan penutupan yang mengambil keadaan saat ini dan mengembalikan keadaan baru.
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
} Sekarang, kami juga dapat menggunakan peredam untuk mendefinisikan pembaruan negara dengan cara yang aman. Jadi mari kita buat peredam. Reducers hanyalah objek yang mengimplementasikan antarmuka Reducer<StateType> , yang berarti mereka memiliki reduce penutupan yang mengambil keadaan saat ini dan mengembalikan keadaan baru. Ini seperti menulis
fun addNumber ( state : CountState , number : Int ) : CountState {
return state.copy(state.total + number)
}Tetapi dengan sedikit lebih banyak keamanan jenis, sehingga Anda dapat memastikan semua perubahan keadaan yang mungkin telah ditentukan dan diuji.
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)
})
} Sekarang, untuk menggunakan peredam ini, kita perlu membuat toko ReducedStore<StateType, ReducerType> . Ini adalah toko yang hanya dapat diperbarui melalui jenis peredam yang ditentukan. Implementasi default disediakan sebagai BasicReducedStore, tetapi Anda dapat dengan mudah membuat sendiri.
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
}Tindakan adalah potongan kode yang tidak sinkron yang dapat dieksekusi. Biasanya mereka digunakan untuk melakukan efek samping, seperti panggilan jaringan, atau mengirimkan tindakan dan pembaruan lainnya. Misalnya, mengambil daftar item dari server, dan kemudian mengirim tindakan untuk memperbarui status dengan hasilnya.
Tindakan sedikit lebih rumit, karena mereka biasanya harus dapat mengakses dependensi. Untuk melakukan ini, kami menyediakan <Context> yang berisi semua dependensi, dan kemudian membuat tindakan yang dieksekusi dengan konteks sebagai penerima, yang berarti mereka memiliki akses ke this . Dengan ini, kita dapat mengirimnya ke Dispatcher<ActionType> , yang seharusnya menangani tindakan.
Penangan tindakan berbasis coroutine dasar diimplementasikan secara default di bawah TypedActionHandler dan yang kustom dapat dengan mudah diimplementasikan dengan memberikan Dispatcher<ActionType> atau AsyncDispatcher<ActionType> untuk kontrol eksekusi lebih lanjut (melalui ditangguhkan). Juga, yang disediakan adalah kelas AsyncAction yang dapat digunakan untuk hanya melewati penutupan melalui konstruktor.
Sekarang, mari kita lihat bagaimana mendefinisikan tindakan async.
Pertama, kita membutuhkan konteks bahwa tindakan ini dapat dieksekusi.
Sedangkan untuk kasus sederhana (yaitu hanya memperbarui toko) Anda dapat menggunakan Store<StateType> , lebih baik membuat konteks khusus. Ini memberi Anda cara mudah untuk mengakses ketergantungan, mengejek mereka, menggantinya, dll. Juga, ini adalah praktik yang baik untuk menjaga tindakan tersebut secara erat digabungkan dengan mereka, sehingga Anda dapat dengan mudah melihat tindakan apa yang membutuhkan dependensi apa.
Dengan penerima konteks Kotlin di jalan, kode dapat bahkan lebih bersih di masa depan, menghapus kebutuhan akan tindakan sepenuhnya dan hanya menerapkan fungsi sederhana pada penerima. Tetapi karena sebagian besar basis kode belum memiliki penerima konteks, kami hanya akan fokus pada implementasi ini untuk saat ini.
Pertama, kami mendefinisikan antarmuka CountContext yang berisi dependensi yang kami butuhkan:
interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : Dispatcher < Actions >
} Kemudian, kita dapat mendefinisikan tindakan melalui kelas -kelas tertutup yang menerapkan 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))
})
}Sekarang, kita dapat mengimplementasikan konteks dan menggunakan dispatcher untuk menjalankan tindakan:
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 ))
Pengujian cukup mudah, untuk pereduksi dan tindakan. Untuk reduksi, Anda dapat dengan mudah membuat keadaan dan peredam, yaitu val state = CountState(0) dan val reducer = Updates.Add(1) , dan kemudian hubungi reducer.reduce(state) . Sekarang Anda dapat menyatakan bahwa hasilnya adalah yang diharapkan.
@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)
} Untuk tindakan, Anda dapat membuat konteks dan dispatcher, yaitu val context = CountWithAnAPI(TestCoroutineScope()) dan val action = Action.Add(1) , dan kemudian hubungi action.execute(context) . Ini memberi Anda cara mudah untuk menguji tindakan, karena Anda dapat dengan mudah mengejek konteks dan menyatakan bahwa tindakan tersebut dieksekusi seperti yang diharapkan atau menggunakan pustaka mengejek seperti Mockk untuk memverifikasi tiruan yang dieksekusi dengan benar.
@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)
}Setiap komponen ditulis dengan cara yang memungkinkan penyesuaian mudah, sehingga Anda dapat dengan mudah membuat sendiri. Artinya dengan mengimplementasikan toko atau dispatcher, Anda dapat dengan mudah memiliki hal -hal seperti API/Socket/Room Backed Store, atau tindakan yang memberi sinyal ketika selesai, atau aliran lain di toko Anda. Jangan takut untuk menyesuaikan pola dengan kebutuhan Anda - kami tidak menulis kode untuk memenuhi polanya, kami menulisnya untuk diri kami sendiri, rekan tim kami dan pengguna kami.
Jangan ragu untuk membuka masalah atau PR jika Anda memiliki saran atau perbaikan.