Android development has come a long way since its inception in 2008. The tools and the framework that developers relied on in the early days have evolved or been replaced entirely, leaving behind a trail of technologies with legacy status. Here, we'll dive into seven of these legacy technologies, explore their importance to android software development, appoint their legacy and modern counterparts, and discuss why the evolution was necessary.
1. From Java to Kotlin
When Android launched, Java was the exclusive language choosen and designed for Android development. A lot of the reason was that Java had a developer community unlike other technologies, allowing Java to have a deeper penetration power into distinct and different markets.
It also had to do with timing and opportunity, due to Apache annoucing the project Apache Harmony, a open source free Java implementation back in year of 2005. One of the biggest reasons for that, was the concept of Sandbox - Each Android application (apk) would run in it's own instance of the JVM, hence making any possible unintentional crash less likely to affect other applications, or protecting the OS somewhat from malicious code. Hence, Java is vital to the Android project.
Of course, if you wanted to design an audio driver or if you wanted to create a specific game, most of the time you had to code some low level code in C/C++, something named native layer by the Android project. However, coding an Android apk with C/C++ came with it's layers of risks and restrictions, given it did not have the Sandbox protection.
It served its purpose well, but the language came with its baggage, notably the infamous Null Pointer Exceptions (NPEs) that caused endless headaches for developers. Also, Java had it's own share of conflicts, primarily conflicts between Microsoft, Sun, Oracle and RedHat leading to the creation of openJDK (an successor to Apache Harmony), and thus the main focus of the community wasn't Google and the Android project, despite Java being vital to Android.
Enter Kotlin, originally unveiled in 2011, but officialy launched in February 2016, as an alternative to Java due to those issues. Kotlin addressed these issues with nullable types and offered a syntax that is concise and readable, significantly simplifying code. While some argue that modern Java (version 21, for example) narrows the gap with Kotlin, back in 2017, Kotlin was a breath of fresh air for developers needing a language more adequate for Android development. The transition from Java to Kotlin took some time to gain traction with Google officially suportting Kotlin only on 2017, and the developer community largely embraced Kotlin since then, and as such, Kotlin has become the official language for Android apk development, despite both Java and C still being supported.
Of course, the main reason Kotlin is able to run on the Android OS is because Kotlin source code is compiled into Java .class (and later .dex) files, hence Kotlin itself is also heavily dependent on the Java Virtual Machine, something not likely to go away at any time in the future.
How to update:
- Add the Kotlin plugin in your project if not added yet:
Groovy
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${version}
or in Kotlin DSL
alias(libs.plugins.jetbrains.kotlin.android)
- In Android Studio, press Code > Convert Java File to Kotlin
- You need to code check the conversion to make sure it's done properly. For that, reading the Kotlin official website docs will help: https://kotlinlang.org/docs/home.html
2. Jetpack Compose: Bye-Bye XML Layouts
Jetpack Compose, first announced in 2019, but released in July 2021, revolutionized UI development in Android. Gone are the days of juggling XML files, the inflater class, and various resource files for layouts, animations, and themes. Gone were the days where an app could crash if the inflater couldn't find a XML reference during runtime, or you could end up with a weird colored dialog box due to using an incorrect app theme. Compose consolidates all these elements into a declarative, Kotlin-exclusive framework, and makes sure that: 1) The code is coherent in compilation time, 2) it is always using the proper app theme, thus avoiding the runtime issues associated with missing classes, weird colors and improper reflection usage.
One standout feature is the composable preview library, which provides a more accurate representation of UI components compared to the old XML layout preview. Despite being stable, Jetpack Compose is still evolving, and developers are encouraged to adopt it gradually because some features are still experimental, like the Canvas
, LazyColumn
, LazyRow
and ConstraintLayout
. As such, those features are still prone to change, and it's not uncommon to find on internet older projects using outdated Compose code for those features - Beware!
Android studio default Jetpack Compose placeholder code:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello world!")
}
}
}
Still, Jetpack Compose's flexibility allows teams to update their apps step by step, changing one screen at a time, or even mixing parts of XML and Compose by using reducing the risk of breaking existing functionality. This can be done either by inserting a Composable inside an XML layout by using an XML element named ComposeView
and calling setContent
on it in the Activity class Kotlin source code. For example:
<androidx.compose.ui.platform.ComposeView
android:id="@+id/my_custom_composable"
android:layout_width="match_parent"
android:layout_height="match_parent" />
findViewById<ComposeView>(R.id.my_custom_composable).setContent {
MaterialTheme {
Surface {
Text(text = "Custom composable!")
}
}
}
Or by calling an composable named AndroidView
in the Activity class Kotlin source code, and then inflating in runtime the necessary XML component tree by coding the factory
and update
methods of AndroidView
. For example:
AndroidView(
modifier = Modifier.fillMaxSize(),
// Occupy the max size in the Compose UI tree
factory = { context ->
// Create or inflate your view hierarchy here
TODO()
},
update = { view ->
// Called each time the composable is updated
TODO()
}
)
How to update:
- You may use ComposeView to insert a compose element inside an XML layout file tree hierarchy (As shown above).
- You may use AndroidView to insert an XML element and inflate it inside a Compose hierarchy (As shown above).
- Entire screens may be redesigned in Compose by studying how Jetpack Compose works. Since Compose uses Kotlin and Android inflater uses different languages, it will require studying Compose and how to build an equivalent screen using it.
- This official tutorial will show how to use Jetpack Compose: https://developer.android.com/develop/ui/compose/tutorial
3. Google Material Design: Evolution to M3
Material Design has been the cornerstone of Android’s visual language since its introduction. Currently in its third iteration, Material Design 3 (M3), released in October 2021, offers a refined subset of color values, text sizes, and typography, enabling developers to create distinctive visual identities for their apps. The colors in Material Design are chosen to ensure they do not clash too strongly, making the app almost always visually pleasant and easy to read. The typography style is also designed to help apps adapt better to screens of any size, ensuring a consistent and polished look. If you want to quickly test or create a material theme, use the web builder for the theme: https://material-foundation.github.io/material-theme-builder/
Google Material Design came to replace older design styles for Android up to version 4.4 KitKat. Until Lollipop, there was no official design style for Android apps, with the design used on apps varying from styles copied from web development, or styles similar to the earlier Macromedia Flash apps, or styles trying to copy iOS.
Updating from M1 or M2 to M3 requires rethinking your app’s style, but the payoff is worth it. The latest version integrates seamlessly with Kotlin and Jetpack Compose, allowing developers to create pleasant apps with less effort than the previous versions. Also, the color scheme of Material 3 makes it far easier to support dark mode with very few changes necessaries to adapt the code.
How to update:
- There is no straightforward way to migrate from earlier version of Material library into Material 3, but you may use the official documentation for some help: https://developer.android.com/develop/ui/compose/designsystems/material2-material3
- If you need the official reference for M3: https://m3.material.io/
4. Dependency Injection: Dagger vs. Koin
Dependency injection (DI) has always been a crucial part of Android development. dagger
, while powerful, wasn’t particularly friendly to Kotlin developers. hilt
improved on dagger
but still retained some limitations, like requiring injection methods to be public.
Koin, launched in November 2017, is a Kotlin-first DI framework that uses a domain-specific language (DSL) to make dependency management more intuitive. By embracing Koin, developers can better align with Kotlin’s object-oriented nature while simplifying DI. Also, the nature of Koin allows the developer to create distinct modules for testing scenarios, and productions scenarios, thus allowing the developer to also mock data under the right circusmtances.
How to update:
- In build.gradle file, application level (not project level) replace the import of any dagger library with Koin library. Example: From
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}
To
dependencies {
implementation("io.insert-koin:koin-core:$koin_version")
implementation("io.insert-koin:koin-android:$koin_android_version")
}
Afterwards, you need to read and understand the documentation of Koin to create and inject Koin modules.
- For more info: https://insert-koin.io/
5. AsyncTasks to Coroutines and Flow
Android has a golden rule when it comes to thread - The main thread, also called UI Thread, should not take more than 16 ms to run. Each method in the Android framework has this time constraint, and if you wish to run a longer, blocking operation like network communication, you should run it on a background thread. So why not run all the code, including UI updating code, on the background thread? Simple, Android also required for it's view objects to be updated only in the UI Thread, to prevent race conditions since the Android UI Toolkit is not Thread safe.
When Android was created, all the parallelism done in the app had to be done using the Java Thread mecanism. While powerful, Java Thread can cause a lot of damage if misused, and Threads were also quite resource expensive in the earlier days of Android developer. To fix this issue, the Handler
and Looper
mechanism was created in API Level 1.
Looper
and Handler
worked like a pair, where the Looper
would create a infinite loop inside a newly started thread, while Handler
would be associated with this Looper
and thus would be able to receive Runnable
objects to run on the thread being managed by the looper. If you found that confusing, this was why the AsyncTask
was created later on the API Level 3, to make short parallel tasks easier to plan and execute. It was much simpler to create an AsyncTask
object, implement it's onPreExecute
, onPostExecute
and doInBackground
methods to guarantee only the blocking code would run on the background, while the other code would run on the UI Thread.
So as it can be seen, managing parallelism back then meant wrestling with AsyncTasks
, Looper
, and Handler
classes—a process fraught with pitfalls like UI thread clogging, the dreaded "Application Not Responding" (ANR) dialog, or crashes if a piece code tried to update the screen from outside the main UI thread. When Google adopted Kotlin as the language for Android development, the door was open for Kotlin based Android apks to use Kotlin Coroutines and Kotlin Flow, which are a Kotlin exclusive paralellism technology, providing a streamlined way to handle long-running and short-lived operations.
Kotlin Coroutines are a method of running parallel code without the need to design specific callback methods, like the AsyncTask used for doInBackground
and onPostExecute
. To create a coroutine, you use a scope as a source, add a job (Although this part may be skipped if you don't care about cancelling the coroutine), and then either launch it, or call it in a suspend mode. The thread being used may be switched by using a suspend method call named withContext
, which returns only after switching the context (eg. The Thread) into the appropriate one. Hence, a code using a coroutine may look like this:
val job = coroutineScope.launch(Dispatchers.Main) {
// Execute setup code
withContext(Dispatchers.IO) {
// Execute the blocking code here
}
withContext(Dispatchers.Main) {
// Execute post blocking code here
}
}
Android Lifecycle Jetpack library added a bunch of new tools to the Kotlin Coroutines, like lifecycleScope
for activity classes and viewModelScope
for ViewModels classes, simplifying memory and resource management. AsyncTask was officially deprecated in Android 11 (September 2020), marking the end of an era.
How to update:
- Most AsyncTask classes may be updated by the rough following procedure. Adjust as needed - 1) Create the coroutine as above, 2) Add the code from
onPreExecute
into the setup part of the coroutine, 3) Add thedoInBackground
code in theDispatchers.IO
part of the coroutine and, 4) Add theonPostExecute
in the blockwithContext(Dispatchers.Main)
- For reference, use the official documentation on the Kotlin website: https://kotlinlang.org/docs/coroutines-overview.html
6. SQLiteDatabase vs. Jetpack Room
SQLite Databases have long been a staple for data management in Android, enabling apps to store SQL information securely. However, for many use cases, they’ve been supplanted by Jetpack Room, introduced in May 2018. Room simplifies database management with features like migration tools and query interfaces, offering an ORM-like experience similar to Hibernate.
To create a database in SQLite using SQLiteDatabase class, it was necessary to configure the params when opening the database (either read only, or read write), treat possible situations like a constraint violation, or a conflict when updating or inserting a data, and rollbacks. Also, each transaction had to be carefully set up before commiting, just like any other SQL database.
Using Room, a component of Android Jetpack, made the SQL database management in Android far easier. You can use an annotation to define the whole database in it's database class, by defining the entities, the version and even how to migrate the database from one version to another. Below is a piece of code from one of my projects:
@Database(
entities = [CurrentWeatherCache::class, WeatherCache::class, HourlyWeatherCache::class, DailyWeatherCache::class],
version = 4,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4)
]
)
abstract class WeatherPeekDatabase : RoomDatabase() {
abstract fun getCurrentWeatherDao(): CurrentWeatherDao
abstract fun getWeatherCacheDao(): WeatherCacheDao
abstract fun getHourlyWeatherCacheDao(): HourlyWeatherCacheDao
abstract fun getDailyWeatherCacheDao(): DailyWeatherCacheDao
}
Creating the select, insert, update and delete methods is even easier, just requiring the developer to create an interface with annotations on each method:
@Dao
interface WeatherCacheDao {
@Query("SELECT * FROM WeatherCache ORDER BY id ASC")
fun getAll(): LiveData<List<WeatherCache>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(vararg weather: WeatherCache)
@Query("DELETE FROM WeatherCache")
suspend fun clear()
}
Compare that to the SQLiteDatabase method of reading from a SQL table from the official documentation:
// you will actually use after this query.
val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)
// Filter results WHERE "title" = 'My Title'
val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
val selectionArgs = arrayOf("My Title")
// How you want the results sorted in the resulting Cursor
val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"
val cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
)
val itemIds = mutableListOf<Long>()
with(cursor) {
while (moveToNext()) {
val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
itemIds.add(itemId)
}
}
cursor.close()
How to update:
- Updating from one to the other is straightforward. You need to create a proper schema, and then a database class, and at least one dao class - Some of the query may be imported in the case of the daos, but most of the code from the database will be deleted, since it's usually boilerplate.
- For reference, use the official documentation: https://developer.android.com/training/data-storage/room
7. Shared Preferences to DataStore
In older Android development, when the developer wanted to save key-value pairs, they used to rely on Shared Preferences, a Android class added on API Level 1, which worked well back when Android didn't require thread intensive applications, or elaborate user interfaces. It worked synchronously, commiting simple key value pairs into a local "database", the database being nothing more than a local file without the typical database expectations and properties.
However, using this Android Context framework component had it's risks in multithread environments, including the possibility of race conditions, and UI Thread blocking. Hence Android Project introduced in 2020 two distinct implementations, one called Preferences DataStore, and other called Proto DataStore as a modern alternative, providing reliable and structured data storage in a multithread environment, along with UI safe execution. For the context of this article, I'm going to consider Preferences DataStore, since this library is the most direct successor do SharedPreferences. Besides those advantages, another big advantage of DataStore is being compatible with Kotlin Coroutines and Kotlin Flow.
For example, to use SharedPreferences, you simply had to create the database and call a get or put method on it:
val sharedPref = activity?.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE)
with (sharedPref.edit()) {
putInt(getString(R.string.saved_high_score_key), newHighScore)
apply()
}
Simple, straightforward, but Thread unsafe. On the other hand, the modern equivalent is also easier to understand, but behind the method calls, it already take cares of things like UI blocking and Thread safety:
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val HIGH_SCORE = intPreferencesKey("high_score")
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
settings[HIGH_SCORE] = newHighScore
}
}
As seen above, DataStore signals a shift toward a Kotlin friendlier, thread safer, error signalling, exception safer solution, with data migration, addressing many of the shortcomings of Shared Preferences.
How to update:
- Updating SharedPreferences to Preferences DataStore is easy - First, add the library in the build.gradle app file:
implementation("androidx.datastore:datastore-preferences:${version}")
Make sure to create only one instance of the DataStore for each file you are using. Afterwards, change each instance of edit()
to make sure it's using Preferences DataStore edit()
version, and it's using it inside a suspend function, like above.
- The hardest part will be replacing the get method from
SharedPreferences
with the.data
method from DataStore, which returns aFlow<T>
. This will require understanding Kotlin Flow, and creating at least a single element Flow when retrieving info from the DataStore. - For reference, use the official documentation: https://developer.android.com/topic/libraries/architecture/datastore
Final Word
Android development is constantly evolving, adopting new languages like Kotlin
, new UI development frameworks like Jetpack Compose
, new coding paradigms like Kotlin Coroutines
and Kotlin Flow
, and even creating some solutions to be more friendly with other modern Android technologies, like Room
, DataStore
and Koin
. Embracing modern tools like Kotlin, Jetpack Compose, Material Design 3, Koin, Coroutines, Jetpack Room, and DataStore not only simplifies development but also ensures apps are more robust, scalable, maintainable and secure.
The transition from legacy systems to modern frameworks isn’t always smooth, requires effort and time, may add some risks into the project, and new unexpected constraints, but the benefits far outweigh the challenges. Take for example the reduced development time by adopting Kotlin instead of Java, or Compose instead of XML. Or the reduction of boilerplate code when using Kotlin instead of Java, or the powerful functionalities of Kotlin DSL libraries like Koin. By keeping up with these advancements, developers can deliver better experiences to users, in a shorter time and accompany the ever-changing Android ecosystem keeping their product up-to-date.
Cover image designed by fullvector at Freepik - www.freepik.com
Top comments (0)