Hi, I have this code in CommonMain: ```//CommonMai...
# multiplatform
j
Hi, I have this code in CommonMain:
Copy code
//CommonMain
LaunchedEffect(true) {
        viewModel.effect.collect { effect ->
            when (effect) {
                is ConnectingContract.Effect.Error -> {
                    snackbarHostState.showSnackbar(
                        message = "Error occured",
                    )
                }
            }
        }
    }
I want to change hardcoded text to resource using
moko
library. Unfortunately, I cannot use the
fun stringResources()
in
LaunchedEffect
, nor do I have access to
Context
. How to achieve this? I have prepared an
expect class Strings
which in the Android implementation takes
context
.
Copy code
expect class Strings {
    fun stringResource(string: StringResource) : String
}
actual class Strings(private val activity: Context) {
    actual fun stringResource(string: StringResource): String {
        return string.getString(activity)
    }
}
But I have to pass it to every screen...
Copy code
// Android Main
val context = LocalContext.current
val strings = getKoin().get<Strings> {
    parametersOf(context)
}
ConnectingScreen(
    viewModel = koinViewModel(),
    strings
)
j
I'm doing it like this in commonMain without any expect actual function
MR.strings.hellow.localized()
j
When I try to use localized() I get an error while building
Unresolved reference: localized
Under
localized()
is this code:
Copy code
package dev.icerock.moko.resources.desc
actual data class ResourceStringDesc actual constructor(
    val stringRes: StringResource
) : StringDesc, Parcelable {

    override fun localized(): String = stringRes.localized(
        locale = StringDesc.localeType.currentLocale
    )
}
j
because
localized()
is composable function
j
So you cannot called it from
LaunchedEffect
j
nope
v
Then call it a line before the LaunchedEffect
And in general if you want some OOP to be used in the Compose tree you don't need to pass it everywhere, there are compose way to do it - CompositionLocal.
j
I cannot call
stringResurce(string: StringDesc)
a line before the
LaunchedEffect
bacause this
StringDesc
i'm getting from
LaunchedEffect
Yes, I've heard of
CompositionLocal
but I don't know how to use it yet 🙂
v
Copy code
val LocalAppResourcesProvider = staticCompositionLocalOf<ResourcesProvider?> {
    null
}
You just declare anything as CompositionLocal, here ResourceProvider my class. Somewhere on top of the tree you initialize the actual value (if it can't be initialized at the declaration time).
Copy code
val resourcesProvider: ResourcesProvider = provideResourceProvider()

CompositionLocalProvider(LocalAppResourcesProvider provides resourcesProvider) {
  MyApp()
}
And anywhere down the tree you can access it as simple as:
Copy code
val resourrsesProvided LocalAppResourcesProvider.current ?: error("App resources provider is not provided for local")
Adding a little bit sugar we can create shortcut to the
LocalAppResourcesProvider.current
via defining object:
Copy code
object AppRes {

    val provider: ResourcesProvider
        @Composable
        get() = LocalAppResourcesProvider.current ?: error("App resources provider is not provided for local")
}
And now I can access it anywhere in the app as simple as
AppRes.provider.getString()
Even further, we can go more with the Compose way and create our own Composable function to fetch the resource:
Copy code
@Composable
fun stringResource(stringToken: StringTokens): String {
    return AppRes.provider.getString(stringToken)
}
And now we have setup to fetch the strings which is fully incapsulates logic how exactly the string is fetched
As for you exact question, I am not sure why you can prefetch resource before LaunchedEffect. What is your StringDesc? Are variations of it know at compile time?
j
Copy code
// commonMain - no context
LaunchedEffect(true) {
    viewModel.effect.collect { effect ->
        when (effect) {
            ConnectingContract.Effect.AutoNavigateForward -> navigateToApp()
            is ConnectingContract.Effect.ErrorDesc -> {
                val msg: StringDesc = effect.msg
                val msgString: String = strings.stringResource(msg)
                snackbarHostState.showSnackbar(msgString)
            }
        }
    }
}
For this reason,
StringDesc
is passed as the
Effect
argument of
viewModel
.
StringDesc
is a cross-platform variant of
StringResource
. To make a
String
from
StringDesc
, in Android I have to use the current activity
context
(to change the language) With CompositionLocal I created:
Copy code
val LocalStrings = staticCompositionLocalOf<Strings?> { null }
I initialize this in Activity
Copy code
val context = LocalContext.current
CompositionLocalProvider(LocalStrings provides Strings(context)) {
    App()
}
Then I can use it like
Copy code
val strings = LocalStrings.current ?: throw Exception()
Nice.. I don't like having to throw an exception when null, but maybe I'll come up with something Thanks!
v
Throwing exception is absolutely normal. It is complete developer error to not provide it.
Why you collecting the effect inside LaunchedEffect?
That is your problem not? Collect it before. Extract and get StringResoruce. Launch effect. Pass the extracted resource to snackbar
j
I'm collecting Channel<Effect> wchich has to be collected in coroutine scope.. so I can't get stringresource out of coroutine scope. Collecting effects inside LaunchedEffect is a normal thing in MVI
a
to read string resource on android you should use Context, on ios nothing required. So, to use in commonMain with Compose string resource inside Effect you can: 1. declare expect
Copy code
expect class ResourceReader {
    fun get(resource: StringResource): String
}

@Composable
expect fun getResourceReader(): ResourceReader
2. implement actual for android:
Copy code
actual class ResourceReader(private val context: Context) {
    actual fun get(resource: StringResource): String {
        return resource.getString(context)
    }
}

@Composable
actual fun getResourceReader(): ResourceReader {
    val context: Context = LocalContext.current
    return remember(context) { ResourceReader(context) }
}
3. implement actual for iOS:
Copy code
actual class ResourceReader {
    actual fun get(resource: StringResource): String {
        return resource.localize()
    }
}

@Composable
actual fun getResourceReader(): ResourceReader {
    return remember { ResourceReader() }
}
Then you can do in commonMain:
Copy code
val resourceReader = getResourceReader()

LaunchedEffect(true) {
        viewModel.effect.collect { effect ->
            when (effect) {
                is ConnectingContract.Effect.Error -> {
                    snackbarHostState.showSnackbar(
                        message = resourceReader.get(effect.resource),
                    )
                }
            }
        }
    }
❤️ 1