【问题标题】:Dispatch user action to activities in backstack将用户操作发送到 backstack 中的活动
【发布时间】:2016-01-03 01:42:42
【问题描述】:

我正在开发社交应用。假设我有一堆活动A -> B -> C -> D。 D 在前台,用户按下“喜欢”按钮到那里(发布、评论、用户等)。通知所有其他活动有关此操作以刷新其数据的最佳方式是什么?我在这里看到 3 个选项:

  1. 使用本地数据库和一些加载器来自动刷新数据。但是,如果我们有不同的数据模型共享数据(例如BasicUserInfoUserInfoDetailedUserInfo),则需要大量代码。
  2. 将 EventBus 用于粘性事件(Otto 的生产者)。在这种情况下,我必须只通知 backstack 活动并忽略那些将被创建的活动。我还必须管理事件覆盖。
  3. 使用带有 WeakReferences 的简单观察者模式来回溯活动。但是我遇到了将要重新实例化的已终止活动的问题。

实例:

在 Instagram 中:我打开一些特定用户的个人资料 (A),在那里我打开一些特定的帖子 (B),再次打开个人资料 (A),依此类推 A -> B -> A -> B -> A ... . 所以它每次都从网上加载数据。在步骤“n+1”上,会出现对该帖子的新评论。如果我开始通过我的后台堆栈返回,我将看到 instagram 已将这个“新”评论发送到所有 B 活动,而无需从 web 重新加载任何数据。所以我很感兴趣他们是如何做到的。

【问题讨论】:

  • 我认为 Otto 是最好的情况。
  • @Sunny,因为活动在后台时没有注册,所以我必须使用生产者来使活动变得粘滞。在这种情况下,我不确定如何仅将这些事件传递给 backstack 活动。

标签: android


【解决方案1】:

通知系统(事件、观察者、BroadcastReceiver、...)的主要用例是您希望接收者在发生某事时或多或少地立即采取行动。

我认为这里不是这种情况:backstack 活动不需要立即行动,因为它们是不可见的。此外,它们甚至可能不再存在(被杀死/冻结)。他们真正需要的是在他们回到前台时(可能在重新创建之后)获取最新数据。

为什么不简单地触发onStart()onResume() 中的刷新(使用Loader 或您已经使用的任何东西)?
如果需要保持“喜欢”状态,您可以在DonPause() 中进行。 如果没有,喜欢的对象可以存储在全局变量中(这实际上是粘性事件)

【讨论】:

  • 这很好。但是新的活动呢?假设用户从D打开另一个AA -> B -> C -> D -> A。这个新活动将从网络加载它自己的最新数据,它不需要知道在D 中发生的任何操作。所以我必须以某种方式划分这些A活动`onStart()逻辑。
  • 因此您希望旧的A 实例拥有最新数据(在D 中更新),而较新的A 实例应该只有旧数据(没有D 更新) ?有趣的。您可以对D 更新和A 创建进行时间戳/排序,以允许每个实例仅(重新)加载相关部分。在杀戮/娱乐周期的情况下,活动序列应保存在onSaveInstanceState()中,以便在onCreate()中检索
  • 较新的A 从网络加载其数据,因此它已经有D 更新(与服务器同步)并且可能来自其他用户的一些更新。无论如何,您关于时间戳的建议都会奏效。但我想知道所有这些社交应用程序都以同样的方式做到这一点吗?我认为应该有一些更简单的通用方法。
  • 然后每个活动都可以在onResume()中做一个“检查是否有新的东西并重新加载”,它不必知道其他活动。这也将涵盖当用户切换到另一个应用程序然后返回到您的应用程序的情况。在 HTTP 的情况下,只要服务器正确使用与修改相关的标头(If-Modified-SinceLast-ModifiedCache-Control,...),就有很多库可以为您处理,
  • 例如在 Instagram 中:我打开一些特定用户的个人资料 (A),然后打开一些特定的帖子 (B) 等等A -> B -> A -> B -> A ...。所以它每次都从网络加载数据。在步骤“n+1”上,会出现对该帖子的新评论。如果我开始回顾我的后台堆栈,我会看到 instagram 已将这个“新”评论发送到所有 B 活动,而无需从网络重新加载任何数据。所以我很感兴趣他们是怎么做到的。
【解决方案2】:

您可以使用LocalBroadcastManager 通知您的堆叠活动您的点赞事件已发生

假设在你的活动 D 中:

private void liked() {
  Log.d("liked", "Broadcasting message");
  Intent intent = new Intent("like-event");
  // You can also include some extra data.
  intent.putExtra("message", "my like event occurs!");
  LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}

现在它会通知所有在这个广播接收器上注册的活动

例如在您的活动 A、B、C 中:

@Override
public void onCreate(Bundle savedInstanceState) {

  ...

  // Register to receive messages.
  // We are registering an observer (mMessageReceiver) to receive Intents
  // with actions named "custom-event-name".
  LocalBroadcastManager.getInstance(this).registerReceiver(mLikeEventReceiver ,
      new IntentFilter("like-event"));
}

// Our handler for received Intents. This will be called whenever an Intent
// with an action named "like-event" is broadcasted.
private BroadcastReceiver mLikeEventReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    // Get extra data included in the Intent
    String message = intent.getStringExtra("message");
    Log.d("receiver", "Got message: " + message);
  }
};

@Override
protected void onDestroy() {
  // Unregister since the activity is about to be closed.
  LocalBroadcastManager.getInstance(this).unregisterReceiver(mLikeEventReceiver );
  super.onDestroy();
}

参考: [http://developer.android.com/reference/android/support/v4/content/LocalBroadcastManager.html][1] [how to use LocalBroadcastManager? [https://androidcookbook.com/Recipe.seam?recipeId=4547][3]

  1. [1]: http://developer.android.com/reference/android/support/v4/content/LocalBroadcastManager.html

  2. [2]: how to use LocalBroadcastManager?

  3. [3]:https://androidcookbook.com/Recipe.seam?recipeId=4547

【讨论】:

  • 我必须在活动生命周期的某个步骤注册和注销接收者。即使我使用不安全的 onCreate/onDestroy 也意味着临时终止的活动不会得到广播信息
  • 你可以简单地调用你的刷新机制 onResume() of activity。
  • “刷新机制”是什么意思?
  • 我的意思是说你已经编写了刷新活动数据的方法,调用它的 onResume() 方法。
【解决方案3】:

为了刷新他们的数据?

答案就在这里。活动不应拥有数据。活动呈现数据并允许用户对其采取行动。数据本身应该位于一个单独的实体中,可以与活动实例交互,即模型。

此外,不应假设在后台堆栈中始终存在一个活动实例。当用户导航返回时,系统可以销毁这些活动,然后将其重新创建为不同的对象。在创建整个 Activity 时,始终刷新数据。

将数据处理分离到专门的类中,这些类可以很容易地被活动访问,并且可以根据需要为活动提供数据/事件绑定。绑定Service 是一个不错的候选对象。

就不从网络加载数据而言,您可以为最近访问的数据设置本地缓存(缓存,因为手机有严格的存储限制,服务器和数据库不是这样)。因此,来自用户端的任何更改也将提交到此缓存,同时传播到服务器。所有这些都最好通过专门的数据类来封装,而不是依赖于活动类中的回栈或特殊代码。

作为一种模式,您可以为所涉及的数据实体构造Model 类。编写一个 Web API 接口实现来与服务器通信。然后,在 API 接口之前放置一个缓存层。缓存将保留来自/来自 API 层的传出更改和传入更新,并在不需要服务器调用时简单地反映数据请求。

缓存主要做了3件事:

  1. Evict:随着新数据的到来,删除最不重要的数据,因此缓存保持固定大小。大多数缓存实现 (like this one) 会自动执行此操作。
  2. 无效:有时,由于用户操作或服务器端的外部事件,某些数据必须刷新。
  3. 过期:数据可以设置时间限制,并将被自动驱逐。这在强制定期刷新数据时很有帮助。

现在大多数缓存实现都处理原始字节。我建议使用Realm 之类的东西,一个对象数据库并将其包装在类似缓存的功能中。因此,请求用户推文的典型流程是:

  1. 活动已显示。
  2. Activity 绑定到数据服务,并表达了对“推文”的兴趣。
  3. 数据服务在 Tweets 缓存数据库表中查找最后获取的推文列表并立即返回。
  4. 但是,数据服务也会调用服务器在数据库中本地拥有的最新推文的时间戳之后提供推文。
  5. 服务器返回最新的推文集。
  6. 数据服务使用新的传入数据更新所有对推文表示兴趣的绑定活动。此数据也在缓存数据库中本地复制。此时,tweet 表也通过删除旧记录进行了优化。
  7. 活动与数据服务解除绑定,因为活动正在消失、停止、销毁等。
  8. 如果没有绑定活动对“推文”感兴趣,数据服务将停止加载更多推文。

通过维护与服务器的套接字连接并实时接收有趣的变化,您的数据实现可以更进一步。也就是说,只要有新数据传入,服务器就会调用上面的第 5 步。

TLDR; 将数据管理与活动分开,然后您可以独立于 UI 问题对其进行改进。

【讨论】:

  • 很好的答案,谢谢!从缓存中删除过期数据以释放存储空间怎么样?比如用户回到A(backstack的根),这意味着不再需要缓存的数据,我怎么知道呢?
  • @user1049280 已更新答案。活动从服务“绑定”和“解除绑定”。
【解决方案4】:

处理这类事情的经典方法是使用BroadcastReceivers

这是一个示例接收器:

public class StuffHappenedBroadcastReciever extends BroadcastReceiver {


    private static final String ACTION_STUFF_HAPPENED = "stuff happened";
    private final StuffHappenedListener stuffHappenedListener;

    public StuffHappenedBroadcastReciever(@NonNull Context context, @NonNull StuffHappenedListener stuffHappenedListener) {

        this.stuffHappenedListener = stuffHappenedListener;
        context.registerReceiver(this, new IntentFilter(ACTION_STUFF_HAPPENED));
    }

    public static void notifyStuffHappened(Context context, Bundle data) {

        Intent intent = new Intent(ACTION_STUFF_HAPPENED);
        intent.putExtras(data);

        context.sendBroadcast(intent);

    }

    @Override
    public void onReceive(Context context, Intent intent) {

        stuffHappenedListener.onStuffHappened(intent.getExtras());

    }


    public interface StuffHappenedListener {

        void onStuffHappened(Bundle extras);
    }
}

以及如何将其附加到活动:

public class MainActivity extends AppCompatActivity implements StuffHappenedBroadcastReciever.StuffHappenedListener {

    private StuffHappenedBroadcastReciever mStuffHappenedReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mStuffHappenedReceiver = new StuffHappenedBroadcastReciever(this, this);
    }

    @Override
    protected void onDestroy() {
        unregisterReceiver(mStuffHappenedReceiver);

        super.onDestroy();
    }

    @Override
    public void onStuffHappened(Bundle extras) {
        // do stuff here
    }
}

只要活动还活着,“onStuffHappened”就会被调用。

【讨论】:

  • 基本上,它与使用 EventBus 或 Otto 的方法相同。如果我的一项后台活动被临时终止以节省内存怎么办?然后它不会注册到广播接收器。
  • 如果您的活动被销毁,那么将创建一个新实例并且 onCreate 将在该实例中发生 - 就像您从头开始一个新活动一样,您应该只加载最新数据.
  • 对我来说,backstack 中的活动必须保持其状态(即使它被重新实例化)并且再次加载数据将起作用,但这是一种解决方法
【解决方案5】:

我同意@bwt 的做法。当您想立即通知某事时,这些通知系统应该很重要。

我的方法是缓存系统。您无需处理“如果活动是回堆或新创建的”,您始终需要在活动的onResume 中查询您需要的内容。因此,您将始终获得最新数据。

在从服务器获取数据时,您还需要在本地数据库中复制数据模型。在你 ping 你的服务器之前,例如用户帖子中有一个赞,您需要在本地数据库中将其设置为Flag,然后将您的 Http 请求发送到您的服务器以说“嘿,这篇帖子很受欢迎”。稍后当您收到此请求的响应时,如果成功与否,请再次尝试修改此 Flag。

因此,在您的活动中,如果您从onResume 中的本地数据库查询,您将获得最新数据。对于其他用户帖子的更改,您可以使用BroadcastReceiver 来反映您的可见活动中的更改。

P.S:您可以检查Realm.io 以获得相对更快的查询,因为您需要更快地进行本地数据库调用。

【讨论】:

    【解决方案6】:

    正如许多其他答案所没有的,这是一个经典的例子,BroadcastReceiver 可以轻松完成工作。

    我还建议将LocalBroadcastManager 类与BroadcastReceiver 结合使用。来自 BroadcastReceiver 的文档:

    如果您不需要跨应用程序发送广播,请考虑将此类与 LocalBroadcastManager 一起使用,而不是下面描述的更通用的工具。这将为您提供更高效的实现(无需跨进程通信),并让您避免考虑与其他应用程序能够接收或发送您的广播相关的任何安全问题。

    【讨论】:

    • 我必须在活动生命周期的某个步骤注册和注销接收者。即使我使用不安全的 onCreate/onDestroy 也意味着临时终止的活动不会得到广播信息
    【解决方案7】:

    如果您必须针对某些数据在所有活动中进行更改,您可以遵循接口模式。例如,您有一个自定义类 ActivityData,其中包含您需要在所有活动中更新的内容

    第 1 步:

    如下创建接口

    public interface ActivityEventListener
    {
    
    
        ActivityData getData( Context context,
                Activity activity );
    
    
    }
    

    第二步:

    创建一个 BaseActivity 以引用您在应用程序中拥有的所有活动,并按如下方式实现接口

    public class BaseActivity extends Activity
    {
        protected AcitivityData mData= null;
    
    
        @Override
        protected void onCreate( Bundle savedInstanceState )
        {
            super.onCreate( savedInstanceState );
        }
    
        protected ActivityEventListener mListener = new ActivityEventListener()
        {
    
    
            @Override
            public ActivityData getData( Context context,
                    Activity activity )
            {
                // TODO Auto-generated method stub
                return mData;
            }
    
    
        };
    }
    

    第 3 步: 使用 BaseActivity 扩展您自己的活动,例如 A 或 B 或 C .......

    公共类 A 扩展 BaseActivity {

      ActivityData mData;
    @Override
    protected void onCreate( Bundle savedInstanceState )
    {
    
         mData = mListener.getData(this,this);
    
         updateWidgets(mData);
    
    }
    

    }

    updateWidgets 是一个函数,您可以在其中定义 UI 元素并使用您在界面中拥有的数据

    因为你所有的活动B/C/等等都可以得到ActivityData的引用。活动表单 backstack 将通过 onStart() 开始执行用户可以处理与 ActivityData 中存在的细节相同的活动

    在您喜欢上一个活动的情况下,您可以更新 ActivtyData 的对象,并且当回栈活动恢复或开始时,您可以获取更新的数据,因为您的回栈活动通过具有接口的 BaseActiivty 扩展。

    【讨论】:

    • 基本上与@bwt建议的方法相同,如何避免ActivityData中的内存泄漏?会有一些数据不再需要
    • 是的,你是对的,但这是在活动中的全部数据的情况下,为了避免内存泄漏而不是使用界面,用户可以使用结果代码启动活动以获取结果,并且可以覆盖每个活动中的 onActivityResult 方法活动
    • 这是一个很好的观点,但是它将需要大量的样板代码来将这些数据分发到 backstack,如果有一天我决定从活动切换到片段,它也将无法工作
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多