mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 03:25:34 +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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user