Cómo lidiar con las clases en desuso en Android para mantener la compatibilidad

Estoy volviendo a trabajar en una aplicación en la que trabajé hace un tiempo, cuando todo estaba basado en Android 2.2 Froyo.

He actualizado mi SDK para las API más recientes y noté que las funciones de ClipboardManager que estaba usando están en desuso. Actualicé el código para usar el nuevo modelo ClipData y lo probé en mi teléfono Froyo y, por supuesto, obtuve un NoClassDefFoundError en el nuevo código.

He echado un vistazo a SO y no he encontrado discusiones reales sobre la estrategia general para mantener la compatibilidad con versiones anteriores.

No estoy del todo seguro de cómo debo manejar esta y otras situaciones en las que difiere la API, porque quiero que los usuarios de todas las versiones puedan usar mi aplicación.

¿Debería hacer un control de la siguiente manera?

if(version == old){ use old API; } else { use new API; } 

Si es así, he desactivado el código en mi clase y Eclipse tendrá la advertencia allí para siempre.

Por otro lado, podría construir contra una versión anterior de la API y esperar que las nuevas versiones lo manejen bien. Pero luego corro el riesgo de construir en contra del código defectuoso o de bajo rendimiento cuando hay una mejor alternativa disponible.

¿Cuál es la mejor manera de lidiar con esto?

Puedes hacerlo (verificando la versión de la API).

También puede usar el reflection para llamar a las clases más nuevas.

No me preocuparía utilizar métodos obsoletos, ya que todas las versiones de Android son compatibles con versiones anteriores, por lo que querría ver cuándo están las cosas para 3.0 Honeycomb, ya que son un poco diferentes.

Aquí hay una explicación de cómo usar la reflexión: (sí, ha estado en SO antes, entonces tal vez busque la reflexión)

http://www.youtube.com/watch?v=zNmohaZYvPw&feature=player_detailpage#t=2087s

Estoy tratando de hacer que el proyecto esté disponible, pero hasta entonces hay un código:

(Puede hacer esto en una clase que amplíe la aplicación, es decir, configuración única)

  public static Method getExternalFilesDir; static { try { Class partypes[] = new Class[1]; partypes[0] = String.class; getExternalFilesDir = Context.class.getMethod("getExternalFilesDir", partypes); } catch (NoSuchMethodException e) { Log.e(TAG, "getExternalFilesDir isn't available in this devices api"); } } 

Ahora getExternalFilesDir () solo está disponible en API nivel 8 o superior, por lo que quiero usar esto si tienen (Froyo), pero de lo contrario necesito otro método.

Ahora tengo mi prueba para el método que puedo seguir e bash usarla:

  if(ClassThatExtendsApplication.getExternalFilesDir != null){ Object arglist[] = new Object[1]; arglist[0] = null; File path = (File) ClassThatExtendsApplication.getExternalFilesDir.invoke(context, arglist); // etc etc } else { // Not available do something else (like your deprecated methods / or load a different class / or notify they should get a newer version of Android to enhance your app ;-)) } 

Espero que ayude y atajos mucho Google 🙂

PD si en el rest quieres usar tus métodos depreciados, solo agrega la @SuppressWarnings("deprecation") encima de esta, esto eliminará la advertencia y lo has hecho por las razones correctas mientras estás usando la última API cuando sea posible.

primero, @Graham Borland tiene razón. Puede optar por usar la antigua API, esto resuelve completamente el problema. Sin embargo, su software no evolucionará y seguirá las mejoras de la API y, en última instancia, coincidirá con una versión de Android que ya no es compatible.

El patrón de diseño que voy a proponer se basa en la introspección, pero proporciona una mejor interfaz de progtwigción que la solución propuesta por @Blundell. Creo que es lo suficientemente potente como para inspirar un enfoque estándar para este problema común. Se basa en muchas publicaciones de Stack Over Flow y otros foros.

Primero, debe definir una interfaz para el servicio que desea implementar. Podrá implementar diferentes versiones de este servicio utilizando diferentes versiones de la API que le interese.

De hecho, como vamos a compartir algún código aquí para cargar nuestras diferentes implementaciones, optamos por utilizar una clase abstracta. Definirá las firmas de métodos públicos como una interfaz. Bu también ofrecerá un método estático para cargar sus diferentes implementaciones.

 /** * Interface used to interact with the actual instance of MessageManager. * This inteface allows will be the type of the reference that will point * to the actual MessageMessenger, which will be loaded dynamically. * @author steff * */ public abstract class MessageManager { /** Request code used to identify mail messages.*/ public final static int FOR_MAIL = 0x3689; /** Request code used to identify SMS messages.*/ public final static int FOR_SMS = 0x3698; /** * Start an activity inside the given context. It will allow to pickup a contact * and will be given an intent code to get contact pick up. * *@param the request code. Has to be a constant : FOR_MAIL or FOR_SMS */ public abstract void pickupContact(int code);//met /** * Start an activity inside the given context. It will allow to pickup a contact * and will be given an intent code to get contact pick up. * *@param the request code. Has to be a constant : FOR_MAIL or FOR_SMS */ public abstract void sendMessage(int code, Intent data, final String body);//met /** * Static methode used as in factory design pattern to create an instance * of messageManager. Here it is combined with the singleton pattern to * get an instance of an inherited class that is supported by current android SDK. * This singleton will be created bu reflexion. * @param activity the activity that needs messaging capabilities. * @return an instance of an inherited class that is supported by current android SDK or null, if not found. */ public static MessageManager getInstance( Activity activity ) { MessageManager instance = null; try { Class messageManagerClass = (Class) activity.getClassLoader().loadClass( "ca.qc.webalterpraxis.cinedroid.message.MessageManagerSDK7" ); Method singletonMethod = messageManagerClass.getMethod("getInstance", Activity.class ); instance = (MessageManager) singletonMethod.invoke( null , activity); } catch (Throwable e) { Log.e( "CinemadroidMain", "Impossible to get an instance of class MessageManagerSDK7",e ); }//met return instance; }//met }//interface 

Luego, puede proporcionar diferentes implementaciones de esta clase abstracta usando diferentes versiones de Android SDK.

Lo que es algo inusual con este método es que es un patrón de diseño de fábrica combinado con un patrón de diseño singleton. Se solicita que todas las subclases sean únicas y que proporcionen un getInstanceMethod estático. El método de fábrica de esta clase abstracta intentará cargar una clase que implemente esta interfaz. Si falla, puede degradar sus requisitos a las clases que implementan el servicio y basar un APIS anterior.

Aquí hay un ejemplo de una clase para enviar correos y sms usando esta interfaz. Está diseñado para Android SDK 7.

 public class MessageManagerSDK7 extends MessageManager { /** Used for logcat. */ private static final String LOG_TAG = "MessageManagerSDK7"; /** Singleton instance. */ private static MessageManagerSDK7 instance = null; /** Activity that will call messaging actions. */ private Activity context; /** Private constructor for singleton. */ private MessageManagerSDK7( Activity context ) { if( instance != null ) throw new RuntimeException( "Should not be called twice. Singleton class."); this.context = context; }//cons /** * Static method that will be called by reflexion; * @param context the activity that will enclose the call for messaging. * @return an instance of this class (if class loader allows it). */ public static MessageManagerSDK7 getInstance( Activity context ) { if( instance == null ) instance = new MessageManagerSDK7( context ); instance.context = context; return instance; }//met /* (non-Javadoc) * @see ca.qc.webalterpraxis.cinedroid.model.MessageManager#pickupContact(int) */ @Override public void pickupContact( int code ) { if( code != FOR_MAIL && code != FOR_SMS ) throw new RuntimeException( "Wrong request code, has to be either FOR_MAIL or FOR_SMS."); Intent intentContact = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); context.startActivityForResult(intentContact, code ); }//met /* (non-Javadoc) * @see ca.qc.webalterpraxis.cinedroid.model.MessageManager#sendMessage(int, android.content.Intent, java.lang.String) */ @Override public void sendMessage( int code, Intent data, final String body ) { //System.out.println( "SendMessage"); if( code != FOR_MAIL && code != FOR_SMS ) throw new RuntimeException( "Wrong request code, has to be either FOR_MAIL or FOR_SMS."); int icon = 0; int noItemMessage = 0; int title = 0; //set the right icon and message for the dialog if( code == FOR_MAIL ) { icon=R.drawable.mail; noItemMessage = R.string.no_email_found; title = R.string.mail_error; }//if else if( code == FOR_SMS ) { icon=R.drawable.sms; noItemMessage = R.string.no_number_found; title = R.string.sms_error; }//if //compose email or sms //pick contact email address final String[] emailsOrPhoneNumbers = (code == FOR_MAIL ) ? getContactsEmails( data ) : getContactPhoneNumber( data ); if( emailsOrPhoneNumbers == null ) { new AlertDialog.Builder( context ).setIcon( icon ).setTitle(title).setMessage( noItemMessage ).show(); return; }//if //in case there are several addresses, we handle this using a dialog. //modal dialog would be usefull but it's bad UI practice //so we use an alert dialog, async .. //all this is poorly coded but not very interesting, not worth having a dedicated inner class if( emailsOrPhoneNumbers.length > 1 ) { selectMultipleAndSend( emailsOrPhoneNumbers, body, code); return; }//if if( code == FOR_MAIL ) sendMail( emailsOrPhoneNumbers, body ); else sendSMS( emailsOrPhoneNumbers, body ); }//met private void sendMail( String[] emails, String body ) { if( body == null ) { new AlertDialog.Builder( context ).setIcon( R.drawable.mail ).setTitle(R.string.mail_error).setMessage( R.string.impossible_compose_message ).show(); return; }//if //prepare email data try { Intent i = new Intent(Intent.ACTION_SEND); i.setType("message/rfc822") ; i.putExtra(Intent.EXTRA_EMAIL, emails ); //i.putExtra(Intent.EXTRA_EMAIL, emails); i.putExtra(Intent.EXTRA_SUBJECT, context.getString( R.string.showtimes ) ); i.putExtra(Intent.EXTRA_TEXT,body); context.startActivity(Intent.createChooser(i, context.getString( R.string.select_application ) ) ); } catch (Throwable e) { new AlertDialog.Builder( context ).setIcon( R.drawable.mail ).setTitle(R.string.mail_error).setMessage( R.string.no_application_mail ).show(); Log.e( LOG_TAG, "No application found", e); }//catch }//met private void sendSMS( String[] phoneNumbers, String body ) { try { Intent sendIntent= new Intent(Intent.ACTION_VIEW); if( body == null ) { new AlertDialog.Builder( context ).setIcon( R.drawable.sms ).setTitle(R.string.sms_error).setMessage( R.string.impossible_compose_message ).show(); return; }//if sendIntent.putExtra("sms_body", body); String phones = ""; for( String phoneNumber : phoneNumbers ) phones += ((phones.length() == 0) ? "" : ";") + phoneNumber; sendIntent.putExtra("address", phones ); sendIntent.setType("vnd.android-dir/mms-sms"); context.startActivity(sendIntent); } catch (Throwable e) { new AlertDialog.Builder( context ).setIcon( R.drawable.sms ).setTitle(R.string.sms_error).setMessage( R.string.no_application_sms ).show(); Log.e( LOG_TAG, "No application found", e); }//catch }//met /** * @param intent the intent returned by the pick contact activity * @return the emails of selected people, separated by a comma or null if no emails has been found; */ protected String[] getContactsEmails(Intent intent) { List resultList = new ArrayList(); //http://stackoverflow.com/questions/866769/how-to-call-android-contacts-list Cursor cursor = context.managedQuery(intent.getData(), null, null, null, null); while (cursor.moveToNext()) { String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID)); // Find Email Addresses Cursor emails = context.getContentResolver().query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,null,ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = " + contactId,null, null); while (emails.moveToNext()) { resultList.add( emails.getString(emails.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)) ); }//while emails.close(); } //while (cursor.moveToNext()) cursor.close(); if( resultList.size() == 0 ) return null; else return resultList.toArray( new String[ resultList.size() ] ); }//met /** * @param intent the intent returned by the pick contact activity * @return the phoneNumber of selected people, separated by a comma or null if no phoneNumber has been found; */ protected String[] getContactPhoneNumber(Intent intent) { List resultList = new ArrayList(); //http://stackoverflow.com/questions/866769/how-to-call-android-contacts-list Cursor cursor = context.managedQuery(intent.getData(), null, null, null, null); while (cursor.moveToNext()) { String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID)); String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)); String hasPhone = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)); if ( hasPhone.equalsIgnoreCase("1")) hasPhone = "true"; else hasPhone = "false" ; if (Boolean.parseBoolean(hasPhone)) { Cursor phones = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,ContactsContract.CommonDataKinds.Phone.CONTACT_ID +" = "+ contactId,null, null); while (phones.moveToNext()) { resultList.add( phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)) ); } phones.close(); } } //while (cursor.moveToNext()) cursor.close(); if( resultList.size() == 0 ) return null; else return resultList.toArray( new String[ resultList.size() ] ); }//met private void selectMultipleAndSend( final String[] emailsOrPhoneNumbers, final String body, final int code ) { int icon = 0; int selectMessage = 0; //set the right icon and message for the dialog if( code == FOR_MAIL ) { icon=R.drawable.mail; selectMessage = R.string.select_email; }//if else if( code == FOR_SMS ) { icon=R.drawable.sms; selectMessage = R.string.select_phone; }//if final boolean[] selected = new boolean[ emailsOrPhoneNumbers.length ]; Arrays.fill( selected, true ); new AlertDialog.Builder( context ).setIcon( icon ).setTitle( selectMessage ).setMultiChoiceItems(emailsOrPhoneNumbers, selected, new OnMultiChoiceClickListener() { @Override public void onClick(DialogInterface dialog, int which, boolean isChecked) { selected[ which ] = isChecked; } }).setPositiveButton( R.string.OK, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { int count = 0; for( int s=0; s< selected.length; s ++ ) if( selected[s] ) count ++; String[] selectedEmailsOrPhoneNumbers = new String[ count ]; int index = 0; for( int s=0; s< selected.length; s ++ ) if( selected[s] ) selectedEmailsOrPhoneNumbers[ index ++ ] = emailsOrPhoneNumbers[ s ]; if( code == FOR_MAIL ) sendMail( selectedEmailsOrPhoneNumbers, body ); else if( code == FOR_SMS ) sendSMS( selectedEmailsOrPhoneNumbers, body ); } }).setNegativeButton( R.string.cancel , null ).show(); }//met }//class 

Y podría proporcionar otras alternativas también. Intentando cargarlos uno tras otro, descendiendo los números de la versión de Android.

Para usar tu servicio de mensajería es bastante simple:

 MessageManager messageManager = MessageManager.getInstance( this ); 

si es nulo, entonces no hay servicio igualado. Si no es nulo, utilícelo a través de la interfaz definida por MessageManager.

Esta técnica podría ampliarse e incluso hacerse más limpia al incluir el número de versión en el que se basa la implementación y construir un pequeño bus para cargar las clases una tras otra en el orden correcto.

Todos los comentarios son bienvenidos.

Saludos, Stéphane

Aquí hay un ejemplo:

 import android.os.Build; public static int getWidth(Context mContext){ int width=0; WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); if(VERSION.SDK_INT > VERSION_CODES.HONEYCOMB){ Point size = new Point(); display.getSize(size); width = size.x; } else{ width = display.getWidth(); // deprecated, use only in Android OS<3.0. } return width; } 

como puedes ver la sección de código:

  if(VERSION.SDK_INT > VERSION_CODES.HONEYCOMB){ Point size = new Point(); display.getSize(size); width = size.x; } 

solo está disponible para Android 3.0 y versiones posteriores; si desea que este código esté disponible al menos para Jelly Bean (Android 4.1), utilice:

  if(VERSION.SDK_INT > VERSION_CODES.JELLY_BEAN){ Point size = new Point(); display.getSize(size); width = size.x; } 

VERSION.SDK_INT La versión del marco de SDK visible para el usuario; sus valores posibles se definen en Build.VERSION_CODES.

Más información sobre: Build.VERSION

Y puede ver las constantes de VERSION_CODES aquí: Build.VERSION_CODES

Has identificado correctamente las dos soluciones posibles: decide en el tiempo de ejecución qué API usar o siempre usa la antigua API.

Si ayuda, puede que solo sea un año más o menos hasta que los dispositivos con la antigua API formen una proporción tan pequeña de la base de instalación que pueda cambiar completamente a la API nueva y no preocuparse por perder demasiados usuarios.