UML for Android Engineers

Learn how to draw UML diagrams to document your Android applications. By Massimo Carli.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 6 of this article. Click here to view the first page.

Creating Class Diagrams

So far, you’ve created diagrams explaining some high-level aspects for the Poster Finder app, like:

  • What the app does and who its users are.
  • How the app communicates with external systems.
  • What the structure of the packages and their dependencies are.

When engineers need to change the app, they need to know how the code works. They need to know what the main components are and how they’re related. This also allows you to recognize design patterns — or the lack of them. When the items you need to document are classes, you create a class diagram. In this case, the main rule is still not to describe everything, but just what you need.

Open the MainActivity file in the ui.screen package and look at the following code:

@AndroidEntryPoint
class MainActivity : AppCompatActivity(), NavigationHandler {

  private lateinit var viewBinder: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewBinder = ActivityMainBinding.inflate(LayoutInflater.from(this))
    setContentView(viewBinder.root)
    if (savedInstanceState == null) {
      displayFragment(MovieListFragment())
    }
  }

  override fun displayFragment(frag: Fragment, backStack: String?) {
    supportFragmentManager.beginTransaction()
        .replace(viewBinder.anchorPoint.id, frag).apply {
          backStack?.let {
            addToBackStack(it)
          }
        }
        .commit()
  }
}

As you can see, the main items here are:

  • MainActivity
  • NavigationHandler
  • ActivityMainBinding

What can you say about them? Consider this class diagram:

Class Diagram for MainActivity

Class Diagram for MainActivity

There are lots of things to say here:

  1. You represent each class with different sections of a box. In this diagram, the name MainActivity also tells you that it’s a concrete class. You’ll see later how to represent abstract classes. In Kotlin, you also know that a class is final by default unless you don’t write otherwise. This aspect isn’t important in the context of this diagram.
  2. The second part of MainActivity contains the instance variables for the class. Of course, this isn’t all of them, just the ones you want to describe. In this case, you have the viewBinder property of type ActivityMainBinding. It’s fundamental to note that the property is private. This is because you have a in front of it. You represent public variables with + and protected with #.
  3. The last section should contain the methods for the class. In this case, you have nothing — not because MainActivity doesn’t have methods, but because they’re not important for this diagram. You’ll see cases later when putting method definitions here is useful.
  4. You represent the AppCompatActivity with a simple rectangle with the name of the class in it. This is because you’re not interested in the structure of AppCompatActivity but, as you’ll see soon, in its relations with the other classes. The same is true for ActivityMainBinding.
  5. NavigationHandler is an interface. You represent an interface with a box with two parts. The first contains the name of the interface and the stereotype «interface». In theory, you could represent an interface by writing its name in italic, but this isn’t a good practice, especially if you draw the diagram on paper.
  6. The lower part of the box for NavigationHandler contains the list of operations that, for an interface, are public. This is the reason for the +. In this diagram, you also see how the operation is in italic, but, as said, this isn’t a must — although it might be useful in the case of some default function implementations.

This is how you can represent classes and interfaces, but there’s something more important to represent: their relations.

Representing Relations

Above, you learned how to represent classes and interfaces, but what makes a class diagram useful is the way it represents relations. What’s the relation between MainActivity, AppCompatActivity, ActivityMainBinding and NavigationHandler? Consider the same class diagram:

Class Diagram and Relations

Class Diagram and Relations for MainActivity

In this diagram, you can see three of the most fundamental relations:

  1. MainActivity extends AppCompatActivity. The correct way to explain this is that AppCompatActivity is an abstraction of MainActivity, and you represent this using a solid line ending in an empty triangle. This is implementation inheritance because you’re saying that MainActivity IS-A AppCompatActivity, and so it does all the things that AppCompatActivity does. In this diagram you’re not saying what MainActivity does in a different way from AppCompatActivity and so you are not showing the method you override.
  2. MainActivity also implements the NavigationHandler interface. This is interface inheritance, and you represent it using a dotted line ending with an empty triangle. With this, you’re saying that MainActivity does what NavigationHandler is supposed to do. The fact that MainActivity isn’t abstract implicitly says that it provides implementation for displayFragment().
  3. The relation between MainActivity and ActivityMainBinding is a very interesting one. A very generic way to read this is MainActivity uses ActivityMainBinding, but here you’re saying something more. With the solid line starting with the full diamond and ending with an open arrow, you’re saying that not only does MainActivity use ActivityMainBinding, but that it also creates an instance of it. This happens through ActivityMainBinding.inflate(), but the important thing here is that the lifecycle of ActivityMainBinding is the same as MainActivity, which creates it. Also, the ActivityMainBinding you create isn’t visible outside and, even more important, it’s not shared with anyone. In short, MainActivity owns the instance of ActivityMainBinding it creates. The name for this relation is composition.

These are some of the most important relations you can represent in a class diagram. However, composition isn’t the one you have when using dependency injection. In that case, you have aggregation.

Understanding Aggregation

As an example of aggregation, consider the following class diagram that describes what’s in the repository package. It’s what you can associate with the following code you find in the OMDbMovieRepository.kt file in the repository package.

class OMDbMovieRepository @Inject constructor(
    private val endpoint: OMDbEndpoint
) : MovieRepository {

  override fun getMovies(query: String): Flow<PagingData<MovieDto>> {
    return Pager(
        config = PagingConfig(pageSize = PAGE_LENGTH, enablePlaceholders = false),
        pagingSourceFactory = { OMDbPagingSource(endpoint, query) }
    ).flow
  }
} 

And in the OMDbEndpoint.kt file in the api package:

interface OMDbEndpoint {

  @GET("?plot=full")
  suspend fun findMoviePoster(
      @Query("s") movieTitle: String,
      @Query("page") page: Int,
      @Query("apikey") apikey: String = BuildConfig.omdbApiKey,
  ): OMDbResponse
} 

Class Diagram – Aggregation

Class Diagram - Aggregation

This diagram has a few important things to note:

  1. As you already know, OMDbMovieRepository implements MovieRepository.
  2. OMDbMovieRepository uses OMDbEndpoint with an aggregation relation you represent with a solid line starting with an empty diamond and ending in an open arrow. You can say that OMDbMovieRepository aggregates an OMDbEndpoint. This means that OMDbMovieRepository isn’t responsible for the creation of OMDbEndpoint. Somebody else is providing the reference OMDbMovieRepository needs. A consequence is that the same OMDbEndpoint can be shared between different objects. It also means that the lifecycle of the object of type OMDbEndpoint isn’t bound to the lifecycle of OMDbMovieRepository. In this diagram, you also mention what the cardinality of the aggregation is. For each OMDbMovieRepository, you have one OMDbEndpoint implementation.
  3. You learned that UML is an open language. You can use your own symbols as long as it’s clear what you want to represent. In this case, you’re using a custom stereotype, «Retrofit». This tells the reader that the OMDbEndpoint interface describes an endpoint using the Retrofit framework. This is quite Android-specific, but you’re expecting the reader to be an Android engineer, so that shouldn’t be a problem.
  4. Open the OMDbEndpoint.kt file in the api package, and you’ll see how operation has the suspend modifier. But, how do you represent a suspend function in UML? This isn’t something that UML provides by default. But, again, the goal is to make this clear. In the previous diagram, you just add the suspend modifier to the signature for findMoviePoster(). Pretty simple, right? :]

As you can see, a class diagram is very simple as long as you decide to describe just a few aspects of your code.

You learned how to represent classes, interfaces and the main relations between them. But how do you represent an abstract class?