추가된 구조
core/common
- util/Util.kt (byte→String / String→byte 등 공통으로 쓰는 함수 정리)
- NFCEventBus (NFC Tag 값 전달 → 다른 모듈에서 알 수 있는 방법 flow로 처리)
core/network
→ 전체적인 network 관리를 여기서 할 예정
core/payon
→ payon sector 접근 및 network통신 처리 (NFC를 읽는 기능은 MainActivity 공통으로 처리)
feature/reader
→ nfc 읽는 화면
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val nfcAdapter by lazy { NfcAdapter.getDefaultAdapter(this) }
val currentRouteFlow = MutableStateFlow<String?>(null)
val currentReadTypeFlow = MutableStateFlow<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val appState = rememberBeaverAppState()
val navController = appState.navController
BeaverPaySampleTheme {
NavGraph(appState)
}
// NavRoute 변경 감지
val navBackStack by navController.currentBackStackEntryAsState()
LaunchedEffect(navBackStack) {
val destination = navBackStack?.destination
val args = navBackStack?.arguments
val route = destination?.route
val readType = args?.getString("readType")
Timber.e("🔥 route changed = $route, readType = $readType")
currentRouteFlow.value = route
currentReadTypeFlow.value = readType
}
}
// 📌 여기서 routeFlow 변화 감지하고 NFC on/off 처리
lifecycleScope.launch {
currentReadTypeFlow.collect { type ->
Timber.e("🔄 routeFlow collect → $type")
if (type == "payon") {
enableReaderMode()
} else {
disableReaderMode()
}
}
}
}
override fun onResume() {
super.onResume()
}
override fun onPause() {
super.onPause()
}
fun enableReaderMode() {
Timber.e("🔵 enableReaderMode() 실행")
nfcAdapter?.enableReaderMode(
this,
{ tag ->
Timber.e("ReaderMode Tag detected: ${tag.id.joinToString { "%02X".format(it) }}")
NFCEventBus.onTagDetected(tag)
},
NfcAdapter.FLAG_READER_NFC_A or
NfcAdapter.FLAG_READER_NFC_B or
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
null
)
}
fun disableReaderMode() {
Timber.e("🟥 disableReaderMode() 실행")
nfcAdapter?.disableReaderMode(this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Timber.e("🔥 onNewIntent 호출, ACTION = ${intent.action}")
if (intent.action == NfcAdapter.ACTION_TECH_DISCOVERED ||
intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED ||
intent.action == NfcAdapter.ACTION_TAG_DISCOVERED
) {
val tag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
}
if (tag == null) {
Timber.d("Tag is NULL")
return
}
Timber.e("✅ Tag detected! techList = ${tag.techList.joinToString()}")
NFCEventBus.onTagDetected(tag)
}
}
}
object NFCEventBus {
private val _nfcFlow = MutableSharedFlow<Tag>()
val nfcFlow = _nfcFlow
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun onTagDetected(tag: Tag) {
scope.launch {
_nfcFlow.emit(tag)
}
}
}
@HiltViewModel
class ReaderViewModel @Inject constructor(
val cardReaderUseCase: CardReaderUseCase
) : ViewModel() {
private val _tagInfo = MutableStateFlow("")
val tagInfo = _tagInfo
private val _uiEvent = MutableSharedFlow<UiEvent>()
val uiEvent: MutableSharedFlow<UiEvent> = _uiEvent
init {
viewModelScope.launch {
NFCEventBus.nfcFlow.collect { tag ->
val id = tag.id.joinToString("") { "%02X".format(it) }
_tagInfo.value = "Tag ID: $id"
cardReaderUseCase(ReaderType.PAYON, tag).collect { result ->
when(result){
is CardResult.Loading -> {
_uiEvent.emit(UiEvent.Loading(result.isLoading))
}
is CardResult.Success -> {
_uiEvent.emit(
UiEvent.ShowDialog(
"카드인식 성공",
"Card : ${Util().bytesToHex(result.data.track2Data)}"
)
)
}
is CardResult.Error -> {
_uiEvent.emit(UiEvent.ShowDialog(result.code, result.message))
}
}
}
}
}
}
}
Compose에서 RouteFlow 변화를 감지하여 NFC ON/OFF 처리 담당
→ 현재는 reader/payon 일 경우 enable, 나머지일 경우 disable
NFC 인식 시 NewIntent에서 Tag 값을 *MutableSharedFlow*로 가지고 있음
ViewModel에서 collect해서 Tag값을 feature/reader 모듈로 가져올 수 있음
🔥 emit vs collect
| 목 | emit | collect |
|---|---|---|
| 역할 | Flow에 값 넣기 | Flow로부터 값 받기 |
| 위치 | 보통 Activity, Repository, EventBus | 보통 ViewModel, UI |
| 종류 | Producer | Consumer |
| 실행 시기 | 이벤트가 발생할 때 | Flow를 collect하는 동안 계속 |
| suspend 여부 | suspend (SharedFlow에서는 tryEmit 사용 가능) | suspend |