الارتقاء بـ LiveData إلى مستوى أعلى مع MediatorLiveData وTransformations
مقدمة إلى MediatorLiveData وTransformations لبناء أنماط تفاعلية (reactive) باستخدام LiveData.

MediatorLiveData
بحسب تعريفها في التوثيق، هي صنف فرعي (subclass) من LiveData يستطيع "مراقبة" كائنات LiveData أخرى، ويستدعي الدالة onChanged() عندما تتغير قيمة LiveData المراقَب.
LiveData<String> source = ....; //
final MediatorLiveData<String> result = new MediatorLiveData<>();
result.addSource(source, new Observer<String>() {
@Override
public void onChanged(@Nullable String x) {
// do something here
}
});
lang-java
في الكود أعلاه، عندما تتغير قيمة source تُستدعى الدالة onChanged الخاصة بالـ Observer.
Transformations
هذا الصنف أشبه بأداة مساعدة مبنية على MediatorLiveData. ويحتوي على دالتين فقط هما map() وswitchMap()
@MainThread
public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
@NonNull final Function<X, Y> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
@Override
public void onChanged(@Nullable X x) {
result.setValue(func.apply(x));
}
});
return result;
}
@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
@NonNull final Function<X, LiveData<Y>> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(trigger, new Observer<X>() {
LiveData<Y> mSource;
@Override
public void onChanged(@Nullable X x) {
LiveData<Y> newLiveData = func.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
@Override
public void onChanged(@Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
lang-java
تؤدي الدالتان الوظيفة نفسها: تطبّقان دالة على القيمة المخزنة في كائن LiveData، ثم تمرّران النتيجة إلى ما بعدها. الفرق أن الدالة التي نمررها إلى switchMap() يجب أن تُرجع كائن LiveData.
أنشأت مشروعًا تجريبيًا هنا. يمكنكم الاطلاع عليه ومتابعتي وأنا أشرح كيف يعمل الكود.
أستخدم https://jsonplaceholder.typicode.com/ كواجهة API للاختبار مع الـ endpoint /todos/1. وأنشأت نموذجًا باسم Todo يطابق شكل الاستجابة
import com.google.gson.annotations.SerializedName
// https://jsonplaceholder.typicode.com/todo/1
class Todo(
@SerializedName("userId") val userId: Int,
@SerializedName("id") val id: Int,
@SerializedName("title") val title: String,
@SerializedName("completed") val completed: Boolean
)
lang-kotlin
وهناك صنفان إضافيان للتعامل مع استجابات الـ API.
import android.arch.lifecycle.LiveData
data class ResponseError(val code: Int? = null, val messages: String? = null)
data class ResponseResult<T>(val data: LiveData<T>, val error: LiveData<ResponseError>)
lang-kotlin
يحتوي ResponseResult على خاصيتين، كلتاهما من نوع LiveData: الأولى للاستجابة، والثانية للخطأ (ResponseError).
وبحسب توصيات Google، أنشأت وحدة repository تتولى جلب البيانات من الشبكة. وفي هذه الوحدة استخدمت حيلة صغيرة مع ResponseResult:
class TodoRepository(private val api: TodoApi) {
fun getTodos(): ResponseResult<Todo> {
val data = MutableLiveData<Todo>()
val error = MutableLiveData<ResponseError>()
api.getTodos().enqueue(object : Callback<Todo> {
override fun onResponse(call: Call<Todo>, response: Response<Todo>) {
if (response.isSuccessful) {
data.value = response.body()
} else {
error.value = ResponseError(response.code(), response.errorBody()?.string())
}
}
override fun onFailure(call: Call<Todo>, t: Throwable) {
error.value = ResponseError()
}
})
return ResponseResult(data, error)
}
}
lang-kotlin
تُرجع الدالة getTodos() كائنًا من نوع ResponseResult<Todo> له خاصيتان: قيمة data تتحدّث عند اكتمال الاستجابة، و قيمة error تتحدّث عند حدوث خطأ.
والتزامًا بمبادئ SOLID، أبقيت صنف الـ Usecase بسيطًا جدًا.
class GetTodoUsecases(private val repository: TodoRepository) {
operator fun invoke() = repository.getTodos()
}
lang-kotlin
ربط كائنات LiveData ببعضها (Chaining)
لنلقِ نظرة على صنفَي الـ ViewModel والـ Fragment:
class MainFragment: Fragment(){
...
...
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel._onActivityCreated.call()
}
...
...
...
}
class MainViewModel(private val getTodoUsecase: GetTodoUsecases) : ViewModel() {
val _onActivityCreated = SingleLiveEvent<Any>()
private val responseResult: LiveData<ResponseResult<Todo>> = Transformations.map(_onActivityCreated) {
getTodoUsecase()
}
val todoResult: LiveData<Todo> = Transformations.switchMap(responseResult) {
it.data
}
val errorResult: LiveData<ResponseError> = Transformations.switchMap(responseResult) {
it.error
}
}
lang-kotlin
أطلقتُ كائن LiveData (SingleLiveEvent) داخل onActivityCreated(). وإذا نظرتم إلى السطر 18 فستجدون أن Transformation.map() تُستدعى، أي أن getTodoUsecase() ستُستدعى. وبعد انتهاء الجلب تُسنَد القيمة إلى responseResult. وإذا نظرتم مجددًا إلى السطرين 22 و 26 فستجدون أن Transformation.switchMap() تُستدعى، أي أنه عندما تتحدّث قيمة responseResult سيشير الكائنان todoResult و errorResult إلى الكائنات نفسها التي تمثلها خصائص responseResult.
نصيحة: لن تُستدعى الدالة OnChanged() إذا لم يكن هناك أي مراقب (observer) لكائن MediatorLiveData.
المرجع:


