mVoter 2020 Backstage#1 : Conductors

Photo by Lwin Kyaw Myat on Unsplash

ကျွန်တော်ဒီ Blog Post Series လေးမှာ ကျွန်တော်တို့ မြန်မာနိုင်ငံ ၂၀၂၀ပြည့်နှစ် အထွေထွေရွေးကောက်ပွဲအတွက် တိကျမှန်ကန်တဲ့အချက်အလက်တွေပေးနိုင်ဖို့ ဖန်တီးခဲ့တဲ့ mVoter 2020 Android App လေးထဲက Tech Stack တွေ အကြောင်းရယ်၊ ဘာလို့ဒီ Tech Stack တွေကိုရွေးချယ်ခဲ့တယ်ဆိုတာကို တစ်ခြားသူတွေလည်း ဗဟုသုတရအောင် မျှဝေပေးသွားပါမယ်။ ပထမဆုံးအနေနဲ့တော့ ကျွန်တော်တို့ Fragment အစား တစ်ခြား View-based Controller လေးသုံးခဲ့တဲ့ပုံစံကိုပြပေးပါမယ်။ ကျွန်တော်တို့ စရေးကတည်းက Single Activity ပဲသွားမယ်လို့ပဲ စဉ်းစားခဲ့တယ်။ ဒါပေမဲ့ ကျွန်တော်တို့က Fragment ကို ကြိုက်တဲ့သူတွေမဟုတ်တဲ့အခါက ဘယ်လိုလုပ်ကြမလဲဆိုပြီးဆွေးနွေးတော့ ကျွန်တော်က ရုံး project မှာ Conductor စမ်းဘူးတယ်ဆိုပြီး သုံးမလားဆိုပြီးမေးကြည့်တယ်။ Ye Min Htut ကလည်း သုံးကြည့်မယ်လေဆိုတာနဲ့ ကျွန်တော်တို့တွေ Fragment အစာ: Conductor သုံးဖြစ်သွားတယ်။

တစ်ခြားအပိုင်းတွေဖတ်ချင်တယ်ဆို

#2- Conductor

#3- Test of Time


Conductor ဆိုတာဘာလဲ

Conductor ဆိုတာကတော့ View ကို အခြေခံထားတဲ့ Reusable Controller တွေဖန်တီးလို့ရတဲ့ Android view wrapper တစ်ခုပါပဲ။ သူရဲ့ lifecycle က fragment နဲ့ယှဉ်လိုက်ရင် အရမ်းရိုးရှင်းတယ်။ Android View တစ်ခုအလုပ်လုပ်ပုံနည်းပါးပဲ လုပ်ထားတယ်။ သူမှာ အဓိက Controller နဲ့ Router ဆိုပြီးရှိမယ်။ Controller ဆိုတာက View ကို လှမ်းခေါ်ဖို့ ထိမ်းပေးတဲ့ အရာတစ်ခုပဲ၊ ပြောမယ်ဆို Fragment အစားထိုးပေါ့။ Router ဆိုတာက Controller တစ်ခုက နောက်တစ်ခု ဘယ်လိုပြမယ် အစရှိသဖြင့် လုပ်ပေးတဲ့အရာပေါ့၊ သူကဘာနဲ့တူမလဲဆိုတော့ FragmentManager နဲ့သွားတူမယ်။

Conductor က ဘာတွေကောင်းလဲ

ပထမသိသာတာကတော့ navigation သွားတဲ့ပုံစံပဲ။ သူက Controller BackStack တစ်ခုလုံးကို list အနေနဲ့ပို့ပေးလိုက်လို့ပါရတယ်၊ ဘာကောင်းလဲဆိုတော့ Deep Link က လာတဲ့အချိန်တို့၊ manually stack ကို ကစားရမဲ့အချိန်တို့ရင် တော်တော်လေးကို လွယ်ကူတာကိုတွေ့ရတယ်။

နောက်တစ်ခုက lifecycle, သူ့ github မှာပြထားတဲ့ lifecycle ကိုကြည့်ကြည့်လိုက်၊ Fragment နဲ့ယှဉ်လိုက်ရင် အများကြီးကို ပိုပြီး ရိုးရှင်းတာကိုတွေ့ရမယ်။ သူက View ကိုအခြေခံထားတော့ View တစ်ခုသဖွယ်စဉ်းစားကြည့်လို့ရတယ်။ View မှာ onResume onPause တွေမရှိဘူး။ ပြပြီဆို onAttach, မပြတော့ဘူးဆို onDetach ။ ကျန်တာက ပုံမှန်တိုင်း onCreateView onDestoryView onSaveViewState onRestoreViewState ဆိုပြီးပဲရှိတယ်။ Fragment Lifecycle ကြီးနဲ့ ယှဉ်လိုက်ရင် အများကြီးသာတယ် ဆိုရမယ်။

Conductor က ဘာတွေမကောင်းဘူးလဲ

တစ်ချို့ Component လေးတွေကိုယ့်ဟာကိုယ်ရေးရတာမျိုးတော့ရှိမယ်။ ဉပမာ Fragment အသုံးပြုထားတဲ့ Bottom Nav တို့လိုမျိုးမှာ custom implementation တွေလုပ်ရတယ်။ ဒါကလည်း Conductor issue တွေမှာလိုက်ရှာရင် သူများလုပ်ထားတာတွေ ကူးလို့ရပါတယ်။ အဆိုးဆုံးတစ်ခုကိုပြောပါဆိုရင်တော့ ViewModel နဲ့တွဲသုံးမယ်ဆို Dagger Hilt သုံးလို့မရသေးတာပဲ။ Hilt ရဲ့ ViewModelInject က ထွက်လာတဲ့ code တွေက Activity နဲ့ Fragment မှာပဲအလုပ်လုပ်တယ်၊ တစ်ခြားမှာသုံးမရသေးဘူး။ Issue ဖွင့်ထားတာတော့ရှိတယ်။ ဒီတော့ Conductor သုံးမယ်ဆို Dagger Hilt ကိုပြောင်းလို့မရသေးတဲ့ ပြဿနာလေးတော့ ရှိတယ်။ ကျွန်တော်တို့ mVoter 2020 မှာတော့ ဒါကို ကျွန်တော်က ပြဿနာအကြီးစားလို့ မယူဆဘူး။ ပြန်ရတဲ့ ရိုးရှင်းမှုနဲ့ ယှဉ်လိုက်ရင် Conductor က တန်ပါတယ်။

mVoter 2020 ထဲ က Conductor အသုံးပြုထားတဲ့ စိတ်ဝင်စရားအပိုင်းတွေ

ပထမတစ်ခု ပြချင်တာက Deep Link၊ ကျွန်တော်တို့ Fragment Nav သုံးရင်တော့ Deep Linking က သူ့ဟာသူ လိုအပ်တဲ့ backstack ကို ဖန်တီးပေးလိမ့်မယ်။ Controller မှာကျ Manual တော့လုပ်ရမယ်၊ ဒါပေမဲ့ သူ့ရဲ့ stack ဖန်တီးရတာက တကယ့်ကိုလွယ်တယ်။ ကျွန်တော်တို့မှာ Single Activity ပဲရှိမယ်၊ သူက နေပြီး ပဲ entry point ရှိတယ်၊ ဒီတော့ Activity ထဲမှာဒီလိုလေးစစ်ထားမယ်။ အောက်က code ကိုကြည့်ကြည့်ရအောင်၊

if (comingFromDeepLink(deepLinkUri)) {
  val partyId = PartyId(deepLinkUri.lastPathSegment ?: return false)
  val partyDetailController = PartyDetailController.newInstance(partyId)

  //Existing stack so we just push on top
  if (router.hasRootController()) {
    router.pushController(RouterTransaction.with(partyDetailController))
  } else {
    //No existing stack, we recreate the stack
    router.setBackstack(
      listOf(
        RouterTransaction.with(SplashController()),
        RouterTransaction.with(partyDetailController)
      ),
      null
    )
  }

  return true
}

ဒီမှာကြည့်မယ်ဆို ကျွန်တော်က အရင်ဆုံး router ထဲမှာ root controller ရှိထားပြီးသားလား စစ်လိုက်မယ်၊ ပြောချင်တာက App က အသုံးပြုနေဆဲ အခြေအနေလားပေါ့။ ရှိတယ်ဆို ပြနေတဲ့ Controller ရဲ့ အပေါ်မှာပဲ Deep Link ကလာတဲ့ Detail Controller ကိုထပ်တင်လိုက်တယ်။ တကယ်လို့မဟုတ်ပါဘူး၊ App ကဖွင့်ထားတာမရှိဘူးဆိုရင်တော့ ကျွန်တော်တို့က stack တစ်ခုဖန်တီးပေးလိုက်မယ်။ ဒီမှာကြည့်မယ်ဆို List လေး ပို့ပေးလိုက်ရုံပဲ၊ ဒါဆိုရင် Detail က ပြန်ထွက်ပြီဆိုတာနဲ့ Splash ကိုပြန်ရောက်ပြီး Splash ကနေ လိုတဲ့နေရာကို ပို့ပေးပါလိမ့်မယ်။

Popping BackStack

ဒုတိယပြချင်တာက stack တွေအထပ်လိုက်ထဲက အရင်တစ်ခုကို ဘယ်လိုပြန်သွားလဲ ဆိုတာလေး။ ဆိုတော့ ဒါစရေးတော့ အသုံးပြုသူတွေအတွက် အခက်အခဲတစ်ခုက ကိုယ်စားလှယ်လောင်းတွေကြည့်တုန်း သူ့ပြိုင်ဘက် ကိုယ်စားလှယ်လောင်းကို ထပ်ကြည့်၊ ထပ်ကြည့်ရင်းနဲ့ Detail Controller တွေများလာပြီး Back key ကို အသေနှိပ်မှ Listing ကိုပြန်ရောက်တော့တယ်။ ဒါနဲ့ပိုမြန်အောင်ဆိုပြီး Bottom Navigation က Candidate ဆိုတာကို ထပ်နှိပ်ရင် အစ Listing ကို ပြန်ခေါ်သွားတာလေး လုပ်ဖို့ဖြစ်လာတယ်။ ဒီတော့ ကျွန်တော်က Candidate List Controller ကို သွားတဲ့ RouterTranscation ထဲမှာ tag လေးတစ်ခု ထည့်ပေးလိုက်တယ်။

RouterTransaction.with(CandidateListController()).tag(CandidateListController.CONTROLLER_TAG)

ပြီးတော့ ဒါကို ပြန်သွားမယ်ဆိုတဲ့အချိန်မှာ ဒီ tag လေးထည့်ပေးလိုက်ရုံပဲ။

binding.bottomNavigationView.setOnNavigationItemReselectedListener { menuItem ->
  when (menuItem.itemId) {
    R.id.navigation_candidate -> {
    bottomNavRouterPagerAdapter.getRouter(0)?.popToTag(CandidateListController.CONTROLLER_TAG)
    }
}

ဒါဆိုရင် ကျွန်တော်တို့ လိုချင်သလိုမျိုး Navigation ကို ပြန်ရွေးလိုက်ရင် ရှေ့ဆုံးပြန်ရောက်သွားတဲ့ feature ကို အလွယ်တကူ 2 lines ထဲနဲ့ ထည့်လို့ရတယ်။

Analytics

တတိယတစ်ခုက Analytics. Firebase က သူ့ default က Activity/Fragment ကိုပဲ Screen track ပေးတယ်။ Controller မှာကျကျွန်တော်တို့ နည်းနည်းလေး Manual ပြန်ရေးရတယ်။ အရင်ဆုံး CanTrackScreen ဆိုတဲ့ Inteface တစ်ခုဖန်တီးမယ်။

interface CanTrackScreen {

  val screenName: String

}

ကျွန်တော်တို့က ရှိသမျှ Controller တိုင်းလည်း မလုပ်ချင်ဘူး၊ ဘာလို့လဲဆိုတော့ Bottom Navigation Host မျိုးဆို မလိုဘူးလေ။ ဒီတော့ Interface တစ်ခုလုပ်ထားလိုက်တာပါ။ ပြီးတော့ BaseController က OnAttach ထဲမှာ ဒီလိုလေးရေးလိုက်မယ်။ (onAttach က View ပြခါနီးတိုင်း ခေါ်ပါလိမ့်မယ်)

if (this is CanTrackScreen) {
  ScreenTrackAnalyticsProvider.screenTackAnalytics(requireContext())
    .trackScreen(this)
}

ဆိုတော့ ScreenTrackAnalytics ထဲမှာကျမှ ဒီလိုလေး manual event မှတ်ထားလိုက်တယ်။

override fun trackScreen(canTrackScreen: CanTrackScreen) {
  val bundle = Bundle()
  bundle.putString(FirebaseAnalytics.Param.SCREEN_NAME, canTrackScreen.screenName)
  bundle.putString(FirebaseAnalytics.Param.SCREEN_CLASS, canTrackScreen.screenName)
  analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, bundle)
}

Crashlytics အတွက်လည်း ဒီလိုပဲ။ ကျွန်တော်တို့က User ဘယ်ကနေဘာသွားတယ်ဆိုတာ သိမှ Crash Log ကိုပြန်ကြည့်လို့ရမှာဖြစ်တယ်။ ကြားဖူးမှာပါ တော်ထဲက ချောကလက်အိမ်ဇာတ်လမ်းကို၊ ဇတ်ကောင်က အိမ်ပြန်ဖို့ လမ်းမှာ ပေါင်မုန့်အစလေးတွေချခဲ့တယ်။ ဒါကို အဆွဲပြုပြီး ဒီလို Crash Log မှာ User ဘာတွေလုပ်ခဲ့လဲဆိုတဲ့ လမ်းကြောင်းမှတ်တာကို “Breadcrumb” လို့ခေါ်တယ်။ ဆိုတော့ ကျွန်တော်တို့ကလည်း Breadcrumb လေးတွေ မှတ်ပေးတဲ့ BreadcrumbControllerChangeHandler တစ်ခုရေးထားတယ်။

val fromControllerName = from?.let(::getControllerName)
val toControllerName = to?.let(::getControllerName)
val arguments = to?.let { to.args }
FirebaseCrashlytics.getInstance().log(
  "Controller Navigation : From $fromControllerName " +
    "To $toControllerName " +
    "With arguments: $arguments " +
    "And push set to $isPush"
)

ဒါဆိုရင် Crash ထဲမှာလည်း ဘယ်ကဘာသွားခဲ့တယ်ဆိုတာသိရင်တော့ retrace လုပ်ဖို့ ပိုလွယ်သွားတယ်။ Manual ရေးရပေမဲ့ ကျွန်တော့်အတွက်က Fragment Nav XML ကြီးနဲ့ ခေါင်းမစားရတာကို ကျေနပ်တယ်။

အပိုင်း(၁) ကတော့ ဒီလောက်ပဲ။ ကျွန်တော်တို့ရေးထားတဲ့ Single Activity Architecture ကို လေ့လာကြည့်လိုရပါတယ်။ Code တွေက Github မှာ Open Source ပေးထားပါတယ်။ mVoter 2020 ကို Download လုပ်မယ်ဆိုရင်တော့ ကျွန်တော်တို့ mvoterapp.com မှာ အခမဲ့ရယူလိုရပါတယ်။


ကျွန်တော်ရေးတဲ့၊ ပြောတဲ့ အကြောင်းတွေသဘာကြတယ်ဆို ကျွန်တော်နဲ့ ကိုလင်းဖြိုးပေါင်းလုပ်နေတဲ့ ပထမဆုံး ဗမာလိုပြောတဲ့ နည်းပညာ Podcast ဖြစ်တဲ့ Techshaw Podcast လေးရှိပါတယ်။ ကျွန်တော်တို့တွေ နည်းပညာအကြောင်းတွေ အစုံအလင် ပြောဖြစ်ပါတယ်။ Follow မလုပ်ရသေးရင် လုပ်လိုက်ဦးနော်။