Blog Infos
Author
Published
Topics
,
Published
Topics
,

This article is a follow-up to one I published in April 2019. You can find it here.

Lots of things have changed during the last years, also in the Android development world. Some new cutting-edge technologies arose, others that are battle-proven became ‘deprecated’.

“Which technologies or/and architecture should I use when starting a new project in 2022?” — this question comes to mind for many of us. I’ll try to find an answer in this blog post based on my personal experience.

But first, let’s start with few disclaimers:

  • #1 — Very advanced Android developers are not the main target audience for this article. Lots of things will sound subjective. And that’s probably true. There is no single answer to the question asked in the previous paragraph. Some will say the whole project is just an overkill and “boilerplate” for such trivial tasks. This might be true as well. Nonetheless, any feedback is very recommended — we are all here to learn.
  • #2 — This blog post is not a guide to each technology/library mentioned here. It would make this blog post an one-hour read. And there are multiple ‘guide’ articles available on the Internet, also being written by libraries’ creators.
  • #3 — Treat this blog post as a retrospective review article. There will be many references and links to not make it too long. So if someone would like to dig deeper into the specific topic, then go ahead. Still, grabbing your coffee/tea/mate/whatever you drink is recommended.
You still talk too much, just show me your code!

Same as previously, you can find the repository with my sample project at the end of this post.

The sample project

The main intention was to show good practices using Kotlin features and latest Android libraries from Jetpack in the simplest possible way, so anyone regardless of their level of skill could get the idea of:

  • Downloading data from public API with offline-first approach
  • Separation of concerns using The Clean Architecture by Uncle Bob
  • Showing and updating UI in a lifecycle-aware manner

 

 

Just like last time I use the public SpaceX API to download data from the Internet. However, compared to the previous article, I restricted data types from three to only one (rocket fleet) to not obscure the overall picture. Moreover, data always comes from the local persistence (offline-first approach) and updates when needed (e.g. via swipe-to-refresh gesture). Last but not least, each item is clickable and will navigate to rocket’s details on Wikipedia website.

Build configuration — Kotlin DSL scripts & Version catalog

Starting from Android Gradle Plugin 4.1.0 (August 2020), it is possible to write Gradle configuration files in Kotlin instead of Groovy language. There are few benefits of it, including better IDE support, however it is well known that build times using Kotlin DSL scripts are somewhat slower than Groovy’s equivalents. Also, as of today (August 2022), there is no automated way to convert them to Kotlin like we could expect when migrating older code from Java. Your best bet is manual migration using this Google guide.

Keeping pros and cons in mind, I decided to migrate my Gradle scripts to Kotlin in the sample project. However, I would think twice (and discuss it with a team) about doing the same when developing a bigger app due to build time concerns.

Nevertheless, there should be no similar discussion regarding dependency management. Starting from Gradle 7.0 (April 2021) when Version catalogs became an experimental feature and moving to Gradle 7.4 (February 2022) when they were promoted to stable release, they offer standardized and highly-readable way of central declaration for project dependencies. No more ext blocks, no more buildSrc — just one TOML file and you are ready to go:

[versions]
hilt = "2.50"
kotlin = "1.9.22"
kotlin-coroutines = "1.7.3"
retrofit = "2.9.0"
room = "2.6.1"
(...)
[plugins]
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
(...)
[libraries]
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlin-coroutines" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
room = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
(...)

Definitely one of the best new features that can be used for Android app development!

Project configuration — Gradle modularization

Back in 2014 when the Android community was just barely crawling in a non-standardized world of app development, this article appeared. Basically Fernando suggested there to create three Gradle modules to separate concerns in compliance with The Clean Architecture approach:

  • data
  • domain
  • and presentation

I believe everyone knows (and probably uses) this nomenclature even today, eight years later. This module distribution is what we call today the “modularization by layer”.

However, not many developers know that Fernando reviewed his point of view (in 2015 and 2019). The latter one is more important because he revised his standpoint regarding Gradle modularization. What did he say? Quoting:

If you did your homework and checked my previous posts about android architecture, you might have seen that I used android modules for representing each layer involved in the architecture.

A recurring question in discussions was: Why? The answer is simple… Wrong technical decision: I relied on different modules in order to be more strict with the dependency rule by establishing borders and, thus, make it more difficult to break it.

But power comes with big responsibilities, and even though this worked pretty well in the beginning, the sample was still a MONOLITH and it would bring problems when scaling up:

* when modifying or adding a new functionality: we had to touch every single module/layer (strong dependencies/coupling between modules).

* conflicts when developers working in the codebase (the bigger the team, the more conflicts, especially with PRs and git).

From personal experience, I also find apps modularized by layers much harder to develop and scale properly.

The alternative approach, becoming more popular recently, is called “modularization by feature” and is used in the sample project in a very simplified way. There are also three modules yet much different than in previous example:

  • app that everyone is familiar with and has access to all other modules,
  • core that gathers all reusable components used by multiple features and doesn’t “see” other modules,
  • basic-feature that sits between these two modules in terms of dependencies (and like any other feature module).

In more complicated projects, core module can be divided into multiple ones (like for example design).

So should you modularize your app? If you work in a multi-team environment, or some part of your code will be reusable in other apps, then it is a good idea. Just remember about some not-so-obvious Gradle traps when reusing code between modules. In other scenarios, good rule of thumb is to look at the final app size in terms of complexity, number of screens and features:

  • Is your app going to have two or three user flows, few screens and is just a “CRUD” app? Don’t waste your time and nerves.
  • Are you creating a new Facebook contender app? Then modularization is a wise choice.
Dependency Injection 101 — Hilt

In the previous section I discussed how app modularization is quite a subjective topic. You can work in a single-module or multi-module project and both scenarios will be fine. However, if you were working in a project without any sort of ‘automated’ Dependency Injection, then you are probably bald because I suppose you already pulled all your hair out of your head.

Personally I think this is the most important technique that should be used when developing a scalable Android app. Going back three years, the selection of “Dependency Injection libraries” was limited:

  • Is your app going to be rather big than small and you’re fine with a steep learning curve and fairly cumbersome initial configuration? Go with Dagger 2.
  • Or is your app going to be rather small, and you’re fine with the lack of compile-time safety? Go with Koin (there is also another library written in Kotlin called Kodein but I haven’t seen any usage of it in production apps).

Leaving aside the discussion whether Koin is a “real DI library”, in my first project I decided to use Koin. It was a fine choice for such a sample project. But fortunately, in 2022 we don’t have to compromise between errors in compile-time and easier configuration. Starting from alpha release in June 2020 and becoming stable in April 2021, Hilt is a Dependency Injection library that layers on top of Dagger 2. You can consider it as a wrapper that one of its main purposes is to simplify usage of Dagger under the hood.

@Module
@InstallIn(SingletonComponent::class)
internal object RocketModule {
@Provides
@Singleton
fun provideRocketApi(
retrofit: Retrofit
): RocketApi {
return retrofit.create(RocketApi::class.java)
}
@Provides
fun provideGetRocketsUseCase(
rocketRepository: RocketRepository
): GetRocketsUseCase {
return GetRocketsUseCase {
getRockets(rocketRepository)
}
}
@Provides
fun provideRefreshRocketsUseCase(
rocketRepository: RocketRepository
): RefreshRocketsUseCase {
return RefreshRocketsUseCase {
refreshRockets(rocketRepository)
}
}
@Module
@InstallIn(SingletonComponent::class)
interface BindsModule {
@Binds
@Singleton
fun bindRocketRepository(impl: RocketRepositoryImpl): RocketRepository
}
}
view raw RocketModule.kt hosted with ❤ by GitHub

If you get along with Dagger, you will get along with Hilt too. Thanks to a predefined set of components (like SingletonComponent or ViewModelComponent), there is no more need to create them by yourself. Hilt became my DI library of choice, no matter the project size.

Reactive programming — Kotlin coroutines & Flow

In recent years the so-called “reactive apps” have gained very much in popularity. With libraries such as RxJava, asynchronous background operations became easier to implement (compared to “callback hell”). And tons of Rx operators allow developers to modify their data streams in any way.

However, the Kotlin team has decided to take a different path to deal with concurrency using the concept of suspending functions. At the time of writing the previous article, there was no stable substitute of RxJava’s Observable / Flowable so using coroutines library was fairly limited.

Thankfully, Kotlin Flows became stable just a few months later (August 2019) in version 1.3.0. And in October 2020, Kotlin coroutines library was expanded with SharedFlow and StateFlow implementations — giving developers the long-awaited equivalent of few RxJava’s Subjects, and even offering an alternative to LiveData like described here (Google “deprecated” its own solution just after few years of using it in every single sample/docs? Who would have thought…)

private fun refreshRockets(): Flow<PartialState> = flow<PartialState> {
refreshRocketsUseCase()
.onFailure {
emit(Error(it))
}
}.onStart {
emit(Loading)
}
Data (network) layer — Retrofit & Kotlin serialization

Just like three years ago, Retrofit is still the most widely used HTTP client for Android. The only important difference is that in Retrofit 2.6.0 (June 2019) they have added built-in support for suspend functions so there is no more need to use Deferred object or CoroutineCallAdapterFactory.

interface RocketApi {
@GET("rockets")
suspend fun getRockets(): List<RocketResponse>
}
view raw RocketApi.kt hosted with ❤ by GitHub

However, there is a new kid in town for JSON data parsing called Kotlin serialization (released as stable in October 2020). It’s a library developed by Kotlin creators (as name suggests) that is a great choice if your codebase targets Kotlin Multiplatform. Nevertheless, we can use it for standard Android apps as well. Moreover, it natively supports lots of Kotlin features so it should provide a top-notch interoperability.

So what would the example response model from API look like?

@Serializable
data class RocketResponse(
@SerialName("id")
val id: String = "",
@SerialName("name")
val name: String = "",
@SerialName("cost_per_launch")
val costPerLaunch: Int = 0,
@SerialName("first_flight")
val firstFlightDate: String = "",
@SerialName("height")
val height: Height = Height(),
@SerialName("mass")
val weight: Weight = Weight(),
@SerialName("wikipedia")
val wikiUrl: String = "",
@SerialName("flickr_images")
val imageUrls: List<String> = emptyList()
) {
@Serializable
data class Height(
val meters: Double = 0.0,
val feet: Double = 0.0
)
@Serializable
data class Weight(
val kg: Int = 0,
val lb: Int = 0
)
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

If you already use other data serialization libraries then I believe it looks pretty familiar. And remember to add a converter made by well-known Jake Wharton when cooperating with Retrofit.

Personally I haven’t tested this library thoroughly yet but I suggest giving it a try. Just a side-note: it doesn’t mean you have to abandon Moshi for Kotlin serialization (just don’t use Gson like I did in my previous article). You can find the comparison between the most popular JSON libraries in this talk by Jesse Wilson. It is from Chicago Roboto 2019 but I definitely recommend watching it.

Data (persistence) layer — Room & offline-first approach

In the previous article I used ObjectBox as my local persistence library of choice. It was kind of an experiment to try out a simple NoSQL database approach. Despite everything, nowadays I would stay with the most used persistent library from Jetpack — and that is Room.

Just a few months after I published my first article, Room in version 2.1.0 (June 2019) received built-in support for Kotlin coroutines, and then in version 2.2.0 (October 2019) Google added support for Kotlin Flows. Over the span of next years, Room was still improving with for example built-in support for Enum classes in version 2.3.0 (April 2021) or auto migrations in version 2.4.0 (December 2021). Room is definitely a good example of Google library that is developed in a proper way (if they could work on how to implement it cleanly in a multi-module project, that would be even better).

Moreover, with the added Database Inspector functionality in Android Studio 4.1 (August 2020), it makes working with Android databases a breeze compared to just a few years ago (who remembers dealing with plain SQLite?).

private const val DATABASE_VERSION = 1
@Database(
entities = [RocketCached::class],
version = DATABASE_VERSION
)
abstract class AppDatabase : RoomDatabase() {
abstract fun rocketDao(): RocketDao
}
view raw AppDatabase.kt hosted with ❤ by GitHub
@Dao
interface RocketDao {
@Query("SELECT * FROM RocketCached")
fun getRockets(): Flow<List<RocketCached>>
@Upsert
suspend fun saveRockets(rockets: List<RocketCached>)
}
view raw RocketDao.kt hosted with ❤ by GitHub
private const val APP_DATABASE_NAME = "app_database_name"
@Module
@InstallIn(SingletonComponent::class)
internal object DatabaseModule {
@Singleton
@Provides
fun provideAppDatabase(
@ApplicationContext context: Context
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
APP_DATABASE_NAME
).build()
}
@Singleton
@Provides
fun provideRocketDao(database: AppDatabase): RocketDao {
return database.rocketDao()
}
}

We have Room configured so what’s the deal with the offline-first approach? Basically it’s an app development paradigm that treats local persistence as a single source of truth for any data shown to the user (compared to traditional approach of showing data that comes from the Internet). My sample project implementation for that is based on this article (that is partially inspired by Now In Android app) so if you want to learn more details, I suggest taking a look at these projects as well.

class RocketRepositoryImpl @Inject constructor(
private val rocketApi: RocketApi,
private val rocketDao: RocketDao
) : RocketRepository {
override fun getRockets(): Flow<List<Rocket>> {
return rocketDao
.getRockets()
.map { rocketsCached ->
rocketsCached.map { it.toDomainModel() }
}
.onEach { rockets ->
if (rockets.isEmpty()) {
refreshRockets()
}
}
}
override suspend fun refreshRockets() {
rocketApi
.getRockets()
.map {
it.toDomainModel().toEntityModel()
}
.also {
rocketDao.saveRockets(it)
}
}
}

And the last information regarding Room (and Retrofit) that as far as I see is not widely known — there is no need to inject Dispatchers into your repositories (like in example above) because both libraries perform suspendable operations using Dispatcher.IO by default!

Domain layer — functional use-cases & CancellationException rethrowing

Just like Google suggests in the recently refreshed Guide to app architecture, the domain layer is an optional layer that sits between the data layer and the presentation layer. Its main goal is to separate and reuse business logic and put it into a use-case structure. For really basic apps, the domain layer might be really optional but I believe for any mid-to-large business apps this additional layer of separation is quite crucial.

There are multiple ways to implement your use-cases. You may find implementations using execute or operator fun invoke on the Internet or in your current codebase. However, recently I refreshed the article about writing use-cases in a functional manner and kind of liked the approach proposed by Denis.

fun interface RefreshRocketsUseCase : suspend () -> Result<Unit>
suspend fun refreshRockets(
rocketRepository: RocketRepository,
): Result<Unit> = resultOf {
rocketRepository.refreshRockets()
}

It saves us some boilerplate comparing to approaches listed earlier, while keeping your default ViewModel’s look:

@HiltViewModel
class RocketsViewModel @Inject constructor(
(...)
private val refreshRocketsUseCase: RefreshRocketsUseCase,
(...)
)

For some, these kind of “just call repository” use-cases are totally redundant and realizes Middle Man code smell. That’s a valid point but I’m afraid there is no golden rule to that. Imagine that you have two kinds of use-cases in your project — first like above, and second with multiple data stream transformations. What would you choose:

  • Create use-cases only for complex business logic, and having both use-cases and repositories in your ViewModel’s constructor?
  • Or create use-cases always, some of them might look “useless”, but you keep consistency across your whole app?

It is a personal matter but I prefer the latter (pardon my rhyme).

You may also notice the resultOf extension function. It is based on this article and this issue thread and deals with the issue of CancellationException being swallowed by the default runCatching block (which is not so obvious at the first glance). It also handles TimeoutCancellationException separately because it is a subclass of CancellationException that (probably) doesn’t have to be rethrown. It would be cool if the Kotlin coroutines team could reach a consensus on some approach to mitigate usage of extension functions like this but the issue thread is already 2.5 years old…

Presentation layer — Model-View-Intent (MVI) & Jetpack Compose

This is gonna be good so bring your own popcorn.

The discussion regarding which architecture pattern should we use in the presentation layer is so long-standing that it became a meme for lots of Android developers. Having MVC, MVP, MVVM, MVI, MVx, along with Redux or PRNSAASPFRUICC seem like a very complicated choice, and I didn’t even mention all the possibilities. If there is no standardization for recommended architecture, maybe let’s take a look at what Google suggests (as Google words are treated like an oracle for some)? After all, they did standardize some ideas for the common issues with the rise of Android Jetpack.

Well, even Google is not so consistent in their pathway. Starting with the “basic” MVVM approach when they first created the ViewModel class in a lifecycle library, recently they started adding extra concepts to it like immutable UiState or Unidirectional Data Flow, which are also core assumptions of MVI. If you want to learn more about MVI objectives, I recommend reading these whole series of articles. TL;DR: you can somewhat treat it as a mix of MVVM and Redux.

Who knows, maybe in the future they will add some sort of state reducer to their guides (events reduced to state? Are we getting closer?), use “Intent” nomenclature and voila — migration from MVVM to MVI as a “suggested” architecture is done. And just a side-note: keep in mind that there is no single “best” MVI architecture, it can be implemented in many different ways. My example implementation in the sample project is not the best, none is.

But that’s not the main reason why I chose MVI as my architecture for the presentation layer. The main reason is that it proved its usability in terms of significantly reduced number of UI bugs in production apps due to state immutability and forcing developers to have a single-source-of-truth approach in mind. Thanks to that, most of the bugs reported in MVI apps I’ve been working on were strictly about incorrect business logic behavior. UI bugs were a minority so personally for me it doesn’t matter if it’s MVI, or Google’s current “MVVM++” — I see benefits of using some of the crucial parts of MVI so I recommend using it as well.

abstract class BaseViewModel<UI_STATE : Parcelable, PARTIAL_UI_STATE, EVENT, INTENT>(
(...)
) : ViewModel(),
IntentDelegate<INTENT, PARTIAL_UI_STATE> by IntentDelegateImpl(),
InternalChangesDelegate<PARTIAL_UI_STATE> by InternalChangesDelegateImpl(),
EventDelegate<EVENT> by EventDelegateImpl() {
init {
viewModelScope.launch {
merge(
getIntents(::mapIntents),
getInternalChanges(),
)
.scan(uiState.value, ::reduceUiState)
.catch { Timber.e(it) }
.collect {
savedStateHandle[SAVED_UI_STATE_KEY] = it
}
}
}
}

Of course, there is no rose without thorns. The most common disadvantage I’ve heard is that MVI is so boilerplate. Well, that’s true, compared to other architectures it is really verbose. But you can use tools from different programming worlds like plop.js to automate your boilerplate. Then you don’t have to type it all on your own. Some other drawbacks can be found here and there (if you are more into memes). One can be sure, there will be a new “better” MVx in the future. That’s the way she goes.

Despite everything, I think MVI with its UiStates fits really well with a new declarative way of creating apps UI — using a library called Jetpack Compose. For over ten years Android developers have been doing it the same way — using XML layout and View system. Thankfully, in July 2021, the stable version was released.

@Composable
fun RocketsRoute(
viewModel: RocketsViewModel = hiltViewModel()
) {
(...)
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
RocketsScreen(
uiState = uiState,
(...)
)
}
@Composable
internal fun RocketsScreen(
uiState: RocketsUiState,
onIntent: (RocketsIntent) -> Unit
) {
(...)
if (uiState.rockets.isNotEmpty()) {
RocketsAvailableContent(
snackbarHostState = snackbarHostState,
uiState = uiState,
onRocketClick = { onIntent(RocketClicked(it)) }
)
} else {
RocketsNotAvailableContent(
uiState = uiState
)
}
}

You may notice the collectAsStateWithLifecycle extension function used there. If you are familiar with collecting states in Compose, you have probably already read this article. Basically in Lifecycle version 2.4.0 (October 2021), Google added two new APIs: repeatOnLifecycle and flowWithLifecycle. Its main purpose is to reduce boilerplate by adding some syntactic sugar. So collectAsStateWithLifecycle is going one step further — adds syntactic sugar on them. It is available from version 2.6.0-alpha01 (June 2022) but I prefer to use stable library releases so I added it manually. You can read more about this new API here.

As of today (August 2022), Compose has just released version 1.2.0 and more and more apps are at least incorporating Compose into their codebase thanks to its interoperability APIs like ComposeView and AndroidView. However, there is one topic that effectively discourages many Android developers to write “100% Compose” apps, and that is…

App navigation

You might hear that plain Navigation-Compose is just bad. Even Google suggests using old Navigation component for hybrid apps which will be the most common case in the near future. But we are all here for the “100% Compose” app so how did I solve it?

Believe it or not, I am not a magician. I’m not going to fix all the core concept issues that Navigation-Compose has. I am also not that innovative so at the time of my research I read all the articles about Navigation-Compose, familiarized myself with the issues that other developers faced and tried to choose the most valuable parts from each article.

Do you want scalability that fits into the multi-module project as well? Dig into this article.

@Composable
fun NavigationHost(
navController: NavHostController,
factories: Set<NavigationFactory>,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = NavigationDestination.Rockets.route,
modifier = modifier
) {
factories.forEach {
it.create(this)
}
}
}

Do you want to know how you can abstract away some key navigation concepts like NavigationCommandNavigationDestination or NavigationManager? Then take a look at this and this article.

interface NavigationCommand {
val destination: String
val configuration: NavOptions
get() = NavOptions.Builder().build()
}
sealed class NavigationDestination(
val route: String
) {
data object Rockets : NavigationDestination("rocketsDestination")
data object Back : NavigationDestination("navigationBack")
}
interface NavigationManager {
val navigationEvent: Flow<NavigationCommand>
fun navigate(command: NavigationCommand)
}

Still not satisfied after reading because some comments say that part of the implementation has bugs/is suboptimal? Well, it happens in a programmer’s life. Then turn your head towards this article.

@Singleton
class NavigationManager @Inject constructor(
@MainImmediateScope private val externalMainImmediateScope: CoroutineScope
) {
private val navigationCommandChannel = Channel<NavigationCommand>(Channel.BUFFERED)
val navigationEvent = navigationCommandChannel.receiveAsFlow()
fun navigate(command: NavigationCommand) {
externalMainImmediateScope.launch {
navigationCommandChannel.send(command)
}
}
}

At this point you probably have more questions than answers. Like:

  • Why @MainImmediateScope? As suggested here by Roman Elizarov (Kotlin’s Project Lead for some that don’t recognize that name).
  • Why Channel.BUFFEREDHonestly, I am not totally convinced but I don’t think anyone knows the best answerChannel.CONFLATED seems like a bad idea, some people suggest using Channel.UNLIMITED, others will disagree. For me, Channel.BUFFERED seems like a fair trade-off.

So at the end, the following question might arise: “why bother with Navigation-Compose, there are so many open-source alternatives like Compose DestinationsSimple Stack integrated with Compose or compose-router” (more details about alternatives and comparison between them can be found here). Well, precisely to avoid what happened to the last one mentioned above — being deprecated.

All the libraries in the previous sections are being created by the Square, Google or Kotlin team. So there is a big chance they will not be deprecated overnight, you may find them in multiple projects so they will be battle-proven in many (also large-scale) production apps, and when having an issue — there will be an answer on StackOverflow.

With all due respect to library developers mentioned above (their work is great, it pushes “bigger” brands to improve their products, and open-source is unquestionably the best thing that happened in a programming world), when choosing a technology for a new project, I always ask myself: “is it likely that XYZ will be supported in, let’s say, three years?”. And I suppose that libraries from the “bigger” companies have a slightly higher percentage of “Yes” answers.

Of course, your mileage may vary.

Yet another “SingleLiveEvent” discussion

Okay, navigation logic has been heavily abstracted away (also for potential navigation library swap sometime in the future) but what about ViewModel events? After all, app navigation is also event-based. Well, here we open a can of worms (or Pandora’s box if you are more into mythology).

Everything was more or less clear till the end of 2021 — just use Channel and remember about Dispatchers.Main.immediate, like mentioned in the previous section, discussed here and there. However, things have changed when Google updated their documentation and then published a highly-subjective, much-controversial article that was based on it. So what’s the deal? Michael described it precisely in his article. Basically event emission becomes UiState update, with the necessity of generating unique event ID and callbacking from the view back to ViewModel.

So will this approach work for some events? Yes.

But for all kinds of events? I doubt it.

And is it more cumbersome? As for me, it is.

If you are more interested in issues that the new Google approach generates (because this blog post is already too long to discuss them all), then take a look at the Google’s article responsesdiscussion on Redditdiscussion on Twitter and here. For me, that’s way too much discussion and concerns to adapt seamlessly into the new approach so I will stay with my Dispatchers.Main.immediate Channels for both navigation and event handling.

class EventDelegateImpl<EVENT> : EventDelegate<EVENT> {
private val eventChannel = Channel<EVENT>(Channel.BUFFERED)
override fun getEvents(): Flow<EVENT> = eventChannel.receiveAsFlow()
override suspend fun setEvent(event: EVENT) {
eventChannel.send(event)
}
}
@Composable
private fun HandleEvents(events: Flow<RocketsEvent>) {
val uriHandler = LocalUriHandler.current
events.collectWithLifecycle {
when (it) {
is OpenWebBrowserWithDetails -> {
uriHandler.openUri(it.uri)
}
}
}
}
@Composable
inline fun <reified T> Flow<T>.collectWithLifecycle(
key: Any = Unit,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
noinline action: suspend (T) -> Unit
) {
val lifecycleAwareFlow = remember(this, lifecycleOwner) {
flowWithLifecycle(
lifecycle = lifecycleOwner.lifecycle,
minActiveState = minActiveState
)
}
LaunchedEffect(key) {
lifecycleAwareFlow.collect { action(it) }
}
}

It is not the prettiest solution but I am happy to hear how it can be improved for Compose.

Bonus — Dark Mode
@Composable
fun AndroidStarterTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = pickColorScheme(darkTheme)
(...)
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@Composable
fun pickColorScheme(
darkTheme: Boolean
): ColorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
view raw Theme.kt hosted with ❤ by GitHub

That was a quick one compared to a boilerplate from the previous article. One of the biggest advantages (along with the new Animations API) of Jetpack Compose.

And this is a quick demo of the sample project:

 

 

Conclusion

That was a long journey. Thank you to everyone who read to the end, you did a great job.

Delving deeper and deeper into the meanders of the current state of Android app development, we traversed from serious matters to irony, absurdities and memes. To sum up, lots of technologies have improved in recent years (like Room), certain have matured (like Kotlin coroutines with Flow), few have become worse (like Google library for navigation).

Personally I feel like we are approaching some sort of plateau — maybe a new MVx pattern will appear, maybe there will be a new Navigation-Compose 2.0, maybe Google will come up with a new SingleLiveEvent solution. But other technologies seem as if they are going to be a “final” product. As long as Google deprecates Android in favor of Fuchsia OS or something else… I keep my fingers crossed that this post will age like wine and not like milk.

Here is the link to my sample project’s repository.

Thanks for your time and see you in the second part where I will discuss techniques not strictly related to the app itself but no less important in a programmer’s life (like unit testing, Compose UI testing, linters and CI/CD).

And don’t forget to multi-clap if you learned something new today!

Thanks to Antoni Moroz and Mikołaj Demków for their valuable feedback.

This article was originally published on proandroiddev.com on August 14, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu