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
Related
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.
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().
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
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).
In my Android app, I have two custom view classes - PortraitClass and LandscapeClass. They both do the same thing. On running the app, the view class fetches some pictures from an SDCard and then maniputlates (skew etc) and displays them. The only difference between the two classes is that the layout of pictures is slightly different on the screen.
I have two display.xml files (one under layout folder and the other under layout-land). The one under the layout folder adds Portrait and the other adds the Landscape class.
On orientation change, I would like to send information (picture numbers and few bitmaps) from one class to another so I won't have to fetch all the bitmaps again and also so that I display the ones that were being displayed.
I find the parcelable thing kind of confusing. I tried following this_example, but I noticed that in onRestoreInstance, the Parcelable has null for the "mSuperState" and I get a classCastException # "SavedState ss = (SavedState)state". The integer (picture number) that I am trying to pass is there. I am not sure what I am doing wrong.
You could use a global singleton in the Application class. For example an "AssetManager" is available here:
public class YourApplication extends Application {
public AssetManager assetManager = new AssetManager();
#Override
public void onCreate()
{
super.onCreate();
}
}
You can then call it from another activity:
YourApplication application = ((YourApplication ) this.getApplication());
application.assetManager.someFunction();
Not sure if this is what you are looking for but my stuff always works in orientation change with onCreate(Bundle savedInstanceState). Mine is all text in edit text boxes though so I am not sure how it would work for you.
Also check this out about midway down under "Persisting State Information During Configuration Change"
http://www.devx.com/wireless/Article/40792/1954