App crashes on First Open only when AdMob is added but working fine on next open
up vote
0
down vote
favorite
My App is working fine and then I tried to add AdMob dependencies. That's when things get complicated. On first install and then open App crashes. But on second and consecutive open the app is working fine, all ads are showing.
For me, this is a bad user experience because at first open they think it is not a good app. And maybe they will uninstall it immediately.
Maybe there's something I need to improve or add but I can't figure it out. I need help regarding polishing my code.
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gumangan.uecfispiritualhymns">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- TODO: Change android:value when final building -->
<!--Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713-->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
<activity
android:name="com.gumangan.uecfispiritualhymns.DictionaryActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity android:name="com.gumangan.uecfispiritualhymns.WordDetailActivity"
android:parentActivityName="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.gumangan.uecfispiritualhymns.DictionaryActivity"/>
</activity>
</application>
Gradle Properties
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://maven.google.com"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Gradle Dependencies
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.gumangan.uecfispiritualhymns"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.google.android.gms:play-services-ads:17.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
DatabaseHelper
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.preference.PreferenceManager
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class DatabaseHelper(private var mContext: Context) : SQLiteOpenHelper(mContext, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private val DATABASE_NAME = "simple_dict.db"
private val DATABASE_VERSION = 1
val DB_CREATED = "DB_CREATED"
}
private var mCreateDb = false
private var mUpgradeDb = false
private fun copyDatabaseFromAssets(db: SQLiteDatabase?) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = mContext.assets.open(DATABASE_NAME)
outputStream = FileOutputStream(db?.path)
val buffer = ByteArray(1024)
var length: Int = inputStream!!.read(buffer)
while (length > 0) {
outputStream.write(buffer, 0, length)
length = inputStream.read(buffer)
}
outputStream.flush()
val copiedDb = mContext.openOrCreateDatabase(DATABASE_NAME, 0, null)
val isDbCreated = copiedDb != null
copiedDb.execSQL("PRAGMA user_version = $DATABASE_VERSION")
copiedDb.close()
// DB_CREATED
val sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext)
val sharePrefEditor = sharedPref.edit()
sharePrefEditor.putBoolean(DB_CREATED, isDbCreated)
sharePrefEditor.apply()
} catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error copying database.")
} finally {
try {
outputStream?.close()
inputStream?.close()
}catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error closing stream.")
}
}
}
override fun onCreate(db: SQLiteDatabase?) {
mCreateDb = true
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if(newVersion > oldVersion) {
mUpgradeDb = true
}
}
override fun onOpen(db: SQLiteDatabase?) {
if(mCreateDb) {
mCreateDb = false
copyDatabaseFromAssets(db)
}
if(mUpgradeDb) {
mUpgradeDb = false
copyDatabaseFromAssets(db)
}
}
fun getWords(wordPrefix: String = ""): Cursor {
return if(wordPrefix.isBlank()) {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
null, null, null, null,
"${DictionaryEntryContract.COLUMN_ID} ASC")
} else {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_WORD} LIKE ?", arrayOf("$wordPrefix%"),
null, null, "${DictionaryEntryContract.COLUMN_ID} ASC")
}
}
fun getWord(id: String): Cursor {
return readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_ID}= ?", arrayOf(id),
null, null, null)
}
}
SearchListAdapter
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
class SearchListAdapter(context: Context, cursor: Cursor) : CursorAdapter(context, cursor, 0) {
private class ViewHolder {
var txtWord: TextView? = null
var txtType: TextView? = null
var txtMeaning: TextView? = null
var wordColumnIndex: Int = 0
var typeColumnIndex: Int = 0
var meaningColumnIndex: Int = 0
}
override fun newView(context: Context?, cursor: Cursor?, parent: ViewGroup?): View {
val layoutInflater = LayoutInflater.from(context)
val newView = layoutInflater.inflate(R.layout.search_list_item, parent, false)
val viewHolder = ViewHolder()
viewHolder.txtWord = newView.findViewById<TextView>(R.id.txtWord)
viewHolder.txtType = newView.findViewById<TextView>(R.id.txtType)
viewHolder.txtMeaning = newView.findViewById<TextView>(R.id.txtMeaning)
viewHolder.wordColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD)
viewHolder.typeColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE)
viewHolder.meaningColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING)
newView.tag = viewHolder
return newView
}
override fun bindView(view: View?, context: Context?, cursor: Cursor?) {
val viewHolder = view!!.tag as ViewHolder
viewHolder.txtWord?.text = cursor?.getString(viewHolder.wordColumnIndex)
viewHolder.txtType?.text = cursor?.getString(viewHolder.typeColumnIndex)
viewHolder.txtMeaning?.text = cursor?.getString(viewHolder.meaningColumnIndex)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = super.getView(position, convertView, parent)
if(position % 2 == 0) {
view.setBackgroundColor(Color.LTGRAY)
} else {
view.setBackgroundColor(Color.WHITE)
}
return view;
}
}
DictionaryEntryContract
package com.gumangan.uecfispiritualhymns
import android.provider.BaseColumns
class DictionaryEntryContract : BaseColumns {
private constructor()
companion object {
val TABLE_NAME = "english_words"
val COLUMN_ID = BaseColumns._ID
val COLUMN_WORD = "word"
val COLUMN_TYPE = "type"
val COLUMN_MEANING = "meaning"
}
}
DictionaryActivity
package com.gumangan.uecfispiritualhymns
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v7.widget.SearchView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.AdapterView
import android.widget.ListView
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView
import com.google.android.gms.ads.MobileAds
import java.lang.ref.WeakReference
class DictionaryActivity : AppCompatActivity() {
companion object {
val TAG = "DictionaryActivity"
val LAST_SEARCH_WORD: String = "LAST_SEARCH_WORD"
}
var mDbHelper: DatabaseHelper? = null
private var mSearchListAdapter: SearchListAdapter? = null
private var mSearchQuery: String = ""
//Google Ads Banner Variable
lateinit var mAdView : AdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.content_dictionary)
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
mDbHelper = DatabaseHelper(applicationContext)
if(!isDbLoaded()) {
showLoadingUI()
LoadViewTask(this).execute()
} else {
showDictUI()
}
/* TODO: Change ADMOB ID's when final building */
//Google Ads
//MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Test Ads
MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Ads Banner
mAdView = findViewById(R.id.adView)
val adRequest = AdRequest.Builder().build()
mAdView.loadAd(adRequest)
}
private fun isDbLoaded(): Boolean {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
return sharedPref.getBoolean(DatabaseHelper.DB_CREATED, false)
}
private fun showLoadingUI() {
setContentView(R.layout.activity_dictionary_loading)
}
private fun showDictUI() {
setContentView(R.layout.activity_dictionary)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setIcon(R.mipmap.ic_launcher)
mSearchListAdapter = SearchListAdapter(applicationContext, mDbHelper!!.getWords(mSearchQuery))
val lstWords = (findViewById<ListView>(R.id.lstWords))
lstWords.adapter = mSearchListAdapter
lstWords.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, id ->
val wordDetailIntent = Intent(applicationContext, WordDetailActivity::class.java)
wordDetailIntent.putExtra(WordDetailActivity.WORD_ID, "$id")
startActivity(wordDetailIntent)
}
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putString(LAST_SEARCH_WORD, mSearchQuery)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
super.onRestoreInstanceState(savedInstanceState)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if(intent?.action.equals(Intent.ACTION_SEARCH)) {
val searchQuery = intent?.getStringExtra(SearchManager.QUERY) ?: ""
updateListByQuery(searchQuery)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_dictionary, menu)
val searchView: SearchView? = menu.findItem(R.id.action_search).actionView as? SearchView
val searchManager: SearchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
updateListByQuery(newText ?: "")
return true
}
})
return true
}
private fun updateListByQuery(searchQuery: String) {
mSearchQuery = searchQuery
mSearchListAdapter?.changeCursor(mDbHelper!!.getWords(searchQuery))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
onSearchRequested()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private class LoadViewTask(activity: DictionaryActivity) : AsyncTask<Void, Void, Void>() {
private var mActivity = WeakReference<DictionaryActivity>(activity)
override fun doInBackground(vararg params: Void): Void? {
if(getActivityInstance()?.mDbHelper?.readableDatabase?.isOpen == true) {
Log.d(TAG, "Db is OK.")
}
return null
}
override fun onPostExecute(result: Void?) {
if(getActivityInstance()?.isDbLoaded() == true) {
getActivityInstance()?.showDictUI()
}
}
private fun getActivityInstance() = mActivity.get()
}
}
WordDetailActivity
package com.gumangan.uecfispiritualhymns
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class WordDetailActivity : AppCompatActivity() {
companion object {
const val WORD_ID = "WORD_ID"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_word_detail)
val wordId = intent.getStringExtra(WORD_ID) ?: ""
if(wordId.isBlank()) {
finish()
}
val dbHelper = DatabaseHelper(applicationContext)
val cursor = dbHelper.getWord(wordId)
if(cursor.moveToFirst()) {
val txtWord = findViewById<TextView>(R.id.txtWord)
val txtType = findViewById<TextView>(R.id.txtType)
val txtMeaning = findViewById<TextView>(R.id.txtMeaning)
txtWord?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD))
txtType?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE))
txtMeaning?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING))
}
}
}
activity_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_dictionary" />
</android.support.design.widget.CoordinatorLayout>
content_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity"
tools:showIn="@layout/activity_dictionary">
<ListView
android:id="@+id/lstWords"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--Google Ads Banner Display-->
<com.google.android.gms.ads.AdView
xmlns:ads="http://schemas.android.com/apk/res-auto"
android:id="@+id/adView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="5dp"
android:visibility="visible"
ads:adSize="LARGE_BANNER"
ads:adUnitId="ca-app-pub-3940256099942544/6300978111"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</com.google.android.gms.ads.AdView>
</android.support.constraint.ConstraintLayout>
activity_dictionary_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/dictionary_db_preparing"
android:textAlignment="center"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/dictionary_db_preparing" />
</android.support.constraint.ConstraintLayout>
search_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
tools:text="Meaning" />
</LinearLayout>
activity_word_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp"
tools:context="com.gumangan.uecfispiritualhymns.WordDetailActivity">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
tools:text="Meaning" />
</LinearLayout>
This is the LogCat error
12-09 07:52:40.554 14146-14146/com.gumangan.uecfispiritualhymns E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.gumangan.uecfispiritualhymns/com.gumangan.uecfispiritualhymns.DictionaryActivity}: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at com.gumangan.uecfispiritualhymns.DictionaryActivity.onCreate(DictionaryActivity.kt:57)
at android.app.Activity.performCreate(Activity.java:5008)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
The app is about dictionary
android database xml sqlite kotlin
New contributor
add a comment |
up vote
0
down vote
favorite
My App is working fine and then I tried to add AdMob dependencies. That's when things get complicated. On first install and then open App crashes. But on second and consecutive open the app is working fine, all ads are showing.
For me, this is a bad user experience because at first open they think it is not a good app. And maybe they will uninstall it immediately.
Maybe there's something I need to improve or add but I can't figure it out. I need help regarding polishing my code.
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gumangan.uecfispiritualhymns">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- TODO: Change android:value when final building -->
<!--Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713-->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
<activity
android:name="com.gumangan.uecfispiritualhymns.DictionaryActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity android:name="com.gumangan.uecfispiritualhymns.WordDetailActivity"
android:parentActivityName="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.gumangan.uecfispiritualhymns.DictionaryActivity"/>
</activity>
</application>
Gradle Properties
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://maven.google.com"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Gradle Dependencies
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.gumangan.uecfispiritualhymns"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.google.android.gms:play-services-ads:17.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
DatabaseHelper
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.preference.PreferenceManager
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class DatabaseHelper(private var mContext: Context) : SQLiteOpenHelper(mContext, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private val DATABASE_NAME = "simple_dict.db"
private val DATABASE_VERSION = 1
val DB_CREATED = "DB_CREATED"
}
private var mCreateDb = false
private var mUpgradeDb = false
private fun copyDatabaseFromAssets(db: SQLiteDatabase?) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = mContext.assets.open(DATABASE_NAME)
outputStream = FileOutputStream(db?.path)
val buffer = ByteArray(1024)
var length: Int = inputStream!!.read(buffer)
while (length > 0) {
outputStream.write(buffer, 0, length)
length = inputStream.read(buffer)
}
outputStream.flush()
val copiedDb = mContext.openOrCreateDatabase(DATABASE_NAME, 0, null)
val isDbCreated = copiedDb != null
copiedDb.execSQL("PRAGMA user_version = $DATABASE_VERSION")
copiedDb.close()
// DB_CREATED
val sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext)
val sharePrefEditor = sharedPref.edit()
sharePrefEditor.putBoolean(DB_CREATED, isDbCreated)
sharePrefEditor.apply()
} catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error copying database.")
} finally {
try {
outputStream?.close()
inputStream?.close()
}catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error closing stream.")
}
}
}
override fun onCreate(db: SQLiteDatabase?) {
mCreateDb = true
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if(newVersion > oldVersion) {
mUpgradeDb = true
}
}
override fun onOpen(db: SQLiteDatabase?) {
if(mCreateDb) {
mCreateDb = false
copyDatabaseFromAssets(db)
}
if(mUpgradeDb) {
mUpgradeDb = false
copyDatabaseFromAssets(db)
}
}
fun getWords(wordPrefix: String = ""): Cursor {
return if(wordPrefix.isBlank()) {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
null, null, null, null,
"${DictionaryEntryContract.COLUMN_ID} ASC")
} else {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_WORD} LIKE ?", arrayOf("$wordPrefix%"),
null, null, "${DictionaryEntryContract.COLUMN_ID} ASC")
}
}
fun getWord(id: String): Cursor {
return readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_ID}= ?", arrayOf(id),
null, null, null)
}
}
SearchListAdapter
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
class SearchListAdapter(context: Context, cursor: Cursor) : CursorAdapter(context, cursor, 0) {
private class ViewHolder {
var txtWord: TextView? = null
var txtType: TextView? = null
var txtMeaning: TextView? = null
var wordColumnIndex: Int = 0
var typeColumnIndex: Int = 0
var meaningColumnIndex: Int = 0
}
override fun newView(context: Context?, cursor: Cursor?, parent: ViewGroup?): View {
val layoutInflater = LayoutInflater.from(context)
val newView = layoutInflater.inflate(R.layout.search_list_item, parent, false)
val viewHolder = ViewHolder()
viewHolder.txtWord = newView.findViewById<TextView>(R.id.txtWord)
viewHolder.txtType = newView.findViewById<TextView>(R.id.txtType)
viewHolder.txtMeaning = newView.findViewById<TextView>(R.id.txtMeaning)
viewHolder.wordColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD)
viewHolder.typeColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE)
viewHolder.meaningColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING)
newView.tag = viewHolder
return newView
}
override fun bindView(view: View?, context: Context?, cursor: Cursor?) {
val viewHolder = view!!.tag as ViewHolder
viewHolder.txtWord?.text = cursor?.getString(viewHolder.wordColumnIndex)
viewHolder.txtType?.text = cursor?.getString(viewHolder.typeColumnIndex)
viewHolder.txtMeaning?.text = cursor?.getString(viewHolder.meaningColumnIndex)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = super.getView(position, convertView, parent)
if(position % 2 == 0) {
view.setBackgroundColor(Color.LTGRAY)
} else {
view.setBackgroundColor(Color.WHITE)
}
return view;
}
}
DictionaryEntryContract
package com.gumangan.uecfispiritualhymns
import android.provider.BaseColumns
class DictionaryEntryContract : BaseColumns {
private constructor()
companion object {
val TABLE_NAME = "english_words"
val COLUMN_ID = BaseColumns._ID
val COLUMN_WORD = "word"
val COLUMN_TYPE = "type"
val COLUMN_MEANING = "meaning"
}
}
DictionaryActivity
package com.gumangan.uecfispiritualhymns
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v7.widget.SearchView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.AdapterView
import android.widget.ListView
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView
import com.google.android.gms.ads.MobileAds
import java.lang.ref.WeakReference
class DictionaryActivity : AppCompatActivity() {
companion object {
val TAG = "DictionaryActivity"
val LAST_SEARCH_WORD: String = "LAST_SEARCH_WORD"
}
var mDbHelper: DatabaseHelper? = null
private var mSearchListAdapter: SearchListAdapter? = null
private var mSearchQuery: String = ""
//Google Ads Banner Variable
lateinit var mAdView : AdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.content_dictionary)
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
mDbHelper = DatabaseHelper(applicationContext)
if(!isDbLoaded()) {
showLoadingUI()
LoadViewTask(this).execute()
} else {
showDictUI()
}
/* TODO: Change ADMOB ID's when final building */
//Google Ads
//MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Test Ads
MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Ads Banner
mAdView = findViewById(R.id.adView)
val adRequest = AdRequest.Builder().build()
mAdView.loadAd(adRequest)
}
private fun isDbLoaded(): Boolean {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
return sharedPref.getBoolean(DatabaseHelper.DB_CREATED, false)
}
private fun showLoadingUI() {
setContentView(R.layout.activity_dictionary_loading)
}
private fun showDictUI() {
setContentView(R.layout.activity_dictionary)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setIcon(R.mipmap.ic_launcher)
mSearchListAdapter = SearchListAdapter(applicationContext, mDbHelper!!.getWords(mSearchQuery))
val lstWords = (findViewById<ListView>(R.id.lstWords))
lstWords.adapter = mSearchListAdapter
lstWords.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, id ->
val wordDetailIntent = Intent(applicationContext, WordDetailActivity::class.java)
wordDetailIntent.putExtra(WordDetailActivity.WORD_ID, "$id")
startActivity(wordDetailIntent)
}
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putString(LAST_SEARCH_WORD, mSearchQuery)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
super.onRestoreInstanceState(savedInstanceState)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if(intent?.action.equals(Intent.ACTION_SEARCH)) {
val searchQuery = intent?.getStringExtra(SearchManager.QUERY) ?: ""
updateListByQuery(searchQuery)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_dictionary, menu)
val searchView: SearchView? = menu.findItem(R.id.action_search).actionView as? SearchView
val searchManager: SearchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
updateListByQuery(newText ?: "")
return true
}
})
return true
}
private fun updateListByQuery(searchQuery: String) {
mSearchQuery = searchQuery
mSearchListAdapter?.changeCursor(mDbHelper!!.getWords(searchQuery))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
onSearchRequested()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private class LoadViewTask(activity: DictionaryActivity) : AsyncTask<Void, Void, Void>() {
private var mActivity = WeakReference<DictionaryActivity>(activity)
override fun doInBackground(vararg params: Void): Void? {
if(getActivityInstance()?.mDbHelper?.readableDatabase?.isOpen == true) {
Log.d(TAG, "Db is OK.")
}
return null
}
override fun onPostExecute(result: Void?) {
if(getActivityInstance()?.isDbLoaded() == true) {
getActivityInstance()?.showDictUI()
}
}
private fun getActivityInstance() = mActivity.get()
}
}
WordDetailActivity
package com.gumangan.uecfispiritualhymns
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class WordDetailActivity : AppCompatActivity() {
companion object {
const val WORD_ID = "WORD_ID"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_word_detail)
val wordId = intent.getStringExtra(WORD_ID) ?: ""
if(wordId.isBlank()) {
finish()
}
val dbHelper = DatabaseHelper(applicationContext)
val cursor = dbHelper.getWord(wordId)
if(cursor.moveToFirst()) {
val txtWord = findViewById<TextView>(R.id.txtWord)
val txtType = findViewById<TextView>(R.id.txtType)
val txtMeaning = findViewById<TextView>(R.id.txtMeaning)
txtWord?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD))
txtType?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE))
txtMeaning?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING))
}
}
}
activity_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_dictionary" />
</android.support.design.widget.CoordinatorLayout>
content_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity"
tools:showIn="@layout/activity_dictionary">
<ListView
android:id="@+id/lstWords"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--Google Ads Banner Display-->
<com.google.android.gms.ads.AdView
xmlns:ads="http://schemas.android.com/apk/res-auto"
android:id="@+id/adView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="5dp"
android:visibility="visible"
ads:adSize="LARGE_BANNER"
ads:adUnitId="ca-app-pub-3940256099942544/6300978111"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</com.google.android.gms.ads.AdView>
</android.support.constraint.ConstraintLayout>
activity_dictionary_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/dictionary_db_preparing"
android:textAlignment="center"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/dictionary_db_preparing" />
</android.support.constraint.ConstraintLayout>
search_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
tools:text="Meaning" />
</LinearLayout>
activity_word_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp"
tools:context="com.gumangan.uecfispiritualhymns.WordDetailActivity">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
tools:text="Meaning" />
</LinearLayout>
This is the LogCat error
12-09 07:52:40.554 14146-14146/com.gumangan.uecfispiritualhymns E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.gumangan.uecfispiritualhymns/com.gumangan.uecfispiritualhymns.DictionaryActivity}: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at com.gumangan.uecfispiritualhymns.DictionaryActivity.onCreate(DictionaryActivity.kt:57)
at android.app.Activity.performCreate(Activity.java:5008)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
The app is about dictionary
android database xml sqlite kotlin
New contributor
add a comment |
up vote
0
down vote
favorite
up vote
0
down vote
favorite
My App is working fine and then I tried to add AdMob dependencies. That's when things get complicated. On first install and then open App crashes. But on second and consecutive open the app is working fine, all ads are showing.
For me, this is a bad user experience because at first open they think it is not a good app. And maybe they will uninstall it immediately.
Maybe there's something I need to improve or add but I can't figure it out. I need help regarding polishing my code.
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gumangan.uecfispiritualhymns">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- TODO: Change android:value when final building -->
<!--Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713-->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
<activity
android:name="com.gumangan.uecfispiritualhymns.DictionaryActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity android:name="com.gumangan.uecfispiritualhymns.WordDetailActivity"
android:parentActivityName="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.gumangan.uecfispiritualhymns.DictionaryActivity"/>
</activity>
</application>
Gradle Properties
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://maven.google.com"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Gradle Dependencies
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.gumangan.uecfispiritualhymns"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.google.android.gms:play-services-ads:17.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
DatabaseHelper
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.preference.PreferenceManager
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class DatabaseHelper(private var mContext: Context) : SQLiteOpenHelper(mContext, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private val DATABASE_NAME = "simple_dict.db"
private val DATABASE_VERSION = 1
val DB_CREATED = "DB_CREATED"
}
private var mCreateDb = false
private var mUpgradeDb = false
private fun copyDatabaseFromAssets(db: SQLiteDatabase?) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = mContext.assets.open(DATABASE_NAME)
outputStream = FileOutputStream(db?.path)
val buffer = ByteArray(1024)
var length: Int = inputStream!!.read(buffer)
while (length > 0) {
outputStream.write(buffer, 0, length)
length = inputStream.read(buffer)
}
outputStream.flush()
val copiedDb = mContext.openOrCreateDatabase(DATABASE_NAME, 0, null)
val isDbCreated = copiedDb != null
copiedDb.execSQL("PRAGMA user_version = $DATABASE_VERSION")
copiedDb.close()
// DB_CREATED
val sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext)
val sharePrefEditor = sharedPref.edit()
sharePrefEditor.putBoolean(DB_CREATED, isDbCreated)
sharePrefEditor.apply()
} catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error copying database.")
} finally {
try {
outputStream?.close()
inputStream?.close()
}catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error closing stream.")
}
}
}
override fun onCreate(db: SQLiteDatabase?) {
mCreateDb = true
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if(newVersion > oldVersion) {
mUpgradeDb = true
}
}
override fun onOpen(db: SQLiteDatabase?) {
if(mCreateDb) {
mCreateDb = false
copyDatabaseFromAssets(db)
}
if(mUpgradeDb) {
mUpgradeDb = false
copyDatabaseFromAssets(db)
}
}
fun getWords(wordPrefix: String = ""): Cursor {
return if(wordPrefix.isBlank()) {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
null, null, null, null,
"${DictionaryEntryContract.COLUMN_ID} ASC")
} else {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_WORD} LIKE ?", arrayOf("$wordPrefix%"),
null, null, "${DictionaryEntryContract.COLUMN_ID} ASC")
}
}
fun getWord(id: String): Cursor {
return readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_ID}= ?", arrayOf(id),
null, null, null)
}
}
SearchListAdapter
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
class SearchListAdapter(context: Context, cursor: Cursor) : CursorAdapter(context, cursor, 0) {
private class ViewHolder {
var txtWord: TextView? = null
var txtType: TextView? = null
var txtMeaning: TextView? = null
var wordColumnIndex: Int = 0
var typeColumnIndex: Int = 0
var meaningColumnIndex: Int = 0
}
override fun newView(context: Context?, cursor: Cursor?, parent: ViewGroup?): View {
val layoutInflater = LayoutInflater.from(context)
val newView = layoutInflater.inflate(R.layout.search_list_item, parent, false)
val viewHolder = ViewHolder()
viewHolder.txtWord = newView.findViewById<TextView>(R.id.txtWord)
viewHolder.txtType = newView.findViewById<TextView>(R.id.txtType)
viewHolder.txtMeaning = newView.findViewById<TextView>(R.id.txtMeaning)
viewHolder.wordColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD)
viewHolder.typeColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE)
viewHolder.meaningColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING)
newView.tag = viewHolder
return newView
}
override fun bindView(view: View?, context: Context?, cursor: Cursor?) {
val viewHolder = view!!.tag as ViewHolder
viewHolder.txtWord?.text = cursor?.getString(viewHolder.wordColumnIndex)
viewHolder.txtType?.text = cursor?.getString(viewHolder.typeColumnIndex)
viewHolder.txtMeaning?.text = cursor?.getString(viewHolder.meaningColumnIndex)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = super.getView(position, convertView, parent)
if(position % 2 == 0) {
view.setBackgroundColor(Color.LTGRAY)
} else {
view.setBackgroundColor(Color.WHITE)
}
return view;
}
}
DictionaryEntryContract
package com.gumangan.uecfispiritualhymns
import android.provider.BaseColumns
class DictionaryEntryContract : BaseColumns {
private constructor()
companion object {
val TABLE_NAME = "english_words"
val COLUMN_ID = BaseColumns._ID
val COLUMN_WORD = "word"
val COLUMN_TYPE = "type"
val COLUMN_MEANING = "meaning"
}
}
DictionaryActivity
package com.gumangan.uecfispiritualhymns
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v7.widget.SearchView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.AdapterView
import android.widget.ListView
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView
import com.google.android.gms.ads.MobileAds
import java.lang.ref.WeakReference
class DictionaryActivity : AppCompatActivity() {
companion object {
val TAG = "DictionaryActivity"
val LAST_SEARCH_WORD: String = "LAST_SEARCH_WORD"
}
var mDbHelper: DatabaseHelper? = null
private var mSearchListAdapter: SearchListAdapter? = null
private var mSearchQuery: String = ""
//Google Ads Banner Variable
lateinit var mAdView : AdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.content_dictionary)
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
mDbHelper = DatabaseHelper(applicationContext)
if(!isDbLoaded()) {
showLoadingUI()
LoadViewTask(this).execute()
} else {
showDictUI()
}
/* TODO: Change ADMOB ID's when final building */
//Google Ads
//MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Test Ads
MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Ads Banner
mAdView = findViewById(R.id.adView)
val adRequest = AdRequest.Builder().build()
mAdView.loadAd(adRequest)
}
private fun isDbLoaded(): Boolean {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
return sharedPref.getBoolean(DatabaseHelper.DB_CREATED, false)
}
private fun showLoadingUI() {
setContentView(R.layout.activity_dictionary_loading)
}
private fun showDictUI() {
setContentView(R.layout.activity_dictionary)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setIcon(R.mipmap.ic_launcher)
mSearchListAdapter = SearchListAdapter(applicationContext, mDbHelper!!.getWords(mSearchQuery))
val lstWords = (findViewById<ListView>(R.id.lstWords))
lstWords.adapter = mSearchListAdapter
lstWords.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, id ->
val wordDetailIntent = Intent(applicationContext, WordDetailActivity::class.java)
wordDetailIntent.putExtra(WordDetailActivity.WORD_ID, "$id")
startActivity(wordDetailIntent)
}
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putString(LAST_SEARCH_WORD, mSearchQuery)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
super.onRestoreInstanceState(savedInstanceState)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if(intent?.action.equals(Intent.ACTION_SEARCH)) {
val searchQuery = intent?.getStringExtra(SearchManager.QUERY) ?: ""
updateListByQuery(searchQuery)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_dictionary, menu)
val searchView: SearchView? = menu.findItem(R.id.action_search).actionView as? SearchView
val searchManager: SearchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
updateListByQuery(newText ?: "")
return true
}
})
return true
}
private fun updateListByQuery(searchQuery: String) {
mSearchQuery = searchQuery
mSearchListAdapter?.changeCursor(mDbHelper!!.getWords(searchQuery))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
onSearchRequested()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private class LoadViewTask(activity: DictionaryActivity) : AsyncTask<Void, Void, Void>() {
private var mActivity = WeakReference<DictionaryActivity>(activity)
override fun doInBackground(vararg params: Void): Void? {
if(getActivityInstance()?.mDbHelper?.readableDatabase?.isOpen == true) {
Log.d(TAG, "Db is OK.")
}
return null
}
override fun onPostExecute(result: Void?) {
if(getActivityInstance()?.isDbLoaded() == true) {
getActivityInstance()?.showDictUI()
}
}
private fun getActivityInstance() = mActivity.get()
}
}
WordDetailActivity
package com.gumangan.uecfispiritualhymns
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class WordDetailActivity : AppCompatActivity() {
companion object {
const val WORD_ID = "WORD_ID"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_word_detail)
val wordId = intent.getStringExtra(WORD_ID) ?: ""
if(wordId.isBlank()) {
finish()
}
val dbHelper = DatabaseHelper(applicationContext)
val cursor = dbHelper.getWord(wordId)
if(cursor.moveToFirst()) {
val txtWord = findViewById<TextView>(R.id.txtWord)
val txtType = findViewById<TextView>(R.id.txtType)
val txtMeaning = findViewById<TextView>(R.id.txtMeaning)
txtWord?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD))
txtType?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE))
txtMeaning?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING))
}
}
}
activity_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_dictionary" />
</android.support.design.widget.CoordinatorLayout>
content_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity"
tools:showIn="@layout/activity_dictionary">
<ListView
android:id="@+id/lstWords"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--Google Ads Banner Display-->
<com.google.android.gms.ads.AdView
xmlns:ads="http://schemas.android.com/apk/res-auto"
android:id="@+id/adView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="5dp"
android:visibility="visible"
ads:adSize="LARGE_BANNER"
ads:adUnitId="ca-app-pub-3940256099942544/6300978111"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</com.google.android.gms.ads.AdView>
</android.support.constraint.ConstraintLayout>
activity_dictionary_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/dictionary_db_preparing"
android:textAlignment="center"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/dictionary_db_preparing" />
</android.support.constraint.ConstraintLayout>
search_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
tools:text="Meaning" />
</LinearLayout>
activity_word_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp"
tools:context="com.gumangan.uecfispiritualhymns.WordDetailActivity">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
tools:text="Meaning" />
</LinearLayout>
This is the LogCat error
12-09 07:52:40.554 14146-14146/com.gumangan.uecfispiritualhymns E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.gumangan.uecfispiritualhymns/com.gumangan.uecfispiritualhymns.DictionaryActivity}: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at com.gumangan.uecfispiritualhymns.DictionaryActivity.onCreate(DictionaryActivity.kt:57)
at android.app.Activity.performCreate(Activity.java:5008)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
The app is about dictionary
android database xml sqlite kotlin
New contributor
My App is working fine and then I tried to add AdMob dependencies. That's when things get complicated. On first install and then open App crashes. But on second and consecutive open the app is working fine, all ads are showing.
For me, this is a bad user experience because at first open they think it is not a good app. And maybe they will uninstall it immediately.
Maybe there's something I need to improve or add but I can't figure it out. I need help regarding polishing my code.
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gumangan.uecfispiritualhymns">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- TODO: Change android:value when final building -->
<!--Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713-->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
<activity
android:name="com.gumangan.uecfispiritualhymns.DictionaryActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity android:name="com.gumangan.uecfispiritualhymns.WordDetailActivity"
android:parentActivityName="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.gumangan.uecfispiritualhymns.DictionaryActivity"/>
</activity>
</application>
Gradle Properties
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://maven.google.com"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Gradle Dependencies
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.gumangan.uecfispiritualhymns"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.google.android.gms:play-services-ads:17.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
DatabaseHelper
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.preference.PreferenceManager
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class DatabaseHelper(private var mContext: Context) : SQLiteOpenHelper(mContext, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private val DATABASE_NAME = "simple_dict.db"
private val DATABASE_VERSION = 1
val DB_CREATED = "DB_CREATED"
}
private var mCreateDb = false
private var mUpgradeDb = false
private fun copyDatabaseFromAssets(db: SQLiteDatabase?) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = mContext.assets.open(DATABASE_NAME)
outputStream = FileOutputStream(db?.path)
val buffer = ByteArray(1024)
var length: Int = inputStream!!.read(buffer)
while (length > 0) {
outputStream.write(buffer, 0, length)
length = inputStream.read(buffer)
}
outputStream.flush()
val copiedDb = mContext.openOrCreateDatabase(DATABASE_NAME, 0, null)
val isDbCreated = copiedDb != null
copiedDb.execSQL("PRAGMA user_version = $DATABASE_VERSION")
copiedDb.close()
// DB_CREATED
val sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext)
val sharePrefEditor = sharedPref.edit()
sharePrefEditor.putBoolean(DB_CREATED, isDbCreated)
sharePrefEditor.apply()
} catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error copying database.")
} finally {
try {
outputStream?.close()
inputStream?.close()
}catch (e: IOException) {
e.printStackTrace()
throw Error("copyDatabaseFromAssets: Error closing stream.")
}
}
}
override fun onCreate(db: SQLiteDatabase?) {
mCreateDb = true
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if(newVersion > oldVersion) {
mUpgradeDb = true
}
}
override fun onOpen(db: SQLiteDatabase?) {
if(mCreateDb) {
mCreateDb = false
copyDatabaseFromAssets(db)
}
if(mUpgradeDb) {
mUpgradeDb = false
copyDatabaseFromAssets(db)
}
}
fun getWords(wordPrefix: String = ""): Cursor {
return if(wordPrefix.isBlank()) {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
null, null, null, null,
"${DictionaryEntryContract.COLUMN_ID} ASC")
} else {
readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_WORD} LIKE ?", arrayOf("$wordPrefix%"),
null, null, "${DictionaryEntryContract.COLUMN_ID} ASC")
}
}
fun getWord(id: String): Cursor {
return readableDatabase.query(DictionaryEntryContract.TABLE_NAME, null,
"${DictionaryEntryContract.COLUMN_ID}= ?", arrayOf(id),
null, null, null)
}
}
SearchListAdapter
package com.gumangan.uecfispiritualhymns
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
class SearchListAdapter(context: Context, cursor: Cursor) : CursorAdapter(context, cursor, 0) {
private class ViewHolder {
var txtWord: TextView? = null
var txtType: TextView? = null
var txtMeaning: TextView? = null
var wordColumnIndex: Int = 0
var typeColumnIndex: Int = 0
var meaningColumnIndex: Int = 0
}
override fun newView(context: Context?, cursor: Cursor?, parent: ViewGroup?): View {
val layoutInflater = LayoutInflater.from(context)
val newView = layoutInflater.inflate(R.layout.search_list_item, parent, false)
val viewHolder = ViewHolder()
viewHolder.txtWord = newView.findViewById<TextView>(R.id.txtWord)
viewHolder.txtType = newView.findViewById<TextView>(R.id.txtType)
viewHolder.txtMeaning = newView.findViewById<TextView>(R.id.txtMeaning)
viewHolder.wordColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD)
viewHolder.typeColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE)
viewHolder.meaningColumnIndex = cursor!!.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING)
newView.tag = viewHolder
return newView
}
override fun bindView(view: View?, context: Context?, cursor: Cursor?) {
val viewHolder = view!!.tag as ViewHolder
viewHolder.txtWord?.text = cursor?.getString(viewHolder.wordColumnIndex)
viewHolder.txtType?.text = cursor?.getString(viewHolder.typeColumnIndex)
viewHolder.txtMeaning?.text = cursor?.getString(viewHolder.meaningColumnIndex)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = super.getView(position, convertView, parent)
if(position % 2 == 0) {
view.setBackgroundColor(Color.LTGRAY)
} else {
view.setBackgroundColor(Color.WHITE)
}
return view;
}
}
DictionaryEntryContract
package com.gumangan.uecfispiritualhymns
import android.provider.BaseColumns
class DictionaryEntryContract : BaseColumns {
private constructor()
companion object {
val TABLE_NAME = "english_words"
val COLUMN_ID = BaseColumns._ID
val COLUMN_WORD = "word"
val COLUMN_TYPE = "type"
val COLUMN_MEANING = "meaning"
}
}
DictionaryActivity
package com.gumangan.uecfispiritualhymns
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v7.widget.SearchView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.AdapterView
import android.widget.ListView
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView
import com.google.android.gms.ads.MobileAds
import java.lang.ref.WeakReference
class DictionaryActivity : AppCompatActivity() {
companion object {
val TAG = "DictionaryActivity"
val LAST_SEARCH_WORD: String = "LAST_SEARCH_WORD"
}
var mDbHelper: DatabaseHelper? = null
private var mSearchListAdapter: SearchListAdapter? = null
private var mSearchQuery: String = ""
//Google Ads Banner Variable
lateinit var mAdView : AdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.content_dictionary)
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
mDbHelper = DatabaseHelper(applicationContext)
if(!isDbLoaded()) {
showLoadingUI()
LoadViewTask(this).execute()
} else {
showDictUI()
}
/* TODO: Change ADMOB ID's when final building */
//Google Ads
//MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Test Ads
MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713")
//Google Ads Banner
mAdView = findViewById(R.id.adView)
val adRequest = AdRequest.Builder().build()
mAdView.loadAd(adRequest)
}
private fun isDbLoaded(): Boolean {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
return sharedPref.getBoolean(DatabaseHelper.DB_CREATED, false)
}
private fun showLoadingUI() {
setContentView(R.layout.activity_dictionary_loading)
}
private fun showDictUI() {
setContentView(R.layout.activity_dictionary)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setIcon(R.mipmap.ic_launcher)
mSearchListAdapter = SearchListAdapter(applicationContext, mDbHelper!!.getWords(mSearchQuery))
val lstWords = (findViewById<ListView>(R.id.lstWords))
lstWords.adapter = mSearchListAdapter
lstWords.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, id ->
val wordDetailIntent = Intent(applicationContext, WordDetailActivity::class.java)
wordDetailIntent.putExtra(WordDetailActivity.WORD_ID, "$id")
startActivity(wordDetailIntent)
}
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putString(LAST_SEARCH_WORD, mSearchQuery)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
mSearchQuery = savedInstanceState?.getString(LAST_SEARCH_WORD) ?: ""
super.onRestoreInstanceState(savedInstanceState)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if(intent?.action.equals(Intent.ACTION_SEARCH)) {
val searchQuery = intent?.getStringExtra(SearchManager.QUERY) ?: ""
updateListByQuery(searchQuery)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_dictionary, menu)
val searchView: SearchView? = menu.findItem(R.id.action_search).actionView as? SearchView
val searchManager: SearchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
updateListByQuery(newText ?: "")
return true
}
})
return true
}
private fun updateListByQuery(searchQuery: String) {
mSearchQuery = searchQuery
mSearchListAdapter?.changeCursor(mDbHelper!!.getWords(searchQuery))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
onSearchRequested()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private class LoadViewTask(activity: DictionaryActivity) : AsyncTask<Void, Void, Void>() {
private var mActivity = WeakReference<DictionaryActivity>(activity)
override fun doInBackground(vararg params: Void): Void? {
if(getActivityInstance()?.mDbHelper?.readableDatabase?.isOpen == true) {
Log.d(TAG, "Db is OK.")
}
return null
}
override fun onPostExecute(result: Void?) {
if(getActivityInstance()?.isDbLoaded() == true) {
getActivityInstance()?.showDictUI()
}
}
private fun getActivityInstance() = mActivity.get()
}
}
WordDetailActivity
package com.gumangan.uecfispiritualhymns
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class WordDetailActivity : AppCompatActivity() {
companion object {
const val WORD_ID = "WORD_ID"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_word_detail)
val wordId = intent.getStringExtra(WORD_ID) ?: ""
if(wordId.isBlank()) {
finish()
}
val dbHelper = DatabaseHelper(applicationContext)
val cursor = dbHelper.getWord(wordId)
if(cursor.moveToFirst()) {
val txtWord = findViewById<TextView>(R.id.txtWord)
val txtType = findViewById<TextView>(R.id.txtType)
val txtMeaning = findViewById<TextView>(R.id.txtMeaning)
txtWord?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_WORD))
txtType?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_TYPE))
txtMeaning?.text = cursor.getString(cursor.getColumnIndexOrThrow(DictionaryEntryContract.COLUMN_MEANING))
}
}
}
activity_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_dictionary" />
</android.support.design.widget.CoordinatorLayout>
content_dictionary.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity"
tools:showIn="@layout/activity_dictionary">
<ListView
android:id="@+id/lstWords"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--Google Ads Banner Display-->
<com.google.android.gms.ads.AdView
xmlns:ads="http://schemas.android.com/apk/res-auto"
android:id="@+id/adView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="5dp"
android:visibility="visible"
ads:adSize="LARGE_BANNER"
ads:adUnitId="ca-app-pub-3940256099942544/6300978111"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</com.google.android.gms.ads.AdView>
</android.support.constraint.ConstraintLayout>
activity_dictionary_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gumangan.uecfispiritualhymns.DictionaryActivity">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/dictionary_db_preparing"
android:textAlignment="center"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/dictionary_db_preparing" />
</android.support.constraint.ConstraintLayout>
search_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
tools:text="Meaning" />
</LinearLayout>
activity_word_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:padding="16dp"
tools:context="com.gumangan.uecfispiritualhymns.WordDetailActivity">
<TextView
android:id="@+id/txtWord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Word" />
<TextView
android:id="@+id/txtType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="italic"
tools:text="Type" />
<TextView
android:id="@+id/txtMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
tools:text="Meaning" />
</LinearLayout>
This is the LogCat error
12-09 07:52:40.554 14146-14146/com.gumangan.uecfispiritualhymns E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.gumangan.uecfispiritualhymns/com.gumangan.uecfispiritualhymns.DictionaryActivity}: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalStateException: findViewById(R.id.adView) must not be null
at com.gumangan.uecfispiritualhymns.DictionaryActivity.onCreate(DictionaryActivity.kt:57)
at android.app.Activity.performCreate(Activity.java:5008)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
The app is about dictionary
android database xml sqlite kotlin
android database xml sqlite kotlin
New contributor
New contributor
New contributor
asked 2 mins ago
Gumangan
12
12
New contributor
New contributor
add a comment |
add a comment |
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Gumangan is a new contributor. Be nice, and check out our Code of Conduct.
Gumangan is a new contributor. Be nice, and check out our Code of Conduct.
Gumangan is a new contributor. Be nice, and check out our Code of Conduct.
Gumangan is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209301%2fapp-crashes-on-first-open-only-when-admob-is-added-but-working-fine-on-next-open%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown