Dynamic Card Injection Framework Using Annotation Processing in Android
This article describes how a mobile development team refactored a card‑creation feature by applying the Open/Closed and Single‑Responsibility principles, introduced a factory pattern with reflection, and ultimately built a fully automated dynamic injection framework using Java Annotation Processing (APT) to eliminate manual code updates.
Introduction
During a routine requirement review, the team discussed redesigning a fund‑card view to support nine different card styles that can be combined freely. The initial implementation used a simple control‑flow function that returned a specific card view based on an integer type.
// CardOutView.kt
fun getCard(type: Int): View? {
var card: View = null
if (type == 1) {
card = Card1()
} else if (type == 2) {
...
} else {
throw IllegalArgumentException("type error!")
}
return card
}Colleagues quickly pointed out three major problems: the code violates the Open/Closed principle, the Single‑Responsibility principle, and lacks abstraction for the cards.
Refactoring with Factory Pattern and Reflection
The team introduced an ICard interface and a simple factory to decouple card creation from the view logic.
// 卡片接口
interface ICard {
fun setData(data: CardData)
fun getView(): View?
}
// CardOutView.kt
fun getCardViewAndSetData(type: Int, data: CardData): View? {
val card: ICard? = CardFactory.getCard(type)
card?.setData(data)
return card?.getView()
}
// CardFactory.kt
fun getCard(type: Int): ICard? {
val clazz = classMap[index] // 获取 class 信息。
val c: Constructor
? = clazz?.getConstructor(Context::class.java)
return c?.newInstance()
}This redesign satisfies the Open/Closed principle (new cards require no changes to CardOutView ), adheres to Single‑Responsibility (card creation is handled by CardFactory ), and provides a clear abstraction for card designers.
Achieving Full Automation with Annotation Processing (APT)
To remove the need for manually updating the classMap when adding new cards, the team leveraged APT. A custom annotation @FundSubView marks each card class, and an annotation processor generates registration code automatically.
// apt-annotation/FundSubView.kt
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface FundSubView {
String type()
}The processor scans for this annotation and emits a class that registers all cards at runtime.
// FundSubProcessor.kt
@AutoService(Processor::class)
public class FundSubProcessor extends AbstractProcessor {
@Override
public Set
getSupportedAnnotationTypes() {
Set
set = new HashSet<>();
set.add(FundSubView.class.getCanonicalName());
return set;
}
@Override
public boolean process(Set
set, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(FundSubView.class)) {
// Generate Java class that registers the card
}
return true;
}
}The generated registration class looks like:
public class FundCard_AutoGen extends CardFactory {
public static void init() {
register("1", FundRateLineView.class);
register("2", FundCardContimuousWinHsView.class);
...;
}
}Application startup simply calls FundCard_AutoGen.init() , and any new card annotated with @FundSubView(type = "newType") is automatically available without further code changes.
Conclusion
The article demonstrates a practical evolution from a naïve control‑flow implementation to a clean, extensible architecture using design patterns and annotation processing. It provides step‑by‑step guidance, code snippets, and diagrams, enabling Android developers to build maintainable, dynamically extensible UI components.
Snowball Engineer Team
Proactivity, efficiency, professionalism, and empathy are the core values of the Snowball Engineer Team; curiosity, passion, and sharing of technology drive their continuous progress.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.