Chapters

Hide chapters

Dagger by Tutorials

First Edition - Early Access 1 · Android 11 · Kotlin 1.4 · AS 4.1

Section III: Components & Scope Management

Section 3: 3 chapters
Show chapters Hide chapters

Section IV: Advanced Dagger

Section 4: 3 chapters
Show chapters Hide chapters

Section V: Introducing Hilt

Section 5: 5 chapters
Show chapters Hide chapters

2. Meet the Busso App
Written by Massimo Carli

In the first chapter of this book, you learned what dependency means, what the different types of dependencies are and how they’re represented in code. You learned details about:

  • Implementation Inheritance
  • Composition
  • Aggregation
  • Interface Inheritance

You saw examples of each type of dependency and you understood which works better in various situations. Using UML diagrams, you also learned why dependency is something you need to control if you want your code to be maintainable. You saw why applying these principles using design patterns is important to make your code testable.

So far, this book has contained a lot of theory with many concepts you need to master if you want to successfully use libraries like Dagger or Hilt for Dependency Injection (DI) on Android. Now, it’s time to move beyond theory and start coding.

In this chapter, you’ll get to know the Busso App, which you’ll work on and improve throughout this book. It’s a client-server app where the server is implemented using Ktor.

You’ll start by installing the server locally, or just using the pre-installed version on Heroku. Then you’ll configure, build and run the Busso Android App.

The version of the app you start with is basic, not something to be proud of. You’ll spend the last part of the chapter understanding why and taking the first steps to improve it.

The Busso App

Throughout this book, you’ll implement the Busso App, which allows you to find bus stops near you and information about arrival times. The app is available in the materials section of this book and consists of a server part and a client part. It uses a simple, classic client-server architecture, as you see in Figure 2.1:

Figure 2.1 — Client Server Architecture
Figure 2.1 — Client Server Architecture

The UML diagram in Figure 2.1 is a deployment diagram that shows you many interesting bits of information:

  1. The big boxes represent physical machines like computers, devices or servers. You call them nodes.
  2. The boxes with the two small rectangles on the left edge are components. The Busso App component lives in the device while the Busso Server lives on a server machine, probably in the cloud.
  3. The Busso Server exposes an interface you represent using something called a lollipop. You can use a label to give information about the specific protocol used in the communication — in this case, HTTP.
  4. The Busso App interacts with the HTTP interface the Busso Server provides. Represent this with a semicircle embracing the lollipop.

Before going into the details of these components, run the app using the following steps.

Installing and running the Busso Server

Busso Server uses Ktor, which you can open using IntelliJ.

Note: You don’t need to know the details now, but if you’re curious, you can read more about Busso Server in the Appendix of this book.

Note: If you don’t want to run the Busso Server locally, there’s an existing installation running on Heroku. Just skip to the next section to find out how to use it.

Open the BussoServer project and you’ll get the following structure of directories:

Figure 2.2 — Busso Server File Structure
Figure 2.2 — Busso Server File Structure

Now, open Application.kt and find main function, which looks like this:

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

Click on the arrow on the left of the code, as in Figure 2.3:

Figure 2.3 — Run Busso Server from the code
Figure 2.3 — Run Busso Server from the code

Alternatively, you can use the same icon in the Configurations section of the IntelliJ toolbar, as shown in Figure 2.4:

Figure 2.4 — Run Busso Server from Configurations
Figure 2.4 — Run Busso Server from Configurations

Now the server starts and you’ll see some log messages in the Run section of IntelliJ, ending with something like:

2020-07-30 01:12:01.177 [main] INFO  Application - No ktor.deployment.watch patterns specified, automatic reload is not active
2020-07-30 01:12:03.320 [main] INFO  Application - Responding at http://0.0.0.0:8080

If you get this, the Busso Server is running. Congratulations!

Now, your next step is to connect the server to the Busso Android App.

Building and running the Busso Android App

In the previous section, you started the Busso Server. Now it’s time to build and run the Busso Android App. For this, you need to:

  1. Define the address of the server
  2. Configure network security
  3. Build and run the app

Note: If you use the Busso Server on Heroku, you can skip this configuration and follow the instructions in the section, “Running the Busso Server on Heroku”.

Defining the server address

Use Android Studio to open the Busso project in the starter folder of the material for this chapter. You’ll see the file structure in Figure 2.5.

Figure 2.5 — Busso Android File Structure
Figure 2.5 — Busso Android File Structure

Configuration.kt contains the following code:

// INSERT THE IP FOR YOUR SERVER HERE
const val BUSSO_SERVER_BASE_URL = "http://<YOUR SERVER IP>:8080/api/v1/"

The Busso App doesn’t know where to connect yet. You need to change this to include the IP of your server. But how do you determine what that IP is? You need to find the IP of your server machine in your local network.

Note: You can’t just use localhost or 127.0.0.1 because that would be the IP address of your Android device, not the device where the Busso Server is running.

If you’re using a Mac, open a terminal and run the following command if you’re using ethernet:

# ipconfig getifaddr en0

Or this, if you’re on wireless:

# ipconfig getifaddr en1

You’ll get an IP like this:

# 192.168.1.124

Remember that your specific IP will be different from the one shown above.

On Windows, run the ifconfig command to get the same information from a terminal prompt.

Now, in Configuration.kt, replace <YOUR SERVER IP> with your IP. With the previous value, your code would be:

// INSERT THE IP FOR YOUR SERVER HERE
const val BUSSO_SERVER_BASE_URL = "http://192.168.1.124:8080/api/v1/"

Great, you’ve completed the first step!

Configuring network security

As you can see, the local server uses the HTTP protocol, which requires additional configuration on the client side. Locate and open network_security_config.xml as a resource of XML type, like in Figure 2.6:

Figure 2.6 — Allow the HTTP protocol from the Android Client
Figure 2.6 — Allow the HTTP protocol from the Android Client

You’ll get the following XML content:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true"><!-- YOUR SERVER IP --></domain>
  </domain-config>
</network-security-config>

Next, replace <!-- YOUR SERVER IP --> with the same IP you got earlier.

Using the IP value from the previous example, you’d end up with:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">192.168.1.124</domain>
  </domain-config>
</network-security-config>

Now, everything’s set up and you’re ready to build and run.

Building and running the app

Now, you can run the app using an emulator or a real device by selecting the arrow shown in Figure 2.7:

Figure 2.7 — Run the Busso Android App
Figure 2.7 — Run the Busso Android App

When the app starts for the first time, it’ll show the Splash Screen and then the dialog asking for location permissions, as in Figure 2.8:

Figure 2.8 — Asking for Permission
Figure 2.8 — Asking for Permission

Of course, if you want to use the app, you have to select the Allow while using the app option. This will bring you to the screen shown in Figure 2.9:

Figure 2.9 — Bus Stops close to you
Figure 2.9 — Bus Stops close to you

If you have a similar result, great job! You’ve successfully run the Busso Android App.

You’re getting fake data — you’re not necessarily in London :] — but that data comes from the Busso Server. For each bus stop, you’ll see something similar to Figure 2.10:

Figure 2.10 — Bus Stop data
Figure 2.10 — Bus Stop data

You can see:

  • An indicator of the bus stop, like M in the picture.
  • Your distance from the bus stop in meters, like 114 m.
  • The name of the bus stop. For example, Piccadilly Circus Haymarket.
  • The destination: RW Office

Now, select one of the cards and you’ll come to a second screen:

Figure 2.11 — Arrival time for the Bus
Figure 2.11 — Arrival time for the Bus

Below the information regarding the selected bus stop in the header, you can see a list of all the lines with their destinations, as well as a list of arrival times. Again, the data is fake but comes from the Busso Server.

The Busso App is now running and you’re ready to start the journey through design principles and, specifically, dependency injection.

Running the Busso Server on Heroku

As mentioned earlier, you might not want to build and run the Busso Server on your own machine. Instead, you can use a running app that’s available on Heroku at the following address:

https://busso-server.herokuapp.com/

Using this server has two main advantages:

  • You don’t overload your machine running the Busso Server Process.
  • The app can use the HTTPS protocol, while the local installation uses HTTP. Using the HTTPS protocol, you don’t need the configuration in Figure 2.6 anymore.

You can easily verify that the server is up and running by accessing the previous URL with your favorite browser. If you use Chrome, you’ll get what is shown in Figure 2.12:

Figure 2.12 — Accessing the public Busso Server
Figure 2.12 — Accessing the public Busso Server

Configuring the Busso App for the Heroku server

To use the server installation on Heroku, you need to enter the following code into Configuration.kt:

const val BUSSO_SERVER_BASE_URL = "https://busso-server.herokuapp.com/api/v1/"

Next, you need to put a valid value into the xml resource folder in network_security_config.xml, like this:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">0.0.0.0</domain>
  </domain-config>
</network-security-config>

The specific IP you use here isn’t important as long as it’s a valid IP address.

Improving the Busso App

Do you like the Busso App? Well, it works, but you can’t say the quality is the best. But what are the problems, and how can you fix them?

Specifically, the Busso App has:

  • A lot of copied and pasted code that leads to repetition you should avoid.
  • No concept of lifecycle or scope.
  • No unit tests.

In the following sections, you’ll learn more about these problems and get some ideas for solving them.

Reducing repetition

SplashActivity.kt contains the following code:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  makeFullScreen()
  setContentView(R.layout.activity_splash)
  // 1
  locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // 2
  locationObservable = provideRxLocationObservable(locationManager, permissionChecker)
  // 3
  navigator = NavigatorImpl(this)
}

Here, you:

  1. Get the reference to LocationManager using getSystemService().
  2. Invoke provideRxLocationObservable() to get a reference to Observable<LocationEvent>, which you’ll subscribe to later. This will provide location events.
  3. Instantiate NavigatorImpl, passing the reference to Activity as the primary constructor parameter.

In BusStopFragment.kt, you’ll find the following code:

override fun onAttach(context: Context) {
  super.onAttach(context)
  // 1
  locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // 2
  locationObservable = provideRxLocationObservable(locationManager, grantedPermissionChecker)
  navigator = NavigatorImpl(context as Activity)
}

The code in onAttach() does basically the same thing as the previous example, because it:

  1. Gets the reference to LocationManager.
  2. Invokes provideRxLocationObservable() to get Observable<LocationEvent>.
  3. Creates another instance of NavigatorImpl, passing the reference to the same Activity as in the previous example.

Better would be to share some of the objects between different components to reduce code duplication. This is a problem that dependency injection helps solve, as you’ll see in the following chapters.

Taking scope and lifecycle into consideration

In any Android app, all the other components of the same app should share some objects, while other objects should exist while a specific Activity or Fragment exists. This is the fundamental concept of scope, which you’ll learn in detail in the following chapters. Scope is a vital part of resource management.

Adding application scope

Look at useLocation() in BusStopFragment.kt:

private fun useLocation(location: GeoLocation) {
  context?.let { ctx ->
    disposables.add(
        provideBussoEndPoint(ctx) // HERE
            .findBusStopByLocation(location.latitude, location.longitude, 500)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(::mapBusStop)
            .subscribe(busStopAdapter::submitList, ::handleBusStopError)
    )
  }
}

Every time you invoke provideBussoEndPoint(ctx), you create a different instance of the implementation of the BussoEndpoint interface that Retrofit provides.

Note: Retrofit is a library created by Square that allows you to implement the network layer in a declarative and easy way.

This also happens in getBusArrivals() in BusArrivalFragment.kt.

private fun getBusArrivals() {
  val busStopId = arguments?.getString(BUS_STOP_ID) ?: ""
  context?.let { ctx ->
    disposables.add(
        provideBussoEndPoint(ctx)
            .findArrivals(busStopId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(::mapBusArrivals)
            .subscribe(::handleBusArrival, ::handleBusArrivalError)
    )
  }
}

Only one instance of a BussoEndpoint implementation should exist, and BusStopFragment, BusArrivalFragment and all the other places where you need to access the server should all share it.

BussoEndpoint should have the same lifecycle as the app.

Adding activity scope

Other objects should have a different lifecycle, such as the Navigator implementation in NavigatorImpl.kt located in libs/ui/navigation, as shown in Figure 2.13:

Figure 2.13 — The NavigatorImpl class
Figure 2.13 — The NavigatorImpl class

class NavigatorImpl(private val activity: Activity) : Navigator {
  override fun navigateTo(destination: Destination, params: Bundle?) {
    // ...
  }
}

As you can see, NavigatorImpl depends on the Activity that it accepts as the parameter in its primary constructor.

This means that NavigatorImpl should have the same lifecycle as the Activity you use for its creation. This currently isn’t happening, as you can see in onAttach() in BusStopFragment.kt:

override fun onAttach(context: Context) {
  super.onAttach(context)
  locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  locationObservable = provideRxLocationObservable(locationManager, grantedPermissionChecker)
  navigator = NavigatorImpl(context as Activity) // HERE
}

This is something else you’ll fix in the following chapters.

The importance of scope

The concept of scope is fundamental, and you’ll read a lot about it in the following chapters.

Figure 2.14 — Different scopes for different components
Figure 2.14 — Different scopes for different components

The diagram in Figure 2.14 gives you an idea about what the scope of the Busso App should be. BussoEndpoint should have the same scope as the app, while the Navigator should have the same scope as the Activity.

What isn’t obvious in the diagram is that each component living within a scope should have access to the instance living in an external scope.

For instance, any component should be able to access the same BussoEndpoint implementation, Fragments living in a specific Activity should share the same instance of the Navigator implementation, and so on.

Don’t worry if this isn’t clear yet. You’ll learn a lot about this concept in the following chapters.

Adding unit tests

The current implementation of the Busso App doesn’t contain any unit tests at all. What a shame! Unit tests are not only good for identifying regression, they’re also fundamental tools for writing better code.

Note: As you’ll see later in this chapter, the Rx Module for Location contains some tests. They’re in a different module, though.

As it is now, the Busso App is almost impossible to test. Just have a look at BusStopFragment.kt. How would you test a function like this?

private fun useLocation(location: GeoLocation) {
  context?.let { ctx ->
    disposables.add(
        provideBussoEndPoint(ctx)
            .findBusStopByLocation(location.latitude, location.longitude, 500)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(::mapBusStop)
            .subscribe(busStopAdapter::submitList, ::handleBusStopError)
    )
  }
}

In the following chapters, you’ll see how using dependency injection and other design patterns will make the Busso App easy to test.

The Rx Module for location

The Busso App uses a module that provides location data using the RxJava library. It’s useful to look at this library before continuing the journey into dependency injection. It’s the module located in the directory structure in Figure 2.15.

Figure 2.15 — The RxLocationObservable.kt file
Figure 2.15 — The RxLocationObservable.kt file

The same module also contains some unit tests, implemented in RxLocationObservableKtTest, which uses the Robot Pattern. This is actually the only module with some tests in the project so far.

The Rx module contains an implementation of the APIs defined in the location/api module in the libs folder, as you can see in Figure 2.16:

Figure 2.16 — RX implementation for the Location API
Figure 2.16 — RX implementation for the Location API

The API contains some basic definitions and abstractions that all the different implementations can use. In the api module, you’ll find the GeoLocation data class, which describes a location in terms of latitude and longitude.

data class GeoLocation(
    val latitude: Double,
    val longitude: Double
)

Every API implementation should provide some events of the LocationEvent type in LocationEvent.kt. This is a sealed class that defines the following specific subtypes:

  • LocationPermissionRequest
  • LocationPermissionGranted
  • LocationNotAvailable
  • LocationData
  • LocationStatus
  • LocationProviderEnabledChanged

The events’ names are self-explanatory but it’s important to note that LocationPermissionRequest is an event that fires when the permission to access the user’s location is missing, and that you need to put some request permission procedure in place.

On the other hand, LocationPermissionGranted fires if you’ve already obtained the permission.

The most important event is LocationData, which contains the information about the location in an object of type GeoLocation.

Permission can be granted in many ways, so you need an abstraction like the one defined by:

interface GeoLocationPermissionChecker {
  val isPermissionGiven: Boolean
}

The Rx module contains an implementation of the previous APIs that use RxJava or RxKotlin. You can take a look at its logic in RxLocationObservable.kt.

Note: RxJava is a library that implements the React specification. It’s used in many commercial apps. This book is not about RxJava, but you can learn all about it in the Reactive Programming With Kotlin book.

Testing the RxLocation module

The Rx module is fairly well tested. Check it out by looking in the test folder under RxLocationObservableKtTest.kt and taking a quick look at the following test:

@Test
fun whenPermissionIsDeniedLocationPermissionRequestIsSentAndThenCompletes() {
  rxLocationTest(context) {
    Given {
      permissionIsDenied()
    }
    When {
      subscribeRx()
    }
    Then {
      permissionRequestIsFired()
      isComplete()
    }
  }
}

It verifies that you receive a LocationPermissionRequest when you subscribe to the RxLocationObservable without having the necessary permissions. After that, Observable will complete.

Note: The Robot Pattern is a useful testing pattern that allows you to write more readable tests. You can learn all about the Robot pattern and other testing procedures in the Android Test-Driven Development by Tutorials book.

Challenge: Some unit tests as a warm-up

After building and running the Busso App, it’s time for a nice challenge. As you know, the Busso App doesn’t have unit tests. Can you write some for the code related to the BusStopMapper.kt and BusArrivalMapper.kt files, as shown in Figure 2.17?

Figure 2.17 — The Mapper classes
Figure 2.17 — The Mapper classes

These files contain simple functions for mapping the Model you get from the network, with the ViewModel you use to display information in the UI.

Challenge solution: Some unit tests as a warm-up

BusStopMapper.kt contains mapBusStop(), which you use to convert a BusStop model into a BusStopViewModel. What’s the difference?

BusStop contains pure data about a bus stop, which you get from the server. It looks like this:

data class BusStop(
    val id: String,
    val name: String,
    val location: GeoLocation,
    val direction: String?,
    val indicator: String?,
    val distance: Float?
)

BusStopViewModel contains the information that’s actually displayed in the app, such as information about the locale or some I18N (Internationalization) Strings. In this case, it’s:

data class BusStopViewModel(
    val stopId: String,
    val stopName: String,
    val stopDirection: String,
    val stopIndicator: String,
    val stopDistance: String
)

For instance, BusModel’s distance property is mapped onto the stopDistance property of BusStopViewModel. The former is an optional Float and the latter is a String. Why do you need to test these?

Tests allow you to write better code. In this case, mapBusStop() is pure, so you have to verify that for a given input, the output is what you expect.

Open BusStopMapper.kt and select the name of mapBusStop(). Now, open the quick actions menu with Control - Enter to what’s shown in Figure 2.18:

Figure 2.18 — Create a new Unit Test
Figure 2.18 — Create a new Unit Test

Select the Test… menu item and the dialog in Figure 2.19 will appear:

Figure 2.19 — Create Test information
Figure 2.19 — Create Test information

Now, press the OK button and a new dialog will appear, asking where to put the test you’re going to create. In this case, you’re creating a unit test, so select the test folder and select the OK button again:

Figure 2.20 — Select the test folder
Figure 2.20 — Select the test folder

Now, Android Studio will create a new file for you, like this:

class BusStopMapperKtTest {
  @Test
  fun mapBusStop() {
  }

  @Test
  fun testMapBusStop() {
  }
}

The first question you need to ask yourself when writing a unit test is: What am I testing?

In this case, the answer is that, given a BusStop, you need to get the expected BusStopViewModel. This must be true in the happy case and in all the edge cases.

Now, replace the existing mapBusStop() with the following code:

@Test
fun mapBusStop_givenCompleteBusStop_returnsCompleteBusStopViewModel() {
  // 1
  val inputBusStop = BusStop(
      "id",
      "stopName",
      GeoLocation(1.0, 2.0),
      "direction",
      "indicator",
      123F
  )
  // 2
  val expectedViewModel = BusStopViewModel(
      "id",
      "stopName",
      "direction",
      "indicator",
      "123 m"
  )
  // 3
  assertEquals(expectedViewModel, mapBusStop(inputBusStop))
}

In this test, you:

  1. Create a BusStop object to use as input for the function.
  2. Define an instance of BusStopViewModel like the one you expect as result.
  3. Use JUnit to verify the result is what you expected.

Now, you can run the test selecting the arrow as in Figure 2.21:

Figure 2.21 — Run the Unit Test
Figure 2.21 — Run the Unit Test

If everything is fine, you’ll get a checkmarks as a result like the following:

Figure 2.22 — Successful test
Figure 2.22 — Successful test

Congratulations and thank you! You’ve improved the Busso App — but there’s still a lot to do.

As an exercise, add the missing tests and check if they’re similar to the ones you’ll find in the final project for this chapter.

Key points

  • The Busso App is a client-server app.
  • The Busso Server has been implemented with Ktor. You can run it locally or use the existing Heroku installation.
  • The Busso App works, but you can improve it by removing code duplication and adding unit tests.
  • The concept of scope or lifecycle is fundamental and you’ll learn much more about it throughout this book.
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.