Epoxy: Google maps fragment in epoxy view

Created on 2 Oct 2018  路  5Comments  路  Source: airbnb/epoxy

I'm currently exploring the new MvRx stack from Airbnb

Rigth now i am adding a google maps fragment successfully to a BaseMvRxFragment in a BottomNavigationView.

The problem is that the 2. time i navigate to the MapFragment my app crashes and i get the following error message:

    E/AndroidRuntime: FATAL EXCEPTION: main
        Process: getflareapp.com.s2s, PID: 19184
        android.view.InflateException: Binary XML file line #7: Binary XML file line #7: Error inflating class fragment
        Caused by: android.view.InflateException: Binary XML file line #7: Error inflating class fragment
        Caused by: java.lang.IllegalArgumentException: Binary XML file line #7: Duplicate id 0x7f080093, tag null, or parent id 0xffffffff with another fragment for com.google.android.gms.maps.SupportMapFragment
            at android.support.v4.app.FragmentManagerImpl.onCreateView(FragmentManager.java:3752)
            at android.support.v4.app.FragmentController.onCreateView(FragmentController.java:120)
            at android.support.v4.app.FragmentActivity.dispatchFragmentsOnCreateView(FragmentActivity.java:405)
            at android.support.v4.app.FragmentActivity.onCreateView(FragmentActivity.java:387)
            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:780)
            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730)
            at android.view.LayoutInflater.rInflate(LayoutInflater.java:863)
            at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
            at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
            at android.view.View.inflate(View.java:23239)
            at getflareapp.com.s2s.views.MapView.<init>(MapView.kt:19)
            at getflareapp.com.s2s.views.MapView.<init>(MapView.kt:14)
            at getflareapp.com.s2s.views.MapView.<init>(Unknown Source:6)
            at getflareapp.com.s2s.views.MapViewModel_.buildView(MapViewModel_.java:42)
            at getflareapp.com.s2s.views.MapViewModel_.buildView(MapViewModel_.java:22)

MapFragment.kt

    /**
     * A simple [BaseFragment] subclass.
     *
     */
    class MapFragment : BaseFragment() {


        private val mViewModel: ChatViewModel by fragmentViewModel()

        override fun epoxyController() = simpleController(mViewModel) {state ->


            mapView{
                id("map")
            }
        }
    }

MapView.kt

 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT)
    class MapView @JvmOverloads constructor(
            context: Context,
            attrs: AttributeSet? = null,
            defStyleAttr: Int = 0
    ) : FrameLayout(context, attrs, defStyleAttr) {

        init {
            // TODO: Fix crash on second view.
            inflate(context, R.layout.view_map, this)
        }
    }

view_map.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/map2"
            android:name="getflareapp.com.s2s.ui.map.MapFragment"
            class="com.google.android.gms.maps.SupportMapFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".ui.MainActivity" />

    </FrameLayout>

Thanks for any help! <3

Most helpful comment

If anybody is wondering, using a MapView in lite mode works just fine in an Epoxy view.

Just put a MapView in a layout xml file:

    <com.google.android.gms.maps.MapView
            android:id="@+id/my_location_map"
            android:layout_width="match_parent"
            android:layout_height="220dp"
            app:cameraZoom="15"
            app:liteMode="true"/>

Then the view (in Kotlin), MyMapView.kt:

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class MyMapView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private var marker: Marker? = null
    private var myLocation: LatLng? = null
    private val markerOptions = MarkerOptions()
    private var googleMap: GoogleMap? = null

    init {
        View.inflate(context, R.layout.my_map_view_layout, this)
        my_location_map.isClickable = false
        my_location_map.isLongClickable = false
    }

    fun startMap() {
        my_location_map.onCreate(null)
        my_location_map.getMapAsync {
            googleMap = it
            googleMap?.uiSettings?.isMapToolbarEnabled = false
            myLocation?.let {
                googleMap?.moveCamera(CameraUpdateFactory.newLatLng(it))
                markerOptions.position(it)
                marker?.remove()
                marker = googleMap?.addMarker(markerOptions)
            }
        }
    }

    @ModelProp
    fun setLocation(newLocation: LatLng?) =
        newLocation?.let {
            myLocation = it
            startMap()
        }
}

Then in the Epoxy controller:

    private fun renderLoadedState(data: MyState) {

        /**
         *  [MyMapView]
         *  data.myLocation is a LatLng
         */
        myMapView {
            id("my_map_view")
            location(data.myLocation)
        }

       //.................

Note that if you use a MapView in regular mode (not litemode) you'll need to route all of the Activity lifecycle events down to the MapView as well.

All 5 comments

I've never used fragments inside an epoxy model, so I'm not sure how to make this work off the top of my head, or even if it is feasible. My first guess is that you need to remove the fragment from the activity when the model is recycled.

If your only view is a map I wouldn't use epoxy for this - just use a normal layout with mvrx

Thanks, that is what i ended up doing.

If anybody is wondering, using a MapView in lite mode works just fine in an Epoxy view.

Just put a MapView in a layout xml file:

    <com.google.android.gms.maps.MapView
            android:id="@+id/my_location_map"
            android:layout_width="match_parent"
            android:layout_height="220dp"
            app:cameraZoom="15"
            app:liteMode="true"/>

Then the view (in Kotlin), MyMapView.kt:

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class MyMapView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private var marker: Marker? = null
    private var myLocation: LatLng? = null
    private val markerOptions = MarkerOptions()
    private var googleMap: GoogleMap? = null

    init {
        View.inflate(context, R.layout.my_map_view_layout, this)
        my_location_map.isClickable = false
        my_location_map.isLongClickable = false
    }

    fun startMap() {
        my_location_map.onCreate(null)
        my_location_map.getMapAsync {
            googleMap = it
            googleMap?.uiSettings?.isMapToolbarEnabled = false
            myLocation?.let {
                googleMap?.moveCamera(CameraUpdateFactory.newLatLng(it))
                markerOptions.position(it)
                marker?.remove()
                marker = googleMap?.addMarker(markerOptions)
            }
        }
    }

    @ModelProp
    fun setLocation(newLocation: LatLng?) =
        newLocation?.let {
            myLocation = it
            startMap()
        }
}

Then in the Epoxy controller:

    private fun renderLoadedState(data: MyState) {

        /**
         *  [MyMapView]
         *  data.myLocation is a LatLng
         */
        myMapView {
            id("my_map_view")
            location(data.myLocation)
        }

       //.................

Note that if you use a MapView in regular mode (not litemode) you'll need to route all of the Activity lifecycle events down to the MapView as well.

Can confirm solution by @dmnugent80 is working for me. Thanks!

@dmnugent80, how does one route the MapView in a regular mode considering a property of it is required in the activity/fragment class and epoxy is not structured to allow for that?

Was this page helpful?
0 / 5 - 0 ratings