简体   繁体   中英

Making a generic network adapter using livedata, retrofit, mvvm and repository pattern

I am new to android architecture components and I am trying to use LiveData and ViewModels with mvvm, repository pattern and retrofit. Referred to GitHubSample google gave in its architecture guide but want to simplify it little bit for my needs. Below is the code which I had so far but having below problems in completing it.

  1. onActive() method in LiveDataCallAdapter is not invoking at all
  2. Not able to figure out how I can get the response as a LiveData(I get this as null always) in SettingsData class? Ideally here I just want to have success and failure listener and I should get the data inside these blocks. All the generic network errors should already be handled before coming to this class. I am not able to figure out how to do this. 3.I do not want to call.enqueue in this SettingsData class which many examples shows

Any help is greatly appreciated. Thanks in advance

//Activity
private fun loadApplicationSettings() {

        val settingsViewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
        settingsViewModel.userApplicationSettings.observe(this, Observer<UserApplicationSettings> { userApplicationSettingsResult ->

            Log.d("UserApplicationSettings", userApplicationSettingsResult.toString())
            userSettingsTextView.text = userApplicationSettingsResult.isPushNotificationEnabled
        })
    }

//ViewModel
class SettingsViewModel : ViewModel() {

    private var settingsRepository: SettingsRepository
    lateinit var userApplicationSettings: LiveData<UserApplicationSettings>

    init {
        settingsRepository = SettingsRepository()
        loadUserApplicationSettings()
    }

    private fun loadUserApplicationSettings() {
        userApplicationSettings = settingsRepository.loadUserApplicationSettings()
    }
}

//Repository
class SettingsRepository {

    val settingsService = SettingsData()

    fun loadUserApplicationSettings(): LiveData<UserApplicationSettings> {
        return settingsService.getUserApplicationSettings()
    }
}

//I do not want to do the network calls in repository, so created a seperate class gets the data from network call 
class SettingsData {

    val apiBaseProvider = ApiBaseProvider()

    fun getUserApplicationSettings(): MutableLiveData<UserApplicationSettings> {

        val userApplicationSettingsNetworkCall = apiBaseProvider.create().getApplicationSettings()

        //Not sure how to get the data from userApplicationSettingsNetworkCall and convert it to livedata to give to repository
        // deally here I just want to have success and failure listener and I should get the data inside these blocks. All the generic network errors should already be handled before coming to this class. I am not able to figure out how to do this.

        val userApplicationSettingsData: LiveData<ApiResponse<UserApplicationSettings>> = userApplicationSettingsNetworkCall  

     //Thinking of having a success and fail block here and create a LiveData object to give to repository. Not sure how to do this

        return userApplicationSettingsData
    }
}

//Settings Service for retrofit 
interface SettingsService {

    @GET("url")
    fun getApplicationSettings(): LiveData<ApiResponse<UserApplicationSettings>>
}

//Base provider of retrofit
class ApiBaseProvider {

    fun create(): SettingsService {

        val gson = GsonBuilder().setLenient().create()
        val okHttpClient = createOkHttpClient()

        val retrofit = Retrofit.Builder()
            .addCallAdapterFactory(LiveDataCallAdapterFactory())
            .addConverterFactory(GsonConverterFactory.create(gson))
            .baseUrl("url")
            .build()

        return retrofit.create(SettingsService::class.java)
    }
}

//
class LiveDataCallAdapterFactory : Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (getRawType(returnType) != LiveData::class.java) {
            return null
        }
        val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
        val rawObservableType = getRawType(observableType)
        if (rawObservableType != ApiResponse::class.java) {
            throw IllegalArgumentException("type must be a resource")
        }
        if (observableType !is ParameterizedType) {
            throw IllegalArgumentException("resource must be parameterized")
        }
        val bodyType = getParameterUpperBound(0, observableType)
        return LiveDataCallAdapter<Any>(bodyType)
    }
}

//Custom adapter that does the network call
class LiveDataCallAdapter<T>(private val responseType: Type) : CallAdapter<T, LiveData<ApiResponse<T>>> {

    override fun responseType(): Type {
        return responseType
    }

        override fun adapt(call: Call<T>): LiveData<ApiResponse<T>> {

        return object : LiveData<ApiResponse<T>>() {
            override fun onActive() {
                super.onActive()

                call.enqueue(object : Callback<T> {
                    override fun onResponse(call: Call<T>, response: Response<T>) {
                        println("testing response: " + response.body())
                        postValue(ApiResponse.create(response)) 

                    }

                    override fun onFailure(call: Call<T>, throwable: Throwable) {
                        postValue(ApiResponse.create(throwable))
                    }
                })
            }
        }
    }
}


//I want to make this class as a generic class to do all the network success and error handling and then pass the final response back
/**
 * Common class used by API responses.
 * @param <T> the type of the response object
</T> */
sealed class ApiResponse<T> {

    companion object {

        fun <T> create(error: Throwable): ApiErrorResponse<T> {
            return ApiErrorResponse(error.message ?: "unknown error")
        }

        fun <T> create(response: Response<T>): ApiResponse<T> {

            println("testing api response in create")

            return if (response.isSuccessful) {
                val body = response.body()
                if (body == null || response.code() == 204) {
                    ApiEmptyResponse()
                } else {
                    ApiSuccessResponse(
                        body = body
                    )
                }
            } else {
                val msg = response.errorBody()?.string()
                val errorMsg = if (msg.isNullOrEmpty()) {
                    response.message()
                } else {
                    msg
                }
                ApiErrorResponse(errorMsg ?: "unknown error")
            }
        }
    }
}

/**
 * separate class for HTTP 204 responses so that we can make ApiSuccessResponse's body non-null.
 */
class ApiEmptyResponse<T> : ApiResponse<T>()

data class ApiErrorResponse<T>(val errorMessage: String) : ApiResponse<T>()

data class ApiSuccessResponse<T>(
    val body: T
) : ApiResponse<T>() {
}

We can connect Activity/Fragment and ViewModel as below:

Firstly, we have to create our ApiResource which will handle the retrofit response.

public class ApiResource<T> {

    @NonNull
    private final Status status;

    @Nullable
    private final T data;

    @Nullable
    private final ErrorResponse errorResponse;

    @Nullable
    private final String errorMessage;

    private ApiResource(Status status, @Nullable T data, @Nullable ErrorResponse errorResponse, @Nullable String errorMessage) {
        this.status = status;
        this.data = data;
        this.errorResponse = errorResponse;
        this.errorMessage = errorMessage;
    }

    public static <T> ApiResource<T> create(Response<T> response) {
        if (!response.isSuccessful()) {
            try {
                JSONObject jsonObject = new JSONObject(response.errorBody().string());
                ErrorResponse errorResponse = new Gson()
                    .fromJson(jsonObject.toString(), ErrorResponse.class);
                return new ApiResource<>(Status.ERROR, null, errorResponse, "Something went wrong.");
            } catch (IOException | JSONException e) {
                return new ApiResource<>(Status.ERROR, null, null, "Response Unreachable");
            }
        }
        return new ApiResource<>(Status.SUCCESS, response.body(), null, null);
    }

    public static <T> ApiResource<T> failure(String error) {
        return new ApiResource<>(Status.ERROR, null, null, error);
    }

    public static <T> ApiResource<T> loading() {
        return new ApiResource<>(Status.LOADING, null, null, null);
    }

    @NonNull
    public Status getStatus() {
        return status;
    }

    @Nullable
    public T getData() {
        return data;
    }

    @Nullable
    public ErrorResponse getErrorResponse() {
        return errorResponse;
    }

    @Nullable
    public String getErrorMessage() {
        return errorMessage;
    }
}

The Status is just an Enum class as below:

public enum Status {
    SUCCESS, ERROR, LOADING
}

The ErrorResponse class must be created in such a way that the getter and setter can handle the error.

RetrofitLiveData class

public class RetrofitLiveData<T> extends LiveData<ApiResource<T>> {

    private Call<T> call;

    public RetrofitLiveData(Call<T> call) {
        this.call = call;
        setValue(ApiResource.loading());
    }

    Callback<T> callback = new Callback<T>() {
        @Override
        public void onResponse(Call<T> call, Response<T> response) {
            setValue(ApiResource.create(response));
        }

        @Override
        public void onFailure(Call<T> call, Throwable t) {
            setValue(ApiResource.failure(t.getMessage()));
        }
    };

    @Override
    protected void onActive() {
        super.onActive();
        call.enqueue(callback);
    }

    @Override
    protected void onInactive() {
        super.onInactive();
        if (!hasActiveObservers()) {
            if (!call.isCanceled()) {
                call.cancel();
            }
        }
    }

}

Repository class

public class Repository {

    public LiveData<ApiResource<JunoBalanceResponse>> getJunoBalanceResponse(Map<String, String> headers) {
        return new RetrofitLiveData<>(ApiClient.getJunoApi(ApiClient.BASE_URL.BASE).getJunoBalance(headers));
    }

}

JunoBalanceResponse contains the objects and its getters and setters that I am waiting as a response of my retrofit request.

below is an example for the api interface.

public interface JunoApi {

    @Headers({"X-API-Version: 2"})
    @GET("balance")
    Call<JunoBalanceResponse> getJunoBalance(@HeaderMap Map<String, String> headers);

}

ApiClient class

public class ApiClient {

    public enum BASE_URL {
        AUTH, BASE
    }

    private static Retrofit retrofit;

    private static final String JUNO_SANDBOX_AUTH_URL = "https://sandbox.boletobancario.com/authorization-server/";
    private static final String JUNO_SANDBOX_BASE_URL = "https://sandbox.boletobancario.com/api-integration/";

    private static Retrofit getRetrofit(String baseUrl) {

        OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
            .connectTimeout(90, TimeUnit.SECONDS)
            .readTimeout(90, TimeUnit.SECONDS)
            .writeTimeout(90, TimeUnit.SECONDS)
            .build();

        retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        return retrofit;
    }

    public static JunoApi getJunoApi(BASE_URL targetPath) {
        switch (targetPath) {
            case AUTH: return getRetrofit(JUNO_SANDBOX_AUTH_URL).create(JunoApi.class);
            case BASE: return getRetrofit(JUNO_SANDBOX_BASE_URL).create(JunoApi.class);
            default: return getRetrofit(JUNO_SANDBOX_BASE_URL).create(JunoApi.class);
        }
    }
}

Now we can connect our Repository and ApiViewModel .

public class ApiViewModel extends ViewModel {

    private Repository repository = new Repository();

    public LiveData<ApiResource<JunoBalanceResponse>> getJunoBalanceResponse(Map<String, String> headers) {
        return repository.getJunoBalanceResponse(headers);
    }
}

And finally, we can observe the retrofit response in our Activity/Fragment

 apiViewModel = ViewModelProviders.of(requireActivity()).get(ApiViewModel.class);
 apiViewModel.getJunoBalanceResponse(headers).observe(getViewLifecycleOwner(), new Observer<ApiResource<JunoBalanceResponse>>() {
    @Override
    public void onChanged(ApiResource<JunoBalanceResponse> response) {
        switch (response.getStatus()) {
            case LOADING:
                Log.i(TAG, "onChanged: BALANCE LOADING");
                break;
            case SUCCESS:
                Log.i(TAG, "onChanged: BALANCE SUCCESS");
                break;
            case ERROR:
                Log.i(TAG, "onChanged: BALANCE ERROR");
                break;
        }
    }
});

First make sure your activity is observing settingsViewModel.userApplicationSettings and are calling loadApplicationSettings() .

Assuming the remaining of the implementation works correctly all you have to do in SettingsData is return the call from ApiBaseProvider().create().getApplicationSettings() as it returns LiveData based on the LiveDataCallAdapter implementation:

class SettingsData {
    fun getUserApplicationSettings(): LiveData<UserApplicationSettings> =
        ApiBaseProvider().create().getApplicationSettings()
}

Therefore Observer<UserApplicationSettings> from the Activity will be notified as you are already subscribed to it.

Also if you take a look at LiveDataCallAdapter :

call.enqueue(object : Callback<T> {
    override fun onResponse(call: Call<T>, response: Response<T>) {
        println("testing response: " + response.body())
        postValue(ApiResponse.create(response))

    }

    override fun onFailure(call: Call<T>, throwable: Throwable) {
        postValue(ApiResponse.create(throwable))
    }
})

You can see that the adapter is posting success and failure to whoever is listening.

After taking a close look at LiveDataCallAdapter at return object : LiveData<ApiResponse<T>>() { override fun onActive() { the onActive is only called when the number of active observers change to 1 from 0.

I took a pick at LiveDataCallAdapter.kt from googlesamples and noticed the use of private var started = AtomicBoolean(false) and if (started.compareAndSet(false, true)) which are not being implemented here, so I suposse this is activating the Livedata.onActive to be triggered.

You may try and add that to your logic as well and if you want to know more about AtomicBoolean take a look at AtomicBoolean.compareAndSet(!flag, flag)?

You can also check LiveData#onactive .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM