gcx-hci / thirtyinch Goto Github PK
View Code? Open in Web Editor NEWa MVP library for Android favoring a stateful Presenter
License: Apache License 2.0
a MVP library for Android favoring a stateful Presenter
License: Apache License 2.0
While I'm working on #54 I've found some strange stuff.
The test testManageViewSubscription_WithDetachSingleSub_ShouldUnsubscribe
inside RxTiPresenterSubscriptionHandlerTest
is equal to testManageViewSubscription_WithDetachView_ShouldUnsubscribe
. So I guess we can remove it?!
Probably I've copied the test code to the rx2
module which can be removed โ as well
testManageViewDisposable_DetachBeforeAttach_ShouldThrowAssertError
in RxiTiPresenterDisposableHandlerTest
(and maybe in RxTiPresenterSubscriptionHandlerTest
too) will - hopefully - never happen.
Is it currently possible to detach a view without attaching? ๐ค
Should we move the TiMockPresenter
inside the test
module? ๐ค
What about support MVP for Views?
Currently we have "hundreds" of different "test matchers".
assertEquals("ThirtyInch", "ThirtyInch")
assertThat("ThirtyInch", isEqualTo("ThirtyInch"))
assertThat("ThirtyInch").isEqualTo("ThirtyInch")
Each test look different. Which is bad.
We should use assertJ everywhere and get rid of the other matcher libs.
To prevent using Hamcrest in the future it would be great if we can exclude it from... Don't know which dependency has hamcrest as dependency. But we have to found out it and exclude it form them if possible.
How can I go about using this Library with a DialogFragment? Since we have to extend TiFragment
I can't extend DialogFragment
.
Possibly a dumb question.
The android instrumentation tests are slow, and starting the emulator takes ages. (reference #116)
Also the environment is flaky. Builds continue to fail because the emulator startup fails or the apk cannot be installed.
We only have a few instrumentation tests. Maybe we can convert them to unit tests (with some effort). Also we can get rid of the CompositeAndroid instrumentation tests once we get rid of this module #109.
I'm trying to call a method in my View through sendToView
. But it is not working ?
I have tried using getView
. It returns NPE because i'm calling the getProductsByCat
after the Presenter onAttach
public void getProductsByCat(String Category) {
// final BrowseView view = getView();
this.sendToView(BrowseView::showProgress);
rxHelper.manageSubscription(
RxFirebaseDatabase.observeValueEvent(mRef,
dataSnapshot -> {
List<Product> mProd = new ArrayList<>();
for (DataSnapshot postSnapshot : dataSnapshot.getChildren()) {
Timber.d("Count >>" + postSnapshot.getChildrenCount());
Product Prod = Product.create(postSnapshot);
mProd.add(Prod);
}
return mProd;
})
.subscribe(mProd -> {
Timber.d(mProd.get(0).productName());
sendToView(view -> {
view.hideProgress();
view.showText("Hello");
view.updateItems(mProd);
});
})
);
}
ThirtyInch has quickly become my favourite MVP implementation. Still, and I guess it's mostly due to my forgetful mind, nearly every time I implement a new Ti-based Activity
, connect everything & run, I encounter the dreaded error I've grown so accustomed to:
java.lang.IllegalArgumentException: This Activity doesn't implement a TiView interface. This is the default behaviour. Override provideView() to explicitly change this.
I'd like to propose some custom Lint checks that facilitate the daily workflow with ThirtyInch. I'm unsure how feasible this would be, especially since you allow this default behaviour to be changed on a per-Activity basis. The error would ideally be triggered on TiActivity
& CompositeActivity
sub-classes with the TiActivityPlugin
applied, if no TiView
interface is implemented.
Again, I'm probably the only one oblivious enough to always forget this step, and maybe the effort outweighs the benefit by a long shot.
RT....
Currently we have a extra thirtyinch-test
dependency which includes just a single file.
Because it is way more overhead to maintain these extra package we agreed that we want to merge it into the "main" thirtyinch
module.
We want to have a similar idea like RxJava in tests.
Just calling:
val presenterInstructor = MyPresenterImpl().test()
Currently we are not really sure if we really want to return the PresenterInstructor
or another (helper) object which hold the real presenter and the instructor...
Maybe later we want to add more functionality to it which can nicely delegate the "actions" (or maybe asserts
๐ค ) to the correct object...
When using your lib, I noticed weird side-effect, when using
compose(RxTiPresenterUtils.deliverLatestCacheToView(this))
it creates too many instances of OperatorSemaphore. After config changes and moving between screens with "Don't Keep Activities" flag. I tried to trigger GC, but it didn't help.
On the opposite side using
compose(RxTiPresenterUtils.deliverToView(this))
worked just fine. Firstly, it didn't create so many instances, secondly, after triggering GC, the existing instances of OperatorSemaphore was destroyed.
I did look into the internals of caching version it seemed to me that after presenter is destroyed lifecycle observers should be removed therefore triggering GC should clear OperatorSemaphore instances.
Can you better explain how it should work, and when GC should clear out those instances.
Couple of unrelated questions
deliverLatestCacheToView
propagate cached Error?doOnSubscribe/doOnTerminate
will crash the app when user will leave and go back to view during network request. I expect isViewReady(presenter) will emit items and deliver cache only when view fully attached.Full code
@Override protected void onWakeUp() {
super.onWakeUp();
loadChats();
}
public void loadChats() {
rxSubscriptionHelper.manageSubscription(
chatOperations.getChats(chatType)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(() -> {
if (getView() != null) {
getView().showRefreshing();
}
})
.doOnTerminate(() -> {
if (getView() != null) {
getView().stopRefresginAndHideMsgs();
}
})
.compose(RxTiPresenterUtils.deliverLatestCacheToView(this))
.subscribe(chats -> {
getView().setChats(chats);
}, e -> {
if (e instanceof RxNetworkException) {
getView().stopRefreshAndShowError(e.getMessage());
} else {
throw new RuntimeException(e);
}
})
);
}
If I receive an intent in my activity that should initialize it's state, how do I pass this to my presenter? Shouldn't onCreate in the presenter receive this intent? Or maybe there's a better way?
By the way, thanks for the great library, I've found it the best one I've tried (out of a whole bunch..).
We have noticed in #88 that we have some javadoc issues.
See this comment. (Maybe there are more than just this ๐)
We have to fix that.
For conciseness, it would be helpful to have method manageSubscriptions(Subscription ...subscription) in RxTiPresenterSubscriptionHandler
We got several cases when LeakCanary pointing on net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE
. Do you have any suggestions where we should look in?
* features.profile.my.sold.MySoldAdsFragment has leaked:
* GC ROOT static net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE
* references net.grandcentrix.thirtyinch.internal.PresenterSavior.mPresenters
* references java.util.HashMap.table
* references array java.util.HashMap$HashMapEntry[].[13]
* references java.util.HashMap$HashMapEntry.value
* references features.profile.my.favorites.MyFavoritesAdsPresenter.mView
* references features.profile.my.favorites.MyFavoritesAdsFragment.mFragmentManager
* references android.support.v4.app.FragmentManagerImpl.mActive
* references java.util.ArrayList.elementData
* references array java.lang.Object[].[2]
* leaks features.profile.my.sold.MySoldAdsFragment instance
* features.chats.list.ChatListFragment has leaked:
* GC ROOT static net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE
* references net.grandcentrix.thirtyinch.internal.PresenterSavior.mPresenters
* references java.util.HashMap.table
* references array java.util.HashMap$HashMapEntry[].[7]
* references java.util.HashMap$HashMapEntry.value
* references features.chats.list.ChatListPresenter.mView
* leaks features.chats.list.ChatListFragment instance
RxTiPresenterUtils.deliverLatestCacheToView
caches and re-delivers all items emitted by the source.
Look at this log output. When the view is detached, several items are emitted by the source, but not delivered. This is correct so far.
After the view is reattached, all of the items are delivered again, including those which had already been delivered (c9915aa
) and those which should have been dropped (6b60904
).
11-02 12:45:45.392 D: Emitting c9915aa
11-02 12:45:45.421 I: Delivering c9915aa
11-02 12:46:02.199 D: Emitting 75d8118
11-02 12:46:02.210 I: Delivering 75d8118
11-02 12:46:02.211 D: Emitting a0e7956
11-02 12:46:02.255 I: Delivering a0e7956
VIEW DETACHED
11-02 12:46:44.932 D: Emitting 6b60904
11-02 12:46:45.052 D: Emitting 27a3122
11-02 12:46:45.668 D: Emitting 50cb170
11-02 12:46:45.682 D: Emitting 1ae3d6e
11-02 12:46:46.478 D: Emitting efbdc0f
11-02 12:46:46.493 D: Emitting b085ea5
VIEW REATTACHED
11-02 12:47:09.806 I: Delivering c9915aa
11-02 12:47:09.807 I: Delivering 75d8118
11-02 12:47:09.807 I: Delivering a0e7956
11-02 12:47:09.808 I: Delivering 6b60904
11-02 12:47:09.809 I: Delivering 27a3122
11-02 12:47:09.809 I: Delivering 50cb170
11-02 12:47:09.810 I: Delivering 1ae3d6e
11-02 12:47:09.811 I: Delivering efbdc0f
11-02 12:47:09.812 I: Delivering b085ea5
11-02 12:47:15.458 D: Emitting 4cdffd
11-02 12:47:15.459 I: Delivering 4cdffd
Here is the code:
val subscription =
Observable.just(/* some object */)
.doOnNext { c -> Timber.d("Emitting %x".format(c.hashCode())) }
.delaySubscription(5, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(RxTiPresenterUtils.deliverLatestCacheToView<Object>(this))
.subscribe { c ->
Timber.i("Delivering %x".format(c.hashCode()))
view.showData(c)
}
rxHelper.manageSubscription(subscription)
Is this the expected behaviour?
In the demo, HelloWorldActivity extends TiActivity, but I can't see any method to get the presenter instance.
Most of my data is returned as Singles instead of Observables, so having Single support built-in for RxTiPresenterUtils would be great.
I can of course work around this limitation by simply doing this:
categoryService.getMainCategories()
.toObservable()
.compose(RxTiPresenterUtils.deliverLatestToView(this))
.subscribe(categories -> ()));
Could you please provide an example of VPVM as described here: https://medium.com/@passsy/thirtyinch-a-new-mvp-library-for-android-bd1a27262fd6#.724gw1bb5
Currently we are using an fori
loop to add subscriptions to the CompositeSubscprion handler.
See here.
I found out that the CompositeSubscrption
(and the RxJava2 part) have a addAll(...)
method.
Think about to use these one instead of our own fori
loop
In the real world it could be happen that we want to clear
all the subscriptions
/ disposables
,which are already added to the handler via manage*
, earlier than the handler does .
So it would be great if you can provide a clear
method to the Rx*Handler ๐
I have an Listener on my presenter. But it is getting called forever.
If I use the same code in a Activity without Ti
. Then it is working as expected.
How can i Solve this ?
public class MyPresenter extends TiPresenter<MyView> {
public MyPresenter(String email, String from) {
Email = email;
From = from;
}
@Override
protected void onAttachView(@NonNull MyView view) {
super.onAttachView(view);
listenChanges();
}
// @DistinctUntilChanged Added this annotation to stop the continuus triggering . But no hope
public void listenChanges() {
DatabaseReference myRef = DB.child(ID);
ChildEventListener myListener = cartRef.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Log.e("onChildAdded > ", dataSnapshot.getKey());
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
Log.e("onChildChanged > ", dataSnapshot.getKey());
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Log.e("onChildRemoved >", dataSnapshot.getKey());
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
myRef .addChildEventListener(myListener);
);
}
}
Activity (which doesn't involve Ti. But works Perfectly)
public class TestActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DatabaseReference myRef = DB.child(ID);
ChildEventListener myListener = cartRef.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Log.e("onChildAdded > ", dataSnapshot.getKey());
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
Log.e("onChildChanged > ", dataSnapshot.getKey());
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Log.e("onChildRemoved >", dataSnapshot.getKey());
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
myRef.addChildEventListener(myListener);
}
LOG CAT
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
It looks like there's no good way to restore the instance state of a Presenter in ThirtyInch when the process is killed and restarted.
The PresenterSavior handles the case when the process is NOT killed by essentially holding a static map of Presenters, but this map will get cleared when the process dies and the Activity gets restored.
This can be reproduced by running the sample code and taking the following steps:
After following the steps you can see in the logs
11-13 05:49:47.407 909 909 I ThirtyInch: HelloWorldActivity:TiActivity@9cbcc1d: could not recover the Presenter although it's not the first start of the Activity. This is normal when configured as .setRetainPresenterEnabled(false).
As far as I understand setRetainPresenterEnabled is true by default and no one sets it in the sample code.
Is this an oversight or is there a recommended way to take care of this case?
I use a viewpager
a) to show data in RecyclerViews
b) to create a new object
and I'm interested how to use it with your lib.
We have in our crashlytics some NPE crashes caused by onClickListeners in ViewHolders and Fragments. They ain't many but still occurs. Do you have any idea why?
Fatal Exception: java.lang.IllegalStateException: The view is currently not attached. Use 'sendToView(ViewAction)' instead.
at net.grandcentrix.thirtyinch.f.Q(TiPresenter.java:334)
at classifieds.yalla.features.chats.chat.regular.i.p(ChatPresenter.java:207)
at classifieds.yalla.features.chats.chat.regular.ChatFragment.b(ChatFragment.java:109)
at classifieds.yalla.features.chats.chat.regular.a.onClick(Unknown Source)
at android.view.View.performClick(View.java:4475)
at android.view.View$PerformClick.run(View.java:18786)
at android.os.Handler.handleCallback(Handler.java:730)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:176)
at android.app.ActivityThread.main(ActivityThread.java:5419)
at java.lang.reflect.Method.invokeNative(Method.java)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1046)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:862)
at dalvik.system.NativeStart.main(NativeStart.java)
I have a situation when user switch fragments
It happens many times and creates serious memory footprint. After 20 times or so the app may even freeze.
Basically when there is a check in onDestroy()
isFragmentInBackstack() = false
, but isFragmentRemoving() = false
in nested fragment and nothing get cleared
How to free presenter manually in such case?
Our TiLifecycleObserver
API is great an maybe the best feature of ThirtyInch. It allows everyone to add functionality to TiPresenters
. We use it for our rx
implementation and the proposed Renderer
uses it, too.
Implementing a TiLifecycleObserver
is hard because the callbacks has a state and a boolean indicating if the method was called before or after the state callback was called. Worst case scenario: You initialize things twice but only expected the initialization once.
Right now it is required to add a TiLifecycleObserver
right after initializing the TiPresenter
. If you add it later you won't receive previous emitted events. The TiLifecycleObserver
also never receives the created
event because there is no way to add the observer before getting it.
And without passing in the Presenter in the constructor it is impossible to know the current presenter state.
Also missing is a callback when the observer is added and removed from a presenter. Cleanup is currently hard/impossible.
pre
- and post
lifecycle callbacks.getPresenter()
to get the current presenter the observer is attached to and it's stateHello Pascal, I have a problem and I need to understand what could cause it, so basically I started FastHub with version rc2 & then I noticed that you have updated the lib, since I like to be updated about everything I did actually update to rc3 however I noticed that whenever the activity goes in background for awhile not to long a minute or less the data is being saved in the presenter are lost which cause the app to somehow lose an important flag or an id. I couldn't really know what is going until i reverted back to rc2 and everything seems to work fine except now the data will live for a bit longer than the rc3 . I know it seem confusing but I couldn't really explained more than that.
Could you let me know what could cause this? is it a memory problem maybe with my phone or there is something I'm doing wrong.
fyi: I'm using the default configuration which mean I didn't touch the presenter nor the view configuration.
Thank you in advance & I hope to see the library continue it's awesome work.
It'd be cool if BottomSheetDialogFragment
is supported out of the box.
Looking forward for the next release ๐
I need pass some data from SharedPrefrences to Presenter through addPlugin(). I have activity class like this, but i got npe error:
public class AddedBooksActivity extends CompositeActivity implements AddedBooksView {
private final NavigationDrawerActivityPlugin mNavigationDrawerPlugin = new NavigationDrawerActivityPlugin();
private final TiActivityPlugin<AddedBooksPresenter, AddedBooksView> mPresenterPlugin =
new TiActivityPlugin<>(new TiPresenterProvider<AddedBooksPresenter>() {
@NonNull
@Override
public AddedBooksPresenter providePresenter() {
String appKey = App.readFromPreferences(getApplicationContext(),
"user_account_key", "");
Log.i("user_account_key", appKey);
addedBooksPresenter = new AddedBooksPresenter(appKey);
return addedBooksPresenter;
}
});
public AddedBooksActivity() {
addPlugin(mPresenterPlugin);
addPlugin(mNavigationDrawerPlugin);
}
}
While I'm not using CompositeActivity and I pass SharedPreferences through providePresenter() everythings works fine.
@NonNull
@Override
public AddedBooksPresenter providePresenter() {
String appKey = App.readFromPreferences(this, "user_account_key", "");
Log.i("app", appKey);
addedBooksPresenter = new AddedBooksPresenter(appKey);
return addedBooksPresenter;
}
Can you help me with this problem?
Do you have any code that tests the sample app?
Reproduce:
showGallery(new ArrayList())
TiFragment implementing this view interface method @DistinctUntilChanged
void showGallery(List<Image> images);
new ArrayList()
)Expected behavior:
View method gets called
Actual behavior:
View method doesn't get called because it's the same data. This is wrong because onCreateView()
was called and a new view implementation was created.
Workaround:
clear the cache manually in onCreateView()
final DistinctUntilChangedInterceptor distinctUntilChangedInterceptor = (DistinctUntilChangedInterceptor)
getInterceptors(interceptor -> interceptor instanceof DistinctUntilChangedInterceptor).get(0);
distinctUntilChangedInterceptor.clearCache(this);
Hey,
I'm trying to convert this Library using ThirtyInch.
This is how my Presenter looks like
This is the Original Library's Presenter.
Now I have to inject the DataManager dependency in my Activity. This is my BrowseActivity.
This is the Original Library's BrowseFragment (For Reference)
How can I inject the Dependency in the Activity or there is any workaround ?
Missing Javadoc, copy from TiTestPresenter
header. https://github.com/grandcentrix/ThirtyInch/blob/master/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java#L389
as per the title, calling getPresenter() on Activity onCreate causes this crash for example
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
getPresenter().onSubmit(getIntent());
}
}
if (intent == null || intent.getExtras() == null) {
getView().onShowError(R.string.general_error);
}
any idea why would this happen? why wouldn't the view being initialised onCreate
?
I got a null view in onWakeUp(), if so, where can I yes be assured to have a view ready, isn't that the whole concept of having onWakeUp()?
Hi, are you planning create a thirtyinch-rx2 dependency with RxJava 2 support?
After upgrading to support library 24.2.1, the Android Studio 2.2(RC) reports "Failed to resolve: com.android.support:appcompat-v7:24.2.0".
TiTestPresenter misses a few unit tests which verify that the lifecycle will be called correctly.
I'm relatively new to MVP and the like--but with Mosby, it looks like I'd make a Presenter
as an interface and fill it with methods I want to implement.
With Ti, I'm implementing those methods in the Activity.
Am I thinking about this wrong or am I doing something wrong?
RxTiPresenterSubscriptionHandler.manageSubscription(Subscription...)
returns void. It would be cool if there is a manageSubscription
function with the following signature:
public Subscription manageViewSubscription(@NonNull final Subscription subscription)
Usage
// inside TiPresenter
val rxHelper: RxTiPresenterSubscriptionHandler(this)
val sub: Subscription? = null
/**
* download some data
*/
fun download() {
// subscription is managed, it will be cancelled when the Presenter gets destroyed
sub = rxHelper.manageSubscription(myService.downloadSomething().subscribe{ data ->
sendToView { view -> view.showData(data) }
})
}
/**
* cancel download before it completes
*/
fun cancelDownload() {
sub?.unsubscribe()
}
I am currently investigating different MVxx libraries and approaches on Android and stumbled upon this one. As a show case for each library I am creating simple list+detail (image gallery) app using REST services as data provider.
Lets assume we are using one Activity (MainActivity) and two fragments (GalleryFragment, DetailsFragment) in our app. Implementation of activity is quite straightforward as it requires no view or presenter - is simply starts and manages fragments.
Now, we need to implement GalleryFragment which needs to display a list of images.
So we need a view:
public interface GalleryView extends TiView {
void startLoading(boolean pullToRefresh);
void stopLoading();
void showError(Throwable error);
void showGallery(List<Image> images);
}
We need layout for fragment:
<?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"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:name="pl.fzymek.tiimagegallery.gallery.GalleryFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="pl.fzymek.tiimagegallery.gallery.GalleryFragment"
tools:listitem="@layout/fragment_gallery_item"/>
</android.support.v4.widget.SwipeRefreshLayout>
<TextView
tools:text="Error happened! :("
android:id="@+id/error"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
And we need a Fragment:
public class GalleryFragment extends TiFragment<GalleryPresenter, GalleryView> implements GalleryView, SwipeRefreshLayout.OnRefreshListener {
@BindView(R.id.recyclerView)
RecyclerView recyclerView;
@BindView(R.id.progress)
ProgressBar progressBar;
@BindView(R.id.error)
TextView error;
@BindView(R.id.contentView)
SwipeRefreshLayout contentView;
GalleryAdapter adapter;
Unbinder unbinder;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_gallery, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
unbinder = ButterKnife.bind(this, view);
contentView.setOnRefreshListener(this);
Context context = view.getContext();
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
recyclerView.setLayoutManager(new LinearLayoutManager(context));
} else {
recyclerView.setLayoutManager(new GridLayoutManager(context, 3));
}
adapter = new GalleryAdapter();
recyclerView.addItemDecoration(new SpaceDecoration());
recyclerView.setAdapter(adapter);
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
@NonNull
@Override
public GalleryPresenter providePresenter() {
Timber.d("providePresenter");
return new GalleryPresenter();
}
@Override
public void startLoading(boolean pullToRefresh) {
progressBar.setVisibility(View.VISIBLE);
contentView.setVisibility(View.GONE);
error.setVisibility(View.GONE);
contentView.setRefreshing(pullToRefresh);
}
@Override
public void stopLoading() {
contentView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
error.setVisibility(View.GONE);
contentView.setRefreshing(false);
}
@Override
public void showError(Throwable err) {
error.setVisibility(View.VISIBLE);
contentView.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
contentView.setRefreshing(false);
}
@Override
public void showGallery(List<Image> images) {
adapter.setData(images);
}
@Override
public void onRefresh() {
loadData(true);
}
private void loadData(boolean pullToRefresh) {
getPresenter().loadData(pullToRefresh);
}
}
And finally, we also have our presenter:
class GalleryPresenter extends TiPresenter<GalleryView> {
private final static TiConfiguration PRESENTER_CONFIGURATION = new TiConfiguration.Builder()
.setRetainPresenterEnabled(true)
.build();
private GettyImagesService service;
GalleryPresenter() {
super(PRESENTER_CONFIGURATION);
Retrofit retrofit = new Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Config.GETTYIMAGES_API_URL)
.build();
service = retrofit.create(GettyImagesService.class);
}
@Override
protected void onCreate() {
super.onCreate();
Timber.d("onCreate");
}
@Override
protected void onDestroy() {
super.onDestroy();
Timber.d("onDestroy");
}
@Override
protected void onSleep() {
super.onSleep();
Timber.d("onSleep");
}
@Override
protected void onWakeUp() {
super.onWakeUp();
Timber.d("onWakeUp");
}
public void loadData(String phrase, boolean pullToRefresh) {
getView().startLoading(pullToRefresh);
Timber.d("loadData %s", phrase);
service.getImages(phrase)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> {
getView().showGallery(result.getImages());
getView().stopLoading();
},
error -> {
getView().showError(error);
}
);
}
}
Simple as that.
What I do not understand, is when should I call loadData()
to trigger a network request, and how and when to persist retrieved data between screen orientation changes.
Usually we'd start network request somewhere in onViewCreated()
or onCreateView
method of fragment as in that moment we have our view ready. Unfortunately, when using ThirtyInch, this in not true, as view is binded to presenter in onStart()
method and calling GalleryPresenter#loadData()
throws NPE as getView()
returns null
.
Solution to this would be to defer it a little and load data in onStart()
method of fragment or onWakeUp()
method of presenter (as presented in sample, but when using onWakeUp()
I cannot pass my constructed query to the presenter). This works like a charm, but...
Since I am retaining a presenter, I see a problem with this solution when rotating a screen while network request is pending. Flow is in this case:
onStart
) and Presenter is woken up onWakeUp
-> trigger request 1onStart
) and Presenter is woken up onWakeUp
-> trigger request 2What I don't understand here is, how to correctly request data and persist it using ThirtyInch - is the persistence handled by framework (setRetainPresenterEnabled(true)
?) or should I rely on androids onSaveInstanceState()
. Not retaining a presenter would leave me nothing more that Mosby (ad described here). So what should I do in order to use all features of TI?
When the back key is pressed to pop a TiFragment
off the backstack, the Presenter does not get destroyed. Here is the log output from TiFragment
:
11-02 09:17:04.985 V: onStop()
11-02 09:17:04.985 V: onDestroyView()
11-02 09:17:04.986 V: onDestroy() recreating=true
11-02 09:17:04.986 V: onDetach()
Because the activity is not being destroyed, the destroyPresenter
flag in TiFragment#onDestroy()
does not get set to true. This means that the Presenter is leaked.
Please support update lib to ver 24.2.1
Since we open sourced our code style I think about to add it here.
We can add it together with a CONTRIBUTING.md ( ๐ค ) to Ti. We won't have conflicts or "rerange the code everytime" if third parties contribute anymore...
What do you think @passsy @rberghegger โ
How Can I get method getPresenter() while im using CompositeActivity?
First of all thank you for a library we are using it for all our presenters and fragments, around 50.
Recently we came across very strange behavior we have an activity with a chain of 10 fragments and when user reach the last one we are doing
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
then we create a different fragment. All these fragments are related to one scope which we keep inside activity and after the user leaves activity component is destroyed.
What we noticed that after one such scenario we are leaking 10 presenters. And if the user does it a couple of times we will have 20 leaked presenters.
Moreover, we investigated and found that we are using popBackStack
very actively throughout our app, that mean we have many presenters which are leaking.
How can we fix it?
I do not fully understand why use setRetainInstance(true)
in TiFragment
by default. Since library always recovers presenter from PresenterSavior
.
Hi TI team, while browsing through your README after you got mentioned in Android Dev Weekly, I would like to suggest a few improvements :
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.