lundi 20 août 2018

Is this Singleton-like design pattern a feasible framework to build on?

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

}

Aucun commentaire:

Enregistrer un commentaire