Skip to content

Commit

Permalink
Merge pull request #136 from LouisCAD/2.0.0-alpha4-source-compatibility
Browse files Browse the repository at this point in the history
2.0.0-alpha4 source compatibility, migration guide, docs update, CHANGELOG,
Checked Lazy update and optional lambda for wrapInRecyclerView extension function.
  • Loading branch information
LouisCAD authored Nov 11, 2018
2 parents 54261e9 + 47cb67a commit aa940e7
Show file tree
Hide file tree
Showing 27 changed files with 1,278 additions and 98 deletions.
483 changes: 483 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Kotlin.
- **[Bundle:](bundle)** `BundleSpec` to use `Bundle` with property syntax for `Intent` extras
and more.
- **[Collections:](collections)** `forEach` for `List`s without `Iterator` allocation.
- **[Checked Lazy:](checkedlazy)** `uiLazy` that checks property access on UI thread, and
- **[Checked Lazy:](checkedlazy)** `mainThreadLazy` that checks property access on main thread, and
`checkedLazy` to make your own variant.
- **[Dimensions:](dimensions)** Android `dp` extensions for `View` and `Context`. Particularly
handy when using [View DSL](viewdsl).
Expand All @@ -42,7 +42,8 @@ boilerplate.
properties.
- **[Init Provider:](initprovider)** Base class for `ContentProvider`s used for automatic
initialization purposes.
- **[Intents:](intents)** Transform `companion object`s into powerful typesafe intent specs.
- **[Intents:](intents)** Transform `companion object`s into powerful typesafe intent specs, and
create `PendingIntent`s the clean and easy way.
- **[Main Handler:](mainhandler)** Top-level `mainHandler` property to stop allocating multiple
`Handler`s for main `Looper`.
- **[Main Thread:](mainthread)** Properties and precondition checkers related to Android main thread.
Expand Down Expand Up @@ -95,7 +96,7 @@ into your root project `build.gradle` file:
```groovy
allProjects {
ext {
splitties_version = '2.0.0-alpha5'
splitties_version = '2.0.0-alpha6'
}
}
```
Expand Down
20 changes: 10 additions & 10 deletions arch-lifecycle/src/main/java/splitties/arch/lifecycle/ViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,41 @@ import android.arch.lifecycle.ViewModelProvider
import android.arch.lifecycle.ViewModelProviders
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import splitties.checkedlazy.uiLazy
import splitties.checkedlazy.mainThreadLazy

inline fun <reified VM : ViewModel> FragmentActivity.activityScope() = uiLazy {
inline fun <reified VM : ViewModel> FragmentActivity.activityScope() = mainThreadLazy {
ViewModelProviders.of(this).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.activityScope() = uiLazy {
inline fun <reified VM : ViewModel> Fragment.activityScope() = mainThreadLazy {
ViewModelProviders.of(activity!!).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.fragmentScope() = uiLazy {
inline fun <reified VM : ViewModel> Fragment.fragmentScope() = mainThreadLazy {
ViewModelProviders.of(this).get(VM::class.java)
}

inline fun <reified VM : ViewModel> FragmentActivity.activityScope(factory: ViewModelProvider.Factory) = uiLazy {
inline fun <reified VM : ViewModel> FragmentActivity.activityScope(factory: ViewModelProvider.Factory) = mainThreadLazy {
ViewModelProviders.of(this, factory).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.activityScope(factory: ViewModelProvider.Factory) = uiLazy {
inline fun <reified VM : ViewModel> Fragment.activityScope(factory: ViewModelProvider.Factory) = mainThreadLazy {
ViewModelProviders.of(activity!!, factory).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.fragmentScope(factory: ViewModelProvider.Factory) = uiLazy {
inline fun <reified VM : ViewModel> Fragment.fragmentScope(factory: ViewModelProvider.Factory) = mainThreadLazy {
ViewModelProviders.of(this, factory).get(VM::class.java)
}

inline fun <reified VM : ViewModel> FragmentActivity.activityScope(noinline factory: () -> VM) = uiLazy {
inline fun <reified VM : ViewModel> FragmentActivity.activityScope(noinline factory: () -> VM) = mainThreadLazy {
ViewModelProviders.of(this, TypeSafeViewModelFactory(factory)).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.activityScope(noinline factory: () -> VM) = uiLazy {
inline fun <reified VM : ViewModel> Fragment.activityScope(noinline factory: () -> VM) = mainThreadLazy {
ViewModelProviders.of(activity!!, TypeSafeViewModelFactory(factory)).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.fragmentScope(noinline factory: () -> VM) = uiLazy {
inline fun <reified VM : ViewModel> Fragment.fragmentScope(noinline factory: () -> VM) = mainThreadLazy {
ViewModelProviders.of(this, TypeSafeViewModelFactory(factory)).get(VM::class.java)
}

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ allprojects {

// Libraries
groupId = "com.louiscad.splitties"
library_version = "2.0.0-alpha5"
library_version = "2.0.0-alpha6"
isSnapshot = library_version.endsWith("-SNAPSHOT")
isRelease = !isSnapshot

Expand Down
12 changes: 6 additions & 6 deletions checkedlazy/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# Checked Lazy
This library provides a `checkedLazy()` method that returns a `Lazy` delegate,
as well as `uiLazy()`.
as well as `mainThreadLazy()`.

`checkedLazy()` takes as first parameter a function where you can implement an access check.

The second parameter is the lazy initializer, as in Kotlin stdlib `lazy`.

`uiLazy { … }` is a shorthand for `checkedLazy(::checkUiThread) { … }`.
It's there because UI thread checking is a common use case on Android due to
`mainThreadLazy { … }` is a shorthand for `checkedLazy(::checkMainThread) { … }`.
It's there because main thread checking is a common use case on Android due to
its synchronized nature and its omnipresence.

## Example

```kotlin
val noUiThreadChecker = noAccessOn(uiThread)
val noMainThreadChecker = noAccessOn(mainThread)

class YourClass {
val greeting: String by uiLazy { "Hello Splitties!" }
val expensiveObject by checkedLazy(noUiThreadChecker) { doHeavyInstantiation() }
val greeting: String by mainThreadLazy { "Hello Splitties!" }
val expensiveObject by checkedLazy(noMainThreadChecker) { doHeavyInstantiation() }
}
```

Expand Down
14 changes: 12 additions & 2 deletions checkedlazy/src/main/java/splitties/checkedlazy/CheckedLazy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@
package splitties.checkedlazy

/**
* Returns a lazy that throws an [IllegalStateException] if its value is accessed outside of UI thread.
* Returns a lazy that throws an [IllegalStateException] if its value is accessed outside of main thread.
*/
fun <T> uiLazy(initializer: () -> T): Lazy<T> = CheckedAccessLazyImpl(initializer, uiChecker)
@Deprecated(
"Although the UI thread is the main thread by default, this is not always the case. " +
"Prefer referring to the main thread instead.",
ReplaceWith("mainThreadLazy(initializer)", "splitties.checkedlazy.mainThreadLazy")
)
fun <T> uiLazy(initializer: () -> T): Lazy<T> = mainThreadLazy(initializer)

/**
* Returns a lazy that throws an [IllegalStateException] if its value is accessed outside of main thread.
*/
fun <T> mainThreadLazy(initializer: () -> T): Lazy<T> = CheckedAccessLazyImpl(initializer, mainThreadChecker)

/**
* Creates a new instance of the [Lazy] that uses the specified initialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package splitties.checkedlazy
import splitties.exceptions.illegal
import splitties.mainthread.checkMainThread

@JvmField internal val uiChecker = { checkMainThread() }
@JvmField internal val mainThreadChecker = { checkMainThread() }

/**
* Throws an [IllegalStateException] if invoked outside of passed [thread].
Expand Down
3 changes: 3 additions & 0 deletions collections/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ This is useful when you need to iterate on a `List` in performance critical
conditions (e.g. code executed/called from an `onDraw` method, or code run
on the UI thread more generally).

You also have the reverse equivalents: `forEachReversedByIndex` and
`forEachReversedWithIndex`.

## Download

```groovy
Expand Down
60 changes: 49 additions & 11 deletions intents/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
# Intents

*Transform `companion object`s into powerful typesafe intent specs.*
*Transform `companion object`s into powerful typesafe intent specs, and
create `PendingIntent`s the clean and easy way.*

## The problem about intents in Android
## Table of contents

* [Using `companion object`s as typesafe and key-safe intent specs](#using-companion-objects-as-typesafe-and-key-safe-intent-specs)
* [The problem about intents in Android](#the-problem-about-intents-in-android)
* [The solution](#the-solution)
* [IntentSpec interfaces](#intentspec-interfaces)
* [IntentSpec implementations providers](#intentspec-implementations-providers)
* [Using an IntentSpec](#using-an-intentspec)
* [IntentSpec example](#intentspec-example)
* [Creating `PendingIntent`s the clean and easy way](#creating-pendingintents-the-clean-and-easy-way)
* [Download](#download)

## Using `companion object`s as typesafe and key-safe intent specs

### The problem about intents in Android

An Android component that can receive an Intent (like an `Activity` or a
`BroacastReceiver`) can expect an action, or extras. In case of an action,
the string must match exactly in order to work properly. In case of an extra,
the type must also match. This makes it not typesafe at all. You need to write
some doc to document which constants to use, and which types are the extras,
and you need to be sure to read the doc, to ensure you do it right.
some documentation to explain which constants to use, with which types,
and you need to be sure to read the doc afterwards, to ensure you do it right.

*There should be a better way.*

## The solution
### The solution

This split provides a few interfaces that your `companion object`s can
implement using delegation so they become an intent specification that you
can use to build and `Intent` or start an `Activity` in a type safe way.

## Content
implement using delegation so they become an intent specification, that you
can use to build and `Intent`, start an `Activity`, start a `Service` or send
a broadcast in a type safe way.

### IntentSpec interfaces

Expand All @@ -35,7 +49,7 @@ defining the intent spec of an `Activity`, `BroadcastReceiver` or `Service`.

### IntentSpec implementations providers

A few methods provide implementation of these interfaces:
A few methods provide implementation of the interfaces mentioned above:
* `activitySpec`
* `activityWithoutExtrasSpec` where `ExtrasSpec` is `Nothing`
* `receiverSpec`
Expand Down Expand Up @@ -66,7 +80,7 @@ the `intent` extension function mentioned above, and the optional expected
lambda has the same parameters as for `intent`. Finally, it calls
`sendBroadcast` with the created `Intent`.

## Example
### IntentSpec example

Let's take the example shown in the [Bundle](../bundle) README, adding an
`IntentSpec` to it. Notice the new `companion object` and how we start the
Expand Down Expand Up @@ -105,6 +119,30 @@ class StartDemoActivity : AppCompatActivity() {
}
```

## Creating `PendingIntent`s the clean and easy way

It's fair to say that the `PendingIntent` Android API is not designed for Kotlin,
and for a reason, it has been there since API level 1, the first Android version.

As a result, code using this API is not always the most readable part of a
Kotlin codebase.

This split provides several extension functions for `Intent` that return a
`PendingIntent`:
* `toPendingActivity()`
* `toPendingService()`
* `toPendingForegroundService()`, which also works before API 26
* `toPendingBroadcast()`

and an extension for `Array<Intent>`: `toPendingActivities()`.

All these functions have two optional parameters that default to zero:
`reqCode` and `flags`.

The `toPendingActivity()` and `toPendingActivities()` functions also have
an `options: Bundle?` parameter that defaults to `null` (and is ignored
below API 16).

## Download

```groovy
Expand Down
79 changes: 70 additions & 9 deletions preferences/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

*Property syntax for Android's SharedPreferences.*

For use in Kotlin. This library uses Kotlin's property delegation to make
using SharedPreferences as easy as accessing a property on an object. It
relies on the `appCtx` module of this library to allow usage in `object` in
Kotlin, and can support storage on device encrypted storage for devices
This library uses Kotlin's property delegation to make using
SharedPreferences as easy as accessing a property on an object. It
relies on the `appCtx` module of this library to allow usage in `object`,
and can support storage on device encrypted storage for devices
supporting Direct Boot. See [the source code](
/src/main/java/splitties/preferences) for more information
/src/main/java/splitties/preferences) for more information.

## Table of contents

* [Defining the preferences properties in an object](#defining-the-preferences-properties-in-an-object)
* [Why `object` and not `class`?](#why-object-and-not-class)
* [Loading the preferences without blocking the main thread](#loading-the-preferences-without-blocking-the-main-thread)
* [Download](#download)

## Defining the preferences properties in an object

### Usage
Define your preferences in an `object` that extends
`splitties.preferences.Preferences`, like in the example below:
```kotlin
Expand Down Expand Up @@ -57,10 +65,11 @@ The supported types are:
For **default SharedPreferences**, make an `object` that extends
`DefaultPreferences` instead of `Preferences`.

#### Why `object` and not `class`?
### Why `object` and not `class`?

Using a `class` instead of an `object` is not recommended because it would
mean you can instantiate it multiple times, while the underlying preferences
Unless you use coroutines (read more about this in next section just below),
a `class` instead of an `object` is not recommended because it would mean
you can instantiate it multiple times, while the underlying preferences
xml file is cached for the rest of your app's process lifetime once loaded,
so in a `class` you'd be allocating the delegates more times than needed,
leading to an additional, unneeded, small pressure on the garbage collector.
Expand All @@ -70,6 +79,58 @@ use cases where adding logic to base `Preferences` or sharing some
properties may be desirable. (If you do, please open an issue to tell us
about this use case. It may become an example shown here.)

## Loading the preferences without blocking the main thread

The `object` approach described above has several advantages, one of
the most significant being ease of use anywhere in your app, but that
also means you can easily access it from the main thread, and the first
time you access the object, the underlying xml file where the preferences
are stored is loaded, which may block the main thread for longer that you
would want, possibly dropping a few frames.

With coroutines, it's easy to offload something on another thread, and
this split embraces this capability.

Let's see a modified version of the `GamePreferences` described above,
before passing in review each change.

```kotlin
import splitties.preferences.experimental.SuspendPrefsAccessor
import splitties.preferences.Preferences

class GamePreferences private constructor() : Preferences("gameState") {
companion object : SuspendPrefsAccessor<GamePreferences>(::GamePreferences)

var magicNumber by intPref(0) // The property name is used as the key.
var currentLevel by IntPref("currentLevel", 1)
var bossesFought by IntPref("bossBattleVictories", 0)
var lastTimePlayed by LongPref("lastSessionTime", 0L)
var pseudo by StringPref("playerPseudo", "Player 1")
var favoriteCharacter by stringOrNullPref()
}
```

Here are all the changes:
- We moved from `object` to `class`.
- We added a `private constructor()`.
- We added a `companion object` that extends the `SuspendPrefsAccessor` abstract
class and calls its constructor with a reference to the constructor.

With this change, we can no longer access the `GamePreferences` singleton directly
from anywhere… unless we are in a coroutine!

From any `suspend` function, you
just have to call `GamePreferences()` like you were calling a constructor, but
in reality, it is a function call that suspends while loading the preferences
for the first time in process life in `Dispatchers.IO`.

If the preferences have already been loaded, it immediately returns the now
instantiated singleton.

If you have non suspending functions that would need to access the preferences,
you have two options: pass your `Preferences` subclass as a parameter, or make
it a `suspend` function.

## Download

```groovy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.louiscad.splittiessample.demo
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.louiscad.splittiessample.R
import splitties.checkedlazy.uiLazy
import splitties.checkedlazy.mainThreadLazy
import splitties.snackbar.action
import splitties.snackbar.onDismiss
import splitties.snackbar.snack
Expand All @@ -31,7 +31,7 @@ import splitties.views.onClick

class DemoActivity : AppCompatActivity(), DemoUi.Host {

private val ui by uiLazy { DemoUi(this, this) }
private val ui by mainThreadLazy { DemoUi(this, this) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@

package com.louiscad.splittiessample.prefs

import splitties.preferences.experimental.SuspendPrefsAccessor
import splitties.preferences.Preferences
import splitties.preferences.experimental.SuspendPrefsAccessor

class GamePreferences private constructor() : Preferences("gameState") {
companion object : SuspendPrefsAccessor<GamePreferences>(::GamePreferences)

var magicNumber by intPref(0)
var currentLevel by IntPref("currentLevel", 1)
var bossesFought by IntPref("bossBattleVictories", 0)
var lastTimePlayed by LongPref("lastSessionTime", 0L)
var pseudo by StringPref("playerPseudo", "Player 1")

companion object : SuspendPrefsAccessor<GamePreferences>(::GamePreferences)
}
Loading

0 comments on commit aa940e7

Please sign in to comment.