Implementing Stripe payment in Eventyay Attendee

In Eventyay Attendee, getting tickets for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Stripe?
  • Implementing Stripe Payment in Eventyay Attendee
  • Conclusion
  • Resources

WHY USING STRIPE?

There are many great APIs to be taken into consideration for making payments but we choose Stripe as one of our payment gateways because of simple implementations, detailed documentation, a good number of supported card type and good security support

IMPLEMENTING STRIPE PAYMENT IN EVENTYAY ATTENDEE

Step 1: Setup dependency in the build.gradle

// Stripe
implementation ‘com.stripe:stripe-android:10.3.0’

Step 2: Set up UI to take card information

The information needed for making payments are Card Number, CVC, Expiration Date, which can be made with simple UI (EditText, Spinner,…). Stripe support getting information with CardInputWidget but we made a custom UI for that. Here is the UI we created.

Step 3: Create a card and validate information

Stripe has an object called Card, which takes card number, expiration date and CVC number as parameter to detect the card type and validate the card information with function .validateCard()

PAYMENT_MODE_STRIPE -> {
  card = Card.create(rootView.cardNumber.text.toString(), attendeeViewModel.monthSelectedPosition,
      rootView.year.selectedItem.toString().toInt(), rootView.cvc.text.toString())

  if (!card.validateCard()) {
      rootView.snackbar(getString(R.string.invalid_card_data_message))
      false
  } else {
      true
  }
}

Step 4: Send the token to the server

If card information is valid, we can create a token from the Card and then send it to the server. The token will act as the identifier of the card in order for the server to charge the payment and create tickets for the user. 

private fun sendToken(card: Card) {
  Stripe(requireContext())
      .createToken(card, BuildConfig.STRIPE_API_KEY, object : TokenCallback {
          override fun onSuccess(token: Token) {
              val charge = Charge(attendeeViewModel.getId().toInt(), token.id, null)
              attendeeViewModel.chargeOrder(charge)
          }
          override fun onError(error: Exception) {
              rootView.snackbar(error.localizedMessage.toString())
          }
      })
}

Step 5: So the rest is already handled by the server. Android application will then just receive the response from the server to see if the order is charged successfully or not.

CONCLUSION

With Stripe, user can easily make payments to get tickets for events. Stripe is a great payment gateway as it is really easy to implement in Android. Hopefully, this blog post will help you create a great shopping cart app or any kind of application that requires fast, simple and easy payments.

RESOURCES

Eventyay Attendee Pull Request on Stripe: https://github.com/fossasia/open-event-attendee-android/pull/1863

Documentation from Stripe for Android: https://stripe.com/docs/mobile/android


Continue Reading

Implementing pagination with Retrofit in Eventyay Attendee

Pagination (Paging) is a common and powerful technique in Android Development when making HTTP requests or fetching data from the database. Eventyay Attendee has found many situations where data binding comes in as a great solution for our network calls with Retrofit. Let’s take a look at this technique.

  • Problems without Pagination in Android Development
  • Implementing Pagination with Kotlin with Retrofit
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Making HTTP requests to fetch data from the API is a basic work in any kind of application. With the mobile application, network data usage management is an important factor that affects the loading performance of the app. Without paging, all of the data are fetched even though most of them are not displayed on the screen. Pagination is a technique to load all the data in pages of limited items, which is much more efficient

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1:  Set up dependency in build.gradle

// Paging
implementation “androidx.paging:paging-runtime:$paging_version”
implementation “androidx.paging:paging-rxjava2:$paging_version”

Step 2:  Set up retrofit to fetch events from the API

@GET(“events?include=event-sub-topic,event-topic,event-type”)
fun searchEventsPaged(
  @Query(“sort”) sort: String,
  @Query(“filter”) eventName: String,
  @Query(“page[number]”) page: Int,
  @Query(“page[size]”) pageSize: Int = 5
): Single<List<Event>>

Step 3: Set up the DataSource

DataSource is a base class for loading data in the paging library from Android. In Eventyay, we use PageKeyedDataSource. It will fetch the data based on the number of pages and items per page with our default parameters. With PageKeyedDataSource, three main functions loadInitial(), loadBefore(), loadAfter() are used to to load each chunks of data.

class EventsDataSource(
  private val eventService: EventService,
  private val compositeDisposable: CompositeDisposable,
  private val query: String?,
  private val mutableProgress: MutableLiveData<Boolean>

) : PageKeyedDataSource<Int, Event>() {
  override fun loadInitial(
      params: LoadInitialParams<Int>,
      callback: LoadInitialCallback<Int, Event>
  ) {
      createObservable(1, 2, callback, null)
  }

  override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
      val page = params.key
      createObservable(page, page + 1, null, callback)
  }

  override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
      val page = params.key
      createObservable(page, page – 1, null, callback)
  }

  private fun createObservable(
      requestedPage: Int,
      adjacentPage: Int,
      initialCallback: LoadInitialCallback<Int, Event>?,
      callback: LoadCallback<Int, Event>?
  ) {
      compositeDisposable +=
          eventService.getEventsByLocationPaged(query, requestedPage)
              .withDefaultSchedulers()
              .subscribe({ response ->
                  if (response.isEmpty()) mutableProgress.value = false
                  initialCallback?.onResult(response, null, adjacentPage)
                  callback?.onResult(response, adjacentPage)
              }, { error ->
                  Timber.e(error, “Fail on fetching page of events”)
              }
          )
  }
}

Step 4: Set up the Data Source Factory

DataSourceFactory is the class responsible for creating DataSource object so that we can create PagedList (A type of List used for paging) for events.

class EventsDataSourceFactory(
  private val compositeDisposable: CompositeDisposable,
  private val eventService: EventService,
  private val query: String?,
  private val mutableProgress: MutableLiveData<Boolean>
) : DataSource.Factory<Int, Event>() {
  override fun create(): DataSource<Int, Event> {
      return EventsDataSource(eventService, compositeDisposable, query, mutableProgress)
  }
}

Step 5: Adapt the current change to the ViewModel. 

Previously, events fetched in List<Event> Object are now should be turned into PagedList<Event>.

sourceFactory = EventsDataSourceFactory(
  compositeDisposable,
  eventService,
  mutableSavedLocation.value,
  mutableProgress
)
val eventPagedList = RxPagedListBuilder(sourceFactory, config)
  .setFetchScheduler(Schedulers.io())
  .buildObservable()
  .cache()

compositeDisposable += eventPagedList
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .distinctUntilChanged()
  .doOnSubscribe {
      mutableProgress.value = true
  }.subscribe({
      val currentPagedEvents = mutablePagedEvents.value
      if (currentPagedEvents == null) {
          mutablePagedEvents.value = it
      } else {
          currentPagedEvents.addAll(it)
          mutablePagedEvents.value = currentPagedEvents
      }
  }, {
      Timber.e(it, “Error fetching events”)
      mutableMessage.value = resource.getString(R.string.error_fetching_events_message)
  })

Step 6: Turn ListAdapter into PagedListAdapter

PageListAdapter is basically the same ListAdapter to update the UI of the events item but specifically used for Pagination. In here, List objects can also be null.

class EventsListAdapter : PagedListAdapter<Event, EventViewHolder>(EventsDiffCallback()) {

  var onEventClick: EventClickListener? = null
  var onFavFabClick: FavoriteFabClickListener? = null
  var onHashtagClick: EventHashTagClickListener? = null

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
      val binding = ItemCardEventsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
      return EventViewHolder(binding)
  }

  override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
      val event = getItem(position)
      if (event != null)
          holder.apply {
              bind(event, position)
              eventClickListener = onEventClick
              favFabClickListener = onFavFabClick
              hashTagClickListAdapter = onHashtagClick
          }
  }

  /**
    * The function to call when the adapter has to be cleared of items
    */
  fun clear() {
      this.submitList(null)
  }

AND HERE ARE THE RESULTS…

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data-binding is that sometimes, it is pretty hard to debug with unhelpful log messages. Hopefully, you can empower your UI in your project now with data-binding. 

Pagination is the way to go for fetching items from the API and making infinite scrolling. This helps reduce network usage and improve the performance of Android applications. And that’s it. I hope you can make your application more powerful with pagination. 

RESOURCES

Open Event Codebase: https://github.com/fossasia/open-event-attendee-android/pull/2012

Documentation: https://developer.android.com/topic/libraries/architecture/paging/ 

Google Codelab: https://codelabs.developers.google.com/codelabs/android-paging/#0

Continue Reading

Tax Information on Public Ticket Page

This blog post will elaborate on how Tax Information is being displayed on the public page of an event. In current implementation, the user gets to know the total tax inclusive amount only after he/she decides to place an order but no such information was given to them on the public ticket page itself.

Order summary example in eventyay

Example : In initial implementation, the user gets to know that the order is of only $120 and no information is given about the additional 30% being charged and taking the total to $156.

To tackle this issue, I added two hybrid components to the ticket object to handle the two tax cases : 

  • Inclusion in the price : In European and Asian Countries , the tax amount is included in the ticket price itself. For this case, I created the following parameter to store the tax amount included in gross amount.
// app/models/ticket.js
includedTaxAmount: computed('event.tax.isTaxIncludedInPrice', 'event.tax.rate', function() {
  const taxType = this.event.get('tax.isTaxIncludedInPrice');
  if (taxType) {
    const taxRate = this.event.get('tax.rate');
    return ((taxRate * this.price) / (100 + taxRate)).toFixed(2);
  }
  return 0;
})
  • Added on the ticket price : In basic US tax policy, the tax amount is added on top of the ticket price. For such cases I have added a new attribute to ticket model which calculates the total amount payable for that particular ticket with tax inclusion
// app/models/ticket.js
ticketPriceWithTax: computed('event.tax.isTaxIncludedInPrice', 'event.tax.rate', function() {
  let taxType = this.event.get('tax.isTaxIncludedInPrice');
  if (!taxType) {
    return ((1 + this.event.get('tax.rate') / 100) * this.price).toFixed(2);
  }
  return this.price;
})

Now, the public ticket page has to be edited accordingly. The design I decided to follow is inspired by eventbrite itself : 

Eventbrite specimen of the proposed implementation

For this implementation, I modified the ticket list template to accommodate the changes in the following way : 

// app/components/public/ticket-list.hbs
<
td id="{{ticket.id}}_price">
{{currency-symbol eventCurrency}} {{format-number ticket.price}}
{{#
if (and taxInfo (not-eq ticket.type 'free'))}}
  {{#
if showTaxIncludedMessage}}
    <
small class="ui gray-text small">
      {{t 'includes'}} {{currency-symbol eventCurrency}} {{format-number ticket.includedTaxAmount}}
    </
small>
  {{else}}
    <
small class="ui gray-text small">
      + {{currency-symbol eventCurrency}} {{format-number (sub ticket.ticketPriceWithTax ticket.price)}}
    </
small>
  {{/
if}}
  <
div>
    <
small class="ui gray-text tiny aligned right">({{taxInfo.name}})</small>
  </
div>
{{/
if}}
</
td>
Tax amount is included in ticket price

Hence making the new public ticket list display to look like this in case of tax amount inclusion and additional charge as follows

Tax amount is charged over the base price

Discount Code application cases:

In the cases when a user applies the discount code, the ticket price need to be updated, hence, the tax applied has to be updated accordingly. I achieved this by updating the two computed properties of the ticket model on each togglePromotionalCode and applyPromotionalCode action. When a promotional code is applied, the appropriate attribute is updated according to the discount offered.

// app/components/public/ticket-list.js
tickets.forEach(ticket => {
let ticketPrice = ticket.get('price');
let taxRate = ticket.get('event.tax.rate');
let discount = discountType === 'amount' ? Math.min(ticketPrice, discountValue) : ticketPrice * (discountValue / 100);
ticket.set('discount', discount);
if (taxRate && !this.showTaxIncludedMessage) {
  let ticketPriceWithTax = (ticketPrice - ticket.discount) * (1 + taxRate / 100);
  ticket.set('ticketPriceWithTax', ticketPriceWithTax);
} else if (taxRate && this.showTaxIncludedMessage) {
  let includedTaxAmount = (taxRate * (ticketPrice - discount)) / (100 + taxRate);
  ticket.set('includedTaxAmount', includedTaxAmount);
}
this.discountedTickets.addObject(ticket);

Similarly, on toggling the discount code off, the ticket’s computed properties are set back to their initial value using the same formula kept during the time of initialization which has been achieved in the following manner.

// app/components/public/ticket-list.js
this.discountedTickets.forEach(ticket => {
let taxRate = ticket.get('event.tax.rate');
let ticketPrice = ticket.get('price');
if (taxRate && !this.showTaxIncludedMessage) {
  let ticketPriceWithTax = ticketPrice * (1 + taxRate / 100);
  ticket.set('ticketPriceWithTax', ticketPriceWithTax);
} else if (taxRate && this.showTaxIncludedMessage) {
  let includedTaxAmount = (taxRate * ticketPrice) / (100 + taxRate);
  ticket.set('includedTaxAmount', includedTaxAmount);
}
ticket.set('discount', 0);
});

This particular change makes sure that the tax amount is calculated properly as per the discounted amount and thus eliminates the possibility of overcharging the attendee.

Tax recalculation for discounted tickets

In conclusion, this feature has been implemented keeping in mind the consumer’s interest in using the Open Event Frontend and the ease of tax application on the public level with minimum required network requests.

Resources

Related Work and Code Repository

Continue Reading

Data Binding with Kotlin in Eventyay Attendee

Databinding is a common and powerful technique in Android Development. Eventyay Attendee has found many situations where data binding comes in as a great solution for our complex UI. Let’s take a look at this technique.

  • Problems without data binding in Android Development
  • Implementing Databinding with Kotlin inside Fragment
  • Implementing Databinding with Kotlin inside RecyclerView/Adapter
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Getting the data and fetching it to the UI is a basic work in any kind of application. With Android Development, the most common way to do is it to call function like .setText(), isVisible = True/False,.. in your fragment. This can create many long boilerplate codes inside Android classes. Databinding removes them and moves to the UI classes (XML).

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1: Enabling data binding in the project build.gradle

android {
  dataBinding {
      enabled = true
  }

Step 2: Wrap the current layout with <layout></layout> tag. Inside that, put <data></data> to indicate any variables needed for data binding. For example, this code here display an event variable for our fragment about event details:

<layout xmlns:android=”http://schemas.android.com/apk/res/android”
  xmlns:app=”http://schemas.android.com/apk/res-auto”
  xmlns:bind=”http://schemas.android.com/tools”>

  <data>

      <variable
          name=”event”
          type=”org.fossasia.openevent.general.event.Event” />
  </data>

  <androidx.coordinatorlayout.widget.CoordinatorLayout
      android:id=”@+id/eventCoordinatorLayout”
      android:layout_width=”match_parent”
      android:layout_height=”match_parent”
      android:background=”@android:color/white”>

Step 3: Bind your data in the XML file and create a Binding Adapter class for better usage

With the setup above, you can start binding your data with “@{<data code here>}”

<TextView
  android:id=”@+id/eventName”
  android:layout_width=”0dp”
  android:layout_height=”wrap_content”
  android:layout_marginLeft=”@dimen/layout_margin_large”
  android:layout_marginTop=”@dimen/layout_margin_large”
  android:layout_marginRight=”@dimen/layout_margin_large”
  android:text=”@{event.name}”
  android:fontFamily=”sans-serif-light”
  android:textColor=”@color/dark_grey”
  android:textSize=”@dimen/text_size_extra_large”
  app:layout_constraintEnd_toEndOf=”@+id/eventImage”
  app:layout_constraintStart_toStartOf=”@+id/eventImage”
  app:layout_constraintTop_toBottomOf=”@+id/eventImage”
  tools:text=”Open Source Meetup” />

Sometimes, to bind our data normally we need to use a complex function, then creating Binding Adapter class really helps. For example, Eventyay Attendee heavily uses Picasso function to fetch image to ImageView:

@BindingAdapter(“eventImage”)
fun setEventImage(imageView: ImageView, url: String?) {
  Picasso.get()
      .load(url)
      .placeholder(R.drawable.header)
      .into(imageView)
}
<ImageView
  android:id=”@+id/eventImage”
  android:layout_width=”@dimen/layout_margin_none”
  android:layout_height=”@dimen/layout_margin_none”
  android:scaleType=”centerCrop”
  android:transitionName=”eventDetailImage”
  app:layout_constraintDimensionRatio=”2″
  app:layout_constraintEnd_toEndOf=”parent”
  app:layout_constraintHorizontal_bias=”0.5″
  app:layout_constraintStart_toStartOf=”parent”
  app:eventImage=”@{event.originalImageUrl}”
  app:layout_constraintTop_toBottomOf=”@id/alreadyRegisteredLayout” />

Step 4: Finalize data binding setup in Android classes. We can create a binding variable. The binding root will serve as the root node of the layout. Whenever data is needed to be bind, set the data variable stated to that binding variable and call function executePendingBingdings()

private lateinit var rootView: View
private lateinit var binding: FragmentEventBinding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_event, container, false)
rootView = binding.root
binding.event = event
binding.executePendingBindings()

SOME NOTES

  • In the example mentioned above, the name of the binding variable class is auto-generated based on the name of XML file + “Binding”. For example, the XML name was fragment_event so the DataBinding classes generated name is FragmentEventBinding.
  • The data binding class is only generated only after compiling the project.
  • Sometimes, compiling the project fails because of some problems due to data binding without any clear log messages, then that’s probably because of error when binding your data in XML class. For example, we encounter a problem when changing the value in Attendee data class from firstname to firstName but XML doesn’t follow the update. So make sure you bind your data correctly
<TextView
  android:id=”@+id/name”
  android:layout_width=”wrap_content”
  android:layout_height=”wrap_content”
  android:layout_marginBottom=”@dimen/layout_margin_large”
  android:textColor=”@color/black”
  android:textSize=”@dimen/text_size_expanded_title_large”
  android:text=”@{attendee.firstname + ` ` + attendee.lastname}”
  tools:text=”@string/name_preview” />

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data binding is that sometimes, it is pretty hard to debug with an unhelpful log message. Hopefully, you can empower your UI in your project now with data binding.  

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee Android PR: #1961 – feat: Set up data binding for Recycler/Adapter

Documentation: https://developer.android.com/topic/libraries/data-binding

Google Codelab: https://codelabs.developers.google.com/codelabs/android-databinding/#0

Continue Reading

Implementing Attendee Forms in Wizard of Open Event Frontend

This blog post illustrates on how the order form is included in the attendee information of the Open Event Frontend form  and enabling the organizer to choosing what information to collect from the attendee apart from the mandatory data i.e. First Name, Last Name and the Email Id during the creation of event itself.

The addition of this feature required alteration in the existing wizard flow to accommodate this extra step. This new wizard flow contains the step :

  • Basic Details : Where organizer fills the basic details regarding the event.
  • Attendee Form : In this step, the organizer can choose what information he/she has to collect from the ticket buyers.
  • Sponsors : This step enables the organizer to fill in the sponsor details
  • Session and Speakers : As the name suggests, this final step enables the organizer to fill in session details to be undertaken during the event.

This essentially condensed the flow to this :

The updated wizard checklist

To implement this, the navigation needed to be altered first in the way that Forward and Previous buttons comply to the status bar steps

// app/controller/create.jsmove() {
    this.saveEventDataAndRedirectTo(
      'events.view.edit.attendee',
      ['tickets', 'socialLinks', 'copyright', 'tax', 'stripeAuthorization']
    );
  }
//app/controller/events/view/edit/sponsorship
move(direction) {
    this.saveEventDataAndRedirectTo(
      direction === 'forwards' ? 'events.view.edit.sessions-speakers' : 'events.view.edit.attendee',
      ['sponsors']
    );
  }

Once the navigation was done, I decided to add the step in the progress bar by simply including the attendees form in the event mixin.

// app/mixins/event-wizard.js
    {
      title     : this.l10n.t('Attendee Form'),
      description : this.l10n.t('Know your audience'),
      icon     : 'list icon',
      route     : 'events.view.edit.attendee'
    }

Now a basic layout for the wizard is prepared, all what is left is setting up the route for this step and including it in the router file. I took my inspiration for setting up the route from events/view/tickets/order-from.js and implemented it like this:

// app/routes/events/view/edit/attendee.js
import Route from '@ember/routing/route';
import CustomFormMixin from 'open-event-frontend/mixins/event-wizard';
import { A } from '@ember/array';
export default Route.extend(CustomFormMixin, {

titleToken() {
  return this.l10n.t('Attendee Form');
},

async model() {
  let filterOptions = [{
    name : 'form',
    op : 'eq',
    val : 'attendee'
  }];

  let data = {
    event: this.modelFor('events.view')
  };
  data.customForms = await data.event.query('customForms', {
    filter       : filterOptions,
    sort         : 'id',
    'page[size]' : 50
  });

  return data;
},
afterModel(data) {
  /**
    * Create the additional custom forms if only the compulsory forms exist.
    */
  if (data.customForms.length === 3) {
    let customForms = A();
    for (const customForm of data.customForms ? data.customForms.toArray() : []) {
      customForms.pushObject(customForm);
    }

    const createdCustomForms = this.getCustomAttendeeForm(data.event);

    for (const customForm of createdCustomForms ? createdCustomForms : []) {
      customForms.pushObject(customForm);
    }

    data.customForms = customForms;
  }
}
});

With the route setup and included in the router, I just need to take care of the form data and pass it to the server. Thankfully, the project was already using EventWizardMixin so all I had to do was utilize these functions (save and move) which saves the event data in the status user decides to save it in i.e. either published or draft state

// app/controllers/events/view/edit/attendee.js
import Controller from '@ember/controller';
import EventWizardMixin from 'open-event-frontend/mixins/event-wizard';

export default Controller.extend(EventWizardMixin, {
async saveForms(data) {
  for (const customForm of data.customForms ? data.customForms.toArray() : []) {
    await customForm.save();
  }
  return data;
},
actions: {
  async save(data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        'events.view.index',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  },
  async move(direction, data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        direction === 'forwards' ? 'events.view.edit.sponsors' : 'events.view.edit.basic-details',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  }
}
});

Apart from that, the form design was already there, essentially, I reutilized the form design provided to an event organizer / co-organizer in the ticket section of the event dashboard to make it look like this form :

Basic attendee information collection

In the end, after utilizing the existing template and adding it in the route’s template, the implementation is ready for a test run!

// app/templates/events/view/edit/attendee.hbs
{{forms/wizard/attendee-step data=model move='move' save='save' isLoading=isLoading}}

This is a simple test run of how the attendees form step works as others work fine along with it!

Demonstration of new event submission workflow

Resources

Related Work and Code Repository

Continue Reading

Dependency Injection with Kotlin Koin in Eventyay Attendee

Eventyay Attendee Android app contains a lot of shared components between classes that should be reused. Dependency Injection with Koin really comes in as a great problem solver.

Dependency Injection is a common design pattern used in various projects, especially with Android Development. In short, dependency injection helps to create/provide instances to the dependent class, and share it among other classes.

  • Why using Koin?
  • Process of setting up Koin in the application
  • Results
  • Conclusion
  • Resources

Let’s get into the details

WHY USING KOIN?

Before Koin, dependency injection in Android Development was mainly used with other support libraries like Dagger or Guice. Koin is a lightweight alternative that was developed for Kotlin developers. Here are some of the major things that Koin can do for your project:

  • Modularizing your project by declaring modules
  • Injecting class instances into Android classes
  • Injecting class instance by the constructor
  • Supporting with Android Architecture Component and Kotlin
  • Testing easily

SETTING UP KOIN IN THE ANDROID APPLICATION

Adding the dependencies to build.gradle

// Koin
implementation “org.koin:koin-android:$koin_version”
implementation “org.koin:koin-androidx-scope:$koin_version”
implementation “org.koin:koin-androidx-viewmodel:$koin_version”

Create a folder to manage all the dependent classes.

Inside this Modules class, we define modules and create “dependency” class instances/singletons that can be reused or injected. For Eventyay Attendee, we define 5 modules: commonModule, apiModule, viewModelModule, networkModule, databaseModule. This saves a lot of time as we can make changes like adding/removing/editing the dependency in one place.

Let’s take a look at what is inside some of the modules:

DatabaseModule

val databaseModule = module {

  single {
      Room.databaseBuilder(androidApplication(),
          OpenEventDatabase::class.java, “open_event_database”)
          .fallbackToDestructiveMigration()
          .build()
  }

  factory {
      val database: OpenEventDatabase = get()
      database.eventDao()
  }

  factory {
      val database: OpenEventDatabase = get()
      database.sessionDao()
  }

CommonModule

val commonModule = module {
  single { Preference() }
  single { Network() }
  single { Resource() }
  factory { MutableConnectionLiveData() }
  factory<LocationService> { LocationServiceImpl(androidContext()) }
}

ApiModule

val apiModule = module {
  single {
      val retrofit: Retrofit = get()
      retrofit.create(EventApi::class.java)
  }
  single {
      val retrofit: Retrofit = get()
      retrofit.create(AuthApi::class.java)
  }

NetworkModule

single {
  val connectTimeout = 15 // 15s
  val readTimeout = 15 // 15s

  val builder = OkHttpClient().newBuilder()
      .connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
      .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
      .addInterceptor(HostSelectionInterceptor(get()))
      .addInterceptor(RequestAuthenticator(get()))
      .addNetworkInterceptor(StethoInterceptor())

  if (BuildConfig.DEBUG) {
      val httpLoggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
      builder.addInterceptor(httpLoggingInterceptor)
  }
  builder.build()
}

single {
  val baseUrl = BuildConfig.DEFAULT_BASE_URL
  val objectMapper: ObjectMapper = get()
  val onlineApiResourceConverter = ResourceConverter(
      objectMapper, Event::class.java, User::class.java,
      SignUp::class.java, Ticket::class.java, SocialLink::class.java, EventId::class.java,
      EventTopic::class.java, Attendee::class.java, TicketId::class.java, Order::class.java,
      AttendeeId::class.java, Charge::class.java, Paypal::class.java, ConfirmOrder::class.java,
      CustomForm::class.java, EventLocation::class.java, EventType::class.java,
      EventSubTopic::class.java, Feedback::class.java, Speaker::class.java, FavoriteEvent::class.java,
      Session::class.java, SessionType::class.java, MicroLocation::class.java, SpeakersCall::class.java,
      Sponsor::class.java, EventFAQ::class.java, Notification::class.java, Track::class.java,
      DiscountCode::class.java, Settings::class.java, Proposal::class.java)

  Retrofit.Builder()
      .client(get())
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .addConverterFactory(JSONAPIConverterFactory(onlineApiResourceConverter))
      .addConverterFactory(JacksonConverterFactory.create(objectMapper))
      .baseUrl(baseUrl)
      .build()
}

As described in the code, Koin support single for creating a singleton object, factory for creating a new instance every time an object is injected.

With all the modules created, it is really simple to get Koin running in the project with the function startKoin() and a few lines of code. We use it inside the application class:

startKoin {
  androidLogger()
  androidContext([email protected])
  modules(listOf(
      commonModule,
      apiModule,
      viewModelModule,
      networkModule,
      databaseModule
  ))
}

Injecting created instances defined in the modules can be used in two way, directly inside a constructor or injecting into Android classes.  

Here is an example of dependency injection to the constructor that we used for a ViewModel class and injecting that ViewModel class into the Fragment:

class EventsViewModel(
  private val eventService: EventService,
  private val preference: Preference,
  private val resource: Resource,
  private val mutableConnectionLiveData: MutableConnectionLiveData,
  private val config: PagedList.Config,
  private val authHolder: AuthHolder
) : ViewModel() {
class EventsFragment : Fragment(), BottomIconDoubleClick {
  private val eventsViewModel by viewModel<EventsViewModel>()
  private val startupViewModel by viewModel<StartupViewModel>()

For testing, it is also really easy with support library from Koin.

@Test
fun testDependencies() {
  koinApplication {
      androidContext(mock(Application::class.java))
      modules(listOf(commonModule, apiModule, databaseModule, networkModule, viewModelModule))
  }.checkModules()
}

CONCLUSION

Koin is really easy to use and integrate into Kotlin Android project. Apart from some of the basic functionalities mention above, Koin also supports other helpful features like Scoping or Logging with well-written documentation and examples. Even though it is only developed a short time ago, Koin has proved to be a great use in the Android community. So the more complicated your project is, the more likely it is that dependency injection with Koin will be a good idea.

RESOURCES 

Documentation: https://insert-koin.io/

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Continue Reading

Omise Integration in Open Event Frontend

This blog post will elaborate on how omise has been integrated into the Open Event Frontend project. Omise is Thailand’s leading online payment gateway offering a wide range of processing solutions for this project and integrating it as a payment option widens the possibilities for user base and ease of payment workflow.

Similar to Paypal, Omise offers two alternatives for using their gateway, Test mode and Live mode, where the former is generally favoured for usage in Development and Testing phase while the latter is used in actual production for capturing live payments. Both these modes require a Public key and Secret key each and are only update-able on the admin route.

This was implemented by introducing appropriate fields in the settings model.

// app/models/setting.js
omiseMode            : attr('string'),
omiseTestPublic      : attr('string'),
omiseTestSecret      : attr('string'),
omiseLivePublic      : attr('string'),
omiseLiveSecret      : attr('string')

Once your Omise credentials are configured, you can go ahead and include the options in your event creation form. You will see an option to include Omise in your payment options if you have configured your keys correctly and if the gateway supports the currency your event is dealing with, for example, even if your keys are correctly configured, you will not get the option to use omise gateway for money collection if the currency is INR.

For showing omise option in the template, a simple computed property did the trick canAcceptOmise  in the form’s component file and the template as follows:

// app/components/forms/wizard/basic-details-step.js
canAcceptOmise: computed('data.event.paymentCurrency', 'settings.isOmiseActivated', function() {
  return this.get('settings.isOmiseActivated') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).omise;
})
// app/templates/components/forms/wizard/basic-details-step.js
{{#
if canAcceptOmise}}
      <
label>{{t 'Payment with Omise'}}</label>
      <
div class="field payments">
        <
div class="ui checkbox">
          {{input type='checkbox' id='payment_by_omise' checked=data.event.canPayByOmise}}
          <
label for="payment_by_omise">
            {{t 'Yes, accept payment through Omise Gateway'}}
            <
div class="ui hidden divider"></div>
            <
span class="text muted">
              {{t 'Omise can accept Credit and Debit Cards , Net-Banking and AliPay. Find more details '}}
              <
a href="https://www.omise.co/payment-methods" target="_blank" rel="noopener noreferrer">{{t 'here'}}</a>.
            </
span>
          </
label>
        </
div>
      </
div>
      {{#
if data.event.canPayByOmise}}
        <
label>{{t 'Omise Gateway has been successfully activated'}}</label>
      {{/
if}}
    {{/
if}}

Once the event has the payment option enabled, an attendee has chosen the option to pay up using omise, they will encounter this screen on their pending order page 

On entering the credentials correctly, they will be forwarded to order completion page. On clicking the “Pay” button, the omise cdn used hits the server with a POST request to the order endpoint  and is implemented as follows :

//controllers/orders/pending.js
isOmise: computed('model.order.paymentMode', function() {
  return this.get('model.order.paymentMode') === 'omise';
}),

publicKeyOmise: computed('settings.omiseLivePublic', 'settings.omiseLivePublic', function() {
  return this.get('settings.omiseLivePublic') || this.get('settings.omiseTestPublic');
}),

omiseFormAction: computed('model.order.identifier', function() {
  let identifier = this.get('model.order.identifier');
  return `${ENV.APP.apiHost}/v1/orders/${identifier}/omise-checkout`;
})
// app/templates/orders/pending.hbs
    {{#
if isOmise}}
      <
div>
        <
form class="checkout-form" name="checkoutForm" method='POST' action={{omiseFormAction}}>
          <
script type="text/javascript" src="https://cdn.omise.co/omise.js"
                  data-key="{{publicKeyOmise}}"
                  data-amount="{{paymentAmount}}"
                  data-currency="{{model.order.event.paymentCurrency}}"
                  data-default-payment-method="credit_card">
          </
script>
        </
form>
      </
div>
    {{/
if}}

Thus primarily using Omise.js CDN and introducing the omise workflow, the project now has accessibility to Omise payment gateway service and the organiser can see his successful charge.

Resources

Related Work and Code Repository

Continue Reading

Implementing a Splash Screen, the wiser way

Implementing a Splash Screen, the wiser way

What is a Splash Screen?

A Splash Screen is basically a nice intro-screen that mobile applications have on startup of the app on a device. The splash screen can be customized according to the app’s UX need-animations, sound effects, etc. are some common tweaks to a simple splash screen.

I have been working with FOSSASIA on the Neurolab Android App where we made a splash screen for the same. Our implemented splash screen is below:

                                                     Neurolab Splash Screen

While developing this, we followed Google Material Design guidelines and the pattern it suggests is termed as ‘Launch Screen’. Displaying a launch screen can decrease the sense of long load time, and has the potential to add delight to the user experience. Launch screen implementation is considered as one of the best-practised development skills for a proper splash screen for an app.

Implementation 

Now, it is not a good idea to use a splash screen that wastes a user’s time. This should be strictly avoided. The right way of implementing a splash screen is a little different. In the new approach specify your splash screen’s background as the activity’s theme background. This way, we can effectively and efficiently use the time gap between the startup of the app and the onCreate() method.

In the Neurolab app, we use the splash screen as a bridge for the time gap between the app startup when we click the app icon and the onCreate method of the Neurolab Activity (Main/Launcher Screen) of the app, wherein the various UI components are laid out on the screen and the functionalities, navigations, listeners are linked to those components.

So, here we won’t be creating a new layout for the Splash screen as a separate activity. Rather we would specify the theme of the landing activity as the splash screen.

We create a drawable named splash_screen.xml in our project and give a parent tag of layer-list. Here is the code for our drawable file:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="@android:color/white" />
    <item>
        <bitmap
            android:gravity="center_horizontal"
            android:src="@drawable/splash_image" />
    </item>
</layer-list>

Next, we are going to create a new theme in the styles resource file. This theme is going to be used as the base theme for the main activity screen of the app. In this style, we specify our created drawable file to the property name windowBackground.

<style name="AppTheme.Launcher">
        <item name="android:windowBackground">@drawable/splash_screen</item>
</style>

Then, update this style in the project manifest file to set the theme of the main activity

android:theme="@style/AppTheme.Launcher"

Having done the steps so far, we create a simple class extending the AppCompatActivity. Note- This may seem like another Activity screen, but it is not. We don’t specify the setContentView() here. Instead of this class just directs to the main/home activity using an Intent. Finally, be sure to finish() the SplashActivity activity (class) to remove prevailing unused/idle activities from back stack.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Start landing activity screen
startActivity(new Intent(SplashActivity.this, MainActivity.class));
finish();
}

We are done!!

Launch your app, and observe your Launch screen. I can promise you that your “time won’t be wasted”(pun intended).

Thanks for reading. Hope this adds value to your Android application development skills. 

References:

Tags: FOSSASIA. Neurolab, GSOC19, Open-source, splash-screen, Android

Continue Reading

Adding different metrics sections to the start page

In the initial version of the SUSI.AI Skill CMS we simply displayed all the skills present in the system in the form of cards. Once the skill analytics was incorporated into the CMS we got a bunch of skill statistics and thus we enhanced the start page by incorporating horizontally scrollable skill cards as per skill metrics like top rated skills, most used skills, skills which have received the most feedback etc. I worked on adding the skills with most feedback section and the section for the top games. This post will majorly deal with how the metrics sections are implemented on the start page and how any new metrics can be incorporated into the system and thus displayed on the CMS.

About the API

/cms/getSkillMetricsData.json?language=${language}

Sample API call:

https://api.susi.ai/cms/getSkillMetricsData.json?language=en

 

This will return a JSON which contains the skill data for all the metrics.

{
 "accepted": true,
 "model": "general",
 "group": "All",
 "language": "en",
 "metrics": {
        "newest": [...],
     "rating": [...],
      ...
 }
 "message": "Success: Fetched skill data based on metrics",
   "session": {"identity": {
           "type": "host",
          "name": "162.158.23.7_68cefd16",
          "anonymous": true
   }}
}

 

All of the data for several metics comes from the metrics object of the response which in turn contains arrays of skill data for each metric.

CMS Implementation

Once the BrowseSkill component is mounted we make an API call to the server to fetch all the data and save it to the component state, this data is then fed to the ScrollCardList component as props and the scroll component is rendered with appropriate data for different metrics.

loadMetricsSkills = () => {
   let url;
   url =
           urls.API_URL +
           '/cms/getSkillMetricsData.json?language=' +
           this.state.languageValue;
   let self = this;
   $.ajax({
           url: url,
           dataType: 'jsonp',
           jsonp: 'callback',
           crossDomain: true,
           success: function(data) {
                   self.setState({
                           skillsLoaded: true,
                           staffPicksSkills: data.metrics.staffPicks,
                           topRatedSkills: data.metrics.rating,
                           topUsedSkills: data.metrics.usage,
                           latestUpdatedSkills: data.metrics.latest,
                           newestSkills: data.metrics.newest,
                           topFeedbackSkills: data.metrics.feedback,
                           topGames: data.metrics['Games, Trivia and Accessories'],
                   });
           },
           error: function(e) {
                   console.log('Error while fetching skills based on top metrics', e);
                   return self.loadMetricsSkills();
           },
   });
};

 

We are using a single component for skill metrics and skill listing which show up on applying any filter or visiting any category. Thus we think of a condition when the skill metrics are to be displayed and conditionally render the metrics section depending on the condition.

So the metrics section shows up only when we have not visited any category or language page, there’s no search query in the search bar, there’s no rating refine filter applied and no time filter applied.

let metricsHidden =
         this.props.routeType ||
         this.state.searchQuery.length > 0 ||
         this.state.ratingRefine ||
         this.state.timeFilter;

 

Depending on the section you want to display, pass appropriate data as props to the SkillCardScrollList component, say we want to display the section with most feedback

{this.state.topFeedbackSkills.length &&
!metricsHidden ? (
   <div style={metricsContainerStyle}>
           <div
                   style={styles.metricsHeader}
                   className="metrics-header"
           >
                   <h4>
                           {'"SUSI, what are the skills with most feedback?"'}
                   </h4>
           </div>
           {/* Scroll Id must be unique for all instances of SkillCardList*/}
           {!this.props.routeType && (
                   <SkillCardScrollList
                           scrollId="topFeedback"
                           skills={this.state.topFeedbackSkills}
                           modelValue={this.state.modelValue}
                           languageValue={this.state.languageValue}
                           skillUrl={this.state.skillUrl}
                   />
           )}
   </div>
) : null}

 

So if there are skills preset in the topFeedbackSkills array which was saved in the state from the server initially and the condition to hide metrics is false we render the component and pass appropriate props for scrollId, skills data, language and model values and skill url.

In a similar way any metrics section can be implemented in the CMS, if the data is not present in the API, modify the endpoint to enclose the data you need, fetch data data from the server and just render it.

So I hope after reading through this you have a more clearer understanding about how the metrics sections are implemented on the CMS.

Resources

Continue Reading

Working with Route Hooks in Badgeyay

Badgeyay is an open source project developed by FOSSASIA Community to generate badges for conferences and events through a simple user interface.

In this post I am going to explain about the route lifecycle hooks in ember and how we have utilized one lifecycle component to reset the controller state in badge generation form. In Ember every entity has some predefined set of methods, that it goes through the existence of the application. Route is not different from it. Our main goal is to restore the state of the controller every time we entered into a route, so that we receive a clean and new instance and not the previous state. The hook that fits in the situation is setupController(). This method is called after model() hook to set the controller state in the route. We will restore the variables in the controller here in this method to reset it to original state. This will help in removing the messages and progress on the previous state of the page on a new visit.

Procedure

  1. Open the route, where we want to override the hook, and create a method setupController() this will call the base hook and override its behaviour.
setupController(controller, model) {
  this._super(…arguments);
  set(controller, ‘defImages’, model.def_images);
  set(controller, ‘user’, model.user);
  this.set(‘controller.badgeGenerated’, false);
  this.set(‘controller.showProgress’, false);
}

 

As we can see in the method, it first initialises the super constructor and then we are writing the logic for the reset state. This will reset the badgeGenerated and showProgress variable in the controller to mentioned values.

  1. Getting the generated badge link from the last promise resolved to set the value of the variable in the controller action.
sendBadge(badgeData) {

        this.set(‘badgeGenerated’, true);

  },

 

This will set the value of the variable to the received object from backend.

  1. Showing the content in frontend based on the values of the variable. When we initially visit the route it is set to as false in the setupController() hook and is changed later on through some promise resolvement in an action.
{{#if badgeGenerated}}

 . . .


{{/if}}

 

This content will be only be shown in the present state and when the user revisits the route the state will be resetted.

Pull Request for the respective issue – https://github.com/fossasia/badgeyay/pull/1313

Resources

Continue Reading
Close Menu