mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-11-22 06:37:30 +00:00
Update ProfileFragmentViewModel
This commit is contained in:
parent
b1628492f5
commit
1ebf7a2e4b
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user