1

I have 4 classes that all implement a very similar block of code. The only difference is that each one needs to instantiate a separate Object and Myclass() at runtime. The below Action classes are a simplified version (real code is much longer).

Normally I would pass in arguments to create the object and class based on what the caller provides. However, due to the callback extension (which is androids ActionCallback to be specific), I am unable to pass in any parameters to the Action class.

What would be the best way of implementing a base class (like GenericAction() below) which adapts for the different object/class each time. This would be a single source of truth and prevent the copy and paste of Action classes ABCD four times.

class ActionA : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        doSomething(context, ObjectA, glanceId)
        val constructSomething = MyClassA(context)
    }
}

class ActionB : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        doSomething(context, ObjectB, glanceId)
        val constructSomething = MyClassB(context)
    }
}

class ActionC : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        doSomething(context, ObjectC, glanceId)
        val constructSomething = MyClassC(context)
    }
}

class ActionD : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        doSomething(context, ObjectD, glanceId)
        val constructSomething = MyClassD(context)
    }
}
class GenericAction<T>(myObject: Any, myClazz: Class<T>) : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        doSomething(context, myObject, glanceId)
        val constructSomething = myClazz<T>(context)
    }
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
mars8
  • 770
  • 1
  • 11
  • 25
  • How about using the `ActionParameters` parameter? That thing seems like it's made for exactly this purpose. Unless you are doing something different to each of the `constructSomething` in each `ActionX`, that would require the specific type of `ClassA`, `ClassB`, `ClassC` etc – Sweeper Nov 12 '22 at 13:35
  • @Sweeper I think that would be more of a hack. Each 'Action' has it's own corresponding 'broadcastReceiver'. The action parameters should ideally be self-contained for each receiver, rather than being used to architect across receivers. – mars8 Nov 12 '22 at 13:49
  • I didn't quite understand the exact reasoning, but the gist is that you still want to keep the four action classes? I think the alternative presented in my answer should work for you. – Sweeper Nov 12 '22 at 14:12

1 Answers1

1

You can pass in parameters to ActionCallback.onAction using ActionParameters.

First, declare your parameter keys:

val myObjectKey = ActionParameters.Key<Any>("my-object-key")
val myClassKey = ActionParameters.Key<Class<*>>("my-class")

Then you can pass the parameters when you run your action:

// e.g. for ActionA
actionRunCallback<GenericAction>(
    parameters = actionParametersOf(
        myObjectKey to Object1
        myClassKey to ClassA::class.java
    )
)

where GenericAction is:

// the type parameter here is unnecessary - Android creates this class for you at runtime, 
// so there is nothing to know about T at compile time
class GenericAction : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        val myObject = parameters[myObjectKey]
        val myClass = parameters[myClassKey]
        doSomething(context, myObject, glanceId)
        val constructSomething = myClass.getConstructor(Context::class.java).newInstance(context)
    }
}

Alternatively, your four specific Action classes can inherit GenericAction, and GenericAction can be made abstract.

abstract class GenericAction(myObject: Any, myClass: Class<*>) : ActionCallback {
    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        doSomething(context, myObject, glanceId)
        val constructSomething = myClass.getConstructor(Context::class.java).newInstance(context)
    }
}

class ActionA: GenericAction(Object1, ClassA::class)
class ActionB: GenericAction(Object2, ClassB::class)
class ActionC: GenericAction(Object3, ClassC::class)
class ActionD: GenericAction(Object4, ClassD::class)

You just need to give one of the ActionX subclasses to actionRunCallback, rather than GenericAction, e.g.

actionRunCallback<ActionA>()

This will work, as ActionA does have a parameterless constructor.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I am getting `Unresolved reference: myClass`. If I add `val myClass` to constructor then I get `Unresolved reference: primaryConstructor`? – mars8 Nov 12 '22 at 15:11
  • 1
    @mairs8 You don't understand what I meant by "You can use `Class<*>` instead, find the specific constructor, and call it"? Sure, I've edited to show you how. You shouldn't be getting a `Unresolved reference` though. `myClass` is clearly defined in my code. Make sure you read my answer carefully - I have changed `myClazz` to `myClass`. I don't see why you should spell it like that. – Sweeper Nov 12 '22 at 15:19
  • I have updated my question with what I have after implementing your alternative answer. I am getting the following error at runtime: `Error in Glance App Widget: java.lang.NoSuchMethodException:com.example.myproject.ClassA. [class android.content.Context].` – mars8 Nov 12 '22 at 19:02
  • 1
    @mairs8 You seem to have missed a few important details of your actual code in your original formulation of the question (and your update too). First, judging from the fact that it cannot find a constructor, I'm guessing the constructor of `WidgetAbstractClass` has some optional parameters. To make it easier to find, you can mark the constructor as `@JvmOverloads`. Second, why are you passing `WidgetAbstractClass::class.java` in the inheritance clause of `WidgetSymbolShowMenu`? Presumably, `WidgetAbstractClass` is abstract, so it cannot be instantiated. Please use a concrete class instead.... – Sweeper Nov 12 '22 at 19:38
  • @mairs8 ...If the concrete class you're using also has optional parameters in its constructor, please also mark it with `@JvmOverloads`. Third, since you need to use methods specific to `WidgetAbstractClass` in `onAction`, but client code can pass in any subclass of `WidgetAbstractClass`, you should use `Class`, not `Class`. – Sweeper Nov 12 '22 at 19:42
  • @mairs8 If you don't know how to annotate a primary constructor, please see [this](https://stackoverflow.com/q/28398572/5133585). I have rolled back your edit, since the errors you are getting have nothing to do with what you originally asked. You can post a new question if you are still having trouble, but I would suggest that you spend some time debugging the problem on your own first. – Sweeper Nov 12 '22 at 19:49
  • correct `WidgetAbstractClass` is abstract and `WidgetSymbolClass` inherits from it. I have replaced `WidgetSymbolShowMenu` to take `WidgetSymbolClass` instead. I also added `@JvmOverloads` to the constructor of `WidgetSymbolClass` but still getting the same `NoSuchMethodException` exception. I have created a new [question](https://stackoverflow.com/q/74416796/15597975). – mars8 Nov 12 '22 at 21:42