Injectview (Roboguice) not working with multiple layouts - android

So I have different layouts for this one Activity.
And I have different classes that each open and do their thing with a layout.
I inject these classes in the Activity via #Inject. All this is without problem.
But when I try to use #InjectView on one of the controls that are in the not active layout I get an error.
11-02 19:17:31.086: ERROR/AndroidRuntime(1326): Caused by:
java.lang.NullPointerException: Can't inject null value into class
be.baes.notes.View.EditNoteImpl.saveButton when field is not #Nullable
This would then be the code.
public class EditNoteImpl implements EditNote {
#Inject CancelEditNoteClickListener cancelEditNoteClickListener;
#Inject SaveNoteClickListener saveNoteClickListener;
#Inject Provider<Activity> activity;
#InjectView(R.id.saveButton) Button saveButton;
/* (non-Javadoc)
* #see be.baes.notes.EditNote#activateEditNote()
*/
#Override
public void activateEditNote()
{
activity.get().setContentView(R.layout.editnote);
this.saveButton.setOnClickListener(saveNoteClickListener);
}
}
I can however do this.
public class EditNoteImpl implements EditNote {
#Inject CancelEditNoteClickListener cancelEditNoteClickListener;
#Inject SaveNoteClickListener saveNoteClickListener;
#Inject Provider<Activity> activity;
private Button saveButton;
/* (non-Javadoc)
* #see be.baes.notes.EditNote#activateEditNote()
*/
#Override
public void activateEditNote()
{
activity.get().setContentView(R.layout.editnote);
saveButton = (Button)activity.get().findViewById(R.id.saveButton);
this.saveButton.setOnClickListener(saveNoteClickListener);
}
}
Is there a better way of doing this?

I have just started to use roboguice so someone else with more experience might be able to give a better answer, but this is what I found so far:
In roboguice-1.1.2 (the current stable release), #InjectView items are injected only when setContentView() is called the first time in the Activity. Since your code calls setContentView() dynamically from the auxiliary classes, the injected items there won't be injected correctly.
In roboguice-2.0b2 (the current beta), there is support for multiple setContentView() calls and your code should work. However, injected views are still tied to the context Activity (instead of the declaring class) so every #InjectView will potentially also need to be #Nullable across all auxiliary classes sharing the same Activity.
Since the issue here seems to be rooted in the multiple layout (and hence multiple setContentView() calls) in a single Activity, one alternative way to do what you want is to avoid it as follow:
Instead of having several layouts, use a single layout using the <include/> tag to load all the layouts into a parent FrameLayout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<include layout="#layout/layout1" />
<include layout="#layout/layout2" />
<!-- other layouts... -->
</FrameLayout>
then, instead of calling setContentView(), use a method that will toggle the visible layout on the Activity, something like this:
// instead of: activity.setContentView(R.layout.layout1);
// use: activity.showLayout(R.id.layoutview1);
public void showLayout(int layoutViewId) {
final View view = findViewById(layoutViewId);
final ViewGroup root = (ViewGroup) view.getParent();
for (int i = 0; i < root.getChildCount(); i++) {
final View v = root.getChildAt(i);
v.setVisibility(v == view ? View.VISIBLE : View.GONE);
}
}
The above alternative should work for both the stable and beta roboguice releases. The trade-off here is we are loading several layouts at the same time instead of loading each one at several different times. It seems to work well enough for me (though it might be different for your needs).
One thing I should note is that on the current "Upgrading from RoboGuice 1.1 to 2.0" page, the following is mentioned:
The ability to use #InjectView in Views (although you'll need to call
RoboGuice.injectMembers() yourself, since there's no RoboView base
class and probably never will be).
It seems that this should allow you to implement your auxiliary classes to be derived from View and make #InjectView in them work better (hopefully no need for them to be #Nullable since they are less tied to the Activity). However, looking at the current code, this feature does not seem to be implemented yet (although I could have been looking at the wrong place).

Related

How to prevent memory leaks when using dynamic screens?

I'm creating an app that has screens that present data to the user.
Each Screen has its own data and its own layout, so it has a method to return an int that represent the layout that is used to inflate it, then this View is passed to a function to find the specific views and populate it with data.
The lifecycle goes like this:
MainPresenter:
screen.getNextScreen ->
screen.getLayout ->
view = inflateScreen ->
screen.populateScreen(view) ->
(wait for time elappsed or click) -> repeat
Those Screens are also needed in SettingsActivity to enable\disable them.
So i've created a singleton ScreenProvider, it initializes once and then returns the list.
public class ScreenProvider {
private List<Screen> screens;
private static ScreenProvider instance = new ScreenProvider();
public static ScreenProvider getInstance(){
return instance;
}
private ScreenProvider() {
screens = new ArrayList<>();
screens.add(new Welcome());
screens.add(new CompoundScreen());
screens.add(new Times());
screens.add(new Messages());
screens.add(new Weekly());
}
public List<Screen> getScreenList() {
return Lists.newArrayList(screens);
}
}
Its seam that when running to long the app crashes or get closed for memory leaks, so i've added leakcanary, and this an example for its report:
MainActivity has leaked:
D: * static ScreenProvider.!(instance)!
D: * ↳ ScreenProvider.!(screens)!
D: * ↳ ArrayList.!(array)!
D: * ↳ array Object[].!([0])!
D: * ↳ CompoundScreen.!(disposable)!
D: * ↳ LambdaObserver.!(onNext)!
D: * ↳ -$$Lambda$Screen$67KdQ1jl3VSjSvoRred5JqLGY5Q.!(f$1)!
D: * ↳ AppCompatTextView.mContext
D: * ↳ MainActivity
This is just a single example, but almost every screen has such leak.
The LeakCanary report shows that TextView has this: D: | mAttachInfo = null so i assume it is not the problem.
Also every Screen has an onHide() to clear disposables, that is called when current Screen hides and in MainActivity.onStop().
How to fix this leak?
Should i not use a singleton for the screens?
If not, how do i access the screen list from other activities?
** Editing **
Adding some of Screen main methods that every screen overrides.
public abstract int getLayout();
public boolean shouldShow()
public void populateData(View view)
public void onHide()
public abstract int getScreenIndex();
public boolean shouldCacheView()
public int getDuration()
Okay. From what you are saying, and what you are showing, it seems you are keeping instances of some of the generated views into the Singleton. Don't. Every View requires a Context to be created, either by code, or by inflation (which is basically an XML-backed, reflection-based factory method), to access the app and system's resources, and keep a reference to said Context, for as long as they live. In your scenario, that means keeping references to the activity where you generated the view. Usually, regarding views and activities, this is what happens regarding the GC:
GC: Hey! Does anybody need this... MainActivity class?
View: I do! I do! I have a reference!
GC: Okay... and besides MainActivity, Does anybody else need this View class?
-Nobody answers-
GC: It does not matter my friend, you are being collected as well. Come with me.
And they both go.
In your case:
GC: Hey! Does anybody need this... MainActivity class?
View: I do! I do! I have a reference! and MainActivity references me as well.
GC: Okay... and besides MainActivity, Does anybody else need this View class?
ScreenProvider: I do.
GC: Okay, keep moving View, and take MainActivity with you. Let me know when you folks are done so I can collect you.
And thus the leak.
In order to pass a view from one activity to another, you would need to remove the reference (the mContext field) to the previous activity. Since there's no API for doing that, you would need to use reflection. And there arises another problem: Every UI piece is a subclass of View. Layouts, Widgets, etc, etc. So either you keep a reference to every piece of your XML file in order to remove the context via reflection, or you traverse the child list of the view, remove the context, and keep going until there are no more child views, at any level. After that, you would have to set the reference to the new activity in the same way. It sounds like a huge hack, because it is, and things are bound to break at some level. A Context after all represents the environment and state within your view exist.
A better solution for your situation is to remove the view references from the singleton, and use it only to keep representations of the state/configuration of a given view. Create a callback backed method (or similar) that inflates the view in background and performs the necessary configurations before returning said view. If you still want to keep a single repository of all the Screens the activity may have, add it to the activity class as a member, so it gets collected alongside the activity.
As a side note, your situation suggests you should use a single activity, and then just swap a "MainScreen" composed of "Screens", or just switch between screens depending on the situation. It would make more sense and would be less risky.
Finally, citing myself: Remember the first rule of the android fight club

Passing values to custom view in android

Cheers,
I have an app that receives user input (2 numbers, width and height) and in theory depending on that input I have a custom view that should draw a grid (width and height).
Note:
These 2 values should be received before view attempts to draw itself.
These 2 values aren't constant and therefore I don't think XML approach can help.
I was told that adding another parameter to the View constructor is evil.
Do not confuse my 2 values with canvas.getWidth or etc.. these are values needed simply to draw something, nothing else.
My View is also a ViewGroup.
Main issue arises with Views declared in XML files.
I have temporarily solved this issue by making an SchemeContext class which contains those 2 static values and I simply set them in onCreate (before onCreateView) then use them in custom View onDraw when needed (SchemeContext.width). This is not really what people would call OOP I'm forcing global variables upon java and those are set on time because of the fragment lifecycle.
I've seen this answer How to pass variables to custom View before onDraw() is called?.
But it's more of a workaround than a solution (and probably not the fastest one). There has to be a sensible solution I don't think 3D games on android resort to these workarounds (SurfaceView with OpenGL is still a View right? :d).
If there is an obvious solution and this is an obvious double I'll remove the question.
I haven't tried this, but I think it would be possible to do this fairly cleanly by overriding the LayoutInflater.Factory. That way, you can intercept the creation of the views that need additional parameters passed to their constructors, and let the rest of them fall through to default inflation.
For example, in your activity, before you inflate the view hierarchy:
LayoutInflater inflater = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE);
MyInflaterFactory factory = new MyInflaterFactory();
// Pass information needed for custom view inflation to factory.
factory.setCustomValue(42);
inflater.setFactory(factory);
For your implementation of the factory:
class MyInflaterFactory implements LayoutInflater.Factory {
public void setCustomValue(int val) {
mCustomVal = val;
}
#Override
public View onCreateView (String name, Context context, AttributeSet attrs) {
if (name.equals("com.package.ViewWithCustomCreation")) {
return new ViewWithCustomCreation(context, attrs, mCustomVal);
}
return null;
}
private int mCustomVal;
}
I was told that adding another parameter to the View constructor is evil.
Nonsense.
There are three (and in the newest APIs, four) different View constructors, each used in a different situation. (See this thread.) If you wanted to be able to declare your view in XML, for example, then you'd have to provide a constructor with exactly the right parameters, and have it call the corresponding superclass constructor. But there's nothing wrong with defining your own constructor (or even several of them) that call the superclass constructor intended for creating views programmatically.
The overriding principle is that every object must be valid when its constructor returns. So unless you can provide reasonable default values in your constructor, you have little choice but to accept the object's properties as constructor parameters.

How to find a View in a BridgeContext?

TL;DR: Is there anything in com.android.layoutlib.bridge.android.BridgeContext that can substitute for Activity#findViewById(...)? I've looked at the source, but I can't find anything.
When running on a real device, an attached view's #getContext() returns the Activity. The view can cast it and call #findViewById(...) to obtain a reference to some other view.
But when running in a WYSIWYG editor, #getContext() returns an instance of a different class. I'm getting com.android.layoutlib.bridge.android.BridgeContext. This class isn't part of the public API, so I'm planning to access it via reflection and degrade gracefully if the implementation changes.
If you're wondering why my view wants a reference to another view... I've created a view that appears to have a hole in it. It works by delegating its drawing to another view. If the view with the hole is placed on top of other views, then it appears to punch a hole through any views beneath it, all the way down to the view it's using for drawing. It works perfectly on a real device, but it would be nice to have it also work in the WYSIWYG editor.
It's bad to assume that View.getContext(), or any other platform method that returns Context, can be cast directly to more concrete classes, like Activity. There exist classes like ContextThemeWrapper which can easily destroy your assumption.
I would recommend restructuring what you are doing so that you have a parent layout that can act as an intermediary for the hole-y View and what's below it.
Or you could have a setter which would provide the View for you.
A last option is to call View.getParent() a bunch of times to get the root View and call findViewById() on that:
ViewParent parent;
while(getParent() != null) {
parent = getParent();
}
View root = (View) parent;
root.findViewById(R.id.my_view);
BTW, BridgeContext is used in the WYSIWYG in place of Activity because it only mocks the Android View/Layout/Rendering system, it doesn't emulate it completely. This can be seen in other ways like how it renders shadows or shape drawable rounded corners.
I awarded the bounty to dandc87 because his answer led me to the solution. However, the code snippet in his answer crashes with a ClassCastException because the root ViewParent is not a View. The mods keep rejecting my edits, so here's the complete and correct solution:
private View findPeerById(int resId) {
View root = this;
while(root.getParent() instanceof View) {
root = (View) root.getParent();
}
return root.findViewById(resId);
}

How to extend a complex Android view like the GridView?

Background:
I'm looking for a way to extend the GridView I need implement a col- and row-span like in HTML. I have hunderds of elements so I cannot use e.g. a TableLayout. However that would be too localized. So far I reduce the question to how to extend the GridView?
What I have tried and where I have failed:
My first approach was to look which methods I need to replace and I found some private methods which cannot be overriden. So I tried to copy the hole source code from the GridView to a new class called CustomGrid. That was a terrible failure since I cannot access the com.android.internal.R class.
Then I dropped that idea and looked if I can normal extend the class and replace all the private methods with custom copies. I took a pen and build a huge tree where all methods are used.
After I found all references I tried to extend the class normal and I added the first method:
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
// ...
Here is my next problem that member variables mBottom and mTop are unknown. I digged a little through the sources and found them finally in the View class, but unfortunately they are hidden.
/**
* The distance in pixels from the top edge of this view's parent
* to the bottom edge of this view.
* {#hide}
*/
#ViewDebug.ExportedProperty(category = "layout")
protected int mBottom;
Question:
How can I extend the GridView without hitting that lamentations and without the usage of massive reflection? I mean it seems to be impossible to write that as a pure custom control.
How can I extend the GridView without hiting that limentations and without the usage of massive reflection?
Most likely, you don't. You copy the code into your project and modify to suit, including all requisite superclasses (up to ViewGroup). I will be stunned if you can achieve your aims by a simple subclass of GridView. You may even have to completely write your desired widget from scratch.
That was a terrible failior due I cannot access the com.android.internal.R class.
You will also need to copy over relevant resources, then fix up R references to point to your own resources.
but unforcantly they are hidden.
You find other ways of getting this data. mBottom, for example, can be changed to getBottom().

Android Development :: View Objects

I'm fairly new to Java & Android, so I have little idea what I am doing.
My test program successfully creates view objects, so now I am trying to organize my code.
I want to make a separate class to manage my GUI, but it always fails.
Basic info about my gui class:
package horc.gui;
...
public class GUI{
private Context context;
From my main activity, I constructed that gui class with the app context.
gui=new GUI(getApplicationContext()); // gui is a var of type GUI, & this sets the context of the class
The problem is when I make/modify a view object that is in the class from outside, it throws an exception.
My main activity...
package horc.test;
...
GUI gui;
LinearLayout test=gui.newLinear("ff", "v"); // <-- this sets fill parent for width/height
// & vertical orientation of the vertical layout.
// Doesnt work for the reason stated above.
// I cannot manage any view objects from a separate class.
gui.lastText.setText("##########"); // <-- a variable in the class to hold the view object I am manipulating
setContentView(t);
...calls this class function:
public TextView newText(String text){
TextView test=new TextView(context);
lastLinear.addView(test);
return test;
}
I tested this similar body within the main activity & it worked fine. It only fails when I do anything from outside that gui class.
Is there a common issue that people run into when managing view objects in separate classes? I have absolutely no idea what I am doing at this point. Coming from C++, java seems like a nutty language. I cannot plan things the way I would in C++.
instead of
gui=new GUI(getApplicationContext());
try
gui=new GUI(MyActivity.this);
Please put your activity name instead of MyActivity

Categories

Resources