UnicornCRM | The Pragmatic End to End Developer Guide to Creating an Agentic AI Powered CRM | Part 1

The Complete End to End Guide to Creating an AI Powered CRM | Part 1 | Dependency Injection Using Koin

Arunabh Das
Developers Inc
4 min readDec 28, 2023

--

Overview

In this developer guide, we document the step by step process of creating an Agentic AI Powered CRM for Android.

Prerequisites

Basic knowledge of Android development is a prerequisite to follow along.

Project Structure

The project structure from the starter kit is as below. For the purposes of this developer guide, we shall refer to app/unicornapp/unicorncrm as ${ROOT_PACKAGE}

Project Structure | ${ROOT_PACKAGE} is app/unicornapp/unicorncrm

Get Started

In order to get started and follow along with this dev guide, please download the starter kit from the feature/starter-kit branch of the repo below

Initial

Upon running the feature/starter-kit branch using Android Studio, you should see the following screen

Unicorn CRM | Agentic AI Powered CRM

Dependency Injection Using Koin

In step 1, we shall perform and document the setup steps needed for dependency injection using Koin, which is a popular library for dependency injection for Android.

Step 1 | Add Koin and Retrofit Dependencies

Add the following dependencies to the app/build.gradle

    // Koin
def koin_version = "3.4.2"
implementation "io.insert-koin:koin-android:$koin_version"
implementation "io.insert-koin:koin-androidx-navigation:$koin_version"
implementation "io.insert-koin:koin-androidx-compose:$koin_version"
testImplementation "io.insert-join:koin-test-junit4:$koin_version"

// Retrofit
def retrofit_version = "2.9.0"
def moshi_version = "5.0.0-alpha.3"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.okhttp3:okhttp:$moshi_version"

// Lifecycle
def lifecycle_viewmodel_version = "2.6.2"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_viewmodel_version"

Step 2 | Create UnicornApi

Create a stub UnicornApi in ${ROOT_PACKAGE}/api package as below

package app.unicornapp.unicorncrm.api

import retrofit2.http.GET

interface UnicornApi {
@GET("/unicorn/api/")
fun callApi()
}

Step 3 | Constructor inject UnicornApi in MainRepositoryImpl as below

Modify MainRepositoryImpl to inject UnicornApi in the MainRepositoryImpl constructor as below


package app.unicornapp.unicorncrm.presentation

import app.unicornapp.unicorncrm.api.UnicornApi

class MainRepositoryImpl(
private val api: UnicornApi
) : MainRepository{
override fun doNetworkCall() {
api.callApi()
}
}

Step 4 | Create UnicornApi singleton in AppModule

Create AppModule.kt in ${ROOT_PACKAGE} package as below

This shows Koin how to create a UnicornApi singleton object using the Builder pattern

package app.unicornapp.unicorncrm

import app.unicornapp.unicorncrm.api.UnicornApi
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory


val appModule = module {
single {
Retrofit.Builder()
.baseUrl("localhost:8000/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(UnicornApi::class.java)
}
}

Step 5 | Create MainRepository singleton in AppModule

Modify AppModule to show Koin how to create a MainRepository singleton object as an abstraction

package app.unicornapp.unicorncrm

import app.unicornapp.unicorncrm.api.UnicornApi
import app.unicornapp.unicorncrm.presentation.MainRepository
import app.unicornapp.unicorncrm.presentation.MainRepositoryImpl
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory


val appModule = module {
single {
Retrofit.Builder()
.baseUrl("localhost:8000/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(UnicornApi::class.java)
}

single<MainRepository> {
MainRepositoryImpl(get())
}
}

Step 6 | Create MainViewModel singleton in AppModule

Modify AppModule to show Koin how to create a MainViewModel singleton object (which uses the viewModel factory pattern behind the scenes)


package app.unicornapp.unicorncrm

import app.unicornapp.unicorncrm.api.UnicornApi
import app.unicornapp.unicorncrm.presentation.MainRepository
import app.unicornapp.unicorncrm.presentation.MainRepositoryImpl
import app.unicornapp.unicorncrm.presentation.MainViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory


val appModule = module {
single {
Retrofit.Builder()
.baseUrl("http://localhost:8000/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(UnicornApi::class.java)
}

single<MainRepository> {
MainRepositoryImpl(get())
}

viewModel {
MainViewModel(get())
}
}

Now that Koin knows how to provide the UnicornApi, MainRepository and MainViewModel dependencies, we need to actually inject the dependencies

Step 7 | Inject MainViewModel instance into MainActivity

Inject instance of MainViewModel into MainActivity and call the doNetworkCall function on the object in the setContent block of onCreate of MainActivity as below

val viewModel = getViewModel<MainViewModel>()
viewModel.doNetworkCall()

MainActivity should now look as below

package app.unicornapp.unicorncrm

import android.Manifest
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.compose.rememberNavController
import androidx.navigation.NavHostController
import app.unicornapp.unicorncrm.presentation.DefaultViewModel
import app.unicornapp.unicorncrm.presentation.MainViewModel
import app.unicornapp.unicorncrm.ui.screens.NavGraphs
import app.unicornapp.unicorncrm.ui.theme.UnicornTheme
import com.ramcosta.composedestinations.DestinationsNavHost
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import org.koin.androidx.viewmodel.ext.android.getViewModel


class MainActivity : ComponentActivity() {
lateinit var navHostController: NavHostController
private val viewModel: DefaultViewModel by viewModels()

@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

installSplashScreen().apply {
setKeepOnScreenCondition {
viewModel.isLoading.value
}
}
setContent {

UnicornTheme {
val viewModel = getViewModel<MainViewModel>()
viewModel.doNetworkCall()
navHostController = rememberNavController()
DestinationsNavHost(navGraph = NavGraphs.root)

}
}
}

@OptIn(ExperimentalPermissionsApi::class)
override fun onStart() {
super.onStart()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
0
)
}

}

override fun onBackPressed() {
super.onBackPressed()
}

}

Step 8 | Initialize and start Koin in UnicornApplication class

Initialize and start Koin in UnicornCrmApplication using startKoin in onCreate

startKoin {
androidLogger()
androidContext(this@UnicornCrmApplication)
modules(appModule)
}

UnicornCrmApplication should now look as below

package app.unicornapp.unicorncrm

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build

import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import timber.log.Timber
import app.unicornapp.unicorncrm.BuildConfig


class UnicornCrmApplication : Application(){
override fun onCreate() {
super.onCreate()

startKoin {
androidLogger()
androidContext(this@UnicornCrmApplication)
modules(appModule)
}

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"channel",
"Scanning Notifications",
NotificationManager.IMPORTANCE_HIGH
)
}

val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
}

Output

Running the above code gives us the following results, and we can note from the logcat logs that the MainViewModel is being initialized and executed using dependency injection as expected

Results from End of Part 1

This bring us to the end of The Pragmatic End to End Guide to Creating an AI Powered CRM | Part 1 | Dependency Injection Using Koin

We continue the journey of The Pragmatic End to End Guide to Creating an AI Powered CRM in Part 2

--

--

Arunabh Das
Developers Inc

Sort of an executive-officer-of-the-week of a-techno-syndicalist commune. Cypherpunk, techno-idealist, peacenik, spiritual, humanist