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

Convert reference layer selection to bottom sheet in select one from map #6118

Merged
merged 27 commits into from
Jun 4, 2024

Conversation

grzesiek2010
Copy link
Member

@grzesiek2010 grzesiek2010 commented Apr 30, 2024

Closes #5845
Closes #5851
Closes #5852

Why is this the best possible solution? Were any other approaches considered?

Generally, everything has been implemented according to what we had discussed earlier. The only difference might be related to this acceptance criteria:

From #5845

Given I'm filling out a form with a select one from map question
And I've copied reference layers to both the global and the current project's directory
When I open the select one from map view
And I press the layers button
And I swipe the bottom sheet up above the middle of the screen
Then the bottom sheet should fill the screen

The bottom sheet can be expanded but only if it contains a long list of layers. Otherwise, it's not possible because everything is visible and there is no need to do that. This is the default behavior and I think it doesn't make sense to change it.

How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?

I think it will be better to these the whole feature at once when all the issues are addressed.

Do we need any specific form for testing your changes? If so, please attach one.

No.

Does this change require updates to documentation? If so, please file an issue here and include the link below.

No.

Before submitting this PR, please make sure you have:

  • added or modified tests for any new or changed behavior
  • run ./gradlew connectedAndroidTest (or ./gradlew testLab) and confirmed all checks still pass
  • added a comment above any new strings describing it for translators
  • verified that any code or assets from external sources are properly credited in comments and/or in the about file.
  • verified that any new UI elements use theme colors. UI Components Style guidelines

@grzesiek2010
Copy link
Member Author

@seadowg could you take a look at the test that is failing here please?

fun `clicking layers button navigates to layers settings`() {

Here is the reason:

Caused by: java.lang.ClassCastException: class org.odk.collect.geo.support.RobolectricApplication cannot be cast to class org.odk.collect.maps.MapsDependencyComponentProvider

The test is in the geo module and it opens the list of layers (before it was a dialog now it's a bottom sheet I called OfflineMapLayersPicker). OfflineMapLayersPicker is in the maps module though and I use dagger to inject dependencies there https://github.com/getodk/collect/pull/6118/files#diff-9e6c9d8d539aa94749d2b7fef8bb4c48090de8859f4484a80945ebb71fdff5ffR37 this line causes the problem because I can't cast org.odk.collect.geo.support.RobolectricApplication to org.odk.collect.maps.MapsDependencyComponentProvider. Do you think solving this is possible? How would you fix this?
You might ask why I didn't use a fragment factory (or a ViewModel) to pass dependencies via constructor so that injecting with dagger wouldn't be needed. The answer is that the new bottom sheet is used in at least three different places so I would need to pass dependencies, I need (there are a few of them) to all those three places and there, use a fragment factory to build my fragment. This doesn't sound easy to maintain so I decided to use dagger. Alternatively, I could have built one fragment factory (with all the dependencies I need) and inject it into those three separate classes which would be easier than injecting all the dependencies I need (3 or 4) separately. But still, that would mean injecting the factory into those three places plus this is not how we have used fragment factories so far.
So basically I still think using dagger to inject dependencies to OfflineMapLayersPicker is the best way but can we solve that problem with tests?

@seadowg
Copy link
Member

seadowg commented May 2, 2024

The test is in the geo module and it opens the list of layers (before it was a dialog now it's a bottom sheet I called OfflineMapLayersPicker). OfflineMapLayersPicker is in the maps module though and I use dagger to inject dependencies there https://github.com/getodk/collect/pull/6118/files#diff-9e6c9d8d539aa94749d2b7fef8bb4c48090de8859f4484a80945ebb71fdff5ffR37 this line causes the problem because I can't cast org.odk.collect.geo.support.RobolectricApplication to org.odk.collect.maps.MapsDependencyComponentProvider. Do you think solving this is possible? How would you fix this?

The RobolectricApplication in geo needs to implement MapsDependencyComponentProvider as well as GeoDependencyComponentProvider (just like how Collect implements both of them).

I do think that having Dagger dependencies go down multiple layers (geo and then maps) in the dependency hierarchy feels awkward though (although we've probably already done it somewhere else). One point to make here is that we should really be using a ViewModel in OfflineMapLayersPicker instead of talking to Scheduler directly as otherwise you'll run into problems with device rotation.

I think a setup like MultiSelectListFragment would be best here: OfflineMapLayersPicker takes a ViewModel (or View Model Factory) and ExternalWebPageHelper in as constructor args and then the parent Fragment SelectionMapFragment passes them via a FragmentFactory on its child FragmentManager. SelectionMapFragment can either construct these (with injected dependencies) or just pass them along from its own constructor. This would be similar to how MultiSelectListFragment is used in DeleteBlankFormFragment. I think my preference overall is to avoid Dagger in components that don't need them, and that's thankfully now true for Fragments.

Happy to discuss more here on Slack if that's not clear!

@grzesiek2010 grzesiek2010 force-pushed the COLLECT-5845q branch 5 times, most recently from 1278726 to fd1d2d5 Compare May 8, 2024 21:43
@grzesiek2010 grzesiek2010 force-pushed the COLLECT-5845q branch 4 times, most recently from 92568d7 to 2ee0d67 Compare May 15, 2024 17:37
@grzesiek2010 grzesiek2010 requested a review from seadowg May 15, 2024 19:06
@grzesiek2010 grzesiek2010 marked this pull request as ready for review May 15, 2024 19:06

<org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton
android:id="@+id/save"
style="?materialButtonStyle"
Copy link
Member Author

Choose a reason for hiding this comment

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

@seadowg please take a look at this button. The styling does not work well. If it was an activity everything would be fine (for example we use the same styling in the crash-handler module and there it works well). There is something wrong with using it in fragments in separate modules I guess. Do you maybe know what is wrong?

Copy link
Member

@seadowg seadowg May 20, 2024

Choose a reason for hiding this comment

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

Oh wow that's weird! It looks to me like bottom sheets might get their own theme overlay set on them (although I haven't looked into it deeply). The hint for that is that you can set style to @style/Widget.Material3.Button and get what you'd expect, but using ?materialButtonStyle is just giving us a bolder text button. My guess is that bottom sheets have an overlay theme that overrides ?materialButtonStyle and that we might need to customise and set our own theme for them (like we do with dialogs). The Material Components repo on Github is probably a good place to look into.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it looks like we need to set our own theme for bottom sheets.

Copy link
Member

@seadowg seadowg left a comment

Choose a reason for hiding this comment

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

@grzesiek2010 grzesiek2010 requested a review from seadowg May 21, 2024 07:50
Copy link
Member

@seadowg seadowg left a comment

Choose a reason for hiding this comment

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

I've just done a quick overview of the general structure and had one comment.

Also, I don't have an any mbtiles to try out, but how does the map reload with the new layer? It seems like the underlying setting changes when the user selects a tile, but it's not clear to me what then triggers that being used. I'm probably missing something obvious!

geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt Outdated Show resolved Hide resolved
@grzesiek2010 grzesiek2010 force-pushed the COLLECT-5845q branch 6 times, most recently from 7b8887e to 96ecc95 Compare May 25, 2024 21:07
@grzesiek2010 grzesiek2010 marked this pull request as draft May 27, 2024 10:41
@grzesiek2010 grzesiek2010 marked this pull request as ready for review May 28, 2024 10:48
@grzesiek2010 grzesiek2010 requested a review from seadowg May 28, 2024 10:48
@grzesiek2010
Copy link
Member Author

Also, I don't have an any mbtiles to try out, but how does the map reload with the new layer? It seems like the underlying setting changes when the user selects a tile, but it's not clear to me what then triggers that being used. I'm probably missing something obvious!

This is a thing I haven't had to touch even. There is a listener that is called whenever the settings are updated and then we update the map in the map fragment that we use (Google, Mapbox or OSM).

@seadowg
Copy link
Member

seadowg commented May 29, 2024

Sorry I really think we should split this up. I've started going through it commit but commit, but because the PR builds things up iteratively (like d2517fe for example), I'd really like to be able to use "Files changed" to also keep track of the final state of things so I'm not commenting on intermediarey states. Just to be clear, I really like the step by step approach, it just hard to combine that with a partial review given the tooling we have.

This should be split after d6c9705 - another branch should be created there and this one should be git reset to a194524 and then pushed. I know managing stacked PRs is a pain, but I'm happy to help with the branch management here. I tend to use git rebase --onto to deal with stacked branch updates, and I've also been looking at Git Town's [stacked changes] tooling which I think we might want to start adopting as I think we most likely want to be doing this more to cut the size of reviews down.

@grzesiek2010
Copy link
Member Author

@seadowg ok the first part is ready.

@grzesiek2010 grzesiek2010 changed the title New offline layers support Convert reference layer selection to bottom sheet in select one from map May 29, 2024
@seadowg
Copy link
Member

seadowg commented May 29, 2024

@seadowg ok the first part is ready.

Thanks so much! That's going to be really helpful.

import org.odk.collect.settings.SettingsProvider
import org.odk.collect.webpage.ExternalWebPageHelper

class OfflineMapLayersPicker(
Copy link
Member

Choose a reason for hiding this comment

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

Just something to consider: I'd be tempted to pass in the ViewModel or ViewModelProvider.Factory here instead of the ViewModel's dependencies. That would let you have more concentrated tests of the Fragment and ViewModel.

I don't think that would be very advantageous in this PR (OfflineMapsLayerPickerTest is pretty simple and easy to read), but it might be useful down the line if you wanted to test something like the ordering of layers (which would be easier to test at the ViewModel level).

This is definitely related to our earlier discussion on the injection setup, and I think I'm again suggesting the approach I put forward in #6118 (comment).

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's keep this in mind but do not change now. I'm not against but I also think it's not something we definitely need to change.
When it comes to testing I already verify the order of displayed layers in OfflineMapLayersPickerTest.
Even if after merging this pr and creating a new one I change my mind and start thinking that yeah we should have it, it would be better to do that later so that I can cherry-pick the commits I've removed easier.

}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.radioButton.setChecked(false)
Copy link
Member

Choose a reason for hiding this comment

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

It looks like the recycling here isn't tested.

It could be worth extracting a helper adapter for single select lists like this (like we already have for multi select in MultiSelectAdapter), but that potentially a follow up change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I wanted to avoid creating a custom view for list elements but that would make testing easier. Let's wait until it has its final form (expandable with the delete button).

private val settingsProvider: SettingsProvider
) : ViewModel() {
private val _data = MutableLiveData<Pair<List<ReferenceLayer>, String?>>()
val data: LiveData<Pair<List<ReferenceLayer>, String?>> = _data
Copy link
Member

Choose a reason for hiding this comment

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

It's probably worth creating a specific data class to represent the view data here (rather than using Pair) just so it's a little more readable.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree and I came to the same conclusion but at a later stage (while working on deleting layers when I need even more details like for example if the layer item is expanded or collapsed). If it's ok, let's maybe keep it unchanged for now so that I can cherry-pick commits that will improve this code without any conflicts (in one of the next prs).

android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:translationX="-5dp"
Copy link
Member

Choose a reason for hiding this comment

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

Why is this here?

Copy link
Member Author

Choose a reason for hiding this comment

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

The RadioButton has a margin that I remove using this. I don't want it. I want the button to start from the edge of the layout without any extra margin. The TriggerWIdget uses the same.

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see now: offline_map_layers_picker.xml defines a guideline that the RecyclerView starts at so we don't need the built in margin in here. The problem there is that we end up with the start edge of the screen not being tappable, even though I'd expect the list item to be selectable across the full horizontal space. I think we should make the RecyclerView full width and remove this (although we might want to adjus the start padding to one of our standard margins).

Copy link
Member Author

Choose a reason for hiding this comment

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

I mean not so long ago we decided not to make the RecyclerView responsible for displaying choices in select_one/select_multiple questions full-width and I think that one is much more important so I thought it's ok. Do you really want it to be the way you described it? If so I'm ok to do that but to me it's not an issue.

Copy link
Member

Choose a reason for hiding this comment

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

I think that was more to do with how we controlled widget margins. I don't think we need to do anything special here and can just let the RecyclerView be full width.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, done.

maps/src/main/res/layout/offline_map_layer.xml Outdated Show resolved Hide resolved
@grzesiek2010 grzesiek2010 requested a review from seadowg May 29, 2024 21:03
Copy link
Member

@seadowg seadowg left a comment

Choose a reason for hiding this comment

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

@grzesiek2010 grzesiek2010 requested a review from seadowg June 3, 2024 12:11
Copy link
Member

@seadowg seadowg left a comment

Choose a reason for hiding this comment

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

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
Copy link
Member

Choose a reason for hiding this comment

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

It's super confusing that you have to do this and can't just set some kind of padding on the MaterialRadioButton. Could you file an issue with this example at https://github.com/material-components/material-components-android? That really needs to get fixed.

Copy link
Member Author

Choose a reason for hiding this comment

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

assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file2))
val file3 = TempFiles.createTempFile(dir)
val mapConfigurator = mock<MapConfigurator>().also {
whenever(it.supportsLayer(file1)).thenReturn(true)
Copy link
Member

Choose a reason for hiding this comment

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

It might be a little more readable (for each test) to write a stub outside of Mockito here. For example:

private class StubMapConfigurator : MapConfigurator {

    private val files = mutableMapOf<File, Pair<Boolean, String>>()

    override fun supportsLayer(file: File?): Boolean {
        return files[file]!!.first
    }

    override fun getDisplayName(file: File?): String {
        return files[file]!!.second
    }

    fun addFile(fike: File, displayName: String, isSupported: Boolean) {

    }

    override fun isAvailable(context: Context?): Boolean {
        TODO("Not yet implemented")
    }

    override fun showUnavailableMessage(context: Context?) {
        TODO("Not yet implemented")
    }

    override fun createPrefs(context: Context?, settings: Settings?): MutableList<Preference> {
        TODO("Not yet implemented")
    }

    override fun getPrefKeys(): MutableCollection<String> {
        TODO("Not yet implemented")
    }

    override fun buildConfig(prefs: Settings?): Bundle {
        TODO("Not yet implemented")
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@grzesiek2010 grzesiek2010 requested a review from seadowg June 3, 2024 16:28
@seadowg seadowg merged commit 4abda64 into getodk:master Jun 4, 2024
6 checks passed
@seadowg
Copy link
Member

seadowg commented Jun 4, 2024

@getodk/testers going to skip "needs testing" here so you can test the whole feature once managing tiles is added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants