is there a nice way to “enhance” expect classes fo...
# multiplatform
y
is there a nice way to “enhance” expect classes for platform groups without declaring actuals in a common sourceSet? (details in thread)
For instance, if I have class Foo
Copy code
expect class Foo {
    fun commonMethod()
}
impleemnted in jvm, android and ios. Android & JVM implementations cannot be implemented on the same sourceSet but I want them to have more common functions so that any downstream sourceSet that is shared in between use those APIs. e.g. jvm:
Copy code
actual class Foo {
    fun commonMethod() {}
    fun commonJvmArt() {}
}
android:
Copy code
actual class Foo {
    fun commonMethod() {}
    fun commonJvmArt() {}
    fun androidSpecific(context: Context) {}
}
Is there any way I can expand the scope of
Foo
in a common source set between jvm & android. (lets call it jvmAndroidMain) For instance, my tests are mostly in jvmAndroidTest sourceSet, and there I can write a test like:
Copy code
@Test fun testFoo() {
    val subject = Foo()
    subject.commonJvmArt()
}
And this compiles just fine but IDE really doesn’t like it :( Which breaks the user experience.
I know I can create a base class on
jvmAndroid
and make my actuals on jvm and androidx extend it, but then the intermediate super class leaks in the API surface
actually, compiler seems to not complain if I redeclare expect class with new signatures on
jvmAndroid
but i’m not super confident that this is WAI or a bug where it doesn’t check.
like I can do: commonMain:
Copy code
expect class Foo {
   fun commonMethod()
}
jvmAndroidCommon:
Copy code
expect class Foo {
   fun commonMethod()
   fun androidJvmMethod()
}
so this works in a sample project, doesn’t work in androidx. But i can make it work in AndroidX by naming the intermeiate source file the same as the common file. So this is super yolo and we cannot really depend on 😞 I also checked how okio handles
FileSystem.SYSTEM
but they basically redeclared a shadowed properly which I cannot here (these classes are designed to be overridden)
a
Would a
jvmAndroidMain
target with a extension function help ?
Copy code
// in jvmAndroidMain
fun Foo.commonJvmArt()
Copy code
androidMain {
dependsOn(jvmAndroidMain.get())
}
you should be able to use in
android
target too
y
so are you looking for something similar to this, so instead of nonDesktop there will be jvmAndAndroid?
y
I cannot do extension functions because these are actually abstract classes that needs to be implemented by the client. So if the client targets jvm and android; i want them to be able to implement that in their common jvmAndroid code (actually, this particular case is web vs non-web). assuming 95%+ of our usarbase will be non-web, we do want to provide them a better API (that is not possible on the web).
in more pracitcal terms, this is more like
Copy code
// common
expect abstract class Foo
// nonWeb
expect abstract class Foo {
   abstract fun doSomething()
}
// web
expect abstract class Foo {
   abstract *suspend* fun doSomething()
}
// android
expect abstract class Foo {
   fun doSomething() {} // due to backwards compat
   abstract fun doSomethingElse()// backwards compat
}
For anyone wanting to use with web from common, we’ll provide a suspending doSomething but everyone else, we prefer the non-suspending…
a
Got it , I faced something similar with
suspend
and
runBlocking
not available for
js
I wish there was a easier way to solve it too ! , but i ended up shadowing it I used 2 classes (one internal and one public) , but you can try
internal
directly too And only expose the
fun
to the needed platform with extension function
Copy code
// common
expect abstract class Foo {
   internal abstract suspend fun _doSomething()
   internal abstract fun _doSomethingElse()
}
-------------------------
// nonWeb
actual abstract class Foo {
   internal abstract suspend fun _doSomething() {// real logic}
   internal abstract fun _doSomethingElse() {// empty logic}
}

fun Foo.doSomething(){ runBlocking {_doSomething()} }

----------------------
// web
actual abstract class Foo {
   internal abstract suspend fun _doSomething() {// real logic}
   internal abstract fun _doSomethingElse() {// real logic}
}

fun Foo.doSomething(){ runBlocking {_doSomething()} }
fun Foo.doSomethingElse(){ doSomethingElse() } // expose doSomethingElse only for web
y
Hmm, because these are intended to be overridden, the extension function won't work. Though, I might try another thing where I can lower that function to common, override it as final in web to prevent non web clients from overriding it. The function would stay there, which is ugly but in this particular case, those methods are not intended to be called by the user, library calls them so the library can make the right call based on the platform That might be a feasible sacrifice. Thanks for the suggestion though. ,🙏
r
I don't know if there's an existing youtrack ticket for defining part of an actual class in an intermediate source set, but seems like it would be a reasonable feature request.
👍 1
e
YouTrack seems to be having issue now, so I'm posting here: This might not cover all cases, but you can use typealiases to accomplish this:
Copy code
// Utility.common.kt
expect class Utility {
    fun commonAction()
}

// Utility.jvmAndroid.kt (alternatively provide different implementations for jvm and Android)
class JvmAndroidUtility {
  fun commonAction() {}
 
  fun openFile(file: File) {}
}

expect typealias Utility = JvmAndroidUtility
I'm not sure if the typealias trick is sticking around long term though. Another way to do this would be with an interface and an expect/actual factory function (which I think is the currently preferred way while expect/actual classes aren't fully API stable).
y
Did you mean to mark that typealias Utility as
actual
? It is probably worth adding to the ticket but that still has the same problem as the one I mentioned in the ticket (getting a new public unwanted
JvmAndroidUtilty
public API). Interface with factory functions are also a nice solution but because these apis already existed on Android, we cannot remove them or change them into interface without breaking API,😫. Thanks for the suggestions though.
e
Yes it should have been marked
actual
. If the API already exists on Android then wouldn't that serve as the target of the typealias, and there would be no additional public API? Although I guess the expect class would have to have a different name, and then that becomes an additional public API? Another option (which I thought wouldn't work, but it seems like it does work in 2.1.0) is:
Copy code
actual class Utility {
  actual fun commonAction() {}

  fun open(file: File) {}
}
I like your request too (although I'd prefer
override expect class
over an annotation).
y
yea the “other class” becomes a public API, so we end up giving a “worse” API to our clients that we care about the most 😢