Skip to content

A Kotlin Multiplatform implementation of Eugene Myers' Diffing Algorithm

License

Notifications You must be signed in to change notification settings

evilya/Difference

 
 

Repository files navigation

Difference

Difference is a Kotlin multiplatform differencing library. Given two lists, Difference will compute the insert and delete operations required to transform the starting list into the final list. Difference can also optionally detect items that have moved to new indices in the list.

Behind the scenes, Difference uses Eugene Myer's Differencing Algorithm. This is the same algorithm used by Android's DiffUtil class, which you may be familiar with if you're an Android developer. Difference is very similar to Android's DiffUtil, but is completely platform agnostic and has more receiver-agnostic APIs to consume the diff result.

Setup

Gradle can automatically resolve which variation of Difference is appropriate for the platform and architecture you're compiling for. To use the universal dependency, add this dependency to your module's build.gradle:

dependencies {
    implementation 'dev.andrewbailey.difference:difference:1.0.0'
}

If you want to explicitly specify which platform variant of the library you want to depend on, you can use any of the following dependencies as appropriate:

dev.andrewbailey.difference:difference-jvm:1.0.0
dev.andrewbailey.difference:difference-js:1.0.0
dev.andrewbailey.difference:difference-linux-x64:1.0.0
dev.andrewbailey.difference:difference-macos-x64:1.0.0
dev.andrewbailey.difference:difference-ios-x64:1.0.0
dev.andrewbailey.difference:difference-ios-arm64:1.0.0
dev.andrewbailey.difference:difference-mingw-x86:1.0.0
dev.andrewbailey.difference:difference-mingw-x64:1.0.0

Generating a diff

To generate a diff, you can call differenceOf with the following arguments:

val listOfBffs = listOf("Merengue", "Sherb", "Tammy")
val revisedListOfBffs = listOf("Merengue", "Sprinkle", "Sherb")

val diff = differenceOf(
    original = listOfBffs,
    updated = revisedListOfBffs,
    detectMoves = false
)

In Java, you can write this code as:

List<String> listOfBffs = Arrays.asList("Merengue", "Sherb", "Tammy");
List<String> revisedListOfBffs = Arrays.asList("Merengue", "Sprinkle", "Sherb");

DiffResult<String> diff = Difference.differenceOf(listOfBffs, revisedListOfBffs, false);

The diff that will be generated for this input will look like this:

Insert(index = 1, item = "Sprinkle")
Remove(index = 3)

Please keep in mind that calling differenceOf is an expensive operation, so it's recommended to offload this call to a background thread. See the Performance section for more info.

Using a DiffResult

differenceOf returns a DiffResult<T> object, typed based on the input lists. To consume a diff, you can call DiffResult<T>.applyDiff. There are several overloads that can increase the performance of how your diff is applied, but the most basic call looks something like this:

...

val diff = differenceOf(
    original = listOfBffs,
    updated = revisedListOfBffs,
    detectMoves = true
)

// When `applyDiff` returns, `output` will be equal to `revisedListOfBffs`
val output = listOfBffs.toMutableList()
diff.applyDiff(
    remove = { index: Int ->
        output.removeAt(index)
    },
    insert = { item: T, index: Int ->
        output.add(index, item)
    },
    move = { oldIndex: Int, newIndex: Int ->
        // You can leave this blank if you set `detectMoves` to false
        output.add(
            element = removeAt(oldIndex),
            index = if (newIndex < oldIndex) {
                newIndex
            } else {
                newIndex - 1
            }
        )
    }
)

If you're using Difference in Java, using applyDiff can be a bit awkward to use because it exposes Kotlin's FunctionN interfaces, named parameters, and default arguments to be easy to call. You can alternatively use DiffReceiver to improve the readability of your diff callbacks in a Java project.

The same receiver logic can be expressed like this in Java:

DiffResult<String> diff = differenceOf(...);

// When `applyDiff` returns, `output` will be equal to `revisedListOfBffs`
List<String> output = ArrayList<>(listOfBffs);
DiffReceiver<String> receiver = new DiffReceiver<>() {
    @Override
    public void remove(int index) {
        output.removeAt(index);
    }

    @Override
    public void insert(String item, int index) {
        output.add(index, item);
    }

    @Override
    public void move(int oldIndex, int newIndex) {
        // You can omit this override if you set `detectMoves` to false
        int index = newIndex;
        if (newIndex >= oldIndex) {
            index--;
        }

        output.add(index, output.removeAt(oldIndex));
    }
};

receiver.applyDiff(diff);

Note that DiffReceiver is only available in Java projects. If your project is a 100% Kotlin project, then you should stick to applyDiff and ignore DiffReceiver.

Performance

Difference uses a linear-space non-recursive implementation of Eugene Myer's Differencing algorithm. The algorithm takes O((M+N)×D + D log D) operations, where M and N are the lengths of the input lists, and D is the minimum number of edits it takes to transform the original list into the final list. If you enable movement detection, this runtime increases by O(D²).

Diff generation can take a while, so for UI-centric applications, you should avoid calling differenceOf on your main thread. Depending on conditions and the client's hardware, diff generation for arbitrary lists can easily cause your application to block the main thread and cause frame drops if you aren't careful.

Benchmarks

Difference has benchmark tests that can run on an Android device. Below is the measured time it takes to compute diffs of various sizes on a Pixel 3 running Android Q.

These values should not be used to estimate how long any call to differenceOf will take. Real-world performance will vary device-to-device, and can be influenced by conditions such as the device's hardware, CPU load, temperature, and battery level, among other factors. You should take these values with a grain of salt and make note of how the time taken to generate a diff grows exponentially based on the size of the inputs.

Pixel 3 Benchmark (without movement detection)

Starting list size Number of operations Time taken
100 10 0.059 ms
1000 100 0.983 ms
5000 500 17.6 ms
10000 1000 55.6 ms

Pixel 3 Benchmark (with movement detection)

Starting list size Number of operations Time taken
100 10 0.055 ms
1000 100 1.158 ms
5000 500 22.0 ms
10000 1000 73.5 ms

About

A Kotlin Multiplatform implementation of Eugene Myers' Diffing Algorithm

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Kotlin 100.0%