A scan button in a fragment, a QR detector in a second activity, and an onActivityResult() override that never runs: the classic Android pattern breaks the moment the caller becomes a fragment. A Fragment is not a Context, so it cannot build the Intent alone, and the legacy result contract was written for activities. If your app went fragment-based while the working scanner stayed an activity, the fix is small: register the receiver on the fragment, launch through the host activity, and deliver the decoded string over a channel both sides can see. You get both routes below — an ActivityResultLauncher on the fragment, and the fragment result API for a scanner that is itself a fragment — with the tests that prove delivery.
Why onActivityResult never fires in a fragment
Two facts explain the silence. First, new Intent(PreScan.this, LeerQR.class) only compiles while PreScan is an Activity; a Fragment is not a Context, so once the button lives in a fragment, the first argument must be the host activity — getActivity(), or requireActivity() on AndroidX. Second, Activity.startActivityForResult(Intent, int) is deprecated, and the onActivityResult(int requestCode, int resultCode, Intent data) contract delivers to whoever the platform treats as the caller. A fragment override in that flow is dead code: nothing routes a result into it.
The modern APIs reverse the ownership. Since AndroidX activity 1.2.0 and fragment 1.3.0, a component registers its launcher or listener up front, and delivery comes back to that exact object — activity or fragment, no forwarding. The old constants still matter at the boundary: Activity.RESULT_OK is -1, RESULT_CANCELED is 0, and a back-press out of the scanner returns the second value.
The decision: pick the route by where the scanner lives
-
Scanner stays an activity — your
LeerQR extends AppCompatActivityrunning aBarcodeDetectorbuilt withsetBarcodeFormats(Barcode.QR_CODE)and aCameraSourceatsetRequestedFps(24)with a 1920×1024 preview: take Route A,registerForActivityResult()on the fragment plussetResult(RESULT_OK, intent)andfinish()in the detector callback. For the code in question, this is the answer. -
Scanner becomes a fragment inside the same activity: take Route B,
setFragmentResult()andsetFragmentResultListener()through oneFragmentManager, with the decoded string in aBundle. -
Libraries older than AndroidX 1.3.0: keep
startActivityForResult()on the support fragment with thegetActivity()context — a short section near the end covers it.
The split is not taste. The fragment result API travels inside a single FragmentManager, so the moment the scanner is a separate activity, Route B cannot reach across — that constraint decides for you.
What you need
- AndroidX activity 1.2.0 or newer and fragment 1.3.0 or newer on the classpath; Route B strictly requires them.
- The existing scanner activity, unchanged: the
BarcodeDetector.Builder(...)setup, theCameraSourcewith auto-focus, and thereceiveDetections(Detector.Detections<Barcode>)processor. - The existing permission block: the check against
Manifest.permission.CAMERAand the request carryingPERMISSION_REQUEST = 200. - Two string keys: the request key
qr_scan_resultand the extra keyqr_value. Both are names chosen for this guide — define them once as constants and keep both sides identical.
Route A: register the launcher in the fragment
Five steps, in order. The one ordering rule: registerForActivityResult() must run before the fragment reaches the STARTED state, so the field declaration in step 1 does the work at construction time.
-
Declare the launcher as a field.
resultViewis the view you already bind inonCreateView():
private final ActivityResultLauncher<Intent> qrLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
String qr = result.getData().getStringExtra("qr_value");
resultView.setText(qr);
processScan(qr); // the same processing your onActivityResult used to run
}
});
-
Launch from the button, with the host activity as the Intent context. A fragment cannot be a Context;
requireActivity()is:
btnScan.setOnClickListener(v ->
qrLauncher.launch(new Intent(requireActivity(), LeerQR.class)));
-
Deliver once from the detector callback.
receiveDetections()fires for every preview frame, soisFinishing()drops the repeats after the first hit:
@Override
public void receiveDetections(Detector.Detections<Barcode> detections) {
SparseArray<Barcode> barcodes = detections.getDetectedItems();
if (barcodes.size() > 0 && !isFinishing()) {
Intent data = new Intent();
data.putExtra("qr_value", barcodes.valueAt(0).displayValue);
setResult(Activity.RESULT_OK, data);
finish();
}
}
-
Delete the fragment's
onActivityResult(int, int, Intent)override and theREQUEST_CODE = 100constant. The launcher owns the request plumbing; the dead override only invites the next reader to trust it. -
Ship the string, not the
Barcodeparcelable.displayValueis the only part your processing ever used; the receiver reads it withgetStringExtra("qr_value")and stays decoupled from the detector library.
Route B: the fragment result API for a scanner fragment
When the preview moves into a fragment of the same activity — the single-activity pattern built with FragmentManager.beginTransaction().replace(...) — results travel through the FragmentManager instead of across the activity boundary. Register the listener before you launch, tied to the view lifecycle so it dies with the view:
getParentFragmentManager().setFragmentResultListener("qr_scan_result",
getViewLifecycleOwner(),
(requestKey, bundle) -> {
String qr = bundle.getString("qr_value");
resultView.setText(qr);
});
The scanning fragment delivers and then pops itself off the back stack:
Bundle result = new Bundle();
result.putString("qr_value", barcode.displayValue);
getParentFragmentManager().setFragmentResult("qr_scan_result", result);
getParentFragmentManager().popBackStack();
Three documented behaviors set the rules. A result stored with no listener waiting is delivered once, then cleared. A listener registered with a destroyed LifecycleOwner is removed automatically, which is why getViewLifecycleOwner() beats this here. And the registry is per-FragmentManager:
If several screens can each request a scan, give every caller its own request key — the same keyed routing as in two barcode listeners on one screen, with no shared state to collide.
Libraries older than AndroidX 1.3.0
Call startActivityForResult(intent, REQUEST_CODE) on the support-library fragment itself — new Intent(getActivity(), LeerQR.class) fixes the context — and keep the onActivityResult() override in the fragment: FragmentActivity delivers results to fragments that launched through their own bridge. Two requirements, both easy to miss: the call must be on the fragment, not the activity, and the class must be the support-library Fragment; the platform android.app.Fragment never receives the callback. The path is deprecated on AndroidX, so treat it as a bridge, not a destination.
Verify the result lands
Test with a payload you know, then the failure paths:
- Encode a QR with a fixed payload — the string
LOT-2025-0142-B, an example value any fixed string can replace. Scan it. The TextView must show exactlyLOT-2025-0142-B, andprocessScan()must receive that same string; a mismatch means the two key strings differ. - Press back without scanning. The callback fires with
RESULT_CANCELED(0); theRESULT_OK(-1) check skips it, and the TextView keeps its previous text. - Log the key at both ends — one line before
setResult()orsetFragmentResult(), one in the receiver. A deliverednullalmost always means the sender wroteqrValuewhile the receiver readsqr_value. - Rotate the device between tap and scan. The registry keeps a pending activity result across recreation, so the callback still fires once the fragment is back.
- Scan twice in a row. Each
launch()call delivers exactly one result; a second delivery on the first scan means the detector's guard is missing, not the fragment code.
Once the payload lands and needs slicing — a lot prefix, a delimiter, a checksum — that is ordinary string work, the same as in trim a substring from a barcode scan string.
Common mistakes and the fix for each
-
new Intent(this, LeerQR.class)inside the fragment.thisis the Fragment, not aContext. UserequireActivity(), orgetActivity()on the old library. -
Registering the launcher inside the click listener. By the time a user can tap, the fragment is STARTED; move registration to a field or
onCreate(). -
A scanner that stops delivering after the first scan. Frames keep arriving after
finish()is queued;isFinishing()drops the repeats. A scanner that never delivers at all is usually permission, not plumbing: withoutManifest.permission.CAMERAgranted, theCameraSourcenever starts. The same class of bug — a swallowed first event behind a null field — is what the nullpreviousValuefix addresses in scanner-to-app code. -
Carrying the
Barcodeobject across the boundary. It travels as a Parcelable, but it couples both sides to the detector types; the string is the contract. - Route B across activities. One FragmentManager per activity; the launcher route exists precisely for that case.
When the phone camera is the wrong reader
A phone camera with an on-device detector is the right tool at bench range: close, lit, flat codes. Across a plant aisle, in glare, or on damaged and dense codes, the decode rate falls away — a hardware limit no fragment code fixes. The in-app preview-and-detector approach itself is the subject of Android barcode scanning without a scanner app. When the codes defeat the camera, an industrial handheld code reader does the optics on its own hardware and hands your app the decoded text over an interface, which collapses the Android side to plain string handling: no camera permission, no preview, no detector lifecycle.
The LS-HCR-0001 links to the host over USB; the LS-HCR-0002 travels with the operator and reports over a wireless link or Ethernet, and both deliver the decoded payload as text your existing processScan() already parses.
- Resolution
- Up to 3 MP
- Shutter
- Global
- Resolution
- Up to 3 MP
- Shutter
- Global
Wiring a reader into a host over serial, USB or Ethernet is covered in barcode scanner input methods to a PLC, and adding an Enter key with programming barcodes shapes the reader's output before it reaches your code. An Android device can host a wired reader over its own USB port — the same OTG wiring as on an embedded controller. For a fixed station where nobody holds anything, a smart camera or vision sensor decodes on the device and ships only the result.
Ship checklist
- Launcher registered in a field or
onCreate()— never inside a callback. - One request key and one extra key, defined as constants, identical on both sides.
-
setResult(RESULT_OK, data)guarded byisFinishing()and followed byfinish(). - The receiver checks
RESULT_OKand non-null data, so a back-press changes nothing. -
Manifest.permission.CAMERArequested before the first scan, denial handled with a message. - The payload crosses as a
String; no detector types past the boundary. - Tested: known payload scan, back-press, rotation, two scans in a row.
More code-reading guides cover the rest of the pipeline — when the scan must drive a machine rather than a view, start at routing a reader result to robot job selection. To pick hardware instead of phone cameras, the code reader finder filters by interface and connection, and the contact page is the direct line when the application is not a catalog row.
Questions
Why does my fragment's onActivityResult never fire?
The legacy activity-result contract delivers to the component that started the target activity, and a Fragment is not a Context that can own that launch. Even the bridged support-library path is deprecated. From AndroidX fragment 1.3.0, registerForActivityResult() with the StartActivityForResult contract registers the fragment itself as the receiver, so the callback lands in the fragment with no forwarding code.
Can setFragmentResult send the QR string to a fragment in another activity?
No. The fragment result API stores results inside one FragmentManager, and every activity owns its own manager, so a result set in the scanner activity never reaches a listener registered under the launching activity. When the scanner is a separate activity, carry the decoded string through the Activity Result API: setResult(RESULT_OK, intent) in the scanner, the launcher callback in the fragment.
How do I stop the detector returning a result on every frame?
receiveDetections() runs for every preview frame, so a code held in view fires it repeatedly. Guard the delivery: check isFinishing() before setResult(), call finish() immediately, or keep a delivered flag you reset when the fragment resumes. The Activity Result callback itself runs once per launch() call, so the fragment side needs no extra guard.
What replaces the Barcode parcelable extra?
Only the decoded text: barcode.displayValue, a String. Put it under one key such as qr_value, defined as a constant both sides reference. The Barcode object would travel as a Parcelable, but carrying it keeps the receiving code coupled to the detector library; the string is all your processing needs, whether you split a payload or look up a record.
Does the camera permission flow change with these APIs?
No. Keep the existing check against Manifest.permission.CAMERA and the request carrying your PERMISSION_REQUEST code from the fragment, with getActivity() as the context. Without the grant the CameraSource never starts and receiveDetections() stays silent, which looks exactly like a broken return path. Request before the first scan and handle the denial with a message or a route to settings.
