Technology17-minute read

Discover the Benefits of Android Clean Architecture

It’s easy to get entangled in spaghetti code, fuzzy responsibilities, and opaque architecture. It is just as easy to write crisp, clear code that is just as easy to write, maintain, and update with a little forethought.

Join Toptal Android Developer Tomasz Czura as he guides you through the process and the reasoning behind creating clean architecture on Android.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

It’s easy to get entangled in spaghetti code, fuzzy responsibilities, and opaque architecture. It is just as easy to write crisp, clear code that is just as easy to write, maintain, and update with a little forethought.

Join Toptal Android Developer Tomasz Czura as he guides you through the process and the reasoning behind creating clean architecture on Android.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Tomasz Czura
Verified Expert in Engineering

Tomasz (MCS) is an Android wizard and team lead. His favorite project has been doing the app and back-end of a hotel entertainment system.

Expertise

Previously At

Welltok
Share

What would you prefer: adding a new feature to a very well working app with awful architecture, or fixing a bug in the well architected, but buggy, Android application? Personally, I would definitely choose the second option. Adding a new feature, even a simple one, can become very laborious in an app, considering all the dependencies from everything in every class. I remember one of my Android projects, where a project manager asked me to add a small feature—something like downloading data and displaying it on a new screen. It was an app written by one of my colleagues who found a new job. The feature should not take more than a half of a working day. I was very optimistic…

After seven hours of investigation into how the app works, what modules there are, and how they communicate with one another, I made some trial implementations of the feature. It was hell. A small change in the data model forced a big change in the login screen. Adding a network request required changes of implementation of almost all screens and the GodOnlyKnowsWhatThisClassDoes class. Button color changes caused weird behavior when saving the data to the database or a total app crash. Halfway through the following day, I told my project manager, “We have two ways to implement the feature. First, I can spend three more days on it and finally will implement it in a very dirty way, and the implementation time of every next feature or bugfix will grow exponentially. Or, I can rewrite the app. This will take me two or three weeks, but we will save time for the future app changes.”” Fortunately, he agreed to the second option. If I ever had doubts why good software architecture in an app (even a very small one) is important, this app dispelled them totally. But which Android architecture pattern should we use to avoid such problems?

In this article, I’d like to show you a clean architecture example in an Android app. The main ideas of this pattern, however, can be adapted to every platform and language. Good architecture should be independent of details like platform, language, database system, input, or output.

Example App

We will create a simple Android app to register our location with the following features:

  • The user can create an account with a name.
  • The user can edit the account name.
  • The user can delete the account.
  • The user can select the active account.
  • The user can save location.
  • The user can see the location list for a user.
  • The user can see a list of users.

Clean Architecture

The layers are the main core of a clean architecture. In our app, we will use three layers: presentation, domain, and model. Each layer should be separated and shouldn’t need to know about other layers. It should exist in its own world and, at most, share a small interface to communicate.

Layer responsibilities:

  • Domain: Contains the business rules of our app. It should provide use cases which reflect the features of our app.
  • Presentation: Presents data to the user and also collects necessary data like the username. This is a kind of input/output.
  • Model: Provides data for our app. It is responsible for obtaining data from external sources and save them to the database, cloud server, etc.

Which layers should know about the others? The simplest way to get the answer is thinking about changes. Let’s take the presentation layer—we will present something to the user. If we change something in presentation, should we also make a change in a model layer? Imagine we have a “User” screen with the user’s name and last location. If we want to present the user’s last two locations instead of only one, our model should not be affected. So, we have the first principle: The presentation layer does not know about the model layer.

And, the opposite—should the model layer know about presentation layer? Again—no, because if we change, e.g., the source of data from a database to a network, it should not change anything in the UI (if you thought about adding a loader here—yes, but we can also have a UI loader when using a database). So the two layers are completely separate. Great!

What about the domain layer? It is the most important one because it contains all the main business logic. This is where we want to process our data before passing it to the model layer or presenting it to the user. It should be independent of any other layer—it does not know anything about the database, the network, or the user interface. As this is the core, other layers will communicate only with this one. Why do we want to have this completely independent? Business rules will probably change less often than the UI designs or something in the database or the network storage. We will communicate with this layer via some provided interfaces. It does not use any concrete model or UI implementation. These are details, and remember—details change. A good architecture is not bound to details.

Enough theory for now. Let’s start coding! This article revolves around the code, so—for better understanding—you should download the code from GitHub and check what is inside. There are three Git tags created—architecture_v1, architecture_v2, and architecture_v3, which correspond to the article’s parts.

App Technology

In the app, I use Kotlin and Dagger 2 for dependency injection. Neither Kotlin nor Dagger 2 is necessary here, but it makes things far easier. You might be surprised that I do not use RxJava (nor RxKotlin), but I didn’t find it usable here, and I do not like using any library only because it is on top and somebody says it is a must. As I said—language and libraries are details, so you can use what you want. Some Android unit test libraries are used as well: JUnit, Robolectric, and Mockito.

Domain

The most important layer in our Android application architecture design is the domain layer. Let’s start with it. This is where our business logic and the interfaces to communicate with other layers will be. The main core is the UseCases, which reflect what the user can do with our app. Let’s prepare an abstraction for them:

abstract class UseCase<out Type, in Params> {

    private var job: Deferred<OneOf<Failure, Type>>? = null

    abstract suspend fun run(params: Params): OneOf<Failure, Type>

    fun execute(params: Params, onResult: (OneOf<Failure, Type>) -> Unit) {
        job?.cancel()
        job = async(CommonPool) { run(params) }
        launch(UI) {
            val result = job!!.await()
            onResult(result)
        }
    }

    open fun cancel() {
        job?.cancel()
    }

    open class NoParams
}

I decided to use Kotlin’s coroutines here. Each UseCase has to implement a run method to provide the data. This method is called on a background thread, and after a result is received, it is delivered on the UI thread. The returned type is OneOf<F, T>—we can return an error or success with data:

sealed class OneOf<out E, out S> {
    data class Error<out E>(val error: E) : OneOf<E, Nothing>()
    data class Success<out S>(val data: S) : OneOf<Nothing, S>()

    val isSuccess get() = this is Success<S>
    val isError get() = this is Error<E>

    fun <E> error(error: E) = Error(error)
    fun <S> success(data: S) = Success(data)

    fun oneOf(onError: (E) -> Any, onSuccess: (S) -> Any): Any =
            when (this) {
                is Error -> onError(error)
                is Success -> onSuccess(data)
            }
}

The domain layer needs its own entities, so the next step is to define them. We have two entities for now: User and UserLocation:

data class User(var id: Int? = null, val name: String, var isActive: Boolean = false)

data class UserLocation(var id: Int? = null, val latitude: Double, val longitude: Double, val time: Long, val userId: Int)

Now that we know what data to return, we have to declare our data providers’ interfaces. These will be IUsersRepository and ILocationsRepository. They have to be implemented in the model layer:

interface IUsersRepository {
    fun setActiveUser(userId: Int): OneOf<Failure, User>
    fun getActiveUser(): OneOf<Failure, User?>
    fun createUser(user: User): OneOf<Failure, User>
    fun removeUser(userId: Int): OneOf<Failure, User?>
    fun editUser(user: User): OneOf<Failure, User>
    fun users(): OneOf<Failure, List<User>>
}

interface ILocationsRepository {
    fun locations(userId: Int): OneOf<Failure, List<UserLocation>>
    fun addLocation(location: UserLocation): OneOf<Failure, UserLocation>
}

This set of actions should be enough to provide the necessary data for the app. At this stage, we do not decide how the data will be stored—this is a detail which we want to be independent of. For now, our domain layer doesn’t even know that it’s on Android. We will try to keep this state (Sort of. I’ll explain later).

The last (or almost last) step is to define implementations for our UseCases, which will be used by the presentation data. All of them are very simple (just like our app and data are simple)—their operations are limited to call a proper method from the repository, e.g.:

class GetLocations @Inject constructor(private val repository: ILocationsRepository) : UseCase<List<UserLocation>, UserIdParams>() {
    override suspend fun run(params: UserIdParams): OneOf<Failure, List<UserLocation>> = repository.locations(params.userId)
}

The Repository abstraction makes our UseCases very easy to test—we do not have to care about a network or a database. It can be mocked in any way, so our unit tests will test actual use cases and not other, unrelated classes. This will make our unit tests simple and fast:

@RunWith(MockitoJUnitRunner::class)
class GetLocationsTests {
    private lateinit var getLocations: GetLocations
    private val locations = listOf(UserLocation(1, 1.0, 1.0, 1L, 1))

    @Mock
    private lateinit var locationsRepository: ILocationsRepository

    @Before
    fun setUp() {
        getLocations = GetLocations(locationsRepository)
    }

    @Test
    fun `should call getLocations locations`() {
        runBlocking { getLocations.run(UserIdParams(1)) }
        verify(locationsRepository, times(1)).locations(1)
    }

    @Test
    fun `should return locations obtained from locationsRepository`() {
        given { locationsRepository.locations(1) }.willReturn(OneOf.Success(locations))
        val returnedLocations = runBlocking { getLocations.run(UserIdParams(1)) }
        returnedLocations shouldEqual OneOf.Success(locations)
    }
}

For now, the domain layer is finished.

Model

As an Android developer, you will probably choose Room, the new Android library for storing data. But let’s imagine that the project manager asked if you can put off the decision about the database because management is trying to decide between Room, Realm, and some new, super fast storage library. We need some data to start working with the UI, so we will just keep it in memory for now:

class MemoryLocationsRepository @Inject constructor(): ILocationsRepository {
    private val locations = mutableListOf<UserLocation>()

    override fun locations(userId: Int): OneOf<Failure, List<UserLocation>> = OneOf.Success(locations.filter { it.userId == userId })

    override fun addLocation(location: UserLocation): OneOf<Failure, UserLocation> {
        val addedLocation = location.copy(id = locations.size + 1)
        locations.add(addedLocation)
        return OneOf.Success(addedLocation)
    }
}

Presentation

Two years ago, I wrote an article about MVP as a very good app structure for Android. When Google announced the great Architecture Components, which made Android application development far easier, MVP is no longer needed and can be replaced by MVVM; however, some ideas from this pattern are still very useful—like the one about dumb views. They should only care about displaying the data. To achieve this, we will make use of ViewModel and LiveData.

The design of our app is very simple—one activity with bottom navigation, in which two menu entries show the locations fragment or the users fragment. In these views we use ViewModels, which in turn use UseCases from the domain layer, keeping the communication neat and simple. For example, here is LocationsViewModel:

class LocationsViewModel @Inject constructor(private val getLocations: GetLocations,
                                             private val saveLocation: SaveLocation) : BaseViewModel() {
    var locations = MutableLiveData<List<UserLocation>>()

    fun loadLocations(userId: Int) {
        getLocations.execute(UserIdParams(userId)) { it.oneOf(::handleError, ::handleLocationsChange) }
    }

    fun saveLocation(location: UserLocation, onSaved: (UserLocation) -> Unit) {
        saveLocation.execute(UserLocationParams(location)) {
            it.oneOf(::handleError) { location -> handleLocationSave(location, onSaved) }
        }
    }

    private fun handleLocationSave(location: UserLocation, onSaved: (UserLocation) -> Unit) {
        val currentLocations = locations.value?.toMutableList() ?: mutableListOf()
        currentLocations.add(location)
        this.locations.value = currentLocations
        onSaved(location)
    }

    private fun handleLocationsChange(locations: List<UserLocation>) {
        this.locations.value = locations
    }
}

A little explanation for those who are not familiar with ViewModels—our data is stored in the locations variable. When we obtain data from the getLocations use case, they are passed to the LiveData value. This change will notify the observers so that they can react and update their data. We add an observer for the data in a fragment:

class LocationsFragment : BaseFragment() {

...

    private fun initLocationsViewModel() {
       locationsViewModel = ViewModelProviders.of(activity!!, viewModelFactory)[LocationsViewModel::class.java]
       locationsViewModel.locations.observe(this, Observer<List<UserLocation>> { showLocations(it ?: emptyList()) })
       locationsViewModel.error.observe(this, Observer<Failure> { handleError(it) })
    }

    private fun showLocations(locations: List<UserLocation>) {
        locationsAdapter.locations = locations
    }

    private fun handleError(error: Failure?) {
        toast(R.string.user_fetch_error).show()
    }

}

On every location change, we just pass the new data to an adapter assigned to a recycler view—and that’s where the normal Android flow for showing data in a recycler view goes.

Because we use ViewModel in our views, their behavior is also easy to test—we can just mock the ViewModels and not care about the data source, network, or other factors:

@RunWith(RobolectricTestRunner::class)
@Config(application = TestRegistryRobolectricApplication::class)
class LocationsFragmentTests {

    private var usersViewModel = mock(UsersViewModel::class.java)
    private var locationsViewModel = mock(LocationsViewModel::class.java)

    lateinit var fragment: LocationsFragment

    @Before
    fun setUp() {
        UsersViewModelMock.intializeMock(usersViewModel)
        LocationsViewModelMock.intializeMock(locationsViewModel)

        fragment = LocationsFragment()
        fragment.viewModelFactory = ViewModelUtils.createFactoryForViewModels(usersViewModel, locationsViewModel)
        startFragment(fragment)
    }


    @Test
    fun `should getActiveUser on start`() {
        Mockito.verify(usersViewModel).getActiveUser()
    }

    @Test
    fun `should load locations from active user`() {
        usersViewModel.activeUserId.value = 1
        Mockito.verify(locationsViewModel).loadLocations(1)
    }

    @Test
    fun `should display locations`() {
        val date = Date(1362919080000)//10-03-2013 13:38

        locationsViewModel.locations.value = listOf(UserLocation(1, 1.0, 2.0, date.time, 1))

        val recyclerView = fragment.find<RecyclerView>(R.id.locationsRecyclerView)
        recyclerView.measure(100, 100)
        recyclerView.layout(0,0, 100, 100)
        val adapter = recyclerView.adapter as LocationsListAdapter
        adapter.itemCount `should be` 1
        val viewHolder = recyclerView.findViewHolderForAdapterPosition(0) as LocationsListAdapter.LocationViewHolder
        viewHolder.latitude.text `should equal` "Lat: 1.0"
        viewHolder.longitude.text `should equal` "Lng: 2.0"
        viewHolder.locationDate.text `should equal` "10-03-2013 13:38"
    }
}

You may notice that the presentation layer is also separated into smaller layers with clear borders. Views like activities, fragments, ViewHolders, etc. are responsible only for displaying data. They are aware only about the ViewModel layer—and use only that to get or to send users and locations. It is a ViewModel which communicates with the domain. ViewModel implementations are the same for the view as the UseCases are for the domain. To paraphrase, clean architecture is like an onion—it has layers, and layers can also have layers.

Dependency Injection

We have created all the classes for our architecture, but there is one more thing to do—we need something that connects everything together. The presentation, domain, and model layers are kept clean, but we need one module which will be the dirty one and will know everything about everything—by this knowledge, it will be able to connect our layers. The best way to make it is using one of the common design patterns (one of the clean code principles defined in SOLID)—dependency injection, which creates proper objects for us and injects them to desired dependencies. I used Dagger 2 here (in the middle of the project, I changed the version to 2.16, which has less boilerplate), but you can use any mechanism you like. Recently, I played a bit with Koin library, and I think it is also worth a try. I wanted to use it here, but I had a lot of problems with mocking the ViewModels when testing. I hope I find a way to resolve them quickly—if so, I can present differences for this app when using Koin and Dagger 2.

You can check the app for this stage on GitHub with the tag architecture_v1.

Changes

We finished our layers, tested the app—everything is working! Except for one thing—we still need to know what database our PM wants to use. Assume they came to you and said that the management agreed to use Room, but they still want to have a possibility to use the newest, superfast library in the future, so you need to keep potential changes in mind. Also, one of the stakeholders asked if the data can be stored in a cloud and wants to know the cost of such a change. So, this is the time to check if our architecture is good and if we can change the data storage system without any changes in the presentation or the domain layer.

Change 1

The first thing when using Room is defining entities for a database. We already have some: User and UserLocation. All we have to do is to add annotations like @Entity and @PrimaryKey, and then we can use it in our model layer with a database. Great! This is an excellent way to break all the architecture rules we wanted to keep. Actually, the domain entity cannot be converted to a database entity this way. Just imagine that we also want to download the data from a network. We could use some more classes to handle the network responses—converting our simple entities to make them work with a database and a network. That is the shortest path to a future catastrophe (and crying, “Who the hell wrote this code?”). We need separate entity classes for every data storage type we use. It doesn’t cost much, so let’s define the Room entities correctly:

@Entity
data class UserEntity(
        @PrimaryKey(autoGenerate = true) var id: Long?,
        @ColumnInfo(name = "name") var name: String,
        @ColumnInfo(name = "isActive") var isActive: Boolean = false
)

@Entity(foreignKeys = [
    ForeignKey(entity = UserEntity::class,
            parentColumns = [ "id" ],
            childColumns = [ "userId" ],
            onDelete = CASCADE)
])
data class UserLocationEntity(
    @PrimaryKey(autoGenerate = true) var id: Long?,
    @ColumnInfo(name = "latitude") var latitude: Double,
    @ColumnInfo(name = "longitude") var longitude: Double,
    @ColumnInfo(name = "time") var time: Long,
    @ColumnInfo(name = "userId") var userId: Long
)

As you can see, they are almost the same as domain entities, so there’s a big temptation to merge them. This is merely an accident—with more complicated data, the similarity will be smaller.

Next, we have to implement the UserDAO and the UserLocationsDAO, our AppDatabase, and finally—the implementations for IUsersRepository and ILocationsRepository. There’s a small problem here—ILocationsRepository should return a UserLocation, but it receives a UserLocationEntity from the database. The same is for User-related classes. In the opposite direction, we pass the UserLocation when the database requires UserLocationEntity. To resolve this, we need Mappers between our domain and data entities. I used one of my favorite Kotlin features—extensions. I created a file named Mapper.kt, and put all the methods for mapping between the classes there (of course, it’s in the model layer—the domain doesn’t need it):

fun User.toEntity() = UserEntity(id?.toLong(), name, isActive)
fun UserEntity.toUser() = User(this.id?.toInt(), name, isActive)
fun UserLocation.toEntity() = UserLocationEntity(id?.toLong(), latitude, longitude, time, userId.toLong())
fun UserLocationEntity.toUserLocation() = UserLocation(id?.toInt(), latitude, longitude, time, userId.toInt())

The little lie I mentioned before is about domain entities. I wrote that they do not know anything about Android, but this is not entirely true. I added @Parcelize annotation to the User entity and extend Parcelable there, making it possible to pass the entity to a fragment. For more complicated structures, we should provide the view layer’s own data classes, and create mappers like between domain and data models. Adding Parcelable to the domain entity is a small risk I dared to take—I am aware of that, and in case of any User entity changes I will create separate data classes for the presentation and remove Parcelable from the domain layer.

The last thing to do is to change our dependency injection module to provide the newly created Repository implementation instead of the previous MemoryRepository. After we build and run the app, we can go to the PM to show the working app with a Room database. We can also inform the PM that adding a network will not take too much time, and that we are open to any storage library the management wants. You can check what files have been changed—only the ones in the model layer. Our architecture is really neat! Every next storage type can be built in the same way, just by extending our repositories and providing the proper implementations. Of course, it could turn out that we need multiple data sources, such as a database and a network. What then? Nothing to it, we’d just have to create three repository implementations—one for the network, one for the database, and a main one, where the correct data source would be selected (e.g., if we have a network, load from the network, and if not, load from a database).

You can check out the app for this stage on GitHub with the tag architecture_v2.

So, the day is almost finished—you are sitting in front of your computer with a cup of coffee, the app is ready to be sent to Google Play, when suddenly the project manager comes to you and asks “Could you add a feature that can save the user’s current location from the GPS?”

Change 2

Everything changes… especially the software. This is why we need clean code and clean architecture. However, even the cleanest things can be dirty if we’re coding without thinking. The first thought when implementing getting a location from the GPS would be to add all the location-aware code in the activity, run it in our SaveLocationDialogFragment and create a new UserLocation with corresponding data. This could be the fastest way. But what if our crazy PM comes to us and asks us to change getting the location from GPS to some other provider (e.g., something like Bluetooth or network)? The changes would soon get out of hand. How can we do it in a clean way?

The user location is data. And getting a location is a UseCase—so I think that our domain and model layers should also be involved here. Thus, we have one more UseCase to implement—GetCurrentLocation. We also need something that will provide a location for us—an ILocationProvider interface, to make the UseCase independent of details like the GPS sensor:

interface ILocationProvider {
    fun getLocation(): OneOf<Failure, SimpleLocation>
    fun cancel()    
}

class GetCurrentLocation @Inject constructor(private val locationProvider: ILocationProvider) : UseCase<SimpleLocation, UseCase.NoParams>() {
    override suspend fun run(params: NoParams): OneOf<Failure, SimpleLocation> =
            locationProvider.getLocation()

    override fun cancel() {
        super.cancel()
        locationProvider.cancel()
    }
}

You can see that we have one additional method here—cancel. This is because we need a way to cancel GPS location updates. Our Provider implementation, defined in the model layer, goes here:

class GPSLocationProvider constructor(var activity: Activity) : ILocationProvider {

    private var locationManager: LocationManager? = null
    private var locationListener: GPSLocationListener? = null

    override fun getLocation(): OneOf<Failure, SimpleLocation> = runBlocking {
        val grantedResult = getLocationPermissions()

        if (grantedResult.isError) {
            val error = (grantedResult as OneOf.Error<Failure>).error
            OneOf.Error(error)
        } else {
            getLocationFromGPS()
        }
    }

    private suspend fun getLocationPermissions(): OneOf<Failure, Boolean> = suspendCoroutine {
        Dexter.withActivity(activity)
                .withPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                .withListener(PermissionsCallback(it))
                .check()
    }

    private suspend fun getLocationFromGPS(): OneOf<Failure, SimpleLocation> = suspendCoroutine {
        locationListener?.unsubscribe()
        locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        locationManager?.let { manager ->
            locationListener = GPSLocationListener(manager, it)
            launch(UI) {
                manager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0.0f, locationListener)
            }
        }
    }

    override fun cancel() {
        locationListener?.unsubscribe()
        locationListener = null
        locationManager = null
    }
}

This provider is prepared to work with Kotlin coroutines. If you remember, the UseCases’ run method is called on a background thread—so we have to make sure and properly mark our threads. As you can see, we have to pass an activity here—it is crucially important to cancel updates and unregister from the listeners when we no longer need them to avoid memory leaks. Since it implements the ILocationProvider, we can easily modify it in the future to some other provider. We can also easily test the handling of the current location (automatically or manually), even without enabling the GPS in our phone—all we have to do is to replace the implementation to return a randomly constructed location. To make it work, we have to add the newly created UseCase to the LocationsViewModel. The ViewModel, in turn, has to have a new method, getCurrentLocation, which will actually call the use case. With only a few small UI changes to call it and register the GPSProvider in Dagger—and voila, our app is finished!

Summary

I was trying to show you how we can develop an Android app which is easy to maintain, test, and change. It should be also easy to understand—if somebody new comes to your work, they shouldn’t have a problem with understanding the data flow or the structure. If they are aware that the architecture is clean, they can be sure that changes in the UI won’t affect anything in the model, and adding a new feature will not take more than predicted. But this is not the end of the journey. Even if we have a nicely structured app, it is very easy to break it by messy code changes “just for a moment, just to work.” Remember—there is no code “just for now.” Each code which breaks our rules can persist in the codebase and can be a source of future, bigger breaks. If you come to that code just a week later, it will look like somebody implemented some strong dependencies in that code and, to resolve it, you’ll have to dig through many other parts of the app. A good code architecture is a challenge not only at the beginning of the project—it is a challenge for any part of an Android app lifetime. Thinking and checking the code should be accounted for every time something is going to change. To remember this, you can, for example, print and hang your Android architecture diagram. You can also force layers’ independence a bit by separating them to three Gradle modules, where the domain module is not aware of the others and the presentation and model modules don’t use each other. But not even this can replace awareness that mess in the app code will take its revenge on us when we least expect it.

Further Reading on the Toptal Blog:

Understanding the basics

  • What is the use of Dagger in Android?

    Dagger is a dependency injection library, which uses code generation and annotations. It is used to manage dependencies, which makes code easier to test and maintain.

  • Google announced official support for Kotlin on Android on I/O 2017, and since you can not take advantage of the newest Java features on that platform, modern Kotlin language is very good for Android.

  • This year, in I/O 2018, Google announced new Android Architecture Components such as LiveData, ViewModel, and Room. They are created to simplify SQLite operations, ease the handling of data changes, and help developers to handle the lifecycle.

Hire a Toptal expert on this topic.
Hire Now
Tomasz Czura

Tomasz Czura

Verified Expert in Engineering

Kraków, Poland

Member since February 6, 2016

About the author

Tomasz (MCS) is an Android wizard and team lead. His favorite project has been doing the app and back-end of a hotel entertainment system.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

Welltok

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.