0

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"));
    }

}
4
  • 1
    It's not so much about being "bug prone" as it is a maintainability and testability issue. In the long run, chances are pretty decent that you'll find it may have been easier to set up "more normal" OOP constructs rather than singletons. On the other hand, refactoring is typically easy enough later (provided you have a clean base to work with anyway), so I wouldn't count it as the end of the world, but why do it better later if you can do it better now? That tends to be the issue with singletons. Not so much with bugs directly.
    – jleach
    Commented Aug 21, 2018 at 9:49
  • The thing that makes Singletons redundant is that clients of the singleton by definition don't care either way if multiple instances can exist or not. A class that so happens to only ever have one instance is indistinguishable from a singleton.
    – Caleth
    Commented Aug 21, 2018 at 13:53
  • @Caleth: An application that never encounters a FK constraint violation is also unable to tell the difference between a database with FK columns and a database without FK column. But that's not an argument in and of itself to stop using FKs altogether. The singleton pattern enforces that a single object exists, as opposed to leaving it up to luck for the developer to get it right and pray that he doesn't make a mistake somewhere.
    – Flater
    Commented Aug 21, 2018 at 14:06
  • @Flater, no it's only different when you put things in global scope: class Collaborator { private readonly ISingleton service = Singleton.instance(); ... } vs class 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
    – Caleth
    Commented Aug 21, 2018 at 14:10

2 Answers 2

6

It makes perfect sense to have only one instance of a class if it has a large cost when being initialized or it allows continuous operation as in your case.

But that does not indicate that the rest of the system should know this as a singleton. It can still be injected, only that the part of the system that hold it and injects it sees it as a singleton.

You might even have modules in your code that needs to be unaware of an object being a singleton or not.

A database connection might be a singleton on a mobile device but needs to be pooled on a web server.

The same module needing a database connection injected should not know if it runs on a web server or on a mobile device, all it needs to know is that it can use the injected database connection for its needs.

1

It's not so much about being "bug prone" as it is a maintainability and testability issue. In the long run, chances are pretty decent that you'll find it may have been easier to set up "more normal" OOP constructs rather than singletons. On the other hand, refactoring is typically easy enough later (provided you have a clean base to work with anyway), so I wouldn't count it as the end of the world, but why do it better later if you can do it better now? That tends to be the issue with singletons. Not so much with bugs directly.

I think what you're really after here is similar to what a DI/IOC container library does: it creates a "globally accessible instance" of something that you can grab and use as needed later (well, that's one of the things they (can) do, anyway, though not the primary use - the primary use is a bit more disciplined than that, and I'm boiling it down a lot). The idea is that you don't use singleton classes, but instead register your classes at application startup and tell your DI/IOC where they'll be used, and it'll make it available to you. A side-feature of these types of libraries is that you can more or less usually grab an instance of it whenever you might like (which seems to be what you're after).

In any case, using that approach, you have the expense of complicating things a bit more upfront while you have the longer term savings of having a more "properly" structured project for testing/maintaining later.

(as a personal aside, I don't tend to care for DI all that much myself, typically preferring the simplicity of your approach instead: it's later down the road that the DI really pays off)

2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.