DEV Community

Cover image for Seven Legacy Technologies in Android Development and Their Modern Counterparts
marcotc
marcotc

Posted on

Seven Legacy Technologies in Android Development and Their Modern Counterparts

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}
Enter fullscreen mode Exit fullscreen mode

or in Kotlin DSL

alias(libs.plugins.jetbrains.kotlin.android)
Enter fullscreen mode Exit fullscreen mode
  • 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!")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode
findViewById<ComposeView>(R.id.my_custom_composable).setContent {
    MaterialTheme {
        Surface {
          Text(text = "Custom composable!")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
    )
Enter fullscreen mode Exit fullscreen mode

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:

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'
}
Enter fullscreen mode Exit fullscreen mode

To

dependencies {
    implementation("io.insert-koin:koin-core:$koin_version")
    implementation("io.insert-koin:koin-android:$koin_android_version")
}
Enter fullscreen mode Exit fullscreen mode

Afterwards, you need to read and understand the documentation of Koin to create and inject Koin modules.

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the doInBackground code in the Dispatchers.IO part of the coroutine and, 4) Add the onPostExecute in the block withContext(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
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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 a Flow<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

Do your career a favor. Join DEV. (The website you're on right now)

It takes one minute and is worth it for your career.

Get started

Top comments (0)

Sentry mobile image

Improving mobile performance, from slow screens to app start time

Based on our experience working with thousands of mobile developer teams, we developed a mobile monitoring maturity curve.

Read more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay