This library is an attempt to allow use of composition in Android
(which is initially spoiled by no-arg constructors and reflection)
and add static typing to Fragments by using generics instead of setArguments(Bundle)
and onActivityResult(Intent)
.
repositories {
...
maven { url 'https://dl.bintray.com/miha-x64/maven' }
}
implementation 'net.aquadc.flawless:flawless:0.0.7'
In Activity of parent Fragment, you implement ScreenFactory
:
class MainActivity : AppCompatActivity(), ScreenFactory {
...
fun createScreen(intent: AnyScreenIntent): AnyScreen = select(intent) {
// composition: you can pass to constructor whatever you want
RootScreenTag then { RootScreen(args, Companion::openDialogFragment, DialogScreenTag) }
DialogScreenTag then { DialogScreen(args) }
// 'args: ScreenArgs' contains argument, host (Fragment, DialogFragment, etc), and saved state
}
private companion object {
private val RootScreenTag
by tag(of<RootScreen>())
private val DialogScreenTag
by tag(of<DialogScreen>())
// ^ exact type of a screen
fun openDialogFragment(host: Fragment, new: DialogFragment) {
new.show(host.fragmentManager, null)
}
}
}
class RootScreen(
private val args: StatelessActionScreenArgs<SupportFragment>,
private val openDialog: (Fragment, DialogFragment) -> Unit,
private val questionScreenTag: SupportDialogFragScreenTag<ParcelString, ParcelString, *>
) : StatelessSupportFragScreen<ParcelUnit, ParcelUnit> {
// ^ input ^ output
...
private fun openDialog() {
val host = args.host
openDialog(host,
SupportDialogFragment(
questionScreenTag, // screen tag
ParcelString(input!!.text.toString()), // argument
host, OpenDialogRequestCode, // target, requestCode
pureParcelFunction2(RootScreen::gotResponse), // response callback
pureParcelFunction1(RootScreen::onCancel) // cancellation callback
)
)
}
private fun gotResponse(string: ParcelString) {
output.text = string.value
}
...
}
...and delivering results:
class DialogScreen : StatelessSupportDialogFragScreen<ParcelString, ParcelString> {
...
override fun createView(host: SupportDialogFragment<ParcelString, ParcelString>, parent: Context, argument: ParcelString): Dialog {
...
return AlertDialog.Builder(parent)
.setTitle(argument.value)
.setView(view)
.setPositiveButton("Ok") { _, _ ->
returnValue = ParcelString(view.text.toString())
}
.setNegativeButton("Cancel", null)
.create()
}
// will be automatically delivered when this fragment finish
override var returnValue: ParcelString? = null
private set
}
private fun takePhoto() {
host.requestPermissions(
RequestCameraPermCode,
pureParcelFunction2(RootScreen::takePhotoPermResult),
{ _, userAgreed ->
AlertDialog.Builder(host.activity)
.setMessage("We need permission to camera to do this.")
.setPositiveButton("Let's grant", { _, _ -> userAgreed.run() })
.setNegativeButton("Meh", null)
.show()
},
Manifest.permission.CAMERA
)
}
private fun takePhotoPermResult(granted: Collection<String>) {
if (Manifest.permission.CAMERA !in granted) {
return host.toast("Camera permission was denied.")
}
val i = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (i.resolveActivity(host.activity.packageManager) == null) {
return host.toast("Can't find app for taking pictures.")
}
host.registerRawResultCallback(TakePhotoRequestCode, pureParcelFunction3(RootScreen::photoTaken))
host.startActivityForResult(i, TakePhotoRequestCode)
}
private fun photoTaken(responseCode: Int, data: Intent?) {
host.toast(when (responseCode) {
Activity.RESULT_OK -> "OK"
Activity.RESULT_CANCELED -> "Canceled"
else -> "response code: $responseCode"
})
}
Screen consumes some arguments and returns a value after interacting with a user. Thus it can be treated as a suspend-function.
Library does not provide any coroutine-based APIs at the moment because continuations cannot be serialized yet: #76 in kotlinx.coroutines, #44 in kotlinx.serialization.
You can check out a sample flow which should not be used in production because it cannot handle process death.