Jetpack Compose - store internet connection state in view model - android-jetpack-compose

I have the below code to check the live network connection state.
How can I call the function to start checking for this network connection state from the view model init function?
How can I also make it so if there is a change in internet connection state, it updates this in the view model?
interface ConnectivityObserver {
fun observe(): Flow<Status>
enum class Status {
Available,
Unavailable,
Losing,
Lost
}
}
class NetworkConnectivityObserver(private val context: Context) : ConnectivityObserver {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override fun observe(): Flow<ConnectivityObserver.Status> {
return callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
launch { send(ConnectivityObserver.Status.Available) }
}
override fun onLosing(network: Network, maxMsToLive: Int) {
super.onLosing(network, maxMsToLive)
launch { send(ConnectivityObserver.Status.Losing) }
}
override fun onLost(network: Network) {
super.onLost(network)
launch { send(ConnectivityObserver.Status.Lost) }
}
override fun onUnavailable() {
super.onUnavailable()
launch { send(ConnectivityObserver.Status.Unavailable) }
}
}
connectivityManager.registerDefaultNetworkCallback(callback)
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}.distinctUntilChanged()
}
}
class NetworkStatusViewModel : ViewModel() {
var connectedToInternet: Boolean by mutableStateOf(false)
init {
connectedToInternet = checkNetworkConnection()
}
}
fun checkNetworkConnection(): Boolean {
//Make call here to start checking on network connection and return a boolean
//return true if connected to the internet
//return false if not connected to the internet
}

just create a state variable in your viewmodel such as:
private val _connectivityState = mutableStateOf(Status. Unavailable)
val connectivityState: State<Status> = _connectivityState // this is for exposing UI if you need
then there are two possible scenarios:
1- if you want to do some work in case of connectivity changes, directly use a combine flow and connect your different flows such as:
fun someWork() {
viewModelScope.launch(Dispatchers.IO) {
combine(
ConnectivityObserver.observe(),
YourAnotherWorkStuffReturnsAFlow()
).collect { status, payloadOfWork ->
_connectivityState = status
when(status) {
// check payload and do work
}
}
}
}
2- directly call your observer as above in a separate vievmodelScope and update value on collect changes. the code will be same except the combine part and other workload such as :
init{
getConnectivity()
}
fun getConnectivity() {
viewModelScope.launch(Dispatchers.IO) {
ConnectivityObserver.observe().collect { status ->
_connectivityState = status
}
}
}
Finally, by using two variables, which are given in the beginning, you will ensure that UI cannot make any changes on _connectivityState as only the connectivityState is exposed to the UI and it only observes through _connectivityState.

Related

How to navigate to another screen after call a viemodelscope method in viewmodel - Jetpack Compose

Hello developers i have a problem i calling an api from viewmodel but is a coroutine and i do not how to wait to api response to make login .... i have this method in viewmodel:
fun createUser(context: Context){
viewModelScope.launch {
val jsonObject= JSONObject()
jsonObject.put("correo", register.value.correo)
jsonObject.put("password", register.value.password)
jsonObject.put("nombre", register.value.nombre)
jsonObject.put("apellido", register.value.apellido)
jsonObject.put("direccion",register.value.direccion)
jsonObject.put("telefono", register.value.telefono)
var bodyrequest=jsonObject.toString().toRequestBody()
val result: RegisterResponse = registerService.register(bodyrequest)
response.value=result
Log.d("response",result.message)
}
}
and this is onClick event from button in a #Composable function:
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser(context)
Log.d("result from compose","${vm.response.value.message}")
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}else{
showPopUp = !showPopUp
}
}
The big problem is that when the navigation is triggered, the coroutine has not finished yet and therefore I cannot find out if the api response was successful or not. How could I do this?
You can let createUser return the Job, and then join it in onClick. Like this:
fun createUser(context: Context) = viewModelScope.launch {...}
...
onClick = {
scope.launch {
viewModel.createUser(context).join()
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}
}
But this makes the code difficult to maintain. I suggest having the ViewModel layer send an Event when createUser is done and the View layer collects it and navigate to a new screen. Like this:
interface Event
abstract class BaseViewModel<E : Event> : ViewModel() {
private val _event = Channel<E>()
val event = _event.receiveAsFlow().shareIn(viewModelScope, SharingStarted.Lazily)
protected suspend fun sendEvent(event: E) = _event.send(event)
protected fun sendEventSync(event: E) = viewModelScope.launch { _event.send(event) }
}
#Composable
fun <E : Event> OnEvent(event: Flow<E>, onEvent: (E) -> Unit) {
LaunchedEffect(Unit) {
event.collect(onEvent)
}
}
sealed interface LoginEvent : Event {
object LoginSuccess : LoginEvent
data class LoginFailure(val exception: Throwable) : LoginEvent
}
class LoginViewModel() : BaseViewModel<LoginEvent>() {
fun createUser(context: Context){
viewModelScope.launch {
repository.loign()
.onSuccess { sendEvent(LoginEvent.LoginSuccess) }
.onFailure { sendEvent(LoginEvent.LoginFailure(it)) }
}
}
}
#Composable
fun Screen() {
...
OnEvent(viewModel.event) {
when (it) {
LoginEvent.LoginSuccess ->
navController.navToXXXScreen()
is LoginEvent.LoginFailure ->
context.toast(it.exception.localizedMessage ?: "")
}
}
}

Ui is not updating from viewmodel kotlin flow

I am quite new in Android Flow and JetPack compose,
I am trying to update my UI when mutable state is being changed , but this is not calling our composable , here is my code
#Composable
fun Grid() {
val mainViewModel by viewModels<DashBoardViewModel>()
mainViewModel.getData()
when (val result = mainViewModel.mutableState.value) {
is Resource.Success -> {
LazyVerticalGrid(
cells = GridCells.Adaptive(100.dp)
) {
items(result.device.items.first().devices.count()) {
EachItem(it)
}
}
}
is Resource.Error -> { Text(text = result.message) }
Resource.Loading -> { CircularProgressIndicator() }
Resource.Empty -> {}
else -> { CircularProgressIndicator() }
}
}
ViewModel:
#HiltViewModel
class DashBoardViewModel #Inject constructor(
private val dashBoardRepository: DashBoardRepository
) : ViewModel() {
val mutableState = MutableLiveData<Resource>()
fun getData() = viewModelScope.launch {
flow {
emit(Resource.Loading)
try {
val mResponse = dashBoardRepository.getDevice()
emit(Resource.Success(mResponse))
} catch (e: Exception) {
e.printStackTrace()
emit(Resource.Error("Error"))
}
}.flowOn(Dispatchers.IO).collect {
mutableState.value = it
}
}
}
There are two problems in your code:
mainViewModel.mutableState.value gets only the current value from your mutable state. Your composable will not be notified when this value changes and thus it cannot reflect the change. If you want to use LiveData in your viewmodel, you have to use observeAsState() extension function which converts LivaData to State that can be automatically observed by composable function. Other option is to have (Mutable)State directly in your viewmodel. See this state explanation.
Your mainViewModel.getData() function will be called every time your Grid() function recomposes, which will be every time your mainViewModel.mutableState changes (once you observe it correctly). You definitely don't want that. Better solution would be to call getData() from your viewModel's init block, or, if you really need to call it from your composable function, use LaunchedEffect.
And, as a side note, the way you are creating flow and then collecting it into LiveData is really odd and unnecessary. You can do something like this instead:
fun getData() = viewModelScope.launch {
mutableState.value = Resource.Loading
try {
val mResponse = dashBoardRepository.getDevice()
mutableState.value = Resource.Success(mResponse)
} catch (e: Exception) {
e.printStackTrace()
mutableState.value = Resource.Error("Error")
}
}

iOS: Swinject correct way to create hierarchy of dependency injection containers

I have used sample project from KooberApp (it is Raywenderlich book advanced architectures example project) and tried to replace custom Inversion of Control Containers and Dependency Injection code with usage of some framework.
I think most popular Dependency Injection framework for iOS is Swinject.
There I can register components for services.
I would like to resemble original app components lifespan. After some run and try, application seems to work correctly. But I am not 100% sure that approach I have used is the best, and that I haven't missed something important. I think I can still have some inconsistency with components lifespan i.e. usage of scopes .container, .graph, .transient, .weak
I someone could advise whether this Container implementation is correct or something should be fixed, changed, modified, do better?
App Container
import Swinject
import SwinjectAutoregistration
public class DIAppContainer {
public class func get() -> Container {
let container = Container()
container.register(Container.self, name: "main") { r in
return DIMainContainer.get(parent: container)
}
container.autoregister(UserSessionCoding.self, initializer: UserSessionPropertyListCoder.init)
container.autoregister(AuthRemoteAPI.self, initializer: FakeAuthRemoteAPI.init)
#if USER_SESSION_DATASTORE_FILEBASED
container.autoregister(UserSessionDataStore.self, initializer: FileUserSessionDataStore.init)
#else
container.autoregister(UserSessionDataStore.self, initializer: KeychainUserSessionDataStore.init)
#endif
container.autoregister(UserSessionRepository.self, initializer: KooberUserSessionRepository.init)
container.autoregister(MainViewModel.self, initializer: MainViewModel.init).inObjectScope(.container)
container.register(LaunchViewModel.self) { r in
return LaunchViewModel(userSessionRepository: r.resolve(UserSessionRepository.self)!, notSignedInResponder: r.resolve(MainViewModel.self)!, signedInResponder: r.resolve(MainViewModel.self)!)
}.inObjectScope(.transient)
container.register(LaunchViewController.self) { r in
let vc = LaunchViewController(viewModel: r.resolve(LaunchViewModel.self)!)
return vc
}
container.register(MainViewController.self) { r in
let vc = MainViewController( viewModel: r.resolve(MainViewModel.self)!,
launchViewController: r.resolve(LaunchViewController.self)!,
mainContainer: r.resolve(Container.self, name: "main")! )
return vc
}
return container
}
}
Main Container
public class DIMainContainer {
public class func get(parent: Container) -> Container {
let container = Container(parent: parent, defaultObjectScope: .container)
container.register(Container.self, name: "onboarding") { r in
return DIOnboardingContainer.get(parent: container)
}
container.register(Container.self, name: "signedin") { r in
return DISignedInContainer.get(parent: container)
}
container.autoregister(OnboardingViewModel.self, initializer: OnboardingViewModel.init).inObjectScope(.weak)
container.register(OnboardingViewController.self) { r in
return OnboardingViewController(viewModel: r.resolve(OnboardingViewModel.self)!, onboardingContainer: r.resolve(Container.self, name: "onboarding")! )
}.inObjectScope(.transient)
container.autoregister(SignedInViewModel.self, initializer: SignedInViewModel.init).inObjectScope(.weak)
container.register(SignedInViewController.self) { (r : Resolver, userSession : UserSession) in
return SignedInViewController(viewModel: r.resolve(SignedInViewModel.self)!, userSession: userSession, signedinContainer: r.resolve(Container.self, name: "signedin")!)
}.inObjectScope(.transient)
return container
}
}
Signed In Container
public class DISignedInContainer {
public class func get(parent: Container) -> Container {
let container = Container(parent: parent)
container.register(Container.self, name: "pickmeup") { r in
return DIPickMeUpContainer.get(parent: container)
}
//container.autoregister(SignedInViewModel.self, initializer: SignedInViewModel.init)
container.autoregister(ImageCache.self, initializer: InBundleImageCache.init)
container.autoregister(Locator.self, initializer: FakeLocator.init)
// Getting Users Location
container.register(DeterminedPickUpLocationResponder.self) { r in
return r.resolve(SignedInViewModel.self)!
}
container.register(GettingUsersLocationViewModel.self) { r in
return GettingUsersLocationViewModel(determinedPickUpLocationResponder: r.resolve(DeterminedPickUpLocationResponder.self)!, locator: r.resolve(Locator.self)!)
}
container.register(GettingUsersLocationViewController.self) { r in
return GettingUsersLocationViewController(viewModel: r.resolve(GettingUsersLocationViewModel.self)!)
}
// Pick Me Up
container.register(PickMeUpViewController.self) { (r: Resolver, location: Location) in
return PickMeUpViewController(location: location, pickMeUpContainer: r.resolve(Container.self, name: "pickmeup")! )
}
// Waiting For Pickup
container.register(WaitingForPickupViewModel.self) { r in
return WaitingForPickupViewModel(goToNewRideNavigator: r.resolve(SignedInViewModel.self)!)
}
container.register(WaitingForPickupViewController.self) { r in
return WaitingForPickupViewController(viewModel: r.resolve(WaitingForPickupViewModel.self)!)
}
// Profile
container.register(NotSignedInResponder.self) { r in
return r.resolve(MainViewModel.self)!
}
container.register(DoneWithProfileResponder.self) { r in
return r.resolve(SignedInViewModel.self)!
}
container.register(ProfileViewModel.self) { (r: Resolver, userSession: UserSession) in
return ProfileViewModel(userSession: userSession, notSignedInResponder: r.resolve(NotSignedInResponder.self)!, doneWithProfileResponder: r.resolve(DoneWithProfileResponder.self)!, userSessionRepository: r.resolve(UserSessionRepository.self)!)
}
container.register(ProfileContentViewController.self) { (r: Resolver, userSession: UserSession) in
return ProfileContentViewController(viewModel: r.resolve(ProfileViewModel.self, argument: userSession)!)
}
container.register(ProfileViewController.self) { (r: Resolver, userSession: UserSession) in
return ProfileViewController(contentViewController: r.resolve(ProfileContentViewController.self, argument: userSession)!)
}
return container
}
}
Onboarding Container
public class DIOnboardingContainer {
public class func get(parent: Container) -> Container {
let container = Container(parent: parent)
container.register(GoToSignUpNavigator.self) { r in
return r.resolve(OnboardingViewModel.self)!
}
container.register(GoToSignInNavigator.self) { r in
return r.resolve(OnboardingViewModel.self)!
}
container.autoregister(WelcomeViewModel.self, initializer: WelcomeViewModel.init).inObjectScope(.transient)
container.autoregister(WelcomeViewController.self, initializer: WelcomeViewController.init).inObjectScope(.weak)
container.register(SignedInResponder.self) { r in
return r.resolve(MainViewModel.self)!
}
container.register(SignInViewModel.self) { r in
return SignInViewModel(userSessionRepository: r.resolve(UserSessionRepository.self)!, signedInResponder: r.resolve(SignedInResponder.self)!)
}.inObjectScope(.transient)
container.autoregister(SignInViewController.self, initializer: SignInViewController.init).inObjectScope(.transient)
container.register(SignUpViewModel.self) { r in
return SignUpViewModel(userSessionRepository: r.resolve(UserSessionRepository.self)!, signedInResponder: r.resolve(SignedInResponder.self)!)
}.inObjectScope(.transient)
container.autoregister(SignUpViewController.self, initializer: SignUpViewController.init)
return container
}
}
Here App Container is created in AppDelegate, child containers are registered in parent container and init injected to view controllers and then stored in properties and used to initialize this child view controllers
let container : Container = DIAppContainer.get()
Here is MainViewController example with injected child container (Main-Scoped)
// MARK: - Main-Scoped Container
let mainContainer: Container
// MARK: - Properties
// View Model
let viewModel: MainViewModel
// Child View Controllers
let launchViewController: LaunchViewController
var signedInViewController: SignedInViewController?
var onboardingViewController: OnboardingViewController?
// State
let disposeBag = DisposeBag()
// MARK: - Methods
public init(viewModel: MainViewModel,
launchViewController: LaunchViewController,
mainContainer: Container) {
self.viewModel = viewModel
self.launchViewController = launchViewController
self.mainContainer = mainContainer
super.init()
}

RxJava- Turn Observable into Iterator, Stream, or Sequence

I know this breaks a lot of Rx rules, but I really like RxJava-JDBC and so do my teammates. Relational databases are very core to what we do and so is Rx.
However there are some occasions where we do not want to emit as an Observable<ResultSet> but would rather just have a pull-based Java 8 Stream<ResultSet> or Kotlin Sequence<ResultSet>. But we are very accustomed to the RxJava-JDBC library which only returns an Observable<ResultSet>.
Therefore, I am wondering if there is a way I can turn an Observable<ResultSet> into a Sequence<ResultSet> using an extension function, and not do any intermediary collection or toBlocking() calls. Below is all I have so far but my head is spinning now trying to connect push and pull based systems, and I cannot buffer either as the ResultSet is stateful with each onNext() call. Is this an impossible task?
import rx.Observable
import rx.Subscriber
import java.sql.ResultSet
fun Observable<ResultSet>.asSequence() = object: Iterator<ResultSet>, Subscriber<ResultSet>() {
private var isComplete = false
override fun onCompleted() {
isComplete = true
}
override fun onError(e: Throwable?) {
throw UnsupportedOperationException()
}
override fun onNext(rs: ResultSet?) {
throw UnsupportedOperationException()
}
override fun hasNext(): Boolean {
throw UnsupportedOperationException()
}
override fun next(): ResultSet {
throw UnsupportedOperationException()
}
}.asSequence()
I'm not sure that's the easiest way to achieve what you want but you can try this code. It converts an Observable to an Iterator by creating a blocking queue and publishing all events from the Observable to this queue. The Iterable pulls events from the queue and blocks if there're none. Then it modify its own state depending on received current event.
class ObservableIterator<T>(
observable: Observable<T>,
scheduler: Scheduler
) : Iterator<T>, Closeable {
private val queue = LinkedBlockingQueue<Notification<T>>()
private var cached: Notification<T>? = null
private var completed: Boolean = false
private val subscription =
observable
.materialize()
.subscribeOn(scheduler)
.subscribe({ queue.put(it) })
override fun hasNext(): Boolean {
cacheNext()
return !completed
}
override fun next(): T {
cacheNext()
val notification = cached ?: throw NoSuchElementException()
check(notification.isOnNext)
cached = null
return notification.value
}
private fun cacheNext() {
if (completed) {
return
}
if (cached == null) {
queue.take().let { notification ->
if (notification.isOnError) {
completed = true
throw RuntimeException(notification.throwable)
} else if (notification.isOnCompleted) {
completed = true
} else {
cached = notification
}
}
}
}
override fun close() {
subscription.unsubscribe()
completed = true
cached = null
}
}
You can use the following helper function:
fun <T> Observable<T>.asSequence() = Sequence { toBlocking().getIterator() }
The observable will be subscribed to when the sequence returned is called for iterator.
If an observable emits elements on the same thread it was subscribed to (like Observable.just for example), it will populate the buffer of the iterator before it gets a chance to be returned.
In this case you might need to direct subscription to the different thread with a call to subscribeOn:
observable.subscribeOn(scheduler).asSequence()
However, while toBlocking().getIterator() doesn't buffer all results it could buffer some of them if they aren't consumed timely by the iterator. That might be a problem if a ResultSet gets somehow expired when the next ResultSet arrives.

Wait for view pager animations with espresso?

Trying to do some tests with a ViewPager.
I want to swipe between tabs, and I don't want to continue until the swipe is complete. But there doesn't appear to be a way to turn off the animation for the view pager (all animations under the developer options are disabled).
So this always results in a test failure, because the view pager hasn't completed it's animation, and so the view is not completely displayed yet:
// swipe left
onView(withId(R.id.viewpager)).check(matches(isDisplayed())).perform(swipeLeft());
// check to ensure that the next tab is completely visible.
onView(withId(R.id.next_tab)).check(matches(isCompletelyDisplayed()));
Is there an elegant or maybe even recommended way to do this, or am I stuck putting some kind of timed wait in there?
The IdlingResource #Simas suggests is actually pretty simple to implement:
public class ViewPagerIdlingResource implements IdlingResource {
private final String mName;
private boolean mIdle = true; // Default to idle since we can't query the scroll state.
private ResourceCallback mResourceCallback;
public ViewPagerIdlingResource(ViewPager viewPager, String name) {
viewPager.addOnPageChangeListener(new ViewPagerListener());
mName = name;
}
#Override
public String getName() {
return mName;
}
#Override
public boolean isIdleNow() {
return mIdle;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
mResourceCallback = resourceCallback;
}
private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {
#Override
public void onPageScrollStateChanged(int state) {
mIdle = (state == ViewPager.SCROLL_STATE_IDLE
// Treat dragging as idle, or Espresso will block itself when swiping.
|| state == ViewPager.SCROLL_STATE_DRAGGING);
if (mIdle && mResourceCallback != null) {
mResourceCallback.onTransitionToIdle();
}
}
}
}
Since I've done this at least twice now, here is the accepted answer in Kotlin and with androidx ViewPager2:
class ViewPager2IdlingResource(viewPager: ViewPager2, name: String) : IdlingResource {
private val name: String
private var isIdle = true // Default to idle since we can't query the scroll state.
private var resourceCallback: IdlingResource.ResourceCallback? = null
init {
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
isIdle = (state == ViewPager.SCROLL_STATE_IDLE // Treat dragging as idle, or Espresso will block itself when swiping.
|| state == ViewPager.SCROLL_STATE_DRAGGING)
if (isIdle && resourceCallback != null) {
resourceCallback!!.onTransitionToIdle()
}
}
})
this.name = name
}
override fun getName(): String {
return name
}
override fun isIdleNow(): Boolean {
return isIdle
}
override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
this.resourceCallback = resourceCallback
}
}
And here is how you use it from a UI test using ActivityScenarioRule:
#get:Rule
val testRule = ActivityScenarioRule(OnboardingActivity::class.java)
private lateinit var viewPager2IdlingResource: ViewPager2IdlingResource
....
#Before
fun setUp() {
testRule.scenario.onActivity {
viewPager2IdlingResource =
ViewPager2IdlingResource(it.findViewById(R.id.onboarding_view_pager), "viewPagerIdlingResource")
IdlingRegistry.getInstance().register(viewPager2IdlingResource)
}
}
#After
fun tearDown() {
IdlingRegistry.getInstance().unregister(viewPager2IdlingResource)
}
The androidx.test.espresso:espresso-core library offers a ViewPagerActions class which contains a number of methods for scrolling between the pages of a ViewPager. It takes care of waiting until the scroll is complete so you don't need to add any explicit waits or sleeps in your test methods.
If you need to perform similar scrolling on a ViewPager2 instance, you can take the source code of the ViewPagerActions class and make some minor tweaks to it to get it to work for ViewPager2. Here is an example which you are welcome to take and use.
Try this,
onView(withId(R.id.pager)).perform(pagerSwipeRight()).perform(pagerSwipeLeft());
private GeneralSwipeAction pagerSwipeRight(){
return new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER_RIGHT, Press.FINGER);
}
private GeneralSwipeAction pagerSwipeLeft(){
return new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_RIGHT,
GeneralLocation.CENTER_LEFT, Press.FINGER);
}
I was having issues with #vaughandroid approach, so I did some changes to his approach. This approach will set idle to false as soon as it detects a scrolling is happening and "force" the ViewPager to finish scrolling by using setCurrentItem().
public class ViewPagerIdlingResource implements IdlingResource {
private volatile boolean mIdle = true; // Default to idle since we can't query the scroll state.
private ResourceCallback mResourceCallback;
private ViewPager mViewPager;
public static ViewPagerIdlingResource waitViewPagerSwipe(ViewPager viewPager) {
return new ViewPagerIdlingResource(viewPager);
}
private ViewPagerIdlingResource(ViewPager viewPager) {
mViewPager = viewPager;
mViewPager.addOnPageChangeListener(new ViewPagerListener());
}
#Override
public String getName() {
return ViewPagerIdlingResource.class.getSimpleName();
}
#Override
public boolean isIdleNow() {
return mIdle;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
mResourceCallback = resourceCallback;
}
private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {
float mPositionOffset = 0.0f;
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (isSwipingToRight(positionOffset)) {
mIdle = false;
mViewPager.setCurrentItem(position + 1);
} else if (isSwipingToLeft(positionOffset)) {
mIdle = false;
mViewPager.setCurrentItem(position - 1);
}
mPositionOffset = positionOffset;
if (positionOffset == 0 && !mIdle && mResourceCallback != null) {
mResourceCallback.onTransitionToIdle();
mIdle = true;
mPositionOffset = 0.0f;
}
}
private boolean isSwipingToRight(float positionOffset) {
return mPositionOffset != 0.0f && positionOffset > mPositionOffset && mIdle;
}
private boolean isSwipingToLeft(float positionOffset) {
return mPositionOffset != 0.0f && positionOffset < mPositionOffset && mIdle;
}
}
}
My goal was to make a screenshot of the screen with ViewPager2 using Facebook screenshot test library. The easiest approach for me was to check almost every frame whether animation completed, if yes then it's time to make a screenshot:
fun waitForViewPagerAnimation(parentView: View) {
if (parentView is ViewGroup) {
parentView.childrenViews<ViewPager2>().forEach {
while (it.scrollState != ViewPager2.SCROLL_STATE_IDLE) {
Thread.sleep(16)
}
}
}
}
childrenViews function can be found here
You can either do a lot of work and use an IdlingResource to implement an OnPageChangeListener
or simply:
SystemClock.sleep(500);

Resources