Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compose previews #834

Merged
merged 4 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions base/api/android/base.api
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
public final class com/splendo/kaluga/base/ApplicationHolder {
public static final field Companion Lcom/splendo/kaluga/base/ApplicationHolder$Companion;
public fun <init> ()V
}

public final class com/splendo/kaluga/base/ApplicationHolder$Companion {
public final fun getApplication ()Landroid/app/Application;
public static final field INSTANCE Lcom/splendo/kaluga/base/ApplicationHolder;
public final fun getApplicationContext ()Landroid/content/Context;
public final fun setApplication (Landroid/app/Application;)V
public final fun isInitialized ()Z
public final fun setApplicationContext (Landroid/content/Context;)V
}

public final class com/splendo/kaluga/base/KalugaThread {
Expand Down
63 changes: 29 additions & 34 deletions base/src/androidMain/kotlin/ApplicationHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,37 @@ package com.splendo.kaluga.base

import android.app.Application
import android.content.Context
import com.splendo.kaluga.base.ApplicationHolder.Companion.application

/**
* Class holding reference to the [Application] running Kaluga
* set [application] to your Application so default constructors work with the proper [Context]
* Class holding reference to the [Application] [Context] running Kaluga
* set [applicationContext] to your Application so default constructors work with the proper [Context]
*/
class ApplicationHolder {
vpodlesnyak marked this conversation as resolved.
Show resolved Hide resolved
companion object {

/**
* The [Application] running Kaluga
*/
var application: Application? = null
set(application) {
check(field == null) { "Application object can only be set once." }
field = application
object ApplicationHolder {

private var _applicationContext: Context? = null
vpodlesnyak marked this conversation as resolved.
Show resolved Hide resolved
set(value) {
check(field == null) { "Application object can only be set once." }
field = value
}

/**
* Indicates whether [Application] [Context] was set
*/
val isInitialized: Boolean get() = _applicationContext != null

/**
* The [Context] of the [Application] running Kaluga
*/
var applicationContext: Context
get() {
val context = _applicationContext
checkNotNull(context) {
"You've used ApplicationHolder.applicationContext without setting it on this holder " +
"(you should do this from Application.onCreate() or in your test)"
}

/**
* The [Context] of the [application]
*/
val applicationContext: Context
get() {
val application = this.application
checkNotNull(application) {
"You've used ApplicationHolder.applicationContext without setting the Application on this holder " +
"(you should do this from Application.onCreate() or in your test)"
}

val context = application.applicationContext
checkNotNull(context) {
"ApplicationContext is null, this should not happen during runtime if you set a real Application instance on this holder." +
"For testing make sure you set an Application with a real or mock ApplicationContext available."
}

return context
}
}
return context
}
set(value) {
_applicationContext = value.applicationContext
}
}
4 changes: 4 additions & 0 deletions resources-compose/api/resources-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public final class com/splendo/kaluga/resources/compose/KalugaLabelKt {
public static final fun PreviewKalugaLabel (Landroidx/compose/runtime/Composer;I)V
}

public final class com/splendo/kaluga/resources/compose/KalugaPreviewKt {
public static final fun KalugaPreview (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
}

public final class com/splendo/kaluga/resources/compose/TextAlignmentKt {
public static final fun getComposable (Lcom/splendo/kaluga/resources/stylable/KalugaTextAlignment;)I
}
Expand Down
58 changes: 58 additions & 0 deletions resources-compose/src/main/kotlin/KalugaPreview.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2024 Splendo Consulting B.V. The Netherlands

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.splendo.kaluga.resources.compose

import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.content.res.Resources
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.splendo.kaluga.base.ApplicationHolder
import com.splendo.kaluga.resources.FontLoader
import com.splendo.kaluga.resources.KalugaFont

/**
* Sets up an infrastructure for previews.
* @param resourcePackageOverride if provided, overrides a lookup package for resources
* @param content a preview content
*/
@Composable
fun KalugaPreview(resourcePackageOverride: String? = null, content: @Composable () -> Unit) {
ApplicationHolder.applicationContext = PreviewContextWrapper(
base = LocalContext.current.applicationContext,
packageNameOverride = resourcePackageOverride,
)

content()
}

private class PreviewContextWrapper private constructor(
base: Context,
private val packageNameOverride: String?,
private val resourcesOverride: Resources?,
) : ContextWrapper(base) {
constructor(base: Context, packageNameOverride: String?) : this(base, packageNameOverride, null)
override fun getPackageName(): String = packageNameOverride ?: super.getPackageName()
override fun createConfigurationContext(overrideConfiguration: Configuration): Context {
@Suppress("DEPRECATION")
return super.createConfigurationContext(overrideConfiguration)
?: PreviewContextWrapper(this, packageNameOverride, Resources(resources.assets, resources.displayMetrics, overrideConfiguration))
}
override fun getResources(): Resources = resourcesOverride ?: super.getResources()
}
6 changes: 3 additions & 3 deletions resources/api/resources.api
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ public final class com/splendo/kaluga/resources/DefaultColors {

public final class com/splendo/kaluga/resources/DefaultFontLoader : com/splendo/kaluga/resources/FontLoader {
public fun <init> ()V
public fun <init> (Landroid/content/Context;Landroid/os/Handler;)V
public fun loadFont (Ljava/lang/String;Landroid/graphics/Typeface;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun <init> (Landroid/content/Context;)V
public fun loadFont (Ljava/lang/String;Landroid/graphics/Typeface;)Landroid/graphics/Typeface;
}

public final class com/splendo/kaluga/resources/DefaultImageLoader : com/splendo/kaluga/resources/ImageLoader {
Expand All @@ -410,7 +410,7 @@ public final class com/splendo/kaluga/resources/DefaultStringLoader : com/splend
}

public abstract interface class com/splendo/kaluga/resources/FontLoader {
public abstract fun loadFont (Ljava/lang/String;Landroid/graphics/Typeface;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun loadFont (Ljava/lang/String;Landroid/graphics/Typeface;)Landroid/graphics/Typeface;
}

public final class com/splendo/kaluga/resources/FontStyle : java/lang/Enum {
Expand Down
116 changes: 40 additions & 76 deletions resources/src/androidMain/kotlin/Resources.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@file:JvmName("ResourcesAndroidKt")
/*
Copyright 2022 Splendo Consulting B.V. The Netherlands

Expand All @@ -17,67 +18,57 @@

package com.splendo.kaluga.resources

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Typeface
import android.os.Handler
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.splendo.kaluga.base.ApplicationHolder.Companion.application
import com.splendo.kaluga.base.ApplicationHolder.Companion.applicationContext
import kotlinx.coroutines.CompletableDeferred
import com.splendo.kaluga.base.ApplicationHolder

@Suppress("EnumEntryName")
private enum class DefType {
string, plurals, color, drawable, font
}

@SuppressLint("DiscouragedApi")
private fun <T> Context.getResource(name: String, defType: DefType, get: Context.(Int) -> T?): T? =
try {
resources.getIdentifier(name, defType.name, packageName)
.takeIf { it != 0 }
?.let { id -> get(id) }
} catch (e: Resources.NotFoundException) {
null
}

/**
* Default implementation of a [StringLoader].
* @param context the [Context] from which to load the string resources
*/
actual class DefaultStringLoader(private val context: Context?) : StringLoader {
actual constructor() : this(if (application != null) applicationContext else null)
actual override fun loadString(identifier: String, defaultValue: String): String {
if (context == null) {
return defaultValue
}
val id = context.resources.getIdentifier(identifier, "string", context.packageName)
return try {
context.getString(id)
} catch (e: Resources.NotFoundException) {
defaultValue
}
}
actual override fun loadQuantityString(identifier: String, quantity: Int, defaultValue: String): String {
if (context == null) {
return defaultValue
}
val id = context.resources.getIdentifier(identifier, "plurals", context.packageName)
return try {
context.resources.getQuantityString(id, quantity, quantity)
} catch (e: Resources.NotFoundException) {
defaultValue
}
}
actual constructor() : this(if (ApplicationHolder.isInitialized) ApplicationHolder.applicationContext else null)
vpodlesnyak marked this conversation as resolved.
Show resolved Hide resolved
actual override fun loadString(identifier: String, defaultValue: String): String =
context?.getResource(identifier, DefType.string, Context::getString)
?: defaultValue

actual override fun loadQuantityString(identifier: String, quantity: Int, defaultValue: String): String =
context?.getResource(identifier, DefType.plurals) { id -> resources.getQuantityString(id, quantity, quantity) }
?: defaultValue
}

/**
* Default implementation of a [KalugaColorLoader].
* @param context the [Context] from which to load the color resources
*/
actual class DefaultColorLoader(private val context: Context?) : KalugaColorLoader {
actual constructor() : this(if (application != null) applicationContext else null)
actual override fun loadColor(identifier: String, defaultValue: KalugaColor?): KalugaColor? {
if (context == null) {
return defaultValue
}
val id = context.resources.getIdentifier(identifier, "color", context.packageName)
return try {
actual constructor() : this(if (ApplicationHolder.isInitialized) ApplicationHolder.applicationContext else null)
actual override fun loadColor(identifier: String, defaultValue: KalugaColor?): KalugaColor? =
context?.getResource(identifier, DefType.color) { id ->
KalugaColor.DarkLightColor(
ContextCompat.getColor(context.themedContext(false), id),
ContextCompat.getColor(context.themedContext(true), id),
)
} catch (e: Resources.NotFoundException) {
defaultValue
}
}
} ?: defaultValue

private fun Context.themedContext(withNightMode: Boolean): Context {
val res: Resources = resources
Expand All @@ -93,47 +84,20 @@ actual class DefaultColorLoader(private val context: Context?) : KalugaColorLoad
* @param context the [Context] from which to load the image resources
*/
actual class DefaultImageLoader(private val context: Context?) : ImageLoader {
actual constructor() : this(if (application != null) applicationContext else null)
actual override fun loadImage(identifier: String, defaultValue: KalugaImage?): KalugaImage? {
if (context == null) {
return defaultValue
}
val id = context.resources.getIdentifier(identifier, "drawable", context.packageName)
return try {
ContextCompat.getDrawable(context, id)?.let { KalugaImage(it) }
} catch (e: Resources.NotFoundException) {
defaultValue
}
}
actual constructor() : this(if (ApplicationHolder.isInitialized) ApplicationHolder.applicationContext else null)
actual override fun loadImage(identifier: String, defaultValue: KalugaImage?): KalugaImage? =
context?.getResource(identifier, DefType.drawable) { id -> ContextCompat.getDrawable(this, id) }
?.let(::KalugaImage)
?: defaultValue
}

/**
* Default implementation of a [FontLoader].
* @param context the [Context] from which to load the font resources
* @param handler a [Handler] for the thread the completion of loading the font should called on. If `null`, the UI thread will be used.
*/
actual class DefaultFontLoader(private val context: Context?, private val handler: Handler?) : FontLoader {
actual constructor() : this(if (application != null) applicationContext else null, null)
actual override suspend fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont? {
if (context == null) {
return defaultValue
}
val id = context.resources.getIdentifier(identifier, "font", context.packageName)
return try {
val deferredFont = CompletableDeferred<Typeface?>()
val callback = object : ResourcesCompat.FontCallback() {
override fun onFontRetrievalFailed(reason: Int) {
deferredFont.complete(defaultValue)
}

override fun onFontRetrieved(typeface: Typeface) {
deferredFont.complete(typeface)
}
}
ResourcesCompat.getFont(context, id, callback, handler)
deferredFont.await()
} catch (e: Resources.NotFoundException) {
defaultValue
}
}
actual class DefaultFontLoader(private val context: Context?) : FontLoader {
actual constructor() : this(if (ApplicationHolder.isInitialized) ApplicationHolder.applicationContext else null)
actual override fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont? =
context?.getResource(identifier, DefType.font) { id -> ResourcesCompat.getFont(context, id) }
vpodlesnyak marked this conversation as resolved.
Show resolved Hide resolved
?: defaultValue
}
4 changes: 2 additions & 2 deletions resources/src/commonMain/kotlin/Resources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ interface FontLoader {
* @param defaultValue The [KalugaFont] to return if no match was found for the identifier.
* @return The associated [KalugaFont] resources or [defaultValue] if no such resource was found.
*/
suspend fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont?
fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont?
}

/**
* Default implementation of a [FontLoader].
*/
expect class DefaultFontLoader() : FontLoader {
override suspend fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont?
override fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont?
}

/**
Expand Down
2 changes: 1 addition & 1 deletion resources/src/iosMain/kotlin/Resources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@ actual class DefaultImageLoader(private val bundle: NSBundle, private val traitC
* Default implementation of a [FontLoader].
*/
actual class DefaultFontLoader actual constructor() : FontLoader {
actual override suspend fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont? = UIFont.fontWithName(identifier, UIFont.labelFontSize) ?: defaultValue
actual override fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont? = UIFont.fontWithName(identifier, UIFont.labelFontSize) ?: defaultValue
}
4 changes: 2 additions & 2 deletions test-utils-resources/api/test-utils-resources.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public final class com/splendo/kaluga/test/resources/MockFontLoader : com/splend
public fun <init> ()V
public fun <init> (Z)V
public synthetic fun <init> (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getLoadFontMock ()Lcom/splendo/kaluga/test/base/mock/SuspendMethodMock;
public fun loadFont (Ljava/lang/String;Landroid/graphics/Typeface;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getLoadFontMock ()Lcom/splendo/kaluga/test/base/mock/MethodMock;
public fun loadFont (Ljava/lang/String;Landroid/graphics/Typeface;)Landroid/graphics/Typeface;
}

public final class com/splendo/kaluga/test/resources/MockImageLoader : com/splendo/kaluga/resources/ImageLoader {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class MockFontLoader(private val returnMock: Boolean = false) : FontLoader {
}
}

override suspend fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont? = loadFontMock.call(identifier, defaultValue)
override fun loadFont(identifier: String, defaultValue: KalugaFont?): KalugaFont? = loadFontMock.call(identifier, defaultValue)
}

/**
Expand Down
Loading