Ordinary ViewModels might be implemented like this. But those implementation are not clear, for example which are states and which are dependencies and so on.
class CounterViewModel : ViewModel() {
val count: Flow<String>
get() = _count.map { it.toString() }
val isCountDownEnabled: Flow<Boolean>
get() = _isCountDownEnabled
private val _count = MutableStateFlow(0)
private val _isCountDownEnabled = MutableStateFlow(false)
fun countUp() {
_count.value = _count.value + 1
_isCountDownEnabled.value = _count.value > 0
}
fun countDown() {
_count.value = _count.value - 1
_isCountDownEnabled.value = _count.value > 0
}
}
Unio is kProperty based Unidirectional Input / Output framework that works with Flow.
The rule of Input is having MutableSharedFlow properties that are defined internal (or public) scope.
class CounterUnioInput : Unio.Input {
val countUp = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val countDown = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
}
Properties of Input are defined internal (or public) scope.
But these can only access InputProxy#getLambda
via kProperty if Input is wrapped with InputProxy
.
val input: InputProxy<CounterUnioInput>
input.getLambda(CounterUnioInput::countUp).invoke() // accesses `MutableSharedFlow#tryEmit`
The rule of Output is having MutableStateFlow properties that are defined internal (or public) scope.
class CounterUnioOutput(
val count: Flow<String>,
val isCountDownEnabled: MutableStateFlow<Boolean>,
) : Unio.Output
Properties of Output are defined internal (or public) scope.
But these can only access Flow
(or StateFlow) via kProperty if Output is wrapped with OutputProxy
.
val output: OutputProxy<CounterUnioOutput>
output.getFlow(CounterUnioOutput::count)
.onEach { Log.d("UNIO_DEBUG", it.toString()) })
output.getFlow(CounterUnioOutput::isCountDownEnabled)
.onEach { Log.d("UNIO_DEBUG", it.toString()) })
If a property is MutableStateFlow, be able to access value via kProperty.
output.getValue(CounterUnioOutput::isCountDownEnabled)
If a property is defined as Computed
, be able to access computed value.
class Output: Unio.Output {
val isEnabled: Computed<Bool>
}
var _isEnabled = false
let output = OutputProxy(Output(Computed<Boolean> { _isEnabled }))
output.getComputed(CounterUnioOutput::isEnabled) // false
_isEnabled = true
output.getComputed(CounterUnioOutput::isEnabled) // true
The rule of State is having inner states of Unio.
class State: Unio.State {
val count = MutableStateFlow(0)
val isCountDownEnabled = MutableStateFlow(false)
}
The rule of Extra is having other dependencies of Unio.
class Extra(val githubApi: GitHubhAPI): Unio.Extra
The rule of Unio is generating Unio.Output from Dependency<Input, State, Extra>.
It generates Unio.Output to call OutputFactory#create
.
It is called once when Unio is initialized.
class CounterUnio(
input: CounterUnioInput,
state: State,
extra: Extra,
viewModelScope: CoroutineScope,
) : Unio<
CounterUnioInput,
CounterUnioOutput,
CounterUnio.Extra,
CounterUnio.State
>(
input = input,
extra = extra,
state = state,
outputFactory = CounterUnio,
viewModelScope = viewModelScope
)
Connect sequences and generate Unio.Output in OutputFactory#create
to use below properties and methods.
Dependency#state
Dependency#extra
Dependency#getFlow
... Returns a flow that is property of Unio.Input.viewModelScope
... It might be ViewModel lifecycle.
Here is a exmaple of implementation.
companion object : OutputFactory<
CounterUnioInput,
CounterUnioOutput,
Extra,
State
> {
override fun create(
dependency: Dependency<CounterUnioInput, Extra, State>,
viewModelScope: CoroutineScope
): CounterUnioOutput {
val state = dependency.state
val extra = dependency.extra
listOf(
dependency.getFlow(CounterUnioInput::countUp).map { 1 },
dependency.getFlow(CounterUnioInput::countDown).map { -1 }
)
.merge()
.map { state.count.value + it }
.onStart { emit(extra.startValue) }
.onEach {
state.count.emit(it)
state.isCountDownEnabled.emit(it > 0)
}
.launchIn(viewModelScope)
return CounterUnioOutput(
count = state.count.map { it.toString() },
isCountDownEnabled = state.isCountDownEnabled,
)
}
}
The rule of UnioFactory is generating Unio.
class CounterUnioFactoryImpl : UnioFactory<CounterUnioInput, CounterUnioOutput> {
override fun create(
viewModelScope: CoroutineScope,
onCleared: Flow<Unit>,
) = CounterUnio(
input = CounterUnioInput(),
state = CounterUnio.State(),
extra = CounterUnio.Extra(5),
viewModelScope = viewModelScope
)
}
UnioViewModel represents AAC ViewModel.
It has val input: InputProxy<Input>
and val output: OutputProxy<Output>
.
It automatically generates val input: InputProxy<Input>
and val output: OutputProxy<Output>
from instances of Unio.Input, Unio.State, Unio.Extra and UnioFactory.
Be able to define a subclass of UnioViewModel like this.
class CounterViewModel : UnioViewModel<CounterUnioInput, CounterUnioOutput>(CounterUnioFactoryImpl())
This is example usage in an Activity.
class MainActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
private var scope: CoroutineScope? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.count_up_button).setOnClickListener {
viewModel.input.getLambda(CounterUnioInput::countUp).invoke()
}
val countDownButton = findViewById<Button>(R.id.count_down_button)
countDownButton.setOnClickListener {
viewModel.input.getLambda(CounterUnioInput::countDown).invoke()
}
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main).also { scope ->
viewModel.output
.getFlow(CounterUnioOutput::isCountDownEnabled)
.onEach {
countDownButton.isEnabled = it
}
.launchIn(scope)
val textView = findViewById<TextView>(R.id.textView)
viewModel.output
.getFlow(CounterUnioOutput::count)
.onEach {
textView.text = it
}
.launchIn(scope)
}
}
override fun onDestroy() {
super.onDestroy()
scope?.cancel()
scope = null
}
}
You can use unio-kt with Dagger Hilt.
@HiltViewModel
class CounterViewModel @Inject constructor(
@CounterUnioFactory unioFactory: UnioFactory<CounterUnioInput, CounterUnioOutput>,
) : UnioViewModel<CounterUnioInput, CounterUnioOutput>(unioFactory)
class CounterUnio @AssistedInject constructor(
input: CounterUnioInput,
state: State,
@Assisted viewModelScope: CoroutineScope,
@Assisted onCleared: Flow<Unit>,
) : Unio<...>(...) { ... }
@AssistedFactory
interface CounterUnioFactory: UnioFactory<CounterUnioInput, CounterUnioOutput> {
override fun create(
@Assisted viewModelScope: CoroutineScope,
@Assisted onCleared: Flow<Unit>,
): CounterUnio
}
@Module
@InstallIn(ViewModelComponent::class)
interface ViewModelBindModule {
@Binds
fun bindCounterUnioFactory(
unioFactory: CounterUnioFactory,
): UnioFactory<CounterUnioInput, CounterUnioOutput>
}
@Module
@InstallIn(ViewModelComponent::class)
object ViewModelProvideModule {
@Provides
fun provideCounterUnioInput() = CounterUnioInput()
@Provides
fun provideCounterUnioState() = CounterUnio.State()
}
Add maven { url 'https://jitpack.io' }
to settings.gradle
.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" } // <--
}
}
In your app build.gradle
:
implementation 'com.github.marty-suzuki:unio-kt:TAG'
When import unio-kt
, Hyphone (marty-suzuki) is wrong, Underscore (marty_suzuki) is correct.
(import com.github.marty_suzuki.unio.*
)
- Unio for iOS.
unio-kt is released under the Apache License 2.0.