Kotlin下OkHttp WebSocket客户端简单用法

Introduction

OkHttp provides convenient way to create a WebSocket client. We can send and receive messages from and to a WebSocket server. But there are some notable problems


Use OkHttp

It is simple. Open your project level build.gradle file and add following codes in the dependencies section.

implementation 'com.squareup.okhttp3:okhttp:3.11.0'

Don’t forget to add INTERNET permission in AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="..." >

    <uses-permission android:name="android.permission.INTERNET" />

    <application>
       ...
    </application>

</manifest>

Create WebSocket Client

We need to build a request with a WebSocket url, such as “ws://…” or “was://…”. Next, get a OkHttpClient and new a WebSocket connection with the previous request and a listener, WebSocketListener().

val request = Request.Builder().url(YOUR_WS_PATH).build()

val webSocket = OkHttpClient().newWebSocket(request, object WebSocketListener() {...})

In the WebSocketListener(), we can override onOpen(), onMessage(), onFailure(), onClosed() and so on to trigger desired logics.

Then, An Exception Occurs ?!

All functions in WebSocketListener() are called in a background thread.

OkHttp use an async call to deal a WebSocket connect to prevent blocking main thread. Therefore, if you call functions doing UI related logics after receiving some specific messages, an exception will occur and your application maybe crash at that moment. You can simply use a Handler() or RxJava skills to back to UI thread.

val handler = Handler()
override() fun onMessage(...) {
    handler.post{...}
}

原文链接:https://medium.com/@yangcar/use-okhttp-to-implement-websocket-client-in-android-app-72483224310d

理解Android Core: Looper, Handler, 和HandlerThread

This Article covers Android Looper, Handler, and HandlerThread. These are among the building blocks of Android OS.

In my own experience, I have used them in a very limited context until recently. My use case involved sending tasks to the main/ui thread, primarily to update the UI from any other thread. The other aspects of the multi-threaded operation were handled through alternate ways like ThreadPoolExecutor, IntentService, and AsyncTask.

MultiThreading and task running are old subjects. Java itself has java.util.concurrent package and Fork/Join framework to facilitate it. Several libraries have been written to streamline asynchronous operations. RxJava is the most popular library today for reactive programming and designing an asynchronous application.

So, why am I writing about the old school?

Looper, Handler, and HandlerThread are the Android’s way of solving the problems of asynchronous programming. They are not old school, but a neat structure on which a complex android framework is built.

For new developers, it’s highly recommended to understand the principles behind them and experienced one’s should revisit this topic to recollect the minor details.

I have also created a video tutorial for this subject, and I highly recommend to watch it. Click here to watch now.

Use Cases:

  1. The main thread in Android is built with a Looper and Handlers. So, the understanding of it is essential to create an unblocked responsive UI.
  2. The developers writing libraries cannot afford to use third party libraries because of the library size. So, for them, the best option is to utilize the existing available resource. Writing own solution for it may not always get that level of efficiency and optimization.
  3. The same argument can also be made for companies/individuals shipping out SDKs. The clients can have varied implementations, but all of them will share the common android framework APIs.
  4. Understanding them fully will enhance the capacity to follow the Android SDK and package classes in general.

Let’s start the exploration/revision with a questionnaire.

I expect the reader to have the basic understanding of java threads. If you need, then get a quick overview of javaThreadandRunnable.

What is the problem with java thread?

Java threads are one-time use only and die after executing its run method.

Can we improve upon it?

The Thread is a double edged sword. We can speed up the execution by distributing the tasks among threads of execution, but can also slow it down when threads are in excess. Thread creation in itself is an overhead. So, the best option is to have an optimum number of threads and reuse them for tasks execution.

Model for thread reusability:

  1. The thread is kept alive, in a loop via it’s run() method.
  2. The task is executed serially by that thread and is maintained in a queue (MessageQueue).
  3. The thread must be terminated when done.

What is the Android’s way of doing it?

The above model is implemented in the Android via Looper, Handler, and HandlerThread. The System can be visualized to be a vehicle as in the article’s cover.

  1. MessageQueue is a queue that has tasks called messages which should be processed.
  2. Handler enqueues task in the MessageQueue using Looper and also executes them when the task comes out of the MessageQueue.
  3. Looper is a worker that keeps a thread alive, loops through MessageQueue and sends messages to the corresponding handler to process.
  4. Finally Thread gets terminated by calling Looper’s quit() method.

One thread can have only one unique Looper and can have many unique Handlers associated with it.

Creating Looper and MessageQueue for a Thread:

A thread gets a Looper and MessageQueue by calling Looper.prepare() after its running. Looper.prepare() identifies the calling thread, creates a Looper and MessageQueue object and associate the thread with them in ThreadLocal storage class. Looper.loop()must be called to start the associated looper. Similarly, the looper must be terminated explicitly through looper.quit().

class LooperThread extends Thread {
      public Handler mHandler; 

      public void run() { 
          Looper.prepare();

          mHandler = new Handler() { 
              public void handleMessage(Message msg) { 
                 // process incoming messages here
                 // this will run in non-ui/background thread
              } 
          }; 

          Looper.loop();
      } 
  }

Creating Handler for a Thread:

A Handler gets implicitly associated with the thread that instantiates it via thread’s Looper, but we can explicitly tie it to a thread by passing the thread’s looper in the constructor of the Handler.

handler = new Handler() {
@Override
public void handleMessage(Message msg) {
        // process incoming messages here
        // this will run in the thread, which instantiates it
    }
};

Sending messages to the MessageQueue via Handler can be done by two modes:

  1. Message: It is a class that defines various useful methods to deal with message data. To send an object we set the obj variable.
Message msg = new Message();
msg.obj = "Ali send message";
handler.sendMessage(msg);

Detailed overview of Message class can be found here: https://developer.android.com/reference/android/os/Message.html

2. Runnable: A runnable can also be posted in the MessageQueue. Ex: posting and running a task in the main thread.

new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
        // this will run in the main thread
    }
});

In the above example, we create a Handler and provide Looper associated with the main thread. This associate this handler to the main thread. When we post the Runnable, it gets queued in the main thread’s MessageQueue and then executed in the main thread.

Handler is capable of message manipulation in a wide variety of ways, which can found here: https://developer.android.com/reference/android/os/Handler.html

Creating an own thread and providing Lopper and MessageQueue is not the right way to deal with the problem. So, Android has provided HandlerThread(subclass of Thread) to streamline the process. Internally it does the same things that we have done but in a robust way. So, always use HandlerThread.

One of the ways to create the HandlerThread is to subclass it and most of the time you will be using this method.

private class MyHandlerThread extends HandlerThread {

    Handler handler;

    public MyHandlerThread(String name) {
        super(name);
    }

    @Override
    protected void onLooperPrepared() {
        handler = new Handler(getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // process incoming messages here
                // this will run in non-ui/background thread
            }
        };
    }
}

Note: We have instantiated the Handler when the onLooperPrepared() is called. So, that Handler can be associated with that Looper.

  1. Looper is only prepared after HandlerThread’s start() is called i.e. after the thread is running.
  2. A Handler can be associated with a HandlerThread, only after it’s Looper is prepared.

Other way to create the HandlerThread:

HandlerThread handlerThread = new HandlerThread("MyHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());

Note: HandlerThread needs to call myHandlerThread.quit() to free the resources and stop the execution of the thread.

I would suggest practicing the above codes, so you can grasp their little details.

I have created an example project for Post Office simulation. Post Office is built upon HandlerThread and Clients communicate with the help of the Post Office. A Simulator class creates few Client Bots and delegate their communication to the MainActivity, which renders it in a live feed.

The link to this Example

I have also created a video tutorial for this subject, and I highly recommend to watch it. Click here to watch now.

Also, Let’s become friends on Twitter, Linkedin, Github, and Facebook.

Learning is a journey, let’s learn together!

原文链接:https://blog.mindorks.com/android-core-looper-handler-and-handlerthread-bd54d69fe91a

Kotlin HashMap用法示例

MainActivity.kt

package com.cfsuman.test

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Make the text view content scrollable
        textView.movementMethod = ScrollingMovementMethod()

        // Mutable hash map example
        button.setOnClickListener{
            val builder = StringBuilder()

            // Initialize a new empty mutable hash map
            val colors = mutableMapOf<String,String>()
            // Put some key value pairs to hash map
            colors.put("INDIANRED","#CD5C5C")
            colors.put("CRIMSON","#DC143C")
            colors.put("SALMON","#FA8072")
            colors.put("LIGHTCORAL","#F08080")

            builder.append("Loop through the mutable hash map")
            colors.forEach{key,value ->
                builder.append("\n$key,$value")
            }

            colors.remove("CRIMSON")
            builder.append("\n\n After remove an item")
            for ((key,value) in colors){
                builder.append("\n$key:$value")
            }


            // Replace/update a value
            colors.put("SALMON","NEW VALUE")
            builder.append("\n\n After updating a value")
            colors.forEach{key,value ->
                builder.append("\n$key,$value")
            }

            // Check whether hash map is empty
            builder.append("\n\nHashMap is empty? : ${colors.isEmpty()}")

            // Get value by key from hash map
            val value = colors.get("LIGHTCORAL")
            builder.append("\n\nLIGHTCORAL value $value")

            // Initialize a new hash map with key and value pairs
            val reds = mutableMapOf("RED" to "#FF0000", "FIREBRICK" to "#B22222", "CRIMSON" to "#DC143C")

            // Loop through the map
            builder.append("\n\nLoop through the new mutable hash map")
            reds.forEach{key,value->
                builder.append("\n$key : $value")
            }

            textView.text = builder.toString()
        }



        // Immutable hash map example
        button2.setOnClickListener{
            val builder = StringBuilder()

            // Initialize a new hash map with keys and values
            val colors = mapOf(
                    "GOLD" to "#FFD700",
                    "YELLOW" to "#FFFF00",
                    "ALICEBLUE" to "#F0F8FF",
                    "BISQUE" to "#FFE4C4"
            )

            builder.append("Loop through the map\n")
            // Loop through the map
            colors.forEach{key,value ->
                builder.append("\n$key : $value")
            }

            // Map keys to list
            val keys:List<String> = colors.keys.toList()

            // Map values to list
            val values:List<String> = colors.values.toList()


            // Loop through the map keys list
            builder.append("\n\nHashMap keys list\n")
            keys.forEach{
                builder.append("$it,")
            }

            // Loop through the map values list
            builder.append("\n\nHashMap values list\n")
            values.forEach{
                builder.append("$it,")
            }

            textView.text = builder.toString()
        }



        // Linked hash map example
        button3.setOnClickListener{
            val builder = StringBuilder()

            val colors = linkedMapOf<String,String>()
            colors.put("TEAL","#008080")
            colors.put("AQUA","#00FFFF")
            colors.put("LIGHTCYAN","#E0FFFF")
            colors.put("BLUE","#0000FF")
            colors.put("ROSYBROWN","#BC8F8F")

            builder.append("Loop through the linked hash map\n")
            // Loop through the linked hash map
            colors.forEach{key,value ->
                builder.append("\n$key : $value")
            }

            textView.text = builder.toString()
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:layout_editor_absoluteY="81dp">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Mutable HashMap"
        app:layout_constraintBottom_toTopOf="@+id/textView"
        app:layout_constraintEnd_toStartOf="@+id/button2"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />
    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="8dp"
        android:text="HashMap"
        app:layout_constraintEnd_toStartOf="@+id/button3"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="LinkedHashMap"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/button2"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/button"
        android:textColor="#ff1557"
        />
</android.support.constraint.ConstraintLayout>

原文链接:https://android–code.blogspot.com/2018/06/android-kotlin-hashmap-example.html

Kotlin版RecyclerView简要教程

Welcome, Here we are going to learn about Android’s RecyclerView. When we need a list of lots of data to display on screen we use RecyclerView.

Why the name is RecyclerView?


We all had seen some kind of list in the app which we are using daily, Let’s take an example of WhatsApp where you have a list of people. Let’s say if the view is displaying 0 to 9 items at a time, means recycler view had created those and bind the data with it and also RecyclerView had bound the data of the 10th item. So, if the user scroll, the next time is ready to display.Now, if user scroll, RecyclerView had to create the 11th item. So this way the items which had gone off the screen, they get reused. This way RecyclerView gets its name.

In this chapter, we are going to develop an app which will display 3 items in the RecyclerView list. Let’s start the development:

  1. Create a new project and here to use the RecyclerView, we need to add its dependency.
implementation ‘androidx.recyclerview:recyclerview:1.0.0’

2. Add RecyclerView in our activity_main.xml which is our main screen layout file.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvChapterList"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

3. Now, we need to create a new layout file. This will be layout of the item of the list.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp">

    <TextView
        android:id="@+id/tvChapterName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="26sp" />
</LinearLayout>

4. Here, we again need an adapter class same as viewPager, to fill the data and bind with the items.

class ChapterAdapter(
    private val context: MainActivity,
    private val chaptersList: ArrayList<String>
) :
    RecyclerView.Adapter<ChapterAdapter.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            LayoutInflater.from(context).inflate(
                R.layout.item_mindorks_chapters,
                parent,
                false
            )
        )
    }

    override fun getItemCount(): Int {
        return chaptersList.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.chapterName?.text = chaptersList.get(position)
        holder.itemView.setOnClickListener {
            Toast.makeText(context, chaptersList.get(position), Toast.LENGTH_LONG).show()
        }
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val chapterName = view.tvChapterName
    }
}


We will extend the adapter class with the RecyclerView.Adapter class.

5. Now, In our MainActivity class, we will create an ArrayList of string which we will pass to the adapter class to display on the RecyclerView.

class MainActivity : AppCompatActivity() {
    val chaptersList: ArrayList<String> = ArrayList()
    private lateinit var layoutManager: RecyclerView.LayoutManageroverride
    fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) chaptersList . add ("Android MVP Introduction")
        chaptersList.add("Learn RxJava")
        chaptersList.add("Advance Kotlin") layoutManager = LinearLayoutManager (this)
        rvChapterList.layoutManager = layoutManager
        rvChapterList.adapter = ChapterAdapter(this, chaptersList)
    }
}

6. Here, we also create a LinearLayoutManager and give it to the layoutManager of RecyclerView. We also set the adapter to the RecyclerView adapter and pass the array list to adapter class.

7. Inside the adapter class we have some override methods:

  • onCreateViewHolder() — which set the view to display to the items.
  • onBindViewHolder() — this method will fetch the specific data for that item’s position and set to the item view.
  • getItemCount() — this will give the count of the items in the list.

Let’s run this application. Great Work!! We had learn how to implement RecyclerView. There is much more in it like how to have a different layout of items in the list, how to have infinite list etc. Try to explore more and share us with on our twitter or slack channel.

原文链接:https://blog.mindorks.com/android-recyclerview-in-kotlin

[精华] 通过实例理解LiveData

Architecture components is now a big thing for Android developers. LiveData is one of the major component that we want to look in detail here.

In case you are are not familiar with Architecture Component, below might help.Android Architecture Components for Dummies in Kotlin (50 lines of code)Once upon a time, building Android App is as simple as writing all your code in an Activity, and you are done.medium.com

In my blog above, it covers ViewModels and LiveData. But the LiveData is illustrated like just a normal data passing mechanism from ViewModels to the View. I have not done LiveData justice, and now it’s pay back time for it.

So what’s so special about LiveData?

What is Live Data?

Formally it is defined as below

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state

Well, it’s a mouth full of explanation.

To make it simple let me share with you some history to understand the context better, as that helps appreciating it.

In the beginning…

When Android first introduced, most developer started coding in a single activity project.

However, it is not ideal that everything lump into the single God Activity class. Android Activity is hard to be unit tested too.

Due to this, everyone came up with various architecture model like MVP, MVC, MVVM… etc, where the logic side is done by another part e.g. Presenter, ViewModel, Controller etc.

This is better as it separate logic from the view. However it also has it’s problem. The Presenter, ViewModel, Controller etc, are not aware of the Activity Lifecycle. It has to be told of the Activity’s lifecycle.


Google now started to think, instead of letting everyone like wild-wild west defining what to do, it came up with Architecture Components.

The components e.g. ViewModels have a special capability. They are could made aware of the Activity LiveCycle without need the Activity telling them (at least explicitly).

Other than ViewModel, the mean that is used to communicate information back from ViewModel to Activity, the data in another word, also have the ability to be aware of the Activity’s lifecycle.

That’s the reason is is call LiveData, data that is aware of the LiveCycle of the interested observer (e.g. Activity).

Illustrate LiveData by making it the central of universal…

To illustrate better, let’s put LiveData in the center as the below diagram

From the diagram, you could see that the LiveData could be modified by the ViewModels (or whatever you like to use to modify the liveData)…

Upon updated, it would then notify all it’s observers (activities, fragment, service etc). Nonetheless, unlike any other approach (e.g. Rxjava), it doesn’t blindly notify them all, but instead check their live state first.

If the observer is active, then it could be notified of the change of data in the LiveData. However, if the Observer is Paused or Destroyed, it would then not be notified.

This is so cool, as we no longer need to worry about unsubscribing it onPause or onDestroy.

Besides, once the Observer is resumed, it would be immediately notified of the latest data from the LiveData.

Type of LiveData

LiveData is actually just an Abstract Class. So it can’t be used as itself. Fortunately Google has implemented some simple concrete class we could make use of.

MutableLiveData

This is the most simplest LiveData, where it would just get updated and notify it’s observer.

Defining it is as simple as below (2 lines)

// Declaring it
val liveDataA = MutableLiveData<String>()// Trigger the value change
liveDataA.value = someValue// Optionally, one could use liveDataA.postValue(value) 
// to get it set on the UI thread

To observe the LiveData is also simple. I have a fragment that observe the LiveDataA as below

class MutableLiveDataFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        getLiveDataA().observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

The result as below, once the data is set on LiveDataA i.e. 7567 and 6269, it is detected by the Fragment

From the code, you could see

getLiveDataA().observe(this, changeObserver)

but there’s no code to unsubscribe it when the Fragment is pausing or terminating.

Even without unsubscribing it, it won’t cause any problem.

Check out the below, even when the Fragment died, the LiveData value generate i.e. 1428 is not causing crashes due to setting on an inactive Fragment.

You would also notice when the Fragment came alive again, it will fetch the latest data i.e. 1428 from the LiveData.

Transformations.Map

Imagine if you are loading a data from a repository. Before you pass to the view, you would like to have it modified first.

We could still use LiveData, to pass the data across various entity as below.

We could transform the data from one LiveData and pass on to the other one using Transformations.map() function.

class TransformationMapFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        val transformedLiveData = Transformations.map(
                getLiveDataA()) { "A:$it" }
        transformedLiveData.observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

The behavior as shown below, where the data i.e. 5116 is transformed to a new form i.e. A:5116, before passing over to the Fragment.

Using Transformations.map is helpful to ensure that the LiveData doesn’t pass information over when the destination e.g. ViewModel and View is dead.

This is so cool, as we don’t need to worry about unsubscribing it as well.

Let’s look at underlying how Trasnformation.map is done…

@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;
}

Oh, it is using another type of LiveData named MediatorLiveData. Let’s dive into it…

MediatorLiveData

If you observe the above code, the main interesting part of MediatorLiveData is the ability to add source to it, and the code that change the content of the data.

This means we could have multiple LiveData feed to one destination through the MediatorLiveData as below.

We could use MediatorLiveData directly as below

class MediatorLiveDataFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        val mediatorLiveData = MediatorLiveData<String>()
        mediatorLiveData.addSource(getliveDataA())
              { mediatorLiveData.value = "A:$it" }
        mediatorLiveData.addSource(getliveDataB()) 
              { mediatorLiveData.value = "B:$it" }
        mediatorLiveData.observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

With this, you could see below, the Fragment able to receive from both LiveDataA and LiveDataB as it change.

There’s one caveat though. If the Fragment is not alive, and the data changes both on LiveDataA and LiveDataB, when the Fragment came alive, the MediatorLiveData will take the LiveData that is last added as source, i.e. LiveDataB.

Shown above, you could see that the Fragment upon restored, will always take from the LiveDataB, regardless if LiveDataB has change latter compare to LiveDataA or not. This is because in the code, you could see that LiveDataB is last added source to the MediatorLiveData.

Transformations.SwitchMap

Having ability to listen from 2 sources of LiveData is nice. But what if we want to control to only listen to one, and not the other, and switch between them as needed, should we write our logic do to so?

We can, but we don’t need to. Google has provided us another nice function, Transformations.switchMap().

@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;
}

The function is simply adding source and removing previous source accordingly. So there’s only one source always feed to the MediatorLiveData.

The Switch is also a liveData. So the entire operation looks as below.

To use it, would be simply as below

class TransformationSwitchMapFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)

        val transformSwitchedLiveData =
            Transformations.switchMap(getLiveDataSwitch()) { 
                switchToB ->
                if (switchToB) {
                     getLiveDataB()
                } else {
                     getLiveDataA()
                }
        }

        transformSwitchedLiveData.observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

With this, we could easily control which data would feed to our view as below. The data updated to the Fragment is when the connected data source change, or when the switch changes.

This is very handy to control application that has data feed from different sources controlled by the certain setting (e.g. user login session).

References

The above GIF are all created using an example App I created. You could get the source code from belowelye/demo_android_livedata_illustrationDemo live data and illustrating it. Contain MutableLiveData, MediatorLiveData, Transform.Map and Transform.SwitchMap …github.com

Compile and play around it, to get a good solid idea of how LiveData works.

More example

In case you don’t like the data to be made available to from your LiveData upon subscribing to it, you could refer to the below blogLiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)A convenient way for a view (activity or fragment) to communicate with a ViewModel is to use LiveData observables. The…medium.com


I hope this post is helpful to you. You could check out my other interesting topics here.

Follow me on medium, TwitterorFacebook for little tips and learning on Android, Kotlin etc related topics. ~Elye~

原文链接:https://medium.com/@elye.project/understanding-live-data-made-simple-a820fcd7b4d0