New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Templated base class : Cannot find symbol class ActivityType in LightCycleBinder #49

Closed
MehdiChouag opened this Issue Jun 14, 2016 · 15 comments

Comments

5 participants
@MehdiChouag
Copy link

MehdiChouag commented Jun 14, 2016

Hello,

I'm trying to reproduce the same behavior as LightCycleAppCompatActivity in a BaseActivity, so I implemented LightCycleDispatcher<ActivityLightCycle<T>> in my base class and dispatch event in each lifecycle callback.

My base class prototype :

public abstract class BaseActivity<ActivityType extends BaseActivity> extends AppCompatActivity
    implements LightCycleDispatcher<ActivityLightCycle<ActivityType>>

which is the same as LightCycleAppCompatActivity :

public abstract class LightCycleAppCompatActivity<ActivityType extends LightCycleAppCompatActivity>
        extends AppCompatActivity
        implements LightCycleDispatcher<ActivityLightCycle<ActivityType>> 

The issue is when I use my base class LightCycleBinder cannot resolve the type of my child activity and generate this code below :

public final class MainActivity$LightCycleBinder {
  public static void bind(MainActivity target) {
    final com.soundcloud.lightcycle.ActivityLightCycle<ActivityType> mPresenter$Lifted = com.soundcloud.lightcycle.LightCycles.lift(target.mPresenter);
    target.bind(mPresenter$Lifted);
  }
}

But when my MainActivity extends directly LightCycleAppCompatActivity the type is revolved :

public final class MainActivity$LightCycleBinder {
  public static void bind(MainActivity target) {
    final com.soundcloud.lightcycle.ActivityLightCycle<MainActivity> mPresenter$Lifted = com.soundcloud.lightcycle.LightCycles.lift(target.mPresenter);
    target.bind(mPresenter$Lifted);
  }
}

I tried to create another base class with exactly the same code contain in LightCycleAppCompatActivity but it still doesn't work, it's seems to work only with LightCycleAppCompatActivity.

It's there any way to make it work using my BaseActivity class

@glung

This comment has been minimized.

Copy link
Contributor

glung commented Jun 14, 2016

@MehdiChouag Do you have LightCycles declared in your templated BaseActivity ?

If yes, then you would have to declare you would have to do something like :

public class BaseActivity extends MyDispatcherActivity<BaseActivity> {

The problem is that because of the way annotation processors work, the binder failed to determine what is the actual type of the activity (it just took the template name, i.e. ActivityType).

Does it make sense ?

@MehdiChouag

This comment has been minimized.

Copy link
Author

MehdiChouag commented Jun 14, 2016

@glung By saying "Do you have LightCycles declared in your templated BaseActivity ?" are you talking about calling this method LightCycles.bind(this) in my BaseActivity ? If this is your question the answer is yes.

So to make it work I should make my BaseActivity extends LightCycleAppCompatActivity like this ?

public abstract class BaseActivity extends LightCycleAppCompatActivity<BaseActivity>

If I use the solution above my child presenter will have an instance of BaseActivity.
So my MainActivity would be declared this way :

public class MainActivity extends BaseActivity 

And my MainActivity's presenter :

public class MainPresenter extends DefaultActivityLightCycle<BaseActivity>

But I would like my presenter receive MainActivity in the lifecycle callbacks like this :

public class MainPresenter extends DefaultActivityLightCycle<MainActivity>

Is there any solution possible to make it work this way ?

@glung

This comment has been minimized.

Copy link
Contributor

glung commented Jun 15, 2016

Can you send me your original solution (with the compiling issue), please ? I will be easier actually.

@MehdiChouag

This comment has been minimized.

Copy link
Author

MehdiChouag commented Jun 15, 2016

@glung Here is the full implementation of my original solution.

My BaseActivity :

/**
 * Base {@link android.support.v7.app.AppCompatActivity} class for every Activity in this
 * application.
 *
 * Implement {@link OnAccountsUpdateListener} to detect whenever Account change on the device.
 */
public abstract class BaseActivity<T extends BaseActivity> extends AppCompatActivity
    implements LightCycleDispatcher<ActivityLightCycle<T>>, OnAccountsUpdateListener,
    AccountManagerCallback<Bundle> {

  public static final String EXTRA_IS_ACCOUNT_NEEDED = "com.edenbeat.view.activities.EXTRA_IS_ACCOUNT_NEEDED";

  private final ActivityLightCycleDispatcher<T> mLightCycleDispatcher;

  @Bind(R.id.toolbar) protected Toolbar mToolbar;

  protected AccountManager mAccountManager;

  private boolean mIsAccountEnable;

  /**
   * Default Constructor.
   */
  public BaseActivity() {
    mLightCycleDispatcher = new ActivityLightCycleDispatcher<>();
  }

  @Override
  public void bind(ActivityLightCycle<T> lightCycle) {
    mLightCycleDispatcher.bind(lightCycle);
  }

  @DebugLog
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(getLayoutResources());

    initializeInjector();

    ButterKnife.bind(this);
    LightCycles.bind(this);

    init();

    mAccountManager = mIsAccountEnable ? AccountManager.get(this) : null;
    mLightCycleDispatcher.onCreate(activity(), savedInstanceState);
  }

  /**
   * Default implementation sets Toolbar as support ActionBar.
   */
  @SuppressWarnings("unused")
  protected void initToolbar() {
    if (mToolbar != null) {
      setSupportActionBar(mToolbar);
    }
  }

  /**
   * Init with data getting from {@link Intent}
   */
  private void init() {
    final Intent intent = getIntent();

    mIsAccountEnable = intent.getBooleanExtra(EXTRA_IS_ACCOUNT_NEEDED, true);
  }

  /**
   * Initialize Dagger's injector.
   */
  protected void initializeInjector() {
    /* no-op */
  }

  /**
   * Callback triggered when the
   * {@link com.edenbeat.account.AccountAuthenticator#addAccount(android.accounts.AccountAuthenticatorResponse,
   * String, String, String[], Bundle)} finish.
   *
   * @param future contains a Bundle with account information.
   */
  @Override
  public void run(AccountManagerFuture<Bundle> future) {
    try {
      final Bundle bundle = future.getResult();
      final Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
      startActivity(intent);
      finish();
    } catch (OperationCanceledException | IOException | AuthenticatorException e) {
      e.printStackTrace();
    }
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    mLightCycleDispatcher.onNewIntent(activity(), intent);
  }

  @Override
  protected void onStart() {
    super.onStart();
    mLightCycleDispatcher.onStart(activity());
  }

  /**
   * Register to Account update.
   */
  @Override
  protected void onResume() {
    super.onResume();
    mLightCycleDispatcher.onResume(activity());
    if (mIsAccountEnable) {
      mAccountManager.addOnAccountsUpdatedListener(this, null, true);
    }
  }

  /**
   * Unregister to Account update.
   */
  @Override
  protected void onPause() {
    mLightCycleDispatcher.onPause(activity());
    super.onPause();
    if (mIsAccountEnable) {
      mAccountManager.removeOnAccountsUpdatedListener(this);
    }
  }

  @Override
  protected void onStop() {
    mLightCycleDispatcher.onStart(activity());
    super.onStop();
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    mLightCycleDispatcher.onSaveInstanceState(activity(), outState);
  }

  @Override
  protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    mLightCycleDispatcher.onRestoreInstanceState(activity(), savedInstanceState);
  }

  /**
   * Release Account Manager.
   */
  @Override
  protected void onDestroy() {
    mLightCycleDispatcher.onDestroy(activity());
    super.onDestroy();
    mAccountManager = null;
  }

  /**
   * Callback trigger when the Accounts on the device change.
   * If the EdenBeat Account doesn't exist, that
   * launch will {@link com.edenbeat.onboarding.AuthenticatorActivity}
   *
   * @param accounts Array of accounts present on the device.
   */
  @Override
  public void onAccountsUpdated(Account[] accounts) {
    if (!AccountUtils.isAccountExist(AccountConstants.ACCOUNT_TYPE, accounts)) {
      if (BuildConfig.DEBUG) {
        Timber.d("onAccountsUpdate triggered");
      }
      mAccountManager.addAccount(AccountConstants.ACCOUNT_TYPE, AccountConstants.AUTHTOKEN_TYPE,
                                 null, null, null, this, null);
    }
  }

  /**
   * Get the Activity Layout to inflate.
   *
   * @return {@link com.edenbeat.R.layout}
   */
  @LayoutRes
  protected abstract int getLayoutResources();

  /**
   * Get the Main Application component for dependency injection.
   *
   * @return {@link ApplicationComponent}
   */
  protected ApplicationComponent getApplicationComponent() {
    return EBApplication.get(this)
        .getApplicationComponent();
  }

  /**
   * Get the Application scoped component for dependency injection.
   *
   * @return {@link ActivityComponent}
   */
  protected ActivityComponent getActivityComponent() {
    return getApplicationComponent().plus(getActivityModule());
  }

  /**
   * Get an Activity module for dependency injection.
   *
   * @return {@link ActivityModule}
   */
  protected ActivityModule getActivityModule() {
    return new ActivityModule(this);
  }

  @SuppressWarnings("unchecked")
  private T activity() {
    return (T) this;
  }

My child activity :

/**
 * Search music or artist activity
 */
public class SearchActivity extends BaseActivity<SearchActivity>
    implements com.edenbeat.search.SearchView, SearchView.OnQueryTextListener {

  @Inject @LightCycle SearchPresenter mPresenter;

  /**
   * Create the activity intent
   *
   * @param context needed to instantiate an {@link Intent}
   * @return {@link Intent}
   */
  @NonNull
  public static Intent createIntent(@NonNull Context context) {
    return new Intent(context, SearchActivity.class);
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
      actionBar.setDisplayHomeAsUpEnabled(true);
    }
  }

  @Override
  protected void initializeInjector() {
    getSearchComponent().inject(this);
    mPresenter.setView(this);
  }

  @Override
  protected int getLayoutResources() {
    return R.layout.activity_search;
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.search, menu);

    // Associate searchable configuration with the SearchView
    SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView = (SearchView) menu.findItem(R.id.action_search)
        .getActionView();
    searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
    searchView.setOnQueryTextListener(this);
    searchView.setIconified(false);

    return true;
  }

  @Override
  public boolean onQueryTextSubmit(String query) {
    mPresenter.startSearch(query);
    return true;
  }

  @Override
  public boolean onQueryTextChange(String newText) {
    mPresenter.startSearch(newText);
    return true;
  }

  @Override
  public void showLoadingView() {

  }

  @Override
  public void hideLoadingView() {

  }

  @Override
  public void showRetry() {

  }

  @Override
  public void hideRetry() {

  }

  @Override
  public void showError(@NonNull String message) {

  }

  @Override
  public void displaySearchResult(List<SearchResult> responses) {

  }

  @NonNull
  @Override
  public Context getViewContext() {
    return getApplicationContext();
  }

  private SearchComponent getSearchComponent() {
    return getApplicationComponent().plus(new SearchModule());
  }
}

My child presenter :

/**
 * {@link com.edenbeat.base.Presenter} that controls communication between views and models of the
 * presentation
 * layer of the search.
 */
public class SearchPresenter extends DefaultActivityLightCycle<SearchActivity>
    implements Presenter<SearchView> {

  private UseCase mUseCase;
  private SearchErrorMessage mErrorMessage;
  private ErrorConverter mErrorConverter;

  private SearchView mView;

  /**
   *
   * @param useCase Interactor representing search use case.
   * @param errorConverter Used to getting error message.
   * @param searchErrorMessage Used to get an error message depending the exception threw.
   */
  @Inject
  public SearchPresenter(@Named("search") UseCase useCase,
      @Named("ApiConverter") ErrorConverter errorConverter, SearchErrorMessage searchErrorMessage) {
    mUseCase = useCase;
    mErrorConverter = errorConverter;
    mErrorMessage = searchErrorMessage;
  }

  /**
   * Start the search using query parameter.
   *
   * @param query Research query
   */
  @SuppressWarnings("unchecked")
  public void startSearch(String query) {
    final Subscriber subscriber = getSubscriber();
    mUseCase.execute(subscriber, query);
  }

  @Override
  public void onCreate(SearchActivity activity, Bundle bundle) {
    super.onCreate(activity, bundle);
    Timber.d("OnCreate called");
  }

  @Override
  public void onDestroy(SearchActivity activity) {
    super.onDestroy(activity);
    mUseCase.unsubscribe();
  }

  @Override
  public void setView(@NonNull SearchView view) {
    mView = view;
  }

  private SearchSubscriber getSubscriber() {
    return new SearchSubscriber(mView.getViewContext(), mErrorMessage, mErrorConverter);
  }

  private final class SearchSubscriber extends DefaultSubscriber<List<SearchResult>> {

    public SearchSubscriber(@NonNull Context context, @NonNull AbstractErrorMessage errorMessage,
        @NonNull ErrorConverter errorConverter) {
      super(context, errorMessage, errorConverter);
    }

    @Override
    public void onCompleted() {
      Timber.d("onComplete");
    }

    @Override
    public void onError(@NonNull Exception e) {
      mView.showError(e.getMessage());
      Timber.d(e.getMessage());
    }

    @Override
    public void onNext(List<SearchResult> results) {
      Timber.d(String.valueOf(results.size()));
      mView.displaySearchResult(results);
    }
  }
}

The error returned by the compiler :

/Users/mehdichouag/github/EdenBeat-Android/presentation/build/generated/source/apt/debug/com/edenbeat/search/SearchActivity$LightCycleBinder.java:5: error: cannot find symbol
    final com.soundcloud.lightcycle.ActivityLightCycle<T> mPresenter$Lifted = com.soundcloud.lightcycle.LightCycles.lift(target.mPresenter);
                                                       ^
  symbol:   class T

And the generated class Binder

public final class SearchActivity$LightCycleBinder {
  public static void bind(SearchActivity target) {
    final com.soundcloud.lightcycle.ActivityLightCycle<T> mPresenter$Lifted = com.soundcloud.lightcycle.LightCycles.lift(target.mPresenter);
    target.bind(mPresenter$Lifted);
  }
}
@MehdiChouag

This comment has been minimized.

Copy link
Author

MehdiChouag commented Jun 25, 2016

@glung Did you figured out how I can resolve this problem ?

@glung

This comment has been minimized.

Copy link
Contributor

glung commented Jun 28, 2016

Hello @MehdiChouag, I just took a look.

You spotted a weakness, we should at least update the documentation. Long story short, you should set the type explicitly, i.e. make your SearchActivity implements LightCycleDispatcher<ActivityLightCycle<SearchActivity>>.

Note : You could also make your BaseActivity implements LightCycleDispatcher<ActivityLightCycle<BaseActivity>> but then the SearchPresenter won't accept SearchActibity.

This is because of the Processor not beeing able to determine the dispatched type because it did not recognize your BaseActivity. As of now, it only recognizes the following

glung added a commit that referenced this issue Jun 28, 2016

@glung glung closed this Jun 28, 2016

@cesards

This comment has been minimized.

Copy link

cesards commented Oct 31, 2016

I had the same problem. The documentation about using a dispatcher is not "clear" at all.

@glung

This comment has been minimized.

Copy link
Contributor

glung commented Nov 1, 2016

@cesards thx for the feedback (I will update it asap).

If you have some feedback, could you take some time to add it to the v2 discussion, please ?
#48

@michaelengland I think this conversation here is interesting for you too.

@glung

This comment has been minimized.

Copy link
Contributor

glung commented Nov 1, 2016

@cesards does it make sense ? https://github.com/soundcloud/lightcycle/commits/master

If no, I encourage you to either create a PR or provide details here. Thx !

@cesards

This comment has been minimized.

Copy link

cesards commented Nov 1, 2016

Thanks for the fast response! I commented the commit. BTW I still owe you a response for this but I wanted to explain in details what I think the is missing in the library. I think it's a good step to have the code more maintainable. If you are working in a second version. Let me know if you need help. It's a library it's worth contributing on, and even if I know some concepts about how an annotation processor works behind the scenes, it'd be a good change about getting into it :-D

@michaelengland

This comment has been minimized.

Copy link
Contributor

michaelengland commented Nov 6, 2016

This is the same problem I had, hence adding the base classes. I'm currently working on a fair number of changes that we needed/wanted and the PR will be coming up shortly. In the changes I've done, it currently removes the logic around lifting, which is causing the problems here.

@cesards

This comment has been minimized.

Copy link

cesards commented Nov 6, 2016

👍

@glung

This comment has been minimized.

Copy link
Contributor

glung commented Nov 7, 2016

@michaelengland The pb here is that the processor does not understand the real type. If you remove type lifting (Dispatcher< A >, capable of dispatching B when B extends A), SoundCloud won't be able to use LC anymore. So it will have to be a fork.

@michaelengland

This comment has been minimized.

Copy link
Contributor

michaelengland commented Nov 7, 2016

@glung Oh sure, I think we should bring it back in, but until now it was pretty coupled to the processor & the binding, so I've removed it during the work I was doing and maybe we can come up with an abstraction around it. I'll create the PR in the next week or so when I've got somewhere, and let's chat from there 👍

@renaudfavier

This comment has been minimized.

Copy link

renaudfavier commented Nov 21, 2016

From the documentation I understand that this is an example of an inheritable LightCycleDispatcher Activity, but it uses exactly what cause problem here right ?
I followed the example and got the same build error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment