Search code examples
androidkotlingenericsglance-appwidget

How to implement base class that takes object and class as parameter for Glance Widgets?


I am trying to create a base class GenericAction to handle Glance widget updates. It needs to take two parameters: an Object GlanceStateDefinition and Class GlanceAppWidget() as each widget will have a different state definition and broadcast receiver to be updated. This is called in updateAppWidgetState and updateStart respectively.

I have followed the guidance provided here and got to the below code.

There are two problems I am facing:

  1. Compilation error in lambda of updateAppWidgetState() I get error - Unresolved reference: copy. I believe this is because the compiler doesn't know how to infer the generic type T.
  2. Runtime error on instantiation of glanceWidget. Error in Glance App Widget java.lang.NoSuchMethodException: com.example.myproject.WidgetSymbolClass.<init> [class android.content.Context]. I believe android can't find the constructor of my GlanceAppWidget WidgetSymbolClass() for some reason.

I would appreciate if anyone has any idea of how I can get rid of these errors and fix the below code in GenericAction().

abstract class GenericAction<T>(val myObject: GlanceStateDefinition<T>, val myClass: Class<out WidgetSymbolClass>) : ActionCallback {

    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        val index = parameters.get(ActionParameters.Key<Int>("IndexParam")) ?: 0
        val portfolioId = parameters.get(ActionParameters.Key<String>("PortfolioId")) ?: "Portfolio 1"
        val showPortfolioMenu = parameters.get(ActionParameters.Key<Boolean>("showMenu")) ?: false

        updateAppWidgetState(context, myObject, glanceId) { state ->
            state.copy(
                /*error - Unresolved reference: copy*/
                /*error - Type mismatch. Required: T Found: Unit*/
                showPortfolioMenu = showPortfolioMenu,
            )
        }

        val glanceWidget = myClass.getConstructor(Context::class.java).newInstance(context) /*error java.lang.NoSuchMethodException WidgetSymbolClass.<init>*/
        glanceWidget.updateStart( glanceId = glanceId, portfolioId = portfolioId, index = index)
    }
}
class WidgetSymbolShowMenu: GenericAction<WidgetSymbolState>(WidgetSymbolStateDefinition, WidgetSymbolClass::class.java)
class WidgetSymbolClass @JvmOverloads constructor() : WidgetAbstractClass() 
abstract class WidgetAbstractClass (): GlanceAppWidget() {

    fun updateStart(
        glanceId: GlanceId?,
        appWidgetId: Int? = null,
        index: Int = 0,
        portfolioId: String = "Portfolio 1"
    ) {
       /*start updating widget*/
    }
}
/*myObect1 - T is WidgetSymbolState*/
object StateDefinitionSymbol: GlanceStateDefinition<WidgetSymbolState> {
    override suspend fun getDataStore(
        context: Context,
        fileKey: String,
    ): DataStore<WidgetSymbolState> = context.widgetSymbolFile

    override fun getLocation(context: Context, fileKey: String): File {
        return context.dataStoreFile(widgetSymbolJson)
    }
}
/*myObect2 - T is WidgetTableState*/
object StateDefinitionTable: GlanceStateDefinition<WidgetTableState> {
    override suspend fun getDataStore(
        context: Context,
        fileKey: String,
    ): DataStore<WidgetTableState> = context.widgetTableFile

    override fun getLocation(context: Context, fileKey: String): File {
        return context.dataStoreFile(widgetTableJson)
    }
}
@Serializable
data class WidgetSymbolState(
    val index: Int = 0,
    val portfolio: String = "Portfolio 1",
    val portfolioSize: Int = 0,
    val showPortfolioMenu: Boolean = false,
)
@Serializable
data class WidgetTableState(
    val portfolioId: String = "",
    val showPortfolioMenu: Boolean = false,
)

Solution

  • For the compiler error, you need to assure the compiler that you can call copy on T. However, WidgetSymbolState.copy has a different signature from WidgetTableState.copy, despite both having a showPortfolioMenu parameter - they have different parameters because they are generated from different data classes. This means you cannot create a common interface between them and put copy there.

    I would suggest that you add a updateShowPortfolioMenu method in both classes, and use that as the common interface.

    interface ShowPortfolioMenuState<T: ShowPortfolioMenuState<T>> {
        fun updateShowPortfolioMenu(newValue: Boolean): T
    }
    
    @Serializable
    data class WidgetSymbolState(
        val index: Int = 0,
        val portfolio: String = "Portfolio 1",
        val portfolioSize: Int = 0,
        val showPortfolioMenu: Boolean = false,
    ): ShowPortfolioMenuState<WidgetSymbolState> {
        override fun updateShowPortfolioMenu(newValue: Boolean) =
            copy(showPortfolioMenu = newValue)
    }
    
    @Serializable
    data class WidgetTableState(
        val portfolioId: String = "",
        val showPortfolioMenu: Boolean = false,
    ): ShowPortfolioMenuState<WidgetTableState> {
        override fun updateShowPortfolioMenu(newValue: Boolean) =
            copy(showPortfolioMenu = newValue)
    }
    

    Then you can constraint T in GenericAction to ShowPortfolioMenuState:

    abstract class GenericAction<T: ShowPortfolioMenuState<T>>(...)
    

    And then call state.updateShowPortfolioMenu(showPortfolioMenu), instead of state.copy.

    As for the runtime error, WidgetSymbolClass does not have a constructor that takes a Context. I'm not sure why you are passing a context to it. Either don't pass a context to it:

    myClass.getConstructor().newInstance()
    

    Or add a Context parameter, if other classes that would passed to myClass does take a Context.

    Also, you don't need the @JvmOverloads. There are no optional parameters anywhere.