Issue
I am trying to create a native ui
with fragment
on android
CealScanQrView.kt
class CealScanQrView(context: Context): FrameLayout(context) {
...
//Contains all the logic of integrating camerax, check below code repo to see the full source code
...
//Now while submitting the data from native side to react-native I get error
val reactContext = context as ReactContext ///This line causes error and makes my app crash
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, topChange, event)
}
CealScanQrFragment.kt
class CealScanQrFragment: Fragment() {
private lateinit var cealScanQrView: CealScanQrView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
cealScanQrView = CealScanQrView(requireNotNull(context))
return cealScanQrView // this CustomView could be any view that you want to render
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// do any logic that should happen in an `onCreate` method, e.g:
cealScanQrView.setUpCamera(requireActivity())
}
override fun onDestroy() {
super.onDestroy()
cealScanQrView.destroyCamera()
}
}
CealScanQrViewManager.kt
class CealScanQrViewManager(
private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>() {
private val cealScanQrView = "CealScanQrView"
private val topChange = "topChange"
private val phasedRegistrationNames = "phasedRegistrationNames"
private val bubbled = "bubbled"
private val onChange = "onChange"
private val create = "create"
companion object {
private const val COMMAND_CREATE = 1
}
private var propWidth: Int? = null
private var propHeight: Int? = null
override fun getName() = cealScanQrView
override fun createViewInstance(reactContext: ThemedReactContext) = FrameLayout(reactContext)
override fun getCommandsMap() = mapOf("create" to COMMAND_CREATE)
override fun receiveCommand(root: FrameLayout, commandId: String?, args: ReadableArray?) {
super.receiveCommand(root, commandId, args)
val reactNativeViewId = requireNotNull(args).getInt(0)
when (commandId?.toInt()) {
COMMAND_CREATE -> createFragment(root, reactNativeViewId)
}
}
private fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
setupLayout(parentView)
val myFragment = CealScanQrFragment()
val activity = reactContext.currentActivity as FragmentActivity
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, myFragment, reactNativeViewId.toString())
.commit()
}
private fun setupLayout(view: View) {
Choreographer.getInstance().postFrameCallback(object: Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
manuallyLayoutChildren(view)
view.viewTreeObserver.dispatchOnGlobalLayout()
Choreographer.getInstance().postFrameCallback(this)
}
})
}
@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FrameLayout, index: Int, value: Int) {
if (index == 0) propWidth = value
if (index == 1) propHeight = value
}
private fun manuallyLayoutChildren(view: View) {
// propWidth and propHeight coming from react-native props
val width = requireNotNull(propWidth)
val height = requireNotNull(propHeight)
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
view.layout(0, 0, width, height)
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return mapOf(
topChange to mapOf(
phasedRegistrationNames to mapOf(
bubbled to onChange
)
)
)
}
}
val reactContext = context as ReactContext
causes the app to crash and throw error saying
java.lang.ClassCastException: com.myapp.MainActivity cannot be cast to com.facebook.react.bridge.ReactContext
What kind of context
I should pass from my fragment
to Native UI View
so I can use getJSModule
method to send data
Full source code here
Solution
You can not cast android's Context class to the ReactContext.
If you will use reactContext inside your View class, then you can pass ReactContext instance from ViewManager --> Fragment --> View
1 - Write interface like this:
interface ReactContextProvider {
fun provideReactContext(): ReactApplicationContext
}
2 - Implement this interface in your CealScanQrViewManager class:
class CealScanQrViewManager(
private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>(), ReactContextProvider {
...
override fun provideReactContext(): ReactApplicationContext {
return reactContext
}
...
}
3 - Add reactContextProvider to the CealScanQrFragment class
var reactContextProvider: ReactContextProvider? = null
4 - Pass this interface to the CealScanQrFragment class in your createFragment function on CealScanQrViewManager class.
val myFragment = CealScanQrFragment()
myFragment.reactContextProvider = this
5 - Define a variable in your CealScanQrView class:
var reactContextProvider: ReactContextProvider? = null
6 - Pass this variable to the CealScanQrView instance on your onCreateView function on CealScanQrFragment:
cealScanQrView = CealScanQrView(requireNotNull(context))
cealScanQrView.reactContextProvider = reactContextProvider
7 - Use your reactContextProvider inside View like below:
reactContextProvider?.provideReactContext()?.let { reactContext -->
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, topChange, event)
}
Answered By - okarakose
Answer Checked By - David Goodson (JavaFixing Volunteer)