【问题标题】:How to deal with deprecated classes in Android to keep compatibility如何处理 Android 中已弃用的类以保持兼容性
【发布时间】:2011-06-12 11:21:36
【问题描述】:

我正在重新开始开发我不久前开发的应用程序,当时我的一切都是围绕 Android 2.2 Froyo 构建的。

我为最新的 API 更新了我的 SDK,并注意到我使用的 ClipboardManager 功能已被弃用。我更新了代码以使用较新的 ClipData 模型并在我的 Froyo 手机上进行了试用,当然,我在新代码中得到了 NoClassDefFoundError。

我环顾了 SO,但没有找到任何关于保持向后兼容性的一般策略的真正讨论。

我不完全确定应该如何处理这种情况以及 API 不同的其他情况,因为我希望所有版本的用户都能使用我的应用。

我应该做如下检查吗?

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

如果是这样,我已经在我的班级中弃用了代码,Eclipse 将永远在那里发出警告。

另一方面,我可以针对旧版本的 API 进行构建,并希望新版本能够正常处理它。但是,当有更好的替代方案可用时,我冒着构建错误或低性能代码的风险。

解决这个问题的最佳方法是什么?

【问题讨论】:

    标签: android compatibility deprecated backwards-compatibility


    【解决方案1】:

    您可以这样做(检查 API 版本)。

    您也可以使用反射来调用较新的类。

    我不会担心使用不推荐使用的方法,因为所有 Android 版本都向后兼容,说你想在 3.0 Honeycomb 的情况下观看,因为它们有点不同。

    下面是如何使用反射的解释:(是的,它以前在 SO 上,所以也许搜索反射)

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

    我正在考虑使该项目可用,但在此之前这里有一些代码:

    (您可以在扩展应用程序的类中执行此操作,即一次性设置)

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

    现在getExternalFilesDir() 仅适用于 API 级别 8 或更高级别,所以如果他们有 (Froyo),我想使用它,否则我需要另一种方法。

    现在我已经测试了我可以继续尝试使用它的方法:

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

    希望能帮助并缩短很多谷歌搜索:-)

    附:如果在 else 中您仍想使用已弃用的方法,只需在其上方添加 @SuppressWarnings("deprecation") 注释,这将消除警告,并且您出于正确的原因这样做,因为您尽可能使用最新的 API。

    【讨论】:

      【解决方案2】:

      这是一个例子:

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

      如你所见的代码部分:

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

      仅适用于 Android 3.0 及更高版本,如果您希望此代码至少可用于 Jelly Bean (Android 4.1) 使用:

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

      VERSION.SDK_INT 用户可见的框架 SDK 版本;这是可能的 值在 Build.VERSION_CODES 中定义。

      更多信息:Build.VERSION

      您可以在此处查看 VERSION_CODES 常量:Build.VERSION_CODES

      【讨论】:

      • 简洁优雅。并且您可以使用 @SuppressWarnings("deprecation") 注释用于包含已弃用代码(在这种情况下为 getWidth(Context mContext))的特定方法,以抑制弃用警告。
      【解决方案3】:

      首先,@Graham Borland 是对的。您可以选择使用旧 API,这完全解决了问题。但是,您的软件不会随着 API 的改进而发展,最终会匹配不再支持的 android 版本。

      我要提出的设计模式是基于自省的,但提供了比@Blundell 提出的解决方案更好的编程接口。 我认为它足以激发针对这个常见问题的标准方法。它基于 Stack Over Flow 和其他论坛的许多帖子。

      首先,您需要为要实现的服务定义一个接口。您将能够使用您感兴趣的不同版本的 API 来实现此服务的不同版本。

      确实,因为我们将在这里分享一些代码来加载我们不同的实现,所以我们选择使用抽象类。它将公共方法签名定义为接口,还提供静态方法来加载您的不同实现。

      /**
       * 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<? extends MessageManager> messageManagerClass = (Class<? extends MessageManager>) 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
      

      然后,您可以使用不同版本的 android SDK 提供此抽象类的不同实现。

      这种方法有点不寻常的是,它是一种工厂设计模式与单例设计模式的结合。要求所有子类都是单例的,并提供一个静态的 getInstanceMethod。这个抽象类的工厂方法会尝试加载一个实现这个接口的类。如果失败,您可以将您的要求降级为实现该服务并基于旧 APIS 的类。

      这是一个使用此接口发送邮件和短信的类的示例。它是为 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<String> resultList = new ArrayList<String>();
              //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<String> resultList = new ArrayList<String>();
              //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
      

      您也可以提供其他选择。尝试一个接一个地加载它们,降序的 android 版本号。

      使用你的信使服务非常简单:

      MessageManager messageManager = MessageManager.getInstance( this );
      

      如果它为空,则没有匹配的服务。如果不为null,则通过MessageManager定义的接口使用。

      通过包含实现所基于的版本号,并构建一个小总线以正确的顺序一个接一个地加载类,这种技术可以得到扩展,甚至变得更简洁。

      欢迎所有反馈。

      问候, 斯蒂芬

      【讨论】:

        【解决方案4】:

        您已正确确定了两种可能的解决方案:在运行时决定使用哪个 API,或始终使用旧 API。

        如果有帮助,可能只需要一年左右的时间,使用旧 API 的设备在安装基数中所占比例就会很小,您可以完全切换到新 API,而不必担心失去太多用户。

        【讨论】:

        • 在 GoogleIO 上,有人提到多个版本在 Android 中很常见,因此值得为多个版本处理这个弃用问题,因为这似乎是一个持续存在的问题。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-09-04
        • 2011-02-09
        • 1970-01-01
        • 2020-12-10
        • 1970-01-01
        • 1970-01-01
        • 2022-11-20
        相关资源
        最近更新 更多