Databases and Dependency Injection in Kotlin

This tutorial explains how a Kotlin Android application can store structured data locally and provide database-related objects through dependency injection. It includes the original Anko SQLite and Dagger 2 examples by Aanand Shekhar Roy, followed by guidance for understanding and modernizing the architecture.

How databases and dependency injection work together in Kotlin

A local database keeps structured application data available after the process stops or the device loses its network connection. Dependency injection controls how objects such as database helpers, repositories, and presenters receive access to that database.

  • SQLite or Room stores and retrieves the application data.
  • A database helper or database class creates the database connection and schema.
  • A repository provides a focused API for reading and writing data.
  • Dagger or Hilt creates these objects and supplies them to consumers.
  • A presenter or ViewModel requests data through the repository instead of constructing the database directly.

This separation makes database code easier to test and prevents activities, fragments, and other UI classes from managing connection details themselves.

Compatibility note for the Anko SQLite examples

The original recipes below use Anko SQLite, Anko layouts, the obsolete Gradle compile configuration, and older Android support APIs. They are retained to explain the original implementation and may still help when maintaining a legacy application. Anko is no longer the recommended database abstraction for a new Android project.

For current Android development, use Room over SQLite and consider Hilt for Android-oriented dependency injection. The underlying design remains the same: create one application-scoped database, expose its data-access objects, and inject a repository into the class that needs the data.

Using the legacy Anko SQLite database in Kotlin

Android includes SQLite for relational data stored on the device. Direct SQLite code requires explicit work with schemas, cursors, resource management, and migrations. Anko SQLite provided a Kotlin-oriented wrapper around those APIs.

Adding the original Anko SQLite dependency

The original project used Android Studio 3.0 and added anko-sqlite to its Gradle dependencies:

</>
Copy
dependencies {
    compile "org.jetbrains.anko:anko-sqlite:$anko_version"
}

This block is historical. In a maintained legacy build, the Anko version must match the rest of that project. A new project should not copy this dependency configuration.

Creating the Anko SQLite database helper

The database helper extends ManagedSQLiteOpenHelper. Its companion object maintains one helper instance based on the application context, avoiding an accidental reference to an activity.

</>
Copy
class DatabaseHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "SupportDatabase", null, 1) {
    companion object {
        private var instance: DatabaseHelper? = null

        @Synchronized
        fun getInstance(context: Context): DatabaseHelper {
            if (instance == null) {
                instance = DatabaseHelper(context.applicationContext)
            }
            return instance!!
        }
    }

    override fun onCreate(db: SQLiteDatabase) {
        db.createTable("Requests", true,
                "id" to INTEGER + PRIMARY_KEY + UNIQUE,
                "name" to TEXT,
                "message" to TEXT)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.dropTable("Requests", true)
    }
}

onCreate() creates the Requests table when the database is first opened. The table contains id, name, and message columns. Although the example declares only id as the primary key, the original destructive onUpgrade() implementation drops the table and therefore loses its data. Production applications should use explicit migrations when existing data must be preserved.

An extension property makes the singleton helper available from a Context:

</>
Copy
// Access property for Context
val Context.database: DatabaseHelper
    get() = DatabaseHelper.getInstance(getApplicationContext())

Inserting a request with the Anko database helper

The original activity builds its interface with the Anko layout DSL and inserts a request when the user selects the Enter button:

</>
Copy
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
btn_send.onClick {
            database.use {
                insert("Requests",
                        "id" to 1,
                        "name" to name.text.toString(),
                        "message" to message.text.toString())
            }
        }
    }

    class MainActivityUI : AnkoComponent<MainActivity> {
        override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            verticalLayout {
                gravity = Gravity.CENTER
                padding = dip(20)

                textView {
                    gravity = Gravity.CENTER
                    text = "Enter your request"
                    textColor = Color.BLACK
                    textSize = 24f
                }.lparams(width = matchParent) {
                    margin = dip(20)
                }

                val name = editText {
                    id = R.id.name
                    hint = "What is your name?"
                }

                editText {
                    id = R.id.message
                    hint = "What is your message?"
                    lines = 3
                }

                button("Enter") {
                    id = R.id.btn_send
                }
            }
        }
    }
}

The database operations run inside the Anko use block. The helper opens the database for the operation and releases the associated resource afterward. The fixed ID in this introductory example permits only one row with id = 1; a real application should generate unique IDs or use an auto-incrementing key.

The original form appears as follows:

Databases and Dependency Injection in Kotlin

After a request is entered, its values can be inspected in the database:

Databases and Dependency Injection in Kotlin

The original project used Stetho (https://github.com/facebook/stetho) to inspect its SQLite database through Chrome developer tools. This is part of the historical development setup rather than a required component of SQLite or dependency injection.

Creating multiple SQLite tables with Anko

The next recipe creates Requests and customers tables. The original code is preserved below so that the complete legacy example remains available.

  1. The Requests table can have the name and message fields, and you can directly create them in the onCreate method of your database helper, as shown below:

    db.createTable("Requests", true,
        "id" to INTEGER + PRIMARY_KEY + UNIQUE,
        "name" to TEXT,
        "message" to TEXT)
  2. For the customers table, you’ll need to use a better coding practice by making a data class and using it to define the columns of the customers table. Here’s the code for the Customer data class:

    data class Customer(val id: Int, val name: String, val phone_num: String) {
        companion object {
            val COLUMN_ID = "id"
            val TABLE_NAME = "customers"
            val COLUMN_NAME = "name"
            val COLUMN_PHONE_NUM = "phone_num"
        }
    }
  3. Use this data class to create the table as follows:

    db.createTable(Customer.TABLE_NAME,
            true,
            Customer.COLUMN_ID to INTEGER + PRIMARY_KEY,
            Customer.COLUMN_NAME to TEXT,
            Customer.COLUMN_PHONE_NUM to TEXT)
  4. The following is how your database helper finally looks after filling in the code for drop tables:

    class DatabaseHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "SupportDatabase", null, 1) {
        companion object {
            private var instance: DatabaseHelper? = null
    
            @Synchronized
            fun getInstance(context: Context): DatabaseHelper {
                if (instance == null) {
                    instance = DatabaseHelper(context.applicationContext)
                }
                return instance!!
            }
        }
    
        override fun onCreate(db: SQLiteDatabase) {
    db.createTable("Requests", true,
                    "id" to INTEGER + PRIMARY_KEY + UNIQUE,
                    "name" to TEXT,
                    "message" to TEXT)
    
            db.createTable(Customer.TABLE_NAME,
                    true,
                    Customer.COLUMN_ID to INTEGER + PRIMARY_KEY,
                    Customer.COLUMN_NAME to TEXT,
                    Customer.COLUMN_PHONE_NUM to TEXT)
        }
    
        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            db.dropTable("Requests", true)
            db.dropTable(Customer.TABLE_NAME, true)
        }
    }
    
    // Access property for Context
    val Context.database: DatabaseHelper
        get() = DatabaseHelper.getInstance(getApplicationContext())
  5. Databases and Dependency Injection in Kotlin

    Time to install your app and see whether the two tables have been formed in the database. The following screenshot shows how the database will look (using Stetho):

Keeping table and column names in one place reduces typographical errors, but it does not make the data class itself a database entity. Room improves this design by declaring tables and columns with annotations and validating queries during compilation.

Injecting Kotlin dependencies with Dagger 2

Dependency injection means that a class receives its collaborators instead of constructing them internally. Dagger builds a dependency graph from injectable constructors, modules, provider methods, components, and scopes. Android’s dependency-injection guidance is available at developer.android.com/training/dependency-injection.

The original Dagger 2 examples demonstrate field injection, provider methods, and constructor injection:

  1. To inject the object, you just need to add the @Inject annotation before the variable and the object will be injected there. Take a look at the following example:

    @Inject
    lateinit var mPresenter:AddActivityMvpPresenter

    The lateinit modifier is also used to void null checks before using the variable.

  2. Another way to do it is by constructor injection. To understand it, take a look at the given code:
    @Module
    class AddActivityModule {
      @Provides @ControllerScope
      fun providesAddActivityPresenter(addActivityPresenter: AddActivityPresenter):AddActivityMvpPresenter =addActivityPresenter
    }
  3. As you can see, AddActivityPresenter is sent to the providesAddActivityPresenter, but the module doesn’t provide it. This usually won’t work unless you provide AddActivityPresnter as follows:

    class AddActivityPresenter @Inject constructor(var mDataManager:DataManager):AddActivityMvpPresenter

    When you use the @Inject annotation in the constructor, it means that the class needs the DataManager object before it can be created. Dagger2 will look into the dependency tree and provide you the dependency if it can.

lateinit allows a non-null property to be initialized after object construction, but reading it before injection causes an exception. Constructor injection is generally easier to reason about because the required dependency is visible in the constructor and the object cannot be created without it.

Reading SQLite rows with the Anko query builder

The final legacy recipe adds a button that reads all rows from Requests, converts the cursor rows into Request objects, and writes their values to Logcat.

  1. Add a button to the existing layout; on clicking, it should retrieve all the data from the Requests table. Check out the updated code, which is as follows:

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            MainActivityUI().setContentView(this)
            val btn_send = find<Button>(R.id.btn_send)
            btn_send.onClick {
                database.use {
                    insert("Requests",
                            "name" to name.text.toString(),
                            "message" to message.text.toString())
                }
                toast("success")
                name.text.clear()
                message.text.clear()
            }
            val btn_read = find<Button>(R.id.btn_read)
            btn_read.onClick {
    var reqs = database.use {
                    select("Requests").parseList(classParser<Request>())
                }
                for(x in reqs) {
                    logd(x.name + ": " + x.message)
                }
            }
        }
    
        private fun logd(s: String) {
            Log.d("request", s)
        }
    
        class MainActivityUI : AnkoComponent<MainActivity> {
            override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
                verticalLayout {
                    padding = dip(20)
    
                    textView {
                        gravity = Gravity.CENTER
                        text = "Enter your request"
                        textColor = Color.BLACK
                        textSize = 24f
                    }.lparams(width = matchParent) {
                        margin = dip(20)
                    }
    
                    val name = editText {
                        id = R.id.name
                        hint = "What is your name?"
                    }
    
                    editText {
                        id = R.id.message
                        hint = "What is your message?"
                        lines = 3
                    }
    
                    button("Enter") {
                        id = R.id.btn_send
                    }
    
                    button("Show me requests") {
                        id = R.id.btn_read
                    }
                }
            }
        }
    
        class Request(val id: Int, val name: String, val message: String)
    
    }

     

  2. Use Anko DSL to create the layout for the activity. To read data from the database, we use the select function. The syntax is as follows:
    db.select(tableName, vararg columns) // where db is an instance of the SQLiteDatabase
  3. Inside database.use {…} you can directly use methods such as select and insert.
    This is the data:
    Databases and Dependency Injection in Kotlin
    Here’s the output:

    11-18 18:21:34.709 12523-12523/android.my_company.com.helloworldapp D/request: name 1: request 1
    11-18 18:21:34.709 12523-12523/android.my_company.com.helloworldapp D/request: name 2: request 2
    11-18 18:21:34.709 12523-12523/android.my_company.com.helloworldapp D/request: name 3 : request 3
  4. There’s a lot more you can do with the query builder; listed here are the methods provided by Anko:
    • column(String): This is used to add a column to the select query
    • distinct(Boolean): This is used to add distinct to the query
    • whereArgs(String): This is used to specify the raw where string
    • whereArgs(String, args): This is used to specify the where query and the corresponding arguments
    • whereSimple(String, args): This is used to specify a where query with the ? marks and corresponding arguments for ?
    • orderBy(String, [ASC/DESC]): This is used to specify a column for order by
    • groupBy(String): This is used to specify a column for group by
    • limit(count: Int): This is used to limit the number of rows returned by the query
    • limit(offset: Int, count: Int): This is used to limit the number of rows returned by the query after an offset
    • having(String): This is used to specify the raw having expression
    • having(String, args): This is used to specify the raw having expression with arguments
  5. Here’s another example. In this example, you will select data from a database using the where clause:

    select("Requests")
        .whereArgs("(id > {userId})",
            "userId" to 1)

    Here’s the output of the about query:

    11-18 21:11:04.328 18149-18149/android.my_company.com.helloworldapp D/request: name 2: request 2
    11-18 21:11:04.329 18149-18149/android.my_company.com.helloworldapp D/request: name 3 : request 3
  6.  After getting the query results, you also need to parse the result. You’ll get a cursor as a result from the query and using methods provided by Anko, you can easily parse them into regular classes. In the preceding example, you made a class named Request:

    class Request(val id: Int, val name: String, val message: String) 
  7. The class has all the fields that you may get as columns from your query result cursor. The following are the methods that you can use for parsing results:
    • parseSingle(rowParser): T: This parses only one row; if there’s more than one row in the cursor, then it throws an exception
    • parseOpt(rowParser): T?: This parses zero or one row, but if there’s more than one row in the cursor, then it throws an exception
    • parseList(rowParser): List<T>: This parses zero or more rows

The example uses parseList because the query can return any number of rows. A class parser can also create a row parser for a matching custom type:

valrowParser= classParser<Person>()

Modern Kotlin database architecture with Room and Hilt

A current Android implementation normally replaces the Anko helper and cursor parsing with Room entities, data-access objects, and a Room database. Hilt can then provide the database, DAO, and repository from an application-scoped dependency graph.

Defining a Room entity for stored requests

</>
Copy
@Entity(tableName = "requests")
data class RequestEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val message: String
)

The entity defines the table schema. The generated key avoids the fixed-ID problem shown in the introductory Anko insert.

Creating a Room DAO for insert and read operations

</>
Copy
@Dao
interface RequestDao {
    @Insert
    suspend fun insert(request: RequestEntity): Long

    @Query("SELECT * FROM requests ORDER BY id DESC")
    fun observeAll(): Flow<List<RequestEntity>>

    @Query("SELECT * FROM requests WHERE id = :id LIMIT 1")
    suspend fun findById(id: Long): RequestEntity?
}

Room validates the SQL against the declared schema. The write methods are suspending functions, while Flow lets consumers observe table changes without repeatedly running a manual cursor query.

Declaring the Room database

</>
Copy
@Database(
    entities = [RequestEntity::class],
    version = 1,
    exportSchema = true
)
abstract class SupportDatabase : RoomDatabase() {
    abstract fun requestDao(): RequestDao
}

Increase the version and provide a migration whenever a released schema changes. Avoid destructive migration behavior when users’ stored data must survive an application update.

Providing Room dependencies through a Hilt module

</>
Copy
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(
        @ApplicationContext context: Context
    ): SupportDatabase = Room.databaseBuilder(
        context,
        SupportDatabase::class.java,
        "support.db"
    ).build()

    @Provides
    fun provideRequestDao(database: SupportDatabase): RequestDao =
        database.requestDao()
}

The database is scoped as a singleton because the application should share one Room database instance. The DAO is obtained from that same instance. Using @ApplicationContext prevents an activity context from being retained by the long-lived database.

Injecting the Room DAO into a Kotlin repository

</>
Copy
@Singleton
class RequestRepository @Inject constructor(
    private val requestDao: RequestDao
) {
    val requests: Flow<List<RequestEntity>> = requestDao.observeAll()

    suspend fun addRequest(name: String, message: String): Long {
        require(name.isNotBlank()) { "Name must not be blank" }
        require(message.isNotBlank()) { "Message must not be blank" }

        return requestDao.insert(
            RequestEntity(
                name = name.trim(),
                message = message.trim()
            )
        )
    }
}

The repository gives the rest of the application a domain-focused interface. UI code no longer needs table names, SQL statements, cursors, or a database context.

Using the injected repository from a Hilt ViewModel

</>
Copy
@HiltViewModel
class RequestViewModel @Inject constructor(
    private val repository: RequestRepository
) : ViewModel() {

    val requests = repository.requests
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    fun submit(name: String, message: String) {
        viewModelScope.launch {
            repository.addRequest(name, message)
        }
    }
}

The ViewModel receives the repository through constructor injection. Hilt resolves the chain from RequestViewModel to RequestRepository, RequestDao, and finally SupportDatabase.

Manual dependency injection for a small Kotlin application

Dagger or Hilt is not mandatory. A small application can create an application-level container and pass its repository to consumers explicitly. This is called manual dependency injection.

</>
Copy
class AppContainer(context: Context) {
    private val database = Room.databaseBuilder(
        context.applicationContext,
        SupportDatabase::class.java,
        "support.db"
    ).build()

    val requestRepository = RequestRepository(database.requestDao())
}

Manual injection keeps object creation visible and requires no code-generation framework. Its main cost is that the application must manage scopes and pass dependencies through factories or constructors. Hilt becomes more useful as the dependency graph and Android component integration grow.

Database dependency-injection mistakes to avoid

  • Creating a database for every screen: share one application-scoped Room database instead.
  • Using an activity context for a singleton: inject or pass the application context.
  • Running blocking database work on the main thread: expose suspending functions, Flow, or another asynchronous API.
  • Dropping tables during every upgrade: define and test migrations when stored user data must be retained.
  • Injecting Room directly into UI code: place database operations behind a DAO and repository.
  • Using field injection where constructor injection is possible: constructor parameters make required collaborators explicit and simplify testing.
  • Giving every object singleton scope: use the narrowest scope that matches the object’s intended lifetime.
  • Sharing entities indiscriminately across layers: introduce domain or UI models when persistence details should not leak beyond the data layer.

Testing a Kotlin database with injected dependencies

Dependency injection makes the database boundary replaceable during tests. DAO tests can use an in-memory Room database, while repository and ViewModel tests can use a fake implementation that returns controlled data.

  • Test each DAO query against representative rows, empty results, and ordering rules.
  • Test migrations with database files created at earlier schema versions.
  • Verify that repository validation handles blank or invalid input.
  • Use a fake repository when testing UI state and ViewModel behavior.
  • Confirm that concurrent reads and writes do not create multiple database instances.

Frequently asked questions about Kotlin databases and dependency injection

Why inject a database instead of creating it inside an activity?

Creating the database inside an activity couples storage configuration to the UI lifecycle and makes the activity difficult to test. Injection lets the application share one configured database and lets the activity or ViewModel depend on a repository abstraction.

Should a new Kotlin Android project use Anko SQLite?

No. The Anko examples on this page are useful for understanding or maintaining older code. Room is the normal choice for a new Android application that stores structured data in SQLite.

What is the difference between Dagger and Hilt?

Dagger is the underlying compile-time dependency-injection framework. Hilt builds on Dagger and supplies Android-specific components, scopes, entry points, and conventions, reducing the setup required for activities, fragments, services, and ViewModels.

Can Room be used without Hilt or Dagger?

Yes. Room does not require a dependency-injection framework. You can construct the database in an application-level container and pass the DAO or repository through constructors and factories.

Should a Room database be a singleton?

An application normally maintains one Room database instance for a database file. Sharing that instance avoids unnecessary connection pools and keeps database access consistent. The DAOs and repositories can then be provided from that instance according to their required lifetimes.

Editorial QA checklist for this Kotlin database tutorial

  • Confirm that the Anko examples are clearly identified as legacy code.
  • Verify that every new Kotlin code block uses the language-kotlin PrismJS class.
  • Check that the Room entity, DAO, and database use matching table and type names.
  • Confirm that the Hilt database provider uses the application context and singleton scope.
  • Verify that database writes are not presented as main-thread operations.
  • Check that schema changes advise migrations instead of unconditional table deletion.
  • Confirm that all four original screenshots and the Stetho URL remain present.
  • Review the official Android dependency-injection link when revising framework guidance.