Andoroid MVI Example: Difference between revisions
Tag: Reverted |
|||
(28 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
=Introduction= | =Introduction= | ||
Wanted to revisit the MVI pattern to just have another look using another approach this example uses ktor, an asyncronous client, where I was previously using Retrofit, and Oribit MVI which provides the container and the Store elements. | Revisiting the MVI pattern, I'll explore an alternative approach leveraging ktor's asynchronous client and Orbit MVI's container and Store elements. (Original: Wanted to revisit the MVI pattern to just have another look using another approach this example uses ktor, an asyncronous client, where I was previously using Retrofit, and Oribit MVI which provides the container and the Store elements.) | ||
==Consists of== | |||
*Model Represents app data and logic | |||
*View Displays UI components | |||
*Intent Captures user actions sent to ViewModel | |||
==Flow== | |||
The flow of MVI is | |||
Intent -> ViewModel -> Model Update -> State emission -> View Update | |||
=The Pattern= | =The Pattern= | ||
Last time I looked at this we had this diagram<br> | Last time I looked at this we had this diagram<br> | ||
Line 6: | Line 14: | ||
For this example I will be using this one<br> | For this example I will be using this one<br> | ||
[[File:MVI2a.png|700px]]<br> | [[File:MVI2a.png|700px]]<br> | ||
Here is another example of the same thing. I have been trying to see the advantage of using the Store, Reduce, approach which is not mentioned anywhere aside from the video. I can only find Orbit MVI which has the support - so far.<br> | |||
[[File:Mvi5.png]]<br> | |||
=Setup= | =Setup= | ||
==The Good== | |||
<syntaxhighlight lang=" | You can do the following and it seems to work which is good | ||
<syntaxhighlight lang="groovy"> | |||
implementation("org.orbit-mvi:orbit-core:<latest-version>") | implementation("org.orbit-mvi:orbit-core:<latest-version>") | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==The Bad== | |||
What I did not realize is that the serialization and Orbit seem to have change since the demo. To get them to work I had to use the following | |||
*ktor 3.1.1 | |||
*orbit 7.2.0 | |||
I also had to add the plugin for serialization with | |||
<syntaxhighlight lang="groovy" highlight="5"> | |||
plugins { | |||
alias(libs.plugins.android.application) | |||
alias(libs.plugins.kotlin.android) | |||
alias(libs.plugins.kotlin.compose) | |||
kotlin("plugin.serialization") version "2.1.10" | |||
} | |||
</syntaxhighlight> | |||
implementation( | ==The Ugly== | ||
implementation( | So got it working with the following | ||
implementation( | <syntaxhighlight lang="groovy"> | ||
implementation(libs.orbit.core) | |||
implementation(libs.orbit.compose) | |||
implementation(libs.orbit.viewmodel) | |||
implementation(libs.ktor.client.core) | |||
implementation(libs.ktor.client.android) | |||
implementation(libs.ktor.client.serialization) | |||
implementation(libs.ktor.client.content.negotiation) | |||
implementation(libs.ktor.serialization.kotlinx.json) | |||
</syntaxhighlight> | </syntaxhighlight> | ||
=Resource= | =Resource= | ||
Resource is a sealed class which represents the states you are going to handle for the view. E.g. Loading, Success, Error. This was confusing to be at first because it looks a bit like the data for each state. It is not. That is what the ViewState is for. In the video the author called this DataState but I am told this is called Resource. | |||
<syntaxhighlight lang="kotlin"> | <syntaxhighlight lang="kotlin"> | ||
sealed class DataState<T> { | sealed class DataState<T> { | ||
data class Loading<T>(val isLoading: Boolean) : DataState<T>() | data class Loading<T>(val isLoading: Boolean) : DataState<T>() | ||
Line 38: | Line 62: | ||
sealed class UIComponent { | sealed class UIComponent { | ||
data class | data class Toast(val text: String) : UIComponent() | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Post API= | =Post API= | ||
This not part of MVI, it is a service used for the data. Previously I have used Retrofit to do this job and it would live in the Data Layer. | This not part of MVI, it is a service used for the data. Previously I have used Retrofit to do this job and it would live in the Data Layer. | ||
Line 47: | Line 72: | ||
<syntaxhighlight lang="kotlin"> | <syntaxhighlight lang="kotlin"> | ||
interface PostApi { | interface PostApi { | ||
suspend fun getPosts(): List< | suspend fun getPosts(): List<PostModel> | ||
companion object { | companion object { | ||
val httpClient = HttpClient(Android) { | val httpClient = HttpClient(Android) { | ||
install(ContentNegotiation) { | install(ContentNegotiation) { | ||
Json { | json ( | ||
Json { | |||
this.ignoreUnknownKeys = true | |||
} | |||
) | |||
} | } | ||
} | } | ||
Line 62: | Line 89: | ||
} | } | ||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Post API Implementation== | ==Post API Implementation== | ||
And here is the implementation | And here is the implementation | ||
Line 71: | Line 98: | ||
private val httpClient :HttpClient | private val httpClient :HttpClient | ||
):PostApi { | ):PostApi { | ||
override suspend fun getPosts(): List< | override suspend fun getPosts(): List<PostModel> { | ||
return httpClient.get( | return httpClient.get( | ||
"https://jsonplaceholder.typicode.com/posts" | "https://jsonplaceholder.typicode.com/posts" | ||
Line 85: | Line 112: | ||
private val postApi: PostApi | private val postApi: PostApi | ||
) { | ) { | ||
fun execute(): Flow<DataState<List< | fun execute(): Flow<DataState<List<PostModel>>> { | ||
return flow { | return flow { | ||
emit(DataState.Loading(true)) | emit(DataState.Loading(true)) | ||
Line 95: | Line 122: | ||
} catch (e: Exception) { | } catch (e: Exception) { | ||
e.printStackTrace() | e.printStackTrace() | ||
emit(DataState.Error(UIComponent. | emit(DataState.Error(UIComponent.Toast("Failed to get posts"))) | ||
} | } | ||
finally { | finally { | ||
emit(DataState.Loading(false)) | emit(DataState.Loading(false)) | ||
Line 104: | Line 130: | ||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=MVI Components= | =MVI Components= | ||
==View State== | ==View State== | ||
Line 154: | Line 181: | ||
We create our ViewModel and for each State we have we define what should happen. | We create our ViewModel and for each State we have we define what should happen. | ||
===Our Example ViewModel=== | ===Our Example ViewModel=== | ||
So here is the View Model. This initiates the use case. In our case this is getPosts(). The Post API would normally be injected into to Data Layer. | So here is the View Model. This initiates the use case. In our case this is getPosts(). The Post API would normally be injected into to Data Layer. Each Data State we are managing we provide how to update the View State appropriately | ||
<syntaxhighlight lang="kotlin"> | <syntaxhighlight lang="kotlin"> | ||
class PostViewModel: ViewModel(), ContainerHost<PostViewState, UIComponent > { | class PostViewModel : ViewModel(), ContainerHost<PostViewState, UIComponent> { | ||
val getPosts = GetPosts(PostApi.providePostApi()) | val getPosts = GetPosts(PostApi.providePostApi()) | ||
Line 163: | Line 191: | ||
fun getPosts() { | fun getPosts() { | ||
intent { | intent { | ||
val posts = getPosts.execute() | val posts = getPosts.execute().collect { dataState -> | ||
when (dataState) { | when (dataState) { | ||
is DataState.Loading -> { | is DataState.Loading -> { | ||
Line 187: | Line 214: | ||
} | } | ||
} | } | ||
} | |||
} | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
===The View=== | |||
This is now really easy thanks to Compose. No more xml. | |||
<syntaxhighlight lang="kotlin"> | |||
val viewModel by viewModels<PostViewModel>() | |||
enableEdgeToEdge() | |||
setContent { | |||
MVIExampleTheme { | |||
val state by viewModel.collectAsState() | |||
val content = LocalContext.current | |||
Surface( | |||
modifier = Modifier.fillMaxSize(), | |||
color = MaterialTheme.colorScheme.background | |||
) { | |||
Column() { | |||
Button( | |||
onClick = { | |||
viewModel.getPosts() | |||
}, | |||
modifier = Modifier.padding(16.dp) | |||
) | |||
{ | |||
Text("Clicky") | |||
} | |||
LazyColumn( | |||
modifier = Modifier.fillMaxSize(), | |||
verticalArrangement = Arrangement.spacedBy(5.dp) | |||
) | |||
{ | |||
items(state.posts) { post -> | |||
Text( | |||
text = post.title, | |||
modifier = Modifier.padding(18.dp) | |||
) | |||
Text( | |||
text = post.body, | |||
modifier = Modifier.padding(10.dp) | |||
) | |||
} | |||
} | |||
if (state.progressBar) { | |||
Box( | |||
modifier = Modifier.fillMaxSize(), | |||
contentAlignment = Alignment.Center | |||
) | |||
{ | |||
CircularProgressIndicator() | |||
} | |||
} | |||
viewModel.collectSideEffect | |||
{ | |||
uiComponent -> | |||
when (uiComponent) { | |||
is UIComponent.Toast -> { | |||
Toast.makeText( | |||
content, | |||
uiComponent.text, | |||
Toast.LENGTH_SHORT | |||
).show() | |||
} | } | ||
} | |||
} | } | ||
} | |||
} | } | ||
} | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
= | =Kotlin StateFlow API= | ||
During the revisiting of this topic I came across State Flow. Previously I using RxJava for this but this not looks like the new kid on the block. One tip that was provided was the you would always create two states, a private for doing the updates and a public one for using with the view | |||
<syntaxhighlight lang="kotlin"> | |||
private val _users = MutableStateFlow<List<Users>>(emptyList()) | |||
val users = _users.asStateFlow() | |||
</syntaxhighlight> | |||
But with Stateflow you can also create derived states where in other approaches you might manually code the derived state. Here we use stateIn to update each time the users changes and the ViewModel is subscribed | |||
<syntaxhighlight lang="kotlin"> | <syntaxhighlight lang="kotlin"> | ||
private val _users = MutableStateFlow<List<Users>>(emptyList()) | |||
val users = _users.asStateFlow() | |||
val localUser = users.map { users -> | |||
users.find { it .id == "local"} | |||
}.stateIn(viewModelScope, SharingStarted.WhereSubscribed(), null) | |||
</syntaxhighlight>state | |||
You can do most things that could be done in RxJava. So for instance you can combine n number of states together with combine(state1, state2, state3) { ... }.stateIn(viewModelScope, SharingStarted.WhereSubscribed(), null) | |||
=How to Write you own MVI Library= | |||
Well as mentioned at the top, I could not see anywhere where the Container/Reducer had been used. So I googled and found Matthew Dolan on [[https://youtu.be/E6obYmkkdko?si=ZvGjMAW6MCwQ6Cz7 YouTube]] but have got to the end of the video I realized that Matthew Dolan endorses Orbit MVI<br> | |||
<br> | |||
But watching the video provided my with my answer for what the library does for you. Here is the simplistic version shown in the video. | |||
<syntaxhighlight lang="kotlin"> | |||
class Container<TState, TSideEffect>( | |||
private val scope: CoroutineScope, | |||
private val initialState: TState | |||
) { | |||
private val _state = MutableStateFlow(initialState) | |||
val state: StateFlow<TState> = _state | |||
private val _sideEffect = Channel<TSideEffect>(Channel.BUFFERED) | |||
val sideEffect = _sideEffect.receiveAsFlow() | |||
fun intent(transform: suspend Container<TState, TSideEffect>.() -> Unit) { | |||
scope.launch(SINGLE_THREAD) { | |||
this@Container.transform() | |||
} | |||
} | |||
suspend fun reduce(reducer: TState.() -> TState) { | |||
withContext(SINGLE_THREAD) { | |||
_state.value = _state.value.reducer() | |||
} | } | ||
} | |||
suspend fun setSideEffect(sideEffect: TSideEffect) { | |||
_sideEffect.send(sideEffect) | |||
} | |||
companion object { | |||
private val SINGLE_THREAD = newSingleThreadContext("Container") | |||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
And example usage would be | |||
<syntaxhighlight lang="kotlin"> | <syntaxhighlight lang="kotlin"> | ||
class PostListViewModel( | |||
private val getPosts: GetPosts | |||
): ViewModel() { | |||
val container = Container<PostListState, NavigateToDetails>(viewModelScope) | |||
fun loadOverviews() = container.intent { | |||
val posts = postRepository.getOverviews() | |||
reduce { | |||
copy(overviews = posts) | |||
} | |||
} | |||
fun onPostClicked(postOverview: PostOverview) = container.intent { | |||
postSideEffect(NavigateToDetails(postOverview.id)) | |||
} | |||
} | |||
</syntaxhighlight> | |||
So I guess rightly the question is why not use this and this was answered in the video. With missing features | |||
*Stricter DSL Scoping | |||
*Improved thread Model | |||
*Unit Tests | |||
*Testing Framework | |||
*Idling resource support | |||
*Save State Support | |||
=Example Code= | |||
Finally found an example I like from [[https://github.com/kaleidot725/Jetpack-Compose-Orbit-MVI-Demo kaleidot725]]. This has a great example which uses<br> | |||
[[File:MVI6.png | 300px]]<br> | |||
*App, Domain, Data Clean approach | |||
*Orbit MVI Approach | |||
*Repository Pattern | |||
*Use of Room | |||
*OkHttp3 | |||
*Kapt | |||
*Compose | |||
*Search Button | |||
*Coil Image Loading Library | |||
A bit of a struggle to get it to build but got there in the end. This is what did it for me. | |||
<syntaxhighlight lang="groovy"> | |||
ext { | |||
// Plugin | |||
android_plugin_version = '8.8.2' | |||
ktlint_plugin_version = '11.0.0' | |||
// DI | |||
koin_version = '3.1.5' | |||
// MVI | |||
orbit_version = '5.0.0' | |||
// Kotlin | |||
kotlin_version = '1.9.22' | |||
serialization_json_version = '1.4.0' | |||
// Android Jetpack | |||
appcompat_version = '1.6.0' | |||
core_ktx_version = '1.9.0' | |||
lifecycle_ktx_version = '2.6.1' | |||
room_version = '2.6.1' | |||
// Jetpack Compose | |||
compose_version = '1.2.0' | |||
compose_compiler_version = '1.5.10' | |||
coil_compose_version = '2.2.1' | |||
activity_compose_version = '1.6.0' | |||
navigation_compose_version = '2.5.2' | |||
accompanist_flowlayout_version = '0.26.2-beta' | |||
// Test | |||
junit_version = '4.13.2' | |||
junit_ext_version = '1.1.3' | |||
espresso_version = '3.4.0' | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 06:24, 26 March 2025
Introduction
Revisiting the MVI pattern, I'll explore an alternative approach leveraging ktor's asynchronous client and Orbit MVI's container and Store elements. (Original: Wanted to revisit the MVI pattern to just have another look using another approach this example uses ktor, an asyncronous client, where I was previously using Retrofit, and Oribit MVI which provides the container and the Store elements.)
Consists of
- Model Represents app data and logic
- View Displays UI components
- Intent Captures user actions sent to ViewModel
Flow
The flow of MVI is
Intent -> ViewModel -> Model Update -> State emission -> View Update
The Pattern
Last time I looked at this we had this diagram
For this example I will be using this one
Here is another example of the same thing. I have been trying to see the advantage of using the Store, Reduce, approach which is not mentioned anywhere aside from the video. I can only find Orbit MVI which has the support - so far.
Setup
The Good
You can do the following and it seems to work which is good
implementation("org.orbit-mvi:orbit-core:<latest-version>")
The Bad
What I did not realize is that the serialization and Orbit seem to have change since the demo. To get them to work I had to use the following
- ktor 3.1.1
- orbit 7.2.0
I also had to add the plugin for serialization with
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.1.10"
}
The Ugly
So got it working with the following
implementation(libs.orbit.core)
implementation(libs.orbit.compose)
implementation(libs.orbit.viewmodel)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.android)
implementation(libs.ktor.client.serialization)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
Resource
Resource is a sealed class which represents the states you are going to handle for the view. E.g. Loading, Success, Error. This was confusing to be at first because it looks a bit like the data for each state. It is not. That is what the ViewState is for. In the video the author called this DataState but I am told this is called Resource.
sealed class DataState<T> {
data class Loading<T>(val isLoading: Boolean) : DataState<T>()
data class Success<T>(val data: T) : DataState<T>()
data class Error<T>(val uiComponent: UIComponent) : DataState<T>()
}
sealed class UIComponent {
data class Toast(val text: String) : UIComponent()
}
Post API
This not part of MVI, it is a service used for the data. Previously I have used Retrofit to do this job and it would live in the Data Layer.
Post API Interface
Here we create an interface which would normally be in the Domain Layer. It has a companion object to create the http client, this would normally be injected using Dagger or some other DI.
interface PostApi {
suspend fun getPosts(): List<PostModel>
companion object {
val httpClient = HttpClient(Android) {
install(ContentNegotiation) {
json (
Json {
this.ignoreUnknownKeys = true
}
)
}
}
fun providePostApi(): PostApi {
return PostApiImpl(httpClient)
}
}
}
Post API Implementation
And here is the implementation
class PostApiImpl(
private val httpClient :HttpClient
):PostApi {
override suspend fun getPosts(): List<PostModel> {
return httpClient.get(
"https://jsonplaceholder.typicode.com/posts"
).body()
}
}
Use Case
This is the use case to get the posts
class GetPosts(
private val postApi: PostApi
) {
fun execute(): Flow<DataState<List<PostModel>>> {
return flow {
emit(DataState.Loading(true))
try {
val posts = postApi.getPosts()
emit(DataState.Success(posts))
} catch (e: Exception) {
e.printStackTrace()
emit(DataState.Error(UIComponent.Toast("Failed to get posts")))
}
finally {
emit(DataState.Loading(false))
}
}
}
}
MVI Components
View State
We need to hold the state of the view so we make a data class to hold this. This is like the state in react.
class PostViewState {
val progressBar: Boolean = false
val posts: List<Post> = emptyList()
val error: String? = null
}
ViewModel
Simple ViewModel
Remember this section is just for this part.
Before providing the view model for the example let just look at a simple example. Here are the components that make up the ViewModel when using the Orbit MVI and ContainerHost
- View: The UI layer that displays the current state and sends user actions (intents) to the ViewModel. The View observes the state and reacts to it.
- ViewModel: Implements the ContainerHost interface and manage the state and side effects. It processes intents from the View, updates the state, and handles side effects.
- State: A data class that represents the current state of the UI. It is immutable and can only be modified by the ViewModel.
- Intent: Represents user actions or events that trigger state changes or side effects. Intents are handled by the ViewModel.
- Side Effects: Actions that do not directly affect the state, such as navigation, showing a toast, or logging. They are managed separately to keep the state management clean.
The side effect was perhaps the thing I struggle with but really it just something we my like to construct as a consequence of the state changing but does not influence the state.
This is very similar to the way Redux works in that you make a reducer for each state the model is managing. Here is a simple example.
data class MyState(val count: Int = 0)
We can modify the status with a view model like this.
class MyViewModel : ViewModel(), ContainerHost<MyState, Nothing> {
override val container = container<MyState, Nothing>(MyState())
fun increment() = intent {
reduce {
state.copy(count = state.count + 1)
}
}
fun decrement() = intent {
reduce {
state.copy(count = state.count - 1)
}
}
}
We create our ViewModel and for each State we have we define what should happen.
Our Example ViewModel
So here is the View Model. This initiates the use case. In our case this is getPosts(). The Post API would normally be injected into to Data Layer. Each Data State we are managing we provide how to update the View State appropriately
class PostViewModel : ViewModel(), ContainerHost<PostViewState, UIComponent> {
val getPosts = GetPosts(PostApi.providePostApi())
override val container: Container<PostViewState, UIComponent> = container(PostViewState())
fun getPosts() {
intent {
val posts = getPosts.execute().collect { dataState ->
when (dataState) {
is DataState.Loading -> {
reduce {
state.copy(progressBar = dataState.isLoading)
}
}
is DataState.Success -> {
reduce {
state.copy(posts = dataState.data)
}
}
is DataState.Error -> {
when (dataState.uiComponent) {
is UIComponent.Toast -> {
reduce {
state.copy(error = dataState.uiComponent.text)
}
}
}
}
}
}
}
}
}
The View
This is now really easy thanks to Compose. No more xml.
val viewModel by viewModels<PostViewModel>()
enableEdgeToEdge()
setContent {
MVIExampleTheme {
val state by viewModel.collectAsState()
val content = LocalContext.current
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column() {
Button(
onClick = {
viewModel.getPosts()
},
modifier = Modifier.padding(16.dp)
)
{
Text("Clicky")
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(5.dp)
)
{
items(state.posts) { post ->
Text(
text = post.title,
modifier = Modifier.padding(18.dp)
)
Text(
text = post.body,
modifier = Modifier.padding(10.dp)
)
}
}
if (state.progressBar) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
)
{
CircularProgressIndicator()
}
}
viewModel.collectSideEffect
{
uiComponent ->
when (uiComponent) {
is UIComponent.Toast -> {
Toast.makeText(
content,
uiComponent.text,
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
}
}
Kotlin StateFlow API
During the revisiting of this topic I came across State Flow. Previously I using RxJava for this but this not looks like the new kid on the block. One tip that was provided was the you would always create two states, a private for doing the updates and a public one for using with the view
private val _users = MutableStateFlow<List<Users>>(emptyList())
val users = _users.asStateFlow()
But with Stateflow you can also create derived states where in other approaches you might manually code the derived state. Here we use stateIn to update each time the users changes and the ViewModel is subscribed
private val _users = MutableStateFlow<List<Users>>(emptyList())
val users = _users.asStateFlow()
val localUser = users.map { users ->
users.find { it .id == "local"}
}.stateIn(viewModelScope, SharingStarted.WhereSubscribed(), null)
state
You can do most things that could be done in RxJava. So for instance you can combine n number of states together with combine(state1, state2, state3) { ... }.stateIn(viewModelScope, SharingStarted.WhereSubscribed(), null)
How to Write you own MVI Library
Well as mentioned at the top, I could not see anywhere where the Container/Reducer had been used. So I googled and found Matthew Dolan on [YouTube] but have got to the end of the video I realized that Matthew Dolan endorses Orbit MVI
But watching the video provided my with my answer for what the library does for you. Here is the simplistic version shown in the video.
class Container<TState, TSideEffect>(
private val scope: CoroutineScope,
private val initialState: TState
) {
private val _state = MutableStateFlow(initialState)
val state: StateFlow<TState> = _state
private val _sideEffect = Channel<TSideEffect>(Channel.BUFFERED)
val sideEffect = _sideEffect.receiveAsFlow()
fun intent(transform: suspend Container<TState, TSideEffect>.() -> Unit) {
scope.launch(SINGLE_THREAD) {
this@Container.transform()
}
}
suspend fun reduce(reducer: TState.() -> TState) {
withContext(SINGLE_THREAD) {
_state.value = _state.value.reducer()
}
}
suspend fun setSideEffect(sideEffect: TSideEffect) {
_sideEffect.send(sideEffect)
}
companion object {
private val SINGLE_THREAD = newSingleThreadContext("Container")
}
}
And example usage would be
class PostListViewModel(
private val getPosts: GetPosts
): ViewModel() {
val container = Container<PostListState, NavigateToDetails>(viewModelScope)
fun loadOverviews() = container.intent {
val posts = postRepository.getOverviews()
reduce {
copy(overviews = posts)
}
}
fun onPostClicked(postOverview: PostOverview) = container.intent {
postSideEffect(NavigateToDetails(postOverview.id))
}
}
So I guess rightly the question is why not use this and this was answered in the video. With missing features
- Stricter DSL Scoping
- Improved thread Model
- Unit Tests
- Testing Framework
- Idling resource support
- Save State Support
Example Code
Finally found an example I like from [kaleidot725]. This has a great example which uses
- App, Domain, Data Clean approach
- Orbit MVI Approach
- Repository Pattern
- Use of Room
- OkHttp3
- Kapt
- Compose
- Search Button
- Coil Image Loading Library
A bit of a struggle to get it to build but got there in the end. This is what did it for me.
ext {
// Plugin
android_plugin_version = '8.8.2'
ktlint_plugin_version = '11.0.0'
// DI
koin_version = '3.1.5'
// MVI
orbit_version = '5.0.0'
// Kotlin
kotlin_version = '1.9.22'
serialization_json_version = '1.4.0'
// Android Jetpack
appcompat_version = '1.6.0'
core_ktx_version = '1.9.0'
lifecycle_ktx_version = '2.6.1'
room_version = '2.6.1'
// Jetpack Compose
compose_version = '1.2.0'
compose_compiler_version = '1.5.10'
coil_compose_version = '2.2.1'
activity_compose_version = '1.6.0'
navigation_compose_version = '2.5.2'
accompanist_flowlayout_version = '0.26.2-beta'
// Test
junit_version = '4.13.2'
junit_ext_version = '1.1.3'
espresso_version = '3.4.0'
}