{"record":{"id":"147d2795af34ce6f","repo":"material-components/material-components-android","slug":"there-must-be-a-keyline-marked-as-focal","errorCode":null,"errorMessage":"There must be a keyline marked as focal.","messagePattern":"There must be a keyline marked as focal\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"lib/java/com/google/android/material/carousel/KeylineState.java","lineNumber":604,"sourceCode":"        int count,\n        boolean isFocal) {\n      if (count <= 0 || maskedItemSize <= 0F) {\n        return this;\n      }\n\n      for (int i = 0; i < count; i++) {\n        float loc = offsetLoc + (maskedItemSize * i);\n        addKeyline(loc, mask, maskedItemSize, isFocal);\n      }\n\n      return this;\n    }\n\n    /** Builds and returns a {@link KeylineState}. */\n    @NonNull\n    public KeylineState build() {\n      if (tmpFirstFocalKeyline == null) {\n        throw new IllegalStateException(\"There must be a keyline marked as focal.\");\n      }\n\n      List<Keyline> keylines = new ArrayList<>();\n      for (int i = 0; i < tmpKeylines.size(); i++) {\n        Keyline tmpKeyline = tmpKeylines.get(i);\n        Keyline keyline =\n            new Keyline(\n                calculateKeylineLocationForItemPosition(\n                    tmpFirstFocalKeyline.locOffset, itemSize, firstFocalKeylineIndex, i),\n                tmpKeyline.locOffset,\n                tmpKeyline.mask,\n                tmpKeyline.maskedItemSize,\n                tmpKeyline.isAnchor,\n                tmpKeyline.cutoff,\n                tmpKeyline.leftOrTopPaddingShift,\n                tmpKeyline.rightOrBottomPaddingShift);\n        keylines.add(keyline);\n      }","sourceCodeStart":586,"sourceCodeEnd":622,"githubUrl":"https://github.com/material-components/material-components-android/blob/ac7e18efeefb331850c561faf9ab8bf81d27ba68/lib/java/com/google/android/material/carousel/KeylineState.java#L586-L622","documentation":"KeylineState.Builder.build() throws this IllegalStateException when the builder never received a keyline marked as focal (isFocal=true). Focal keylines define the fully-unmasked center range of a Carousel and are structurally required: build() uses tmpFirstFocalKeyline to compute every keyline's resolved location. Without a focal keyline the KeylineState cannot be constructed.","triggerScenarios":"Calling KeylineState.Builder.build() after only adding non-focal keylines: every addKeyline(...) invoked with isFocal=false, only addAnchorKeyline(...) calls (anchors cannot be focal), or addKeylineRange(offsetLoc, mask, maskedItemSize, count, isFocal=false). Typical when implementing a custom Carousel strategy (e.g. customizing Carousel.Config or building a KeylineState manually instead of using KeylineStateList).","commonSituations":"Customizing Carousel keylines beyond the default strategy; porting/calculating keyline math by hand and forgetting to mark the mask==0 center range as focal; count passed to addKeylineRange computing to 0 for the focal range so no focal keyline is ever added; version upgrades where strategy APIs changed to require explicit focal ranges.","solutions":["Mark the center (unmasked) keylines as focal: addKeylineRange(offsetLoc, 0F, itemSize, count, true) or addKeyline(offsetLoc, 0F, itemSize, true, false)","If you do not need custom keylines, stop building KeylineState manually and use KeylineStateList.create(...) / the default Carousel configuration","If a conditional strategy sometimes has no focal keylines, guard before build() and fall back to the default KeylineState","Verify the focal count argument is >= 1 when computing ranges dynamically"],"exampleFix":"// before\nnew KeylineState.Builder(carouselSize, itemSize)\n    .addKeyline(startLoc, 1F, maskedSize, false, true)\n    .addKeylineRange(centerStart, 0F, itemSize, count, false) // never focal\n    .addKeyline(endLoc, 1F, maskedSize, false, true)\n    .build(); // throws: no focal keyline\n// after\nnew KeylineState.Builder(carouselSize, itemSize)\n    .addKeyline(startLoc, 1F, maskedSize, false, true)\n    .addKeylineRange(centerStart, 0F, itemSize, count, true) // focal range\n    .addKeyline(endLoc, 1F, maskedSize, false, true)\n    .build();","handlingStrategy":"validation","validationCode":"KeylineState.Builder builder = new KeylineState.Builder(carouselSize, itemSize);\nboolean[] addedFocal = {false};\n// wrap every focal addition:\n//   addKeylineRange(offsetLoc, 0F, itemSize, count, true) -> addedFocal[0] = true;\nif (!addedFocal[0]) {\n  throw new IllegalStateException(\"KeylineState config bug: no focal keyline added\");\n}\nKeylineState state = builder.build();","typeGuard":null,"tryCatchPattern":"try {\n  KeylineState state = builder.build();\n} catch (IllegalStateException e) {\n  if (e.getMessage() != null && e.getMessage().contains(\"focal\")) {\n    Log.e(TAG, \"Falling back to default keylines: \" + e.getMessage());\n    state = KeylineStateList.create(carousel, itemSize);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Always mark the center unmasked range with addKeylineRange(..., true)","Favor KeylineStateList.create over hand-built keyline lists unless customization is required","Add a unit test asserting your strategy's build() succeeds for every carousel width you support","Remember anchor keylines can never be focal (separate IllegalArgumentException)"],"tags":["android","material-components","carousel","keyline-state","builder","validation"],"backgroundTag":null,"analyzedSha":"ac7e18efeefb331850c561faf9ab8bf81d27ba68","analyzedAt":"2026-08-14T15:35:35.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}