mikepenz / MaterialDrawer

The flexible, easy to use, all in one drawer library for your Android project. Now brand new with material 2 design.
https://mikepenz.dev
Apache License 2.0
11.67k stars 2.05k forks source link

Is it possible to use the same Drawer object in different activities? #1385

Closed acosti closed 8 years ago

acosti commented 8 years ago

Hello,

I'm getting an error if I attempt to hold the Drawer object and display it more than once. Is it possible, for performance considerations, to create exactly one (static) Drawer and use it in other activities? I realize the best-practice would be doing one activity with fragments, but this is not the case.

Thanks A Costi

mikepenz commented 8 years ago

@acosti if you can not use one activity with fragments (which is definitely the correct implementation) you will have to go a different approach.

There is definitely NO chance to have the drawer static.

What you can do is to have a helper class which creates the drawer for you (with the same logic) and you just call this helper in all your activities. But the drawer .build() has to be called in each of those activities otherwise the drawer is not added.

acosti commented 8 years ago

Ok, so my question would be: Is there a way for said helper class to only create the drawer once, but .build() it multiple times at different stages of the app?

The rational stems from the fact that it is not cheap for me to create the drawer, as it is highly customized and interacts with various other components of the app. So recreating it each time from scratch would be suicidal in terms of performance.

mikepenz commented 8 years ago

@acosti yes that's possible. You can keep the DrawerBuilder global once. And call the build() per activity then, as this will initiate the inflating of the layouts into your app in the end.

acosti commented 8 years ago

Ok mike, thanks. Appreciate the help.

acosti commented 8 years ago

excuse me Mike but it does not seem to work.

I am getting an explicit runtime error: "you must not reuse a DrawerBuilder builder" .. gotta admit, that's one of the more informative error messages. Any ideas?

mikepenz commented 8 years ago

@acosti oh I forgot about this behavior as I had to prevent people calling .build() multiple times (which would result in them having multiple drawers in the same activity)

The easiest solution would be that you just keep the "expensive" information global and just rebuild the drawer as normal. I assume getting the items is expensive for you so just keep those items globally or so?

acosti commented 8 years ago

yeah I guess that's the sane solution for my crazy problem. if you happen to think of a better one for holding the whole Drawer object I'd love to know.

either way, thanks for the swift responses. your library has saved me tons of time.

mikepenz commented 8 years ago

@acosti you could do a pull request and add a method reset to the DrawerBuilder which sets the mUsed field back to false.

https://github.com/mikepenz/MaterialDrawer/blob/develop/library/src/main/java/com/mikepenz/materialdrawer/DrawerBuilder.java#L71

this would then allow you to do this. And it will still prevent reusing the DrawerBuilder for people who are not quite sure about what they are doing

mikepenz commented 8 years ago

@acosti created a new version v5.3.5 which allows this. https://github.com/mikepenz/MaterialDrawer/releases/tag/v5.3.5

acosti commented 8 years ago

Hello,

I have tried to use the .reset() method you supplied but I'm facing issues. Here's the error I'm getting.

Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

What I did was:

  1. Create a DrawerBuilder
  2. Save the DrawerBuilder as a static variable
  3. Build() the DrawerBuilder on Activity A
  4. When loading Activity B - reset() the DrawerBuilder
  5. Let the DrawerBuilder know the new activity - .withActivity(Activity B)
  6. Build() the DrawerBuilder on Activity B

And then it crashed.

mikepenz commented 8 years ago

@acosti I think this may be related to the DrawerLayout being reused: https://github.com/mikepenz/MaterialDrawer/issues/1399#issuecomment-233190027

Can you try to also call withDrawerLayout(-1) additionally to withActivity

acosti commented 8 years ago

Holy shit that was fast. Thanks man, really.

Same result unfortunately. Let me paste the whole log here.

FATAL EXCEPTION: main
                                                                                               Process: com.example.agombiprototypetest.prototypetest, PID: 20689
                                                                                               java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.agombiprototypetest.prototypetest/com.example.agombiprototypetest.prototypetest.Prototypes.core.aboutUs}: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
                                                                                                   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2416)
                                                                                                   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
                                                                                                   at android.app.ActivityThread.-wrap11(ActivityThread.java)
                                                                                                   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
                                                                                                   at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                                                   at android.os.Looper.loop(Looper.java:148)
                                                                                                   at android.app.ActivityThread.main(ActivityThread.java:5417)
                                                                                                   at java.lang.reflect.Method.invoke(Native Method)
                                                                                                   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
                                                                                                   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)`

`Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
                                                                                                   at android.view.ViewGroup.addViewInner(ViewGroup.java:4309)
                                                                                                   at android.view.ViewGroup.addView(ViewGroup.java:4145)
                                                                                                   at android.view.ViewGroup.addView(ViewGroup.java:4117)
                                                                                                   at com.mikepenz.materialdrawer.DrawerBuilder.createContent(DrawerBuilder.java:1629)
                                                                                                   at com.mikepenz.materialdrawer.DrawerBuilder.buildView(DrawerBuilder.java:1483)
                                                                                                   at com.mikepenz.materialdrawer.DrawerBuilder.build(DrawerBuilder.java:1281)
                                                                                                   at com.example.agombiprototypetest.prototypetest.BL.Navigation.DrawerNavigationModel.BuildDrawerWithToolbar(DrawerNavigationModel.java:119)
                                                                                                   at com.example.agombiprototypetest.prototypetest.BL.Navigation.DrawerNavigationModel.injectView(DrawerNavigationModel.java:96)
                                                                                                   at com.example.agombiprototypetest.prototypetest.BL.Models.AgombiNavActivity.onCreate(AgombiNavActivity.java:37)
                                                                                                   at android.app.Activity.performCreate(Activity.java:6237)
                                                                                                   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
                                                                                                   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369)
                                                                                                   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476) 
                                                                                                   at android.app.ActivityThread.-wrap11(ActivityThread.java) 
                                                                                                   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344) 
                                                                                                   at android.os.Handler.dispatchMessage(Handler.java:102) 
                                                                                                   at android.os.Looper.loop(Looper.java:148) 
                                                                                                   at android.app.ActivityThread.main(ActivityThread.java:5417) 
                                                                                                   at java.lang.reflect.Method.invoke(Native Method) 
mikepenz commented 8 years ago

@acosti hmm it fails here: https://github.com/mikepenz/MaterialDrawer/blob/develop/library/src/main/java/com/mikepenz/materialdrawer/DrawerBuilder.java#L1629

Weird enough as this one is inflated newly during .build() here https://github.com/mikepenz/MaterialDrawer/blob/develop/library/src/main/java/com/mikepenz/materialdrawer/DrawerBuilder.java#L1469

are you sure you called it?

acosti commented 8 years ago

I'm sure.

Here's the process:

  1. Activity A needs a Drawer
  2. DrawerBuilder prepares the drawer
  3. DrawerBuilder gets bound to Activity A via .withActivity(Activity A) and also resets the layout via .withDrawerLayout(-1)
  4. DrawerBuilder is built()
  5. Activity B needs a Drawer
  6. DrawerBuilder gets reset()
  7. DrawerBuilder gets bound to Activity B via withActivity(Activity B) and also resets the layout via .withDrawerLayout(-1)
  8. DrawerBuilder is built
mikepenz commented 8 years ago

could you please send this steps as sample so I can debug it? thanks

acosti commented 8 years ago

hmm that's gonna be a bit tough. let me try to pull something together for you to see.

acosti commented 8 years ago

Ok, this will be tricky. Please bear with me and I'll be glad to respond to any question ASAP.

Each activity that contains a Drawer uses the following code:

private void initDrawerForActivity(Context activityContext)
    {
        // Get the activity
        AgombiNavActivity activity = (AgombiNavActivity) activityContext;

        if (sSharedDrawer == null) {
            sSharedDrawer =generateDrawer(activityContext);
        }
        else {
            sSharedDrawer.reset();
        }

        sSharedDrawer.withActivity(activity)
                     .withDrawerLayout(-1);
    }

The drawer generation code of generateDrawer(activityContext) is as follows:

private DrawerBuilder generateDrawer(Context context) {

        // Get the menu items
        RealmResults<AgombiComponentRealm> menuItems =
                NavigationManager.getInstance().getMenu();

        List<IDrawerItem> drawerItems = new LinkedList<>();

        for (final AgombiComponentRealm menuItem : menuItems) {

        // Get the drawer item externally
            PrimaryDrawerItem item =
                    AgombiViewLoader.getInstance().initializeMenuItem(context, menuItem);
            drawerItems.add(item);
        }

       // Get the footer externally
        ViewGroup footer = AgombiViewLoader.getInstance().initializeDrawerFooter(context);

        DrawerBuilder drawerBuilder = new DrawerBuilder()
                .withTranslucentStatusBar(false)
                .withDelayDrawerClickEvent(300)
                .withDrawerItems(drawerItems)
                .withFooter(footer)
                .withFooterDivider(false);
        // TODO: turn footer to sticky (removing its divider as sticky doesn't work - attempt to solve)

        return drawerBuilder;
    }

Now, the activity calls for the build function. But it isn't so simple. Each activity in my project is responsible for its own navigation, so an activity might be with a drawer but without a toolbar. Or with a toolbar. So I gotta check for this first. Following is the code that checks whether a toolbar is required or not, and accordingly, invokes the DrawerBuilder's build().

public void injectView(ViewGroup activityLayout) {

        Drawer projectedDrawer = null;
        Toolbar potentialToolbar = getToolbar();
        // Check whether a toolbar is required. If so, build a combined Drawer + Toolbar
        if (potentialToolbar == null)
        {
            projectedDrawer = sSharedDrawer.build();
        }
        else
        {
            projectedDrawer = BuildDrawerWithToolbar(sSharedDrawer, activityLayout, getToolbar());
        }
    }

And the following is the extra code that builds the Drawer given a Toolbar...

private Drawer BuildDrawerWithToolbar(DrawerBuilder drawerBuilder, ViewGroup activityLayout, Toolbar optionalToolbar) {

        AttachToolbarToLayout(activityLayout, optionalToolbar);

        // Create the drawer
        return drawerBuilder
                .withToolbar(optionalToolbar)
                .withActionBarDrawerToggle(true)
                .withActionBarDrawerToggleAnimated(true)
                .build();
    }

Damn. I hope this is clear enough. Sorry, my code is a bit complex. I have a huge task to accomplish but my knowledge in Android is a WIP, haha. Hope the code is fine :)

Let me know if you can catch anything, Mike. Appreciate the help and swift responses, really.

acosti commented 8 years ago

Updated the formatting for easier access

caralin3 commented 8 years ago

@mikepenz How would I use fragments to do this? I'm not that familiar with fragments.

mikepenz commented 8 years ago

@acosti sorry for the late answer. It's quite complex the above. Perhaps posting a "zip" containing the simple sample code would be easier

mikepenz commented 8 years ago

@caralin3 if you have one activity with fragments. Just use the listener and change out the fragments if the user selects a new one

caralin3 commented 8 years ago

@mikepenz Do you have a link that can help me create a fragment?

mikepenz commented 8 years ago

@caralin3 the official android documentation shows how to comit fragments to a container view https://developer.android.com/guide/components/fragments.html

just use the listener to detect selections of the user, and switch out the fragment

acosti commented 8 years ago

hey @mikepenz,

I'm trying a new approach: instead of redrawing the drawer anew on each activity, and instead of having the "1 activity - many fragments" implementation:

I'll do 1 activity - many activities. that is - there's one drawer activity, responsible for holding the drawer and drawing it ONCE. and upon navigation - the setContentView has an override which gets the target layout, inflates it, and places it in the one-drawer-activity's layout in a "content frame".

So basically, same implementation I guess as you'd have with fragments except with activities and self-managed.

My question is, how would you approach such solution?

I was entertaining the idea of simply (I hope it's simple) overriding the drawer's layout in order to add the "content frame". from there it should be easy to do the layout inflation on setContentView's override. your thoughts?

Rainer-Lang commented 8 years ago

Soneone tried with Dagger2?

mikepenz commented 8 years ago

@acosti I remember there was a library which did exactly this. Keeping one activity but filling it up with different views depending on the context.

I am not quite sure if this is a good or the best approach. Android comes with a pretty good and feature rich concept of Fragments which will exactly do what you need.

It is also rather simple to split things up into fragments, and to handle fragments and doing the switching with the Drawer as you just need to commit the correct fragment when an item is selected. You can even move the whole fragment commiting logic into the Listener of the drawer, as you can trigger it after start, and also when the onCreate is called with onSavedInstanceState setting the correct fragment again.

mikepenz commented 8 years ago

Ok to finally close this issue. After some more digging and debugging and trying. It is not really possible to reuse the same DrawerBuilder, as it turns out that the RecyclerView is not happy if the Adapter is reused.

The optimal solution for this usecase is to remember the complex data you have to fetch, and then just build the drawer with the items, based on the already loaded data.