UML for Android Engineers
Learn how to draw UML diagrams to document your Android applications. By Massimo Carli.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
UML for Android Engineers
50 mins
- Getting Started
- Understanding UML
- Deciding What to Document
- Creating Use Case Diagrams
- Creating Complex Use Case Diagrams
- Creating a Deployment Diagram
- Creating a Dependency Diagram
- Creating Class Diagrams
- Representing Relations
- Understanding Aggregation
- Representing Abstract Classes
- Representing Static Members in UML
- Representing Just What You Need: Another Example
- Using Stereotypes
- Creating Dynamic UML Diagrams
- Representing Objects
- Understanding Collaboration Diagrams
- Understanding Sequence Diagrams
- Understanding Asynchronous Invocations
- Understanding State Diagrams
- Diving Deeper Into State Diagrams
- Where to Go From Here?
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:
There are lots of things to say here:
- 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.
- 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 theviewBinder
property of typeActivityMainBinding
. 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 #. - 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. - 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 ofAppCompatActivity
but, as you’ll see soon, in its relations with the other classes. The same is true forActivityMainBinding
. -
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. - 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:
In this diagram, you can see three of the most fundamental relations:
-
MainActivity
extendsAppCompatActivity
. The correct way to explain this is thatAppCompatActivity
is an abstraction ofMainActivity
, and you represent this using a solid line ending in an empty triangle. This is implementation inheritance because you’re saying thatMainActivity
IS-AAppCompatActivity
, and so it does all the things thatAppCompatActivity
does. In this diagram you’re not saying whatMainActivity
does in a different way fromAppCompatActivity
and so you are not showing the method you override. -
MainActivity
also implements theNavigationHandler
interface. This is interface inheritance, and you represent it using a dotted line ending with an empty triangle. With this, you’re saying thatMainActivity
does whatNavigationHandler
is supposed to do. The fact thatMainActivity
isn’t abstract implicitly says that it provides implementation fordisplayFragment()
. - The relation between
MainActivity
andActivityMainBinding
is a very interesting one. A very generic way to read this isMainActivity
usesActivityMainBinding
, 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 doesMainActivity
useActivityMainBinding
, but that it also creates an instance of it. This happens throughActivityMainBinding.inflate()
, but the important thing here is that the lifecycle ofActivityMainBinding
is the same asMainActivity
, which creates it. Also, theActivityMainBinding
you create isn’t visible outside and, even more important, it’s not shared with anyone. In short,MainActivity
owns the instance ofActivityMainBinding
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
}
This diagram has a few important things to note:
- As you already know,
OMDbMovieRepository
implementsMovieRepository
. -
OMDbMovieRepository
usesOMDbEndpoint
with an aggregation relation you represent with a solid line starting with an empty diamond and ending in an open arrow. You can say thatOMDbMovieRepository
aggregates anOMDbEndpoint
. This means thatOMDbMovieRepository
isn’t responsible for the creation ofOMDbEndpoint
. Somebody else is providing the referenceOMDbMovieRepository
needs. A consequence is that the sameOMDbEndpoint
can be shared between different objects. It also means that the lifecycle of the object of typeOMDbEndpoint
isn’t bound to the lifecycle ofOMDbMovieRepository
. In this diagram, you also mention what the cardinality of the aggregation is. For eachOMDbMovieRepository
, you have oneOMDbEndpoint
implementation. - 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. - 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 forfindMoviePoster()
. 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?