Skip to main content

Document Reading - OCR Implementation

This guide details the implementation of the Document Reading SDK using a Clean Architecture approach. By separating concerns into Data Sources, Providers, and ViewModels, the system remains modular, testable, and ready for your transition to Jetpack Compose.


Domain Layer

Document Data Source

The DocumentDataSource handles the physical instantiation of the SDK fragment and manages the global state of the DotSdk.

DocumentDataSource Interface

interface DocumentDataSource {
fun getCaptureFragment(
activity: MainActivity,
onResult: (MBDocumentCaptureResult) -> Unit
): MBDocumentCaptureFragment
}

Data Layer

Data source Implementation

This implementation ensures the SDK is re-initialized with the correct license before the fragment is created.

class DocumentDataSourceImpl: DocumentDataSource {
override fun getCaptureFragment(
activity: MainActivity,
onResult: (MBDocumentCaptureResult) -> Unit
): MBDocumentCaptureFragment {

// 1. Load the license from raw resources
val lic = activity.resources.openRawResource(R.raw.iengine).use { it.readBytes() }

// 2. Manage SDK Lifecycle
if (DotSdkInitializer.isInitialized()) {
DotSdkInitializer.deinitialize()
}
DotSdkInitializer.initialize(activity as Context, lic)

// 3. Configure Scan Options
val options = MBDocumentCaptureOptions.Builder()
.enableMrzReading(true)
.build()

return MBDocumentCaptureFragment.newInstance(
options = options,
license = lic,
listener = MBDocumentCaptureFragmentListener { result -> onResult(result) },
) as MBDocumentCaptureFragment
}
}

Configuration Options Reference

The MBDocumentCaptureOptions class allows you to customize the behavior of the scanning interface. Use the Builder to set these values.

val options = MBDocumentCaptureOptions.Builder()
.enableMrzReading(true)
.previewScaleType(PreviewScaleType.FILL_SCREEN)
.build()

Configuration: MBDocumentCaptureOptions

Builder MethodPropertyTypeDefault ValueDescription
enableMrzReading(Boolean)validationRequiredBooleanfalseWhen true, the SDK will only capture the document once a valid Machine Readable Zone (MRZ) is detected. And any type of documents if set to false.
previewScaleType(ScaleType)scaleTypeEnumFILL_SCREENDetermines how the camera feed is scaled to fit the designated UI container.

Preview Scale Types

When integrating with Jetpack Compose or traditional XML layouts, the scale type determines how the camera aspect ratio interacts with your view bounds.

Scale TypeBehaviorBest Used For
FILL_SCREEN Scales the preview to fill the entire container. The feed is cropped to match the container's aspect ratioImmersive, full-screen scanning experiences.
FIT_CENTERScales the preview so the entire camera frame is visible. This may result in black bars (letterboxing).Scanners where the user needs to see the absolute edges of the camera sensor.

Presentation Layer

Fragment Container Provider

The FragmentContainerProvider abstracts the FragmentManager transactions, allowing the ViewModel to manage the camera lifecycle without direct fragment manipulation.

interface FragmentContainerProvider {
fun setupFragment(
activity: AppCompatActivity,
containerId: Int,
onReadingResult: (MBDocumentCaptureResult) -> Unit
)
fun removeFragment(
activity: MainActivity,
id: Int
)
}

Fragment Container Implementation

The setupFragment method ensures that we don't accidentally recreate the fragment if it is already present, while removeFragment ensures a clean teardown once the document is scanned.

class FragmentContainerProviderImpl(
private val documentDataSource: DocumentDataSource = DocumentDataSourceImpl()
): FragmentContainerProvider {

override fun setupFragment(
activity: AppCompatActivity,
containerId: Int,
onReadingResult: (MBDocumentCaptureResult) -> Unit
) {
val fragmentManager = activity.supportFragmentManager

// Fetch the fragment from the Data Source
val fragment = documentDataSource.getCaptureFragment(
activity as MainActivity,
onResult = { onReadingResult(it) }
)

// Perform Fragment Transaction safely
if (!fragmentManager.isStateSaved) {
val existingFragment = fragmentManager.findFragmentById(containerId)

// Only replace if the container is empty or holds a different fragment type
if (existingFragment == null || existingFragment::class != fragment::class) {
fragmentManager
.beginTransaction()
.replace(containerId, fragment)
.commitAllowingStateLoss()
}
}
}

override fun removeFragment(activity: MainActivity, id: Int) {
val fragmentManager = activity.supportFragmentManager
val fragment = fragmentManager.findFragmentById(id)

if (fragment != null && !fragmentManager.isStateSaved) {
fragmentManager
.beginTransaction()
.remove(fragment)
.commitNow()
fragmentManager.executePendingTransactions()
}
}
}

ViewModel Implementation

The DocumentReaderViewModelholds the state of the captured document and triggers the camera lifecycle via the provider.

class DocumentReaderViewModel(
private val fragmentContainerProvider: FragmentContainerProvider = FragmentContainerProviderImpl()
): ViewModel() {

private val _document = MutableStateFlow<MBDocumentCaptureResult?>(null)
val document: StateFlow<MBDocumentCaptureResult?> = _document.asStateFlow()

/**
* Injects the Camera Fragment into the provided UI container ID.
*/
fun startCamera(context: Context, containerId: Int) {
fragmentContainerProvider.setupFragment(
activity = context as MainActivity,
containerId = containerId,
onReadingResult = { _document.value = it }
)
}

fun removeFragment(activity: MainActivity, id: Int) {
fragmentContainerProvider.removeFragment(activity, id)
}
}

UI Layer

Usage in Fragments

Inject the ViewModel and observe results inside onViewCreated.

// Inside DocumentReadingFragment.kt
private val viewModel: DocumentReaderViewModel

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Start camera in the designated container
viewModel.startCamera(requireActivity(), binding.container.id)

// Collect scan results
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.document.collect { result ->
result?.let {
val keys = extractKeys(document = result)
val bundle = Bundle().apply {
putString("document_number", keys?.first)
putString("birth_date", keys?.second)
putString("expiry_date", keys?.third)
}

// Cleanup and Navigate
viewModel.removeFragment(activity as MainActivity, binding.container.id)
findNavController().navigate(R.id.action_to_nfc, bundle)
}
}
}
}
}

Usage in Jetpack Compose

Create the Fragment Container Provider The FragmentContainerProvider interface abstracts the fragment transactions. This ensures that the SDK’s camera fragment is correctly instantiated, attached, and removed without leaking memory or causing state inconsistencies.

interface FragmentContainerProvider {
fun getFragmentContainerView(context: Context): FrameLayout
fun setupFragment(activity: FragmentActivity, containerId: Int, mrzDetection: Boolean, fragmentTag: String)
fun removeFragment(activity: FragmentActivity, fragmentTag: String)
}

Implementation Logic

The DefaultFragmentContainerProvider manages the FragmentManager. It ensures that:

  • Unique IDs: A new ID is generated for the container to avoid collisions.

  • State Safety: It checks isStateSaved before performing transactions to prevent IllegalStateException.

  • Idempotency: It only replaces the fragment if the container is empty or the fragment type has changed.

Compose screen implementation

@Composable
fun DocumentScannerScreen(
documentReaderViewModel: DocumentReaderViewModel,
) {
val fragmentActivity = LocalContext.current as FragmentActivity
val fragmentTag = "CameraSdkFragment"

// Remember the provider to keep it across recompositions
val fragmentContainerProvider = remember {
DefaultFragmentContainerProvider(documentReaderViewModel)
}

AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
// 1. Create the physical view container
val container = fragmentContainerProvider.getFragmentContainerView(ctx)

// 2. Initialize the SDK Fragment within that container
fragmentContainerProvider.setupFragment(
activity = fragmentActivity,
containerId = container.id,
mrzDetection = true,
fragmentTag = fragmentTag,
)
container
},
onRelease = {
// 3. Clean up the fragment when the Composable leaves the composition
fragmentContainerProvider.removeFragment(fragmentActivity, fragmentTag)
}
)
}