📜 ⬆️ ⬇️

Create Android apps using Anko Layouts and Anko Coroutines

image


About a year ago, I started using Kotlin in my Android projects. I wanted to try something new that would be interesting to learn. Then I came across Anko . By that time, writing UI to xml is fed up. I always liked to write the interface with my hands, without resorting to WYSIWYG and the xml markup used in Android Studio. The only drawback is that you have to restart the application to check for any changes. You can use a plugin that shows how ui will look without launching the application, but it seemed to me rather strange. It also has a cool ability to convert xml to Anko Layouts DSL.


The biggest drawback of the library is the almost complete lack of documentation. To figure out how to properly use it, you often had to look at the source. This article will detail the application creation using Anko Layouts and Anko Coroutines.


The Anko library itself is divided into 4 independent parts:



To add a library to a project, it’s enough to add one line depending on the project:


implementation "org.jetbrains.anko:anko:$anko_version"


where anko_version is the current version of the library, written in the build.gradle file at the project level:


ext.anko_version='0.10.8'


Anko layouts


Anko Layouts allows you to develop UI Android applications more efficiently than it was using Java.


The main player on the field is the AnkoComponent<T> interface with the only createView method that accepts AnkoContext<T> and returns a View. It is in this method that the entire UI is created. The AnkoContext<T> interface is a wrapper over the ViewManager . More about him later.


Having a little understood how AnkoComponent<T> is arranged, we will try to create a simple UI in the class of our Activity. It is worth clarifying that such a "direct" writing of UI is possible only in Activity, since a separate extension function ankoView is written for it, in which the addView method is called , and AnkoContextImpl<T> is created in the method itself with the parameter setContentView = true .


 class AppActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) verticalLayout { lparams(matchParent, matchParent) gravity = Gravity.CENTER textView("Cool Anko TextView") { gravity = Gravity.CENTER } } } 

Obviously, for something larger than one TextView, the onCreate method will quickly turn into a dump. Let's try to separate the Activity class from the UI. To do this, create another class in which it will be described.


 class AppActivityUi: AnkoComponent<AppActivity> { override fun createView(ui: AnkoContext<AppActivity>): View = with(ui) { verticalLayout { lparams(matchParent, matchParent) gravity = Gravity.CENTER textView("Cool Anko TextView") { gravity = Gravity.CENTER } } } } 

Now, in order to transfer our UI to our Activity, you can use


 AppActivityUi().setContentView(this) 

Good, but what if we want to create a UI for a fragment? To do this, you can use the createView method directly by calling it from the fragment's onCreateView method. It looks like this:


 class AppFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return AppFragmentUi().createView(AnkoContext.create(requireContext(), this)) } } 

As already mentioned, AnkoContext<T> is a wrapper over the ViewManager. Its companion object has three main methods that return AnkoContext<T> . Let us examine them in more detail.


create


  fun <T> create(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T> 

and his twin brother


 fun create(ctx: Context, setContentView: Boolean = false): AnkoContext<Context> 

return AnkoContextImpl<T> .


Methods are used in all standard cases, as, for example, in the previous examples with Activity and Fragment. The most interesting here are the parameters owner and setContentView. The first allows you to pass to the createView instance method of a specific Fragment, Activity, or something else.


 MyComponent().createView(AnkoContext.create(context, myVew)) class MyComponent: AnkoComponent<View> { override fun createView(ui: AnkoContext<View>): View = with(ui) { val myView: View= ui.owner //     } } 

The second parameter, setContentView, will automatically try to add the resulting view if the Context is an instance of an Activity or ContextWrapper . If he doesn't succeed, he throws an IllegalStateException.


createDelegate


This method can be very useful, however it is not written about it anywhere in the official documentation on the Gihab . I stumbled upon it in the source when I solved the problem of blowing up UI classes:


 fun <T: ViewGroup> createDelegate(owner: T): AnkoContext<T> = DelegatingAnkoContext(owner) 

It allows you to add the result of the createView component to the owner.


Consider its use by example. Suppose we have a large class that describes one of the application screens - AppFragmentUi.


 verticalLayout { relativeLayout { id = R.id.toolbar //    view } relativeLayout{ id = R.id.content //     view } } 

Logically, it can be divided into two parts - the toolbar and content, AppFragmentUiToolbar and AppFragmentUiContent, respectively. Then our main AppFragmentUi class will become much simpler:


 class AppFragmentUi: AnkoComponent<AppFragment> { override fun createView(ui: AnkoContext<AppFragment>) = with(ui) { verticalLayout { AppFragmentUiToolbar().createView(AnkoContext.createDelegate(this)) AppFragmentUiContent().createView(AnkoContext.createDelegate(this)) } } } class AppFragmentUiToolbar : AnkoComponent<_LinearLayout> { override fun createView(ui: AnkoContext<_LinearLayout>): View = with(ui.owner) { relativeLayout { id = R.id.toolbar //   view } } } 

Notice that the u function is not ui, but ui.owner as an object.
Thus, we have the following algorithm:


  1. A component instance is created.
  2. The createView method creates a View to be added.
  3. The resulting View is added to the owner.

More approximate analog: this.addView(AppFragmentUiToolbar().createView(...))
As you can see, the createDelegate option is more enjoyable to read.


createReusable


It looks like a standard AnkoContext.create, but with a small addition - the most recent root view is considered:


 class MyComponent: AnkoComponent<MyObject> { override fun createView(ui: AnkoContext<MyObject>): View = with(ui) { textView("Some text") //       IllegalStateException: View is already set //     "Another text" textView("Another text") } } 

In the standard implementation, if the root view is set, an attempt to set the second view in parallel will raise an exception .


The createReusable method returns the ReusableAnkoContext class, which is inherited from AnkoContextImpl and overrides the alreadyHasView() method.


Customview


Fortunately, Anko Layouts is not limited to this functionality. If we need to show our own CustomView, we don’t have to write


 verticalLayout { val view = CustomView(context) //.... addView(view) //  addView(view.apply { ... }) } 

To do this, you can add your own wrapper, which will do the same.


The main component here is the <T: View>ankoView(factory: (Context) -> T, theme: Int, init: T.() -> Unit) method from ViewManager, Context or Activity.



Add our own implementation for our CustomView


 inline fun ViewManager.customView(theme: Int = 0, init: (CustomView).() -> Unit): CustomView { return ankoView({ CustomView(it) }, theme, init) } 

Now our CustomView is created very simply:


 customView { id = R.id.customview //   } 

You can use lparams to apply LayoutParams to View.


 textView("text") { textSize = 12f }.lparams(width = matchParent, height = wrapContent) { centerInParent() } 

It is worth noting that not all View is applicable - all lparams methods are usually declared in wrappers. For example, _RelativeLayout is a wrapper over RelativeLayout . And so for everyone.


Fortunately, several wrappers are written for the Android Support Library, so you can only include dependencies in a gradle file.


  // Appcompat-v7 (Anko Layouts) implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version" implementation "org.jetbrains.anko:anko-coroutines:$anko_version" // CardView-v7 implementation "org.jetbrains.anko:anko-cardview-v7:$anko_version" // Design implementation "org.jetbrains.anko:anko-design:$anko_version" implementation "org.jetbrains.anko:anko-design-coroutines:$anko_version" // GridLayout-v7 implementation "org.jetbrains.anko:anko-gridlayout-v7:$anko_version" // Percent implementation "org.jetbrains.anko:anko-percent:$anko_version" // RecyclerView-v7 implementation "org.jetbrains.anko:anko-recyclerview-v7:$anko_version" implementation "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version" // Support-v4 (Anko Layouts) implementation "org.jetbrains.anko:anko-support-v4:$anko_version" // ConstraintLayout implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version" 

In addition, the library allows more convenient implementation of various listeners. A small example from the repository:


 seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStopTrackingTouch(seekBar: SeekBar) = Unit }) 

and now using anko


 seekBar { onSeekBarChangeListener { onProgressChanged { seekBar, progress, fromUser -> // do something } } } 

Also some listeners support corutines:


  verticalLayout{ val anyCoroutineContext = GlobalScope.coroutineContext onClick(anyCoroutineContext) { //this: CoroutineScope } } 

Anko coroutines


For safe transmission of leak-sensitive objects, the asReference method is asReference . It is based on the WeakReference and returns a Ref.

 verticalLayout{ val activity = ui.owner val activityReference: Ref<AppActivity> = activity.asReference() onClick(anyCoroutineContext) { ref().doSomething() } } 

Suppose you want to add support for corutin to the standard ViewPager.OnPageChangeListener. Let's make it as cool as the seekbar example.
First, we create a separate class and inherit from ViewPager.OnPageChangeListener .


 class CoroutineOnPageChangeListener( private val coroutineContext: CoroutineContext = Dispatchers.Main ) : ViewPager.OnPageChangeListener { } 

In the variables, we will store lambdas that will be called by ViewPager.OnPageChangeListener .


  private var onPageScrollStateChanged: ((Int, CoroutineContext) -> Unit)? = null private var onPageScrolled: ((Int, Float, Int, CoroutineContext) -> Unit)? = null private var onPageSelected: ((Int, CoroutineContext) -> Unit)? = null 

We implement initialization for one of these variables (the rest are done in the same way)


  fun onPageScrollStateChanged(action: ((Int, CoroutineContext) -> Unit)?) { onPageScrollStateChanged = action } 

And at the end we implement the function with the same name.


  override fun onPageScrollStateChanged(state: Int) { GlobalScope.launch(coroutineContext) { onPageScrollStateChanged?.invoke(state, coroutineContext) } } 

It remains to add the extension function to make it all work.


 fun ViewPager.onPageChangeListenerCoroutines(init: CoroutineOnPageChangerListener.() -> Unit) { val listener = CoroutineOnPageChangerListener() listener.init() addOnPageChangeListener(listener) } 

And paste the whole thing under ViewPager


 viewPager { onPageChangeListenerCoroutines { onPageScrolled { position, offset, pixels, coroutineContext -> //  -   . } } } 

Full code here
 class CoroutineOnPageChangeListener( private val coroutineContext: CoroutineContext = Dispatchers.Main ) : ViewPager.OnPageChangeListener { private var onPageScrollStateChanged: ((Int, CoroutineContext) -> Unit)? = null private var onPageScrolled: ((Int, Float, Int, CoroutineContext) -> Unit)? = null private var onPageSelected: ((Int, CoroutineContext) -> Unit)? = null fun onPageScrollStateChanged(action: ((Int, CoroutineContext) -> Unit)?) { onPageScrollStateChanged = action } fun onPageScrolled(action: ((Int, Float, Int, CoroutineContext) -> Unit)?) { onPageScrolled = action } fun onPageSelected(action: ((Int, CoroutineContext) -> Unit)?) { onPageSelected = action } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { GlobalScope.launch(coroutineContext) { onPageScrolled?.invoke(position, positionOffset, positionOffsetPixels, coroutineContext) } } override fun onPageSelected(position: Int) { GlobalScope.launch(coroutineContext) { onPageSelected?.invoke(position, coroutineContext) } } override fun onPageScrollStateChanged(state: Int) { GlobalScope.launch(coroutineContext) { onPageScrollStateChanged?.invoke(state, coroutineContext) } } } fun ViewPager.onPageChangeListenerCoroutines(init: CoroutineOnPageChangerListener.() -> Unit) { val listener = CoroutineOnPageChangerListener() listener.init() addOnPageChangeListener(listener) } 

Also in the Anko Layouts library there are many useful methods, such as translations to various metrics .


')

Source: https://habr.com/ru/post/442440/


All Articles