Update ProfileFragmentViewModel

This commit is contained in:
Ammar Githam 2021-06-23 20:52:45 +09:00
parent b1628492f5
commit 1ebf7a2e4b
5 changed files with 214 additions and 54 deletions

View File

@ -192,6 +192,7 @@ dependencies {
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
// Room
def room_version = "2.3.0"
@ -244,6 +245,7 @@ dependencies {
testImplementation "androidx.test:core-ktx:1.3.0"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.robolectric:robolectric:4.5.1"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
androidTestImplementation 'androidx.test:core:1.3.0'

View File

@ -6,11 +6,12 @@ import androidx.savedstate.SavedStateRegistryOwner
import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.webservices.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
class ProfileFragmentViewModel(
state: SavedStateHandle,
@ -21,12 +22,61 @@ class ProfileFragmentViewModel(
graphQLRepository: GraphQLRepository,
accountRepository: AccountRepository,
favoriteRepository: FavoriteRepository,
ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val _profile = MutableLiveData<Resource<User?>>(Resource.loading(null))
private val _isLoggedIn = MutableLiveData(false)
private val _currentUser = MutableLiveData<Resource<User?>>(Resource.loading(null))
private var messageManager: DirectMessagesManager? = null
val profile: LiveData<Resource<User?>> = _profile
val currentUser: LiveData<Resource<User?>> = _currentUser
val isLoggedIn: LiveData<Boolean> = currentUser.map { it.data != null }
private val currentUserAndStateUsernameLiveData: LiveData<Pair<Resource<User?>, Resource<String?>>> =
object : MediatorLiveData<Pair<Resource<User?>, Resource<String?>>>() {
var user: Resource<User?> = Resource.loading(null)
var stateUsername: Resource<String?> = Resource.loading(null)
init {
addSource(currentUser) { currentUser ->
this.user = currentUser
value = currentUser to stateUsername
}
addSource(state.getLiveData<String?>("username")) { username ->
this.stateUsername = Resource.success(username)
value = user to this.stateUsername
}
// trigger currentUserAndStateUsernameLiveData switch map with a state username success resource
if (!state.contains("username")) {
this.stateUsername = Resource.success(null)
value = user to this.stateUsername
}
}
}
val profile: LiveData<Resource<User?>> = currentUserAndStateUsernameLiveData.switchMap {
val (userResource, stateUsernameResource) = it
liveData<Resource<User?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
if (userResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null))
return@liveData
}
val user = userResource.data
val stateUsername = stateUsernameResource.data
if (stateUsername.isNullOrBlank()) {
emit(Resource.success(user))
return@liveData
}
try {
val fetchedUser = if (user != null) {
userRepository.getUsernameInfo(stateUsername) // logged in
} else {
graphQLRepository.fetchUser(stateUsername) // anonymous
}
emit(Resource.success(fetchedUser))
} catch (e: Exception) {
emit(Resource.error(e.message, null))
}
}
}
/**
* Username of profile without '`@`'
@ -37,30 +87,12 @@ class ProfileFragmentViewModel(
Resource.Status.SUCCESS -> it.data?.username ?: ""
}
}
val isLoggedIn: LiveData<Boolean> = _isLoggedIn
var currentUser: Resource<User?>? = null
set(value) {
_isLoggedIn.postValue(value?.data != null)
// if no profile, and value is valid, set it as profile
val profileValue = profile.value
if (
profileValue?.status != Resource.Status.LOADING
&& profileValue?.data == null
&& value?.status == Resource.Status.SUCCESS
&& value.data != null
) {
_profile.postValue(Resource.success(value.data))
}
field = value
}
init {
// Log.d(TAG, "${state.keys()} $userRepository $friendshipRepository $storiesRepository $mediaRepository")
val usernameFromState = state.get<String?>("username")
if (usernameFromState.isNullOrBlank()) {
_profile.postValue(Resource.success(null))
}
}
fun setCurrentUser(currentUser: Resource<User?>) {
_currentUser.postValue(currentUser)
}
fun shareDm(result: RankedRecipient) {
@ -104,6 +136,7 @@ class ProfileFragmentViewModelFactory(
graphQLRepository,
accountRepository,
favoriteRepository,
Dispatchers.IO,
) as T
}
}

View File

@ -12,7 +12,7 @@ import org.json.JSONObject
import java.util.*
class GraphQLRepository(private val service: GraphQLService) {
open class GraphQLRepository(private val service: GraphQLService) {
// TODO convert string response to a response class
private suspend fun fetch(
@ -176,7 +176,7 @@ class GraphQLRepository(private val service: GraphQLService) {
}
// TODO convert string response to a response class
suspend fun fetchUser(
open suspend fun fetchUser(
username: String,
): User {
val response = service.getUser(username)

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package awais.instagrabber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* MainCoroutineRule installs a TestCoroutineDispatcher for Disptachers.Main.
*
* Since it extends TestCoroutineScope, you can directly launch coroutines on the MainCoroutineRule
* as a [CoroutineScope]:
*
* ```
* mainCoroutineRule.launch { aTestCoroutine() }
* ```
*
* All coroutines started on [MainCoroutineScopeRule] must complete (including timeouts) before the test
* finishes, or it will throw an exception.
*
* When using MainCoroutineRule you should always invoke runBlockingTest on it to avoid creating two
* instances of [TestCoroutineDispatcher] or [TestCoroutineScope] in your test:
*
* ```
* @Test
* fun usingRunBlockingTest() = mainCoroutineRule.runBlockingTest {
* aTestCoroutine()
* }
* ```
*
* You may call [DelayController] methods on [MainCoroutineScopeRule] and they will control the
* virtual-clock.
*
* ```
* mainCoroutineRule.pauseDispatcher()
* // do some coroutines
* mainCoroutineRule.advanceUntilIdle() // run all pending coroutines until the dispatcher is idle
* ```
*
* By default, [MainCoroutineScopeRule] will be in a *resumed* state.
*
* @param dispatcher if provided, this [TestCoroutineDispatcher] will be used.
*/
@ExperimentalCoroutinesApi
class MainCoroutineScopeRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
TestWatcher(),
TestCoroutineScope by TestCoroutineScope(dispatcher) {
override fun starting(description: Description?) {
super.starting(description)
// If your codebase allows the injection of other dispatchers like
// Dispatchers.Default and Dispatchers.IO, consider injecting all of them here
// and renaming this class to `CoroutineScopeRule`
//
// All injected dispatchers in a test should point to a single instance of
// TestCoroutineDispatcher.
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}

View File

@ -3,6 +3,7 @@ package awais.instagrabber.viewmodels
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import awais.instagrabber.MainCoroutineScopeRule
import awais.instagrabber.common.*
import awais.instagrabber.db.datasources.AccountDataSource
import awais.instagrabber.db.datasources.FavoriteDataSource
@ -12,6 +13,7 @@ import awais.instagrabber.getOrAwaitValue
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.webservices.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
@ -21,12 +23,22 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class ProfileFragmentViewModelTest {
private val testPublicUser = User(
pk = 100,
username = "test",
fullName = "Test user"
)
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@ExperimentalCoroutinesApi
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@ExperimentalCoroutinesApi
@Test
fun testNoUsernameNoCurrentUser() {
val accountDataSource = AccountDataSource(AccountDaoAdapter())
val viewModel = ProfileFragmentViewModel(
SavedStateHandle(),
UserRepository(UserServiceAdapter()),
@ -34,46 +46,75 @@ internal class ProfileFragmentViewModelTest {
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(accountDataSource),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter()))
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
coroutineScope.dispatcher,
)
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
viewModel.setCurrentUser(Resource.success(null))
assertNull(viewModel.profile.getOrAwaitValue().data)
assertEquals("", viewModel.username.getOrAwaitValue())
viewModel.currentUser = Resource.success(null)
viewModel.setCurrentUser(Resource.success(null))
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
}
@ExperimentalCoroutinesApi
@Test
fun testNoUsernameWithCurrentUser() {
// val state = SavedStateHandle(
// mutableMapOf<String, Any?>(
// "username" to "test"
// )
// )
val userRepository = UserRepository(UserServiceAdapter())
val friendshipRepository = FriendshipRepository(FriendshipServiceAdapter())
val storiesRepository = StoriesRepository(StoriesServiceAdapter())
val mediaRepository = MediaRepository(MediaServiceAdapter())
val graphQLRepository = GraphQLRepository(GraphQLServiceAdapter())
val accountDataSource = AccountDataSource(AccountDaoAdapter())
val accountRepository = AccountRepository(accountDataSource)
val favoriteRepository = FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter()))
val viewModel = ProfileFragmentViewModel(
SavedStateHandle(),
userRepository,
friendshipRepository,
storiesRepository,
mediaRepository,
graphQLRepository,
accountRepository,
favoriteRepository
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
coroutineScope.dispatcher,
)
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
assertNull(viewModel.profile.getOrAwaitValue().data)
val user = User()
viewModel.currentUser = Resource.success(user)
viewModel.setCurrentUser(Resource.success(user))
assertEquals(true, viewModel.isLoggedIn.getOrAwaitValue())
assertEquals(user, viewModel.profile.getOrAwaitValue().data)
var profile = viewModel.profile.getOrAwaitValue()
while (profile.status == Resource.Status.LOADING) {
profile = viewModel.profile.getOrAwaitValue()
}
assertEquals(user, profile.data)
}
@ExperimentalCoroutinesApi
@Test
fun testPublicUsernameWithNoCurrentUser() {
// username without `@`
val state = SavedStateHandle(
mutableMapOf<String, Any?>(
"username" to testPublicUser.username
)
)
val graphQLRepository = object : GraphQLRepository(GraphQLServiceAdapter()) {
override suspend fun fetchUser(username: String): User {
return testPublicUser
}
}
val viewModel = ProfileFragmentViewModel(
state,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(null))
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
var profile = viewModel.profile.getOrAwaitValue()
while (profile.status == Resource.Status.LOADING) {
profile = viewModel.profile.getOrAwaitValue()
}
assertEquals(testPublicUser, profile.data)
}
}