没有Room的NetworkBoundResource助手类

时间:2017-10-25 03:05:59

标签: android android-architecture-components android-livedata

当我尝试为Room Db和Retrofit实现NetworkBoundResourceResource助手类时,它完美无缺。但是,我需要使用仅在没有房间的情况下进行改造来实现RESTful的搜索结果。 Resources类是好的,我不需要改变它。我想要做的是尝试删除此类中的数据库源。

public abstract class NetworkBoundResource<ResultType, RequestType> {
  private final AppExecutors appExecutors;

  private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

  @MainThread
  public NetworkBoundResource(AppExecutors appExecutors) {
    this.appExecutors = appExecutors;
    result.setValue(Resource.loading(null));
    LiveData<ResultType> dbSource = loadFromDb();
    result.addSource(dbSource, data -> {
      result.removeSource(dbSource);
      if (shouldFetch(data)) {
        fetchFromNetwork(dbSource);
      } else {
        result.addSource(dbSource, newData -> setValue(Resource.success(newData)));
      }
    });
  }

  @MainThread
  private void setValue(Resource<ResultType> newValue) {
    if (!Objects.equals(result.getValue(), newValue)) {
      result.setValue(newValue);
    }
  }

  private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
    LiveData<ApiResponse<RequestType>> apiResponse = createCall();
    // we re-attach dbSource as a new source, it will dispatch its latest value quickly
    result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));
    result.addSource(apiResponse, response -> {
      result.removeSource(apiResponse);
      result.removeSource(dbSource);
      //noinspection ConstantConditions
      if (response.isSuccessful()) {
        appExecutors.diskIO().execute(() -> {
          saveCallResult(processResponse(response));
          appExecutors.mainThread().execute(() ->
              // we specially request a new live data,
              // otherwise we will get immediately last cached value,
              // which may not be updated with latest results received from network.
              result.addSource(loadFromDb(),
                  newData -> setValue(Resource.success(newData)))
          );
        });
      } else {
        onFetchFailed();
        result.addSource(dbSource,
            newData -> setValue(Resource.error(response.errorMessage, newData)));
      }
    });
  }

  protected void onFetchFailed() {
  }

  public LiveData<Resource<ResultType>> asLiveData() {
    return result;
  }

  @WorkerThread
  protected RequestType processResponse(ApiResponse<RequestType> response) {
    return response.body;
  }

  @WorkerThread
  protected abstract void saveCallResult(@NonNull RequestType item);

  @MainThread
  protected abstract boolean shouldFetch(@Nullable ResultType data);

  @NonNull
  @MainThread
  protected abstract LiveData<ResultType> loadFromDb();

  @NonNull
  @MainThread
  protected abstract LiveData<ApiResponse<RequestType>> createCall();
}

5 个答案:

答案 0 :(得分:5)

问题是,任何加载的数据必须首先通过数据库,然后将其从数据库加载到UI,如NetworkBoundResource所做的那样。因此,我所做的是分离持久数据库并创建一个临时字段来加载。

例如,如果我想编辑original搜索方法,我建议:

public LiveData<Resource<List<Repo>>> search(String query) {
    return new NetworkBoundResource<List<Repo>, RepoSearchResponse>(appExecutors) {

        // Temp ResultType
        private List<Repo> resultsDb;

        @Override
        protected void saveCallResult(@NonNull RepoSearchResponse item) {
            // if you don't care about order
            resultsDb = item.getItems();
        }

        @Override
        protected boolean shouldFetch(@Nullable List<Repo> data) {
            // always fetch.
            return true;
        }

        @NonNull
        @Override
        protected LiveData<List<Repo>> loadFromDb() {
            if (resultsDb == null) {
                return AbsentLiveData.create();
            }else {
                return new LiveData<List<Repo>>() {
                    @Override
                    protected void onActive() {
                        super.onActive();
                        setValue(resultsDb);
                    }
                };
            }
        }

        @NonNull
        @Override
        protected LiveData<ApiResponse<RepoSearchResponse>> createCall() {
            return githubService.searchRepos(query);
        }

        @Override
        protected RepoSearchResponse processResponse(ApiResponse<RepoSearchResponse> response) {
            RepoSearchResponse body = response.body;
            if (body != null) {
                body.setNextPage(response.getNextPage());
            }
            return body;
        }
    }.asLiveData();
}

我跑了它就行了。

修改 我制作了另一个更简单的类来处理它(Daniel Wilson的另一个答案有更多功能并且已更新)。

但是,此类没有依赖关系,并且只转换为基础知识以仅生成获取响应:

abstract class NetworkBoundResource<RequestType> {

    private val result = MediatorLiveData<Resource<RequestType>>()

    init {
        setValue(Resource.loading(null))
        fetchFromNetwork()
    }

    @MainThread
    private fun setValue(newValue: Resource<RequestType>) {
        if (result.value != newValue) {
            result.value = newValue
        }
    }

    private fun fetchFromNetwork() {
        val apiResponse = createCall()
        result.addSource(apiResponse) { response ->
            result.removeSource(apiResponse)

            when (response) {
                is ApiSuccessResponse -> {
                        setValue(Resource.success(processResponse(response)))
                }

                is ApiErrorResponse -> {
                    onFetchFailed()
                    setValue(Resource.error(response.errorMessage, null))

                }
            }
        }
    }

    protected fun onFetchFailed() {
    }

    fun asLiveData() = result as LiveData<Resource<RequestType>>

    @WorkerThread
    protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body

    @MainThread
    protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}

因此,在使用它时,只能实现一种方法createCall()

fun login(email: String, password: String) = object : NetworkBoundResource<Envelope<User>>() {
    override fun createCall() = api.login(email, password)
}.asLiveData()

答案 1 :(得分:2)

这是我很久以后的尝试!

abstract class NetworkOnlyResource<ResultType, RequestType>
@MainThread constructor(private val appExecutors: AppExecutors) {

    private val result = MediatorLiveData<Resource<ResultType>>() //List<Repo>
    private val request = MediatorLiveData<Resource<RequestType>>() //RepoSearchResponse

    init {
        result.value = Resource.loading(null)
        fetchFromNetwork()
    }

    @MainThread
    private fun setResultValue(newValue: Resource<ResultType>) {
        if (result.value != newValue) {
            result.value = newValue
        }
    }

    private fun fetchFromNetwork() {
        val apiResponse = createCall()

        result.addSource(apiResponse) { response ->
            result.removeSource(apiResponse)

            response?.let {
                if (response.isSuccessful) {
                    appExecutors.diskIO().execute({
                        val requestType = processResponse(response)
                        val resultType = processResult(requestType)
                        appExecutors.mainThread().execute({
                            setResultValue(Resource.success(resultType))
                        }
                        )
                    })
                } else {

                    val errorMessage = when (response.errorThrowable) {
                        is HttpException -> "An error has occurred: ${response.errorThrowable.code()} Please try again."
                        is SocketTimeoutException -> "A timeout error has occurred, please check your internet connection and try again"
                        is IOException -> "An IO error has occurred, most likely a network issue. Please check your internet connection and try again"
                        is UnauthorizedCredentialsException -> "This user name or password is not recognized"
                        else -> {
                            response.errorMessage
                        }
                    }

                    Timber.e(errorMessage)

                    errorMessage?.let {
                        val requestType = processResponse(response)
                        val resultType = processResult(requestType)
                        setResultValue(Resource.error(errorMessage, resultType, response.errorThrowable))
                    }

                    onFetchFailed()
                }
            }
        }
    }

    protected open fun onFetchFailed() {}

    fun asLiveData() = result as LiveData<Resource<ResultType>>

    @WorkerThread
    protected open fun processResponse(response: ApiResponse<RequestType>) = response.body

    @WorkerThread
    protected abstract fun processResult(item: RequestType?): ResultType?

    @MainThread
    protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}

processResult()函数允许您将成功的RequestType转换为ResultType。它似乎对我有用,但会喜欢知道他们在做什么的人的任何反馈:)

Fyi Yigit已经使用更好的错误处理更新了NetworkBoundResource,这也应该在不成功的'else'语句中使用。

答案 2 :(得分:1)

这是我以前写的版本:

import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.support.annotation.MainThread

/**
 * A generic class to send loading event up-stream when fetching data
 * only from network.
 *
 * @param <RequestType>
</RequestType></ResultType> */
abstract class NetworkResource<RequestType> @MainThread constructor() {

    /**
     * The final result LiveData
     */
    private val result = MediatorLiveData<Resource<RequestType>>()

    init {
        // Send loading state to UI
        result.value = Resource.loading()
        fetchFromNetwork()
    }

    /**
     * Fetch the data from network and then send it upstream to UI.
     */
    private fun fetchFromNetwork() {
        val apiResponse = createCall()
        // Make the network call
        result.addSource(apiResponse) { response ->
            result.removeSource(apiResponse)

            // Dispatch the result
            response?.apply {
                when {
                    status.isSuccessful() -> setValue(this)
                    else -> setValue(Resource.error(errorMessage))
                }
            }
        }
    }

    @MainThread
    private fun setValue(newValue: Resource<RequestType>) {
        if (result.value != newValue) result.value = newValue
    }

    fun asLiveData(): LiveData<Resource<RequestType>> {
        return result
    }

    @MainThread
    protected abstract fun createCall(): LiveData<Resource<RequestType>>
}

答案 3 :(得分:0)

仅在您需要时使用此操作(使用kotlin coroutine

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

/**
 * A generic class that can provide a resource backed by the sqlite database.
 *
 *
 * @param <ResultType>
</ResultType> */
abstract class DatabaseResource<ResultType> {

    private val result = MediatorLiveData<Resource<ResultType>>()

     init {
         result.value = Resource.loading(null)
         GlobalScope.launch(Dispatchers.IO) {
         val dbSource = performDbOperation()
         GlobalScope.launch(Dispatchers.Main) {
            result.addSource(dbSource) { data ->
                result.removeSource(dbSource)
                result.addSource(dbSource) { newData ->
                    setValue(Resource.success(newData))
                }
              }
            }
         }
     }

    private fun setValue(newValue: Resource<ResultType>) {
        if (result.value != newValue) {
            result.value = newValue
        }
    }

    fun asLiveData() = result as LiveData<Resource<ResultType>>

    protected abstract fun performDbOperation(): LiveData<ResultType>
}

答案 4 :(得分:0)

对于未来的 Kotlin 用户,请简化为:

1.资源类:

sealed class Resource<T>(
    val data: T? = null,
    val error: Throwable? = null
) {
    class Success<T>(data: T) : Resource<T>(data)
    class Loading<T>(data: T? = null) : Resource<T>(data)
    class Error<T>(throwable: Throwable, data: T? = null) : Resource<T>(data, throwable)
}

2.网络绑定资源:

inline fun <T> networkBoundResource(
    crossinline fetch : suspend () -> Response<T>
) = flow {

    emit(Resource.Loading(null))

    try {
        emit(Resource.Success(fetch().body()))
    }catch(throwable : Throwable){
        emit(Resource.Error(throwable, null))
    }
}