feat(creator): 채널 홈 ViewModel을 추가한다
This commit is contained in:
@@ -176,6 +176,7 @@ import kr.co.vividnext.sodalive.user.UserViewModel
|
|||||||
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
|
||||||
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
import kr.co.vividnext.sodalive.user.login.LoginViewModel
|
||||||
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModel
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeApi
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeApi
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeRepository
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeRepository
|
||||||
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
|
||||||
@@ -407,6 +408,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
viewModel { DmChatRoomViewModel(get()) }
|
viewModel { DmChatRoomViewModel(get()) }
|
||||||
viewModel { HomeCreatorRankingViewModel(get()) }
|
viewModel { HomeCreatorRankingViewModel(get()) }
|
||||||
viewModel { HomeRecommendationViewModel(get()) }
|
viewModel { HomeRecommendationViewModel(get()) }
|
||||||
|
viewModel { CreatorChannelHomeViewModel(get()) }
|
||||||
viewModel { PushNotificationListViewModel(get()) }
|
viewModel { PushNotificationListViewModel(get()) }
|
||||||
viewModel { CharacterTabViewModel(get()) }
|
viewModel { CharacterTabViewModel(get()) }
|
||||||
viewModel { CharacterDetailViewModel(get()) }
|
viewModel { CharacterDetailViewModel(get()) }
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.toUiContent
|
||||||
|
|
||||||
|
class CreatorChannelHomeViewModel(
|
||||||
|
private val repository: CreatorChannelHomeRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val _homeStateLiveData = MutableLiveData<CreatorChannelHomeUiState>()
|
||||||
|
val homeStateLiveData: LiveData<CreatorChannelHomeUiState>
|
||||||
|
get() = _homeStateLiveData
|
||||||
|
|
||||||
|
private val _toastLiveData = MutableLiveData<ToastMessage?>()
|
||||||
|
val toastLiveData: LiveData<ToastMessage?>
|
||||||
|
get() = _toastLiveData
|
||||||
|
|
||||||
|
private val _chatRoomIdLiveData = MutableLiveData<CreatorChannelEvent<Long>>()
|
||||||
|
val chatRoomIdLiveData: LiveData<CreatorChannelEvent<Long>>
|
||||||
|
get() = _chatRoomIdLiveData
|
||||||
|
|
||||||
|
private var isFollowInProgress = false
|
||||||
|
private var isCreateChatRoomInProgress = false
|
||||||
|
|
||||||
|
fun loadHome(creatorId: Long) {
|
||||||
|
if (creatorId <= 0) return
|
||||||
|
|
||||||
|
_homeStateLiveData.value = CreatorChannelHomeUiState.Loading
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getHome(creatorId = creatorId, token = authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
val data = it.data
|
||||||
|
if (it.success && data != null) {
|
||||||
|
_homeStateLiveData.value = data.toUiContent()
|
||||||
|
} else {
|
||||||
|
showUnknownError(it.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
showUnknownError(it.message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun follow(follow: Boolean, notify: Boolean) {
|
||||||
|
val content = _homeStateLiveData.value as? CreatorChannelHomeUiState.Content ?: return
|
||||||
|
if (isFollowInProgress) return
|
||||||
|
|
||||||
|
isFollowInProgress = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.followCreator(
|
||||||
|
creatorId = content.header.creatorId,
|
||||||
|
follow = follow,
|
||||||
|
notify = notify,
|
||||||
|
token = authToken()
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
isFollowInProgress = false
|
||||||
|
if (it.success) {
|
||||||
|
_homeStateLiveData.value = content.copy(
|
||||||
|
header = content.header.copy(isFollow = follow, isNotify = notify)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showUnknownErrorToast()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isFollowInProgress = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
showUnknownErrorToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createChatRoom(characterId: Long) {
|
||||||
|
if (characterId <= 0 || isCreateChatRoomInProgress) return
|
||||||
|
|
||||||
|
isCreateChatRoomInProgress = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.createChatRoom(characterId = characterId, token = authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
isCreateChatRoomInProgress = false
|
||||||
|
val data = it.data
|
||||||
|
if (it.success && data != null) {
|
||||||
|
_chatRoomIdLiveData.value = CreatorChannelEvent(data.chatRoomId)
|
||||||
|
} else {
|
||||||
|
showUnknownErrorToast()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCreateChatRoomInProgress = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
showUnknownErrorToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUnknownError(message: String?) {
|
||||||
|
_homeStateLiveData.value = CreatorChannelHomeUiState.Error(message = message)
|
||||||
|
showUnknownErrorToast()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUnknownErrorToast() {
|
||||||
|
_toastLiveData.value = ToastMessage(resId = R.string.common_error_unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreatorChannelEvent<out T>(private val value: T) {
|
||||||
|
private var consumed: Boolean = false
|
||||||
|
|
||||||
|
fun consume(): T? {
|
||||||
|
if (consumed) return null
|
||||||
|
consumed = true
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||||
|
import io.reactivex.rxjava3.core.Scheduler
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelActivityResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCreatorResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelFanTalkSummaryResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSnsResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.never
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class CreatorChannelHomeViewModelTest {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
private lateinit var repository: CreatorChannelHomeRepository
|
||||||
|
private lateinit var viewModel: CreatorChannelHomeViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
setImmediateRxSchedulers()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
SharedPreferenceManager.init(context)
|
||||||
|
SharedPreferenceManager.token = "test-token"
|
||||||
|
repository = mock()
|
||||||
|
viewModel = CreatorChannelHomeViewModel(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `creatorId가 0 이하이면 홈 API를 호출하지 않는다`() {
|
||||||
|
viewModel.loadHome(0L)
|
||||||
|
|
||||||
|
verify(repository, never()).getHome(any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `유효한 creatorId 로드는 bearer token으로 홈 API를 호출하고 Content를 emit한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
|
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||||
|
assertEquals(100L, state.header.creatorId)
|
||||||
|
verify(repository).getHome(100L, "Bearer test-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `홈 API failure는 Error와 unknown toast를 emit한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(false, null, "failed")))
|
||||||
|
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
assertTrue(viewModel.homeStateLiveData.requireValue() is CreatorChannelHomeUiState.Error)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `홈 API null data는 Error와 unknown toast를 emit한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, null, null)))
|
||||||
|
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
assertTrue(viewModel.homeStateLiveData.requireValue() is CreatorChannelHomeUiState.Error)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `홈 API throwable은 Error와 unknown toast를 emit한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.error(IllegalStateException("network")))
|
||||||
|
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Error
|
||||||
|
assertEquals("network", state.message)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팔로우 성공은 현재 content header의 follow와 notify를 갱신한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(
|
||||||
|
Single.just(ApiResponse(true, response(isFollow = false), null))
|
||||||
|
)
|
||||||
|
whenever(repository.followCreator(100L, true, true, "Bearer test-token")).thenReturn(
|
||||||
|
Single.just(ApiResponse(true, Any(), null))
|
||||||
|
)
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
viewModel.follow(follow = true, notify = true)
|
||||||
|
|
||||||
|
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||||
|
assertTrue(state.header.isFollow)
|
||||||
|
assertTrue(state.header.isNotify)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팔로우 진행 중 중복 요청은 무시한다`() {
|
||||||
|
val pending = SingleSubject.create<ApiResponse<Any>>()
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
|
whenever(repository.followCreator(100L, false, false, "Bearer test-token")).thenReturn(pending)
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
viewModel.follow(follow = false, notify = false)
|
||||||
|
viewModel.follow(follow = false, notify = false)
|
||||||
|
|
||||||
|
verify(repository, times(1)).followCreator(100L, false, false, "Bearer test-token")
|
||||||
|
pending.onSuccess(ApiResponse(true, Any(), null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `팔로우 실패는 현재 content를 유지하고 unknown toast만 emit한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
|
whenever(repository.followCreator(100L, false, false, "Bearer test-token")).thenReturn(
|
||||||
|
Single.just(ApiResponse(false, null, "failed"))
|
||||||
|
)
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
viewModel.follow(follow = false, notify = false)
|
||||||
|
|
||||||
|
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||||
|
assertEquals(100L, state.header.creatorId)
|
||||||
|
assertTrue(state.header.isFollow)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅방 생성은 characterId가 유효할 때만 호출하고 chatRoomId를 emit한다`() {
|
||||||
|
whenever(repository.createChatRoom(200L, "Bearer test-token"))
|
||||||
|
.thenReturn(Single.just(ApiResponse(true, CreateChatRoomResponse(chatRoomId = 300L), null)))
|
||||||
|
|
||||||
|
viewModel.createChatRoom(0L)
|
||||||
|
viewModel.createChatRoom(200L)
|
||||||
|
|
||||||
|
verify(repository, never()).createChatRoom(eq(0L), any())
|
||||||
|
verify(repository).createChatRoom(200L, "Bearer test-token")
|
||||||
|
val event = viewModel.chatRoomIdLiveData.requireValue()
|
||||||
|
assertEquals(300L, event?.consume())
|
||||||
|
assertEquals(null, event?.consume())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅방 생성 진행 중 중복 요청은 무시한다`() {
|
||||||
|
val pending = SingleSubject.create<ApiResponse<CreateChatRoomResponse>>()
|
||||||
|
whenever(repository.createChatRoom(200L, "Bearer test-token")).thenReturn(pending)
|
||||||
|
|
||||||
|
viewModel.createChatRoom(200L)
|
||||||
|
viewModel.createChatRoom(200L)
|
||||||
|
|
||||||
|
verify(repository, times(1)).createChatRoom(200L, "Bearer test-token")
|
||||||
|
pending.onSuccess(ApiResponse(true, CreateChatRoomResponse(chatRoomId = 300L), null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅방 생성 실패는 현재 content를 유지하고 unknown toast만 emit한다`() {
|
||||||
|
whenever(repository.getHome(100L, "Bearer test-token")).thenReturn(Single.just(ApiResponse(true, response(), null)))
|
||||||
|
whenever(repository.createChatRoom(200L, "Bearer test-token")).thenReturn(
|
||||||
|
Single.just(ApiResponse(false, null, "failed"))
|
||||||
|
)
|
||||||
|
viewModel.loadHome(100L)
|
||||||
|
|
||||||
|
viewModel.createChatRoom(200L)
|
||||||
|
|
||||||
|
val state = viewModel.homeStateLiveData.requireValue() as CreatorChannelHomeUiState.Content
|
||||||
|
assertEquals(100L, state.header.creatorId)
|
||||||
|
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setImmediateRxSchedulers() {
|
||||||
|
val trampoline = { _: Scheduler -> Schedulers.trampoline() }
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler(trampoline)
|
||||||
|
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun response(isFollow: Boolean = true) = CreatorChannelHomeResponse(
|
||||||
|
creator = CreatorChannelCreatorResponse(
|
||||||
|
creatorId = 100L,
|
||||||
|
characterId = 200L,
|
||||||
|
nickname = "소다",
|
||||||
|
profileImageUrl = "https://example.com/profile.png",
|
||||||
|
followerCount = 1234,
|
||||||
|
isAiChatAvailable = true,
|
||||||
|
isDmAvailable = true,
|
||||||
|
isFollow = isFollow,
|
||||||
|
isNotify = false
|
||||||
|
),
|
||||||
|
currentLive = CreatorChannelLiveResponse(1L, "라이브", null, "2026-06-11T12:00:00Z", 0, false),
|
||||||
|
latestAudioContent = CreatorChannelAudioContentResponse(1L, "오디오", null, null, 0, true, false, null, null),
|
||||||
|
channelDonations = emptyList(),
|
||||||
|
notices = emptyList(),
|
||||||
|
schedules = listOf(CreatorChannelScheduleResponse("2026-06-11T12:00:00Z", "일정", CreatorActivityType.Live, 1L)),
|
||||||
|
audioContents = emptyList(),
|
||||||
|
series = emptyList(),
|
||||||
|
communities = emptyList(),
|
||||||
|
fanTalk = CreatorChannelFanTalkSummaryResponse(0, null),
|
||||||
|
introduce = "",
|
||||||
|
activity = CreatorChannelActivityResponse(null, "D+1", 1, 2, 3, 4, 5),
|
||||||
|
sns = CreatorChannelSnsResponse("", "", "", "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun <T> LiveData<T>.requireValue(): T? {
|
||||||
|
var value: T? = null
|
||||||
|
val observer = Observer<T> { value = it }
|
||||||
|
observeForever(observer)
|
||||||
|
removeObserver(observer)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user