A look back
Orbital maneuver
OrbitViewModel
Middleware
OrbitContainer
OrbitViewModel
ContainerHost, ViewModel
OrbitContainer
interface Container<STATE : Any, SIDE_EFFECT : Any> {
val currentState: STATE
val stateStream: Stream<STATE>
val sideEffectStream: Stream<SIDE_EFFECT>
fun orbit(build: Builder<...>.() -> Builder<...>)
}
interface ContainerHost<STATE : Any, SIDE_EFFECT : Any> {
val container: Container<STATE, SIDE_EFFECT>
fun orbit(build: Builder<...>.() -> Builder<...>) =
container.orbit(build)
}
data class TweetListState(
val tweets: List<Tweet> = emptyList(),
val loading: Boolean = false,
val filteredTweets: List<Tweet> = emptyList()
)
class TweetListViewModel : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
}
open class RealContainer<STATE : Any, SIDE_EFFECT : Any>(
...,
parentScope: CoroutineScope
) : Container<STATE, SIDE_EFFECT> {
fun <STATE : Any, SIDE_EFFECT : Any> CoroutineScope.container(
...
): Container<STATE, SIDE_EFFECT> =
RealContainer(
...
parentScope = this
)
fun <STATE : Any, SIDE_EFFECT : Any> ViewModel.container(
...
): Container<STATE, SIDE_EFFECT> {
return viewModelScope.container(...)
}
interface TwitterApi {
fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = ???
}
interface TwitterApi {
fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = orbit {
???
}
}
interface TwitterApi {
fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transform { twitterApi.getTweetList() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
interface TwitterApi {
fun getTweetList(): Single<List<Tweet>>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transformRx2Single { twitterApi.getTweetList() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
interface TwitterApi {
suspend fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transformSuspend { twitterApi.getTweetList() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
interface TwitterApi {
suspend fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transformSuspend { twitterApi.getTweetList() }
.transformRx2Single { twitterApi.getStats() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
interface TwitterApi {
suspend fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState())
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transformSuspend { twitterApi.getTweetList() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
interface TwitterApi {
suspend fun getTweetList(): List<Tweet>
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState()) {
getTweetList()
}
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transformSuspend { twitterApi.getTweetList() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState()) {
getTweetList()
}
fun getTweetList() = orbit {
reduce { state.copy(loading = true) }
.transformSuspend { twitterApi.getTweetList() }
.reduce { state.copy(loading = false, tweets = event) }
}
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState()) {
getTweetList()
}
fun getTweetList() = orbit { ... }
fun filterTweets(filter: String) = orbit {
reduce {
state.copy(
filteredTweets = state.tweets.filter {
it.author.contains(filter) || it.content.contains(filter)
}
)
}
}
}
class TweetListViewModel @Inject constructor(
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container = container<TweetListState, Nothing>(TweetListState()) {
getTweetList()
}
fun getTweetList() = orbit { ... }
fun filterTweets(filter: String) = orbit {
reduce {
if (filter.isBlank()) {
state.copy(filteredTweets = state.tweets)
} else {
state.copy(
filteredTweets = state.tweets.filter {
it.author.contains(filter) || it.content.contains(filter)
}
)
}
}
}
}
class TweetListActivity: AppCompatActivity() {
private val viewModel by viewModel<TweetListViewModel>()
private val adapter: RecyclerView.Adapter
override fun onCreate(savedState: Bundle?) {
...
swipeRefresh.setOnRefreshListener { viewModel.getTweetList() }
filterEditText.setOnTextChanged { viewModel.filter(it.text) }
viewModel.container.state.observe(this, Observer { render(it) })
}
private fun render(state: TweetListState) {
swipeRefresh.setRefreshing(state.loading)
adapter.setData(state.filteredTweets)
}
}
@Parcelize
data class TweetListState(
val tweets: List<Tweet> = emptyList(),
val loading: Boolean = false,
val filteredTweets: List<Tweet> = emptyList()
): Parcelable
class TweetListViewModel @Inject constructor(
private val savedStateHandle,
private val twitterApi: TwitterApi
) : ContainerHost<TweetListState, Nothing>, ViewModel() {
override val container =
container<TweetListState, Nothing>(TweetListState(), savedStateHandle) {
getTweetList()
}
fun getTweetList() = orbit { ... }
fun filterTweets(filter: String) = orbit { ... }
}
class TweetListTests {
private val tweetList = listOf(...)
private val twitterApi = mock<TwitterApi> {
on { getTweetList() } thenReturn { tweetList }
}
@Test
fun gettingTweetList() {
val testSubject = TweetListViewModel(twitterApi).test(TweetListState())
testSubject.getTweetList()
testSubject.assert {
states(
{ copy(loading = true) },
{ copy(loading = false, tweets = tweetList) }
)
}
}
}
class ExampleTests {
@Test
fun exampleTest() {
...
testSubject.assert {
states(
{ copy(loading = true) },
{ copy(loading = false, data = someData) }
)
}
}
}
class ExampleTests {
@Test
fun exampleTest() {
...
testSubject.assert {
sideEffects(
ExampleSideEffects.Toast("foo"),
ExampleSideEffects.Toast("bar")
)
}
}
}
class ExampleTests {
private class ExampleViewModel : ContainerHost<ExampleState, Nothing>, ViewModel() {
override val container = container<ExampleState, Nothing>(ExampleState())
fun doSomething() = orbit {
reduce { state.copy(loading = true) }
.sideEffect { doSomethingElse() }
}
fun doSomethingElse() = orbit {
...
}
}
@Test
fun exampleTest() {
...
testSubject.doSomething()
testSubject.assert {
loopBack { doSomethingElse() }
}
}
}