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
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}
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
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
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