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 Method | Property | Type | Default Value | Description |
|---|---|---|---|---|
enableMrzReading(Boolean) | validationRequired | Boolean | false | When 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) | scaleType | Enum | FILL_SCREEN | Determines 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 Type | Behavior | Best Used For |
|---|---|---|
FILL_SCREEN | Scales the preview to fill the entire container. The feed is cropped to match the container's aspect ratio | Immersive, full-screen scanning experiences. |
FIT_CENTER | Scales 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
isStateSavedbefore performing transactions to preventIllegalStateException. -
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)
}
)
}