Chapters

Hide chapters

Real-World Android by Tutorials

First Edition · Android 10 · Kotlin 1.4 · AS 4

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

2. Starting from the Beginning
Written by Ricardo Costeira

Android development can be both straightforward and extremely complex. Not only does the framework keep growing at a ridiculously fast pace, but it also repeatedly reinvents itself — think asynchronous programming, lifecycle and state management and even animations. It’s common to feel overwhelmed by or even lost amidst all the continuous library releases, shiny new features and multiple ways of achieving the same goals.

One section of this book (or, truth be told, the whole book!) wouldn’t be enough to cover everything the Android framework has to offer. However, as you develop real-world apps, you start to notice that apps gravitate around a few common ways to use the framework. Additionally, many design decisions and best practices can be universally applied to produce better software.

In this chapter, you’ll read about some of these design decisions and practices that you can follow in the early development stages. They’ll allow you to build a solid foundation for your app while avoiding over-engineering.

More specifically, you’ll learn:

  • How to structure and organize your app so you can tell what it does just by looking at the package names.
  • Why it’s essential to keep high cohesion and low coupling.
  • How to produce better apps by investing in upfront planning.
  • Why you should use a layered architecture.

Buckle up!

Package by feature approach

First, check out the code you’ll work with by opening the starter project in the material for this chapter, and examining its contents. Expand the com.raywenderlich.android.petsave package. Did you hear that? That was the project screaming its purpose at you!

Figure 2.1 — Screaming Architecture
Figure 2.1 — Screaming Architecture

This project is organized in a package by feature structure. Everything that’s related to a feature, and only to that feature, is stored inside the same package. Code shared by two or more features is stored in separate common packages. This type of package organization has a few advantages:

  1. Just by looking at the package structure, you easily get a feeling for what the app does. Some people also like to call this a screaming architecture — hence the awful “screaming” joke attempt earlier.

  2. You end up with packages that not only have high cohesion, they’re also either loosely coupled or completely decoupled from one another. Cohesion and coupling are two very important metrics in software development that you should always consider.

Figure 2.2 — Not Screaming vs Screaming Architecture
Figure 2.2 — Not Screaming vs Screaming Architecture

High cohesion

Cohesion refers to the relationship between different programming elements. The stronger the connection between code inside a component, the more cohesive that component is.

For instance, imagine you have a class that’s responsible for applying a cute filter to a cat’s picture, called CatFilter.

class CatFilter(private val picture: Picture) {

  // properties related to filter and picture state
  // ...

  private fun parsePixels() {
    // store individual pixels and relationships between them
  }

  private fun filterPixels() {
    // apply the filter to each pixel
  }

  private fun smoothenResult() {
    // apply picture smoothing techniques
  }

  fun apply(): Picture {
    // use methods above
  }

  // other methods
}

The methods and properties of this class are all closely related to each other, which means that the class is highly cohesive.

Now, imagine the case where you start adding more responsibilities to the class. Not only does CatFilter apply a filter, but now it also saves and loads the result with the help of the file system. You’ll start having elements in the class that have nothing to do with each other — parsePixels() and save(picture: Picture) have completely different purposes.

In other words, your class will now have a lower degree of cohesion.

Low coupling

Coupling has to do with dependencies between programming elements. Continuing from the previous example, say you move the I/O logic to another class called CatPictureFileSaver.

class CatPictureFileSaver {

  fun save(picture: Picture) {
    // file writing code.
    // Calls compression and encoding methods.
  }

  fun load(picturePath: String): Picture {
    // file reading code
    // Calls decompression and decoding methods.
  }

  private fun compress(picture: Picture): CompressedPicture {
    // fancy compression algorithm
  }

  private fun encode(
    compressedPicture: CompressedPicture
  ): ByteArray {
    // byte encoding
  }

  // other methods
}

This new class also has methods to compress/decompress and encode/decode the image, which are strongly related to its purpose. Nice, now CatFilter and CatPictureFileSaver are two highly cohesive classes!

After some time, requirements change. You now have to cache the intermediate results of the filtering. To implement this, you call the persistence methods of CatPictureFileSave directly in a few different places in CatFilter.

This may seem like the logical way to accomplish your goals but, by doing so, you’re forcing CatFilter to be tightly coupled with CatPictureFileSaver. Consider a scenario where a requirement change dictates that you drastically change or even remove CatPictureFileSaver. Due to the coupled nature of the classes, you’d have to make significant changes to CatFilter as well.

On the other hand, if you have something like a CatPictureSaver interface that CatPictureFileSaver extends, and have CatFilter depend on it, then the classes would be loosely coupled. Changes to CatPictureFileSaver that don’t affect this interface would likely not affect CatFilter at all.

interface CatPictureSaver {
  fun save(picture: Picture)
  fun load(picturePath: String): Picture
}

class CatPictureFileSaver : CatPictureSaver {
  // interface overrides and private methods/properties
}

class CatFilter(
  private val picture: Picture,
  private val pictureSaver: CatPictureSaver
) {

  // code...

  private fun filterPixels() {
    // ...
    // CatFilter knows nothing about save method's inner
    // workings!
    pictureSaver.save(filteredPicture)
    // ...
  }

  // more code...
}

The interface would ideally use generic naming to keep your implementation options open. For example, a CatPictureSaver interface with a method named savePictureToFile() would be a bad choice. You’d have to change the method name if you stop using the file system to save pictures!

Aiming for orthogonality

Cohesion and coupling can be observed — and achieved — at different conceptual levels of a project. You just saw a few examples with classes, but the same principles apply to the project’s package structure. Generally speaking, these patterns improve software by:

  • Making it easier to maintain, less risky to change and more future proof.
  • Allowing it to be orthogonal. When software is orthogonal, you can change its components freely without affecting the other components’ behavior. This is possible because, inside each component, the code is closely related (cohesion) and doesn’t depend directly on other components (coupling).

A good way to achieve cohesion is to ensure your code components each have a single responsibility. To keep components decoupled, you can use things like interfaces or polymorphism. In other words, by following the SOLID principles, you’ll automatically follow these two principles as well.

Note: SOLID stands for Single responsibility principle, Open closed principle, Liskov substitution principle, Interface segregation principle and Dependency injection principle. These are the priciples that establish practices that lend to development software with consideration for maintainability and extensibility of a project.

You should definitely strive for high cohesion and low coupling, but be warned: Software principles are addictive. As with design patterns, you can easily get carried away and start applying the principles to every corner of your codebase.

If you go down this rabbit hole, you’ll end up with an over-engineered app that lost track of its initial purpose. Remember: all things in moderation.

Full stack features through layers

Back to the project. Locate the common package and expand it.

Figure 2.3 — Package by Layer Architecture
Figure 2.3 — Package by Layer Architecture

You’ll see a few other packages inside, but the three main ones are:

  • domain: Home to all the use cases, entities and value objects that describe the domain of the app.
  • data: Layer responsible for enabling all the interactions with data sources, both internal, like shared preferences or the database, and external, like a remote API.
  • presentation: Where the Android framework does most of its heavy lifting, setting up the UI and reacting to user input.

You might have seen Android projects where the outer folder structure is divided into packages named like this. This kind of package organization is called package by layer since it separates the code by conceptual layer and responsibility. You’ll read why this approach doesn’t scale below.

Boundaries between layers

Typically, the conceptual layers of an app have well-defined boundaries between them so they stay decoupled from one another. You create these boundaries using interfaces.

It’s especially common for projects that follow clean architecture to display such boundaries. Clean architecture goes the extra mile, inverting the dependencies to ensure that they only flow inwards toward the domain layer at the center. In other words, the domain layer never depends on other layers. This idea borrows from hexagonal architecture. As you’ll see in the next chapters, PetSave borrows it as well.

Why use layered features?

While organizing top-level packages by layer works for simple, small apps, it will become extremely difficult to deal with once the app gets more complicated. Imagine an app with dozens of different screens and features. The cognitive load required to be aware of which parts of the enormous presentation package called which specific part of the domain and data package would be astronomical.

This type of organization leads to many problems:

  • Features become extremely difficult to implement since it’s nearly impossible to understand the entire code structure at scale.
  • Developers may duplicate behavior that already exists elsewhere in the codebase without realizing it.
  • Routine maintenance turns into something out of a nightmare.

Now, it’s not all bad news. If you use it right, packaging by layer can be beneficial. Packaging by feature on the outside and by layer on the inside enables you to:

  • Have highly cohesive, loosely coupled code throughout your app.
  • Reduce your cognitive load when dealing with each layer. For instance, you can change the domain layer of a feature while mostly ignoring the presentation and data layers, due to the interface boundaries.
  • Test entire layers by replacing other layer dependencies with mocks or fakes.
  • Easily refactor a layer’s implementation without messing with the other ones. This is somewhat rare, but it does happen.

Figure 2.4 — Layer Versus Feature Packages
Figure 2.4 — Layer Versus Feature Packages

Each feature will have this structure. Anything that’s shared between different features will live in the common package. You’ll set up the layers in the upcoming chapters. There, you’ll also see how defining such a package architecture enables the app to scale more easily.

Having learned how to structure an app in a maintainable and future-proof way, the next step is to go through the process of preparing yourself for the actual development.

Bridging requirements and implementation

Before starting any software project, Android or not, you need to understand what the system is supposed to do, and how it’s supposed to do it. You do this by analyzing the requirements that you gathered in the initial stages of the system’s development.

These requirements typically come from a joint effort that might involve project owners, senior engineers, architects and stakeholders. In order to achieve the requirements (the what), you build features (the how).

No, no, don’t panic, you’re not about to go through the whole software development cycle! The requirements analysis for PetSave is complete, and you’re already aware of the feature set to implement. On top of this, you can deduce both the what and the how from the features. Now, you’ll dive directly into the software design and development phase.

Mastering your domain

There’s no fixed way to start this phase. One reasonable approach is to get acquainted with the domain of the problem you’re solving. This is valid for both new projects and when you are new to an existing project. The domain is the subject area the software applies to or provides a solution to. In other words, it’s the environment you extract the requirements from, and it’s where the app’s features act.

Do you have any idea what the domain for PetSave might be? Looking at a few of the feature requirements should give you a hint:

PetSave’s features:

  1. A user can see a list of animals near them, possibly from different organizations.
  2. A user has the ability to search for animals and get matches according to several different types of filters.
  3. A user can report a lost pet.
  4. There should be an onboarding process to better match a user with a potential pet.

For this app, you’ll work in the domain of pet adoption and fostering. You’ll juggle concepts like animals, breeds, species, adoption organizations, animal health and training.

In the next chapter, you’ll start reasoning about the domain to define its entities. Also, if you’re wondering why the name of the app doesn’t completely match the domain, it’s just that “PetSave” sounds cooler than “PetAdopt” or “PetFoster”. At least, that’s what the app’s stakeholders think, and you know that they’re always right — even when they aren’t!

Knowing the problem space

OK, so why should you know about the app’s domain? Here are a few good reasons:

  • To properly implement features that cover all the requirements, you need to understand the domain. If you don’t establish a clear boundary around the requirements, how can you be sure you’re covering all possible use cases? If you don’t understand what you’re working with and why, you’re bound to make mistakes, which usually cost money!

  • Your app’s users usually have a deep understanding of the domain. This means that you can predict how people will use your app by being aware of the domain’s caveats and intricacies. You can anticipate problems and even do a better job testing your app.

  • It’s an excellent way to communicate with non-technical people about technical stuff. The domain is a language everyone involved in the project shares.

  • Requirements might be incomplete or not even make sense. When you understand the domain, you can look at the requirements objectively, contributing to their completeness and checking their sanity.

The list goes on, but you get the point. Domain knowledge is critical! However, it takes time to learn. Some domains are quite complex or can be extremely important to get right. For instance, it might be OK to miss out on some details of PetSave’s domain, but the domain of a water treatment and supply station is different.

On an important note, don’t expect to master every little detail instantly. Doing so can be challenging, especially before you get your hands dirty with the code. Until you see the app’s rules and logic flow through the code, the domain is just a set of abstract ideas about a subject that you might have never even heard about before.

So do take some time to get familiar with the domain initially, but don’t worry about going much deeper than that. You’ll inevitably learn significantly more while you’re implementing the code.

Software is liquid

Now that you’re aware of the app’s domain, you’re probably eager to start the actual implementation. However, there are still a few things to consider before that. They’ll be quick, promise!

One of the most important tenets to consider is that requirements change. Your stakeholders’ needs are not set in stone, and your software shouldn’t be, either.

You should build your app in a way that makes it relatively easy or, in some unfortunate cases, just possible, to both add and remove behavior. This need for extensibility is one of the reasons why you use development patterns and architectures.

Nevertheless, you do have to be careful to avoid over-engineering things: Consider the future possibilities, but always focus more on what’s happening in the present.

Another important thing is to be aware of is that building software is an iterative process, sometimes due to changing requirements. You analyze what you need to implement, implement it and possibly return to the analysis stage in the future.

Don’t be bummed if this happens. It’s natural, and good! It usually means your software is gaining traction.

Devising a plan of attack

It’s time for you to look at the features you’ll develop in this section. You’ll reason about what you need to do for each one and try and predict any difficulties that might arise. Here’s a quick recap of the features:

  • Animals near you: Displays a list of animals near you, according to your postal code and a specific distance threshold.
  • Search: Searches for an animal by name, type and age.

Animals near you

For simplicity, you’ll use hard-coded postal code and distance values for now because you won’t implement the mechanism to get the real values until later.

Designing the UI

The list part seems fairly easy: It screams RecyclerView all over the place. And where there’s a RecyclerView, there’s also an Adapter. Your requirement is to show a fairly simple item with the animal picture and name, so one ViewHolder should be sufficient. The UI code seems fairly straightforward.

Adding data sources

For the data source, you’ll use a remote API called Petfinder. Petfinder matches almost all the data needs of the entire app, along with everything you need for these specific features.

This one’s also easy: Retrofit is the standard for Android networking nowadays. You might need to adjust the data so it matches the domain, and a few interceptors might also come in handy. The API returns chunks of paginated data, so you’ll have to handle that pagination. Otherwise, it should be a fairly standard implementation.

The app needs to work offline, so you’ll need to store the data in some format. This complicates things a little, as having both a cache data source and a network data source can become tricky to manage.

The safest approach is to follow a single source of truth implementation. Keep the cache in sync with the network, but ensure that the UI accesses only the cache. This way, the app displays the latest data both on and offline. Still, keeping everything in sync can be tricky, so you should expect some complexity.

Relational databases perform well for reading large amounts of data. You’ll use Room to implement the cache.

Modeling the domain

You’ll define the core domain entities in the next chapter. However, you’ll leave the use cases until you’re ready to connect all the layers. Use cases are feature specific so by implementing them later, development will flow much more naturally. Implementing them now would require a lot of abstract thinking, which would just waste your time.

Making a dry run

Your goals for this feature are: Fetch data from the API, store it in the database, feed the UI with data from the database, display the data with the RecyclerView and handle possible errors. When considering what you’ll implement ahead of time, it always helps to try and put yourself in the user’s shoes… while also not forgetting that you’re the developer. Try to understand how the user will feel while using your app. At the same time, keep an eye out for potential technical issues.

Picture for a moment that you’re a user opening your app for the first time:

  • You open the app and see a blank screen. This happens because the cache is still empty. Whoops! That’s not good UX (user experience). You should at least show a progress bar while the adapter has no data.
  • As soon as the data arrives, your app stores it. Since the UI feeds itself from the cache, the progress bar should disappear while the RecyclerView displays the data.
  • You scroll down the list of animals. Whoops! Another issue: Nothing’s triggering a request for more data, so you’re stuck with just the first chunk of cached data. To fix this, you need to add a callback for the RecyclerView’s scrolling that triggers a new network request when the scroll reaches a specific position.

This is a simple feature with a simple use case. Still, as you can see, there were some details missing. That’s OK. There are probably even more missing, but you’ll only find them at development time. Still, dry runs like this help you find the most noticeable issues before touching the code.

This sums up the “animals near you” feature. On to the next one!

Search

The goal here is to type a search query, possibly filter it and show either results or a warning saying that there are no results. This feature will be harder to implement, given the added complexity of the mutating state.

Designing the UI

OK, so you’ll need a search bar, a few drop-down menus with options for filtering, and a RecyclerView to display the results. You’ll display animals as well, so you’ll be able to reuse the RecyclerView from the other feature.

Adding data sources

The search should go through the cache and, if it doesn’t find anything, try to find results via a network call. If there aren’t any results there, either, then you show the “no results” warning.

It seems like you can reuse the data handling code from “animals near you”. Nice! You might need to add new queries and change requests, but that shouldn’t be a problem.

Modeling the domain

It shouldn’t come as a surprise that you’ll reuse the domain entities. You’ll probably need to create new ones specific to this feature to better handle the search state, but that’s something you can only be sure about when you dig into the code.

As for use cases, you’ll follow the same approach as with the previous feature.

Making a dry run

Here are your goals: Write a query, maybe filter it, search for results, show them in the RecyclerView, show a warning if no results exist and handle possible errors.

Now, thinking like a user:

  • You get to the screen, and there’s nothing there other than the search interface. Maybe adding a placeholder for the items is a good idea.
  • You start searching, and items from the cache might appear. If not, the app makes a network request, but no items will appear until the response arrives. Again, a progress bar can save the day.
  • If the app finds no results, a warning shows up. Otherwise, a list of cute little animals, ready to be loved, appears.

Again, the trick here will be to manage the constantly mutating state. You’ll have to juggle the constant changes due to:

  • Result list values that can be empty.
  • The search query.
  • The search filters.

There will be a lot of intermediate states, so you’ll have to be careful to avoid inconsistencies. But there’s no use in rambling about it now. You know it’ll be tricky, so wait until you’re working on the code to figure out the best way to do it. The important thing is that you’re already aware of the increased difficulty.

So, what’s the plan?

At this point, you know the features and their main pain points. You’re ready to begin development, but where should you start?

You can’t build a house starting from the roof, and the same is valid in software. There’s no use in starting to develop features without a solid foundation to build them on. If you try, be ready for future refactoring.

A great way to build a solid base is by starting with feature-independent code. This is why, in the next chapter, you’ll start with the most central part: the domain layer.

Key points

  • Packaging by feature enables you to work out what an app does just by looking at the packages.
  • You should strive for high cohesion and low coupling on every level of your apps.
  • Separating the code of each feature by internal layers is an excellent way to have maintainable and flexible code, while also achieving high cohesion and low coupling.
  • Being familiar with your app’s domain enables you to better understand what it does and what its users expect.
  • Thinking about the implementation ahead of time is an effective way of planning your work to avoid extra refactoring in the future.
  • Putting yourself in your users’ shoes helps you identify potential problems with your app.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.