Objects with Composables!??

Design Image

In a lot of my testing and examples, I tend to wrap code in objects. You won't see this in production so it may come as a bit of a shock.

I'd like to briefly explain why I do this. I can then link this to all my tutorials so people don't get too confused (thank you if you are coming here from one of those).

There are three main advantages to using objects: It's better, safer, and faster! I realize I'll need to justify those three.

This should be a friendly format for people of all experience levels, but there may be code that newer programmers don't understand. That should not affect the message.

All final GitHub Gists are linked at the end of this post if you find those easier to read.

Standard Practice

Throughout most projection apps, everything is structured like this:

Standard1.kt

/**
* Internal can be accessed within this package (whyuseobjects)
* If we make a new module, it would not be able to access this
*/
@Composable
internal fun TitleBar(title: String) { } // Imagine implementation inside

/**
* Private can only be accessed within this file.
* We cannot access this from MainActivity, for example.
* This does not pollute the global namespace
*/
@Composable
private fun BodyText(text: String) { } // Imagine implementation inside

/**
* Public (no modifier) can be accessed from anywhere in the project.
* MainActivity and other files can call this.
* Can be accessed from other packages (modules) if imported
*/
@Composable
fun StandardApp() { } // Imagine implementation inside

/**
* We mark the preview private so it doesn't show up outside of this file
*/
@Preview
@Composable
private fun StandardAppPreview() = StandardApp()

Everything would exposed to the global namespace, but we are using private, internal, and no modifiers (public).

  • public (no modifier) — anywhere
  • internal — same package only
  • private — same file only
Notice: I've also marked the preview private so you don't accidently call it when typing StandardApp. Even when starting out, I would advise this as it is just easier to work with.

Problems?

You may wonder how this could be a problem. The only public functions here are TitleBar (which we could make private) and StandardApp.

Well, there is no real problem with this. This is good standard practice. However....

The problem comes when you need to test different versions of this (during a prototyping phase, for example). I have projects with 30 versions of a set of compose functions. Each one is doing things in a different way that has different advantages and disadvantages and which version I end up using may depend on a lot of specific details.

Concrete Problem Examples

Copying Files

Alright, let's duplicate our Standard1.kt and name it Standard2.kt (not 02 as we won't go very high with this).

We immediately get two errors we need to fix: namespace collisions with TitleBar()and StandardApp().

Okay, let's fix those. Let's make TitleBar() private. Oh, this doesn't fix it! We still have the internal version of this in Standard1.kt so we would need to make that private as well...

Ok, I want to keep them both internal so let's rename it TitleBar2() and accept the default options. Oh, that also renamed TitleBar() in Standard1.kt so we need to unselect the overloads.... 

overloads image

Ewww

So we obviously also need to rename StandardApp() to StandardApp2(). Oh, and be sure to refactor it and not just rename it so the preview doesn't point to the old version.

We only had two functions to rename so it wasn't too bad, but imagine if we had any classes, interfaces, or anything in that file for test purposes. Every time we make a copy to work on, we have to mess around renaming things.

But wait, there's more

Sorry, it gets worse.

Adding

If we need to add function, templates, or classes, and move stuff around, it gets even more error prone and uncomfortable. Let's try something

First, let make a new copy and call it Standard3 (apologies for the renaming involved).

We need a UiState data class so add this:
data class UiState(
val message: String = "",
val showMessage: Boolean = true
)
Perfect. Now we can track our UiState. Imagine we spend a little time implementing everything to get it all working. We need a function to host this:
@Composable
private fun MssageHost(uiState: UiState) { } // Imagine implementation inside
So we're all set.

But I want to test a different format for the MessageHost() in one of my projects. Let's do that in Standard2.kt as we haven't really many changes to that yet.

We can copy that class into Standard2.kt. Already, we have a problem as there are now two classes with the same name in the global namespace. So we can add our parameter. This gives us a different function signature so the conflict is resolved.
data class UiState(
val message: String = "",
val showMessage: Boolean = true,
val onDismiss: ()-> Unit = {} // Imagine implementation
)
But, we can now accidently call the wrong UiState from the other file. Ok, easy fix. Make the class private. Perfect!
private data class UiState(
val message: String = "",
val showMessage: Boolean = true,
val onDismiss: ()-> Unit = {} // Imagine implementation
)
and we then use it like this:
@Composable
private fun MessageHost(uiState: UiState) { } // Imagine implementation inside
Oh, but that doesn't work because the functions have no idea which UiState they should be using. 

And we get a lovely error message telling us we cant import the class we need even with it being in the same file... 
"Cannot access 'data class UiState : Any': it is private in file. ðŸ˜­
- Ok, the cry emoji is not part of the official message, but I think it should be in this case.

Basically, Kotlin is strict and won't let us use a private class in the signature of a public function; it creates a type that outsiders can't access, breaking the function's usability.

Anyway, the more you try to do the more messy it gets. You can put these UiState classes into separate files, but then you completely lose all the advantages of grouping everything into one file for easier separation.

Summary - No Thanks!

So for all the reasons, I hate trying to prototype this way. I probably have around 100 or so Android Compose projects and a lot of those have multiple prototype versions of things that I need to be able to refer back to at some point in the future.

I can simply copy out one or two files (e.g. XxxUi and XxxViewModel) to throw into another project and know that everything will work. Once everything is working in that new project, I can then simply extract everything into separate files. It only takes a minute or two.

Object Version

All of these problems can be avoided with one extremely simple change. Wrap everything in an object. That's it. It's a super simple solution. 

Once you are happy with the code and want to move it to production, just cut and paste out of the object.

I strongly recommend this pattern when you are not sure what your final code will look like, which for me is pretty much every time I start a new project and need to write code that does something unique.

If I've done it before, I can usually just open the old project and copy and paste my previous solution.

So let's see what it looks like:

Object1.kt

object Object1 {
@Composable
internal fun TitleBar(title: String) { } // Imagine implementation inside

@Composable
private fun BodyText(text: String) { } // Imagine implementation inside

@Composable
fun ObjectApp() { } // Imagine implementation inside
}

@Preview
@Composable
private fun ObjectAppPreview() = Object1.ObjectApp()

Now, we just copy that file to Object12.kt. We also add our missing UiState and the associated MessageHost() screen. 

Note: We do have to do one manual change - we have to change where the preview points (so make sure it points to Object2 instead of Object1).  Even that annoys me, but it is far better than the previous situation.

object Object2{
private data class UiState(
val message: String = "",
val showMessage: Boolean = true,
val onDismiss: ()-> Unit = {} // Imagine implementation
)

@Composable
internal fun TitleBar(title: String) { } // Imagine implementation inside

@Composable
private fun BodyText(text: String) { } // Imagine implementation inside

@Composable
private fun MessageHost(uiState: UiState) { } // Imagine implementation inside

@Composable
fun ObjectApp() { } // Imagine implementation inside
}

@Preview
@Composable
private fun ObjectAppPreview() = Object2.ObjectApp()

Now comes the really nice part. Copy Object12.kt to Object13.kt. Remove the onDismiss action from UiState. Fix the preview. Done. That's it. No horrible name conflicts. Nothing to worry about.

Remember our TitleBar() is internal? We can instantly swap this out in any of our test projects with a simple qualifier:

@Composable
fun ObjectApp() {
TitleBar("This Object Title")
Object2.TitleBar("Object2 Title")
}

This allows us to rapidly prototype and compare different versions when needed. Having the object namespace right in front of it also make it obvious where the resource is coming from.

In a lot of my blog tutorials, I will often have a Shared object or something similar to make it clear when code is being reused by multiple examples.

Let's also go back to our MainActivity.kt and see how that looks. 

setContent {
WhyUseObjectsTheme {
// scaffold etc.
StandardApp()
StandardApp2()
StandardApp3()
Object1.ObjectApp()
Object2.ObjectApp()
Object3.ObjectApp()
}
}

Hmm, not bad. We also have one more secret advantage. We can rename the App entry points within the objects to make it clear what is being tested:

Object1.BasicExample()
Object2.WithOnDismissUi()
Object3.SimpleUiState()

You don't have to do that, of course, but it gives some insights into the possibilities.

My Original Claim

I claimed this was bettersafer, and faster! The holy trinity. My justification:
  • Better - Very easy to use. Reduces stress and irritation.
  • Safter - Hard to mix anything private within an object. Preview is the only real risk as it is outside of the object.
  • Faster - By being easier to use and safer, you spend less time trying to figure out why things aren't working.
Most importantly: when I work with these objects, I feel 😊 instead of ðŸ˜Ÿ.

Conclusion

Don't be afraid of objects! Maybe this habit came from my C++ background, but it has a lot of obvious benefits.

You don't have to follow this pattern, but I was asked about it a bunch of times in my Android discussion group and Salil from that group recommended I explain this clearly. Thank you, Salil. I quite enjoyed writing this post and hope others will find it useful (or at least interesting).

Links

The Gists may be easier to read on some devices I've included everything outside of MainActivity below:

Standard (painful) versions:

Objects (best) versions:

I'll also include a screenshot of my project structure in case anyone finds that helpful:

Project Structure Image


Comments

Popular Posts