Deperated.
A Kotlin DSL to bind Android UI components to your app state.
// TODO
You can create a KiteDslScope
from either Activity or Fragment via extension function kiteDsl
:
In Actvity
:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
kiteDsl {
// Write Kite DSL here
}
}
In Fragment
:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
kiteDsl {
// Write Kite DSL here
}
}
In the KiteDslScope
, you can create state via extension function state
:
kiteDsl {
// Create a state with initial value
val count = state { 0 }
// Assign a new value
count.value = 3
// Read the value
println(count.value)
}
When you create kiteDsl
inside Activity/Fragment
, all declared states will be saved into an Android ViewModel
.
So states can survive after Activity/Fragment
recreation.
You can create a action that will rerun when its dependent states changed via extension function subscribe
:
val count = state { 0 }
// When click the button, textView.text will change.
subscribe {
textView.text = count.value.toString()
}
button.setOnClickListener {
count.value++
}
You can subscribe to more than one state:
val count1 = state { 0 }
val count2 = state { 0 }
// Whether count1 or count2 changed, the textView.text will change.
subscribe {
textView.text = (count1.value + count2.value).toString()
}
The action will only rerun when its referenced state changed:
val count1 = state { 0 }
val count2 = state { 0 }
// Will only rerun when count1 changed
subscribe {
textView1.text = count1.value.toString()
}
// Will only rerun when count2 changed
subscribe {
textView2.text = count2.value.toString()
}
KiteDslScope
implemented CoroutineScope
, so you can use coroutine inside KiteDslScope
:
val count = state { 0 }
button.setOnClickListener {
launch {
delay(1000)
count1.value++
}
}
You can set some contextual value inside KiteDslScope
and then get them later:
fun KiteDslScope.setCount() {
val count = state { 0 }
set("count", count)
}
fun KiteDslScope.getCount() {
setCount()
val count = get<KiteMutableState<Int>>("count") // nullable
val count = require<KiteMutableState<Int>>("count") // non null
}
You can construct a KiteScopeModelFactory
and add any dependencies needed into it via addService
:
@Provide
fun provideKiteScopeModelFactory(repository: Repository): KiteScopeModelFactory {
return KiteScopeModelFactory().apply {
addService(repository)
}
}
Then initialize kiteDsl
with injected KiteScopeModelFactory
.
Now all services added into KiteScopeModelFactory
will set into KiteDslScope
as contextual value:
@Inject
lateinit var scopeModelFactory: KiteScopeModelFactory
kiteDsl(scopeModelFactory = scopeModelFactory) {
val repository = requireByType<Repository>()
}
It's better not to directly write Kite DSL inside Activity/Fragment
.
Instead, separate your business logic and UI binding into separate extension functions of KiteDslScope
:
data class CounterUseCase(
val count: KiteState<Int>,
val increment: () -> Unit,
val decrement: () -> Unit
)
fun KiteDslScope.counterUseCase(): CounterUseCase {
val count = state { 0 }
return CounterUseCase(
count = count,
increment = { count.value++ },
decrement = { count.value-- }
)
}
fun KiteDslScope.bindCounter(counterUseCase: CounterUseCase) {
val binding = requireByType<FragmentCounterBinding>()
subscribe {
binding.textView.text = counterUseCase.count.toString()
}
binding.incrementButton.setOnClickListener {
counterUseCase.increment.invoke()
}
binding.decrementButton.setOnClickListener {
counterUseCase.decrement.invoke()
}
}
Now you can test business logic via runTestKiteDsl
:
@Test
fun testCounter() = runTestKiteDsl {
val counter = counterUseCase()
assert(counter.count.value == 0)
counter.increment.invoke()
assert(counter.count.value == 1)
}
Test UI with TestKiteActivity
or TestKiteFragment
:
@Test
fun testDisplayCount() = runTestKiteDsl {
val factory = TestKiteFragment.makeFactory(
R.layout.fragment_counter,
TestKiteFragment.Config {
setByType(FragmentCounterBinding.bind(it.requireView()))
val count = state { 3 }
val counter = CounterUseCase(
count = count,
increment = {},
decrement = {}
)
bindCounter(counter)
}
)
launchFragmentInContainer<TestKiteFragment>(factory = factory)
.moveToState(Lifecycle.State.RESUMED)
Espresso.onView(ViewMatchers.withText("3"))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}