Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added VersionCheck class and support for verifying data and showing a default dialog (#5, #6, #7) #13

Merged
merged 21 commits into from
Oct 1, 2021

Conversation

ssawchenko
Copy link
Collaborator

@ssawchenko ssawchenko commented Sep 28, 2021

Summary of Problem:

Android project had some initial work done on it, but didn't end up getting fully flushed out.

Sorry, there's a few issues being addressed here all at the same time, I should have broken them up but just kept going down rabbit holes and they were somewhat intertwined.

Proposed Solution:

This PR addresses the following tickets:

This PR adds:

  • PlatformVersionData, VersionData, Status and DisplayState data models; named to match iOS as close as possible
  • URLFetcher interface and a default implementation that pulls down a string from a given URL
  • VersionCheck contains flows for exposing both the display and status states; it can also be setup as a lifecycle listener to automatically run checks "on start"
  • DefaultUpgradeDialog collects display flow from VersionCheck and handles creation of a default alert dialog; it should be setup as an activity lifecycle listener to automatically handle the show/dismiss of the dialog.
  • VersionDataConverter interface and a default implementation to parse JSON into a VersionData object; to avoid having to import specific parsing libraries, this is done with basic JSONObject and JSONArray manual parsing.
  • Unit tests

Setup and Usage

Currently, to add version checking at the application level, setup is as follows (the MainActivity is still playing around with using the ViewModel as a proof of concept, but in practice I am guessing we'd be applying this at the application level)

class App: Application(), LifecycleObserver {

    override fun onCreate() {
        super.onCreate()
        setupVersionCheck()
    }

    private fun setupVersionCheck() {
        val versionChecker = VersionCheck(
            VersionCheckConfig(
              appVersionName = BuildConfig.VERSION_NAME,
              appVersionCode = BuildConfig.VERSION_CODE,
              url = "https://doesn't_matter",
              urlFetcher = MockURLFetcher
          )
        )
        val upgradeDialog = DefaultUpgradeDialog(versionChecker.displayStateFlow)

        ProcessLifecycleOwner.get().lifecycle.addObserver(versionChecker)
        registerActivityLifecycleCallbacks(upgradeDialog)
    }
}

Testing Steps:

  1. Run sample app
  2. Default dialog should automatically pop up indicating that an upgrade is required
    Screen Shot 2021-09-28 at 10 29 06 AM

Still todo:

@ssawchenko ssawchenko changed the title [WIP] Ss/adding main functionality Added VersionCheckViewModel and support for fetching, parsing, verifying data and showing a default dialog (#5, #6, #7) Sep 28, 2021
@ssawchenko ssawchenko changed the title Added VersionCheckViewModel and support for fetching, parsing, verifying data and showing a default dialog (#5, #6, #7) Added VersionCheckViewModel and support for verifying data and showing a default dialog (#5, #6, #7) Sep 28, 2021
Copy link

@jacobminer jacobminer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My biggest concern with using a viewModel is that I think it means we lose the ability to validate the version from the App class, and to replicate it we'd need to make sure that every activity in the app is calling runVersionCheck in the onResume via the ViewModel. I think we could get around that and keep most of this functionality if we refactored out the runVersionCheck into a separate Repository which could be used at the app level directly, but keep the VersionCheckViewModel, and have it monitor the repository instead.

So it might looks something like:
VersionCheckRepository

  • Contains a shared flow of Status
  • has runVersionCheck

VersionCheckViewModel

  • monitors the VersionCheckRepository's flow, converts it to LiveData for observers

Activity/Fragment can monitor VersionCheckViewModel (essentially the same)

App can call runVersionCheck via the VersionCheckRepository if necessary

}

class DefaultUpgradeDialog(
private val activity: Activity): UpgradeDialog, LifecycleObserver {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this leak activity?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm, good question. I didn't run leak canary, but this felt like the only way to get access to the activity to get context to show the dialog. Maybe I could extract this out by having a delegate/interface that the Activity or App implements to give context or create the dialog. I'll ponder that a little more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should no longer be an issue.

requiresUpdate: Boolean,
canDismiss: Boolean) {

val builder = AlertDialog.Builder(activity).apply {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if activity is still active here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should no longer be an issue

@jacobminer jacobminer assigned ssawchenko and unassigned jacobminer Sep 28, 2021
@ssawchenko
Copy link
Collaborator Author

ssawchenko commented Sep 28, 2021

My biggest concern with using a viewModel is that I think it means we lose the ability to validate the version from the App class, and to replicate it we'd need to make sure that every activity in the app is calling runVersionCheck in the onResume via the ViewModel. I think we could get around that and keep most of this functionality if we refactored out the runVersionCheck into a separate Repository which could be used at the app level directly, but keep the VersionCheckViewModel, and have it monitor the repository instead.

So it might looks something like:
VersionCheckRepository

Contains a shared flow of Status
has runVersionCheck
VersionCheckViewModel

monitors the VersionCheckRepository's flow, converts it to LiveData for observers
Activity/Fragment can monitor VersionCheckViewModel (essentially the same)

App can call runVersionCheck via the VersionCheckRepository if necessary

Yeah, I agree with you on these points for sure. I went this route for a few reasons:

  1. I figured we could add this to a BaseActivity class to easily get the version checking across all pages
  2. I wanted to make the required setup code as small as possible (ie. avoiding having to listen for the activity lifecycles in the Application object, but maybe that's just the way it has to be. I'll play around with that option shortly.

@ssawchenko ssawchenko assigned jacobminer and unassigned ssawchenko Sep 29, 2021
@ssawchenko
Copy link
Collaborator Author

ssawchenko commented Sep 29, 2021

@jacobminer Ok ready for another round of review! Big refactor here - I have moved the bulk of the logic into the VersionRepository and exposed those updates via state flows. This allows us to setup the version checks easily on the application object. Please see my updated PR description for comments on what the new classes are. I'm still super new to flows, so it is possible there are nuances I have not got correct.

Let me know if this is closer to what you were thinking!

Edit: This broke some of my unit tests, but once we have accepted a final implementation I will update them to use the repositories instead.

// Looking at documentation we may be able to trigger the update with each collection so
// we do not actually have to call this manually? <-- todo
applicationScope.launch(Dispatchers.IO) {
// Delay to show that we do get the initial state value before we call the update
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to remove this before committing this is mostly for testing right now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer an issue

// repeatOnLifecycle(Lifecycle.State.STARTED) as our collection scope.
versionRepository.displayStateFlow.collect {
upgradeDialog.show(activity, it)
Toast.makeText(activity, it.toString(), Toast.LENGTH_LONG).show()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: Remove toast before releasing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer an issue

@ssawchenko
Copy link
Collaborator Author

ssawchenko commented Sep 29, 2021

@jacobminer Ok, 3rd time's the charm? Hopefully my changes reflect what you were suggesting in our talk. I have

  • Moved flow logic into VersionCheck (repository is gone); this class can be setup as a process lifecycle observer if desired to automatically trigger the version check on app "start".
  • Changed DefaultUpgradeDialog to take the displayState flow; this class now handles activity lifecycle changes regarding the dialog.
  • Nuked the ViewModel; as it is no longer really useful.
  • Added a config object since I think we may be able to create/inject it nicer using koin.

@@ -0,0 +1,55 @@
package com.steamclock.versioncheckkotlin.utils

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can probably remove this file now that we are no longer using LiveData; when I update the test scripts I will look into this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@@ -0,0 +1,96 @@
//import androidx.arch.core.executor.testing.InstantTaskExecutorRule
//import com.steamclock.versioncheckkotlin.utils.TestConstants
//import com.steamclock.versioncheckkotlin.VersionCheckViewModel
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update this to test VersionCheck once we have finalized implementation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with new unit test

)
val upgradeDialog = DefaultUpgradeDialog(versionChecker.displayStateFlow)

ProcessLifecycleOwner.get().lifecycle.addObserver(versionChecker)
Copy link
Collaborator Author

@ssawchenko ssawchenko Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and looked at the Swift library and I now see what you mean about how they are listening for the application events.

I'm totally fine with pulling out the lifecycle listener code out of VersionCheck and simply making the app itself handle the lifecycle observation manually. Thoughts?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way works for me! From an app standpoint, it's pretty convenient to use it this way, but the runVersionCheck function is also exposed, so it could be used either way. Maybe leave it as is, then note in the docs that you can choose to observe the lifecycle in the app and call runVersionCheck or call ProcessLifecycleOwner.get().lifecycle.addObserver(versionChecker) depending on what behaviour best fits the project?

Copy link

@jacobminer jacobminer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! 🚀

)
val upgradeDialog = DefaultUpgradeDialog(versionChecker.displayStateFlow)

ProcessLifecycleOwner.get().lifecycle.addObserver(versionChecker)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way works for me! From an app standpoint, it's pretty convenient to use it this way, but the runVersionCheck function is also exposed, so it could be used either way. Maybe leave it as is, then note in the docs that you can choose to observe the lifecycle in the app and call runVersionCheck or call ProcessLifecycleOwner.get().lifecycle.addObserver(versionChecker) depending on what behaviour best fits the project?

@jacobminer
Copy link

Sorry, it looks like my comments from the previous version got included in this PR review. Disregard those!

@jacobminer jacobminer assigned ssawchenko and unassigned jacobminer Oct 1, 2021
@ssawchenko ssawchenko changed the title Added VersionCheckViewModel and support for verifying data and showing a default dialog (#5, #6, #7) Added VersionCheck class and support for verifying data and showing a default dialog (#5, #6, #7) Oct 1, 2021
@ssawchenko ssawchenko merged commit 1c51994 into main Oct 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants