Skip to content

Instantly share code, notes, and snippets.

@thegarlynch
Last active June 24, 2020 20:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thegarlynch/476ba1474a2ad96ff28872dd7e9d70ef to your computer and use it in GitHub Desktop.
Save thegarlynch/476ba1474a2ad96ff28872dd7e9d70ef to your computer and use it in GitHub Desktop.
Dynamic Form
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import io.reactivex.Observable
interface Input {
val mandatory : Boolean
val focusIds : Array<Int>
fun buildView(context : Context) : View
val completeSignal : Observable<Boolean>
val layoutParams : LinearLayout.LayoutParams
}
class InputForeman(private val schema : List<Input>) {
fun build(context: Context) : LinearLayout {
TODO("buildView into vertical LinearLayout based on it's layoutParams")
}
fun observeCompletion() : Observable<Boolean> {
TODO("join all schema completeSignal")
}
}
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import io.reactivex.Observable
interface Input {
val mandatory : Boolean
fun buildView(context : Context) : View
val completeSignal : Observable<Boolean>
val layoutParams : LinearLayout.LayoutParams
}
/**
* Then I realize that every [Input] needs id so you can set value to it
*/
typealias ID = String
class InputForeman(private val schema : Map<ID, Input>) {
private lateinit var inputViews : Map<ID, View>
fun build(context: Context) : LinearLayout {
TODO("buildView into vertical LinearLayout based on it's layoutParams")
}
fun observeCompletion() : Observable<Boolean> {
TODO("join all input completeSignal")
}
fun getValue() : Map<ID, Any?> {
TODO("")
}
fun setValue(values : Map<ID, Any?>){
TODO("")
}
}
/**
*
* But this is still bad since it turns out, every implementation of [Input] i made, also made schema holds [View] instance unintentionally
*
* @sample
*
* class EditInput(override mandatory : Boolean) : Input {
*
* private lateinit var editText : EditText
*
* override fun buildView(context : Context) : EditText {
* editText = EditText(context)
* return editText
* }
*
* override val completeSignal : Observable<Boolean>
* get() {
* return Observable.create { emit ->
* if(mandatory){
* it.onNext(true)
* editText.addOnTextChanged {
* emit.onNext(it.toString().isNotEmpty())
* }
* }else{
* it.onNext(true)
* }
* }
* }
*
* override val layoutParams = LinearLayout.LayoutParams(-1, -2)
*
* }
*
* So, i can't do this, [Input] is better if it is immutable, so that i can save schema to storage
*/
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import io.reactivex.Observable
interface Input {
val mandatory : Boolean
fun buildView(context : Context) : InputHolder
val layoutParams : LinearLayout.LayoutParams
}
/**
* Now this is better
*/
class InputHolder(
val view : View,
val completeSignal : Observable<Boolean>,
/**
* imagine if i have Composite Input where it buildView based on it's child input
* and imagine if this Composite Input somehow compose all EditText in horizontal manner
*
*
* Turns out i need view focusesId too. you can use [View.generateViewId] when you build the view
*/
val focusedIds : Array<Int>
)
typealias ID = String
class InputForeman(private val schema : Map<ID, Input>) {
private lateinit var inputHolders : Map<ID, InputHolder>
fun build(context: Context) : LinearLayout {
TODO("buildView into vertical LinearLayout based on it's layoutParams")
}
fun observeCompletion() : Observable<Boolean> {
TODO("join all input completeSignal")
}
/**
* Turns out by making value [Any] is something you should not do.
* since you will lose the benefit of knowing what type the input value should be cast with
*/
fun getValue() : Map<ID, Any?> {
TODO("")
}
fun setValue(values : Map<ID, Any?>){
TODO("")
}
}
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import io.reactivex.Observable
/**
* i want to made Input<T> with generic T so i can enforce the value for InputHolder
* is it good ? hmm.. i dunno.. if you do this. you might have need class<T> TOO!!
*
* but let's try it..
*/
interface Input<T> {
val cls : Class<T>
val mandatory : Boolean
fun buildView(context : Context) : InputHolder<T>
val layoutParams : LinearLayout.LayoutParams
}
abstract class InputHolder<T>(
val view : View,
val completeSignal : Observable<Boolean>,
val focusedIds : Array<Int>
){
abstract fun setValue(value : T)
abstract fun getValue() : T
}
/**
* But this is BAD TOO!!
*
* T will be an invariant!!
*/
typealias ID = String
class InputForeman(private val schema : Map<ID, Input<*>>) {
private lateinit var inputHolders : Map<ID, InputHolder<*>>
fun build(context: Context) : LinearLayout {
TODO("buildView into vertical LinearLayout based on it's layoutParams")
}
fun observeCompletion() : Observable<Boolean> {
TODO("join all input completeSignal")
}
fun getValue() : Map<ID, Any?> {
TODO("")
}
fun setValue(values : Map<ID, Any?>){
TODO("")
}
}
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import io.reactivex.Observable
/**
* Enter my inspiration, JsonWriter and JsonReader
*/
typealias ID = String
interface Input<T> {
/**
* But writer and reader needs ID!!
*/
val id : ID
val cls : Class<T>
val mandatory : Boolean
fun buildView(context : Context) : InputHolder<T>
val layoutParams : LinearLayout.LayoutParams
}
class Writer(){
private val innerMap = mutableMapOf<ID, Any?>()
val map : Map<ID, Any?> get() = innerMap
fun <T> write(input : Input<T>, value : T){
innerMap[input.id] = input.cls.cast(value)
}
}
class Reader(private val map : Map<ID, Any?>){
fun <T> read(input : Input<T>) : T?{
val value = map.getValue(input.id)
return input.cls.cast(value)
}
}
abstract class InputHolder<T>(
/**
* Turns out you need input from input holder
*/
val input : Input<T>,
val view : View,
val completeSignal : Observable<Boolean>,
val focusedIds : Array<Int>
){
abstract fun write(writer: Writer)
abstract fun read(reader: Reader)
}
class InputForeman(private val schema : List<Input<*>>) {
private lateinit var inputHolders : Map<ID, InputHolder<*>>
fun build(context: Context) : LinearLayout {
TODO("buildView into vertical LinearLayout based on it's layoutParams")
}
fun observeCompletion() : Observable<Boolean> {
TODO("join all input completeSignal")
}
/**
* Turns out this is different than you think,
* [InputHolder.write] is used to get value from view
* and [InputHolder.read] is used to set value from Values Map
*/
fun getValue() : Map<ID, Any?> {
val writer = Writer()
for((id, inputHolder) in inputHolders){
inputHolder.write(writer)
}
return writer.map
}
fun setValue(values : Map<ID, Any?>){
val reader = Reader(values)
for((id, inputHolder) in inputHolders){
inputHolder.read(reader)
}
}
}
/**
* Another idea i have tried is making T a subclass of Value object to made it easier
* to be converted to moshi
*
* To make it more complicated sometimes,
* sometimes you need [LiveData] object so that you can generate for example Dropdown options on REAL TIME
*
* where do you put it ?
* i don't code how to solve this yet. but better watch out for this kind of thing
*
* Oh, some advice. don't use recyclerView in this. JUST DON'T
* I have thinking a lot about how to abstract this into RecyclerView
* but it is too hard. you have to consider binding. ViewType.
*
* if you think about it, why do you need recyclerview at all ?
* the only benefit you get from recyclerView is reducing view.
*
* but all the cases that i found. you don't always get 10 views with the same input
* it is always at most 6 to 8 EditTextInput, 2 or 3 ImageInput. your ViewHolder isn't gonna save you from
* input variation
*
* that's the end of our journey. i hope this is helpful
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment