I am posting this question here after it having been determined to be "off-topic" for stackoverflow, and "too hypothetical" for codereview.
I am experimenting with different singleton-style design patterns for an app component having to do with app-wide sound effects and speech.
My theory is that while often frowned upon, by making these types of objects global/static/singleton-like will have advantages such as:
- Memory use being more stable/constant with a predictable baseline, making OOM errors easier to foresee.
- Expensive initializations within these objects (such as TTS engines and large audio buffers) will happen once only.
- Sounds are not cut short by activity transitions.
- Memory leaks caused by constant and laggy de-and-re-initializations of objects (for example the Google TTS engine) can be avoided by having a single global instance.
My question is: "Is the design pattern shown in my code sample feasible / acceptable / solid?" ... and by this I don't mean in your opinion -- I mean is there someone out there with more experience than me (not hard to find) that can see some reason why this code would be bug-prone?
I have mainly used the help of this article.
manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.boober.speechsingletonsimplifiedunit">
<application
android:name="App"
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">
<activity android:name=".MainActivity"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SettingsActivity"
>
</activity>
</application>
<!-- android:screenOrientation="portrait" -->
</manifest>
Application Class:
package com.example.boober.speechsingletonsimplifiedunit;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import com.squareup.leakcanary.LeakCanary;
public class App extends Application {
private static Context appContext;
public static GodOfSpeechSingleton GOSS;
@Override
public void onCreate() {
super.onCreate();
// BEGIN LEAK CANARY CODE:
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// END LEAK CANARY CODE, PROCEED...
Log.i("XXX", "App.onCreate() called.");
appContext = this;
GOSS = new GodOfSpeechSingleton();
}
public static Context getAppContext() {
return appContext;
}
}
The "GOSS":
package com.example.boober.speechsingletonsimplifiedunit;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import java.util.HashMap;
import java.util.Locale;
import java.util.Random;
public class GodOfSpeechSingleton { // aka "the GOSS"
private boolean thisClassHasBeenInitAlready = false;
TextToSpeech tts1;
boolean tts1IsInit;
public GodOfSpeechSingleton() {
Log.i("XXX", "GOSS constructor called.");
}
// This method body was created/moved (execution delayed) from the constructor to here
// because when constructor was called from Application.onCreate(),
// the code "android.content.Context.getPackageManager()" inside the TTS
// causes a null pointer exception.
public void initFromMainActivity(String caller) {
if (!caller.equals("MainActivity")) { return; }
if (thisClassHasBeenInitAlready) {
Log.i("XXX", "GOSS init already.");
return; }
thisClassHasBeenInitAlready = true;
tts1 = new TextToSpeech(App.getAppContext(), new TextToSpeech.OnInitListener() {
@Override
public void onInit(int i) {
tts1IsInit = true;
}
}, "com.google.android.tts");
}
public void speak(String stringToSpeak, Locale localeToUseForThisUtterance) {
if (!tts1IsInit) {
Log.i("XXX", "speak() called, but tts1 not init yet!");
return;
}
tts1.setLanguage(localeToUseForThisUtterance);
tts1.speak(stringToSpeak, TextToSpeech.QUEUE_FLUSH, null);
}
}
MainActivity:
package com.example.boober.speechsingletonsimplifiedunit;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
// LIFECYCLE
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("XXX", "Main.onCreate() called.");
App.GOSS.initFromMainActivity("MainActivity");
}
@Override
protected void onDestroy() {
Log.i("XXX", "Main.onDestroy() called.");
super.onDestroy();
}
// BUTTON PRESSES
public void settingsButtonPressed(View ignored) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
}
public void speakButtonPressed(View ignored) {
App.GOSS.speak("you are in the main activity!",new Locale("en"));
}
}
SettingsActivity:
package com.example.boober.speechsingletonsimplifiedunit;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import java.util.Locale;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
}
// BUTTON PRESSES
public void speakButtonPressed(View ignored) {
App.GOSS.speak("you are in the settings activity!", new Locale("en"));
}
}
class Collaborator { private readonly ISingleton service = Singleton.instance(); ... }
vsclass Collaborator { private readonly ISingleton service; public Collaborator(ISingleton _service) { service = _service; } ... }
. The second can't distinguish a singleton from a non singleton. More importantly, it doesn't need to