Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No adapter attached; skipping layout when clicking back button and starting the app again

Android Studio 3.2 Canary 18
kotlin_version = 1.2.50

I have a simple app that uses a recyclerview and adapter. When the app starts is load all the data. However, when I click the back button and start the app again. It won't display the data (blank). If I clear the app from memory and start the app. The data will load as normal.

I am loading the data from sqlite and the data is loaded each time. as it populates the insectDataModelList.

After going into the RecyclerView.java source code the reason is the mAdapter is null. However, I have checked that the adapter is correct when I set it to the recyclerview.

void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        ...
}

My MainActivity.java is Java

public class MainActivity extends AppCompatActivity {
    private RecyclerView rvInsects;

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

        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        rvInsects = (RecyclerView)findViewById(R.id.recycler_view);

        DatabaseManager databaseManager = DatabaseManager.getInstance(this);
        databaseManager.queryAllInsects("friendlyName");
    }

    private void setupAdapter(List<InsectDataModel> insectDataModelList) {
        final LayoutManager layoutManager = new LinearLayoutManager(
                this, LinearLayoutManager.VERTICAL, false);
        rvInsects.setLayoutManager(layoutManager);
        rvInsects.setHasFixedSize(true);
        final InsectAdapter insectAdapter = new InsectAdapter(insectDataModelList);
        rvInsects.setAdapter(insectAdapter);
        insectAdapter.notifyDataSetChanged();
    }

    /* Callback from database */
    public void loadAllInsects(final Cursor cursor) {
        InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
        final List<InsectDataModel> insectDataModelList = insectInteractorMapper.map(cursor);
        /* data loaded with 24 items */
        setupAdapter(insectDataModelList);
    }
}

InsectAdapter.kt is Kotlin.

class InsectAdapter(private val insectList: MutableList<InsectDataModel>)
    : RecyclerView.Adapter<InsectAdapter.CustomInsectHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomInsectHolder {
        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.insect_row_item, parent, false)
        return CustomInsectHolder(view)
    }

    override fun onBindViewHolder(holder: CustomInsectHolder, position: Int) {
        holder.tvFriendlyName.text = insectList[position].friendlyName
        holder.tvScientificName.text = insectList[position].scientificName
    }

    override fun getItemCount(): Int {
        return insectList.size
    }

    class CustomInsectHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val ivDangerLevel: DangerLevelView = itemView.findViewById(R.id.ivDangerLevel)
        val tvFriendlyName: TextView = itemView.findViewById(R.id.tvFriendlyName)
        val tvScientificName: TextView = itemView.findViewById(R.id.tvScientificName)
    }
}

The database I use rxjava2 to do the query

public class DatabaseManager {
    private static DatabaseManager sInstance;
    private MainActivity mainActivity;
    private BugsDbHelper mBugsDbHelper;

    public static synchronized DatabaseManager getInstance(MainActivity context) {
        if (sInstance == null) {
            sInstance = new DatabaseManager(context);
        }

        return sInstance;
    }

    private DatabaseManager(MainActivity context) {
        mBugsDbHelper = new BugsDbHelper(context);

        mainActivity = context;
    }

    @SuppressLint("CheckResult")
    public void queryAllInsects(String sortOrder) {
        final InsectStorageInteractorImp insectStorageInteractorImp
                = new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));

        insectStorageInteractorImp.getAllSortedInsects(sortOrder)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new SingleObserver<Cursor>() {
                    Disposable disposable;

                    @Override
                    public void onSubscribe(Disposable d) {
                        disposable = d;
                    }

                    @Override
                    public void onSuccess(Cursor cursor) {
                        mainActivity.loadAllInsects(cursor);
                        disposable.dispose();
                    }

                    @Override
                    public void onError(Throwable e) {
                        disposable.dispose();
                    }
                });
    }
}

Everything works as expected when the apps installs for the first time. And if you clear it out of memory. However, its only when you click the back button, and then try and start the app it will not load any data because of the mAdapter being null in the RecyclerView class.

When I click the back button and then start the app again. All I get is a blank screen i.e.

enter image description here

Updated DatabaseManager class that removes the singleton and used a weakreference to ensure that the MainActivity instance is garbage collected.

public class DatabaseManager {
    private WeakReference<MainActivity> mainActivity;
    private BugsDbHelper mBugsDbHelper;

    public DatabaseManager(MainActivity context) {
        mBugsDbHelper = new BugsDbHelper(context);
        mainActivity = new WeakReference<>(context);
    }

    @SuppressLint("CheckResult")
    public void queryAllInsects(String sortOrder) {
        final InsectStorageInteractorImp insectStorageInteractorImp
                = new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));

        insectStorageInteractorImp.getAllSortedInsects(sortOrder)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new SingleObserver<Cursor>() {
                    Disposable disposable;

                    @Override
                    public void onSubscribe(Disposable d) {
                        disposable = d;
                    }

                    @Override
                    public void onSuccess(Cursor cursor) {
                        mainActivity.loadAllInsects(cursor);
                        disposable.dispose();
                    }

                    @Override
                    public void onError(Throwable e) {
                        disposable.dispose();
                    }
                });
    }
}

Many thanks for any suggestions,

like image 986
ant2009 Avatar asked Jun 23 '18 12:06

ant2009


3 Answers

When you click the back button and relaunch the app, a new instance of MainActivity is started.

At the same time, your DatabaseManager is a singleton. Its reference is stored as a static variable. It survives the activity recreation. It will live until the process is killed.

So, when you run queryAllInsects for the second time, the callback is sent to the old instance of MainActivity, which is not visible anymore.

You should not keep a reference to MainActivity in DatabaseManager. It's a memory leak, because it cannot be garbage collected.

like image 82
dev.bmax Avatar answered Oct 13 '22 00:10

dev.bmax


Few observations:

  1. You are still passing the MainActivity to the BugsDbHelper class, take care of the reference there.
  2. It's probably a good idea to include a "cleaning method" in Singleton classes, which should be called in onStop() or onDestroy() of an activity. onStop() is preferred since onDestroy() is not guaranteed to be called immediately.
  3. The "cleaning method" in Singleton class should do the following: a) Nullify any references to the parameters, objects, context or callbacks you have asked as a dependency in the constructor or otherwise. b) If the Singleton class has created "new" objects with context dependencies, make sure to include similar cleaning methods in these classes too.
  4. To avoid crashes and memory leakage in fragment/activities, make sure you are cleaning up your recycler view/adapter in onStop(). The callbacks can be received anytime, and if that happens while your activity is in the background, you are bound to get a "force close" fortune cookie.
  5. Keep an eye on the activity/fragment lifecycle. A lot of issues are just because of ignoring the lifecycle callbacks. These are there for a reason, utilize them.
like image 20
Rahul Shukla Avatar answered Oct 13 '22 01:10

Rahul Shukla


Put this 2 lines in onResume() and remove from onCreate() and try it.

 DatabaseManager databaseManager = DatabaseManager.getInstance(this);
 databaseManager.queryAllInsects("friendlyName");
like image 33
Arpit bandil Avatar answered Oct 12 '22 23:10

Arpit bandil