Embed a custom plugin
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 plug-ins according to their specific needs and embed them into Flexible Classroom.
Widgets are stand-alone plugins that contain interface and functionality. Developers can implement a widget based on customization of the base class, and then register the widget in the Agora Classroom SDK. Agora Classroom SDK supports registering multiple widgets. Widgets can communicate with other widgets, as well as with other plugins in the UI layer.
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 _29class FCRCloudDiskWidget : AgoraBaseWidget() {_29override val tag = "AgoraEduNetworkDiskWidget"_29_29private var cloudDiskContent: AgoraEduCloudDiskWidgetContent? = null_29_29// Initialize Widget_29// Add your custom UI layout to the container here_29override fun init(container: ViewGroup) {_29super.init(container)_29container.post {_29widgetInfo?.let {_29// Instantiate content and add CloudDisk view to container_29cloudDiskContent = AgoraEduCloudDiskWidgetContent(container, it)_29}_29}_29}_29_29// Release used resources_29override fun release() {_29cloudDiskContent?.dispose()_29super.release()_29}_29_29// The content class is only for encapsulation of the code._29// Functional details are placed in the content._29private inner class AgoraEduCloudDiskWidgetContent(val container: ViewGroup, val widgetInfo: AgoraWidgetInfo) {_29......_29}_29}The complete code can be viewed in the CloudClass-Android repository. 
- 
Configure the plugin in to register it with the Agora Classroom SDK _21private fun configPublicCourseware(launchConfig: AgoraEduLaunchConfig) {_21val courseware0 = CoursewareUtil.transfer(DefaultPublicCoursewareJson.data0)_21val courseware1 = CoursewareUtil.transfer(DefaultPublicCoursewareJson.data1)_21val publicCoursewares = ArrayList<AgoraEduCourseware>(2)_21publicCoursewares.add(courseware0)_21publicCoursewares.add(courseware1)_21_21// custom data for map structure_21val cloudDiskExtra = mutableMapOf<String, Any>()_21cloudDiskExtra[publicResourceKey] = publicCoursewares_21cloudDiskExtra[configKey] = Pair(launchConfig.appId, launchConfig.userUuid)_21val widgetConfigs = mutableListOf<AgoraWidgetConfig>()_21widgetConfigs.add(_21// Instantiate widgetConfig and add it to widgetConfig collection_21AgoraWidgetConfig(widgetClass = FCRCloudDiskWidget::class.java, widgetId = AgoraWidgetDefaultId.AgoraCloudDisk.id,_21extraInfo = cloudDiskExtra)_21)_21// Copy widgetConfigs into the SDK's startup parameters._21// After calling launch, Smart Class will pass widgetConfigs to internal to complete the registration._21launchConfig.widgetConfigs = widgetConfigs_21}
- 
Instantiate and initialize the widget _90// Add an AgoraWidgetActiveObserver through widgetContext.addWidgetActiveObserver_90// to monitor the activation and deregistration of the Widget_90private val widgetActiveObserver = object : AgoraWidgetActiveObserver {_90override fun onWidgetActive(widgetId: String) {_90createWidget(widgetId)_90}_90_90override fun onWidgetInActive(widgetId: String) {_90destroyWidget(widgetId)_90}_90}_90_90private fun createWidget(widgetId: String) {_90if (teachAidWidgets.contains(widgetId)) {_90AgoraLog?.w("$tag->'$widgetId' is already created, can not repeat create!")_90return_90}_90AgoraLog?.w("$tag->create teachAid that of '$widgetId'")_90// Here, the previously registered widgetConfigs are searched by widgetId_90val widgetConfig = eduContext?.widgetContext()?.getWidgetConfig(widgetId)_90widgetConfig?.let { config ->_90_90// Take widgetClass from widgetConfig and instantiate a Widget object instance through reflection_90val widget = eduContext?.widgetContext()?.create(config)_90widget?.let {_90AgoraLog?.w("$tag->successfully created '$widgetId'")_90when (widgetId) {_90CountDown.id -> {_90(it as? AgoraCountDownWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId)_90}_90}_90Selector.id -> {_90(it as? AgoraIClickerWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId)_90}_90}_90Polling.id -> {_90(it as? AgoraVoteWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId)_90}_90}_90}_90// record widget_90teachAidWidgets[widgetId] = widget_90// Create widgetContainer and bind to root Container_90val widgetContainer = managerWidgetsContainer(allWidgetsContainer = binding.root, widgetId = widgetId)_90AgoraLog?.i("$tag->successfully created '$widgetId' container")_90widgetContainer?.let { group ->_90AgoraLog?.w("$tag->initialize '$widgetId'")_90// Initialize the Widget_90widget.init(group)_90}_90}_90}_90}_90_90private fun destroyWidget(widgetId: String) {_90// remove from map_90val widget = teachAidWidgets.remove(widgetId)_90// remove UIDataProviderListener_90when (widgetId) {_90CountDown.id -> {_90(widget as? AgoraTeachAidCountDownWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)_90}_90}_90Selector.id -> {_90(widget as? AgoraTeachAidIClickerWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)_90}_90}_90Polling.id -> {_90(widget as? AgoraTeachAidVoteWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)_90}_90}_90AgoraCloudDisk.id -> {_90(widget as? AgoraTeachAidVoteWidget)?.getWidgetMsgObserver()?.let { observer ->_90eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)_90}_90}_90}_90widget?.let {_90handler.post { it.release() }_90it.container?.let { group ->_90managerWidgetsContainer(binding.root, widgetId, group)_90}_90}_90}
API Reference
AgoraBaseWidget class
_125// Widget base class_125abstract class AgoraBaseWidget {_125    // The parent layout of the current Widget_125    var container: ViewGroup? = null_125_125    // Information about the current widget_125    var widgetInfo: AgoraWidgetInfo? = null_125_125    // Initialize the current widget_125    // @param container The parent layout of the current Widget_125    open fun init(container: ViewGroup) {_125        this.container = container_125    }_125_125    // position and size ratio information of the current Widget to the remote_125    // @param frame The position and size ratio information of the current Widget_125    // @param contextCallback callback listener for synchronous operations_125    protected fun updateSyncFrame(frame: AgoraWidgetFrame, contextCallback: EduContextCallback<Unit>? = null) {_125        ......_125    }_125_125    // @param properties Map of custom room properties in the Widget to be updated. _125    // Need to pass the full path: "a.b.c.d": value_125    // @param cause custom update reason_125    // @param contextCallback callback listener for update operations_125    _125    protected fun updateRoomProperties(_125            properties: MutableMap<String, Any>,_125            cause: MutableMap<String, Any>,_125            contextCallback: EduContextCallback<Unit>? = null_125    ) {_125        ......_125    }_125_125    // @param keys The set of key values for custom user properties in the widget that needs to be deleted. _125    // Need to pass the full path: a.b.c.d_125    // @param cause custom delete reason_125    // @param contextCallback callback listener for delete operation_125    protected fun deleteRoomProperties(_125            keys: MutableList<String>,_125            cause: MutableMap<String, Any>,_125            contextCallback: EduContextCallback<Unit>? = null_125    ) {_125        ......_125    }_125_125    // @param properties Map of custom user properties within the Widget to be updated. Need to pass the full path: "a.b.c.d": value_125    // @param cause custom update reason_125    // @param contextCallback callback listener for update operations_125    protected fun updateUserProperties(_125        properties: MutableMap<String, Any>,_125        cause: MutableMap<String, Any>,_125        contextCallback: EduContextCallback<Unit>? = null_125    ) {_125        ......_125    }_125_125    // @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_125    // @param cause custom delete reason_125    // @param contextCallback callback listener for delete operation_125    protected fun deleteUserProperties(_125        keys: MutableList<String>,_125        cause: MutableMap<String, Any>,_125        contextCallback: EduContextCallback<Unit>? = null_125    ) {_125        ......_125    }_125_125    // Send messages from inside the Widget to the outside_125    protected fun sendMessage(message: String) {_125        ......_125    }_125_125    // Widget's position and size changed_125    // @params frame Widget position and size scale information_125    open fun onSyncFrameUpdated(frame: AgoraWidgetFrame) {_125    }_125_125    // Receive messages from outside the widget. The message comes from sendMessageToWidget in widgetContext_125    open fun onMessageReceived(message: String) {_125    }_125_125    // Callback for the update of local user information_125    open fun onLocalUserInfoUpdated(info: AgoraWidgetUserInfo) {_125        widgetInfo?.let {_125            ......_125        }_125    }_125_125    // Callback when room information is updated_125    open fun onRoomInfoUpdated(info: AgoraWidgetRoomInfo) {_125        widgetInfo?.let {_125            ......_125        }_125    }_125_125    // Callback for when the custom room property in the widget is updated_125    open fun onWidgetRoomPropertiesUpdated(properties: MutableMap<String, Any>, cause: MutableMap<String, Any>?, keys: MutableList<String>) {_125        ......_125    }_125_125    // Callback when custom room property in Widget is deleted_125    open fun onWidgetRoomPropertiesDeleted(properties: MutableMap<String, Any>, cause: MutableMap<String, Any>?, keys: MutableList<String>) {_125        ......_125    }_125_125    // Callback for when custom local user properties in Widget are updated_125    open fun onWidgetUserPropertiesUpdated(properties: MutableMap<String, Any>,_125        cause: MutableMap<String, Any>?,_125        keys: MutableList<String>) {_125        ......_125    }_125_125    // The callback for the deletion of the custom local user attribute in the Widget_125    open fun onWidgetUserPropertiesDeleted(properties: MutableMap<String, Any>,_125        cause: MutableMap<String, Any>?,_125        keys: MutableList<String>) {_125        ......_125    }_125_125    // Release resources_125    open fun release() {_125        ......_125    }_125}
AgoraWidgetContext interface
_62// WidgetContext capability interface_62interface AgoraWidgetContext {_62    // Create a Widget object instance_62    // @param config The configuration information of this Widget object_62    // @return Widget instance, if it is empty, it means the creation failed_62    fun create(config: AgoraWidgetConfig): AgoraBaseWidget?_62_62    // Get the current position and size ratio information of the Widget_62    // @param widgetId Widget's unique identifier_62    // @return widget's current position and scale information. The benchmark of the ratio needs to be unified by the developers themselves._62    fun getWidgetSyncFrame(widgetId: String): AgoraWidgetFrame?_62_62    // Get the configuration information of all registered widgets_62    fun getWidgetConfigs(): MutableList<AgoraWidgetConfig>_62_62    // Get the configuration information of a registered widget_62    // @param widgetId Widget's unique identifier_62    fun getWidgetConfig(widgetId: String): AgoraWidgetConfig?_62_62    // Activate a widget._62    // After the operation is successful, you will receive a callback from AgoraWidgetActiveObserver.onWidgetActive_62    // @param widgetId Widget's unique identifier_62    // @param ownerUserUuid The userUuid of the user who owns the currently activated Widget_62    // @param roomProperties initialized room properties_62    // @param callback The callback listener for the activation operation_62    fun setWidgetActive(widgetId: String, ownerUserUuid: String? = null,_62                        roomProperties: Map<String, Any>? = null,_62                        callback: EduContextCallback<Unit>? = null)_62_62    // Log out the specified Widget_62    // @param widgetId Widget's unique identifier_62    // @param isRemove Whether to completely delete all the information of this Widget in the current classroom:_62    // - true: delete completely. All information under roomProperties.widgets.'widgetId' and userProperties.widgets.'widgetId' will be deleted_62    // - false: Only set roomProperties.widgets.'widgetId'.state to '0', that is, set the current Widget to inactive state. _62    // No matter what value is passed, you will receive the AgoraWidgetActiveObserver.onWidgetInActive callback._62    // @param callback callback listener for logout operation_62    fun setWidgetInActive(widgetId: String, isRemove: Boolean = false, callback: EduContextCallback<Unit>? = null)_62_62_62    // Get the activation state of a widget_62    // @param widgetId Widget's unique identifier_62    fun getWidgetActive(widgetId: String): Boolean_62_62    // Get the activation status of all registered widgets_62    // @param widgetId Widget's unique identifier_62    fun getAllWidgetActive(): Map<String, Boolean>_62_62    // Add an AgoraWidgetActiveObserver listener to monitor whether the Widget is active_62    fun addWidgetActiveObserver(observer: AgoraWidgetActiveObserver, widgetId: String)_62_62    // Remove an AgoraWidgetActiveObserver listener_62    fun removeWidgetActiveObserver(observer: AgoraWidgetActiveObserver, widgetId: String)_62_62    // Add an AgoraWidgetMessageObserver listener to listen for all messages sent by this Widget_62    fun addWidgetMessageObserver(observer: AgoraWidgetMessageObserver, widgetId: String)_62_62    // Remove an AgoraWidgetMessageObserver listener_62    fun removeWidgetMessageObserver(observer: AgoraWidgetMessageObserver, widgetId: String)_62_62    // Send a message to a widget_62    fun sendMessageToWidget(msg: String, widgetId: String)_62}
Type definition
_49// Widget configuration class_49data class AgoraWidgetConfig(_49    // The custom Widget class corresponding to this WidgetId_49    var widgetClass: Class<out AgoraBaseWidget>,_49    // Unique identifier for the current Widget_49    var widgetId: String,_49    // The icon in the unselected (default or inactive) state_49    val image: Int? = null,_49    // The icon in the selected or activated state_49    val selectedImage: Int? = null,_49    // Custom data, which is passed to AgoraWidgetInfo.extraInfo after Widget is instantiated_49    var extraInfo: Any? = null_49)_49_49// Widget information class_49data class AgoraWidgetInfo(_49    var roomInfo: AgoraWidgetRoomInfo,_49    var localUserInfo: AgoraWidgetUserInfo,_49    // Unique identifier for the current Widget_49    val widgetId: String,_49    // Custom data passed in externally_49    val extraInfo: Any?,_49    // custom room properties in the current widget_49    var roomProperties: MutableMap<String, Any>?,_49    // Custom local user properties in the current Widget_49    var localUserProperties: MutableMap<String, Any>?_49)_49_49// Widget's position and size ratio information class_49data class AgoraWidgetFrame(_49    val x: Float? = null,_49    val y: Float? = null,_49    val width: Float? = null,_49    val height: Float? = null_49)_49_49// User information class in Widget_49data class AgoraWidgetUserInfo(_49        val userUuid: String,_49        val userName: String,_49        val userRole: Int_49)_49_49// Room information class in Widget_49data class AgoraWidgetRoomInfo(_49        val roomUuid: String,_49        val roomName: String,_49        val roomType: Int_49)