Embed a custom plugin
Updated
Develop a custom plugin, such as a countdown plugin or a dice, and embed the plugin in the flexible classroom.
In online classroom applications, some customization is often necessary to meet the needs of a particular use case. Agora provides Widgets to help users develop plugins according to their specific needs and embed them into Flexible Classroom.
Android
Implement a custom plugin
This section uses the cloud disk plug-in as an example to introduce the basic steps of implementing a custom plug-in through Widget, and embedding the plug-in in Flexible Classroom. The complete code can be viewed in the CloudClass-Android repository.
-
Define the class
class FCRCloudDiskWidget : AgoraBaseWidget() { override val tag = "AgoraEduNetworkDiskWidget" private var cloudDiskContent: AgoraEduCloudDiskWidgetContent? = null // Initialize Widget // Add your custom UI layout to the container here override fun init(container: ViewGroup) { super.init(container) container.post { widgetInfo?.let { // Instantiate content and add CloudDisk view to container cloudDiskContent = AgoraEduCloudDiskWidgetContent(container, it) } } } // Release used resources override fun release() { cloudDiskContent?.dispose() super.release() } // The content class is only for encapsulation of the code. // Functional details are placed in the content. private inner class AgoraEduCloudDiskWidgetContent(val container: ViewGroup, val widgetInfo: AgoraWidgetInfo) { ...... } }The complete code can be viewed in the CloudClass-Android repository.
-
Configure the plugin in to register it with the Agora Classroom SDK
private fun configPublicCourseware(launchConfig: AgoraEduLaunchConfig) { val courseware0 = CoursewareUtil.transfer(DefaultPublicCoursewareJson.data0) val courseware1 = CoursewareUtil.transfer(DefaultPublicCoursewareJson.data1) val publicCoursewares = ArrayList<AgoraEduCourseware>(2) publicCoursewares.add(courseware0) publicCoursewares.add(courseware1) // custom data for map structure val cloudDiskExtra = mutableMapOf<String, Any>() cloudDiskExtra[publicResourceKey] = publicCoursewares cloudDiskExtra[configKey] = Pair(launchConfig.appId, launchConfig.userUuid) val widgetConfigs = mutableListOf<AgoraWidgetConfig>() widgetConfigs.add( // Instantiate widgetConfig and add it to widgetConfig collection AgoraWidgetConfig(widgetClass = FCRCloudDiskWidget::class.java, widgetId = AgoraWidgetDefaultId.AgoraCloudDisk.id, extraInfo = cloudDiskExtra) ) // Copy widgetConfigs into the SDK's startup parameters. // After calling launch, Smart Class will pass widgetConfigs to internal to complete the registration. launchConfig.widgetConfigs = widgetConfigs } -
Instantiate and initialize the widget
// Add an AgoraWidgetActiveObserver through widgetContext.addWidgetActiveObserver // to monitor the activation and deregistration of the Widget private val widgetActiveObserver = object : AgoraWidgetActiveObserver { override fun onWidgetActive(widgetId: String) { createWidget(widgetId) } override fun onWidgetInActive(widgetId: String) { destroyWidget(widgetId) } } private fun createWidget(widgetId: String) { if (teachAidWidgets.contains(widgetId)) { AgoraLog?.w("$tag->'$widgetId' is already created, can not repeat create!") return } AgoraLog?.w("$tag->create teachAid that of '$widgetId'") // Here, the previously registered widgetConfigs are searched by widgetId val widgetConfig = eduContext?.widgetContext()?.getWidgetConfig(widgetId) widgetConfig?.let { config -> // Take widgetClass from widgetConfig and instantiate a Widget object instance through reflection val widget = eduContext?.widgetContext()?.create(config) widget?.let { AgoraLog?.w("$tag->successfully created '$widgetId'") when (widgetId) { CountDown.id -> { (it as? AgoraCountDownWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId) } } Selector.id -> { (it as? AgoraIClickerWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId) } } Polling.id -> { (it as? AgoraVoteWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId) } } } // record widget teachAidWidgets[widgetId] = widget // Create widgetContainer and bind to root Container val widgetContainer = managerWidgetsContainer(allWidgetsContainer = binding.root, widgetId = widgetId) AgoraLog?.i("$tag->successfully created '$widgetId' container") widgetContainer?.let { group -> AgoraLog?.w("$tag->initialize '$widgetId'") // Initialize the Widget widget.init(group) } } } } private fun destroyWidget(widgetId: String) { // remove from map val widget = teachAidWidgets.remove(widgetId) // remove UIDataProviderListener when (widgetId) { CountDown.id -> { (widget as? AgoraTeachAidCountDownWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId) } } Selector.id -> { (widget as? AgoraTeachAidIClickerWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId) } } Polling.id -> { (widget as? AgoraTeachAidVoteWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId) } } AgoraCloudDisk.id -> { (widget as? AgoraTeachAidVoteWidget)?.getWidgetMsgObserver()?.let { observer -> eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId) } } } widget?.let { handler.post { it.release() } it.container?.let { group -> managerWidgetsContainer(binding.root, widgetId, group) } } }
API Reference
AgoraBaseWidget class
// Widget base class
abstract class AgoraBaseWidget {
// The parent layout of the current Widget
var container: ViewGroup? = null
// Information about the current widget
var widgetInfo: AgoraWidgetInfo? = null
// Initialize the current widget
// @param container The parent layout of the current Widget
open fun init(container: ViewGroup) {
this.container = container
}
// position and size ratio information of the current Widget to the remote
// @param frame The position and size ratio information of the current Widget
// @param contextCallback callback listener for synchronous operations
protected fun updateSyncFrame(frame: AgoraWidgetFrame, contextCallback: EduContextCallback<Unit>? = null) {
......
}
// @param properties Map of custom room properties in the Widget to be updated.
// Need to pass the full path: "a.b.c.d": value
// @param cause custom update reason
// @param contextCallback callback listener for update operations
protected fun updateRoomProperties(
properties: MutableMap<String, Any>,
cause: MutableMap<String, Any>,
contextCallback: EduContextCallback<Unit>? = null
) {
......
}
// @param keys The set of key values for custom user properties in the widget that needs to be deleted.
// Need to pass the full path: a.b.c.d
// @param cause custom delete reason
// @param contextCallback callback listener for delete operation
protected fun deleteRoomProperties(
keys: MutableList<String>,
cause: MutableMap<String, Any>,
contextCallback: EduContextCallback<Unit>? = null
) {
......
}
// @param properties Map of custom user properties within the Widget to be updated. Need to pass the full path: "a.b.c.d": value
// @param cause custom update reason
// @param contextCallback callback listener for update operations
protected fun updateUserProperties(
properties: MutableMap<String, Any>,
cause: MutableMap<String, Any>,
contextCallback: EduContextCallback<Unit>? = null
) {
......
}
// @param keys The set of key values for custom user properties in the widget that needs to be deleted. Need to pass the full path: a.b.c.d
// @param cause custom delete reason
// @param contextCallback callback listener for delete operation
protected fun deleteUserProperties(
keys: MutableList<String>,
cause: MutableMap<String, Any>,
contextCallback: EduContextCallback<Unit>? = null
) {
......
}
// Send messages from inside the Widget to the outside
protected fun sendMessage(message: String) {
......
}
// Widget's position and size changed
// @params frame Widget position and size scale information
open fun onSyncFrameUpdated(frame: AgoraWidgetFrame) {
}
// Receive messages from outside the widget. The message comes from sendMessageToWidget in widgetContext
open fun onMessageReceived(message: String) {
}
// Callback for the update of local user information
open fun onLocalUserInfoUpdated(info: AgoraWidgetUserInfo) {
widgetInfo?.let {
......
}
}
// Callback when room information is updated
open fun onRoomInfoUpdated(info: AgoraWidgetRoomInfo) {
widgetInfo?.let {
......
}
}
// Callback for when the custom room property in the widget is updated
open fun onWidgetRoomPropertiesUpdated(properties: MutableMap<String, Any>, cause: MutableMap<String, Any>?, keys: MutableList<String>) {
......
}
// Callback when custom room property in Widget is deleted
open fun onWidgetRoomPropertiesDeleted(properties: MutableMap<String, Any>, cause: MutableMap<String, Any>?, keys: MutableList<String>) {
......
}
// Callback for when custom local user properties in Widget are updated
open fun onWidgetUserPropertiesUpdated(properties: MutableMap<String, Any>,
cause: MutableMap<String, Any>?,
keys: MutableList<String>) {
......
}
// The callback for the deletion of the custom local user attribute in the Widget
open fun onWidgetUserPropertiesDeleted(properties: MutableMap<String, Any>,
cause: MutableMap<String, Any>?,
keys: MutableList<String>) {
......
}
// Release resources
open fun release() {
......
}
}AgoraWidgetContext interface
// WidgetContext capability interface
interface AgoraWidgetContext {
// Create a Widget object instance
// @param config The configuration information of this Widget object
// @return Widget instance, if it is empty, it means the creation failed
fun create(config: AgoraWidgetConfig): AgoraBaseWidget?
// Get the current position and size ratio information of the Widget
// @param widgetId Widget's unique identifier
// @return widget's current position and scale information. The benchmark of the ratio needs to be unified by the developers themselves.
fun getWidgetSyncFrame(widgetId: String): AgoraWidgetFrame?
// Get the configuration information of all registered widgets
fun getWidgetConfigs(): MutableList<AgoraWidgetConfig>
// Get the configuration information of a registered widget
// @param widgetId Widget's unique identifier
fun getWidgetConfig(widgetId: String): AgoraWidgetConfig?
// Activate a widget.
// After the operation is successful, you will receive a callback from AgoraWidgetActiveObserver.onWidgetActive
// @param widgetId Widget's unique identifier
// @param ownerUserUuid The userUuid of the user who owns the currently activated Widget
// @param roomProperties initialized room properties
// @param callback The callback listener for the activation operation
fun setWidgetActive(widgetId: String, ownerUserUuid: String? = null,
roomProperties: Map<String, Any>? = null,
callback: EduContextCallback<Unit>? = null)
// Log out the specified Widget
// @param widgetId Widget's unique identifier
// @param isRemove Whether to completely delete all the information of this Widget in the current classroom:
// - true: delete completely. All information under roomProperties.widgets.'widgetId' and userProperties.widgets.'widgetId' will be deleted
// - false: Only set roomProperties.widgets.'widgetId'.state to '0', that is, set the current Widget to inactive state.
// No matter what value is passed, you will receive the AgoraWidgetActiveObserver.onWidgetInActive callback.
// @param callback callback listener for logout operation
fun setWidgetInActive(widgetId: String, isRemove: Boolean = false, callback: EduContextCallback<Unit>? = null)
// Get the activation state of a widget
// @param widgetId Widget's unique identifier
fun getWidgetActive(widgetId: String): Boolean
// Get the activation status of all registered widgets
// @param widgetId Widget's unique identifier
fun getAllWidgetActive(): Map<String, Boolean>
// Add an AgoraWidgetActiveObserver listener to monitor whether the Widget is active
fun addWidgetActiveObserver(observer: AgoraWidgetActiveObserver, widgetId: String)
// Remove an AgoraWidgetActiveObserver listener
fun removeWidgetActiveObserver(observer: AgoraWidgetActiveObserver, widgetId: String)
// Add an AgoraWidgetMessageObserver listener to listen for all messages sent by this Widget
fun addWidgetMessageObserver(observer: AgoraWidgetMessageObserver, widgetId: String)
// Remove an AgoraWidgetMessageObserver listener
fun removeWidgetMessageObserver(observer: AgoraWidgetMessageObserver, widgetId: String)
// Send a message to a widget
fun sendMessageToWidget(msg: String, widgetId: String)
}Type definition
// Widget configuration class
data class AgoraWidgetConfig(
// The custom Widget class corresponding to this WidgetId
var widgetClass: Class<out AgoraBaseWidget>,
// Unique identifier for the current Widget
var widgetId: String,
// The icon in the unselected (default or inactive) state
val image: Int? = null,
// The icon in the selected or activated state
val selectedImage: Int? = null,
// Custom data, which is passed to AgoraWidgetInfo.extraInfo after Widget is instantiated
var extraInfo: Any? = null
)
// Widget information class
data class AgoraWidgetInfo(
var roomInfo: AgoraWidgetRoomInfo,
var localUserInfo: AgoraWidgetUserInfo,
// Unique identifier for the current Widget
val widgetId: String,
// Custom data passed in externally
val extraInfo: Any?,
// custom room properties in the current widget
var roomProperties: MutableMap<String, Any>?,
// Custom local user properties in the current Widget
var localUserProperties: MutableMap<String, Any>?
)
// Widget's position and size ratio information class
data class AgoraWidgetFrame(
val x: Float? = null,
val y: Float? = null,
val width: Float? = null,
val height: Float? = null
)
// User information class in Widget
data class AgoraWidgetUserInfo(
val userUuid: String,
val userName: String,
val userRole: Int
)
// Room information class in Widget
data class AgoraWidgetRoomInfo(
val roomUuid: String,
val roomName: String,
val roomType: Int
)