Abstract room database implementation not found dagger hilt - kotlin

I am new to Android dev and need to build my first project for university.
The university was using the old XML version with Java, but I wanted to learn Compose so I learnt Kotlin.
Now after getting everything setup, I am trying to use hiltViewModel() method to inject the view model inside the composable function and I am getting an error.
I watched this tutorial: https://www.youtube.com/watch?v=A7CGcFjQQtQ&t=10s and a few other stack overflow questions which suggest doing the same thing but I am not sure what is going on.
After getting this to work. Now it says a database class implementation is not found, but I expect Dagger Hilt to produce this? for Room database
Here is the basic code:
Dependencies:
build.gradle:
buildscript {
ext {
compose_version = '1.0.0'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10"
//->Adding the Dagger Hilt class path here as suggested:
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
app/build.gradle:
plugins {
id 'com.android.application'
id 'kotlin-android'
//->Adding Kotlin annotation processing plugin and the dagger hilt plugin:
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk 31
defaultConfig {
applicationId "ac.gre.mxpenseresit"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.5.10'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.activity:activity-compose:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
//This version for navigation is hard coded, probably will need to update later, but should be fine for assignment
implementation("androidx.navigation:navigation-compose:2.4.2")
//Room Dependencies:
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
//Coroutine dependency:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
//->Dagger Hilt Dependency for DI copied from official docs:
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-compiler:2.38.1"
}
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
/**
* Adding the Hilt Android App dependency for the Application class
*/
#HiltAndroidApp
class MExpenseApp : Application() {
}
Main & only activity:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MXPenseResitTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
App(modifier = Modifier)
}
}
}
}
}
Main Module:
**
* Setup dependencies:
*/
#Module
#InstallIn(ActivityComponent::class)
object MainModule {
#Provides
#Singleton
fun provideDb(application: Application): MExpenseDb {
return Room
.databaseBuilder(
application,
MExpenseDb::class.java,
"MExpenseDb"
).build()
}
/**
* We are type hinting to the Interface so we can change implementations
*/
#Provides
#Singleton
fun provideTripRepository(mExpenseDb: MExpenseDb): TripRepositoryContract {
return TripRepository(mExpenseDb.tripDao)
}
}
View Model:
#HiltViewModel
class TripListVm #Inject constructor(
private val tripRepository: TripRepository
) : ViewModel() {
/**
* #var trips
* Get all trips
*/
val trips = tripRepository.getTrips()
val testStr: String = "Test String!"
}
Composable:
#Composable
fun TripList(
navController: NavHostController,
modifier: Modifier = Modifier,
tripListVm: TripListVm = hiltViewModel()
) {
Text(text = "Trip List")
TripListItem(
name = "Trip Name",
date = "16/04/1885",
amount = 46.66,
modifier = modifier
)
}
Result:
I am also getting an error: [Dagger/MissingBinding] exception.
ac.gre.mxpenseresit.data.repository.TripRepository cannot be provided without an #Inject constructor or an #Provides-annotated method
Here is the code for the data layer:
Database class:
#Database(
entities = [
Trip::class,
Expense::class,
],
version = 1
)
abstract class MExpenseDb : RoomDatabase() {
abstract val tripDao: TripDao
}
Dao:
#Dao
interface TripDao {
/**
* Gets all trips
*/
#Query("SELECT * FROM Trip")
fun getTrips(): Flow<List<Trip>>
/**
* Gets trips based on a named search,
* This functionality can be extended later
*/
#Query("SELECT * FROM Trip t WHERE t.name LIKE :name")
suspend fun getTripsBySearchName(name: String)
/**
* Gets trip by Id
*/
#Query("SELECT * FROM Trip WHERE Trip.id = :id")
suspend fun getTripById(id: Long)
/**
*
*/
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveTrip(trip: Trip): Long
#Delete
suspend fun deleteTrip(trip: Trip)
}
Trip Repository Contract (Interface):
interface TripRepositoryContract {
fun getTrips(): Flow<List<Trip>>
suspend fun getTripById(id: Long): Trip?
suspend fun getTripsBySearchName(keyword: String): List<Trip>
suspend fun saveTripLocal(trip: Trip)
suspend fun saveTripRemote(trip: Trip)
suspend fun deleteTrip(trip: Trip)
}
Trip Repository implementation:
class TripRepository (val tripDao: TripDao) : TripRepositoryContract {
override fun getTrips(): Flow<List<Trip>> {
return tripDao.getTrips();
}
override suspend fun getTripById(id: Long): Trip? {
TODO("Not yet implemented")
}
override suspend fun getTripsBySearchName(keyword: String): List<Trip> {
TODO("Not yet implemented")
}
override suspend fun saveTripLocal(trip: Trip) {
TODO("Not yet implemented")
}
override suspend fun saveTripRemote(trip: Trip) {
TODO("Not yet implemented")
}
override suspend fun deleteTrip(trip: Trip) {
TODO("Not yet implemented")
}
}
Now the hiltViewModel() method works correctly, but I am getting a MExpenseDb_impl class not found
I looked at this stack overflow question: Android room persistent: AppDatabase_Impl does not exist
And it says to use the kept dependency, I already have the same thing with annotationProcessing so I'm not sure if this is an issue
All the tutorials online are either too long & irrelevant or too vague and I am trying to gain a deeper understanding about how this works.
Any advice would be appreciated

To provide the TripRepository you need to create a class with Hilt's Bind functions. Example:
#Module
#InstallIn(SingletonComponent::class)
interface RepositoryModule {
#Binds
#Singleton
fun bindTripRepository(
repository: TripRepository // here the class
): TripRepositoryContract // here the interface that the class implements
}
It is also necessary to modify the TripRepository. You must add the #Inject constructor() (even if your class doesn't use any dependencies) so that Hilt can create the class.
In your case it will look like this:
class TripRepository #Inject constructor(
val tripDao: TripDao
) : TripRepositoryContract {
// ...
}
The last change is in your MainModule:
#Module
#InstallIn(SingletonComponent::class)
object MainModule {
// providing the db implementation normally
#Provides
#Singleton
fun provideDb(application: Application): MExpenseDb {
return Room.databaseBuilder(
application,
MExpenseDb::class.java,
"MExpenseDb"
).build()
}
// providing your dao interface to be injected into TripRepository
#Provides
#Singleton
fun provideTripDao(
mExpenseDb: MExpenseDb
): TripDao = mExpenseDb.tripDao
}
Note that I changed from ActivityComponent to SingletonComponent in the hilt's modules, in both cases we want them to be singleton throughout the project, not just a component created for activity (which can also be singleton).
See components life cycles here.
I also recommend upgrading the Hilt version in your project, because you are using a very old one. The newest version is 2.42.
I think this video can help you better understand some things about Hilt. And there is also this repository of a project that uses Hilt together with Room that can be useful for you to consult.
Important edit:
In the TripListVm you are using the private val tripRepository: TripRepository (your class that implements the TripRepositoryContract), it is not recommended to directly inject the implementation class, instead you should inject the interface (TripRepositoryContract) and let Hilt take care of providing the implementation of it. Because that's what we taught Hilt to do in the RepositoryModule.
So to make it ideal, the TripListVm would look like this:
#HiltViewModel
class TripListVm #Inject constructor(
private val tripRepositoryContract: TripRepositoryContract
) : ViewModel() {
// ...
}

Related

Android Kotlin Proguard Obfuscation of Kotlin Default Impl

I use Android + Kotlin + Proguard
Code
inferface Authentication {
val authenticated: Boolean
val lastAuthentication: Instant
fun authenticate(force: Boolean = false): Flow<Event>
suspend fun revokeAccess()
}
And an implementation of that interface
internal sealed class AuthenticationInternal(
private val trustProvider: TrustProvider) : Authentication {
override val authenticated: Boolean
get() = trustProvider.authenticatedBefore()
override val lastAuthentication: Instant
get() = trustProvider.getLastAuthenticatedTime()
override fun authenticate() = flow {
trustProvider.checkIntegrity()
emitAll(internalAuthenticate())
}.flowOn(Dispatchers.Default)
override suspend fun revokeAccess() {
trustProvider.revokeAccess()
}
}
-> During Kotlin Annotation Processing this interface gets (as expected) a default implementation:
Kotlin generated code:
Class: Authentication$DefaultImpls.dex
-> Now my issue:
Since this class is generated and not reachable from Kotlin code, how can I keep this class using progurard.
In my release build, this class is put out of the release dex and the application crashes with an RuntimeException.
Note:
Adding this to the proguard mappings file:
-keep class com.my.package.AuthenticationInternal$DefaultImpls { *; }
will not work, since it is not reachable
How can I keep kapt-generated classes with proguard?

Dagger 2 with ViewModel, Repository, Room and Coroutines

I' trying to utilize Dagger 2 in a ViewModel + Respository + Room + Retrofit + Coroutines project written in Kotlin.
Currently each ViewModel initializes required repositories and their dependences by itself like so
class HomeViewModel(
application: Application
) : AndroidViewModel(application) {
private val repository: UserRepository =
UserRepository(
Webservice.create(),
AppDatabase.getDatabase(application, viewModelScope).userDao()
)
I would like to get this simplified to this
class HomeViewModel #Inject constructor(
private val repository: UserRepository
): ViewModel()
What I have achieved so far
Created the dagger component and modules
#Singleton
#Component(modules = [
AppModule::class,
NetworkModule::class,
DataModule::class,
RepositoryModule::class
])
interface AppComponent {
val webservice: Webservice
val userRepository: UserRepository
}
#Module
class AppModule(private val app: Application) {
#Provides
#Singleton
fun provideApplication(): Application = app
}
#Module
class DataModule {
#Provides
#Singleton
fun provideApplicationDatabase(app: Application, scope: CoroutineScope) =
AppDatabase.getDatabase(app, scope)
#Provides
#Singleton
fun provideUserDao(db: AppDatabase) = db.userDao()
}
#Module
class NetworkModule {
#Provides
#Singleton
fun provideWebservice() = Webservice.create()
}
#Module
class RepositoryModule {
#Provides
#Singleton
fun provideUserRepository(webservice: Webservice, userDao: UserDao) =
UserRepository(webservice, userDao)
}
Got the AppComponent initilized in the application class
class App : Application() {
companion object {
lateinit var appComponent: AppComponent
}
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
}
private fun initDagger(app: App): AppComponent =
DaggerAppComponent.builder()
.appModule(AppModule(app))
.build()
}
And now I'm stuck.
The first question is: How do I make the ViewModel's inject constructor work?
Originally it was initialized from the HomeFragment like so
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)
How do I call the initializer now?
The second question is a bit harder.
The database constructor requies a Coroutine scope in order to prepopulate it in a background thread during creation. How do I pass in a scope now?
Here is the definition of the database and the callback
#Database(
entities = [User::class],
version = 1, exportSchema = false
)
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context, scope: CoroutineScope): AppDatabase {
val tempInstance =
INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"database"
)
.fallbackToDestructiveMigration()
.addCallback(AppDatabaseCallback(scope))
.build()
INSTANCE = instance
return instance
}
}
}
private class AppDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let { database ->
scope.launch(Dispatchers.IO) {
//inserts
}
}
}
}
}
The second question is a bit harder.
The database constructor requies a Coroutine scope in order to prepopulate it in a background thread during creation. How do I pass in a scope now?
It's actually easier, don't pass in a CoroutineScope, use the GlobalScope for this operation.
The first question is: How do I make the ViewModel's inject constructor work?
You need to obtain the Provider<HomeViewModel> from Dagger, then invoke it inside a ViewModelProvider.Factory to create the instance of HomeViewModel via the provider registered in the Dagger component.
Alternately, if the Activity has its own subcomponent, then you can use #BindsInstance to get the Activity instance into the graph, then move ViewModelProviders.of(activity).get(HomeViewModel::class.java, object: ViewModelProvider.Factory {
...
return homeViewModelProvider.get() as T
...
}) into a module of that subcomponent. Then, from that subcomponent, it would be possible to obtain an actual instance of the HomeViewModel, and still obtain proper scoping + onCleared() callback.
You don't need to pass a coroutine scope just run a coroutine in IO dispacher like:
#Database(
entities = [
Login::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun loginDao(): LoginDao
companion object {
#Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(app: Application): AppDatabase = INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(app).also { INSTANCE = it }
}
private fun buildDatabase(app: Application) =
Room.databaseBuilder(app,
AppDatabase::class.java,
"daily_accountant")
// prepopulate the database after onCreate was called
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// Do database operations through coroutine or any background thread
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught during database creation --> $exception")
}
CoroutineScope(Dispatchers.IO).launch(handler) {
// Pre-populate database operations
}
}
})
.build()
}
}
And remove coroutineScope from from function parameter.

lateinit property has not been initialized with dagger and coroutine

I'm trying MVVM, Dagger2, Retrofit and Coroutine. Now I have the problem that I can successfully inject a ProfileService into my activity, but not into my repository. I get a profileService lateinit property has not been initialized
//MainActivity
#Inject
lateinit var profileService: ProfileService //only for testing
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val getTheExpectedResult = profileService.hasValidName("asdffff") //true
profileViewModel.createProfile("ku") //throw the not initialized
}
The viewmodel calls the repository.
// profile repository
private fun getProfileRepository(userId: String = "", apiKey: String = ""): ProfileRepository {
return ProfileRepository(ApiFactory.getApi(userId, apiKey))
}
fun createProfile(name: String) {
scope.launch {
try {
val profile = getProfileRepository().createProfile(name)
profileLiveData.postValue(profile)
}
//...
In the repository I inject the profileService
class ProfileRepository(private val api: NIPApiInterface) {
#Inject
lateinit var profileService: ProfileService
suspend fun createProfile(name: String): ProfileResponse? {
if (!profileService.hasValidName(name)) { //throw the not initialized
//...
My unspectacular ProfileService
class ProfileService #Inject constructor() {
fun hasValidName(name: String): Boolean {
return name.length > 3
}
}
So that I don't post too much code, the following info. In my Application I seem to initialize everything correctly, because the Activity can access the ProfileService. Here are my Dagger configurations:
//AppComponent
#Singleton
#Component(
modules = [
AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class
]
)
interface AppComponent : AndroidInjector<NIPApplication> {
#Component.Factory
interface Factory {
fun create(#BindsInstance application: NIPApplication): AppComponent
}
}
The AppModule
#Module
class AppModule {
#Provides
#Singleton
fun provideContext(app: NIPApplication): Context = app
}
If you need more code, please send a comment.
Dagger won't inject dependencies unless it creates an instance of a class that needs injection or is explicitly asked to. Since the ProfileRepository object is created by you, it won't get its dependencies injected by Dagger.
The best way to solve this is to let Dagger create ProfileRepository objects by #Inject-annotated constructor (or using #Provides-annotated method in a Dagger module (provider), however this seems quite redundant in this particular case):
class ProfileRepository #Inject constructor(
private val api: NIPApiInterface,
private val profileService
)
Note that now Dagger will want to inject an NIPApiInterface object as well, so you have to create a provider for that or remove it from the constructor and pass it in some other way.

error: method execute in class CoroutinesRoom cannot be applied to given types

I am using coroutines with room database and while fetching data from room I am getting below error
\app\build\generated\source\kapt\debug\co\location\locationapp\data\source\local\LocationDataDao_Impl.java:87:
error: method execute in class CoroutinesRoom cannot be applied to
given types;
return CoroutinesRoom.execute(__db, true, new Callable() {
^ required: RoomDatabase,Callable,Continuation found:
RoomDatabase,boolean,>,Continuation
reason: cannot infer type-variable(s) R
(actual and formal argument lists differ in length) where R is a type-variable:
R extends Object declared in method execute(RoomDatabase,Callable,Continuation) where
CAP#1 is a fresh type-variable:
CAP#1 extends Object super: Unit from capture of ? super Unit
Below is my Dao class
#Dao
interface LocationDataDao {
#Query("SELECT * FROM location limit :limit offset :offset")
suspend fun queryLocationData(limit: Int, offset: Int): List<Location>
#Query("DELETE FROM location")
suspend fun deleteAllLocationData(): Int
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllLocationData(locationData: List<Location>)
}
Below is my repository class
class LocationRepository #Inject
constructor(private val apiInterface: ApiInterface, private val locationDataDao: LocationDataDao) {
suspend fun getLocationDataFromApi(limit: Int, offset: Int): List<Location> {
try {
return apiInterface.getLocationData(offset, limit).await()
} catch (ex: HttpException) {
throw ApiError(ex)
}
}
suspend fun getLocationDataFromDb(limit: Int, offset: Int): List<Location> {
return locationDataDao.queryLocationData(limit, offset)
}
suspend fun insertData(locationList: List<Location>) {
locationDataDao.insertAllLocationData(locationList)
}
suspend fun deleteDataFromDB() {
locationDataDao.deleteAllLocationData()
}
}
I am fetching data from a method like below
fun loadAfter() {
Job job = CoroutineScope(Dispatchers.IO).launch {
val data = locationRepository.getLocationDataFromDb(BuildConfig.PAGE_SIZE, params.key)
}
}
Please let me know if further info is required
Make sure that all room libraries have the same version in your app build.gradle:
...
dependencies {
...
def room_version = "2.2.0-alpha02"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
See the Declaring dependencies documentation section.
Make sure to upgrade to the latest version.
I upgraded from 2.2.6 to 2.3.0 and the problem has been solved.
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"

Kotlin Multiplatform: sharing actual class implementation for multiple targets (iOS, macOS)

I am working on a Kotlin/Native Multiplatform project that supports JVM, iOS, and macOS. My setup has the following modules:
- common
- ios
- jvm
- macos
I want to use some native code as an actual class and put an expected class in common. However, the actual class implementation is identical for multiple targets (iOS and macOS). Is there a way I can set up my sources (maybe in Gradle) so that I don't have to maintain 2 identical copies of the actual class?
Stately has a fairly involved config. iOS and Macos share all of the same code.
To structure the project, there's commonMain, nativeCommonMain depends on that, and actually appleMain which depends on nativeCommonMain.
commonMain {
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
}
}
jvmMain {
dependsOn commonMain
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
}
nativeCommonMain {
dependsOn commonMain
}
appleMain {
dependsOn nativeCommonMain
}
configure([iosX64Main, iosArm64Main, macosMain, iosArm32Main]) {
dependsOn appleMain
}
That structure is probably deeper than you need, but we needed something for linux and windows that was different. Egor's answer above is easier to follow I think.
We actually define multiplatform atomics in Stately, so you can use them as inspiration or actually just use the library itself.
https://github.com/touchlab/Stately
Common
expect class AtomicInt(initialValue: Int) {
fun get(): Int
fun set(newValue: Int)
fun incrementAndGet(): Int
fun decrementAndGet(): Int
fun addAndGet(delta: Int): Int
fun compareAndSet(expected: Int, new: Int): Boolean
}
JVM
actual typealias AtomicInt = AtomicInteger
Native
actual class AtomicInt actual constructor(initialValue:Int){
private val atom = AtomicInt(initialValue)
actual fun get(): Int = atom.value
actual fun set(newValue: Int) {
atom.value = newValue
}
actual fun incrementAndGet(): Int = atom.addAndGet(1)
actual fun decrementAndGet(): Int = atom.addAndGet(-1)
actual fun addAndGet(delta: Int): Int = atom.addAndGet(delta)
actual fun compareAndSet(expected: Int, new: Int): Boolean = atom.compareAndSet(expected, new)
}
In Okio, we declare two additional source sets, nativeMain and nativeTest, and configure the built in native source sets to depend on them:
apply plugin: 'org.jetbrains.kotlin.multiplatform'
kotlin {
iosX64()
iosArm64()
linuxX64()
macosX64()
mingwX64('winX64')
sourceSets {
nativeMain {
dependsOn commonMain
}
nativeTest {
dependsOn commonTest
}
configure([iosX64Main, iosArm64Main, linuxX64Main, macosX64Main, winX64Main]) {
dependsOn nativeMain
}
configure([iosX64Test, iosArm64Test, linuxX64Test, macosX64Test, winX64Test]) {
dependsOn nativeTest
}
}
}
If all three implementations are identical, just put that code in common. expect/actual is only used for things that are different on different platforms
If you use Kotlin DSL your build.gradle.kts file might look like this:
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "yourframeworkname"
}
}
sourceSets {
val commonMain by getting {
dependencies {
...
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val androidMain by getting
val androidTest by getting
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
}
}