Skip to main content

Posts​

Posts

The two posts sample apps are also adapted from Orbit. They demonstrate:

  • how to implement a dual pane layout
  • how to run more complex asynchronous operations (this time real ones) with loading spinners and error handling
  • how to use initializers
  • how to use side effects
  • how to combine Redux and BlocOwner / MVVM+ syntax
  • how to use Blocs with Fragments on Android
  • how to define a contract for the view component

Posts 1​

The first example app uses two different Blocs with separate BlocStates.

data class PostsState(
val loading: Boolean = false,
val posts: Result<List<Post>, Throwable> = Ok(emptyList()),
)

object Posts {
// the Action class
sealed class Action {
object Load : Action()
object Loading : Action()
data class Loaded(val posts: Result<List<Post>, Throwable>) : Action()
data class Clicked(val post: Post) : Action()
}

// the SideEffect class
class OpenPost(val post: Post)

fun bloc(context: BlocContext) = bloc<PostsState, Action, OpenPost, PostsState>(
context,
blocState(PostsState())
) {
val repository = getKoinInstance<PostRepository>()

// start loading the posts in the initializer
onCreate {
if (state.isEmpty()) dispatch(Action.Load)
}

// we could also put the thunk code into the onCreate block
// but we want to illustrate the use of thunks
thunk<Action.Load> {
dispatch(Action.Loading)
val result = repository.getOverviews()
dispatch(Action.Loaded(result))
}

// this is an anti-pattern according to
// https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95
// but we're doing it anyway
sideEffect<Action.Clicked> { OpenPost(action.post) }

reduce<Action.Loading> {
state.copy(loading = true)
}

reduce<Action.Loaded> {
state.copy(loading = false, posts = action.posts)
}
}
}

Android​

The original sample app in Orbit uses a ViewModel and a Fragment but Kotlin Bloc eliminates the need for a ViewModel:

class PostsFragment : 
Fragment(R.layout.post_list_fragment),
BlocOwner<PostsState, Posts.Action, Posts.OpenPost, PostsState> {

override val bloc by getOrCreate("posts") { Posts.bloc(it) }

override fun onResume() {
super.onResume()

// subscribe to the state and the side effects
subscribe(this, ::render, ::sideEffect)
}

private fun render(state: PostsState) {
// render the view
}

private fun sideEffect(sideEffect: Posts.OpenPost) {
// navigate to the detail view using a side effect
findNavController().navigate(
PostsFragmentDirections.actionListFragmentToDetailFragment(
sideEffect.post
)
)
}
tip

The bloc in above example is created using the key "posts":

override val bloc by getOrCreate("posts") { Posts.bloc(it) }

Activity/Fragment explains in detail why we need that key.

iOS​

There's no iOS implementation for the first posts example.

Posts 2​

The second example app uses a single bloc for the master and the details view/component and defines a contract between the view and the bloc to use function calls instead of actions:

abstract class PostsComponent : BlocOwner<PostsRootState, PostsAction, Unit, PostsRootState> {
// we need this for iOS to prevent erasure of generic types
abstract override val bloc: Bloc<PostsRootState, PostsAction, Unit>

abstract fun onSelected(post: Post)
abstract fun onClosed()
}

The implementing PostsComponent class is pretty straight forward:

sealed class PostsAction

class PostsComponentImpl(context: BlocContext) : PostsComponent() {

// internal actions, not exposed to the View
private object PostsLoading : PostsAction()
private data class PostsLoaded(val result: Result<List<Post>, Throwable>) : PostsAction()
private class PostLoading(val postId: Int) : PostsAction()
private data class PostLoaded(val result: Result<Post, Throwable>) : PostsAction()

// we need to lazy initialize the Bloc so that the component is fully initialized before
// making any calls to load the posts
override val bloc by lazy {
bloc<PostsRootState, PostsAction>(context, blocState) {
onCreate {
// initializer code
}

reduce<PostsLoading> {
// reducer code
}

// more reducer here
}
}

// using BlocOwner / MVVM+ syntax
override fun onSelected(post: Post) = thunk {
// load the selected post asynchronously, simplified:
dispatch(PostLoading(post.id))
val result = repository.getDetail(post.id)
dispatch(PostLoaded(result))
}

// using BlocOwner / MVVM+ syntax
override fun onClosed() = reduce {
state.copy(postState = state.postState.copy(loadingId = null, post = null))
}
}

Android​

The Android implementation doesn't introduce any new concepts compared to previous example apps. If you're interested in implementing a dual pane layout with Jetpack Compose, please consult the source code here.

iOS​

The iOS implementation introduces one new concept which is the BlocComponent.

The difference to using BlocHolders is very small though:

private let component = BlocComponent<PostsComponent> { PostsComponentImpl(context: $0) }

compared to e.g.:

private let holder = BlocHolder { SimpleCounter.shared.bloc(context: $0) }