[android] Map fragments improvements #4808
No reviewers
Labels
No labels
Accessibility
Accessibility
Address
Address
Android
Android
Android Auto
Android Auto
Android Automotive (AAOS)
Android Automotive (AAOS)
API
API
AppGallery
AppGallery
AppStore
AppStore
Battery and Performance
Battery and Performance
Blocker
Blocker
Bookmarks and Tracks
Bookmarks and Tracks
Borders
Borders
Bug
Bug
Build
Build
CarPlay
CarPlay
Classificator
Classificator
Community
Community
Core
Core
CrashReports
CrashReports
Cycling
Cycling
Desktop
Desktop
DevEx
DevEx
DevOps
DevOps
dev_sandbox
dev_sandbox
Directions
Directions
Documentation
Documentation
Downloader
Downloader
Drape
Drape
Driving
Driving
Duplicate
Duplicate
Editor
Editor
Elevation
Elevation
Enhancement
Enhancement
Epic
Epic
External Map Datasets
External Map Datasets
F-Droid
F-Droid
Fonts
Fonts
Frequently User Reported
Frequently User Reported
Fund
Fund
Generator
Generator
Good first issue
Good first issue
Google Play
Google Play
GPS
GPS
GSoC
GSoC
iCloud
iCloud
Icons
Icons
iOS
iOS
Legal
Legal
Linux Desktop
Linux Desktop
Linux packaging
Linux packaging
Linux Phone
Linux Phone
Mac OS
Mac OS
Map Data
Map Data
Metro
Metro
Navigation
Navigation
Need Feedback
Need Feedback
Night Mode
Night Mode
NLnet 2024-06-281
NLnet 2024-06-281
No Feature Parity
No Feature Parity
Opening Hours
Opening Hours
Outdoors
Outdoors
POI Info
POI Info
Privacy
Privacy
Public Transport
Public Transport
Raw Idea
Raw Idea
Refactoring
Refactoring
Regional
Regional
Regression
Regression
Releases
Releases
RoboTest
RoboTest
Route Planning
Route Planning
Routing
Routing
Ruler
Ruler
Search
Search
Security
Security
Styles
Styles
Tests
Tests
Track Recording
Track Recording
Translations
Translations
TTS
TTS
UI
UI
UX
UX
Walk Navigation
Walk Navigation
Watches
Watches
Web
Web
Wikipedia
Wikipedia
Windows
Windows
Won't fix
Won't fix
World Map
World Map
No milestone
No project
No assignees
2 participants
Due date
No due date set.
Dependencies
No dependencies set.
Reference: organicmaps/organicmaps-tmp#4808
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fragments-improvements"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Further improve place page and map buttons fragment abstraction by trying to follow Android's best practices.
With these changes, the main activity does not call fragment functions directly, but instead updates their state using view models. Fragments send data back to the activity using event callbacks.
Saving the UI state between configuration changes is a lot easier as view models handle this natively, it is easier to sync multiple UI components to a single state and we better understand when the data is modified.
The menu bottom sheets would also benefit from this change, but I will keep that for another PR.
This is not ready yet, I still need to find and fix possible regressions, but this will make the code easier to maintain and will also help with implementing organicmaps/organicmaps#4613.
@Jean-BaptisteC you don't need to test yet, I already know of some bugs, but I would greatly appreciate your help when I feel the PR is close enough to being finished :). The PR is only published to let people know I'm working on this.
Fixes #4729
Fixes #4903
Fixes #4680
✅ Pixel 6 - Android 13
I have see weird bugs in first launch, but i'm not able to reproduce
Others problem (buttons zoom is disabled)
✅ Pixel 6 - Android 13
Bugs about layer ✅
Bugs about download button on place page ✅
Find another bug :)
@rtsisyk let's proceed with this refactoring to unblock AA and avoid many merge conflicts caused by delayed merging.
@ -0,0 +188,4 @@
public void onStop()
{
super.onStop();
mViewModel.getMapObject().removeObserver(this);
@ -0,0 +50,4 @@
public void onStop()
{
super.onStop();
mViewModel.getMapObject().removeObserver(this);
Something is very wrong with formatting here.
The observers related to
mMapButtonsViewModel
allow the buttons to react to changes in the app state, such as when the position mode or the layers change, etc.The observer on
mPlacePageViewModel.getPlacePageDistanceToTop
allows the buttons to follow/avoid the place page when it slides. This observer cannot be removed inonPause
otherwise the buttons will not follow the place page if it is sliding while there is a popup over the screen (eg you click share and swipe down the place page at the same time, the place page will slide while the share popup pauses the activity).To make the code easier to understand, I chose to remove every observer in
onStop
.l. That's a leak. Resume may be called many times.
2. It would be safer to leave the old behavior, and only subscribe/unsubscribe in onStop the one needed observer. It will also save users batteries in many cases.
observe
:If the given owner, observer tuple is already in the list, the call is ignored. If the observer is already in the list with another owner, LiveData throws an IllegalArgumentException.
. So there is no leak.onPause
when strictly necessary so we don't introduce strange lifecycle behaviors.For example
onResume
may be called right afteronPause
without triggeringonStop
. That makes handling lifecycle more complex.It is an inconsistent pattern to start something in onResume but stop it onStop.
A more general question: the ViewModel approach doesn't make the code easier, it's more connected now than before the refactoring. Are there any better ways? Maybe not following official "pattern recommendations", but something simpler, with fewer dependencies and fewer lines of code?
I don't see why it is inconsistent as onResume is called both on activity start and on activity resume, but I don't have that much android experience.
After reading the docs a bit more, I think we may be able to simplify this and only observe/remove in onCreate/onDestroy because the viewmodels are linked to the activity lifecycle and thus will never fire when the app goes into background.
Regarding your question, it does make the code simpler. You now have a clear dependency on only the viewmodels, while before you had a mess of callbacks from different classes interacting with each other. Fragments should not depend on any other class than viewmodels. It makes it a lot easier to handle lifecycle as you don't have to worry about using omSaveState, the viewmodel does it for you.
It become too big for a detailed review. We can hope for good testing from @Jean-BaptisteC
It would be great to do similar refactoring in steps or to extract independent parts of them into separate PRs/commits.
nit: I prefer avoiding code like this with additional var:
final ? val = mMapButtonsViewModel.getBottomButtonsHeight().getValue();
nit: requireActivity() I prefer avoiding code like this with additional var
nit: I'd prefer additional var:
final Activity activity = requireActivity();
Is it ok, that we return true (isBehindPlacePage) when there is no placePageWidth ?
@ -0,0 +11,4 @@
private final MutableLiveData<MapButtonsController.LayoutMode> mLayoutMode = new MutableLiveData<>(MapButtonsController.LayoutMode.regular);
private final MutableLiveData<Integer> mMyPositionMode = new MutableLiveData<>();
private final MutableLiveData<Mode> mMapLayerMode = new MutableLiveData<>();
private final MutableLiveData<SearchWheel.SearchOption> mSearchOption = new MutableLiveData<>();
Just curious, what is a MutableLiveData and why it is needed here?
Do we need to call here?
PlacePageUtils.updateMapViewport(mCoordinator, mDistanceToTop, mViewportMinHeight);
mDistanceToTop was changed
@rtsisyk Will be great if you can test this PR on your Huawei with this problem
@ -12,2 +12,4 @@
private final MutableLiveData<List<PlacePageButtons.ButtonType>> mCurrentButtons = new MutableLiveData<>();
private final MutableLiveData<MapObject> mMapObject = new MutableLiveData<>();
private final MutableLiveData<Integer> mPlacePageWidth = new MutableLiveData<>();
private final MutableLiveData<Integer> mPlacePageDistanceToTop = new MutableLiveData<>();
Same here, why MutableLiveData ?
@ -0,0 +11,4 @@
private final MutableLiveData<MapButtonsController.LayoutMode> mLayoutMode = new MutableLiveData<>(MapButtonsController.LayoutMode.regular);
private final MutableLiveData<Integer> mMyPositionMode = new MutableLiveData<>();
private final MutableLiveData<Mode> mMapLayerMode = new MutableLiveData<>();
private final MutableLiveData<SearchWheel.SearchOption> mSearchOption = new MutableLiveData<>();
Here is the documentation: https://developer.android.com/topic/libraries/architecture/livedata#java.
This is a lifecycle aware observable value. When you create the view model, you tie it to an activity. Then the live data inside that view model follows the activity life cycle, so it does not trigger callbacks when the activity is stopped and automatically removes listeners when it is destroyed.
In our case, when we use the viewmodels in fragments we still have to remove the listeners because they have a shorter life cycle than the parent activity (to prevent the listeners from trying to update a destroyed UI).
Tying the view model to the parent activity allows us the reuse the value across different fragments. So the viewmodels store the UI state, and fragments can simply observe the live data to update their UI. No more
onSavedInstanceState
, no more dependency injection all over the place and we are sure all the fragments use the same state.This makes debugging a lot easier and makes the codebase more robust. But then again I am no Android expert.
To improve performance we only update the viewport when the place page is settled, which happens after the place page finished sliding.
If we call
PlacePageUtils.updateMapViewport(mCoordinator, mDistanceToTop, mViewportMinHeight);
here then the viewport will update as the place page moves.Gonna test this PR myself for updateMapViewport.
Thanks! Two more issues I found:
I did testing on my device and it works ok. @rtsisyk Please, check your Huawei.