AndroidViewModel - 重复调用不会在观察函数中返回数据。

8

我的问题与 ViewModel第二次返回null 有关,如果我对服务器进行重复调用,则在observe函数中我无法获取回调。以下是我使用的代码 -

@Singleton
public class NetworkInformationViewModel extends AndroidViewModel {
  private LiveData<Resource<NetworkInformation>> networkInfoObservable;
  private final APIClient apiClient;

  @Inject
  NetworkInformationViewModel(@NonNull APIClient apiClient, @NonNull Application application) {
    super(application);
    this.apiClient = apiClient;
    getNetworkInformation();
  }

  public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
    return networkInfoObservable;
  }

  // making API calls and adding it to Observable
  public void getNetworkInformation() {
    networkInfoObservable = apiClient.getNetworkInformation();
  }
}

在Activity中,ViewModel的定义如下 -
final NetworkInformationViewModel networkInformationViewModel =
      ViewModelProviders.of(this, viewModelFactory).get(NetworkInformationViewModel.class);
    observeViewModel(networkInformationViewModel);

observeViewModel 函数用于在 ViewModel 上添加可观察对象。

public void observeViewModel(final NetworkInformationViewModel networkInformationViewModel) {
    networkInformationViewModel.getNetworkInfoObservable()
      .observe(this, networkInformationResource -> {
        if (networkInformationResource != null) {
          if (networkInformationResource.status == APIClientStatus.Status.SUCCESS) {
            Timber.d("Got network information data");
          } else {
            final Throwable throwable = networkInformationResource.throwable;
            if (throwable instanceof SocketTimeoutException) {
              final NetworkInformation networkInformation = networkInformationResource.data;
              String error = null;
              if (networkInformation != null) {
                error = TextUtils.isEmpty(networkInformation.error) ? networkInformation.reply : networkInformation.error;
              }
              Timber.e("Timeout error occurred %s %s", networkInformationResource.message, error);

            } else {
              Timber.e("Error occurred %s", networkInformationResource.message);
            }
            if (count != 4) {
              networkInformationViewModel.getNetworkInformation();
              count++;
              // Uncommenting following line enables callback to be received every time 
              //observeViewModel(networkInformationViewModel);
            }
          }
        }
      });
  }

在上述函数中取消以下行的注释,可以让回调每次都触发,但必须有适当的方法来实现这一点。
//observeViewModel(networkInformationViewModel);

请注意:我不需要使用RxJava来实现这个。

你能添加代码,展示如何将它添加到可观察对象中吗? - Gautam
@Gautam 没有理解,我已经添加了 ViewModel 和一些代码,在其中添加了可观察对象,请让我知道您还需要什么。 - Rohan Kandwal
我无法理解你的问题。你只想观察网络信息4次吗? - savepopulation
@savepopulation 问题在于,如果我尝试重试或进行二次调用,除非我重新附加可观察对象,否则无法在 Observable 上获取回调。 - Rohan Kandwal
@savepopulation 问题在于,如果我们再次使用networkInfoObservable = apiClient.getNetworkInformation();进行二次调用,就会创建一个新对象,从而导致可观察对象丢失。 - Rohan Kandwal
显示剩余4条评论
3个回答

1

现在在getNetworkInformation()中,您正在:

  1. 创建一个新的LiveData
  2. 使用setValue更新LiveData

相反,您应该有一个作为成员变量创建的单个APIClientLiveData,然后在getNetworkInformation()仅仅更新该成员LiveData

更一般地说,您的APIClient是数据源。对于数据源,您可以让它们包含成员LiveData对象,这些对象在数据更改时进行更新。您可以提供获取器来使这些LiveData对象在ViewModels中可访问,并最终在Activities/Fragments中监听它们。这类似于您可能会获取另一个数据源(例如Room)并侦听由Room返回的LiveData的方式。

因此,在这种情况下,代码应如下所示:

@Singleton
public class APIClient {
    private final MutableLiveData<Resource<NetworkInformation>> mNetworkData = new MutableLiveData<>(); // Note this needs to be MutableLiveData so that you can call setValue

    // This is basically the same code as the original getNetworkInformation, instead this returns nothing and just updates the LiveData
    public void fetchNetworkInformation() {
        apiInterface.getNetworkInformation().enqueue(new Callback<NetworkInformation>() {
          @Override
          public void onResponse(
            @NonNull Call<NetworkInformation> call, @NonNull Response<NetworkInformation> response
          ) {
            if (response.body() != null && response.isSuccessful()) {
              mNetworkData.setValue(new Resource<>(APIClientStatus.Status.SUCCESS, response.body(), null));
            } else {
              mNetworkData.setValue(new Resource<>(APIClientStatus.Status.ERROR, null, response.message()));
            }
          }

          @Override
          public void onFailure(@NonNull Call<NetworkInformation> call, @NonNull Throwable throwable) {
            mNetworkData.setValue(
              new Resource<>(APIClientStatus.Status.ERROR, null, throwable.getMessage(), throwable));
          }
        });
    }

    // Use a getter method so that you can return immutable LiveData since nothing outside of this class will change the value in mNetworkData
    public LiveData<Resource<NetworkInformation>> getNetworkData(){
        return mNetworkData;
    }

}

然后在您的ViewModel中...
// I don't think this should be a Singleton; ViewModelProviders will keep more than one from being instantiate for the same Activity/Fragment lifecycle
public class SplashScreenViewModel extends AndroidViewModel {

private LiveData<Resource<NetworkInformation>> networkInformationLiveData;

  @Inject
  SplashScreenViewModel(@NonNull APIClient apiClient, @NonNull Application application) {
    super(application);
    this.apiClient = apiClient;

    // Initializing the observable with empty data
    networkInfoObservable = apiClient.getNetworkData()

  }

  public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
    return networkInformationLiveData;
  }

}

您的活动可以与您最初编码的活动相同; 它只会从ViewModel获取和观察LiveData。

那么Transformations.switchMap是什么?

switchMap在这里并不是必须的,因为您不需要更改APIClient中的基础LiveData实例。这是因为只有一个正在更改的数据。假设您的APIClient出于某种原因需要4个不同的LiveData,并且您想要更改观察的LiveData:

public class APIClient {
    private MutableLiveData<Resource<NetworkInformation>> mNetData1, mNetData2, mNetData3, mNetData4;

    ...
}

那么假设您的fetchNetworkInformation根据不同情况引用不同的LiveData进行观察,它可能会像这样:
public  LiveData<Resource<NetworkInformation>> getNetworkInformation(int keyRepresentingWhichLiveDataToObserve) {
    LiveData<Resource<NetworkInformation>> currentLiveData = null;
    switch (keyRepresentingWhichLiveDataToObserve) {
        case 1:
            currentLiveData = mNetData1; 
            break;
        case 2:
            currentLiveData = mNetData2; 
            break;
        //.. so on
    }

    // Code that actually changes the LiveData value if needed here

    return currentLiveData;
}

在这种情况下,来自getNetworkInformation的实际LiveData发生了变化,并且您还使用某种参数来确定要使用哪个LiveData。在这种情况下,您将使用switchMap,因为您希望确保您在Activity / Fragment中调用的observe语句观察从APIClient返回的LiveData,即使更改基础LiveData实例也是如此。而且您不想再次调用observe。
现在这是一个抽象的例子,但基本上就是您对Room Dao的调用所做的——如果您有一个Dao方法查询基于id的RoomDatabase并返回LiveData,它将根据id返回不同的LiveData实例。

最后一个问题,您已将APIInterface类中的mNetworkData设置为私有全局变量,这在某种程度上是有道理的,因为我将在其他地方使用它。然而,应用程序可能会有30个以上的可观察对象,用于不同屏幕上的不同API,这种情况下,将它们全部设置为全局变量是没有意义的,如何解决这种情况。 - Rohan Kandwal
1
你可以为应用程序中不同的数据类型拥有不同的存储库,就像这个Github示例中所展示的那样。这可以帮助你将数据分离,并仅使用代表特定ViewModel所需数据的存储库。 - Lyla
1
如果你想知道为什么Github示例似乎返回LiveData,那是因为它返回了一个名为NetworkResource的MediatorLiveData对象。这种方式使用MediatorLiveData在这里有所描述。 - Lyla
在这个示例中,如果调用失败,则使用“重试逻辑”。类似于之前我使用的方式,该示例使用Transformation.switchMap()。唯一的区别是,由于API不需要任何输入,因此我正在使用Void。按照您的ViewModel,我们如何进行重试调用? - Rohan Kandwal

0

我已经更新了相关问题的答案。在这里重新发布,因为我已经对该问题设置了赏金,希望有人可以验证这是否是处理该问题的正确方法。

以下是更新后可行的解决方案 -

@Singleton
public class SplashScreenViewModel extends AndroidViewModel {
  private final APIClient apiClient;
  // This is the observable which listens for the changes
  // Using 'Void' since the get method doesn't need any parameters. If you need to pass any String, or class
  // you can add that here
  private MutableLiveData<Void> networkInfoObservable;
  // This LiveData contains the information required to populate the UI
  private LiveData<Resource<NetworkInformation>> networkInformationLiveData;

  @Inject
  SplashScreenViewModel(@NonNull APIClient apiClient, @NonNull Application application) {
    super(application);
    this.apiClient = apiClient;

    // Initializing the observable with empty data
    networkInfoObservable = new MutableLiveData<Void>();
    // Using the Transformation switchMap to listen when the data changes happen, whenever data 
    // changes happen, we update the LiveData object which we are observing in the MainActivity.
    networkInformationLiveData = Transformations.switchMap(networkInfoObservable, input -> apiClient.getNetworkInformation());
  }

  /**
   * Function to get LiveData Observable for NetworkInformation class
   * @return LiveData<Resource<NetworkInformation>> 
   */
  public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
    return networkInformationLiveData;
  }

  /**
   * Whenever we want to reload the networkInformationLiveData, we update the mutable LiveData's value
   * which in turn calls the `Transformations.switchMap()` function and updates the data and we get
   * call back
   */
  public void setNetworkInformation() {
    networkInfoObservable.setValue(null);
  }
}

Activity的代码将被更新为-

final SplashScreenViewModel splashScreenViewModel =
  ViewModelProviders.of(this, viewModelFactory).get(SplashScreenViewModel.class);
observeViewModel(splashScreenViewModel);
// This function will ensure that Transformation.switchMap() function is called
splashScreenViewModel.setNetworkInformation();

观看她的droidCon NYC视频以获取有关LiveData的更多信息。LiveData的官方Google存储库是https://github.com/googlesamples/android-architecture-components/,寻找GithubBrowserSample项目。 apiClient.getNetworkInformation()调用不需要任何参数来获取其他信息。因此,在MutableLiveData中添加了'Void'。
public LiveData<Resource<NetworkInformation>> getNetworkInformation() {
    final MutableLiveData<Resource<NetworkInformation>> data = new MutableLiveData<>();

    apiInterface.getNetworkInformation().enqueue(new Callback<NetworkInformation>() {
      @Override
      public void onResponse(
        @NonNull Call<NetworkInformation> call, @NonNull Response<NetworkInformation> response
      ) {
        if (response.body() != null && response.isSuccessful()) {
          data.setValue(new Resource<>(APIClientStatus.Status.SUCCESS, response.body(), null));
        } else {
          data.setValue(new Resource<>(APIClientStatus.Status.ERROR, null, response.message()));
        }
      }

      @Override
      public void onFailure(@NonNull Call<NetworkInformation> call, @NonNull Throwable throwable) {
        data.setValue(
          new Resource<>(APIClientStatus.Status.ERROR, null, throwable.getMessage(), throwable));
      }
    });
    return data;
  }

通常情况下,您会将“input”用作“getNetworkInformation”函数的ID或其他内容,这将需要返回与该ID相关联的不同LiveData。能否分享一下“getNetworkInformation”函数正在做什么?这可能是您根本问题所在的地方。我猜它正在构建一个新的LiveData - 确定它是否需要构建一个新的LiveData或者是否可以使用“setValue”/“postValue”来更新现有的LiveData将非常有帮助。 - Lyla
有关更新检索网络数据的LiveData的示例(与创建新的LiveData相反),请参见此类 - Lyla
这里创建了一个LiveData,并在网络请求完成后进行更新。 (https://github.com/googlecodelabs/android-build-an-app-architecture-components/blob/arch-training-steps/app/src/main/java/com/example/android/sunshine/data/network/WeatherNetworkDataSource.java#L64) (https://github.com/googlecodelabs/android-build-an-app-architecture-components/blob/arch-training-steps/app/src/main/java/com/example/android/sunshine/data/network/WeatherNetworkDataSource.java#L180) - Lyla
@Lyla 感谢您抽出时间,我已经更新了答案,API调用不需要任何输入,因此将Void添加为输入。但是在必要的地方,我会将Void更改为相关的类或对象。希望这是正确的实现。 - Rohan Kandwal

0

我没有遇到同样的问题,但我遇到了一个类似的情况,即每次我保存数据到数据库时观察者的数量都在增加。我的调试方式是查看有多少个实例或不同的实例被调用,我发现当你从视图模型中获取实时数据时,需要检查它是否为非空或者只返回一个实例。

private LiveData<T> data;
    public LiveData<T> getLiveData(){
        if(data ==null){
            data = //api call or fetch from db
        }
        return data;
    }

之前我只是返回了 data 对象,然后在检查源代码后得出结论,即livedata自动更新您的对象,而每次都会创建新实例,而不带有空值检查,并且会附加新的观察者。 如果我对livedata的理解有误,请某人纠正我。


你看过droidCon的视频吗?那很有帮助。 - Rohan Kandwal
与她的会面很有帮助!她推荐了它。 - Rohan Kandwal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接