Android

Architecture Overview

Note

show and describe architecture

Basics

The App is mostly written in Kotlin (old Java parts should be converted when working on them) and always targets the newest API. Where possible and sensible, we use the Android Application Architecture Guidelines as recommended by Google Developers.

Layered architecture of the Android application

Component diagram

Component diagram

Data flow

The rough data flow of the application in one component is outlined below.

Data flow

General

Whenever information needs to be passed around in the application, especially for keeping the UI current, we utilize RxJava with the RxKotlin and RxAndroid On the “last mile” to the UI, this is converted to Android LiveData with the android.arch.lifecycle:reactivestreams library as LiveData ties in nicely to Fragments’ Lifecycle. If using Observables directly, much attention has to be paid to register them with the Autodispose library or else there is the danger of major memory leaks if the Observables are not disposed!

Used components from top to bottom

  • The UI is based around Fragments, as opposed to Activities, to keep transitions fast and seamless.

  • All operations which can be used by the Fragments are represented in the ViewModel. This class usually is rather lean and just takes care of the conversion of RxJava Observables to LiveData to be used by the UI

  • Behind the ViewModel, there is one Repository per feature (news, food, …) as the central data management place. The ViewModel is responsible for the following: - It will download data from the server via the Retrofit library - Convert downloaded data if necessary and - Store it in the database via the Data Access Objects (Dao) provided by the Room ORM library. - Offer a RxJava Observable representing the current loading status which will be presented to the user - Pass through Observables from the Daos for read access

  • Network access is realized with the Retrofit2 library, which also has support for RxJava Observables.

  • It is advisable to always feed the UI directly from the DB and not manually publish any data from the Repository to keep things consistent. Room supports getting Observables which emit every time the already made selection gets new data.

Dependency Injection

To keep the principle of loosely coupled components, we use the Dependency Injection pattern via the Dagger2 Library. For more information about the setup, see Android Dependency Injection

Data models

Starting from conceptual models to concrete database models or Data exchange models will be defined in this section.

Graphical representations (class, ER diagrams, etc.) are very welcome, must of course also always be described.

Tooling

Note

short description of team specific tools

Figma

UniApp

Bulletin Board

News and Events * Events Listing: https://www.figma.com/file/qkLG81OP1N3hoWQM1AkRJx/Event?node-id=14%3A2

Public Transit * Favorite BusLine: https://www.figma.com/file/yyONOvK9CvpMhajshsgoeD/Bus-Line-Favorite

Outstanding Issues

TODOs:

  • update dependencies

  • apply and fix lints/ kotlin debugger

  • instrumentation tests

  • elevate own Markersymbol. In buildings but not outside? Issue 71

  • Fix Pipeline!

  • Fix download Dialog for Map (make progress visible) Issue 94

  • Change Layout of Selected Place on Map Issue 149

  • Refactor FAQ Issue 157

  • Adapt to new Advert Type in Bulletin Board (depending on back end) Issue 143

  • Get nearest Bus Stop Issue 85

How to start a new feature

Note

describe how to create a feature

Quality Control Measurements

Note

describe measures like testing, standards, conventions etc.

This article provides insight into basic unit tests in android and provides information and documentation that might be helpful for unit testing.

Introduction

Unit tests cover test cases for the application logic. Usually, the logic somehow interacts with the local database or a service which requires the server to be connected and to be able to respond. In some cases the server even needs to have to respond with a needed data set for testing. Since this is barely ever possible to achieve in testing environment, we use Mockito to mock databases and services.

Mockito provides the ability to “fake” a database or a service. For example, one can mock a service and “fake” (“mock”) responses of requests that would usually be called by that service. But instead of calling the service and sending a request to the actual server, the mock simply returns a predefined response or an error code, whatever is needed.

Testing with Mockito

This section will use an example to provide information about how to use mockito for mocking in Android. In our project structure, we use repositories which connect the UI with the DAOs (database) and the services which will call server requests. The repository “sits between” UI and database/service and handles the datatransfer between the three parts. If you want to write unit tests for such a repository, it is very helpful to use mocks for the DAOs and the services due to the circumstances mentioned above.

Given Source Code

The following will shortly give an example on how the actual implementation of the application might look like. Repository:

 class ExampleRepository @Inject constructor (private val exampleService,
                                              private val exampleDao) {

  // used for feedback on the UI: Set behaviour subject and change status to be able to react on the UI
  private val fetchStatus: BehaviourSubject<NetworkStatus> = BehaviourSubject.createDefault(NetworkStatus.FINISHED)

  // for compositing disposables
  private val compositeDisposable: CompositeDisposable = CompositeDisposable()

  fun getUser(id: Long): Observable<NetworkStatus> {
    // set status to STARTED -> UI will know that server request is started
    this.fetchStatus = NetworkStatus.STARTED

    // create disposable. Not necessary for the example itself but is here for the purpose of completion.
    // call the request.
    val disposable = this.exampleService.getUser(id)
      .subscribeOn(Schedulers.io())
      .subscribe({user ->
        // request is done and was successful. Save user into the DAO.
        this.exampleDao.insertUser(user)

        // set fetchStatus to FINISHED -> UI can indicate that action was successful
        this.fetchStatus = NetworkStatus.FINISHED
      }, {error ->
        // error appeard. Set fetchStatus to FAILED so UI can show error.
        this.fetchStatus = NetworkStatus.FAILED
      })
      // Add disposable to compositeDisposable to be able to dispose it later.
      compositeDisposable.add(disposable)

      return this.fetchStatus
  }

  fun onDestroy() {
    // dispose disposables when lifecicle ends
    compositeDisposable.dispose()
    compositeDisposable.clear()
  }
}

For more information about disposables, see this article or the documentation. These are part of this example for the purpose of a complete example as used in our project but are not needed for the test and can be ignored for this article. The service:

interface ExampleService {
  @GET("/api/data")
  fun getUser(@Query ("id") id: Long): Observable<User>
}

And the DAO:

@Dao
interface ExampleDao {
  @INSERT(onConflict = OnConflictStrategy.REPLACE)
  fun insertUser(user: User)

  @Query(SELECT FROM users WHERE user.id == id)
  fun getUser(id: Long): LiveData<User>
}

Mocking

Unit tests cover logic used in the application. Obiously, the repository is part of the logic. The repository now uses the ExampleService and the ExampleDao for server requests and for storing data in the local database. There are multiple reasons why it may be reasonable to mock both the service and the DAO.

First, the server might not be accessible due to different reasons, for example because the environment where the tests are executed does not have access to the internet. Second, the developer might be unsure of what the server will respond with exactly. This might also be a problem when testing. Third, it might be possible to write data into the database that might result in strange behaviour when using the actual app (e.g. more information popping up for a user than usually if the inserted test data isn’t completely removed) …

To mitigate these negative effects, we use mocks for the services and DAOs. The following will illustrate on how to actually to this. First, it is necessary to create the mocks for the service and the DAO used in our example. For the DAO, it is possible to just create a simple class that handles these calls and use this instead of using the actual DAO. To be able to actually use this in our Repository, we extend the class from the actual DAO:

 class InMemoryExampleDao : ExampleDao {
   val mStoredUsers: MutableMap<Long, User> = mutableMapOf()

  override fun insertUser(user: User) {
    this.mStoredUsers[user.id] = user
  }

   override fun getUser(id: Long): LiveData<User> {
    return toLiveData(Observable.just(this.mStoredUsers[id]))
  }

  fun clear() {
    this.mStoredUsers = mutableMapOf()
  }

  fun getSize(): Int{
    return this.mStoredUsers.size
  }

  private fun <T> toLiveData(p: Observable<T>) = LiveDataReactiveStreams.fromPublisher<T>(
    p.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).toFlowable(BackpressureStrategy.LATEST)
  )
}

The in memory DAO provides the same functionality as the actual DAO but does not use an actual database. It only stored the data as long as the instantiated InMemoryExampleDao lives. Second, we need to somehow mock the server responses when the service is called. The easiest way to do this, is the whenever functionality which comes with the MockitoJUnitRunner used for our tests. It enables us to catch the actual request and directly return a response instead of calling the reqeust. This is illustrated best inside an actual example test.

Testing

@RunWith(MockitoJUnitRunner::class)
class ExmapleRepositoryTest : RxJavaTestCase("ExampleRepositoryTest") {
   @Mock
   private lateinit var mMockExampleService: ExampleService
   private var mExampleDao: InMemoryExampleDao = InMemoryExampleDao()

   @Test
   fun testSomething() {}
}

For the whenever functionality to work, the test class is told to run with the MockitoJUnitRunner. Since the repository that will be tested uses RxJava observables and livedata, it is necessary to use a RxJavaTestCase. Since the actual ExampleService should not come to use and call requests, the ExampleService is annotated as @Mock. Android will mock the instance of the Service instead of using the actual service. The instatiation is done automatically when annotated as @Mock, so there is no need to do this. With this setup done, it is now possible to write an actual test case:

@RunWith(MockitoJUnitRunner::class)
class ExmapleRepositoryTest : RxJavaTestCase("ExampleRepositoryTest") {
  @Before
  fun clearDao() {
   this.mExampleDao.clear()
  }

  @Test
  fun testGetUser() {
    // Create Instance for testing
    val repository = ExampleRepository(
      this.mMockExampleService,
      this.mExampleDao
    )

    // set up mock response for service call
    whenever(this.mMockExampleService.getUser(0)).thenReturn(this.getUserResponse())

    // check if Dao is empty before method call
    assertEquals(0, this.mExampleDao.getSize()

    // call repository method. The "whenever" set above will interrupt the request and return the defined response below.
    val response: Observable<NetworkStatus> = repository.getUser(0)
    val testObserver = TestObserver<NetworkStatus>()
    response.subscribe(testObserver)
    testObserver.assertNoErrors()

    // check if the returned user was actually stored
    assertEquals(1, this.mExampleDao.getSize()
    val userIds: List<Long> = this.mExampleDao.storedUsers.map { entry -> entry.value.id }
    assertTrue(userIds.contains(0))

    // check if the correct NetworkStatus was set
    assertEquals(1, testObserver.values().size)
    assertTrue(testObserver.values().contains(NetworkStatus.FINISHED))
  }

  // Mocked server response
  private fun getUserResponse(): Observable<User> {
     return Observable.just(User(0, "Alexander", "Raschke"))
  }
}

This does also work for testing error responses:

@Test
fun testGetUserWithError() {
  // Create Instance for testing
  val repository = ExampleRepository(
    this.mMockExampleService,
    this.mExampleDao
  )

  // set up mock response for service call
  whenever(this.mMockExampleService.getUser(0)).thenReturn(Observable.just("Some error"))

  // check if Dao is empty before method call
  assertEquals(0, this.mExampleDao.getSize()

  // make method call
  val response: Observable<NetworkStatus> = repository.getUser(0)
  val testObserver = TestObserver<NetworkStatus>()
  response.subscribe(testObserver)
  testObserver.assertNoErrors()

  // check if nothing was stored and NetworkStatus was net to FAILED
  assertTrue(testObserver.values().contains(NetworkStatus.FAILED))
  assertEquals(0, this.mExampleDao.getSize()
}

Additional Information

Call<ResponseBody>

In some cases it is not possible for a service method to return an observable. This is mostly the case if the service method adds an image to the request. The Server will then return a Call<ResponseBody> instead of an Observable. If this is the case, unit tests have to be handled a little differently. Assume that the repository defined above has an additional method where an image is posted to the backend. The method call also looks a little different than the one already known.:

interface ExampleService {
 @GET("/api/data")
 fun getUser(@Query ("id") id: Long): Observable<User>

 @Multipart
 @POST("/api/image")
 fun postImage(@Query ("text") additionalText: String,
               @Part image: MultipartBody.Part): Call<ResponseBody>
}

class ExampleRepository @Inject constructor (private val exampleService,
                                             private val exampleDao) {

  // used for feedback on the UI: Set behaviour subject and change status to be able to react on the UI
  private val fetchStatus: BehaviourSubject<NetworkStatus> = BehaviourSubject.createDefault(NetworkStatus.FINISHED)
  private val sendStatus: BehaviourSubject<NetworkStatus> = BehaviourSubject.createDefault(NetworkStatus.FINISHED)

  // for compositing disposables
  private val compositeDisposable: CompositeDisposable = CompositeDisposable()

  fun getUser(id: Long): Observable<NetworkStatus> {...}

  fun postImage(additionalText: String, image: MultipartBody.Part): Observable<NetworkStatus> {
    this.sendStatus = NetworkStatus.STARTED
    val call: Call<ResponseBody> = this.exampleService.postImage(additionalText, image)
    call.enqueue(object: Callback<ResponseBody> {
      override fun onResponse(call: Call<ResponseBody>?, response: Response<ResponseBody>) {
        if (response.isSuccessful) {
          this.sendStatus.onNext(NetworkStatus.SUCCESS)
        } else {
          this.onFailure(call, Throwable(response.message()))
        }
      }
      override fun onFailure(call: Call<ResponseBody>?, t: Throwable) {
        this.sendStatus.onNext(NetworkStatus.FAILED)
      }
    }
    return sendStatus
  }

  fun onDestroy() {
    // dispose disposables when lifecicle ends
    compositeDisposable.dispose()
    compositeDisposable.clear()
  }
}

A test for such a method might look as follows:

@Test
fun testGetUserWithError() {
  // Create Instance for testing
  val repository = ExampleRepository(
    this.mMockExampleService,
    this.mExampleDao
  )

  // get some image with MultipartBody.Part image with some method.
  val image: Multipart.Body = someMethod()

  // set up mock response for service call
  whenever(call.enqueue(any())).thenAnswer {
    val callback: Callback<ResponseBody> = it.arguments[0] as Callback<ResponseBody>
    val response: Response<ResponseBody> = mock {
      on { isSuccessful } doReturn true // or false if testing error case
    }
    callback.onResponse(call, response)
    null
  }

  val response: Observable<NetworkStatus> = repository.postImage("hi", image)
  val testObserver = TestObserver<NetworkStatus>()
  response.subscribe(testObserver)

  testObserver.assertNoErrors()
  assertTrue(testObserver.values().contains(NetworkStatus.SUCCESS)
}

UI Tests

This Page covers the usage of UI tests in Android using the Espresso framework. Although Espresso itself is pretty easy to use, there is still some issues with setting up the test environment for UI testing.

Espresso

Espresso is the framework used for UI testing in the Android UniApp. Espresso provides the ability to perform actions on the UI and check if UI elements are presented as expected.

Basic UI tests with Espresso

Espresso tests are actually implemented stright forward. The first task is to set up the environment for the test.

class someTest {
  @Rule
  @JvmField
  var activityTestRule = ActivityTestRule(BaseNavigationDrawerActivity::class.java)
}

The ActivityTestRule is responsible for starting the activity before a test. It basically ensures that the correct activity is running before running a test case. For more detailles, see this page The Android app is organized in fragments which are accessed through the main navigation. These fragments include the news feed, mensa, etc. Before running a test, it is necessary to start the fragment to be tested. This is done by the following code snipped.

class someTest {
 @Rule
 @JvmField
 var activityTestRule = ActivityTestRule(BaseNavigationDrawerActivity::class.java)

 @Before
 fun setupFragment() {
   runOnUiThread {
     activityTestRule.activity.navigationEventBus.accept(FragmentNavigationTarget.SomeTarget)
   }
 }

}

In this case, the Fragment SomeTarget will be started. FragmentNavigationTargets are defined in the BaseNavigationDrawerActivity which handles the navigation inside the app. After the test is set up and the correct fragment is loaded, the actual test cases can be written. These usually include checks and actions on the UI

class someTest {
 @Rule
 @JvmField
 var activityTestRule = ActivityTestRule(BaseNavigationDrawerActivity::class.java)

 @Before
 fun setupFragment() {
   runOnUiThread {
     activityTestRule.activity.navigationEventBus.accept(FragmentNavigationTarget.SomeTarget)
   }
 }

 @Test
 fun isSearchBarClickable() {
   onView(withId(R.id.search_bar)).check(matches(isClickable))
 }

 @fun testSomeButton() {
   onView(withId(R.id.some_text)).check(matches(withText("Button not clicked")))
   onView(withId(R.id.some_button)).check(matches(isClickable))
     .perform(click())
   onView(withId(R.id.some_text)).check(matches(withText("Button clicked!")))
 }

}

This also works for dynamic lists or (more commonly used in our project and Android in general) recycler views

class someTest {
 @Rule
 @JvmField
 var activityTestRule = ActivityTestRule(BaseNavigationDrawerActivity::class.java)

 @Before
 fun setupFragment() {
   runOnUiThread {
     activityTestRule.activity.navigationEventBus.accept(FragmentNavigationTarget.SomeTarget)
   }
 }

 @Test
 fun recyclerViewTest() {
   onView(withId(R.id.faq_recycler_view))
     .perform(
       RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
         0, click()))
 }

}

However, these recycler views are not as easy testable as static UI elements. The UI testing itself is not actually harder than testing buttons or text views. But since recycler views often use dynamically loaded resources, it is not always obvious which elements are actually part of the recylcer view when running the test. For example, the Ticket Feature displays all tickets of an user inside a recylcer view. The tickets are dynamically loaded from the database and the number varies depending on how many tickets the user opened. So it might be possible that there are currently no tickets inside the database when running the test case. Thus, the test might fail if the test is trying to click on an element in the recylcer view which does not exist. This circumstance makes it clear that it is necessary to mock databases and services for UI testing to be able to use the full potential. Unfortunately, this is not as easy as in Unit Tests.

Mocking Databases and Services for UI tests

Note

here, something might be gone lost

Detailed Feature Listing

Feature A

Note

describe detailed and team specific feature here with code examples

Functional requirements

This is the main part of the document. All requirements are described here. I would like to proceed iteratively. This means that all (known) requirements are first roughly described and then refined little by little.

Note

Definition of the requirementstemplate to be done!

It is important that requirements are not only described in brief, but really in detail, without getting too much into prose.

Important quality aspects are:

  • identifiable (IDs assigned)

  • députting

  • precise

  • realizable

UUID

Basic Usage

The uuid is generated when the app gets installed. The generation of the uuid is handled by the java.util.uuid class. For this purpose, the UuidProvider is used:

private static void writeInstallationFile(File installation) throws IOException {
    FileOutputStream out = new FileOutputStream(installation);
    String id = UUID.randomUUID().toString();
    out.write(id.getBytes(Charset.defaultCharset()));
    out.close();
}

check the java.util.uuid documentation.

When calling a request which has to deal with user related data, the uuid is simply attached as String to the request.

UUID

Changing UUIDs (to be implemented)

At some point, a user might re-install the app on his device or he might even get a different device. In these cases, he would get a new uuid, which implies that all user-related data he used on his old device/old installation would not be usable to him now due to the change in the uuid. To mitigate this issue, some functionality is needed which allows the user to tell the backend that he is now using a new uuid. The basic idea is that the user can call a specific request which will result in the generation of a random code. This code will be sent to the user. He can then use this code to switch to his new uuid by calling another request on his new device. None of this is currently implemented. For more details, see the backend documentation

Additional Information The uuid works as identifier for the application, but other possibilities are also conceivable like using the actual device id or a hashed device id. Android currently uses the pseudo random uuid provided by java.util.uuid to not leak any information about the actual device of the user.