feat: APP内容配置重构-附属需求:弹窗顺序 https://jira.shanqu.cc/browse/GHZS-4086

This commit is contained in:
chenjuntao
2024-01-22 17:31:54 +08:00
parent 20d1b86842
commit 7ec9bf08e2
24 changed files with 250 additions and 46 deletions

View File

@ -42,7 +42,7 @@ class AccelerateNotificationHandler(priority: Int) : PriorityChainHandler(priori
processNext()
} else {
updateStatus(STATUS_VALID)
onProcess()
process()
}
} else {
if (gameEntityList == null) {

View File

@ -30,7 +30,7 @@ class CustomFloatingWindowHandler(priority: Int) : PriorityChainHandler(priority
if (getStatus() == STATUS_PENDING) {
if (data.isNotEmpty()) {
updateStatus(STATUS_VALID)
onProcess()
process()
} else {
processNext()
}

View File

@ -42,7 +42,7 @@ class FloatingWindowHandler(priority: Int) : PriorityChainHandler(priority) {
if (getStatus() == STATUS_PENDING) {
if (!mWindowList.isNullOrEmpty()) {
updateStatus(STATUS_VALID)
onProcess()
process()
} else {
processNext()
}

View File

@ -45,11 +45,9 @@ object GlobalPriorityChainHelper : ISuperiorChain {
}
/**
* 启动优先级弹窗管理链的执行
* 启动所有的优先级弹窗管理链
*/
fun start() {
if (!mainChain.isHandlerQueueEmpty()) return
fun preStart() {
val launchRedirectHandler = LaunchRedirectHandler(-101)
val updateDialogHandler = UpdateDialogHandler(-100)
val privacyPolicyDialogHandler = PrivacyPolicyDialogHandler(-99)
@ -68,6 +66,13 @@ object GlobalPriorityChainHelper : ISuperiorChain {
updateDialogHandler.doPreProcess()
requestOpeningDialogData(welcomeDialogHandler, privacyPolicyDialogHandler)
requestReserveDialogData(reserveDialogHandler)
}
/**
* 启动优先级弹窗管理链的执行
*/
fun start() {
if (mainChain.isHandlerQueueEmpty()) return
mainChain.start()

View File

@ -15,7 +15,7 @@ class HomePushHandler(priority: Int): PriorityChainHandler(priority) {
if (getStatus() == STATUS_PENDING) {
if (shouldShow && smartRefreshFragment != null) {
updateStatus(STATUS_VALID)
onProcess()
process()
} else {
processNext()
}

View File

@ -10,6 +10,7 @@ import com.gh.gamecenter.common.utils.singleToMain
import com.gh.gamecenter.core.AppExecutor
import com.gh.gamecenter.core.utils.CurrentActivityHolder
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.gamecenter.wrapper.MainWrapperRepository
import com.halo.assistant.HaloApp
/**
@ -21,6 +22,7 @@ class LaunchRedirectHandler(priority: Int) : PriorityChainHandler(priority) {
@SuppressLint("CheckResult")
fun doPreProcess() {
// if (true) {
if (HaloApp.getInstance().isBrandNewInstall) {
RetrofitManager.getInstance().newApi
.getLaunchRedirect(BuildConfig.VERSION_NAME, HaloApp.getInstance().channel)
@ -29,9 +31,20 @@ class LaunchRedirectHandler(priority: Int) : PriorityChainHandler(priority) {
override fun onSuccess(data: LaunchRedirectWrapper) {
launchData = data.launchRedirect
if (launchData == null) {
updateStatus(STATUS_INVALID)
processNext()
return
}
// 提前设置 tab default 避免首页数据加载完成tab 选中会闪烁
if (launchData?.type == "bottom_tab") {
MainWrapperRepository.getInstance().sendSelectMainTabEvent(launchData!!)
}
if (getStatus() == STATUS_PENDING) {
updateStatus(STATUS_VALID)
onProcess()
process()
} else {
updateStatus(STATUS_VALID)
}

View File

@ -21,7 +21,10 @@ class PriorityChain(private val completeCallback: (() -> Unit)? = null) {
* 启动队列中的 handler
*/
fun start() {
handlerQueue.peek()?.process(handlerQueue)
handlerQueue.peek()?.let {
it.injectQueue(handlerQueue)
it.process()
}
}
/**
@ -33,7 +36,7 @@ class PriorityChain(private val completeCallback: (() -> Unit)? = null) {
if (handler?.getStatus() == PriorityChainHandler.STATUS_HANDLING) {
Utils.log(PriorityChainHandler.TAG, "${handler.javaClass.simpleName} 处于执行中状态,不用恢复")
} else {
handler?.onProcess()
handler?.process()
}
}

View File

@ -30,15 +30,23 @@ abstract class PriorityChainHandler(private val mPriority: Int) : Comparable<Pri
this.priorityChain = priorityChain
}
fun process(queue: Queue<PriorityChainHandler>) {
fun injectQueue(queue: Queue<PriorityChainHandler>) {
this.queue = queue
}
fun process() {
Utils.log(TAG, "${javaClass.simpleName} process $status")
this.queue = queue
// 若当前 handler 未经处理,将其状态改为 pending
if (status == STATUS_UNKNOWN) {
updateStatus(STATUS_PENDING)
}
if (status == STATUS_HANDLING) {
Utils.log(TAG, "${javaClass.simpleName} 已经处于执行中状态,不用再次执行")
return
}
val isHandling = onProcess()
if (isHandling) {
@ -62,7 +70,10 @@ abstract class PriorityChainHandler(private val mPriority: Int) : Comparable<Pri
if (queue?.isEmpty() == true) {
priorityChain?.onHandleComplete()
} else {
queue?.peek()?.process(queue!!)
queue?.peek()?.let {
it.injectQueue(queue!!)
it.process()
}
}
}

View File

@ -20,7 +20,7 @@ class PrivacyPolicyDialogHandler(priority: Int) : PriorityChainHandler(priority)
processNext()
} else {
updateStatus(STATUS_VALID)
onProcess()
process()
}
} else {
if (privacyPolicyEntity == null) {

View File

@ -20,7 +20,7 @@ class ReserveDialogHandler(priority: Int) : PriorityChainHandler(priority) {
processNext()
} else {
updateStatus(STATUS_VALID)
onProcess()
process()
}
} else {
if (reserveData.isNullOrEmpty()) {

View File

@ -11,7 +11,7 @@ class UpdateDialogHandler(priority: Int) : PriorityChainHandler(priority) {
if (getStatus() == STATUS_PENDING) {
if (UpdateHelper.isUpdateValid(false)) {
updateStatus(STATUS_VALID)
onProcess()
process()
} else {
updateStatus(STATUS_INVALID)
processNext()

View File

@ -25,7 +25,7 @@ class WelcomeDialogHandler(priority: Int) : PriorityChainHandler(priority) {
override fun onFirst(first: Bitmap) {
if (getStatus() == STATUS_PENDING) {
updateStatus(STATUS_VALID)
onProcess()
process()
} else {
updateStatus(STATUS_VALID)
}

View File

@ -93,6 +93,7 @@ import com.gh.gamecenter.video.detail.VideoDetailContainerViewModel
import com.gh.gamecenter.video.game.GameVideoActivity
import com.gh.gamecenter.video.videomanager.VideoManagerActivity
import com.gh.gamecenter.wrapper.MainWrapperFragment
import com.gh.gamecenter.wrapper.MainWrapperRepository
import com.gh.gamecenter.wrapper.ToolbarWrapperActivity
import com.gh.vspace.VDownloadManagerActivity
import com.halo.assistant.HaloApp
@ -473,11 +474,14 @@ object DirectUtils {
// 选中首页底部 tab
"bottom_tab" -> {
// TODO 通知首页选中具体指定 tab
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
}
context.startActivity(intent)
if (linkEntity is LaunchRedirect) {
MainWrapperRepository.getInstance().sendSelectMainTabEvent(linkEntity)
}
}
"" -> {

View File

@ -23,7 +23,7 @@ data class BottomTab(
@SerializedName("search_style")
val searchStyle: SearchStyle = SearchStyle(), // 搜索样式
@SerializedName("is_default_page")
val default: Boolean = false, // 是否为默认显示页
var default: Boolean = false, // 是否为默认显示页
var isTransparentStyle: Boolean = false // 本地字段透明底部Tab
): Parcelable {
@Parcelize

View File

@ -18,7 +18,7 @@ data class MultiTabNav(
@SerializedName("tab_show_img")
var showImgOnSelected: Boolean? = false, // 选中时是否显示图片
@SerializedName("is_default_page")
val default: Boolean = false, // 是否为默认显示页
var default: Boolean = false, // 是否为默认显示页
val link: LinkEntity? = null, // 通用链接
// 本地字段

View File

@ -66,6 +66,17 @@ class MainWrapperFragment : BaseBottomTabFragment<PieceBottomTabBinding>(), OnBa
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel?.tabSelectedLiveData?.observe(viewLifecycleOwner) {
val selectedTab = it.getContentWithHandled()
if (selectedTab is MainSelectedEvent.SelectedTab && selectedTab.bottomTabIndex != -1) {
setCurrentItem(selectedTab.bottomTabIndex)
}
}
}
fun setCurrentItem(page: Int) {
if (page < mBottomTabList.size) {
mViewPager?.setCurrentItem(page, false)

View File

@ -3,6 +3,7 @@ package com.gh.gamecenter.wrapper
import android.annotation.SuppressLint
import androidx.lifecycle.MutableLiveData
import com.gh.common.util.HomeBottomBarHelper
import com.gh.gamecenter.common.entity.LaunchRedirect
import com.gh.gamecenter.common.retrofit.BiResponse
import com.gh.gamecenter.common.utils.singleToMain
import com.gh.gamecenter.core.utils.SingletonHolder
@ -10,9 +11,18 @@ import com.gh.gamecenter.entity.*
import com.gh.gamecenter.home.custom.model.CustomPageData
import com.gh.gamecenter.home.custom.model.CustomPageItem
import com.gh.gamecenter.retrofit.RetrofitManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
class MainWrapperRepository {
private val mNewApi = RetrofitManager.getInstance().newApi
private var mIsMainDataReceived = false // 首页数据是否已加载
private var mHighPrioritySelectedBottomTabId = "" // 首页底部 tab 选中的id (优先级比 default 高)
private var mHighPrioritySelectedTopTabId = "" // 首页顶部 tab 选中的id (优先级比 default 高)
var defaultNavId = ""
var defaultCustomPageId = ""
@ -23,12 +33,60 @@ class MainWrapperRepository {
val customPageLiveData = MutableLiveData<CustomPageData?>()
val errorLiveData = MutableLiveData<Exception>()
private val mTabSelectEventFlow = MutableSharedFlow<MainSelectedEvent>()
val tabSelectEventFlow = mTabSelectEventFlow as SharedFlow<MainSelectedEvent>
/**
* 发送首页子页面选中事件
*/
fun sendSelectMainTabEvent(launchRedirect: LaunchRedirect) {
if (!mIsMainDataReceived) {
mHighPrioritySelectedBottomTabId = launchRedirect.link ?: ""
mHighPrioritySelectedTopTabId = launchRedirect.topTabLink?.link ?: ""
} else {
CoroutineScope(SupervisorJob()).launch {
val bottomTabIndex = bottomTabListLiveData.value?.indexOfFirst {
it.id == launchRedirect.link
} ?: -1
val topTabIndex = multiTabNavLiveData.value?.linkMultiTabNav?.indexOfFirst {
it.id == launchRedirect.topTabLink?.link
} ?: -1
mTabSelectEventFlow.emit(
MainSelectedEvent.SelectedTab(
bottomTabIndex = bottomTabIndex,
navTabId = launchRedirect.navTabLink?.link ?: "",
topTabIndex = topTabIndex,
topTabId = launchRedirect.topTabLink?.link ?: ""
)
)
}
}
}
/**
* 发送首页子页面选中事件
* @param bottomTabIndex 底部 tab 选中的 index
* @param topTabIndex 顶部 tab 选中的 index
*/
fun sendSelectMainTabEvent(bottomTabIndex: Int, topTabIndex: Int) {
CoroutineScope(SupervisorJob()).launch {
mTabSelectEventFlow.emit(
MainSelectedEvent.SelectedTab(
bottomTabIndex = bottomTabIndex,
topTabIndex = topTabIndex
)
)
}
}
@SuppressLint("CheckResult")
fun getDataUnion() {
mNewApi.dataUnion
.compose(singleToMain())
.subscribe(object : BiResponse<DataUnionEntity>() {
override fun onSuccess(data: DataUnionEntity) {
mIsMainDataReceived = true
processBottomTabData(data.bottomTab)
processMultiTabData(data.multiTabNav)
customPageLiveData.postValue(data.customPage?.apply {
@ -59,6 +117,23 @@ class MainWrapperRepository {
private fun processBottomTabData(bottomTabList: List<BottomTab>) {
if (bottomTabList.isNotEmpty()) {
HomeBottomBarHelper.updateDefaultHomeBottomTabData(bottomTabList)
// 如果有优先级更高的选中 tab id则将优先级更高的选中 tab 设置为 default
if (mHighPrioritySelectedBottomTabId.isNotEmpty()) {
val selectedTab = bottomTabList.firstOrNull {
it.id == mHighPrioritySelectedBottomTabId
}
// 将优先级更高的选中 tab 设置为 default
if (selectedTab != null) {
bottomTabList.forEach { it.default = false }
selectedTab.default = true
}
mHighPrioritySelectedBottomTabId = ""
}
bottomTabList.forEachIndexed { index, bottomTab ->
if (bottomTab.default) {
defaultBottomTabIndex = index
@ -67,7 +142,7 @@ class MainWrapperRepository {
bottomTab.isTransparentStyle = true
}
}
HomeBottomBarHelper.updateDefaultHomeBottomTabData(bottomTabList)
bottomTabListLiveData.postValue(bottomTabList)
defaultNavId = bottomTabList.find { it.link?.type == "multi_tab_nav" && it.default }?.link?.link ?: ""
defaultCustomPageId = bottomTabList.find { it.link?.type == "custom_page" && it.default }?.link?.link ?: ""
@ -81,12 +156,45 @@ class MainWrapperRepository {
}
private fun processMultiTabData(multiTabNav: MultiTabNav?) {
defaultCustomPageId = multiTabNav?.linkMultiTabNav?.find { it.link?.type == "custom_page" && it.default }?.link?.link ?: ""
// 如果有优先级更高的选中 tab id则将优先级更高的选中 tab 设置为 default
if (mHighPrioritySelectedTopTabId.isNotEmpty()) {
val selectedTab = multiTabNav?.linkMultiTabNav?.firstOrNull {
it.link?.link == mHighPrioritySelectedTopTabId
}
// 将优先级更高的选中 tab 设置为 default
if (selectedTab != null) {
multiTabNav.linkMultiTabNav.forEach { it.default = false }
selectedTab.default = true
}
mHighPrioritySelectedTopTabId = ""
}
defaultCustomPageId =
multiTabNav?.linkMultiTabNav?.find { it.link?.type == "custom_page" && it.default }?.link?.link ?: ""
multiTabNavLiveData.postValue(multiTabNav)
}
companion object : SingletonHolder<MainWrapperRepository>({ MainWrapperRepository() }) {
const val TYPE_TAB_VIDEO = "video_stream"
}
}
sealed class MainSelectedEvent {
/**
* 首页子页面选中
* @param bottomTabIndex 首页底部 tab 选中的 index
* @param navTabId 首页顶部 tab 的 id
* @param topTabIndex 首页顶部 tab 选中的 index
* @param topTabId 首页顶部 tab 的 id
*/
data class SelectedTab(
val bottomTabIndex: Int = -1,
val navTabId: String = "",
val topTabIndex: Int = -1,
val topTabId: String = ""
) : MainSelectedEvent()
}

View File

@ -1,22 +1,17 @@
package com.gh.gamecenter.wrapper
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.alibaba.android.arouter.launcher.ARouter
import androidx.lifecycle.*
import com.gh.common.util.CheckLoginUtils
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.common.retrofit.Response
import com.gh.gamecenter.core.provider.IFloatingWindowProvider
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.WelcomeDialogEntity
import com.gh.gamecenter.floatingwindow.FloatingWindowEntity
import com.gh.gamecenter.livedata.Event
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.retrofit.RetrofitManager
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformWhile
import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.ResponseBody
@ -35,6 +30,14 @@ class MainWrapperViewModel(application: Application, private val mRepository: Ma
val accelerateNotificationPopup = MutableLiveData<List<GameEntity>?>()
val tabSelectedLiveData = mRepository.tabSelectEventFlow
.map { it as? MainSelectedEvent.SelectedTab }
.transformWhile { originalEvent ->
emit(Event(originalEvent))
true
}
.asLiveData()
/**
* 请求各种弹窗的数据
*/

View File

@ -458,6 +458,15 @@ class SearchToolbarTabWrapperFragment : BaseTabWrapperFragment(), ISmartRefresh,
}
initNoTabViewPager()
}
mViewModel.tabSelectLiveData.observe(viewLifecycleOwner) {
val selectTab = it.getContentWithHandled()
if (selectTab is MainSelectedEvent.SelectedTab) {
if (selectTab.topTabIndex != -1) {
mViewPager?.setCurrentItem(selectTab.topTabIndex, false)
}
}
}
}
override fun hideTab() {
@ -1074,7 +1083,9 @@ class SearchToolbarTabWrapperFragment : BaseTabWrapperFragment(), ISmartRefresh,
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(status: EBDownloadStatus) {
setDownloadHint(mPackageViewModel.filterSameUpdateLiveData.value!!)
mPackageViewModel.filterSameUpdateLiveData.value?.let {
setDownloadHint(it)
}
// 下载被删除事件
if ("delete" == status.status) {
if (status.gameId == mPullDownPush?.game?.id) {

View File

@ -1,11 +1,12 @@
package com.gh.gamecenter.wrapper
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.*
import com.gh.gamecenter.common.utils.safelyGetInRelease
import com.gh.gamecenter.entity.MultiTabNav
import com.gh.gamecenter.livedata.Event
import com.halo.assistant.HaloApp
import kotlinx.coroutines.flow.*
class SearchToolbarTabWrapperViewModel(
application: Application,
@ -16,9 +17,31 @@ class SearchToolbarTabWrapperViewModel(
var noTabStyle = MultiTabNav.LinkMultiTabNav.TabStyle()
var appBarOffset = 0
fun isTabCustomPage(position: Int) = tabListLiveData.value?.safelyGetInRelease(position)?.link?.type == "custom_page" || noTabLinkId.isNotEmpty()
val tabSelectLiveData =
repository.tabSelectEventFlow
.map { it as? MainSelectedEvent.SelectedTab }
.transformWhile { originalEvent ->
if (originalEvent?.navTabId == multiTabNavId) {
if (originalEvent.topTabIndex == -1 && originalEvent.topTabId.isNotEmpty()) {
multiTabLoadedFlow.receiveAsFlow().collect { tabList ->
val newTopTabIndex = tabList.indexOfFirst {
it.id == originalEvent.topTabId
}
emit(Event(MainSelectedEvent.SelectedTab(topTabIndex = newTopTabIndex)))
}
} else {
emit(Event(originalEvent))
}
}
true
}
.asLiveData()
fun getCustomPageTabEntity(pageId: String): MultiTabNav.LinkMultiTabNav? = tabListLiveData.value?.find { it.link?.type == "custom_page" && it.link.link == pageId }
fun isTabCustomPage(position: Int) =
tabListLiveData.value?.safelyGetInRelease(position)?.link?.type == "custom_page" || noTabLinkId.isNotEmpty()
fun getCustomPageTabEntity(pageId: String): MultiTabNav.LinkMultiTabNav? =
tabListLiveData.value?.find { it.link?.type == "custom_page" && it.link.link == pageId }
fun enableSlideBackgroundColor(pageId: String, isSlideBackgroundColorEnable: Boolean) {
if (multiTabNavId.isNotEmpty()) {

View File

@ -2,16 +2,16 @@ package com.gh.gamecenter.wrapper
import android.annotation.SuppressLint
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.*
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.singleToMain
import com.gh.gamecenter.common.utils.toColor
import com.gh.gamecenter.entity.MultiTabNav
import com.gh.gamecenter.retrofit.RetrofitManager
import com.halo.assistant.HaloApp
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
open class TabWrapperViewModel(
application: Application,
@ -25,6 +25,8 @@ open class TabWrapperViewModel(
val tabListLiveData = MutableLiveData<List<MultiTabNav.LinkMultiTabNav>>()
val errorLiveData = MutableLiveData<Throwable>()
internal var multiTabLoadedFlow: Channel<List<MultiTabNav.LinkMultiTabNav>> = Channel(Channel.CONFLATED)
init {
val isDefaultPage =
repository.defaultNavId.isNotEmpty() && repository.defaultNavId == mMultiTabNavId && repository.multiTabNavLiveData.value != null
@ -64,6 +66,12 @@ open class TabWrapperViewModel(
}
}
tabListLiveData.postValue(tabList)
// 延迟 10ms让 viewPager 先 setAdapter 再触发页面选中
viewModelScope.launch {
delay(10L)
multiTabLoadedFlow.send(tabList)
}
}
class Factory(private val mMultiTabNavId: String) : ViewModelProvider.NewInstanceFactory() {

View File

@ -30,6 +30,7 @@ import com.gh.ad.AdDelegateHelper;
import com.gh.base.GlobalActivityLifecycleObserver;
import com.gh.common.FixedRateJobHelper;
import com.gh.common.filter.RegionSettingHelper;
import com.gh.common.prioritychain.GlobalPriorityChainHelper;
import com.gh.common.util.ActivationHelper;
import com.gh.common.util.DataUtils;
import com.gh.common.util.DeviceTokenUtils;
@ -334,6 +335,8 @@ public class HaloApp extends MultiDexApplication {
// 必须放在外面,否则不能及时刷新用户数据
UserRepository.getInstance().getLoginUserInfo();
GlobalPriorityChainHelper.INSTANCE.preStart();
MainWrapperRepository.Companion.getInstance().getDataUnion();
AppExecutor.getUiExecutor().executeWithDelay(() -> {

View File

@ -4,12 +4,12 @@ import com.google.gson.annotations.SerializedName
class LaunchRedirectWrapper(
@SerializedName("link")
val launchRedirect: LaunchRedirect
val launchRedirect: LaunchRedirect? = null,
)
class LaunchRedirect(
@SerializedName("multi_tab_nav_link")
val tabNavLink: LinkEntity,
@SerializedName("multi_tab_detail")
val tabDetail: LinkEntity
val navTabLink: LinkEntity? = null,
@SerializedName("multi_tab_link")
val topTabLink: LinkEntity? = null,
) : LinkEntity()

View File

@ -61,6 +61,7 @@ dependencies {
api "com.squareup.retrofit2:retrofit:${retrofit}"
api "com.squareup.retrofit2:converter-gson:${retrofit}" // include gson 2.7
api "com.squareup.retrofit2:adapter-rxjava2:${retrofit}"
api "androidx.lifecycle:lifecycle-runtime-ktx:$lifeCycle"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifeCycle"
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifeCycle"
api "androidx.lifecycle:lifecycle-common-java8:$lifeCycle"